Skip to content

Commit 4dd85e3

Browse files
committed
Add APIS
1 parent 3f05444 commit 4dd85e3

File tree

7 files changed

+264
-20
lines changed

7 files changed

+264
-20
lines changed

src/GM/gmApiDefinitions.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ export const GM_API_DEFINITIONS = {
5252
category: GM_API_CATEGORIES.STORAGE,
5353
description: "Lists all keys stored with GM_setValue"
5454
},
55+
GM_addValueChangeListener: {
56+
signature: "declare function GM_addValueChangeListener(name: string, callback: (name: string, oldValue: any, newValue: any, remote: boolean) => void): number;",
57+
name: "GM_addValueChangeListener",
58+
el: "gmAddValueChangeListener",
59+
tmName: "GM_addValueChangeListener",
60+
category: GM_API_CATEGORIES.STORAGE,
61+
description: "Adds a listener to watch for changes to a stored value."
62+
},
63+
GM_removeValueChangeListener: {
64+
signature: "declare function GM_removeValueChangeListener(listenerId: number): void;",
65+
name: "GM_removeValueChangeListener",
66+
el: "gmRemoveValueChangeListener",
67+
tmName: "GM_removeValueChangeListener",
68+
category: GM_API_CATEGORIES.STORAGE,
69+
description: "Removes a previously added value change listener."
70+
},
5571

5672
// Browser & UI APIs
5773
GM_openInTab: {
@@ -136,6 +152,14 @@ export const GM_API_DEFINITIONS = {
136152
category: GM_API_CATEGORIES.RESOURCES_NETWORK,
137153
description: "Copies data to the clipboard"
138154
},
155+
GM_download: {
156+
signature: "declare function GM_download(url: string, name?: string): Promise<void>;",
157+
name: "GM_download",
158+
el: "gmDownload",
159+
tmName: "GM_download",
160+
category: GM_API_CATEGORIES.RESOURCES_NETWORK,
161+
description: "Downloads a file from a URL."
162+
},
139163

140164
// Advanced APIs
141165
unsafeWindow: {
@@ -145,6 +169,14 @@ export const GM_API_DEFINITIONS = {
145169
tmName: "unsafeWindow",
146170
category: GM_API_CATEGORIES.ADVANCED,
147171
description: "Direct access to the page's window object"
172+
},
173+
GM_log: {
174+
signature: "declare function GM_log(...args: any[]): void;",
175+
name: "GM_log",
176+
el: "gmLog",
177+
tmName: "GM_log",
178+
category: GM_API_CATEGORIES.ADVANCED,
179+
description: "Logs messages to the console."
148180
}
149181
};
150182

src/GM/gm_core.js

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,13 @@
109109
}
110110

111111
call(action, payload = {}) {
112+
const newPayload = { ...payload, scriptId: this.scriptId };
112113
// If in the ISOLATED world, use the direct chrome.runtime.sendMessage
113114
if (this.worldType === 'ISOLATED' && typeof chrome?.runtime?.sendMessage === 'function') {
114-
return this.callIsolated(action, payload);
115+
return this.callIsolated(action, newPayload);
115116
}
116117
// Otherwise, fallback to the MAIN world postMessage mechanism
117-
return this.callMain(action, payload);
118+
return this.callMain(action, newPayload);
118119
}
119120

120121
callMain(action, payload = {}) {
@@ -216,6 +217,24 @@
216217
this.bridge = bridge;
217218
this.resourceManager = resourceManager;
218219
this.cache = new Map();
220+
this.valueChangeListeners = new Map();
221+
this.listenerIdCounter = 0;
222+
223+
window.addEventListener("message", (event) => {
224+
if (event.source === window && event.data?.type === "GM_VALUE_CHANGED") {
225+
const { name, oldValue, newValue, remote } = event.data.payload;
226+
const listeners = this.valueChangeListeners.get(name);
227+
if (listeners) {
228+
for (const listener of listeners.values()) {
229+
try {
230+
listener(name, oldValue, newValue, remote);
231+
} catch (e) {
232+
console.error("Error in value change listener:", e);
233+
}
234+
}
235+
}
236+
}
237+
});
219238
}
220239

221240
async setValue(name, value) {
@@ -327,11 +346,31 @@
327346
}
328347

329348
if (enabled.gmAddValueChangeListener) {
330-
const fn = (name, callback, options) => {
331-
// Placeholder: implement value change listener
332-
return this.bridge.call("addValueChangeListener", { name, options });
349+
const addFn = (name, callback) => {
350+
if (typeof name !== 'string' || typeof callback !== 'function') return;
351+
const listenerId = this.listenerIdCounter++;
352+
if (!this.valueChangeListeners.has(name)) {
353+
this.valueChangeListeners.set(name, new Map());
354+
}
355+
this.valueChangeListeners.get(name).set(listenerId, callback);
356+
this.bridge.call("addValueChangeListener", { name });
357+
return listenerId;
358+
};
359+
window.GM_addValueChangeListener = window.GM.addValueChangeListener = addFn;
360+
361+
const removeFn = (listenerId) => {
362+
for (const [name, listeners] of this.valueChangeListeners.entries()) {
363+
if (listeners.has(listenerId)) {
364+
listeners.delete(listenerId);
365+
if (listeners.size === 0) {
366+
this.valueChangeListeners.delete(name);
367+
this.bridge.call("removeValueChangeListener", { name });
368+
}
369+
break;
370+
}
371+
}
333372
};
334-
window.GM_addValueChangeListener = window.GM.addValueChangeListener = fn;
373+
window.GM_removeValueChangeListener = window.GM.removeValueChangeListener = removeFn;
335374
}
336375
}
337376

@@ -408,12 +447,18 @@
408447

409448
_registerResources(enabled) {
410449
if (enabled.gmGetResourceText) {
411-
const fn = (name) => this.resourceManager.getText(name);
450+
const fn = (name) => new Promise(resolve => {
451+
const text = this.resourceManager.getText(name);
452+
resolve(text === null ? undefined : text);
453+
});
412454
window.GM_getResourceText = window.GM.getResourceText = fn;
413455
}
414456

415457
if (enabled.gmGetResourceURL) {
416-
const fn = (name) => this.resourceManager.getURL(name);
458+
const fn = (name) => new Promise(resolve => {
459+
const url = this.resourceManager.getURL(name);
460+
resolve(url === null ? undefined : url);
461+
});
417462
window.GM_getResourceURL = window.GM.getResourceURL = fn;
418463
}
419464
}
@@ -504,6 +549,17 @@
504549
window.GM_setClipboard = window.GM.setClipboard = fn;
505550
}
506551

552+
if (enabled.gmDownload) {
553+
const fn = (urlOrDetails, name) => {
554+
const details = typeof urlOrDetails === 'object' ? urlOrDetails : { url: urlOrDetails, name: name };
555+
const cloneableDetails = Object.fromEntries(
556+
Object.entries(details).filter(([, v]) => typeof v !== 'function')
557+
);
558+
return this.bridge.call("download", cloneableDetails);
559+
};
560+
window.GM_download = window.GM.download = fn;
561+
}
562+
507563
// GM_xmlhttpRequest
508564
if (enabled.gmXmlhttpRequest) {
509565
const fn = (details = {}) => {
@@ -751,6 +807,11 @@
751807
window.unsafeWindow = window;
752808
}
753809
}
810+
811+
if (enabled.gmLog) {
812+
const fn = (...args) => console.log(...args);
813+
window.GM_log = window.GM.log = fn;
814+
}
754815
}
755816
}
756817

src/background/background.js

Lines changed: 138 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class BackgroundState {
1010
this.ports = new Set();
1111
this.executedScripts = new Map();
1212
this.creatingOffscreenDocument = null;
13+
this.valueChangeListeners = new Map();
1314
}
1415

