LoginSignup
1
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-02-23

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

image.png

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

poruruba/ClipShare

テキストの共有方法

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

/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化するために、マニフェストファイルを作成します。

/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で指定します。

/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を作成します。

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

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

/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を試してみよう

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

以下の通りです。

/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ソースを以下に示します。

/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でダウンロードします。

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

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

以上

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0