見出し画像

GAS+Vue.js+BootstrapでVoicyビンゴ用のVoicyチャンネルルーレットを作った話

このnoteは、全文公開している「投げ銭」スタイルです。

Voicyチャンネルルーレットとは、ネットラジオ「Voicy」で配信しているチャンネルをランダムに表示するルーレットのことです。

2019年5月に長野で開催されたVoicyのチャンネル「ろりラジ」のオフ会で「Voicyビンゴ」というゲームをして遊ぼうとの発案からVoicyファンラボ1期生のちひろーかるさんが作成してくれました。

短期間でのプログラム作成ということもあり、ビンゴゲームで遊ぶには以下の問題点がありました。

・重複してチャンネルが表示されてしまう
・表示されたチャンネルの履歴が分からない

「なんとかしたいな…」とぼんやり思っていたところ、2019年7月にGoogle Apps Script (GAS) があることを知り、2020年5月にはGASを使用してWebアプリが作成できることを知った私は勉強も兼ねて、問題点を解消した新たなVoicyチャンネルルーレットを作成することにしました。

★ Voicyチャンネルルーレット ★


遊びたい方は
こちらをクリック
★ 音が出るので注意 ★

*Googleサイトに埋め込んでいないものはこちら*

ちなみに、Voicyファンラボ1期生のさんがチャンネルID順の一覧を作成しています。
配信が終了してしまい、アプリのランキングやおすすめにも表示がされなくなってしまったチャンネルもアーカイブが残っていれば聴くことができますので、新たなチャンネルの出会いにどうぞ!

▼ 参考サイト

いつも隣にITのお仕事

連載目次:Google Apps ScriptでWebアプリケーションを作る【全4回】
連載目次:GASユーザーのための初めてのHTML・CSS講座【全16回】
連載目次:GASユーザーのためのVue.js&Webアプリ作成入門【全8回】

【保存版】初心者向け実務で使えるGoogle Apps Script完全マニュアル

連載目次:初心者向け!Bootstrapで簡単レスポンシブサイト制作【全9回】

----------------------

Google Apps Script試行錯誤Blog

Googleドライブ内の音声ファイルをaudioタグで再生したい
Google Drive内の画像ファイルをHTMLのimgタグで表示したい

----------------------

Qiita

今から10分ではじめる Google Apps Script(GAS) で Web API公開
結婚式の二次会用のビンゴマシンをVue.jsで作り直した
Audioを停止(終了)させるには pause() → currentTime = 0;

----------------------

その他

jQueryで作るビンゴマシン
HTML / CSS / JavaScript(jQuery)でビンゴゲームを作成

今更聞けないBootstrapのレスポンシブ Bootstrap 4 beta 対応版
Bootstrap4(早見表)
とほほのBootstrap 4入門
Bootstrap 4で固定ヘッダーと固定フッターを設定する簡単な方法

他にも細かい部分でたくさんのサイトを参考にしました。

▼ 参考書籍

「詳解! GoogleAppsScript完全入門」がとても参考になりました。
著者は「いつも隣にITのお仕事」の運営者である高橋宣成さんです。
参考にしたのは第1版ですが、2021年7月6日に第3版が出版されています。

こちらの書籍も参考にしました。

▼ 仕様

なにそれおいしいの?

▼ スプレッドシート

・A列:チャンネル名
・B列:パーソナリティ名
・C列:チャンネルURL
・1行目:項目名

▼ コード

index.html

<!DOCTYPE html>
<html>
 <head>
   <base target="_top">
   <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>
 </head>
 <body>
   <header class="text-center">
     <h4 class="bg-info text-white">Voicyチャンネルルーレット</h4>
   </header>
   <div id="app" class="text-center">
     <div class="container-fluid">
       <div class="row align-items-center">
         <div class="col">
           <button type="button" class="btn btn-danger btn-sm" id="stopButton" v-bind:hidden="stopButton" v-on:click="runStop()">STOP</button>
           <button type="button" class="btn btn-success btn-sm" id="spinButton" v-bind:hidden="spinButton" v-on:click="runSlot()">START</button>
         </div>
         <div class="col text-danger">
           <h5><strong>{{ message }}</strong></h5>
         </div>
         <div class="col">
           <button type="button" class="btn btn-secondary btn-sm" id="resetButton" v-bind:hidden="resetButton" v-on:click="runReset()">RESET</button>
         </div>
       </div>
       <div class="row">
       </div>
       <div class="row justify-content-center" style="height:40px">
         <h4>{{ result[0] }}</h4>
       </div>
       <div class="row justify-content-center align-items-center" style="height:40px">
         <h5>{{ result[1] }}</h5>
       </div>
       <div class="row justify-content-center" style="height:40px">
         <a v-bind:href="result[2]">{{ result[2] }}</a>
       </div>
     </div>
     <div class="container-fluid">
       <div class="row justify-content-center">
         <div class="col bg-warning"><strong>チャンネル名</strong></div>
         <div class="col bg-warning"><strong>パーソナリティ名</strong></div>
       </div>
       <div class="row justify-content-center" v-for="history in historys">
           <div class="col">{{ history[0] }}</div>
           <div class="col">{{ history[1] }}</div>
       </div>
     </div>
   </div>
   <audio id="audio_drum" src="https://drive.google.com/uc?export=view&id=1eZYM6TKicQ6IwFeLr9j8y25m2WUB3MW-"></audio>
   <audio id="audio_cymbal"src="https://drive.google.com/uc?export=view&id=1MB_xsbVrYgYZ1EuIiA12bae4eoyfpOYV"></audio>
   <?!= HtmlService.createHtmlOutputFromFile('js').getContent(); ?>
 </body>
</html>


js.html

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script>

google.script.run.withSuccessHandler(initializeVue).getSpreadsheetValues();

function initializeVue(values){

 // 配列から最初の要素(見出し行のデータ)を取り除く
 values.shift();

 //***** 変数宣言 *****//
 var max = values.length; // チャンネル一覧にあるチャンネル数
 var narabikae = values;  // 並べ替え後のチャンネル一覧を格納する配列
 var kekkalist = [];      // 結果履歴を格納する配列
 var times = 0;           // ルーレット回数
 var randomCNT = 0;       // ランダム表示回数
 var randomID;            // ランダム表示用
 var timerID;             // タイマーID

 var drumRoll = document.getElementById('audio_drum'); // ドラムロール
 var cymbal = document.getElementById('audio_cymbal'); // シンバル

 // 配列の中身をランダムに並び替え
 for (let i = max - 1; i >= 0; i--) {
   const j = parseInt(Math.floor(Math.random() * (i + 1)));
   [narabikae[i], narabikae[j]] = [narabikae[j], narabikae[i]];
 }

 // HTMLにバインドするデータをVue.jsのインスタンスに設定する
 new Vue({
   el: '#app',
   data: {

       // 並び替え後のチャンネル一覧
       lists: narabikae,

       // ルーレット結果
       result: ['','',''],

       // ルーレット結果(履歴)
       historys: kekkalist,

       // ボタンの表示:非表示(false:表示、true:非表示)
       spinButton: false,
       stopButton: true,
       resetButton: true,

       // メッセージ
       message: '音が出ます'
   },
   methods:{

       //***** リセット処理 *****//
       runReset:function(){

           // ルーレット回数を初期化
           times = 0;

           // ルーレット結果(履歴)を初期化
           kekkalist.length = 0;

           // 配列の中身をランダムに並び替え
           for (let i = max - 1; i >= 0; i--) {
             const j = parseInt(Math.floor(Math.random() * (i + 1)));
             [narabikae[i], narabikae[j]] = [narabikae[j], narabikae[i]];
           }

           // ルーレット結果
           this.result = ['','',''];

           // ルーレット結果(履歴)
           this.history = kekkalist;

           // ボタンの表示:非表示(false:表示、true:非表示)
           this.spinButton = false;
           this.stopButton = true;
           this.resetButton = true;

           // メッセージ
           this.message = '音が出ます';
       },

       //***** ルーレット処理 *****//
       runSlot:function(){

           // シンバルの停止
           cymbal.pause();
           cymbal.currentTime = 0;

           // ドラムロールの再生
           drumRoll.play();

           // ボタンの表示:非表示(false:表示、true:非表示)
           this.spinButton = true;
           this.stopButton = false;
           this.resetButton = true;

           // メッセージ
           this.message = '';

           randomID = parseInt(Math.floor(Math.random() * max));
           this.result = [this.lists[randomID][0],this.lists[randomID][1],''];

           // 一定時間経ったらストップ処理をする(25ミリ秒 * 100回 = 2.5秒)
           if (randomCNT > 100){
                timerID = setTimeout(this.runStop, 25);
                randomCNT = 0;
           } else {
                timerID = setTimeout(this.runSlot, 25);
                randomCNT++;
           }
       },

       //***** ストップ処理 *****//
       runStop:function(){

           // setTimeout()でセットしたタイマーを解除する
           clearTimeout(timerID);

           // ランダム表示回数を初期化
           randomCNT = 0;

           // シンバルの再生
           cymbal.play();

           // ドラムロールの停止
           drumRoll.pause();
           drumRoll.currentTime = 0;

           // ボタンの表示:非表示(false:表示、true:非表示)
           this.spinButton = false;
           this.stopButton = true;
           this.resetButton = false;

           // メッセージ
           this.message = '';

           // ルーレット結果
           this.result = this.lists[times];

           // 配列の先頭末尾に要素を追加
           kekkalist.unshift(this.lists[times]);

           // ルーレット結果(履歴)
           this.history = kekkalist;

           // ルーレット回数に1を加算
           times ++;

           // 最後だったら
           if (times == max){

                // ボタンの表示:非表示(false:表示、true:非表示)
                this.spinButton = true;
                this.stopButton = true;
                this.resetButton = false;

                // メッセージの表示
                this.message = '★ END ★';

                return;
           } 
       }
   }
 })
}
</script>


css.html

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">


main.gs

function doGet() {
 var htmlOutput = HtmlService.createTemplateFromFile("index").evaluate();
 htmlOutput
   .setTitle('Voicyチャンネルルーレット')
   .addMetaTag('viewport', 'width=device-width, initial-scale=1')
 return htmlOutput;
}

function getSpreadsheetValues(){
 return SpreadsheetApp.getActiveSpreadsheet().getDataRange().getValues();
}

▼ 筆者スペック

ExcelVBA歴:12年(たぶん)
ASP / ASP.NET(HTML / CSS / JavaScript)歴:1年(たぶん)
※ キャリアのスタートはCOBOL

▼ おわりに

GASもVue.jsもBootstrapも初めて触れるし、HTML・CSS・JavaScriptに触れるのもかなりブランクがあったので、四苦八苦しながら作成しました。
おかげ、ソースが美しくありません。
もっと勉強して、ブラッシュアップしたいです。

次は、このルーレットに使用しているVoicyチャンネル一覧の更新作業の簡略化に挑戦したいと思います。

▼ おまけ

ここから先は

0字

¥ 100