Skip to content

Commit de3336b

Browse files
committed
Add script updating and fix match pattern (CRITICAL FIX)
1 parent c03d181 commit de3336b

File tree

11 files changed

+610
-35
lines changed

11 files changed

+610
-35
lines changed

src/background/background.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { parseUserScriptMetadata } from '../core/metadataParser.js';
44
import { ScriptRegistry } from '../core/scriptRegistry.js';
55
import { UserScriptsAdapter } from '../core/userscriptAdapter.js';
66
import { getStorageApi } from '../shared/browserAdapter.js';
7+
import { ScriptUpdater } from '../core/scriptUpdater.js';
78

89
class BackgroundState {
910
constructor() {
@@ -26,6 +27,9 @@ class BackgroundState {
2627
const state = new BackgroundState();
2728
const userscriptAdapter = new UserScriptsAdapter();
2829
const scriptRegistry = new ScriptRegistry(getStorageApi(), userscriptAdapter);
30+
const scriptUpdater = new ScriptUpdater(getStorageApi());
31+
32+
scriptUpdater.setupAlarm();
2933

3034
function isIgnorableTabError(error) {
3135
const ignorableMessages = ['No tab with id', 'Invalid tab ID', 'Receiving end does not exist'];
@@ -365,11 +369,15 @@ class GMAPIHandler {
365369
return await handleCrossOriginXmlhttpRequest(details, this.sender.tab.id);
366370
}
367371

368-
async notification({ details }) {
372+
async notification(args) {
373+
const { details, ondone } = args;
374+
const options =
375+
typeof details === 'object' && details ? details : { text: details, title: ondone };
376+
369377
const baseOptions = {
370378
type: 'basic',
371-
title: details.title || 'CodeTweak Notification',
372-
message: details.text || '',
379+
title: options.title || 'CodeTweak Notification',
380+
message: options.text || '',
373381
};
374382

375383
const defaultIconUrl = chrome.runtime.getURL('assets/icons/icon128.png');
@@ -434,15 +442,25 @@ class GMAPIHandler {
434442
return { error: 'Clipboard API is unavailable in this browser.' };
435443
}
436444

437-
async download({ url, name }) {
445+
async download(args) {
446+
const { url, name } = args;
447+
const downloadUrl = typeof url === 'object' && url ? url.url : url;
448+
const filename = typeof url === 'object' && url ? url.name || url.filename : name;
449+
438450
return new Promise((resolve) => {
439-
chrome.downloads.download({ url, filename: name }, (downloadId) => {
440-
if (chrome.runtime.lastError) {
441-
resolve({ error: chrome.runtime.lastError.message });
442-
} else {
443-
resolve({ result: { downloadId } });
451+
chrome.downloads.download(
452+
{
453+
url: downloadUrl,
454+
filename: filename,
455+
},
456+
(downloadId) => {
457+
if (chrome.runtime.lastError) {
458+
resolve({ error: chrome.runtime.lastError.message });
459+
} else {
460+
resolve({ result: { downloadId } });
461+
}
444462
}
445-
});
463+
);
446464
});
447465
}
448466
}

src/core/scriptRegistry.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { urlMatchesPattern } from '../utils/urls.js';
1+
import { urlMatchesPattern, normalizeMatchPattern } from '../utils/urls.js';
22

33
function normalizeScript(script) {
4+
const targetUrls = (script.targetUrls || [script.targetUrl].filter(Boolean)).map(
5+
normalizeMatchPattern
6+
);
7+
48
return {
59
...script,
610
id: script.id || crypto.randomUUID(),
7-
targetUrls: script.targetUrls || [script.targetUrl].filter(Boolean),
11+
targetUrls,
812
};
913
}
1014

src/core/scriptUpdater.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import logger from '../utils/logger.js';
2+
import { parseUserScriptMetadata } from './metadataParser.js';
3+
4+
export class ScriptUpdater {
5+
constructor(storageApi) {
6+
this.storageApi = storageApi;
7+
}
8+
9+
compareVersions(v1, v2) {
10+
if (!v1 || !v2) return 0;
11+
const parts1 = String(v1)
12+
.split(/[.-]/)
13+
.map((p) => parseInt(p, 10) || 0);
14+
const parts2 = String(v2)
15+
.split(/[.-]/)
16+
.map((p) => parseInt(p, 10) || 0);
17+
const len = Math.max(parts1.length, parts2.length);
18+
19+
for (let i = 0; i < len; i++) {
20+
const p1 = parts1[i] || 0;
21+
const p2 = parts2[i] || 0;
22+
if (p1 > p2) return 1;
23+
if (p1 < p2) return -1;
24+
}
25+
return 0;
26+
}
27+
28+
async checkAndPerformUpdates() {
29+
logger.info('Starting automatic script update check...');
30+
const { scripts = [] } = await this.storageApi.local.get('scripts');
31+
let anyUpdated = false;
32+
33+
for (let i = 0; i < scripts.length; i++) {
34+
const script = scripts[i];
35+
const updateUrl =
36+
script.updateURL ||
37+
script.downloadURL ||
38+
(script.updateInfo && script.updateInfo.updateUrl);
39+
40+
if (!updateUrl) continue;
41+
42+
try {
43+
logger.info(`Checking update for script: ${script.name} at ${updateUrl}`);
44+
const response = await fetch(updateUrl);
45+
if (!response.ok) {
46+
logger.warn(`Failed to fetch update for ${script.name}: ${response.status}`);
47+
continue;
48+
}
49+
50+
const newCode = await response.text();
51+
const newMetadata = parseUserScriptMetadata(newCode);
52+
53+
if (!newMetadata.version) {
54+
logger.warn(`Could not find version in updated script ${script.name}`);
55+
continue;
56+
}
57+
58+
if (this.compareVersions(newMetadata.version, script.version) > 0) {
59+
logger.info(
60+
`Updating script ${script.name} from ${script.version} to ${newMetadata.version}`
61+
);
62+
scripts[i] = {
63+
...script,
64+
...newMetadata,
65+
code: newCode,
66+
updatedAt: new Date().toISOString(),
67+
};
68+
69+
if (newMetadata.updateURL) scripts[i].updateURL = newMetadata.updateURL;
70+
if (newMetadata.downloadURL) scripts[i].downloadURL = newMetadata.downloadURL;
71+
72+
anyUpdated = true;
73+
} else {
74+
logger.info(`Script ${script.name} is up to date (${script.version})`);
75+
}
76+
} catch (error) {
77+
logger.error(`Error updating script ${script.name}:`, error);
78+
}
79+
}
80+
81+
if (anyUpdated) {
82+
await this.storageApi.local.set({ scripts });
83+
chrome.runtime.sendMessage({ action: 'scriptsUpdated' });
84+
logger.info('Automatic updates completed. Some scripts were updated.');
85+
} else {
86+
logger.info('Automatic update check finished. No updates found.');
87+
}
88+
89+
await this.storageApi.local.set({ lastUpdateCheck: Date.now() });
90+
}
91+
92+
setupAlarm() {
93+
const ALARM_NAME = 'codetweak-auto-update';
94+
chrome.alarms.onAlarm.addListener((alarm) => {
95+
if (alarm.name === ALARM_NAME) {
96+
this.checkAndPerformUpdates();
97+
}
98+
});
99+
100+
chrome.alarms.get(ALARM_NAME, (alarm) => {
101+
if (!alarm) {
102+
chrome.alarms.create(ALARM_NAME, {
103+
periodInMinutes: 24 * 60, // Once a day
104+
});
105+
}
106+
});
107+
108+
this.checkIfInitialCheckNeeded();
109+
}
110+
111+
async checkIfInitialCheckNeeded() {
112+
const { lastUpdateCheck } = await this.storageApi.local.get('lastUpdateCheck');
113+
const now = Date.now();
114+
const ONE_DAY = 24 * 60 * 60 * 1000;
115+
116+
if (!lastUpdateCheck || now - lastUpdateCheck > ONE_DAY) {
117+
setTimeout(() => {
118+
this.checkAndPerformUpdates();
119+
}, 5000);
120+
}
121+
}
122+
}

src/core/userscriptAdapter.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logger from '../utils/logger.js';
22
import { getBrowserApi, promisifyChromeCall } from '../shared/browserAdapter.js';
3+
import { normalizeMatchPattern } from '../utils/urls.js';
34

45
const REGISTRATION_PREFIX = 'ct-us-';
56
const REGISTRATION_STORE_KEY = 'userScriptRegistrationIds';
@@ -36,7 +37,11 @@ function normalizeMatches(script) {
3637
? script.targetUrls
3738
: [script.targetUrl].filter(Boolean);
3839

39-
return [...new Set(matches)].filter(Boolean);
40+
const uniqueMatches = [...new Set(matches)]
41+
.filter(Boolean)
42+
.filter((match) => !/^https?:\/\/$/.test(match));
43+
44+
return uniqueMatches.map((m) => normalizeMatchPattern(m));
4045
}
4146

4247
function normalizeInjectInto(injectInto) {
@@ -322,7 +327,7 @@ function buildRuntimeBootstrap(config, userCode) {
322327
}
323328
324329
if (enabled.gmNotification) {
325-
const notifyFn = (details) => __ctPostBackground('notification', { details });
330+
const notifyFn = (details, ondone) => __ctPostBackground('notification', { details, ondone });
326331
window.GM_notification = notifyFn;
327332
GM.notification = notifyFn;
328333
}
@@ -584,11 +589,22 @@ export class UserScriptsAdapter {
584589
const registrations = await Promise.all(
585590
userScriptsEligible.map((script) => this.toRegistration(script))
586591
);
587-
const nextIds = new Set(registrations.map((entry) => entry.id));
592+
593+
// Filter out duplicates by ID before processing.
594+
const uniqueRegistrations = [];
595+
const seenIds = new Set();
596+
for (const reg of registrations) {
597+
if (!seenIds.has(reg.id)) {
598+
uniqueRegistrations.push(reg);
599+
seenIds.add(reg.id);
600+
}
601+
}
602+
603+
const nextIds = new Set(uniqueRegistrations.map((entry) => entry.id));
588604

589605
const idsToRemove = [...previousIds].filter((id) => !nextIds.has(id));
590-
const registrationsToAdd = registrations.filter((entry) => !previousIds.has(entry.id));
591-
const registrationsToUpdate = registrations.filter((entry) => previousIds.has(entry.id));
606+
const registrationsToAdd = uniqueRegistrations.filter((entry) => !previousIds.has(entry.id));
607+
const registrationsToUpdate = uniqueRegistrations.filter((entry) => previousIds.has(entry.id));
592608

593609
try {
594610
if (idsToRemove.length > 0) {

src/editor/editor.html

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -509,22 +509,68 @@ <h3 class="panel-title">Script Errors</h3>
509509
</p>
510510
</div>
511511
<div class="panel-content">
512-
<div class="error-log-controls">
513-
<button
514-
type="button"
515-
id="clearErrorsBtn"
516-
class="btn-secondary"
517-
title="Clear all errors"
518-
>
519-
<i data-feather="trash-2"></i>
520-
<span data-i18n="editorClearErrors">Clear Errors</span>
521-
</button>
512+
<div class="debug-section">
513+
<div class="debug-section-header">
514+
<h4 class="debug-section-title">Error Log</h4>
515+
<div class="error-log-controls">
516+
<button
517+
type="button"
518+
id="clearErrorsBtn"
519+
class="btn-secondary"
520+
title="Clear all errors"
521+
>
522+
<i data-feather="trash-2"></i>
523+
<span data-i18n="editorClearErrors">Clear Errors</span>
524+
</button>
525+
</div>
526+
</div>
527+
<div id="errorLogContainer" class="error-log-container">
528+
<div class="error-log-empty">
529+
<i data-feather="check-circle"></i>
530+
<p>No errors detected</p>
531+
<small>Errors will appear here when your script runs</small>
532+
</div>
533+
</div>
522534
</div>
523-
<div id="errorLogContainer" class="error-log-container">
524-
<div class="error-log-empty">
525-
<i data-feather="check-circle"></i>
526-
<p>No errors detected</p>
527-
<small>Errors will appear here when your script runs</small>
535+
536+
<div class="debug-section">
537+
<div class="debug-section-header">
538+
<h4 class="debug-section-title">GM Storage Viewer</h4>
539+
<div class="storage-controls-inline">
540+
<button type="button" id="refreshStorageBtn" class="btn-secondary">
541+
<i data-feather="refresh-cw"></i>
542+
<span>Refresh</span>
543+
</button>
544+
</div>
545+
</div>
546+
547+
<div class="storage-add-form">
548+
<input
549+
type="text"
550+
id="storageKeyInput"
551+
class="form-input"
552+
placeholder="Key"
553+
aria-label="Storage key"
554+
/>
555+
<textarea
556+
id="storageValueInput"
557+
class="form-textarea"
558+
placeholder='Value (JSON or plain text). Example: {"enabled": true}'
559+
rows="3"
560+
aria-label="Storage value"
561+
></textarea>
562+
<button type="button" id="setStorageBtn" class="btn-secondary">
563+
<i data-feather="save"></i>
564+
<span>Set Value</span>
565+
</button>
566+
</div>
567+
568+
<div id="storageViewerContainer" class="storage-viewer-container">
569+
<div class="storage-empty">
570+
<i data-feather="database"></i>
571+
<p>No stored values</p>
572+
<small>Values written by GM_setValue will appear here</small>
573+
</div>
528574
</div>
529575
</div>
530576
</div>

0 commit comments

Comments
 (0)