Google Apps Script html Javascript プログラミング

画像の名前を当てるクイズアプリをGoogleドライブ+GASで作った

画像のファイル名を当てるだけのクイズ

トシを取るとともに記憶力が怪しくなってきます。

頭の中に絵は浮かぶのに名前が出て来ない、ということが頻繁に起こります。

老化に少しでも抗うために画像クイズアプリを作りました。

任意のGoogleドライブフォルダに問題にしたい画像ファイルを入れます。

画像ファイルの名前から拡張子を除いたものがクイズの答えになります。

例えば、徳川家康の画像に「徳川家康.png」みたいな名前を付けておきます。

フォルダは「アクセスを管理」の設定でURLを知っていれば誰でもアクセスできるようにしておきます。

なので誰に見られても大丈夫な画像にしておくことをお勧めします。

アプリはGoogle Apps Script(GAS)のWebアプリケーションです。

画像の用意ができたらWebアプリのURLにアクセスします。

するとフォルダ内からランダム選ばれた画像が表示されます。

画像をクリックするとファイル名=答えが表示されます。

もう一度クリックすると次の画像が表示されます。これを繰り返して遊びます。

とても単純なクイズアプリですが、画像ファイルに名前を付けてGoogleドライブに保存するだけで、簡単に問題が作れます。

人名とか、地図から地名を当てるとか、動物の名前とか、画像からテキストを連想するものなら何でも問題にできます。

答え合わせや採点機能を付けろと言われそうですが、問題も自分で作るような自習用アプリには、答え合わせや採点の機能は不要だと思います。

GoogleドライブへのアクセスにフォルダIDが必要

スクリプトはスプレッドシートのバインドスクリプトにしました。

GASからGoogleドライブのフォルダにアクセスするにはフォルダIDが必要です。

フォルダIDは、ブラウザでGoogleドライブのフォルダを開いた時のURLのforlders/以降の文字列です。

https://drive.google.com/drive/u/0/folders/******* FOLDER ID *******

スプレッドシートのB1セルにこのIDを記入しておきます。ここを書き換えるだけで問題を別のフォルダにある画像に変更できます。

私の場合は空いたセルに他のフォルダのIDを書いてあります。

もうちょっとコードを書き足せば、スプレッドシートの情報を参照してWebアプリに問題を選択するメニューを追加できます。

Googleドライブ内のファイル取得と画像表示については以下の記事を参照にしました。大感謝。

Qiita:GASで動的HTML② HTML内でGoogleドライブ内の画像ファイルをダイナミックに表示させる。

https://qiita.com/jooji/items/b89ce338af359742e813

GASでhtmlをホスティング

アプリはGASでhtml + javascript + cssをホスティングする形で作りました。

基本的な手法は以下の記事にあります。

コードは、code.js、index.html、script.html、style.htmlの4つです。

scriptやcssも拡張子がhtmlなのはGASでホスティングする際のお約束です。

実際のコードを以下に残します。

code.js

const sheet = SpreadsheetApp.getActiveSheet();
const FOLDER_ID = sheet.getRange("B1").getValue();

function doGet() {
  htmlOutput = HtmlService.createTemplateFromFile("index.html").evaluate();
  htmlOutput.setTitle("Image Quiz - what/who is this? -");
  return htmlOutput;
}

function getFileListInFolder() {
  const folder = DriveApp.getFolderById(FOLDER_ID),
    files = folder.getFiles(),
    list = [];

  while (files.hasNext()) {
    const fileNext = files.next();
    const fileID = fileNext.getId();
    const fileName = String(fileNext);
    const extension = getExtension(fileName);
    if (!isPictureFile(extension)) continue;
    list.push({ fileID: fileID, fileName: removeExtension(String(fileNext)) });

  };
  return list;

}

function removeExtension(filename) {
  var lastDotPosition = filename.lastIndexOf(".");
  if (lastDotPosition === -1) return filename;
  else return filename.substr(0, lastDotPosition);
}

function getExtension(filename) {
  return filename.split(".").pop();
}

function isPictureFile(filename) {
  const pictureExtensions = ["jpg", "JPG", "jpeg", "JPEG", "png", "PNG"];
  const ext = filename.split(".").pop();
  return pictureExtensions.includes(ext);
}

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Picture Quiz</title>

  <base target="_top">
  <?!=HtmlService.createHtmlOutputFromFile('style').getContent();?>
</head>

<body>
  <div id="wrapper">
    <p id="doc">What/Who is this ? Click on Picture to show answer or go next</p>
    <p id="caption"></p>
    <img id="image" src="">
  </div>


  <?!=HtmlService.createHtmlOutputFromFile('script').getContent(); ?>
</body>

</html>

script.html

