plugin creator
This commit is contained in:
parent
7fcc5f0067
commit
6f96c91f68
|
@ -4,3 +4,4 @@ node_modules/
|
|||
bower_components/
|
||||
*.log
|
||||
*.user
|
||||
keys
|
||||
|
|
|
@ -0,0 +1,399 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>KeeWeb Plugin Creation Tool</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, "BlinkMacSystemFont", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;;
|
||||
font-size: 14px;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 12px;
|
||||
}
|
||||
li {
|
||||
margin-left: 2px;
|
||||
}
|
||||
.question {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.question label {
|
||||
display: inline-block;
|
||||
width: 350px;
|
||||
}
|
||||
.question input {
|
||||
width: 400px;
|
||||
height: 20px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.question input[type=color] {
|
||||
width: 40px;
|
||||
padding: 0 2px;
|
||||
height: 30px;
|
||||
}
|
||||
.question select {
|
||||
width: 400px;
|
||||
height: 20px;
|
||||
outline: none;
|
||||
}
|
||||
.question input {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.question input:focus {
|
||||
outline: none;
|
||||
border: 1px solid #528BFF;
|
||||
box-shadow: 0 0 3px rgba(82, 139, 255, .7);
|
||||
}
|
||||
.question input:invalid {
|
||||
border: 1px solid #9a0000;
|
||||
box-shadow: 0 0 3px rgba(255, 0, 0, .2);
|
||||
}
|
||||
.question input:invalid:focus {
|
||||
box-shadow: 0 0 3px rgba(255, 0, 0, .7);
|
||||
}
|
||||
.question[data-type] {
|
||||
display: none;
|
||||
}
|
||||
#btn-download {
|
||||
border: 1px solid #528BFF;
|
||||
background: rgba(82, 139, 255, .03);
|
||||
border-radius: 1px;
|
||||
padding: 8px 20px;
|
||||
outline: none;
|
||||
color: #528BFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
#btn-download:hover {
|
||||
background: rgba(82, 139, 255, .05);
|
||||
}
|
||||
#btn-download:active {
|
||||
background: rgba(82, 139, 255, .2);
|
||||
}
|
||||
.theme {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Roboto,Arial,sans-serif;
|
||||
}
|
||||
.theme .error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.theme button {
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: .75em 1.5em;
|
||||
background: transparent;
|
||||
margin-left: 12px;
|
||||
border-radius: 1px;
|
||||
transition: all 100ms linear;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.theme button[type=reset] {
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--medium-color);
|
||||
}
|
||||
.theme button[type=submit] {
|
||||
color: var(--text-color);
|
||||
background: var(--action-color);
|
||||
border: 1px solid var(--action-color);
|
||||
}
|
||||
.theme ul {
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
}
|
||||
.theme li {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border-top: 1px solid transparent;
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
}
|
||||
.theme li.selected {
|
||||
border-top: 2px solid var(--action-color);
|
||||
}
|
||||
.theme li:not(.selected):hover {
|
||||
border-top: 1px solid var(--action-color);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const manifest = {
|
||||
version: '0.0.1',
|
||||
manifestVersion: '0.1.0',
|
||||
name: '',
|
||||
description: '',
|
||||
author: {
|
||||
name: '',
|
||||
email: '',
|
||||
url: ''
|
||||
},
|
||||
resources: {},
|
||||
licence: '',
|
||||
url: '',
|
||||
publicKey: ''
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const selType = document.querySelector('#sel-type');
|
||||
selType.addEventListener('change', e => {
|
||||
setPluginType(e.target.value);
|
||||
});
|
||||
setPluginType(selType.value);
|
||||
|
||||
for (const el of document.querySelectorAll('.question input')) {
|
||||
el.addEventListener('input', buildManifest);
|
||||
}
|
||||
for (const el of document.querySelectorAll('input[type=color]')) {
|
||||
el.addEventListener('change', e => applyColor(e.target));
|
||||
applyColor(el);
|
||||
}
|
||||
|
||||
document.querySelector('#btn-download').addEventListener('click', downloadPlugin);
|
||||
});
|
||||
|
||||
function setPluginType(type) {
|
||||
delete manifest.loc;
|
||||
delete manifest.theme;
|
||||
switch (type) {
|
||||
case 'js':
|
||||
manifest.resources = { js: true };
|
||||
break;
|
||||
case 'css':
|
||||
manifest.resources = { css: true };
|
||||
break;
|
||||
case 'js+css':
|
||||
manifest.resources = { js: true, css: true };
|
||||
break;
|
||||
case 'theme':
|
||||
manifest.resources = { css: true };
|
||||
break;
|
||||
case 'loc':
|
||||
manifest.resources = { loc: true };
|
||||
break;
|
||||
}
|
||||
showResFields(type);
|
||||
buildManifest();
|
||||
}
|
||||
|
||||
function showResFields(type) {
|
||||
for (const el of document.querySelectorAll('.question[data-type]')) {
|
||||
const enabled = el.dataset.type.indexOf(type) >= 0;
|
||||
el.style.display = enabled ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function applyColor(el) {
|
||||
const name = '--' + el.id.replace('color-', '') + '-color';
|
||||
const value = el.value;
|
||||
document.querySelector('.theme').style.setProperty(name, value);
|
||||
}
|
||||
|
||||
function buildManifest() {
|
||||
for (const el of document.querySelectorAll('.question input')) {
|
||||
if (!el.dataset.field) {
|
||||
return;
|
||||
}
|
||||
const field = el.dataset.field.split('.');
|
||||
const visible = !!el.offsetParent;
|
||||
if (visible) {
|
||||
if (field.length === 2) {
|
||||
const [section, item] = field;
|
||||
if (!manifest[section]) {
|
||||
manifest[section] = {};
|
||||
}
|
||||
manifest[section][item] = el.value;
|
||||
} else {
|
||||
manifest[field[0]] = el.value || el.placeholder;
|
||||
}
|
||||
}
|
||||
}
|
||||
document.querySelector('#manifest').innerHTML = JSON.stringify(manifest, null, 2);
|
||||
}
|
||||
|
||||
function downloadPlugin() {
|
||||
for (const el of document.querySelectorAll('input:invalid')) {
|
||||
if (el.offsetParent) {
|
||||
alert('There are some invalid fields, please fix them first');
|
||||
el.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const button = document.querySelector('#btn-download');
|
||||
button.innerHTML = 'Generating keys...';
|
||||
setTimeout(() => {
|
||||
generateKeyPair().then(keys => {
|
||||
button.innerHTML = 'Building your plugin...';
|
||||
manifest.publicKey = keys.publicKey;
|
||||
buildManifest();
|
||||
const zip = new JSZip();
|
||||
zip.file('manifest.json', JSON.stringify(manifest, null, 2));
|
||||
zip.file('.gitignore', ['.DS_Store', '*.log', '*.pem'].join('\n'));
|
||||
if (manifest.resources.js) {
|
||||
zip.file('plugin.js', getPluginJs());
|
||||
}
|
||||
if (manifest.resources.css) {
|
||||
zip.file('plugin.css', getPluginCss());
|
||||
}
|
||||
if (manifest.resources.loc) {
|
||||
zip.file(manifest.locale.name + '.json', '{\n}');
|
||||
}
|
||||
zip.file('private_key.pem', keys.privateKey);
|
||||
zip.generateAsync({type: 'blob'}).then(content => {
|
||||
saveAs(content, manifest.name + '.zip');
|
||||
button.innerHTML = 'Download your plugin';
|
||||
});
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
button.innerHTML = 'Download your plugin';
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function getPluginJs() {
|
||||
return `/**
|
||||
* KeeWeb plugin: ${manifest.name}
|
||||
* @author ${manifest.author.name}
|
||||
* @license ${manifest.license}
|
||||
*/
|
||||
|
||||
module.exports.uninstall = function() {
|
||||
};`;
|
||||
}
|
||||
|
||||
function getPluginCss() {
|
||||
if (!manifest.theme) {
|
||||
return '';
|
||||
}
|
||||
let css = `.th-${manifest.theme.name} {\n`;
|
||||
for (const el of document.querySelectorAll('input[type=color]')) {
|
||||
css += el.id.replace('color-', ' --') + 'color: ' + el.value + '\n';
|
||||
}
|
||||
css += '}';
|
||||
return css;
|
||||
}
|
||||
|
||||
function generateKeyPair() {
|
||||
return new Promise((resolve, reject) => {
|
||||
crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSASSA-PKCS1-v1_5',
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||
hash: {name: 'SHA-256'},
|
||||
},
|
||||
true,
|
||||
['sign', 'verify']
|
||||
).then(keys => {
|
||||
crypto.subtle.exportKey('pkcs8', keys.privateKey).then(privateKey => {
|
||||
privateKey = '-----BEGIN PRIVATE KEY-----\n' +
|
||||
btoa(String.fromCharCode(...new Uint8Array(privateKey))) +
|
||||
'\n-----END PRIVATE KEY-----';
|
||||
crypto.subtle.exportKey('spki', keys.publicKey).then(publicKey => {
|
||||
publicKey = btoa(String.fromCharCode(...new Uint8Array(publicKey)));
|
||||
resolve({ privateKey, publicKey });
|
||||
});
|
||||
}).catch(reject);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
function fillTestData() {
|
||||
document.getElementById('txt-name').value = 'my-plugin';
|
||||
document.getElementById('txt-desc').value = 'My plugin';
|
||||
document.getElementById('txt-author-name').value = 'me';
|
||||
document.getElementById('txt-author-email').value = 'me@example.com';
|
||||
document.getElementById('txt-author-url').value = 'http://example.com';
|
||||
document.getElementById('txt-plugin-url').value = 'http://example.com/plugin';
|
||||
document.getElementById('txt-theme-name').value = 'my-theme';
|
||||
document.getElementById('txt-theme-title').value = 'My theme';
|
||||
buildManifest();
|
||||
}
|
||||
</script>
|
||||
<script src="lib/jszip.min.js" async></script>
|
||||
<script src="lib/FileSaver.min.js" async></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>KeeWeb Plugin Creation Tool</h1>
|
||||
|
||||
<div class="question">
|
||||
<label for="sel-type">Plugin type:</label>
|
||||
<select id="sel-type" required>
|
||||
<option value="js" selected>JS code</option>
|
||||
<option value="js+css">JS code + CSS styles</option>
|
||||
<option value="css">CSS styles (not themes)</option>
|
||||
<option value="loc">Locale</option>
|
||||
<option value="theme">CSS Theme</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="question">
|
||||
<label for="txt-name">Plugin name (lowercase letters and dashes):</label>
|
||||
<input type="text" data-field="name" id="txt-name" autocomplete="none" pattern="\w[\w\-]+\w" required />
|
||||
</div>
|
||||
<div class="question">
|
||||
<label for="txt-desc">Description (human-readable):</label>
|
||||
<input type="text" data-field="description" id="txt-desc" autocomplete="none" required />
|
||||
</div>
|
||||
<div class="question">
|
||||
<label for="txt-author-name">Author name:</label>
|
||||
<input type="text" data-field="author.name" id="txt-author-name" autocomplete="none" required />
|
||||
</div>
|
||||
<div class="question">
|
||||
<label for="txt-author-email">Author email:</label>
|
||||
<input type="email" data-field="author.email" id="txt-author-email" required />
|
||||
</div>
|
||||
<div class="question">
|
||||
<label for="txt-author-url">Author URL:</label>
|
||||
<input type="text" data-field="author.url" id="txt-author-url" autocomplete="none" pattern="http(s?)://.+" required />
|
||||
</div>
|
||||
<div class="question">
|
||||
<label for="txt-plugin-url">Plugin page URL:</label>
|
||||
<input type="text" data-field="url" id="txt-plugin-url" autocomplete="none" pattern="http(s?)://.+" required />
|
||||
</div>
|
||||
<div class="question">
|
||||
<label for="txt-license">License:</label>
|
||||
<input type="text" data-field="license" id="txt-license" placeholder="MIT" autocomplete="none" />
|
||||
</div>
|
||||
<div class="question" data-type="loc">
|
||||
<label for="txt-loc-code">Locale code (xx or xx-XX):</label>
|
||||
<input type="text" data-field="locale.name" id="txt-loc-code" autocomplete="none" pattern="[a-z]{2}(-[A-Z]{2})?" required />
|
||||
</div>
|
||||
<div class="question" data-type="loc">
|
||||
<label for="txt-loc-title">Locale name (human-readable):</label>
|
||||
<input type="text" data-field="locale.title" id="txt-loc-title" autocomplete="none" required />
|
||||
</div>
|
||||
<div class="question" data-type="theme">
|
||||
<label for="txt-theme-name">Theme class (lowercase letters and dashes):</label>
|
||||
<input type="text" data-field="theme.name" id="txt-theme-name" autocomplete="none" pattern="\w[\w\-]+\w" required />
|
||||
</div>
|
||||
<div class="question" data-type="theme">
|
||||
<label for="txt-theme-title">Theme name (human-readable):</label>
|
||||
<input type="text" data-field="theme.title" id="txt-theme-title" autocomplete="none" required />
|
||||
</div>
|
||||
<div class="question" data-type="theme">
|
||||
<p>Theme colors:</p>
|
||||
<input type="color" id="color-background" value="#282C34">
|
||||
<input type="color" id="color-medium" value="#ABB2BF">
|
||||
<input type="color" id="color-text" value="#D7DAE0">
|
||||
<input type="color" id="color-action" value="#528BFF">
|
||||
<input type="color" id="color-error" value="#C34034">
|
||||
<div class="theme">
|
||||
Normal text and <span class="error">error</span>
|
||||
<button type="submit">Button</button>
|
||||
<button type="reset">Hollow button</button>
|
||||
<ul>
|
||||
<li>Here's a menu</li>
|
||||
<li class="selected">I'm selected</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="btn-download">Download your plugin</button>
|
||||
|
||||
<h2>Your plugin manifest</h2>
|
||||
<pre id="manifest"></pre>
|
||||
</body>
|
||||
</html>
|
|
@ -3,9 +3,16 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>KeeWeb plugins</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, "BlinkMacSystemFont", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>KeeWeb plugins</h1>
|
||||
<p>This site contains known plugins for <a href="https://keeweb.info">keeweb.info</a>.</p>
|
||||
<p>Want to create your own plugin? <a href="create-plugin.html">Here's a plugin creation tool page</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
|
||||
var saveAs=saveAs||function(e){"use strict";if(typeof e==="undefined"||typeof navigator!=="undefined"&&/MSIE [1-9]\./.test(navigator.userAgent)){return}var t=e.document,n=function(){return e.URL||e.webkitURL||e},r=t.createElementNS("http://www.w3.org/1999/xhtml","a"),o="download"in r,a=function(e){var t=new MouseEvent("click");e.dispatchEvent(t)},i=/constructor/i.test(e.HTMLElement)||e.safari,f=/CriOS\/[\d]+/.test(navigator.userAgent),u=function(t){(e.setImmediate||e.setTimeout)(function(){throw t},0)},s="application/octet-stream",d=1e3*40,c=function(e){var t=function(){if(typeof e==="string"){n().revokeObjectURL(e)}else{e.remove()}};setTimeout(t,d)},l=function(e,t,n){t=[].concat(t);var r=t.length;while(r--){var o=e["on"+t[r]];if(typeof o==="function"){try{o.call(e,n||e)}catch(a){u(a)}}}},p=function(e){if(/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)){return new Blob([String.fromCharCode(65279),e],{type:e.type})}return e},v=function(t,u,d){if(!d){t=p(t)}var v=this,w=t.type,m=w===s,y,h=function(){l(v,"writestart progress write writeend".split(" "))},S=function(){if((f||m&&i)&&e.FileReader){var r=new FileReader;r.onloadend=function(){var t=f?r.result:r.result.replace(/^data:[^;]*;/,"data:attachment/file;");var n=e.open(t,"_blank");if(!n)e.location.href=t;t=undefined;v.readyState=v.DONE;h()};r.readAsDataURL(t);v.readyState=v.INIT;return}if(!y){y=n().createObjectURL(t)}if(m){e.location.href=y}else{var o=e.open(y,"_blank");if(!o){e.location.href=y}}v.readyState=v.DONE;h();c(y)};v.readyState=v.INIT;if(o){y=n().createObjectURL(t);setTimeout(function(){r.href=y;r.download=u;a(r);h();c(y);v.readyState=v.DONE});return}S()},w=v.prototype,m=function(e,t,n){return new v(e,t||e.name||"download",n)};if(typeof navigator!=="undefined"&&navigator.msSaveOrOpenBlob){return function(e,t,n){t=t||e.name||"download";if(!n){e=p(e)}return navigator.msSaveOrOpenBlob(e,t)}}w.abort=function(){};w.readyState=w.INIT=0;w.WRITING=1;w.DONE=2;w.error=w.onwritestart=w.onprogress=w.onwrite=w.onabort=w.onerror=w.onwriteend=null;return m}(typeof self!=="undefined"&&self||typeof window!=="undefined"&&window||this.content);if(typeof module!=="undefined"&&module.exports){module.exports.saveAs=saveAs}else if(typeof define!=="undefined"&&define!==null&&define.amd!==null){define("FileSaver.js",function(){return saveAs})}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue