ほぼ1年ぶりにニコニコサムネイルを更新しました。

ニコニコサムネイル 

前回の記事 → 拡張機能:ニコニコサムネイル

前回の記事の1か月後にも更新していたのですが、
そのことについての記事を書いていなかったので先にそちらを書いて、
後編で今回の更新について書きます。 

まだ Manifest Version 1 の時のものなので現在は使えない情報もあります。
Manifest Version については後編で。


 Ver. 1.1.0

Ver. 1.1.0 の更新内容は以下のとおり。
・ ニコニコ関連のサイト全体で動作するように。
・ オプションの実装。
・ タグなどのIDの前後に文字列が入ったリンクでも動作するように。
・ 他のサムネイルを表示するかマウスクリックするまでサムネイルを表示し続けられるように。
今回の manifest.json はこんな感じ
{
  "name": "ニコニコサムネイル",
  "version": "1.1.0",
  "description": "ニコニコ動画内のリンクにマウスオーバーでサムネイルを表示します。",
  "icons": {
    "48": "icon_small.png",
	"128": "icon_large.png"
  },
  "options_page": "options.html",
  "background_page": "background.html",
  "content_scripts": [
    {
      "matches": ["http://*.nicovideo.jp/*"],
      "js": ["nico_thumbnail.js"]
    }
  ]
}
変更点は "version" と "content_scripts" の "matches" で、
追加は "options_page" と "background_page" です。

"version" はそのままバージョン番号です。
ここのバージョン番号がすでにアップロードされているものより大きくないとアップロードできません。

"matches" は更新内容の一つ目「ニコニコ関連のサイト全体で動作するように」のための変更です。
「www」 の部分を 「*」 に変更したことで seiga.nicovideo.jp などでも動作するようにしています。

"options_page" と "background_page" はオプションの実装のための追加です。
それぞれ使用するファイルの名前を指定します。
background.html は background.js を読み込んでいるだけです。

オプションの実装
オプションの動作について適当に図を書いてみました。
実際は少し違いますが、イメージとしてはこんな感じです。

nico

① background.js でオプション設定の初期化を行います。
  localstorage は HTML5 の Web Storage という機能の一部で、
  ブラウザを閉じても永続的にデータを保持します。
  キーを指定して文字列でデータを保存することができます。
② オプションを開くと、オプションを読み込んでチェックボックスを設定します。
  オプション画面はこんな感じ。
option
③ チェックボックスでオプショを設定することができます。
④ 保存ボタンを押すとチェックボックスの内容を localstorage に保存します。
⑤ manifest.json で指定したページにアクセスすると、
⑥ nico_thumbnail.js が読み込まれ、 background.js にメッセージを送ります。
⑦ メッセージを受け取った background.js はオプション情報を読み込み、
⑧ nico_thumbnail.js に渡します。

ソースコード
では、実際のコードを見ていきます。

background.html
<html>
<head>
<title>ニコニコサムネイル</title>
<script type="text/javascript" src="background.js"></script>
</head>
<body onload="initialize()"></body>
</html>
まぁ、そのまんまです。

background.js
chrome.self.onConnect.addListener(function(port, name){
    // メッセージを受け取ったらオプションの情報を返す
    port.onMessage.addListener(function(info, con){
        con.postMessage({
            watch: getFlag("watch"), mylist: getFlag("mylist"), community: getFlag("community"),
            seiga: getFlag("seiga"), removeClick: getFlag("removeClick")
        });
    });
});
// 選択しているタブが変わったらメッセージを要求
chrome.tabs.onSelectionChanged.addListener(function(tab){
    chrome.tabs.sendRequest(tab, {tb: ""}, function(response){});
});

function getFlag(name) {
    return localStorage[name] == "true";
}

function initialize() {
    if (localStorage.length == 0) {
        localStorage["watch"] = "true";
        localStorage["mylist"] = "true";
        localStorage["community"] = "true";
        localStorage["seiga"] = "true";
        localStorage["removeClick"] = "false";
    }
}
一番上はメッセージのやり取りのための接続が確立した時に、
メッセージを受け取った時に、
オプション情報をメッセージで返す、
という処理をイベントリスナを登録する、
という処理をイベントリスナに登録しています。
要はオプションを教えるための処理です。

次にタブを切り替えた時にメッセージを要求する、というのがありますが別にこれ要りません。
次のバージョンではこの部分は削除しています。

getFlag 関数は指定した名前の設定を bool 値で返します。
localstorage には文字列が保存されているので "true" という文字列と比較して bool 値を作っています。

initialize 関数ではこの拡張機能を入れたときは localstorage に何も入っていないので初期化します。


次にオプション画面。

options.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <title>ニコニコサムネイル オプション</title>
</head>
<script type="text/javascript" src="options.js"></script>

<body onload="restore_options()">

<p>
動作するリンクの選択<br />
<input type="checkbox" id="watch" />動画 (sm0000 or nm0000 or watch/0000)<br />
<input type="checkbox" id="mylist" />マイリスト (mylist/0000)<br />
<input type="checkbox" id="community" />コミュニティ (co0000)<br />
<input type="checkbox" id="seiga" />静画 (im0000)<br />
</p>
<p>
動作設定<br />
<input type="checkbox" id="removeClick" />他のサムネイルを表示するかマウスクリックするまでサムネイルを表示し続ける<br />
</p>
<br />
<button onclick="save_options()">保存</button>
<br />
<div id="status" />
</body>
</html>
なんだか最初の方とか変な感じですが気にしないでください。
ただチェックボックスを並べているだけです。

options.js
// localstorage にオプションを保存。
function save_options() {
    elements = document.getElementsByTagName("input");
    for (i = 0; i < elements.length; i++) {
        if (elements[i].type == "checkbox") {
            localStorage[elements[i].id] = elements[i].checked;
        }
    }

    // 保存完了メッセージを表示。
    var status = document.getElementById("status");
    status.innerHTML = "保存しました。";
    setTimeout(function () {
        status.innerHTML = "";
    }, 1750);
}

// localstorage のデータを復元。
function restore_options() {
    elements = document.getElementsByTagName("input");
    for (i = 0; i < elements.length; i++) {
        if (elements[i].type == "checkbox") {
            elements[i].checked = localStorage[elements[i].id] == "true";
        }
    }
}
オプションを読み込み、 getElement なんちゃらでチェックボックス操作したり、
設定した結果を保存したりしています。
ちなみにこのファイル次のバージョンで結構変わります。


そして最後に本体。

nico_thumbnail.js
function NicoThumbnail() {
    // URLの文字列定義
    var iframeStart = '<iframe width="312" height="176" src="';
    var iframeEnd = '" scrolling="no" style="border:solid 1px #CCC;" frameborder="0" />';
    var watchThumbnailUrl = iframeStart + 'http://ext.nicovideo.jp/thumb/';
    var mylistThumbnailUrl = iframeStart + 'http://ext.nicovideo.jp/thumb_';
    var communityThumbnailUrl = iframeStart + 'http://ext.nicovideo.jp/thumb_community/';
    var seigaThumbnailUrl = iframeStart + 'http://ext.seiga.nicovideo.jp/thumb/';
    
    var thumbnail;
    var option_valid = [];
    var removeClick;
    var isShowed = false;
	
    var connection = chrome.extension.connect();
    connection.postMessage();
    connection.onMessage.addListener(function(info) {
        option_valid["watch"] = info.watch;
        option_valid["mylist"] = info.mylist;
        option_valid["community"] = info.community;
        option_valid["seiga"] = info.seiga;
        removeClick = info.removeClick;
    });

    chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
        connection.postMessage();
    });

    // リンクにイベントリスナを追加
    this.OnLoadHandler = function (e) {
        elements = document.getElementsByTagName("a");
        for (i = 0; i < elements.length; i++) {
            var text = '';
            if (elements[i].firstChild != null) {
                elements[i].addEventListener("mouseover", obj.OnMouseOverHandler, false);
                if (!removeClick) {
                    elements[i].addEventListener("mouseout", obj.OnMouseOutHandler, false);
                }
            }
        }
        connection.postMessage();
    }

    // マウスオーバー時サムネイル作成
    this.OnMouseOverHandler = function (e) {
        createThumbnail(e.target, e.pageX + 5, e.pageY + 5);
    }

    // マウスアウト時サムネイル消去(設定による)
    this.OnMouseOutHandler = function (e) {
        if (!removeClick) {
            removeThumbnail();
        }
    }

    // マウスアップ時サムネイル消去(設定による)
    this.OnMouseUpHandler = function (e) {
        if (removeClick) {
            removeThumbnail();
        }
    }
	
    // サムネイル消去
    function removeThumbnail() {
        if (typeof thumbnail != "undefined" && isShowed) {
            thumbnail.style.display = "none";
            document.body.removeChild(thumbnail);
            isShowed = false;
        }
    }

    // サムネイルの作成
    function createThumbnail(link, x, y) {
        // リンクの文字列からURLを作成
        if (link.firstChild != null) {
            var thumbnailUrl = '';
            var id = link.innerText;
            var matchResult;
            if (id == null) return;
            if (option_valid["watch"] && (matchResult = id.match(/([sn]m[0-9]+)/))) {
                thumbnailUrl = watchThumbnailUrl;
                id = matchResult[1];
            }
            else if (option_valid["mylist"] && id.match(/^mylist\/[0-9]+$/)) {
                thumbnailUrl = mylistThumbnailUrl;
            }
            else if (option_valid["watch"] && (matchResult = id.match(/^watch\/([0-9]+)$/))) {
                thumbnailUrl = watchThumbnailUrl;
                id = matchResult[1];
            }
            else if (option_valid["community"] && id.match(/^co[0-9]+$/)) {
                thumbnailUrl = communityThumbnailUrl;
            }
            else if (option_valid["seiga"] && id.match(/^im[0-9]+$/)) {
                thumbnailUrl = seigaThumbnailUrl;
            }
            else {
                return;
            }
            removeThumbnail();
            // サムネイルのスタイル設定
            thumbnail = document.createElement("div");
            thumbnail.style.position = "absolute";
            thumbnail.style.zIndex = 2147483647;
            thumbnail.style.left = x + "px";
            thumbnail.style.top = y + "px";

            thumbnail.innerHTML = thumbnailUrl + id + iframeEnd;
            document.body.appendChild(thumbnail);
            isShowed = true;
        }
    }
}

var obj = new NicoThumbnail();
window.addEventListener("load", obj.OnLoadHandler, false);
window.addEventListener("mouseup", obj.OnMouseUpHandler, false);
前回から増えたのは接続部分と、マウスアップ時のイベントハンドラが増えたことぐらいだと思います。
あと地味に正規表現パターンが変わっています。

接続部分は、chrome 拡張の機能で接続、その後すぐにメッセージを送って、
メッセージを受け取ったらオプション情報を保存します。

マウスアップイベントは更新内容の4つ目「他のサムネイルを表示するかマウスクリックするまでサムネイルを表示し続けられるように」のために、マウスクリック時にサムネイルを消去します。

正規表現パターンは更新内容の3つ目「タグなどのIDの前後に文字列が入ったリンクでも動作するように」のためです。行頭文字、行末文字をなくしました。

まとめ
Ver. 1.1.0 はこんな感じ。
オプション機能は自分でそんなに使わないですけど、
色々試してみたかったのでつけてみました。

ここまでが去年の話。
後編では Ver. 1.2 の内容になります。

後編 → ニコニコサムネイル更新(後編)