クイズ用のクラスを作成してデータ保持と問題シャッフル、かぶりのない出題といった処理を簡単にしています。

このクラスは割と汎用性があるので、別途書き残そうと思います。

code.js内にあるGoogleドライブにアクセスする関数を呼び出すためにgoogle.script.runを使っています。

script.html内でDriveApp.getFolderById()を使ったときはエラーになりました。ライブラリにDrive APIを入れても解消しませんでした。

<script>
  window.onload=function(){

let quiz={};

google.script.run.withSuccessHandler(setUp)
          .getFileListInFolder();


function setUp(list){
  quiz = new quenstionsClass(list);
  quiz.shuffle();
  const caption = document.getElementById("caption");
	caption.style.visibility ="hidden";
  const nextImage = document.createElement("img");
  const image = document.getElementById("image");

  image.addEventListener("click", function () {
	if(caption.style.visibility=="hidden"){
		// hiddenで非表示
		caption.style.visibility ="visible";
    return;
	}else{
		// visibleで表示
		caption.style.visibility ="hidden";
	}

    image.src =  nextImage.src;
    question = quiz.get;
    caption.innerText = question.fileName;
    nextImage.src = "https://drive.google.com/uc?id=" + quiz.whatsNext.fileID;// preload for faster action
  });

  let question = quiz.get;
  let next = quiz.whatsNext;
  image.src = "https://drive.google.com/uc?id=" + question.fileID;
  nextImage.src = "https://drive.google.com/uc?id=" + next.fileID;
  caption.innerText = question.fileName;

}


class quenstionsClass {
  constructor(_q_array) {
    this._data = _q_array;
    this._counter = 0;
    this._index = Array.from({ length: _q_array.length }, (a, idx) => idx);
    this._lastQuestion = {};
    this._lastCounter = 0;
    // this.shuffle();
  }

  get random() {
    //ランダムにデータをひとつ選んで返す。カウンターは無関係。同じモノが続く場合もある
    const i = Math.floor(Math.random() * this._data.length);
    return this._data[i];
  }

  get data() {
    return this._data;
  }

  get index(){
    return this._lastCounter;
  }

  get isLastQuestion(){
    return this._lastCounter === ( index.length - 1 );
  }

  shuffle() {
    if (this._index.length === 1) return;
    this._lastQuestion = this._index[this._counter];
    for (let _i = this._index.length - 1; _i > 0; _i--) {
      let _j = Math.floor(Math.random() * (_i + 1)); 
      [this._index[_i], this._index[_j]] = [this._index[_j], this._index[_i]];
    }
    if (this._lastQuestion == this._data[this._index[0]]) [this._index[0], this._index[1]] = [this._index[1], this._index[0]];
    this._counter = 0;
  }

  reset() {
    let i = this._index.length;
    while (i--) this._index[i] = i;
    this._counter = 0;
  }

  get get() {
    this._lastQuestion = this._data[this._index[this._counter]];
    this._lastCounter=this._counter;
    if (this._counter === (this._data.length - 1)) {
      // this.shuffle();
      this._counter = 0;
    } else {
      this._counter++;
    }
    return this._lastQuestion;
  }

  get whatsNext() {
    return this._data[this._index[this._counter]];
  }

}

}

</script>

style.html

<style>
  img {
    object-fit: contain;
    width: auto;
    height: auto;
    width: 90%;
    max-height: 90vh;

  }

  #caption {
    text-align: center;
    font-size: xx-large;
    font-weight: bolder;
    margin: 0em;
  }

  #doc{
    text-align: center;
    margin: 0em;


  }
</style>

パソコン版Chrome以外では動かなかった

最初の画像が表示されるまでにちょっと時間がかかりますが、なかなかいい感じのアプリができたなと思っていました。

しかし試しにiPhoneやiPadでアクセスしてみると画像が表示されません。MacのSafariでもダメでした。

どうやらパソコン版のChromeでないと動かないようです。

Googleドライブの画像表示は普通のリンクの処理と違うのでその辺が引っかかってるみたいです。

と、いったんは諦めたのですが、この話には続きが・・・

追記:ChatGPTに聞いてみたら、半分解決した

その後、どうしてモバイルからアクセスすると画像が表示できないのか、ChatGPTに聞いてみました。

すると見事な答えを返してくれました。

Googleドライブ内の画像をimgタグで指定するには、二通りの書き方があります。

私は何の気なしに選択していたのですが、書き方によってモバイルブラウザの対応に違いがあったようです。

実は、掲載してあるコードはすでに修正済みです。

iPhone/iPadのSafariからアクセスすると、ちゃんと画像が表示されました。ただ、iPhone/iPad版のChromeはダメでした。

-Google Apps Script, html, Javascript, プログラミング