Last active
April 22, 2024 16:16
-
-
Save tam710562/d6b4f846f86fedf179d78747b314cd8a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Import Export Command Chains | |
* Written by Tam710562 | |
*/ | |
(async () => { | |
'use strict'; | |
const gnoh = { | |
i18n: { | |
getMessageName(message, type) { | |
message = (type ? type + '\x04' : '') + message; | |
return message.replace(/[^a-z0-9]/g, function (i) { | |
return '_' + i.codePointAt(0) + '_'; | |
}) + '0'; | |
}, | |
getMessage(message, type) { | |
return chrome.i18n.getMessage(this.getMessageName(message, type)) || message; | |
}, | |
}, | |
uuid: { | |
check(id) { | |
return !/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(id); | |
}, | |
generate(ids) { | |
let d = Date.now() + performance.now(); | |
let r; | |
const id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { | |
r = (d + Math.random() * 16) % 16 | 0; | |
d = Math.floor(d / 16); | |
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); | |
}); | |
if (Array.isArray(ids) && ids.includes(id)) { | |
return this.uuid.generate(ids); | |
} | |
return id; | |
}, | |
}, | |
object: { | |
isObject(item) { | |
return (item && typeof item === 'object' && !Array.isArray(item)); | |
}, | |
merge(target, source) { | |
let output = Object.assign({}, target); | |
if (this.isObject(target) && this.isObject(source)) { | |
Object.keys(source).forEach(key => { | |
if (this.isObject(source[key])) { | |
if (!(key in target)) | |
Object.assign(output, { [key]: source[key] }); | |
else | |
output[key] = this.merge(target[key], source[key]); | |
} else { | |
Object.assign(output, { [key]: source[key] }); | |
} | |
}); | |
} | |
return output; | |
}, | |
}, | |
addStyle(css, id, isNotMin) { | |
this.styles = this.styles || {}; | |
if (Array.isArray(css)) { | |
css = css.join(isNotMin === true ? '\n' : ''); | |
} | |
id = id || this.uuid.generate(Object.keys(this.styles)); | |
this.styles[id] = this.createElement('style', { | |
html: css || '', | |
'data-id': id, | |
}, document.head); | |
return this.styles[id]; | |
}, | |
createElement(tagName, attribute, parent, inner, options) { | |
if (typeof tagName === 'undefined') { | |
return; | |
} | |
if (typeof options === 'undefined') { | |
options = {}; | |
} | |
if (typeof options.isPrepend === 'undefined') { | |
options.isPrepend = false; | |
} | |
const el = document.createElement(tagName); | |
if (!!attribute && typeof attribute === 'object') { | |
for (const key in attribute) { | |
if (key === 'text') { | |
el.textContent = attribute[key]; | |
} else if (key === 'html') { | |
el.innerHTML = attribute[key]; | |
} else if (key === 'style' && typeof attribute[key] === 'object') { | |
for (const css in attribute.style) { | |
el.style.setProperty(css, attribute.style[css]); | |
} | |
} else if (key === 'events' && typeof attribute[key] === 'object') { | |
for (const event in attribute.events) { | |
if (typeof attribute.events[event] === 'function') { | |
el.addEventListener(event, attribute.events[event]); | |
} | |
} | |
} else if (typeof el[key] !== 'undefined') { | |
el[key] = attribute[key]; | |
} else { | |
if (typeof attribute[key] === 'object') { | |
attribute[key] = JSON.stringify(attribute[key]); | |
} | |
el.setAttribute(key, attribute[key]); | |
} | |
} | |
} | |
if (!!inner) { | |
if (!Array.isArray(inner)) { | |
inner = [inner]; | |
} | |
for (let i = 0; i < inner.length; i++) { | |
if (inner[i].nodeName) { | |
el.append(inner[i]); | |
} else { | |
el.append(this.createElementFromHTML(inner[i])); | |
} | |
} | |
} | |
if (typeof parent === 'string') { | |
parent = document.querySelector(parent); | |
} | |
if (!!parent) { | |
if (options.isPrepend) { | |
parent.prepend(el); | |
} else { | |
parent.append(el); | |
} | |
} | |
return el; | |
}, | |
createElementFromHTML(html) { | |
return this.createElement('template', { | |
html: (html || '').trim(), | |
}).content; | |
}, | |
get constant() { | |
return { | |
dialogButtons: { | |
submit: { | |
label: this.i18n.getMessage('OK'), | |
type: 'submit' | |
}, | |
cancel: { | |
label: this.i18n.getMessage('Cancel'), | |
cancel: true | |
}, | |
primary: { | |
class: 'primary' | |
}, | |
danger: { | |
class: 'danger' | |
}, | |
default: {}, | |
} | |
}; | |
}, | |
getFormData(formElement) { | |
if (!formElement || formElement.nodeName !== 'FORM') { | |
return; | |
} | |
const data = {}; | |
function setOrPush(key, value, isOnly) { | |
if (data.hasOwnProperty(key) && isOnly !== true) { | |
if (!Array.isArray(data[key])) { | |
data[key] = data[key] != null ? [data[key]] : []; | |
} | |
if (value != null) { | |
data[key].push(value); | |
} | |
} else { | |
data[key] = value; | |
} | |
} | |
const inputElements = Array.from(formElement.elements).filter(function (field) { | |
if (field.name) { | |
switch (field.nodeName) { | |
case 'INPUT': | |
switch (field.type) { | |
case 'button': | |
case 'image': | |
case 'reset': | |
case 'submit': | |
return false; | |
} | |
break; | |
case 'BUTTON': | |
return false; | |
} | |
return true; | |
} else { | |
return false; | |
} | |
}); | |
if (inputElements.length === 0) { | |
return; | |
} | |
inputElements.forEach(function (field) { | |
if (field.name) { | |
switch (field.nodeName) { | |
case 'INPUT': | |
switch (field.type) { | |
case 'color': | |
case 'email': | |
case 'hidden': | |
case 'password': | |
case 'search': | |
case 'tel': | |
case 'text': | |
case 'time': | |
case 'url': | |
case 'month': | |
case 'week': | |
setOrPush(field.name, field.value); | |
break; | |
case 'checkbox': | |
if (field.checked) { | |
setOrPush(field.name, field.value || field.checked); | |
} else { | |
setOrPush(field.name, null); | |
} | |
break; | |
case 'radio': | |
if (field.checked) { | |
setOrPush(field.name, field.value || field.checked, true); | |
} else { | |
setOrPush(field.name, null, true); | |
} | |
break; | |
case 'date': | |
case 'datetime-local': | |
const date = new Date(field.value); | |
if (isFinite(date)) { | |
date.setTime(d.getTime() + d.getTimezoneOffset() * 60000); | |
setOrPush(field.name, date); | |
} else { | |
setOrPush(field.name, null); | |
} | |
break; | |
case 'file': | |
setOrPush(field.name, field.files); | |
break; | |
case 'number': | |
case 'range': | |
if (field.value && isFinite(Number(field.value))) { | |
setOrPush(field.name, Number(field.value)); | |
} else { | |
setOrPush(field.name, null); | |
} | |
break; | |
} | |
break; | |
case 'TEXTAREA': | |
setOrPush(field.name, field.value); | |
break; | |
case 'SELECT': | |
switch (field.type) { | |
case 'select-one': | |
setOrPush(field.name, field.value); | |
break; | |
case 'select-multiple': | |
Array.from(field.options).forEach(function (option) { | |
if (option.selected) { | |
setOrPush(field.name, option.value); | |
} else { | |
setOrPush(field.name, null); | |
} | |
}); | |
break; | |
} | |
break; | |
case 'BUTTON': | |
break; | |
} | |
} | |
}); | |
return data; | |
}, | |
dialog(title, content, buttons = [], config) { | |
let modalBg; | |
let dialog; | |
let cancelEvent; | |
const id = this.uuid.generate(); | |
const inner = document.querySelector('#main > .inner, #main > .webpageview'); | |
if (!config) { | |
config = {}; | |
} | |
if (typeof config.autoClose === 'undefined') { | |
config.autoClose = true; | |
} | |
function onKeyCloseDialog(windowId, key) { | |
if ( | |
windowId === vivaldiWindowId | |
&& key === 'Esc' | |
) { | |
closeDialog(true); | |
} | |
} | |
function onClickCloseDialog(event) { | |
if ( | |
config.autoClose | |
&& !event.target.closest('.dialog-custom[data-dialog-id="' + id + '"]') | |
) { | |
closeDialog(true); | |
} | |
} | |
function closeDialog(isCancel) { | |
if (isCancel === true && cancelEvent) { | |
cancelEvent.bind(this)(gnoh.getFormData(dialog)); | |
} | |
if (modalBg) { | |
modalBg.remove(); | |
} | |
vivaldi.tabsPrivate.onKeyboardShortcut.removeListener(onKeyCloseDialog); | |
document.removeEventListener('mousedown', onClickCloseDialog); | |
} | |
vivaldi.tabsPrivate.onKeyboardShortcut.addListener(onKeyCloseDialog); | |
document.addEventListener('mousedown', onClickCloseDialog); | |
const buttonElements = []; | |
for (let button of buttons) { | |
button.type = button.type || 'button'; | |
const clickEvent = button.click; | |
if (button.cancel === true && typeof clickEvent === 'function') { | |
cancelEvent = clickEvent; | |
} | |
button.events = { | |
click(event) { | |
event.preventDefault(); | |
if (typeof clickEvent === 'function') { | |
clickEvent.bind(this)(gnoh.getFormData(dialog)); | |
} | |
if (button.closeDialog !== false) { | |
closeDialog(); | |
} | |
} | |
}; | |
delete button.click; | |
if (button.label) { | |
button.value = button.label; | |
delete button.label; | |
} | |
buttonElements.push(this.createElement('input', button)); | |
} | |
const focusModal = this.createElement('span', { | |
class: 'focus_modal', | |
tabindex: '0', | |
}); | |
const div = this.createElement('div', { | |
style: { | |
width: config.width ? config.width + 'px' : '', | |
margin: '0 auto', | |
} | |
}); | |
dialog = this.createElement('form', { | |
'data-dialog-id': id, | |
class: 'dialog-custom modal-wrapper', | |
}, div); | |
if (config.class) { | |
dialog.classList.add(config.class); | |
} | |
const dialogHeader = this.createElement('header', { | |
class: 'dialog-header', | |
}, dialog, '<h1>' + (title || '') + '</h1>'); | |
const dialogContent = this.createElement('div', { | |
class: 'dialog-content', | |
style: { | |
maxHeight: '65vh', | |
}, | |
}, dialog, content); | |
if (buttons && buttons.length > 0) { | |
const dialogFooter = this.createElement('footer', { | |
class: 'dialog-footer', | |
}, dialog, buttonElements); | |
} | |
modalBg = this.createElement('div', { | |
id: 'modal-bg', | |
class: 'slide', | |
}, inner, [focusModal.cloneNode(true), div, focusModal.cloneNode(true)]); | |
return { | |
dialog: dialog, | |
dialogHeader: dialogHeader, | |
dialogContent: dialogContent, | |
buttons: buttonElements, | |
close: closeDialog, | |
}; | |
}, | |
alert(message, okEvent) { | |
const buttonOkElement = Object.assign({}, this.constant.dialogButtons.submit, { | |
cancel: true, | |
}); | |
if (typeof okEvent === 'function') { | |
buttonOkElement.click = function (data) { | |
okEvent.bind(this)(data); | |
}; | |
} | |
return this.dialog('Gnoh', message, [buttonOkElement], { | |
width: 400, | |
class: 'dialog-javascript', | |
}); | |
}, | |
timeOut(callback, conditon, timeOut = 300) { | |
let timeOutId = setTimeout(function wait() { | |
let result; | |
if (!conditon) { | |
result = document.getElementById('browser'); | |
} else if (typeof conditon === 'string') { | |
result = document.querySelector(conditon); | |
} else if (typeof conditon === 'function') { | |
result = conditon(); | |
} else { | |
return; | |
} | |
if (result) { | |
callback(result); | |
} else { | |
timeOutId = setTimeout(wait, timeOut); | |
} | |
}, timeOut); | |
function stop() { | |
if (timeOutId) { | |
clearTimeout(timeOutId); | |
} | |
} | |
return { | |
stop, | |
}; | |
}, | |
element: { | |
appendAtIndex(element, parentElement, index) { | |
if (index >= parentElement.children.length) { | |
parentElement.append(element) | |
} else { | |
parentElement.insertBefore(element, parentElement.children[index]) | |
} | |
}, | |
getIndex(element) { | |
return Array.from(element.parentElement.children).indexOf(element); | |
}, | |
}, | |
}; | |
const messageType = 'import-export-command-chains'; | |
let timeOut; | |
const urls = { | |
quickCommands: 'chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path=qc', | |
general: 'chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path=general', | |
}; | |
const icons = { | |
import: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 12H4V17H20V12H22V17C22 18.11 21.11 19 20 19H4C2.9 19 2 18.11 2 17V12M12 15L17.55 9.54L16.13 8.13L13 11.25V2H11V11.25L7.88 8.13L6.46 9.55L12 15Z" /></svg>', | |
export: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 12H4V17H20V12H22V17C22 18.11 21.11 19 20 19H4C2.9 19 2 18.11 2 17V12M12 2L6.46 7.46L7.88 8.88L11 5.75V15H13V5.75L16.13 8.88L17.55 7.45L12 2Z" /></svg>', | |
}; | |
const langs = { | |
general: gnoh.i18n.getMessage('General'), | |
quickCommands: gnoh.i18n.getMessage('Quick Commands'), | |
copy: gnoh.i18n.getMessage('Copy', 'verb'), | |
export: gnoh.i18n.getMessage('Export', 'verb'), | |
import: gnoh.i18n.getMessage('Import'), | |
install: gnoh.i18n.getMessage('Install'), | |
installed: gnoh.i18n.getMessage('Installed'), | |
update: gnoh.i18n.getMessage('Update'), | |
preview: gnoh.i18n.getMessage('Preview'), | |
commandParameter: gnoh.i18n.getMessage('Command Parameter', 'chainedcommand'), | |
}; | |
gnoh.addStyle([ | |
'.import-export-command-chains input[type="file"]::file-selector-button { border: 0px; border-right: 1px solid var(--colorBorder); height: 28px; padding: 0 18px; color: var(--colorFg); background: linear-gradient(var(--colorBgLightIntense) 0%, var(--colorBg) 100%); margin-right: 18px; }', | |
'.import-export-command-chains input[type="file"]::file-selector-button:hover { background: linear-gradient(var(--colorBg), var(--colorBg)); }', | |
'.import-export-command-chains .editor { width: 100%; height: 300px; overflow: auto; white-space: pre-wrap; word-break: break-word; background-color: var(--colorBgIntense); color: var(--colorFg); user-select: text; border-radius: var(--radius); border: 1px solid var(--colorBorder); font-size: 13px; font-family: monospace; line-height: 1.3; tab-size: 2; padding: 6px; }', | |
'.import-export-command-chains .editor::highlight(json-key) { color: #0451a5; }', | |
'.import-export-command-chains .editor::highlight(json-number) { color: #098658; }', | |
'.import-export-command-chains .editor::highlight(json-bool) { color: #0000ff; }', | |
'.import-export-command-chains .editor::highlight(json-string) { color: #a31515; }', | |
'.theme-dark .import-export-command-chains .editor::highlight(json-key) { color: #9cdcfe; }', | |
'.theme-dark .import-export-command-chains .editor::highlight(json-number) { color: #b5cea8; }', | |
'.theme-dark .import-export-command-chains .editor::highlight(json-bool) { color: #569cd6; }', | |
'.theme-dark .import-export-command-chains .editor::highlight(json-string) { color: #ce9178; }', | |
'.import-export-command-chains .export.master-detail { max-height: 335px; height: auto; }', | |
'.import-export-command-chains .chained-command-item-value { background-color: var(--colorBgIntense); padding: 6px 12px; white-space: nowrap; overflow: auto; scrollbar-width: none; user-select: text; }', | |
], 'import-export-command-chains'); | |
const buttons = { | |
import: { | |
icon: icons.import, | |
title: langs.import, | |
click(key) { | |
showDialogImport(); | |
}, | |
index: 2, | |
}, | |
export: { | |
icon: icons.export, | |
title: langs.export, | |
click(key) { | |
showDialogExport(key); | |
}, | |
index: 3, | |
}, | |
}; | |
const commands = await getCommands(); | |
async function getCommands() { | |
const response = await fetch(chrome.runtime.getURL('bundle.js')); | |
const bundleScript = await response.text(); | |
const matches = Array.from(bundleScript.matchAll(/name\s*:\s*"([^"]+)",[\s\S]+?guid\s*:\s*"([^"]+)",[\s\S]+?\("(([^"]+)"\s*,\s*")?([^"]+)"\)/g)); | |
const commands = {}; | |
matches.forEach(match => { | |
commands[match[2]] = { | |
name: match[1], | |
key: match[2], | |
message: match[5], | |
messageType: match[4], | |
label: gnoh.i18n.getMessage(match[5], match[4]), | |
} | |
}); | |
return commands; | |
} | |
function highlightJson(element, text) { | |
CSS.highlights.delete('json-number'); | |
CSS.highlights.delete('json-bool'); | |
CSS.highlights.delete('json-key'); | |
CSS.highlights.delete('json-string'); | |
if (!text) { | |
return; | |
} | |
const jsonNumbers = [...text.matchAll(/-?\d+\.?\d*((E|e)[\+]\d+)?/ig)].map((match) => { | |
const range = new Range(); | |
range.setStart(element.firstChild, match.index); | |
range.setEnd(element.firstChild, match.index + match[0].length); | |
return range; | |
}); | |
const jsonNumbersHighlight = new Highlight(...jsonNumbers); | |
CSS.highlights.set('json-number', jsonNumbersHighlight); | |
const jsonBooleans = [...text.matchAll(/false|true|null/ig)].map((match) => { | |
const range = new Range(); | |
range.setStart(element.firstChild, match.index); | |
range.setEnd(element.firstChild, match.index + match[0].length); | |
return range; | |
}); | |
const jsonBooleansHighlight = new Highlight(...jsonBooleans); | |
CSS.highlights.set('json-bool', jsonBooleansHighlight); | |
const jsonKeys = []; | |
const jsonStrings = []; | |
[...text.matchAll(/(("([^"]|\\")+?[^\\]")|"")(\s*.)/ig)].forEach((match) => { | |
const range = new Range(); | |
range.setStart(element.firstChild, match.index); | |
range.setEnd(element.firstChild, match.index + match[1].length); | |
if (match[4].trim() === ':') { | |
jsonKeys.push(range); | |
} else { | |
jsonStrings.push(range); | |
} | |
}); | |
const jsonKeysHighlight = new Highlight(...jsonKeys); | |
CSS.highlights.set('json-key', jsonKeysHighlight); | |
const jsonStringsHighlight = new Highlight(...jsonStrings); | |
CSS.highlights.set('json-string', jsonStringsHighlight); | |
} | |
function createEditor(attribute = {}, parent, inner, options) { | |
const editor = gnoh.createElement('div', gnoh.object.merge({ | |
class: 'editor', | |
contentEditable: attribute.contentEditable === false ? false : 'plaintext-only', | |
events: { | |
input() { | |
setValue(this.textContent); | |
}, | |
}, | |
}, attribute), parent, inner, options); | |
function setValue(value) { | |
editor.textContent = value; | |
highlightJson(editor, value); | |
} | |
setValue(attribute.value || ''); | |
return { | |
editor, | |
setValue, | |
}; | |
} | |
async function parseTextFile(file) { | |
return new Promise((resolve, reject) => { | |
const fileReader = new FileReader(); | |
fileReader.onload = event => resolve(event.target.result); | |
fileReader.onerror = error => reject(error); | |
fileReader.readAsText(file); | |
}) | |
} | |
async function showDialogImport(commandChainText) { | |
const p1 = gnoh.createElement('p', { | |
class: 'info', | |
text: 'Import from code', | |
}); | |
const editor = createEditor(); | |
let p2 = null; | |
let inputFile = null; | |
const content = [p1, editor.editor]; | |
if (commandChainText) { | |
editor.editor.contentEditable = false; | |
editor.setValue(commandChainText); | |
} else { | |
p2 = gnoh.createElement('p', { | |
class: 'info', | |
text: 'Import from file', | |
}); | |
inputFile = gnoh.createElement('input', { | |
name: 'file', | |
type: 'file', | |
accept: 'application/json', | |
}); | |
content.push(p2, inputFile); | |
} | |
const buttonInputElement = Object.assign({}, gnoh.constant.dialogButtons.submit, { | |
label: langs.import, | |
async click(data) { | |
if (editor.editor.textContent.trim()) { | |
commandChainText = editor.editor.textContent.trim(); | |
} else if (data.file && data.file[0]) { | |
commandChainText = await parseTextFile(data.file[0]); | |
} | |
if (!commandChainText || !checkCommandChain(commandChainText)) { | |
gnoh.alert('Import failed'); | |
} else { | |
await importCommandChain(JSON.parse(commandChainText)); | |
await reloadSetting(); | |
} | |
}, | |
}); | |
const buttonPreviewElement = Object.assign({}, gnoh.constant.dialogButtons.submit, { | |
label: langs.preview, | |
async click(data) { | |
if (editor.editor.textContent.trim()) { | |
commandChainText = editor.editor.textContent.trim(); | |
} else if (data.file && data.file[0]) { | |
commandChainText = await parseTextFile(data.file[0]); | |
} | |
if (!commandChainText || !checkCommandChain(commandChainText)) { | |
gnoh.alert('Import failed'); | |
} else { | |
await showDialogPreview(commandChainText); | |
} | |
}, | |
}); | |
const buttonCancelElement = Object.assign({}, gnoh.constant.dialogButtons.cancel); | |
gnoh.dialog( | |
'Import Command Chain', | |
content, | |
[buttonInputElement, buttonPreviewElement, buttonCancelElement], | |
{ | |
width: 500, | |
class: 'import-export-command-chains', | |
} | |
); | |
} | |
async function showDialogExport(key) { | |
if (!key) { | |
return; | |
} | |
const commandChain = await getCommandChainByKey(key); | |
const commandChainText = JSON.stringify(commandChain); | |
const commandChainUrl = URL.createObjectURL(new Blob([commandChainText], { type: 'application/json' })); | |
const editor = createEditor({ | |
contentEditable: false, | |
value: commandChainText, | |
}); | |
const buttonCopyElement = Object.assign({}, gnoh.constant.dialogButtons.submit, { | |
label: langs.copy, | |
click: () => { | |
navigator.clipboard.writeText(commandChainText); | |
}, | |
}); | |
const buttonExportElement = Object.assign({}, gnoh.constant.dialogButtons.submit, { | |
label: langs.export, | |
click: () => { | |
const filename = commandChain.label.trim() | |
.replace(/\s+/g, '-').toLowerCase() | |
.replace(/[^\p{L}0-9-]/gu, '') | |
.replace(/^-+|-+$/g, '') || key; | |
chrome.downloads.download({ | |
url: commandChainUrl, | |
filename: filename + '.json', | |
saveAs: true, | |
}); | |
}, | |
}); | |
const buttonCancelElement = Object.assign({}, gnoh.constant.dialogButtons.cancel); | |
gnoh.dialog( | |
'Export Command Chain', | |
editor.editor, | |
[buttonCopyElement, buttonExportElement, buttonCancelElement], | |
{ | |
width: 500, | |
class: 'import-export-command-chains', | |
} | |
); | |
} | |
function createCommandChainItem(chain, index) { | |
const chainedCommandItem = gnoh.createElement('div', { | |
class: 'ChainedCommand-Item', | |
}); | |
const chainedCommandItemTitle = gnoh.createElement('div', { | |
class: 'ChainedCommand-Item--Title floating-label', | |
}, chainedCommandItem); | |
const chainedCommandItemName = gnoh.createElement('span', { | |
class: 'chained-command-item-name', | |
text: 'Command ' + index, | |
}, chainedCommandItemTitle); | |
const chainedCommandItemValue = gnoh.createElement('div', { | |
class: 'chained-command-item-value', | |
text: commands[chain.key]?.label || chain.label || '', | |
}, chainedCommandItemTitle); | |
if (typeof chain.param !== 'undefined') { | |
const chainedCommandItemParameter = gnoh.createElement('div', { | |
class: 'ChainedCommand-Item--Parameter floating-label', | |
}, chainedCommandItem); | |
const chainedCommandItemParameterName = gnoh.createElement('span', { | |
class: 'chained-command-item-name', | |
text: langs.commandParameter, | |
}, chainedCommandItemParameter); | |
const chainedCommandItemParameterValue = gnoh.createElement('div', { | |
class: 'chained-command-item-value', | |
text: chain.param || chain.defaultValue || '', | |
}, chainedCommandItemParameter); | |
} | |
return chainedCommandItem; | |
} | |
function createCommandChain(commandChain) { | |
const chainName = gnoh.createElement('div', { | |
class: 'chain-name', | |
text: commandChain.label, | |
}); | |
const chainedCommand = gnoh.createElement('div', { | |
class: 'ChainedCommand', | |
}); | |
commandChain.chain.forEach((chain, index) => { | |
chainedCommand.append(createCommandChainItem(chain, index + 1)); | |
}); | |
return [chainName, chainedCommand]; | |
} | |
async function showDialogPreview(commandChainText) { | |
if (!checkCommandChain(commandChainText)) { | |
return; | |
} | |
const commandChain = JSON.parse(commandChainText); | |
const chainedCommand = createCommandChain(commandChain); | |
const buttonInputElement = Object.assign({}, gnoh.constant.dialogButtons.submit, { | |
label: langs.import, | |
async click() { | |
if (!commandChainText || !checkCommandChain(commandChainText)) { | |
gnoh.alert('Import failed'); | |
} else { | |
await importCommandChain(JSON.parse(commandChainText)); | |
await reloadSetting(); | |
} | |
}, | |
}); | |
const buttonCancelElement = Object.assign({}, gnoh.constant.dialogButtons.cancel); | |
gnoh.dialog( | |
'Preview Command Chain', | |
chainedCommand, | |
[buttonInputElement, buttonCancelElement], | |
{ | |
width: 750, | |
class: 'import-export-command-chains', | |
} | |
); | |
} | |
function checkCommandChain(commandChainText) { | |
let commandChain = null; | |
try { | |
commandChain = JSON.parse(commandChainText); | |
} catch (e) { | |
return false; | |
} | |
if ( | |
commandChain.category !== 'CATEGORY_COMMAND_CHAIN' | |
|| !Array.from(commandChain.chain) | |
|| commandChain.chain.some(c => typeof c.key !== 'string' || gnoh.uuid.check(c.key)) | |
|| typeof commandChain.key !== 'string' | |
|| typeof commandChain.label !== 'string' | |
|| typeof commandChain.name !== 'string' | |
) { | |
return false; | |
} else { | |
return true; | |
} | |
} | |
async function getCommandChains() { | |
return vivaldi.prefs.get('vivaldi.chained_commands.command_list'); | |
} | |
async function getCommandChainByKey(key) { | |
const commandList = await getCommandChains(); | |
const commandChain = commandList.find(c => c.key === key); | |
if (commandChain) { | |
return commandChain; | |
} else { | |
throw new Error('Key not found'); | |
} | |
} | |
async function importCommandChain(commandChain) { | |
const commandList = await getCommandChains(); | |
const index = commandList.findIndex(c => c.key === commandChain.key); | |
if (index === -1) { | |
commandList.push(commandChain); | |
} else { | |
commandList[index] = commandChain; | |
} | |
vivaldi.prefs.set({ | |
path: 'vivaldi.chained_commands.command_list', | |
value: commandList | |
}); | |
} | |
function getMenuItem(name) { | |
const menuItem = document.evaluate(`//div[contains(concat(" ", normalize-space(@class), " "), " tree-row ") and contains(., "${name}")]`, document, null, XPathResult.ANY_TYPE, null); | |
return menuItem.iterateNext(); | |
} | |
async function reloadSetting() { | |
try { | |
const window = await chrome.windows.getLastFocused({ windowTypes: ['popup'] }); | |
if (window) { | |
try { | |
const vivExtData = JSON.parse(window.vivExtData); | |
if (vivExtData.isSettings) { | |
chrome.runtime.sendMessage({ | |
type: messageType, | |
action: 'reload-setting', | |
windowId: window.id, | |
}); | |
} | |
} catch (error) { | |
console.error(error); | |
} | |
} | |
} catch (error) { | |
console.error(error); | |
} | |
const tabs = await chrome.tabs.query({ url: urls.quickCommands }); | |
tabs.forEach(async tab => { | |
chrome.tabs.onUpdated.addListener(async function listener(tabId, changeInfo) { | |
if (changeInfo.status === 'complete' && tabId === tab.id) { | |
chrome.tabs.onUpdated.removeListener(listener); | |
await chrome.tabs.update(tab.id, { url: urls.quickCommands }); | |
} | |
}); | |
await chrome.tabs.update(tab.id, { url: urls.general }); | |
}); | |
} | |
chrome.runtime.onMessage.addListener((info, sender, sendResponse) => { | |
if (info.type === messageType) { | |
(async () => { | |
const window = await chrome.windows.getLastFocused({ windowTypes: ['normal'] }); | |
if (window && window.id === vivaldiWindowId || info.windowId === vivaldiWindowId) { | |
switch (info.action) { | |
case 'import': | |
showDialogImport(info.data); | |
break; | |
case 'check': | |
if (checkCommandChain(info.data)) { | |
const data = JSON.parse(info.data); | |
try { | |
const commandChain = await getCommandChainByKey(data.key); | |
if (JSON.stringify(commandChain) === JSON.stringify(data)) { | |
sendResponse('installed'); | |
} else { | |
sendResponse('update'); | |
} | |
} catch { | |
sendResponse('new'); | |
} | |
} else { | |
sendResponse('fail'); | |
} | |
break; | |
case 'reload-setting': | |
const menuItemGeneralElement = getMenuItem(langs.general); | |
const menuItemQuickCommandsElement = getMenuItem(langs.quickCommands); | |
if (menuItemGeneralElement && menuItemQuickCommandsElement) { | |
setTimeout(() => menuItemGeneralElement.click()); | |
setTimeout(() => menuItemQuickCommandsElement.click()); | |
} | |
break; | |
} | |
} | |
})(); | |
return true; | |
} | |
}); | |
vivaldi.prefs.onChanged.addListener(async newValue => { | |
if (newValue.path === 'vivaldi.chained_commands.command_list') { | |
const tabs = await chrome.tabs.query({ url: '*://forum.vivaldi.net/topic/*' }); | |
tabs.forEach(tab => { | |
chrome.tabs.sendMessage(tab.id, { | |
type: messageType, | |
action: 'change', | |
}); | |
}); | |
} | |
}); | |
function createButtonToolbar(attribute = {}, parent, inner, options) { | |
return gnoh.createElement('button', gnoh.object.merge({ | |
class: 'button-toolbar', | |
}, attribute), parent, inner, options); | |
} | |
function createSettings() { | |
if (timeOut) { | |
timeOut.stop(); | |
} | |
timeOut = gnoh.timeOut(chainedCommand => { | |
chainedCommand.dataset.importExportCommandChains = true; | |
const masterToolbar = chainedCommand.querySelector('.master-toolbar:not([data-import-export-command-chains="true"])'); | |
masterToolbar.dataset.importExportCommandChains = true; | |
const master = chainedCommand.querySelector('.master:not([data-import-export-command-chains="true"])'); | |
master.dataset.importExportCommandChains = true; | |
async function selectedKey() { | |
const itemSelected = master.querySelector('.master-items .item-selected'); | |
if (!itemSelected) { | |
return; | |
} | |
const commandList = await getCommandChains(); | |
const indexSelected = gnoh.element.getIndex(itemSelected); | |
return commandList[indexSelected] && commandList[indexSelected].key; | |
} | |
Object.values(buttons).forEach((button) => { | |
gnoh.element.appendAtIndex( | |
createButtonToolbar({ | |
html: button.icon, | |
title: button.title, | |
events: { | |
async click(e) { | |
e.preventDefault(); | |
const key = await selectedKey(); | |
button.click(key); | |
}, | |
}, | |
}), | |
masterToolbar, | |
button.index, | |
); | |
}); | |
}, '.Setting--ChainedCommand.master-detail:not([data-import-export-command-chains="true"])'); | |
} | |
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { | |
if (changeInfo.status === 'complete') { | |
if (tab.url === urls.quickCommands) { | |
createSettings(); | |
} else if (/https:\/\/forum\.vivaldi\.net\/topic\/*/.test(tab.url)) { | |
chrome.scripting.executeScript({ | |
target: { | |
tabId: tab.id | |
}, | |
args: [messageType, langs], | |
func: (messageType, langs) => { | |
if (window.importExportCommandChains) { | |
return; | |
} else { | |
window.importExportCommandChains = true; | |
} | |
const buttonInstallElements = []; | |
chrome.runtime.onMessage.addListener((info) => { | |
if (info.type === messageType) { | |
switch (info.action) { | |
case 'change': | |
buttonInstallElements.forEach(async buttonElement => { | |
const status = await chrome.runtime.sendMessage({ | |
type: messageType, | |
action: 'check', | |
data: buttonElement.code, | |
}); | |
updateButton(buttonElement, status); | |
}); | |
break; | |
} | |
} | |
}); | |
async function onInstallClick(event) { | |
chrome.runtime.sendMessage({ | |
type: messageType, | |
action: 'import', | |
data: event.target.code, | |
}); | |
} | |
function updateButton(buttonElement, status) { | |
if (status === 'new') { | |
buttonElement.classList.add('btn-primary'); | |
buttonElement.classList.remove('btn-secondary'); | |
buttonElement.innerText = langs.install; | |
buttonElement.disabled = false; | |
buttonElement.addEventListener('click', onInstallClick); | |
} else if (status === 'update') { | |
buttonElement.classList.add('btn-primary'); | |
buttonElement.classList.remove('btn-secondary'); | |
buttonElement.innerText = langs.update; | |
buttonElement.disabled = false; | |
buttonElement.addEventListener('click', onInstallClick); | |
} else if (status === 'installed') { | |
buttonElement.classList.remove('btn-primary'); | |
buttonElement.classList.add('btn-secondary'); | |
buttonElement.innerText = langs.installed; | |
buttonElement.disabled = true; | |
buttonElement.removeEventListener('click', onInstallClick); | |
} | |
} | |
function createButton(node) { | |
const codes = node.querySelectorAll('code:not([data-import-export-command-chains="true"])'); | |
codes.forEach(async (codeElement) => { | |
codeElement.dataset.importExportCommandChains = true; | |
const code = codeElement.innerText.trim(); | |
const status = await chrome.runtime.sendMessage({ | |
type: messageType, | |
action: 'check', | |
data: code, | |
}); | |
if (status === 'fail') { | |
return; | |
} | |
const commandChainElement = document.createElement('div'); | |
commandChainElement.className = 'command-chain'; | |
const buttonInstallElement = document.createElement('button'); | |
buttonInstallElement.code = code; | |
buttonInstallElement.className = 'btn mb-3'; | |
updateButton(buttonInstallElement, status); | |
buttonInstallElements.push(buttonInstallElement); | |
commandChainElement.append(buttonInstallElement); | |
const preElement = codeElement.closest('pre'); | |
if (preElement) { | |
preElement.parentNode.insertBefore(commandChainElement, preElement.nextSibling); | |
} else { | |
codeElement.parentNode.insertBefore(commandChainElement, codeElement.nextSibling); | |
} | |
}); | |
} | |
createButton(document.body); | |
const observer = new MutationObserver((mutationList) => { | |
mutationList.forEach(mutation => { | |
if (mutation.addedNodes.length) { | |
mutation.addedNodes.forEach(node => { | |
if (node.nodeType === 1) { | |
createButton(node); | |
} | |
}); | |
} | |
}); | |
}); | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true, | |
}); | |
}, | |
}); | |
} | |
} | |
}); | |
gnoh.timeOut((main) => { | |
if (document.querySelector('#main > .webpageview')) { | |
const menuItemQuickCommandsElement = getMenuItem(langs.quickCommands); | |
menuItemQuickCommandsElement.addEventListener('click', createSettings) | |
} | |
}, '#main'); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment