Hatena::ブログ(Diary)

Alone Like a Rhinoceros Horn

2011-01-11

ニコニコ動画の検索結果フィルタおよび USConfig 開発終了のお知らせ

最初のリリースからちょうど一年が経過した Nicovideo Results Filter ですが、誠に勝手ながら現在のバージョン(0.2.7_2010-11-21)をもって開発を終了させていただきます。今後、ニコニコ動画の方でページの仕様変更などがあり、スクリプトが動作しなくなっても対応できませんのでご了承ください。

また、これに関連して、Greasemonkeyスクリプトに設定画面を提供するライブラリとして開発を進めていた USConfig も開発を終了させていただきます。これは元々 GM_config の代替として Nicovideo Results Filter で使うために書いたものであり、自分としては今後これを使った Greasemonkeyスクリプトを書く予定もないので、NRF の開発終了を期に機に、こちらも開発終了とさせていただきます。

事情、あるいは理由
  • ある日突然ページの HTML が変わってスクリプトが動かなくなる → 更新しなければ! の流れが正直面倒くさい上にしんどい

    去年一年間でだいぶ懲りました。ある日突然ページの HTML が変わってスクリプトが動かなくなる、みたいなことがこちらの都合に関係なく起こるので、無理から時間を作って対応とか、正直しんどいです。かといって放置するのも精神衛生に悪いし。

    余暇を使ってやっているのに、自分のペースでやれないのはきついです。*1
  • 自分の時間リソースを他のものに割り当てたい

    やりたいことは他にもいろいろあるわけで……

まあそんな感じです。

ソースコード

NRF, USConfig ともにソースコードを MIT License にて公開していますので、自由に改変、再配布していただいて結構です。というか、むしろ歓迎。

USConfig の方は GitHub の方にリポジトリがあります。fork して更新を push したとしても pull request は不要です。

MIT License の日本語訳および原文はこちらです↓

以上です

ご愛顧ありがとうございました。

*1:その上面白くもないし。

2010-10-21

USConfig - 旧バージョンのタグを削除しました

でお知らせしている通り、初回のロード(ブラウザに設定がまた保存されていない状態でのロード)に不具合のあった v1.10未満のバージョンについて、GitHubレポジトリからタグを削除しました。

GitHub のページの「Downloads」から、これらの不具合のあるバージョンが、(あたかも stable であるかのごとく)ダウンロード可能な状態にあることを問題と考えたためです。

直に @require されている可能性を考慮すると、タグの削除は暴挙である可能性大なのですが、以下の理由から削除に踏み切りました。

  • そもそも使われていないだろう
    あれだけの不具合、自分で見つけるまで誰からも報告がなかったことを考えると、まだまだ知名度も低く、使っている人も少ないに違いない(もしかすると皆無かも)
  • 使い続けられても困る
    仮に削除したタグを直に指定して @require しているスクリプトがあったとしても、上記の不具合を抱えているわけなので、そのまま使い続けられても困る。むしろ使えなくすべき。

stable でないものにタグを付けてしまうとこういった問題になるということで、今後は慎重にやりたいと思います。

2010-10-13

USConfig v1.11 をリリース

の v1.11 をリリースしました。

改善点

v1.0x では Config.load() まわりで大ポカをやらかしており、

  • スクリプトの初回インストール後(まだブラウザに設定が保存されていない状態の時)、設定画面から最初に「保存」するまで Config.load() が空オブジェクト({})を返す。(その結果、defaults が返ることを期待しているスクリプトは落ちる)
  • 新しく追加した設定項目も、設定画面を通して「保存」されるまでは Config.load() が返すオブジェクトでは undefined

といった問題がありました。設定画面ライブラリとしては重大な問題で、該当バージョンの USConfig を使われている方は、大変申し訳ありませんが最新バージョン(v1.11)へのアップデートをお願いいたします。

元々 GM_config が保存した設定を継承し、GM_config を置き換えるために書いたという経緯のため、ブラウザに設定が保存されていない状態でのテストが抜けていたのが問題の発見が遅れた原因です。

