Implemented context menu
This commit is contained in:
parent
9b562f3a89
commit
b2cb616929
6 changed files with 181 additions and 40 deletions
|
@ -1,3 +1,92 @@
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
global.browser = require('webextension-polyfill');
|
global.browser = require('webextension-polyfill');
|
||||||
|
|
||||||
|
const menu = {
|
||||||
|
hosts: {},
|
||||||
|
actions: {},
|
||||||
|
scripts: {},
|
||||||
|
categories: {},
|
||||||
|
separator: '//',
|
||||||
|
|
||||||
|
categoriesByHost(host) {
|
||||||
|
return Object.entries({ ...(this.actions || {}), ...(this.scripts || {}) }).reduce((obj, [actionName, action]) => {
|
||||||
|
if (action.hosts.indexOf(host) < 0) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendAction = category => {
|
||||||
|
if (!(category in obj)) {
|
||||||
|
obj[category] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
obj[category][actionName] = action;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!(action.categories && action.categories.length)) {
|
||||||
|
appendAction('');
|
||||||
|
} else {
|
||||||
|
action.categories.forEach(category => appendAction(category));
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
},
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
this.hosts = await utils.methods.getHosts();
|
||||||
|
this.actions = await utils.methods.getActions();
|
||||||
|
this.scripts = await utils.methods.getScripts();
|
||||||
|
browser.contextMenus.removeAll();
|
||||||
|
|
||||||
|
for (const [host] of Object.entries(this.hosts)) {
|
||||||
|
const hostId = this.separator + host;
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: hostId,
|
||||||
|
title: host,
|
||||||
|
});
|
||||||
|
|
||||||
|
const categories = this.categoriesByHost(host);
|
||||||
|
for (const [categoryName, category] of Object.entries(categories)) {
|
||||||
|
const categoryId = hostId + this.separator + (categoryName.length ? categoryName : '[NONE]');
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: categoryId,
|
||||||
|
parentId: hostId,
|
||||||
|
title: categoryName.length ? categoryName : '[No Category]',
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [action] of Object.entries(category)) {
|
||||||
|
const actionId = categoryId + this.separator + action;
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: actionId,
|
||||||
|
parentId: categoryId,
|
||||||
|
title: action,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||||
|
const [host, , action] = info.menuItemId.split(this.separator).slice(1);
|
||||||
|
const target = await utils.methods.getTargetElement();
|
||||||
|
|
||||||
|
if (action in this.actions) {
|
||||||
|
utils.methods.run(this.actions[action], this.hosts[host]);
|
||||||
|
} else {
|
||||||
|
utils.methods.runScript(this.scripts[action].script, this.hosts[host], tab, target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async create() {
|
||||||
|
await this.refresh();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreate = () => {
|
||||||
|
menu.create();
|
||||||
|
};
|
||||||
|
|
||||||
|
onCreate();
|
||||||
|
|
||||||
// vim:sw=2:ts=2:et:
|
// vim:sw=2:ts=2:et:
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
global.browser = require('webextension-polyfill');
|
global.browser = require('webextension-polyfill');
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
targetElement: null,
|
||||||
|
};
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'getURL':
|
case 'getURL':
|
||||||
|
@ -13,7 +17,15 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
case 'setDOM':
|
case 'setDOM':
|
||||||
document.getElementsByTagName('html')[0].innerHTML = message.html;
|
document.getElementsByTagName('html')[0].innerHTML = message.html;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'getTargetElement':
|
||||||
|
sendResponse(context.targetElement ? context.targetElement.outerHTML : null);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('contextmenu', event => {
|
||||||
|
context.targetElement = event.target;
|
||||||
|
});
|
||||||
|
|
||||||
// vim:sw=2:ts=2:et:
|
// vim:sw=2:ts=2:et:
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"48": "icons/icon-48.png",
|
"48": "icons/icon-48.png",
|
||||||
"64": "icons/icon-64.png"
|
"64": "icons/icon-64.png"
|
||||||
},
|
},
|
||||||
"permissions": ["activeTab", "storage", "notifications", "clipboardRead", "clipboardWrite", "<all_urls>"],
|
"permissions": ["activeTab", "storage", "notifications", "clipboardRead", "clipboardWrite", "contextMenus", "<all_urls>"],
|
||||||
|
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"default_title": "platypush",
|
"default_title": "platypush",
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="page backup">
|
<div class="page backup">
|
||||||
<h2>Extension Configuration</h2>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<form class="loader" ref="loader" @submit.prevent="loadURL">
|
|
||||||
<div class="head">
|
<div class="head">
|
||||||
|
<h2>Extension Configuration</h2>
|
||||||
|
<form class="loader" ref="loader" @submit.prevent="loadURL">
|
||||||
|
<div class="loader-head">
|
||||||
Load configuration
|
Load configuration
|
||||||
<input type="radio" id="_file" value="file" v-model="extConfigType" />
|
<input type="radio" id="_file" value="file" v-model="extConfigType" />
|
||||||
<label for="_file">From file</label>
|
<label for="_file">From file</label>
|
||||||
|
@ -12,12 +11,13 @@
|
||||||
<label for="_url">From URL</label>
|
<label for="_url">From URL</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="body">
|
<div class="loader-body">
|
||||||
<input type="file" name="file" placeholder="Configuration file" accept="application/json, text/x-json, text/plain" @change="uploadFile" v-if="extConfigType === 'file'" />
|
<input type="file" name="file" placeholder="Configuration file" accept="application/json, text/x-json, text/plain" @change="uploadFile" v-if="extConfigType === 'file'" />
|
||||||
<input type="text" name="url" placeholder="Configuration URL" v-model="extURL" v-if="extConfigType === 'url'" />
|
<input type="text" name="url" placeholder="Configuration URL" v-model="extURL" v-if="extConfigType === 'url'" />
|
||||||
<input type="submit" value="Load" v-if="extConfigType === 'url'" />
|
<input type="submit" value="Load" v-if="extConfigType === 'url'" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form class="content" ref="content" @submit.prevent="save">
|
<form class="content" ref="content" @submit.prevent="save">
|
||||||
<div class="textarea">
|
<div class="textarea">
|
||||||
|
@ -31,7 +31,6 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -131,27 +130,56 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
textarea,
|
$head-height: 10em;
|
||||||
.loader {
|
$buttons-height: 5em;
|
||||||
width: 60%;
|
|
||||||
min-width: 30em;
|
|
||||||
max-width: 50em;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
.page {
|
||||||
height: 50em;
|
height: 100vh;
|
||||||
max-height: 80vh;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 !important;
|
||||||
|
|
||||||
|
.head,
|
||||||
|
.content {
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
height: $head-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: calc(100% - #{$head-height});
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
height: calc(100% - #{$buttons-height});
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
border: 1px dotted #888;
|
||||||
|
border-radius: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
height: $buttons-height;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
|
||||||
.head {
|
.loader-head {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.loader-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -145,7 +145,7 @@ export default {
|
||||||
host: Object,
|
host: Object,
|
||||||
scriptTemplate: {
|
scriptTemplate: {
|
||||||
type: String,
|
type: String,
|
||||||
default: `async (app, host, browser, window) => {
|
default: `async (app, host, browser, tab, target, ...args) => {
|
||||||
// Run some action on the host
|
// Run some action on the host
|
||||||
const status = await app.run({ name: 'music.mpd.pause' }, host);
|
const status = await app.run({ name: 'music.mpd.pause' }, host);
|
||||||
|
|
||||||
|
|
24
src/utils.js
24
src/utils.js
|
@ -46,15 +46,27 @@ export default {
|
||||||
await browser.tabs.sendMessage(tab.id, { type: 'setDOM', html: html });
|
await browser.tabs.sendMessage(tab.id, { type: 'setDOM', html: html });
|
||||||
},
|
},
|
||||||
|
|
||||||
async run(action, host) {
|
async getTargetElement() {
|
||||||
const url = (host.ssl ? 'https' : 'http') + '://' + host.address + ':' + host.port + '/execute';
|
const tab = await this.getCurrentTab();
|
||||||
|
const target = await browser.tabs.sendMessage(tab.id, { type: 'getTargetElement' });
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DOMParser().parseFromString(target, 'text/html').documentElement.getElementsByTagName('body')[0].firstChild;
|
||||||
|
},
|
||||||
|
|
||||||
|
async run(action, host, url) {
|
||||||
|
const execURL = (host.ssl ? 'https' : 'http') + '://' + host.address + ':' + host.port + '/execute';
|
||||||
const config = {};
|
const config = {};
|
||||||
let args = action.args || {};
|
let args = action.args || {};
|
||||||
let currentURL = null;
|
let currentURL = url;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
try {
|
try {
|
||||||
currentURL = await this.getURL();
|
currentURL = await this.getURL();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(action.args)) {
|
if (Array.isArray(action.args)) {
|
||||||
args = action.args
|
args = action.args
|
||||||
|
@ -84,7 +96,7 @@ export default {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const msg = await axios.post(
|
const msg = await axios.post(
|
||||||
url,
|
execURL,
|
||||||
{
|
{
|
||||||
type: 'request',
|
type: 'request',
|
||||||
action: action.name,
|
action: action.name,
|
||||||
|
@ -105,7 +117,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async runScript(script, host) {
|
async runScript(script, host, tab, target, ...args) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -114,7 +126,7 @@ export default {
|
||||||
script = eval(this.script);
|
script = eval(this.script);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await script(this, host, browser, window);
|
return await script(this, host, browser, tab, target, ...args);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.notify(e.message, 'Script error');
|
this.notify(e.message, 'Script error');
|
||||||
throw e;
|
throw e;
|
||||||
|
|
Loading…
Reference in a new issue