• プラグイン開発ガイド

    6.form.html(その3)

    (3)グリッドレイアウト

      <div class="form-group">
        <div class="col-sm-6">
    

    col-sm-6.png
    form-group内部にはクラスとしてグリッドレイアウトを指定したdiv要素を配置する。
    例えば、1行ぶち抜きでタイトル化したい場合は <div class="col-sm-12">という形で指定する。
    このときに指定したクラスの文字列「col-sm-12」のうち、colはcolumn(行)、smはsmall(画面サイズがsmall以上の場合適用)、12は当該パーツのサイズ(グリッドの横領域を最大12とした場合、どのくらい割当するか)を指定する。
    なお、クラスにグリッドレイアウトを複数指定した場合、画面のサイズから自動的に適用される。
    例えば"col-xs-12 col-sm-6"と指定した場合は、画面サイズがスマートフォン(xtra-small)の場合は領域一杯、タブレット端末(small)以上の場合は領域の半分で割り当てられる。

    グリッドレイアウトでのプラグインの外観制御方法は以下の通り。

    画面サイズ 制御方法
    スマートフォン(xtra-small) 横幅が狭いためxs-12で領域全体をラベルやテキストボックスに割当
    タブレッド以上(small) ラベルとテキストボックスを半々などにして表示させる。
    文字列長さが必要な場合は領域全体を使用

    グリッドレイアウトを指定した場合、その div タグの左右余白は 15pixel 確保されるので、グリッドレイアウトの親タグでform-groupを指定しておくとパーツの縦並びを揃えることが可能になる。

  • 7.form.html(その4)

    (4)ラベル

    <label class="control-label col-sm-4">support PNG</label>
    

    テキストボックス等の設定項目の表題を表示する場合、<label>タグを使用する。
    labelタグのクラスにはcontrol-labelとグリッドレイアウトのサイズを指定する。
    つまりラベルとテキストボックスを組み合わせることで1行にラベルでの説明+テキストボックスでの入力域を構成可能である。
    (上記の例だとsmallの場合はラベルが 4/12 のサイズで表示され、その後ろに テキストボックスを定義すると最大 8/12 で構成可能となる。)

    (5)テキストボックス

    <input type="text" class="run" />
    

    テキストボックスの指定にはinputタグを使用する。
    typeでtextを指定すると文字列、numberを指定すると数字のみ指定(かつ右横に上下の矢印が表示され、これのクリックで値の増減が可能)
    テキストボックのクラスにはrunとconfigの2つが指定出来、以下の通り処理が変更される。

    クラス名 詳細
    run run.shにパラメータとして指定することが可能
    config smpd サーバ上のファイルを後述の方法で直接書換可能
    ※configで値を取得する/ファイルの値を書き換えする場合は以下のとおり実施

     

    • classにconfigを指定(class="config")
    • uriに読込/書換対象のファイル名を指定(url="/etc/config")
    • keyに書換対象行のキー文字列を指定(key="hogehoge")

    つまりuriに指定した設定ファイル名内に key=value という記述を見つけた場合、valueを当該 input タグの値として表示し、
    プラグインの処理終了時にinput タグの値で設定ファイル内の当該キーを書き換えする。

    テキストボックスに対してグリッドレイアウトを適用する場合は適用したいボックスの上位にグリッド用のdivタグを配置し、
    当該テキストボックスを入れ込む。

    <div class="col-sm-6">
      <input type="text" class="run" id="hogehoge">
    </div>
    
  • 8.form.html(その5)

    (6)select タグ

     <div class="select-wrap">
       <select id="mpdnasno2" class="form-control">
         <option value="">NAS0</option>
         <option value="1">NAS1</option>
         <option value="2">NAS2</option>
        *** 中略 ***
       </select>
     </div>
    

    予め指定された選択肢から選択するような場合、select タグとoptionタグを使用する。
    bootstrap の標準設定では select タグのスタイルは無指定だが、webui では他のフォーム要素と同等のスタイルを設定済みである。これを有効にするため、selectタグの親に <div class="select-wrap"> が必要となる。

    selectタグの代わりに datalist タグ+inputタグによるコンボボックスも最新のブラウザでは使用可能ではあるが、
    スマートフォン向けのブラウザではdatalistは解析されず通常の input タグとしてしか機能しないため select の使用をお勧めする。

    (7)チェックボックス・ラジオボタン

    <div class="checkbox-inline">
     <input type="checkbox" id="rescan-flg"/> rescan this directory
    </div>
    

    グリッドレイアウトで配置した他のフォームと同一行内にチェックボックス/ラジオボタンを配置する場合、
    チェックボックス/ラジオボタンの親にdiv要素などでcheckbox-inline や radio-inline をクラス指定しておくと当該要素の高さがグリッドレイアウトのパーツと揃って表示される。
    また、複数のチェックボックス/ラジオボタンをグリッドレイアウトを使用せずに1行に並べて表示したい場合もcheckbox-inline・radio-inline は有効である。

  • 9.form.html(その6)

    (7)JavaScript

    <script>
      $(function(){
        if (config.browsepath.trim() == "") {
          $("input.run").val("/");
        } else {
          $("input.run").val(config.browsepath.trim());
        }
      });
    
      $('#plugin-form').on('shown.bs.modal', function() {
        socket.send('YMPD_GET_MPDHOST');
      });
    
      $("#mpd_database_form button").on('click', function() {
        $(this).off('click');
        var v = 'cmd:' + $(this).attr('id') + (($('#mpdnasno2').val() === '') ? '' : $('#mpdnasno2').val());
        $('input.run').val(v);
        plugin_form_ok();
        $('#plugin-form').modal('hide');
      });
      $('#rescan-flg').on('click', function() {
        var v = $('input.run').val().replace('rescan-', '');
        if ($('#rescan-flg').prop('checked')) v = 'rescan-' + v;
        $('input.run').val(v);
      });
    </script>
    

    script タグで囲まれた、form.html の最後部部分に JavaScript を記載する。
    まず、

      $(function(){
      }
    

    で囲まれた領域にプログラムを記載すると、form.htmlの読み込み終了直後(プラグイン起動直後)に何らかの動作を1度だけ行うことが可能である。
    本領域に記載する内容としては主に「プラグイン起動時にパラメータを読み込み」したりplugin-form-footer 領域にある標準のプラグイン制御ボタン(Cancel・OK)を非表示にする場合などに使用する。

    サンプルで表示している JavaScript では $(function(){ 部分に続き、$('#plugin-form').on('shown.bs.modal', function() { と $("#mpd_database_form button").on('click', function() { の2つの関数定義があるが、
    それぞれ pluguin-form の表示時と mpd_database_form 内のボタンが押された場合の処理を記載している。

    なお、前項で説明したテキストボックスの run と config クラスの実行については原則 plugin-form-footer の 「OK」ボタンを押した際のみ実行される(webui内部のmpd.js スクリプト内で plugin-form-ok()関数が呼ばれて実行される)。

  • 10.標準外のボタンを設置する場合
    03_searchの場合など、標準の「Cancel」・「OK」ボタンを使わずに別なボタンをプラグインの最下部に表示したい場合、
    <div class="form-horizontal"> とは別に <div class="modal-footer"> を作成して、この中にボタンを配置する。
    また、標準のボタンを JavaScript で以下の通り非表示とする。

    $('#plugin-form-footer').addClass('hidden');
    

    (標準ボタンはプラグイン領域が閉じた際に自動的に表示状態に戻るようにメインのスクリプトで記載済みのため、新たに再表示のためのコマンドは記載する必要は不要)

    また、追加で設置したボタンでの処理終了後、以下の通り当該プラグインフォームを閉じるように記述が必要です。

    $('#plugin-form').modal('hide');
    
  • 11.結果だけを表示するプラグインを配置する場合

    <div class="form-horizontal pd-20">
      <div class="panel panel-default">
        <table class="table"><tbody id="mpdstats"></tbody></table>
      </div>
    </div>
    <script>
      $(function() {
        $('#plugin-form-footer').addClass('hidden');
        $('#plugin-form').on('shown.bs.modal', function() {
          $(this).off('shown.bs.modal');
          socket.send('MPD_API_GET_STATS');
        });
      });
    </script>
    

    これはmpdの内部状況を返す 05-stats の form.html ですが、通常のform.htmlの先頭で定義されている form-horizontal が定義されたdivタグに pd-20 というクラスが追加で定義されています。

    このクラスは内部的に上下左右の余白(padding)を20pixelとして設定されているため、このクラスを定義すると上下左右に20pixelの余白を生成します。

    なお、form-horizontalの下層のdivタグにはpanel panel-default と定義されていますが、これはtableの外枠を描くために使用しています。

  • 12.結果だけを表示するプラグインを配置する場合(その2)

    <div class="pd-20" id="plugin-form-body">
      <pre></pre>
    </div>
    

    これはDisk-Usage プラグインの場合です(一部分)。
    当該プラグインの場合、実際にはform.html は存在せず、run.shの動作結果が pre タグとその内部の記述で返されています。

    また、プラグインを起動する際にplugin-form-bodyをIDに持つdivタグに pd-20 のクラスを追加して、上下余白を20pixelに設定しています。

    ※form.html を使用しない結果だけ出力するプラグインは現状 Disk-Usage のみであるため、上記 pd−20 クラスの追加は決め打ちで実行していますが、
    今後そのような事例が追加になった場合は別途対応を考えたいと思います。

  • 13.run.sh

    form.html の OK ボタンを押した際に、run クラスを持つ input タグ(テキストボックス)が1つでも存在する場合、当該タグの値を引数として run.sh が実行されます。
    ちなみに複数値がある場合、form.html 内での出現順に引数が渡されます。

    #!/bin/bash
    # Copyright © 2020 symphonic-mpd Research and Development Club
    
    #target="/var/lib/mpd/music/NAS"
    
    case "$1" in
      cmd:plugin_run_backup*)
        target="/var/lib/mpd/music/NAS${1#cmd:plugin_run_backup}"
        mpc stop > /dev/null 2>&1
        systemctl stop mpd > /dev/null 2>&1
        mount $target > /dev/null 2>&1
        mount -o rw,remount $target  > /dev/null 2>&1
        if [ -e $target/ ]; then
          cd /var/lib/mpd
          tar czf $target/playlists.tar.gz playlists
          cp -p tag_cache $target/
          mount -o ro,remount $target > /dev/null 2>&1
        fi
        systemctl start mpd
        sleep 0.2
        echo "<h2>Backup complete.</h2><pre>"
        ls $target/playlists.tar.gz $target/tag_cache -1
        echo "</pre>"
        echo "<script>setTimeout(function(){$.notifyClose();}, 1500);</script>"
        ;;
    
      cmd:plugin_run_restore*)
        target="/var/lib/mpd/music/NAS${1#cmd:plugin_run_restore}"
        if [ ! -e $target/tag_cache ]; then
          echo "<h3>There is no tag_cache on the NAS.</h3>"
          exit 0
        fi
        mpc stop > /dev/null 2>&1
        systemctl stop mpd > /dev/null 2>&1
        mount $target > /dev/null 2>&1
        cd /var/lib/mpd
        if [ -e $target/tag_cache ]; then
          cp -p $target/tag_cache .
        fi
        if [ -e $target/playlists.tar.gz ]; then
          tar xf $target/playlists.tar.gz
        fi
        systemctl start mpd
        sleep 0.2
        echo "<h2>Restore complete.</h2>"
        echo "<script>setTimeout(function(){$.notifyClose();}, 1500);</script>"
        ;;
      rescan-*)
        mpc rescan "${1#rescan-}" > /dev/null
        ;;
      *)
        mpc update "$1" > /dev/null
        ;;
    esac
    
    exit $?
    
    

    11-update_library の run.sh で説明します。

    本例では引数1個だけの場合で、cmd:plugin_run_backup* ・ cmd:plugin_run_restore* ・ rescan-* *のどれかに該当すると処理を行っています。
    ("*" の部分にはさらに追加指定されたパラメータが付与されている)

    run.sh の実行はもう一パターンあり、当該プラグインのディレクトリ内にform.htmlが存在しない場合(正確にはプラグイン呼び出し時にform.htmlをload関数で呼び出ししたがエラーが返された場合)は引数なしで実行されます。

    #!/bin/bash
    # Copyright © 2020 symphonic-mpd Research and Development Club
    
    echo "<pre>"
    pstree -Uclntp
    echo "</pre><pre>"
    df -BM -T -xdevtmpfs -xtmpfs -l |sed -z 's/\n/<br>/g'
    echo "</pre>"
    
    used_root=` df -h | grep "/dev/root" | awk '{ print $5 }' | sed -e '$s/.$//' `
    if [ $used_root -gt 80 ];then
        echo "<div class='alert alert-danger' role='alert'>Disk utilization has exceeded 80%.</div>"
    fi
    
    exit 0
    

    本例として 81-disk_usage の run.sh を掲載します。
    pstreeとdfの実行結果を pre タグで閉じて文字列として標準出力に返しているのがおわかりいただけるかと思います。

  • 14.run.sh(その2)
    プラグインのボタンクリック時に対し、RaspberryPi側に何らかのアクションを起こさせたい場合にも run.sh を使用することができます。
    実は、先に説明した「run クラスを持つ input タグ」での実行時、webui の内部では以下の処理を行っています。
    ① ブラウザの JavaScript がympdのサーバに向かって 'ACTIVATE_PLUGIN,/opt/plugins/' + プラグインのパス + ’,’ + パラメータ
    という命令を送る。
    ②ympd サーバが①の命令を受信すると、プラグインのパスにある run.sh を指定されたパラメータを与えて実行する。

    つまり、上記と同じ仕組みをプラグインのJavaScriptで構築し、run.sh に直接渡せばよいのです。

    function some_button_click(param) {
      var p = $('#plugin-form').data('path') + ',' + param;
      socket.send('ACTIVATE_PLUGIN,/opt/plugins/' + p);
    }
    

    上記の関数例(プラグインのJavaScriptソースコード)は ①の動きを直接記載したもので、
    関数の引数 param は プラグインの run.sh に渡すパラメータです。
    また、$('#plugin-form').data('path')が当該プラグインのパスが記載されている場所になります。
    この場合は単一の引数ですが、何らかの区切りで文字列を繋げば複数の引数をrun.shに渡し、run.shで区切り文字で配列を分割すれば複数パラメータを渡すことも可能です。

    次に、run.sh の実行結果をどうやってプラグインが受領するかですが、一例として run.sh に以下のように記載すると、
    /var/lib/mpd/RAM/ 上に隠しファイルで実行結果を書き込みできます。

    LOG=/var/lib/mpd/RAM/.bbb.out
    echo "aaa" > $LOG
    echo "---" >> $LOG
    

    さらにこの.bbb.outをプラグインで受信する場合、 JavaScript でこのようにすると良いでしょう。

    var checktimer = setInterval(function () {
        $('#out')
          .load('/RAM/.bbb.out?t=' + Date.now(), '', function (data) {
            if (data.endsWith('---\n')) {
              clearInterval(checktimer);
              var data2 = data.replace('---\n', '');
              //ここに受信したデータの処理を書き込み
            }
          });
      }, 300);
    

    これは、idにoutというタグを予め用意しておき、/RAM/.bbb.out の出力を300ミリ秒間隔で 文字列の最後が "---\n" で終わるまで受信し続けます。
    "---\n"が文字列の最後にあった場合、これを除去して受信したデータを処理すれば良い訳です。

  • 15.任意プログラムの実行方法
    実は webui (で使用しているympd)では、httpで /x/ に続いてコマンドを指定して実行することができます。
    この場合、一般的な cgi と同じように http://(ympdのIPアドレス)/x/(実行コマンド)?(パラメータ)で渡します。
    一連の処理のサンプル(JavaScript)はこちら

        $.ajax({
          url: location.protocol + '//' + location.host + '/x/vsound-setup.sh?interface',
          dataType: 'text',
        }).done(function (data) {
          const v2 = data.split(',');
          //ここに実行処理
        }).fail(function (data) {
          //ここにエラー処理
        });
    

    これは、AoEプラグインでの使用例で、vsound-setup.shにinterface というパラメータを渡した場合です。
    実行は JQuery の$.ajax を使って与えるコマンドを指定し、その実行結果の標準出力をdone関数のdataで受け取るという形になります。
    とりあえず、「/x/を使った実行は RaspberryPi側のPATH が有効な場所にあるアプリしか実行できない」ため、
    新しいプログラムやシェルスクリプトを置く場合は /usr/local/bin になると思います。