· 5 years ago · Nov 06, 2020, 03:18 PM
1'use strict';
2
3browser.userScripts.onBeforeScript.addListener(script => {
4
5 const name = script.metadata.name;
6 const resource = script.metadata.resource;
7
8 // --------------- Script Storage ------------------------
9 const store = '_' + name;
10 let storage = {};
11 const valueChange = {};
12 browser.storage.local.get(store).then((result = {}) => storage = result[store] || {});
13
14 function needListener() {
15 browser.storage.onChanged.hasListener(storageChange) || browser.storage.onChanged.addListener(storageChange);
16 }
17
18 function storageChange(changes, area) {
19 console.log(changes);
20 if (changes.hasOwnProperty(store)) {
21
22 const oldValue = changes[store].oldValue || {};
23 storage = changes[store].newValue || {};
24
25 // process addValueChangeListener (only for remote) (key, oldValue, newValue, remote)
26 Object.keys(valueChange).forEach(item =>
27 oldValue[item] !== storage[item] && (valueChange[item])(item, oldValue[item], newValue[item], true)
28 );
29 }
30 }
31 // --------------- /Script Storage -----------------------
32
33 // --------------- Script Command ------------------------
34 const scriptCommand = {};
35 browser.runtime.onMessage.addListener((message, sender) => {
36
37 switch (true) {
38 // --- to popup.js for registerMenuCommand
39 case message.hasOwnProperty('listCommand'):
40 const command = Object.keys(scriptCommand);
41 command[0] && browser.runtime.sendMessage({name, command});
42 break;
43
44 // from popup.js for registerMenuCommand
45 case message.name === name && message.hasOwnProperty('command'):
46 (scriptCommand[message.command])();
47 break;
48 }
49 });
50 // --------------- Script /Command -----------------------
51
52 /*
53 Ref: robwu (Rob Wu)
54 In order to make callback functions visible
55 ONLY for GM.xmlHttpRequest(GM_xmlhttpRequest)
56 */
57 function callUserScriptCallback(object, name, ...args) {
58 try {
59 const cb = object.wrappedJSObject[name];
60 typeof cb === 'function' && cb(...args);
61 } catch(error) { console.error(name, error.message); }
62 }
63
64 // --- GM4 Object based functions
65 const GM = {
66
67 async getValue(key, defaultValue) { return GM.GM_getValue(key, defaultValue); },
68
69 GM_getValue(key, defaultValue) {
70
71 needListener();
72 return storage.hasOwnProperty(key) ? storage[key] : defaultValue;
73 },
74
75 async listValues() { return GM.GM_listValues(); },
76
77 GM_listValues() {
78
79 needListener();
80 return script.export(Object.keys(storage));
81 },
82
83 setValue(key, value) {
84
85 if (!['string', 'number', 'boolean'].includes(typeof value)) { throw `${name}: Unsupported value in setValue()`; }
86 if (storage[key] === value) { return true; } // return if value hasn't changed
87
88 // process addValueChangeListener (not remote) (key, oldValue, newValue, remote)
89 valueChange.hasOwnProperty(key) && (valueChange[key])(key, storage[key], value, false);
90 storage[key] = value;
91 return browser.storage.local.set({[store]: storage});
92 },
93
94 deleteValue(key) {
95
96 if (!storage.hasOwnProperty(key)) { return; } // return if value hasn't changed
97
98 // process addValueChangeListener (not remote) (key, oldValue, newValue, remote)
99 valueChange.hasOwnProperty(key) && (valueChange[key])(key, storage[key], undefined, false);
100 delete storage[key];
101 return browser.storage.local.set({[store]: storage});
102 },
103
104 addValueChangeListener(key, callback) {
105
106 needListener();
107 valueChange[key] = callback;
108 return key;
109 },
110
111 removeValueChangeListener(key) { delete valueChange[key]; },
112
113
114 async openInTab(url, open_in_background) {
115
116 return await browser.runtime.sendMessage({
117 name,
118 api: 'openInTab',
119 data: {url, active: !open_in_background}
120 });
121 },
122
123 async setClipboard(text) {
124
125 return await browser.runtime.sendMessage({
126 name,
127 api: 'setClipboard',
128 data: {text}
129 });
130 },
131
132 async notification(text, title, image, onclick) {
133 // (text, title, image, onclick) | ({text, title, image, onclick})
134 const txt = typeof text === 'string' ? text : text.text;
135 if (typeof txt !== 'string' || !txt.trim()) { return; }
136 return await browser.runtime.sendMessage({
137 name,
138 api: 'notification',
139 data: typeof text === 'string' ? {text, title, image, onclick} : text
140 });
141 },
142
143 async fetch(url, init = {}) {
144
145 const response = await browser.runtime.sendMessage({
146 name,
147 api: 'fetch',
148 data: {url, init, base: location.href}
149 });
150
151 // cloneInto() work around for https://bugzilla.mozilla.org/show_bug.cgi?id=1583159
152 return response ? (typeof response === 'string' ? script.export(response) : cloneInto(response, window)) : null;
153 },
154
155 async xmlHttpRequest(init) {
156
157 const data = {
158 method: 'GET',
159 data: null,
160 user: null,
161 password: null,
162 responseType: '',
163 base: location.href
164 };
165
166 ['url', 'method', 'headers', 'data', 'overrideMimeType', 'user', 'password',
167 'timeout', 'withCredentials', 'responseType'].forEach(item => init.hasOwnProperty(item) && (data[item] = init[item]));
168
169 const response = await browser.runtime.sendMessage({
170 name,
171 api: 'xmlHttpRequest',
172 data
173 });
174
175 if (!response) { throw 'There was an error with the xmlHttpRequest request.'; }
176
177 // only these 4 callback functions are processed
178 // cloneInto() work around for https://bugzilla.mozilla.org/show_bug.cgi?id=1583159
179 const type = response.type;
180 delete response.type;
181 callUserScriptCallback(init, type,
182 typeof response.response === 'string' ? script.export(response) : cloneInto(response, window));
183 },
184
185 addStyle(css) {
186 try {
187 const style = document.createElement('style');
188 style.textContent = css;
189 (document.head || document.body || document.documentElement || document).appendChild(style);
190 } catch(error) { console.error(name, error.message); }
191 },
192
193 addScript(js) {
194 try {
195 const script = document.createElement('script');
196 script.textContent = js;
197 (document.body || document.head || document.documentElement).appendChild(script);
198 } catch(error) { console.error(name, error.message); }
199 },
200
201 async getResourceText(resourceName) {
202
203 const response = await browser.runtime.sendMessage({
204 name,
205 api: 'fetch',
206 data: {url: resource[resourceName], init: {}, base: ''}
207 });
208
209 return response ? script.export(response) : null;
210 },
211
212 getResourceURL(resourceName) { return resource[resourceName]; },
213
214 registerMenuCommand(text, onclick, accessKey) { scriptCommand[text] = onclick; },
215
216 unregisterMenuCommand(text) { delete scriptCommand[text]; },
217
218 async download(url, filename) {
219
220 return browser.runtime.sendMessage({
221 name,
222 api: 'download',
223 data: {url, filename, base: location.href}
224 });
225 },
226
227 log(...text) { console.log(name + ':', ...text); },
228 info: script.metadata.info,
229
230 popup: class {
231
232 constructor({type = 'center', modal = true} = {}) {
233
234 this.host = document.createElement('gm-popup'); // shadow DOM host
235 const shadow = this.host.attachShadow({mode: 'closed'});
236 this.style = document.createElement('style');
237 shadow.appendChild(this.style);
238 this.content = document.createElement('div'); // main content
239 this.content.className = 'content';
240 shadow.appendChild(this.content);
241 const close = document.createElement('span'); // close button
242 close.className = 'close';
243 close.textContent = '✖';
244 this.content.appendChild(close);
245 close.addEventListener('click', () => this.hide());
246 [this.host, this.content].forEach(item => item.classList.add(type)); // process options
247 this.host.classList.toggle('modal', type.startsWith('panel-') ? modal : true); // process modal
248
249 this.style.textContent = `
250 :host {
251 display: none;
252 align-items: center;
253 justify-content: center;
254 background: transparent;
255 margin: 0;
256 position: fixed;
257 z-index: 10000;
258 transition: all 0.5s ease-in-out;
259 }
260
261 :host(.on) { display: flex; animation: fade-in 0.5s ease-in-out; }
262 .content { background: #fff; }
263 .content.center, .content[class*="slide-"] {
264 min-width: 10em;
265 min-height: 10em;
266 border-radius: 10px;
267 }
268
269 .close {
270 color: #ccc;
271 margin: 0.1em 0.3em;
272 float: right;
273 font-size: 1.5em;
274 border: 0px solid #ddd;
275 border-radius: 2em;
276 cursor: pointer;
277 }
278 .close:hover { color: #f70; }
279 .panel-right .close { float: left; }
280 .panel-top .close, .panel-bottom .close { margin-right: 0.5em; }
281
282 :host(.panel-left), :host(.panel-right), .panel-left, .panel-right { min-width: 14em; height: 100%; }
283 :host(.panel-top), :host(.panel-bottom), .panel-top, .panel-bottom { width: 100%; min-height: 8em; }
284
285 :host(.panel-left) { top: 0; left: 0; justify-content: start; }
286 :host(.panel-right) { top: 0; right: 0; justify-content: end; }
287 :host(.panel-top) { top: 0; left: 0; align-items: start; }
288 :host(.panel-bottom) { bottom: 0; left: 0; align-items: end; }
289
290 :host(.on) .panel-top { animation: panel-top 0.5s ease-in-out; }
291 :host(.on) .panel-bottom { animation: panel-bottom 0.5s ease-in-out; }
292 :host(.on) .panel-left { animation: panel-left 0.5s ease-in-out; }
293 :host(.on) .panel-right { animation: panel-right 0.5s ease-in-out; }
294
295 :host(.on) .slide-top { animation: slide-top 0.5s ease-in-out; }
296 :host(.on) .slide-bottom { animation: slide-bottom 0.5s ease-in-out; }
297 :host(.on) .slide-left { animation: slide-left 0.5s ease-in-out; }
298 :host(.on) .slide-right { animation: slide-right 0.5s ease-in-out; }
299
300 :host(.modal) { width: 100%; height: 100%; top: 0; left: 0; background: rgba(0, 0, 0, 0.4); }
301
302 @keyframes fade-in {
303 0% { opacity: 0; }
304 100% { opacity: 1; }
305 }
306
307 @keyframes panel-top {
308 0% { transform: translateY(-100%); }
309 100% { transform: translateY(0); }
310 }
311
312 @keyframes panel-bottom {
313 0% { transform: translateY(100%); }
314 100% { transform: translateY(0); }
315 }
316
317 @keyframes panel-left {
318 0% { transform: translateX(-100%); }
319 100% { transform: translateX(0); }
320 }
321
322 @keyframes panel-right {
323 0% { transform: translateX(100%); }
324 100% { transform: translateX(0); }
325 }
326
327 @keyframes slide-top {
328 0% { transform: translateY(-200%) scale(0.8); }
329 100% { transform: translateY(0) scale(1); }
330 }
331
332 @keyframes slide-bottom {
333 0% { transform: translateY(200%) scale(0.8); }
334 100% { transform: translateY(0) scale(1); }
335 }
336
337 @keyframes slide-left {
338 0% { transform: translateX(-200%) scale(0.8); }
339 100% { transform: translateX(0) scale(1); }
340 }
341
342 @keyframes slide-right {
343 0% { transform: translateX(200%) scale(0.8); }
344 100% { transform: translateX(0) scale(1); }
345 }
346 `;
347
348 document.body.appendChild(this.host);
349 }
350
351 addStyle(css) { this.style.textContent += '\n\n' + css; }
352 remove() { this.host.remove(); }
353
354 append(...arg) {
355
356 typeof arg[0] === 'string' && /^<.+>$/.test(arg[0].trim()) ?
357 this.content.append(document.createRange().createContextualFragment(arg[0].trim())) :
358 this.content.append(...arg);
359 }
360
361 show() {
362
363 this.host.classList.toggle('on', true);
364 this.host.classList.contains('modal') &&
365 this.host.addEventListener('click', () => this.hide(), {once: true}); // hide when clicking outside
366 }
367
368 hide() {
369
370 this.host.style.opacity = 0;
371 setTimeout(() => { this.host.classList.toggle('on', false); }, 500);
372 }
373 }
374
375 };
376
377
378 script.defineGlobals({
379
380 GM,
381 GM_setValue: GM.setValue,
382 GM_getValue: GM.GM_getValue,
383 GM_deleteValue: GM.deleteValue,
384 GM_listValues: GM.GM_listValues,
385 GM_addValueChangeListener: GM.addValueChangeListener,
386 GM_removeValueChangeListener: GM.removeValueChangeListener,
387
388 GM_openInTab: GM.openInTab,
389 GM_setClipboard: GM.setClipboard,
390 GM_notification: GM.notification,
391 GM_xmlhttpRequest: GM.xmlHttpRequest,
392 GM_fetch: GM.fetch,
393 GM_download: GM.download,
394
395 GM_getResourceText: GM.getResourceText,
396 GM_getResourceURL: GM.getResourceUrl,
397
398 GM_registerMenuCommand: GM.registerMenuCommand,
399 GM_unregisterMenuCommand: GM.unregisterMenuCommand,
400
401 GM_addStyle: GM.addStyle,
402 GM_addScript: GM.addScript,
403 GM_popup: GM.popup,
404 GM_log: GM.log,
405 GM_info: GM.info
406 });
407});