テスト態勢の不備が露見する形となり、非常に恥ずかしい限りです。今後はテスト態勢の充実に努め、より堅実なリリースを心がけます。

告知

USConfig に関しては、cross-browser対応をはじめ、まだまだ改善の余地があります。今後も継続的に機能の追加、改善を行っていく予定で、大きな変更があればこのブログで告知しようと思います。

2010-07-17

USConfig 機能解説 -- 複雑なレイアウトを行う

重要なお知らせ

USConfig の開発は終了しました。(2011-01-11)


の機能解説編その3。

今回は、複雑なレイアウトを行う方法について説明します。
例のごとくサンプルの貼り付けから。

Config.define('usc_complex_layouts', function() { with (this.builder) {

  var options = ["CentOS", "Debian", "Fedora", "Ubuntu"];

  dialog(
    "Complex Layouts",
    { width: 500, height: 400 },

    section(
      "Nested Grids",
      "Grids can be nested.",

      grid(
        grid(
          checkbox("Checkbox 1", 'checkbox_1', true ),
          checkbox("Checkbox 2", 'checkbox_2', false), '\n',
          checkbox("Checkbox 3", 'checkbox_3', false),
          checkbox("Checkbox 4", 'checkbox_4', true)
        ),
        grid(
          button("Button 1", 'button_1',
            function() { alert("Button 1 was clicked!"); })
        ),
        '\n',
        grid(
          select("Select 1", 'select_1', options, "Ubuntu")
        ),
        grid(
          checkbox("Checkbox 5", 'checkbox_5', true ),
          checkbox("Checkbox 6", 'checkbox_6', false), '\n',
          checkbox("Checkbox 7", 'checkbox_7', false),
          checkbox("Checkbox 8", 'checkbox_8', true)
        ),
        '\n',
        grid(
          text("Text 1", 'text_1', "String"), '\n',
          integer("Integer 1", 'integer_1', 100), '\n',
          number( "Number 1", 'number_1', 3.14)
        ),
        textarea("Textarea 1", 'textarea_1', "String", { label: 'top', cols: 20 })
      )
    ),
    section(
      "Manual Layouts",
      "You can control table cell's content manually.",

      grid(
        { layout: 'manual' },
        checkbox("Checkbox 9", 'checkbox_9', true ),
        checkbox("Checkbox 10", 'checkbox_10', false), '|',
        checkbox("Checkbox 11", 'checkbox_11', false),
        checkbox("Checkbox 12", 'checkbox_12', true), '\n',
        textarea(null, 'textarea_2', "String", { cols: 20 }), '|',
        textarea(null, 'textarea_3', "String", { cols: 20 })
      )
    )
  );
}}, {
  aftersave: function() {
    var msg = "Seved settings are:\n\n";
    for (var id in this.settings) {
      msg = msg + id + " = " + this.settings[id] + '\n';
    }
    alert(msg);
  },
});

GM_registerMenuCommand("USConfig Sample - Complex Layouts", Config.open);

 ↓

grid() は入れ子にできる

まずはセクション1から。

grid() は入れ子にできます。サンプルでは 2行3列の親grid に対し、コントロールを実際に内包する grid を入れ子にしています。

f:id:h1mesuke:20100717050730p:image

手動レイアウト

続いてセクション2。

USConfig では、とりあえずコントロールを grid() で囲っておけばそれらしい配置になるようになっています。ラベルとコントロールの整列が自動的に行われるのは便利な半面、それが邪魔をして望み通りの配置が行えない場合があります。

例えば、大きな textarea の上に複数の checkbox を配置したい場合などです。

f:id:h1mesuke:20100717050731p:image

このような場合、grid() のオプションに { layout: 'manual' } を指定することで grid() におけるコントロールの配置方法を手動に切り替えることができます。

grid() においては '\n' によって列の終わり/折り返しを指示しますが、手動レイアウトではそれに加え '|' によってセルの終わりを指示できるようになります。これにより、grid中における個々のセルの内容を自分で決められます。*1

*1:一方、このやり方ではラベルとコントロールを別々のセルに分割することはできなくなります。

2010-07-15

USConfig 機能解説 -- テーマを変更する

重要なお知らせ

USConfig の開発は終了しました。(2011-01-11)


の機能解説編その2。テーマ機能について。

USConfig が表示する設定ダイアログは、デフォルトでは黒とグレーを基調とした配色*1になっています。

しかし、スクリプトが @include しているページによっては、この配色がふさわしくない場合があります。例えば、青を基調とした配色のページであれば、設定ダイアログの配色も青を基調としたものにしたいところです。

ダイアログのスタイルは dialog() のオプション引数に css を設定することでも変更できます*2が、もっと大雑把に「青っぽく」「紫を基調に」といった指定ができると便利です。

そこで USConfig では、表示するダイアログの配色を簡単に変更できるように、dialog() のオプション引数でダイアログのテーマを設定できるようになっています。

  dialog(
    "Title",
    { width: 500, height: 400, theme: 'blue' },
    section(...),
    section(...)
  );

これにより、ダイアログの配色を簡単に変えられます。

f:id:h1mesuke:20100714214758p:image

用意されているテーマの一覧は Wiki を参照して下さい。

新しいテーマを定義して使うこともできます。その方法についてはサンプル samples/themes.user.js を、テーマのフォーマットについては Wiki を参照して下さい。

*1:これは先人である GM_config の配色に倣いました。移行しても違和感がないようにとの配慮です。

*2:USConfig ではダイアログ上のすべてのラベル、コントロールに id と class を割り振っています。

2010-07-14

USConfig 機能解説 -- 国際化する

重要なお知らせ

USConfig の開発は終了しました。(2011-01-11)


の機能解説編その1 *1

@include しているページが日本語のページなら、スクリプトの設定ダイアログを日本語でハードコードしても問題ないと思いますが、

  • ワールドワイドなページを @include しており、
  • ユーザーが日本人に限定されない

というような場合、英語圏のユーザーには英語で、日本人ユーザーには日本語でダイアログを表示したいところです。

そこで、USConfig ではダイアログを国際化できるように、簡易的な i18n機能を実装してみました。サンプルは以下の通りです。(※スクリプト中で日本語などの非ASCII文字を使用する場合、スクリプトのエンコーディングは UTF-8 にして下さい)

■samples/i18n.user.js

Config.locale.addTranslation('en', {
  'title'        : "Title",
  'section'      : "Section ${i}",
  'section_desc' : "This is a description of section ${i}. [optional]",
  'button'       : "Button ${i}",
  'clicked'      : "Button ${i} was clicked!",
  'checkbox'     : "Checkbox ${i}",
  'radio'        : "Radio ${i}",
  'select'       : "Select ${i}",
  'text'         : "Text ${i}",
  'string'       : "String",
  'integer'      : "Integer ${i}",
  'number'       : "Number ${i}",
  'textarea'     : "Textarea ${i}",
  'static_text'  : "This is a static text ${i}.",
  'saved_msg'    : "Seved settings are:\n\n",
});

Config.locale.addTranslation('ja', {
  'title'        : "設定画面のタイトル",
  'section'      : "セクション ${i}",
  'section_desc' : "これはセクション ${i} の説明文です。(省略可)",
  'button'       : "ボタン ${i}",
  'clicked'      : "ボタン ${i} がクリックされました!",
  'checkbox'     : "チェックボックス ${i}",
  'radio'        : "ラジオボタン ${i}",
  'select'       : "選択リスト ${i}",
  'text'         : "テキスト ${i}",
  'string'       : "文字列",
  'integer'      : "整数 ${i}",
  'number'       : "数値 ${i}",
  'textarea'     : "テキストエリア ${i}",
  'static_text'  : "スタティックテキスト ${i}",
  'saved_msg'    : "保存された設定値は以下の通りです。\n\n",
});

Config.define('usc_i18n', function() { with (this.builder) {

  var options_1 = ["BSD", "Linux", "Mac", "Windows"];
  var options_2 = ["CentOS", "Debian", "Fedora", "Ubuntu"];

  dialog(
    _('title'),
    { width: 500, height: 400 },

    section(
      _('section', {i:1}),
      _('section_desc', {i:1}),

      grid(
        // buttons
        button(_('button', {i:1}), 'button_1',
          function() { alert(_('clicked', {i:1})); }),

        button(_('button', {i:2}), 'button_2',
          function() { alert(_('clicked', {i:2})); }),

        button(_('button', {i:3}), 'button_3',
          function() { alert(_('clicked', {i:3})); })
      ),
      grid(
        // checkboxes
        checkbox(_('checkbox', {i:1}), 'checkbox_1', true ),
        checkbox(_('checkbox', {i:2}), 'checkbox_2', false),
        checkbox(_('checkbox', {i:3}), 'checkbox_3', true), '\n',
        checkbox(_('checkbox', {i:4}), 'checkbox_4', false),
        checkbox(_('checkbox', {i:5}), 'checkbox_5', true ),
        checkbox(_('checkbox', {i:6}), 'checkbox_6', false)
      ),
      grid(
        // radio buttons
        radio(_('radio', {i:1}), 'radio_1', options_1, "Linux"), '\n',
        radio(_('radio', {i:2}), 'radio_2', options_1, "Mac")
      ),
      grid(
        // select controls
        select(_('select', {i:1}), 'select_1', options_2, "Ubuntu"),
        select(_('select', {i:2}), 'select_2', options_2, "Debian")
      )
    ),
    section(
      _('section', {i:2}),
      _('section_desc', {i:2}),

      grid(
        // text fields
        text(_('text', {i:1}), 'text_1', _('string')),
        text(_('text', {i:2}), 'text_2', _('string')), '\n',
        integer(_('integer', {i:1}), 'integer_1', 100),
        integer(_('integer', {i:2}), 'integer_2', 100), '\n',
        number(_('number', {i:1}), 'number_1', 3.14),
        number(_('number', {i:2}), 'number_2', 3.14)
      ),
      grid(
        // text area
        textarea(_('textarea', {i:1}), 'textarea_1', _('string'), { label: 'top' }),
        textarea(_('textarea', {i:2}), 'textarea_2', _('string'), { label: 'top' })
      ),
      grid(
        // static texts
        staticText(_('static_text', {i:1})),
        staticText(_('static_text', {i:2})),
        staticText(_('static_text', {i:3}))
      )
    )
  );
}}, {
  aftersave: function() {
    var msg = Config.locale.localizedString('saved_msg');
    for (var id in this.settings) {
      msg = msg + id + " = " + this.settings[id] + "\n";
    }
    alert(msg);
  },
});

GM_registerMenuCommand("USConfig Sample - I18n", Config.open);

 ↓

f:id:h1mesuke:20100714124401p:image

f:id:h1mesuke:20100714124402p:image

ダイアログの構成は samples/basic.user.js と同じですが、コントロールのラベルが国際化されています。

Config.locale.addTranslation() によって言語別リソースを定義し、ダイアログ定義関数の中で _() を使ってメッセージの置換を行います。_() によるメッセージの置換では place holder を使うことができ、メッセージ中の ${name}_('message', {name:"hoge"} によって hoge に展開されます。*2

使用する言語は navigator.language をもとに決定されます。

*1:こういうのって GitHubWiki にでも「英語で」書くべきかと思うのですが、めんどくさいのでまずはこっちに書きます。まずはと言いましたが、はっきり言って英語ドキュメントの整備はやる気ないです。

*2_()Config.locale.localizedString() の別名です。ダイアログ定義関数の外では _() で参照できないため、コールバック関数の中では Config.locale.localizedString() が使われています。

2010-07-13

Greasemonkeyスクリプトに設定画面を追加するライブラリ USConfig

重要なお知らせ

USConfig の開発は終了しました。(2011-01-11)


GitHub に置いておいたので興味のある人は使ってみて下さい。特に、同種のライブラリである GM_config を既に使っていて、

  • コントロールのレイアウトが自由にできない
  • コントロールの位置が揃わない
  • 設定画面がたまに出ない

といった点に不満を感じている方にお勧めです。

開発の経緯

上に挙げた GM_config に対する不満点を解消した上で、GM_config を置き換えるべく開発しました。目標は GM_config の対抗馬となること、そしてより使いやすいライブラリを提供することです。

Greasemonkeyスクリプトに設定画面を付加する方法としては他に jQuery を利用する方法などもあるようですが、導入の敷居などを考えると @require するだけで使える GM_config や USConfig の存在意義はそれなりにあると考えています。

互換性

GM_config を置き換えるために開発したものですが、使い方、中身ともにまったく別物となっています。なので、GM_config からの移行にはソース(ダイアログ定義部分)の書き換えが必要になります。

しかし、最終的に設定を GM_setValue() で保存し、またそれを GM_getValue() でロードする部分は変わりませんので、ライブラリの置き換えを適切に行えば、クライアントが保存している設定を継承できます。詳しくは以下に続く「使い方」の「GM_config からの移行」を見て下さい。

なお、現在のところ cross-browser対応は(その名前に反し)できていません。Greasemonkeyスクリプトがそのメタデータブロックにおいて @require して使うことを想定しています。GM_config の方がいつの間にか cross-browser対応を果たしていたので追随したいところですが、あまり優先度は高くないです。

使い方

サンプルを付けておいたのでそれを見て下さい、で終わりたいところですが、せっかくですので簡単に使い方を説明します。

USConfig による設定ダイアログの定義は以下のような感じになります↓

■samples/basic.user.js

Config.define('usc_basic', function() { with (this.builder) {

  var options_1 = ["BSD", "Linux", "Mac", "Windows"];
  var options_2 = ["CentOS", "Debian", "Fedora", "Ubuntu"];

  dialog(
    "Title",
    { width: 500, height: 400 },

    section(
      "Section 1",
      "This is a description of section 1. [optional]",

      grid(
        // buttons
        button("Button 1", 'button_1',
          function() { alert("Button 1 was clicked!"); }),

        button("Button 2", 'button_2',
          function() { alert("Button 2 was clicked!"); }),

        button("Button 3", 'button_3',
          function() { alert("Button 3 was clicked!"); })
      ),
      grid(
        // checkboxes
        checkbox("Checkbox 1", 'checkbox_1', true ),
        checkbox("Checkbox 2", 'checkbox_2', false),
        checkbox("Checkbox 3", 'checkbox_3', true), '\n',
        checkbox("Checkbox 4", 'checkbox_4', false),
        checkbox("Checkbox 5", 'checkbox_5', true ),
        checkbox("Checkbox 6", 'checkbox_6', false)
      ),
      grid(
        // radio buttons
        radio("Radio 1", 'radio_1', options_1, "Linux"), '\n',
        radio("Radio 2", 'radio_2', options_1, "Mac")
      ),
      grid(
        // select controls
        select("Select 1", 'select_1', options_2, "Ubuntu"),
        select("Select 2", 'select_2', options_2, "Debian")
      )
    ),
    section(
      "Section 2",
      "This is a description of section 2. [optional]",

      grid(
        // text fields
        text("Text 1", 'text_1', "String"),
        text("Text 2", 'text_2', "String"), '\n',
        integer("Integer 1", 'integer_1', 100),
        integer("Integer 2", 'integer_2', 100), '\n',
        number( "Number 1", 'number_1', 3.14),
        number( "Number 2", 'number_2', 3.14)
      ),
      grid(
        // text area
        textarea("Textarea 1", 'textarea_1', "String", { label: 'top' }),
        textarea("Textarea 2", 'textarea_2', "String", { label: 'top' })
      ),
      grid(
        // static texts
        staticText("This is a static text 1"),
        staticText("This is a static text 2"),
        staticText("This is a static text 3")
      )
    )
  );
}}, {
  aftersave: function() {
    var msg = "Seved settings are:\n\n";
    for (var id in this.settings) {
      msg = msg + id + " = " + this.settings[id] + "\n";
    }
    alert(msg);
  },
});

GM_registerMenuCommand("USConfig Sample", Config.open);

 ↓

f:id:h1mesuke:20100713170217p:image

f:id:h1mesuke:20100713170218p:image

@require

メタデータブロックにて @require すれば使えるようになります。この辺の手軽さはウリのひとつです。*1

@require に際しては安定動作を確認したバージョンを自分の管理下にあるサーバーに配置し、それを @require することを推奨。GitHub上にあるものを使う場合はサンプルのように必ずタグで特定のバージョンを指定のこと。間違っても master branch の HEAD を @require したりしないようにして下さい。

ダイアログを定義する

ダイアログの定義は Config.define() によって行います。

Config.define('dialog_name', function() { with (this.builder) {

  dialog(
    "Title",
    { width: 500, height: 400 },

    section(
      "Section 1",
      "This is a description of section 1. [optional]",

      grid(...),
      grid(...)
    ),
    section(...)
  );
}}, {
  aftersave: function() {...},
});
  1. ダイアログの名前
  2. ダイアログの定義関数
  3. コールバック関数などのオプション( → 指定可能なオプション一覧

を指定してダイアログを定義します。

function() { with (this.builder) {...}}

の部分がダイアログ定義関数であり、with の内側は DSLっぽく記述できるようになっています。ここにいくつかの section() を定義し、その内側にいくつかの grid()、さらに内側に button(), checkbox(), radio(), select(), text(), integer(), number(), textarea() などを記述することでダイアログを定義します。*2

section は設定項目のカテゴリであり、grid はレイアウト上のコントロールのグループです。同種のコントロールを同じ grid() にまとめると、ラベルの右端、コントロールの左端がいい感じに揃うようになっています。(※サンプル画像参照*3

checkbox() をはじめとする設定項目のコントロールを生成するメソッドには

  1. ラベル
  2. 設定項目ID
  3. デフォルト値

を指定します。(※他にもHTML要素に設定する属性やツールチップ文字列などを指定できたりしますが詳細はソースを見て下さい(;^ω^))

メニューへ登録する

GM_registerMenuCommand() を使ってメニューに登録する点は GM_config と同じです。要は Config.open() を呼び出せば定義したダイアログを表示させることができます。*4

GM_registerMenuCommand("USConfig Sample", Config.open);
設定をロードする

Config.load() でローカルに保存されている設定をロードすることができます。*5返値は「設定項目ID → 値」な関連をまとめたハッシュ(オブジェクト)です。

var settings = Config.load();
国際化する
テーマを変更する
複雑なレイアウトを行う
GM_config から移行する

Config.define() に渡すオプションで saveKey に 'GM_config' を指定すると GM_config で管理していた設定を継承できます。(注:1.2以前の GM_config で単一ダイアログのものの場合。また設定項目の ID は GM_config と同じものを使う必要があります。*6

Config.define('dialog_name', function() { with (this.builder) {
  dialog(...);
}}, {
  saveKey: 'GM_config',
  aftersave: function() {...},
});

ユーザー側の設定が失われたりすることがないよう、移行に際しては十分なテストを行って下さい。


今後の展開

cross-browser対応したいと思っています。現在は Firefox をメインに使っておりその優先順位はさほど高くありませんが、そのうちやる予定です。

cross-browser対応は、「そのうちやるかも」程度の優先順位です。

*1:その一方、Greasemonkey を前提しているため cross-browser対応を考えるときに面倒なことになる。

*2:これらはすべて Builderオブジェクトのメソッドですが、with (this.builder) {...} によってレシーバを省略できるようにすることで DSL的な記述ができるようにしています。

*3:このレイアウトを GM_config でやるのは至難のワザだろうと思います。

*4:定義したダイアログがひとつの場合。複数のダイアログを定義した場合は Config.open() に表示するダイアログの名前(Config.define() において設定したもの)を指定する必要がある。

*5:定義したダイアログがひとつの場合。複数のダイアログを定義した場合は Config.load() に取得する設定の名前(Config.define() において設定したもの)を指定する必要がある。

*6:それ以外の場合は何という名前で GM_setValue() されているか各自で調べて下さい。