まぐろたべたい。
もうWebAppでよくね?
以前作成したElectronアプリがありまして。
relativelayout.hatenablog.com
これは、GooglePhotosにアップした写真への直リンクを取得し、Markdownなドキュメントに貼り付けられるように変換するツールです。
このアプリは、GooglePhotos上の画像をリンクで共有した際に、そのリンク先のページのOpen Graph Protocolで記載された画像パスを取得し、Markdown形式に変換するアプリです。要はHTTP-GETして、パースして、適当な文字列を付加するだけの単純なアプリです。
もともとElectronで実装していたものの、デスクトップアプリでなくてもいいようなレベルの実装内容なのと、最近地味に使っているChrome OSでも使いたくなった(Electronが動かない)ため、今回はWebApp化を行ってみました。
WebAppをGASで超絶簡単に作成する
GASは光速でWebAPIが無料で作れることに定評がありました。
relativelayout.hatenablog.com
WebAPIができるので、サーバーサイドの処理をWebAPIとして切り出してしまったり、WebAPIの返り値に静的なHTMLを返せばWebページのホスティングもできてしまうすごい子なんです。
今回はこんな感じで処理を書いていました。
- GAS側で実装したのは
markdowner.gs
とindex.html
- UIや基本的な処理は
index.html
記載(めんどくさかった)
- ただし
index.html
上で他のドメインにHTTP-GETを行うとCORSに引っかかってしまう
- そこで
markdowner.gs
側にHTTP-GETをする機能を実装
index.html
からgoogle.script.run
を使ってmarkdowner.gs
の処理を呼び出す
コードは例によって最後に記載しておきます。
これで念願のWebApp化完了
出来上がったものがこちら。
https://bit.ly/3dYB5Yd
これでブラウザさえあればブログ執筆の効率化ができそうです。一応WebAppのURLを公開しておきます、動作の保証はありませんがきっと動きます。
クソコードはこちら
markdowner.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function convertUrl(url){
var response = UrlFetchApp.fetch(url);
return response.getContentText().match(/<meta property="og:image".*?>/g)[0].match(/http.*?=/g)[0].replace("=", "");
}
index.html
<html>
<head>
<base target="_top">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body role="document">
<nav class="navbar navbar-dark bg-primary">
<span class="navbar-brand mb-0 h1" id="result">GooglePhotoMarkdowner Online</span>
</nav>
<form class="mx-2">
<div class="form-group">
<div class="row mt-2 align-items-end">
<div class="col">
<label for="photoLink">URL</label>
<input class="form-control" type="url" id="photoLink" placeholder="Google Photoへの共有リンクを貼り付けてください">
</div>
</div>
<div class="row mt-2">
<div class="col-3">
<label for="photoSize">サイズ</label>
<input class="form-control" type="number" id="photoSize" placeholder="(オプション)">
</div>
<div class="col">
<label for="photoAlt">代替テキスト</label>
<input class="form-control" type="text" id="photoAlt" placeholder="(オプション)">
</div>
<div class="col">
<label for="photoTitle">画像タイトル</label>
<input class="form-control" type="text" id="photoTitle" placeholder="(オプション)">
</div>
</div>
<div class="row justify-content-center mt-2">
<div class="col-auto">
<button type="button" class="btn btn-primary btn-lg" id="start" onclick="convert()">変換</button>
</div>
<div class="col-auto">
<button type="button" class="btn btn-secondary btn-lg" id="clear" onclick="clearAll()">クリア</button>
</div>
</div>
<div class="row mt-4 align-items-end">
<div class="col">
<label for="photoLink">変換されたURL</label>
<input class="form-control" type="text" id="resultUrl">
</div>
</div>
<div class="row mt-2 align-items-end">
<div class="col">
<label for="photoLink">Markdown記法</label>
<input class="form-control" type="text" id="resultMarkdown">
</div>
</div>
<div class="row mt-2 align-items-end">
<div class="col">
<label for="photoLink">HTML記法</label>
<input class="form-control" type="text" id="resultHtml">
</div>
</div>
</div>
</form>
<div class="modal fade" id="processingModal" tabindex="-1" role="dialog" aria-labelledby="processingModalTitle"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm" role="document">
<div class="modal-content">
<div class="modal-body">
変換中…
</div>
</div>
</div>
</div>
<div class="modal fade" id="errorModal" tabindex="-1" role="dialog" aria-labelledby="errorTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title modal-title-danger">エラー</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
変換できませんでした。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" onclick="$('#errorModal').modal('hide');">OK</button>
</div>
</div>
</div>
</div>
<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.6/umd/popper.min.js"
integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" crossorigin="anonymous">
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"
integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous">
</script>
<script>
function pasteFromClipboard() {
if (navigator.clipboard) {
navigator.clipboard.readText()
.then(function (text) {
$("#photoLink")[0].value = text;
});
}
}
function convert() {
if (!$("#photoLink")[0].value) {
$('#errorModal').modal('show');
return;
}
$('#processingModal').modal('show');
getOgImageUrl();
}
function getOgImageUrl() {
google.script.run.withSuccessHandler(function (result) {
var generatedUrl = generateResizedUrl(result);
setAddressResult(generatedUrl);
setMarkdownResult(generatedUrl);
setHtmlResult(generatedUrl);
$('#processingModal').modal('hide');
}).convertUrl($("#photoLink")[0].value);
}
function generateResizedUrl(url) {
var result = url;
if ($("#photoSize")[0].value) {
result += "=s" + $("#photoSize")[0].value;
} else {
result += "=s0";
}
return result;
}
function setAddressResult(result) {
$("#resultUrl")[0].value = result;
}
function setMarkdownResult(result) {
var markdownResult = "![" + $("#photoAlt")[0].value + "](" + result + " \"" + $("#photoTitle")[0].value + "\")";
$("#resultMarkdown")[0].value = markdownResult;
}
function setHtmlResult(result) {
var htmlResult = "<img src=\"" + result + "\" alt=\"" + $("#photoAlt")[0].value + "\" title=\"" + $("#photoTitle")[0].value + "\">";
$("#resultHtml")[0].value = htmlResult;
}
function clearAll() {
$("#photoLink")[0].value = "";
$("#photoSize")[0].value = "";
$("#photoAlt")[0].value = "";
$("#photoTitle")[0].value = "";
}
function copyUrl() {
if (navigator.clipboard) {
navigator.clipboard.writeText($("#resultUrl")[0].value);
}
}
function copyMarkdown() {
if (navigator.clipboard) {
navigator.clipboard.writeText($("#resultMarkdown")[0].value);
}
}
function copyHtml() {
if (navigator.clipboard) {
navigator.clipboard.writeText($("#resultHtml")[0].value);
}
}
</script>
</body>
</html>