クリップボードを共有するPWAアプリの作成

WindowsとAndroidでテキストを共有したいことがよくあり、最近はMacやiPhoneとも共有することが増えてきたので、クリップボードのテキストを共有するPWAアプリを作成します。
PWAにすれば、アプリっぽく使えるのと、後程示すショートカットが便利だったりします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/94d92292-2e5d-b27d-1fe7-a842b7dc008f.png)

以下のGitHubにソースコードを上げておきます。

poruruba/ClipShare

https://github.com/poruruba/ClipShare

#テキストの共有方法

単に、Node.jsサーバを立ち上げて、テキストを保持するようにし、HTTP Post呼び出して、Set/Getするようにしているだけです。
ソースコードを載せますが、大したことはやっていません。
一応、セキュリティを考慮して、APIKeyがあっていないと受け付けないようにするのと、1日間以上はテキストを保持しないようにしています。以下に示す変数APIKEYの部分です。

```js:/api/controllers/clipshare-api/index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || "/opt/";
const Response = require(HELPER_BASE + 'response');

const APIKEY = "【任意のAPIKey】";
const EXPIRES_IN = 60 * 60 * 1000;
var clip = null;

exports.handler = async (event, context, callback) => {
	var body = JSON.parse(event.body);

	if( event.requestContext.apikeyAuth.apikey != APIKEY )
		throw "apikey invalid";

	switch(event.path){
		case "/clipshare-get":{
			if( !clip || new Date().getTime() > clip.created_at + EXPIRES_IN )
				return new Response({status: "ng"});
			return new Response({ status: "ok", clip: clip });
		}
		case "/clipshare-set":{
			clip = { 
				text: body.text,
				created_at: new Date().getTime()
			};
			return new Response({ status: "ok" } );
		}
	}
};
```

※セキュリティ上の配慮はないので、自己責任でお願いします。
※それから、HTTPSで通信するようにしないと、テキストの内容は漏洩します。

#クライアント側

PWA化するために、マニフェストファイルを作成します。

```json:/public/clipshare/manifest.json
{
	"short_name": "クリップシェア",
	"name": "クリップシェア",
	"display": "standalone",
	"start_url": "index.html",
	"icons": [
		{
			"src": "img/192x192.png",
			"sizes": "192x192"
		}
	],
	"shortcuts": [
		{
			"name": "Clip⇒Upload",
			"description": "クリップボードをアップロードします。",
			"url": "/clipshare/index.html?cmd=clip2upload",
			"icons": [
				{
					"src": "img/192x192.png",
					"sizes": "192x192"
				}
			]
		},
		{
			"name": "Download⇒Clip",
			"description": "ダウンロードしてクリップボードにコピーします。",
			"url": "/clipshare/index.html?cmd=download2clip",
			"icons": [
				{
					"src": "img/192x192.png",
					"sizes": "192x192"
				}
			]
		}
	]
}
```

少し補足しますと、「shortcuts」という項目がありますが、これは、PWAとしてインストールした後に、右クリックまたは長押しで、ワンタッチでクリップボード上のテキストをサーバにアップしたり、サーバ上のテキストをクリップボードにコピーするためのショートカットの設定です。ホーム画面にショートカットとしてもおけるのがうれしい。

HTMLで指定します。

```html:/public/clipshare/index.html
  <link rel="manifest" href="manifest.json">
  <link rel="manifest" href="manifest.webmanifest" />
  <script async src="https://cdn.jsdelivr.net/npm/pwacompat" crossorigin="anonymous"></script>
```

また、PWAのために、ServiceWorkerを立ち上げる必要があるため、以下のJSを作成します。

```js:/public/clipshare/sw.js
var CACHE_NAME = 'clipshare-pwa-caches';
self.addEventListener('fetch', function(event) {
  console.log('sw event: fetch called');
});
```

あとは、これを、ページロード後に読み出すようにします。

```js:/public/clipshare/js/start.js
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('sw.js').then(async (registration) => {
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            }).catch((err) => {
                console.log('ServiceWorker registration failed: ', err);
            });
        }
```

PWAについては、以下を参考にしてください。
　[PWAを試してみよう](https://qiita.com/poruruba/items/77cbb6448d804e6e996b)

#クリップボードのコピー/ペースト

以下の通りです。

```js:/public/clipshare/js/start.js
    clip_paste: async function(){
    	return navigator.clipboard.readText();
    },
    clip_copy: async function(text){
    	return navigator.clipboard.writeText(text);
    },
```

async/awaitなので注意です。

参考：Clipboard API
　https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API

#クライアント側ソースコード

Javascriptソースを以下に示します。

```js:/public/clipshare/js/start.js
'use strict';

//const vConsole = new VConsole();
//window.datgui = new dat.GUI();

var vue_options = {
    el: "#top",
    mixins: [mixins_bootstrap],
    data: {
        dialog_input: {},
        base_url: "",
        apikey: "",
        payload: "",
        message: "",
    },
    computed: {
    },
    methods: {
        clipshare_get: async function(){
            try{
                this.message = "";
                var json = await do_post_with_apikey(this.base_url + "/clipshare-get", {}, this.apikey);
                if( json.clip ){
                    this.payload = json.clip.text;
                    await this.clip_copy(this.payload);
                    this.message = "DOWNLOAD created_at " + new Date(json.clip.created_at).toLocaleString( 'ja-JP', {} );
                }else{
                    this.message = "NO Clip Data";
                }
            }catch(error){
                alert(error);
            }
        },
        clipshare_set: async function(){
            try{
                this.message = "";
                var params = {
                    text: this.payload
                };
                await do_post_with_apikey(this.base_url + "/clipshare-set", params, this.apikey);
                this.message = "UPLOAD at " + new Date().toLocaleString( 'ja-JP', {} );
            }catch(error){
                alert(error);
            }
        },
        clipshare_paste: async function(){
            this.payload = await this.clip_paste();
            this.message = "clipboard PASTE";
        },
        clipshare_copy: async function(){
            await this.clip_copy(this.payload);
            this.message = "clipboard COPY";
        },
        clipshare_clear: function(){
            this.message = "";
            this.payload = "";
        },
        set_apikey: function(){
            this.dialog_input = {
                base_url: this.base_url || "",
                apikey: this.apikey || ""
            };
            this.dialog_open('#apikey_dialog');
        },
        apkey_update: function(){
            localStorage.setItem("base_url", this.dialog_input.base_url);
            localStorage.setItem("apikey", this.dialog_input.apikey);
            this.dialog_close('#apikey_dialog');
            alert('設定しました。リロードしてください。');
        }
    },
    created: function(){
    },
    mounted: async function(){
        proc_load();
        
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('sw.js').then(async (registration) => {
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            }).catch((err) => {
                console.log('ServiceWorker registration failed: ', err);
            });
        }

        this.base_url = localStorage.getItem('base_url');
        this.apikey = localStorage.getItem('apikey');
        if( this.apikey ){
            switch(searchs.cmd){
                case 'clip2upload':{
                    this.payload = await this.clip_paste();
                    await this.clipshare_set();
                    alert('クリップボードをアップロードしました。');
                    window.close();
                    break;
                }
                case 'download2clip':{
                    await this.clipshare_get()
                    alert('ダウンロードしてクリップボードにコピーしました。');
                    window.close();
                    break;
                }
                default:{
                    this.clipshare_get();
                    break;
                }
            }
        }else{
           setTimeout( () =>{
                alert('API Keyを指定してください。');
            }, 0);
        }
    }
};
vue_add_data(vue_options, { progress_title: '' }); // for progress-dialog
vue_add_global_components(components_bootstrap);
vue_add_global_components(components_utils);

/* add additional components */
  
window.vue = new Vue( vue_options );

function do_post_with_apikey(url, body, apikey) {
    const headers = new Headers({ "Content-Type": "application/json; charset=utf-8", "X-API-KEY": apikey });
  
    return fetch(url, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: headers
    })
    .then((response) => {
      if (!response.ok)
        throw 'status is not 200';
      return response.json();
  //    return response.text();
  //    return response.blob();
  //    return response.arrayBuffer();
    });
  }
```

#セットアップ

以下から、ZIPでダウンロードします。

https://github.com/poruruba/ClipShare

その後、適当なフォルダで展開して、npm install; node app.js; でOKです。
HTTPSにするには、certフォルダを作ってそこにSSL証明書を置いてください。

クライアント側では最初に立ち上げたサーバのURLとAPIKeyを設定してください。

以上