1516
clearCache() {
@@ -64,6 +65,23 @@ function notifyPorts(action) {
6465
disconnectedPorts.forEach((port) => state.ports.delete(port));
6566
}
6667

68+
async function setupOffscreenDocument() {
69+
if (await chrome.offscreen.hasDocument()) {
70+
return;
71+
}
72+
if (state.creatingOffscreenDocument) {
73+
await state.creatingOffscreenDocument;
74+
return;
75+
}
76+
state.creatingOffscreenDocument = chrome.offscreen.createDocument({
77+
url: 'offscreen/offscreen.html',
78+
reasons: ['CLIPBOARD'],
79+
justification: 'Clipboard access',
80+
});
81+
await state.creatingOffscreenDocument;
82+
state.creatingOffscreenDocument = null;
83+
}
84+
6785
// Script management
6886
async function getFilteredScripts(url, runAt = null) {
6987
if (!url?.startsWith("http")) {
@@ -337,21 +355,125 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
337355
sendResponse({ error: "Request payload is missing." });
338356
return true;
339357
}
340-
const { action, ...payload } = message.payload;
358+
const { action, scriptId, ...payload } = message.payload;
341359

342-
if (action === "getSettings") {
343-
chrome.storage.local.get("settings").then(({ settings = {} }) => {
344-
sendResponse({ result: settings });
345-
});
346-
return true;
347-
} else if (action === "xmlhttpRequest") {
348-
// Still not working properly....
349-
handleCrossOriginXmlhttpRequest(payload.details, sender.tab.id, sendResponse);
360+
const getStorage = async () => {
361+
const key = `script-values-${scriptId}`;
362+
const result = await chrome.storage.local.get(key);
363+
return result[key] || {};
364+
};
365+
366+
const setStorage = async (values) => {
367+
const key = `script-values-${scriptId}`;
368+
await chrome.storage.local.set({ [key]: values });
369+
};
370+
371+
const actionHandlers = {
372+
async setValue({ name, value }) {
373+
const values = await getStorage();
374+
const oldValue = values[name];
375+
values[name] = value;
376+
await setStorage(values);
377+
378+
const listeners = state.valueChangeListeners.get(name);
379+
if (listeners) {
380+
for (const tabId of listeners) {
381+
if (tabId !== sender.tab.id) { // Don't notify the tab that made the change
382+
chrome.tabs.sendMessage(tabId, {
383+
type: "GM_VALUE_CHANGED",
384+
payload: { name, oldValue, newValue: value, remote: true },
385+
}).catch(() => {}); // Ignore errors if tab is closed
386+
}
387+
}
388+
}
389+
sendResponse({ result: null });
390+
},
391+
async getValue({ name, defaultValue }) {
392+
const values = await getStorage();
393+
sendResponse({ result: values[name] ?? defaultValue });
394+
},
395+
async deleteValue({ name }) {
396+
const values = await getStorage();
397+
const oldValue = values[name];
398+
delete values[name];
399+
await setStorage(values);
400+
401+
const listeners = state.valueChangeListeners.get(name);
402+
if (listeners) {
403+
for (const tabId of listeners) {
404+
chrome.tabs.sendMessage(tabId, {
405+
type: "GM_VALUE_CHANGED",
406+
payload: { name, oldValue, newValue: undefined, remote: tabId !== sender.tab.id },
407+
}).catch(() => {});
408+
}
409+
}
410+
sendResponse({ result: null });
411+
},
412+
async listValues() {
413+
const values = await getStorage();
414+
sendResponse({ result: Object.keys(values) });
415+
},
416+
addValueChangeListener({ name }) {
417+
if (!state.valueChangeListeners.has(name)) {
418+
state.valueChangeListeners.set(name, new Set());
419+
}
420+
state.valueChangeListeners.get(name).add(sender.tab.id);
421+
sendResponse({ result: null });
422+
},
423+
removeValueChangeListener({ name }) {
424+
const listeners = state.valueChangeListeners.get(name);
425+
if (listeners) {
426+
listeners.delete(sender.tab.id);
427+
if (listeners.size === 0) {
428+
state.valueChangeListeners.delete(name);
429+
}
430+
}
431+
sendResponse({ result: null });
432+
},
433+
getSettings() {
434+
chrome.storage.local.get("settings").then(({ settings = {} }) => {
435+
sendResponse({ result: settings });
436+
});
437+
},
438+
xmlhttpRequest() {
439+
handleCrossOriginXmlhttpRequest(payload.details, sender.tab.id, sendResponse);
440+
},
441+
notification({ details }) {
442+
const notificationOptions = {
443+
type: 'basic',
444+
iconUrl: details.image || 'assets/icons/icon128.png',
445+
title: details.title || 'CodeTweak Notification',
446+
message: details.text || ''
447+
};
448+
chrome.notifications.create(notificationOptions, () => {
449+
sendResponse({ result: null });
450+
});
451+
return true;
452+
},
453+
async setClipboard({ data, type }) {
454+
await setupOffscreenDocument();
455+
chrome.runtime.sendMessage({
456+
target: 'offscreen',
457+
type: 'copy-to-clipboard',
458+
data: data
459+
});
460+
sendResponse({ result: null });
461+
},
462+
download(details) {
463+
chrome.downloads.download({ url: details.url, filename: details.name }, (downloadId) => {
464+
sendResponse({ result: { downloadId } });
465+
});
466+
return true;
467+
},
468+
};
469+
470+
if (actionHandlers[action]) {
471+
actionHandlers[action](payload);
350472
return true;
351-
} else {
352-
chrome.tabs.sendMessage(sender.tab.id, message, sendResponse);
353-
return true; // Async response
354473
}
474+
475+
sendResponse({ error: `Unknown GM API action: ${action}` });
476+
return true;
355477
}
356478
// Some errors around here.... not all errors being captured
357479
if (message.type === "SCRIPT_ERROR") {
@@ -457,6 +579,10 @@ navigationEvents.forEach((event, index) => {
457579
chrome.tabs.onRemoved.addListener((tabId) => {
458580
state.executedScripts.delete(tabId);
459581
clearInjectedCoreScriptsForTab(tabId);
582+
583+
for (const listeners of state.valueChangeListeners.values()) {
584+
listeners.delete(tabId);
585+
}
460586
});
461587

462588
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {

src/editor/editor.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@ <h4 class="category-title">Storage & Data</h4>
202202
<input type="checkbox" id="gmListValues" class="form-checkbox">
203203
<label for="gmListValues">GM_listValues</label>
204204
</div>
205+
<div class="form-group-checkbox">
206+
<input type="checkbox" id="gmAddValueChangeListener" class="form-checkbox">
207+
<label for="gmAddValueChangeListener">GM_addValueChangeListener</label>
208+
</div>
209+
<div class="form-group-checkbox">
210+
<input type="checkbox" id="gmRemoveValueChangeListener" class="form-checkbox">
211+
<label for="gmRemoveValueChangeListener">GM_removeValueChangeListener</label>
212+
</div>
205213
</div>
206214
</div>
207215

@@ -254,6 +262,10 @@ <h4 class="category-title">Resources & Network</h4>
254262
<input type="checkbox" id="gmSetClipboard" class="form-checkbox">
255263
<label for="gmSetClipboard">GM_setClipboard</label>
256264
</div>
265+
<div class="form-group-checkbox">
266+
<input type="checkbox" id="gmDownload" class="form-checkbox">
267+
<label for="gmDownload">GM_download</label>
268+
</div>
257269
</div>
258270
</div>
259271

@@ -264,6 +276,10 @@ <h4 class="category-title">Advanced</h4>
264276
<input type="checkbox" id="unsafeWindow" class="form-checkbox">
265277
<label for="unsafeWindow">unsafeWindow</label>
266278
</div>
279+
<div class="form-group-checkbox">
280+
<input type="checkbox" id="gmLog" class="form-checkbox">
281+
<label for="gmLog">GM_log</label>
282+
</div>
267283
</div>
268284
</div>
269285
</div>

0 commit comments

Comments
 (0)