Skip to content

Commit 84ba1b6

Browse files
committed
Docs and metadata support
1 parent cb24b8c commit 84ba1b6

16 files changed

Lines changed: 3855 additions & 52 deletions

Chrome/dashboard/dashboard-greasyfork.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { parseUserScriptMetadata } from "../utils/metadataParser.js";
2+
import { showNotification } from "./dashboard-ui.js";
23
function setupGreasyfork(elements) {
34
if (!elements.button) return;
45

@@ -43,7 +44,7 @@ async function searchGreasyfork(elements) {
4344
try {
4445
const encodedQuery = encodeURIComponent(query);
4546
const response = await fetch(
46-
`https://api.greasyfork.org/en/scripts.json?q=${encodedQuery}`
47+
`https://greasyfork.org/en/scripts/list.json?q=${encodedQuery}`
4748
);
4849

4950
if (!response.ok) {

Chrome/dashboard/dashboard-logic.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,6 @@ async function deleteScript(scriptId) {
9494
document.dispatchEvent(new CustomEvent("scriptsChanged"));
9595
chrome.runtime.sendMessage({ action: "scriptsUpdated" });
9696

97-
showNotification(
98-
`Deleted script: ${scriptToDelete?.name || "Unknown"}`,
99-
"success"
100-
);
101-
10297
const elements = {
10398
scriptsTable: document.getElementById("scriptsTable"),
10499
scriptsList: document.getElementById("scriptsList"),

Chrome/editor.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@
9999
<path d="M21 12c0 1.657-4.03 3-9 3s-9-1.343-9-3"></path>
100100
</svg>
101101
</button>
102+
<div class="sidebar-divider"></div>
103+
<button class="sidebar-icon-btn" id="generateHeaderBtn" title="Generate Metadata header">
104+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
105+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
106+
<polyline points="14 2 14 8 20 8"></polyline>
107+
<line x1="16" y1="13" x2="8" y2="13"></line>
108+
<line x1="16" y1="17" x2="8" y2="17"></line>
109+
<polyline points="10 9 9 9 8 9"></polyline>
110+
</svg>
111+
</button>
102112
</div>
103113

104114
<!-- Content Area -->

Chrome/editor.js

Lines changed: 211 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* global chrome */
21

32
import {
43
UIManager,
@@ -30,6 +29,9 @@ class ScriptEditor {
3029
lintingEnabled: localStorage.getItem("lintingEnabled") === "true",
3130
isAutosaveEnabled: localStorage.getItem("autosaveEnabled") === "true",
3231
autosaveTimeout: null,
32+
headerSyncTimeout: null,
33+
sidebarSyncTimeout: null,
34+
isUpdatingFromSidebar: false,
3335
hasUserInteraction: false,
3436
codeEditor: null,
3537
};
@@ -125,34 +127,156 @@ class ScriptEditor {
125127
);
126128
}
127129

128-
_debouncedSave(force = false) {
129-
// Clear existing timeout
130+
_debouncedSave() {
130131
if (this.state.autosaveTimeout) {
131132
clearTimeout(this.state.autosaveTimeout);
132-
this.state.autosaveTimeout = null;
133133
}
134134

135-
if (this.state.isAutosaveEnabled || force) {
136-
this.state.autosaveTimeout = setTimeout(async () => {
137-
if (this.state.hasUnsavedChanges && this.state.codeEditor) {
135+
this.state.autosaveTimeout = setTimeout(async () => {
136+
if (this.state.hasUnsavedChanges) {
137+
if (this.state.isEditMode) {
138138
try {
139139
await this.saveScript(true);
140-
141-
if (!force) {
142-
setTimeout(() => {
143-
if (!this.state.hasUnsavedChanges) {
144-
this.ui.clearStatusMessage();
145-
}
146-
}, 2000);
147-
}
148140
} catch (error) {
149-
console.error("Error during autosave:", error);
150-
if (!force) {
141+
console.error("Autosave failed:", error);
142+
if (this.ui && this.ui.showStatusMessage) {
151143
this.ui.showStatusMessage("Autosave failed", "error");
152144
}
153145
}
154146
}
155-
}, this.config.AUTOSAVE_DELAY);
147+
}
148+
}, this.config.AUTOSAVE_DELAY);
149+
}
150+
151+
_debouncedHeaderSync() {
152+
if (this.state.headerSyncTimeout) {
153+
clearTimeout(this.state.headerSyncTimeout);
154+
}
155+
156+
this.state.headerSyncTimeout = setTimeout(() => {
157+
// Skip if we're currently updating from sidebar to prevent loops
158+
if (!this.state.isUpdatingFromSidebar) {
159+
this.syncHeaderToSidebar();
160+
}
161+
}, 500); // 500ms debounce
162+
}
163+
164+
_debouncedSidebarSync() {
165+
if (this.state.sidebarSyncTimeout) {
166+
clearTimeout(this.state.sidebarSyncTimeout);
167+
}
168+
169+
this.state.sidebarSyncTimeout = setTimeout(() => {
170+
this.syncSidebarToHeader();
171+
}, 500); // 500ms debounce
172+
}
173+
174+
syncHeaderToSidebar() {
175+
try {
176+
const currentCode = this.codeEditorManager.getValue();
177+
const headerMatch = currentCode.match(/\/\/ ==UserScript==[\s\S]*?\/\/ ==\/UserScript==/);
178+
179+
if (!headerMatch) return; // No header found
180+
181+
const metadata = parseUserScriptMetadata(headerMatch[0]);
182+
183+
// Overwrite sidebar fields with header metadata
184+
if (metadata.name) this.elements.scriptName.value = metadata.name;
185+
if (metadata.author) this.elements.scriptAuthor.value = metadata.author;
186+
if (metadata.version) this.elements.scriptVersion.value = metadata.version;
187+
if (metadata.description) this.elements.scriptDescription.value = metadata.description;
188+
if (metadata.license) this.elements.scriptLicense.value = metadata.license;
189+
if (metadata.icon) this.elements.scriptIcon.value = metadata.icon;
190+
if (metadata.runAt) this.elements.runAt.value = metadata.runAt.replace(/-/g, '_');
191+
192+
// Update target URLs (replace list with header values)
193+
if (this.elements.urlList) {
194+
this.elements.urlList.innerHTML = '';
195+
}
196+
if (Array.isArray(metadata.matches) && metadata.matches.length > 0) {
197+
metadata.matches.forEach(match => {
198+
this.ui.addUrlToList(match);
199+
});
200+
}
201+
202+
// Reset and update GM API checkboxes to reflect @grant
203+
if (this.gmApiDefinitions) {
204+
Object.values(this.gmApiDefinitions).forEach(def => {
205+
const el = this.elements[def.el];
206+
if (el) el.checked = false;
207+
});
208+
}
209+
if (metadata.gmApis) {
210+
Object.keys(metadata.gmApis).forEach(apiFlag => {
211+
const element = this.elements[apiFlag];
212+
if (element) {
213+
element.checked = !!metadata.gmApis[apiFlag];
214+
}
215+
});
216+
}
217+
this.updateApiCount();
218+
// Update dependent sections visibility after grants change
219+
this.toggleResourcesSection(
220+
this.elements.gmGetResourceText?.checked || this.elements.gmGetResourceURL?.checked
221+
);
222+
this.toggleRequiredScriptsSection();
223+
224+
// Update resources (replace list with header values)
225+
if (this.elements.resourceList) {
226+
this.elements.resourceList.innerHTML = '';
227+
}
228+
if (Array.isArray(metadata.resources) && metadata.resources.length > 0) {
229+
metadata.resources.forEach(resource => {
230+
this.ui.addResourceToList(resource.name, resource.url);
231+
});
232+
}
233+
234+
// Update required scripts from @require (replace list)
235+
if (this.elements.requireList) {
236+
this.elements.requireList.innerHTML = '';
237+
}
238+
if (Array.isArray(metadata.requires) && metadata.requires.length > 0) {
239+
metadata.requires.forEach(url => this.ui.addRequireToList(url));
240+
}
241+
242+
} catch {
243+
// Silently fail - don't spam console during normal editing
244+
}
245+
}
246+
247+
syncSidebarToHeader() {
248+
try {
249+
const currentCode = this.codeEditorManager.getValue();
250+
const headerMatch = currentCode.match(/\/\/ ==UserScript==[\s\S]*?\/\/ ==\/UserScript==/);
251+
252+
// Generate new header from current sidebar data
253+
const scriptData = this.gatherScriptData();
254+
const newMetadata = buildTampermonkeyMetadata(scriptData);
255+
256+
let newCode;
257+
if (headerMatch) {
258+
// Replace existing header
259+
newCode = currentCode.replace(headerMatch[0], newMetadata);
260+
} else {
261+
// Insert header at the beginning
262+
newCode = newMetadata + '\n\n' + currentCode;
263+
}
264+
265+
// Only update if the code actually changed to avoid infinite loops
266+
if (newCode !== currentCode) {
267+
// Set flag to prevent header sync during this update
268+
this.state.isUpdatingFromSidebar = true;
269+
270+
this.codeEditorManager.setValue(newCode);
271+
272+
// Reset flag after a short delay to allow future header syncing
273+
setTimeout(() => {
274+
this.state.isUpdatingFromSidebar = false;
275+
}, 100);
276+
}
277+
278+
} catch {
279+
// Silently fail - don't spam console during normal editing
156280
}
157281
}
158282

@@ -218,7 +342,8 @@ class ScriptEditor {
218342
"addRequireBtn",
219343
"requireURL",
220344
"requireList",
221-
"helpButton"
345+
"helpButton",
346+
"generateHeaderBtn"
222347
];
223348

224349
const elements = {};
@@ -260,6 +385,8 @@ class ScriptEditor {
260385
if (this.state.isAutosaveEnabled) {
261386
this._debouncedSave();
262387
}
388+
// Check for header changes and update sidebar
389+
this._debouncedHeaderSync();
263390
});
264391
this.codeEditorManager.setImportCallback((importData) => this.handleScriptImport(importData));
265392
this.codeEditorManager.setStatusCallback((message, type) => {
@@ -491,6 +618,12 @@ class ScriptEditor {
491618
e.preventDefault();
492619
this.saveScript();
493620
});
621+
622+
// Add click listener to generate header button
623+
this.elements.generateHeaderBtn?.addEventListener('click', (e) => {
624+
e.preventDefault();
625+
this.generateTampermonkeyHeader();
626+
});
494627

495628
// Setup UI callbacks - UIManager initializes everything in constructor, no init() method needed
496629
const callbacks = {
@@ -514,15 +647,28 @@ class ScriptEditor {
514647

515648
// Setup additional UI components that need callbacks
516649
this.ui.setupSettingsModal(callbacks);
517-
this.ui.setupUrlManagement({ markAsUnsaved: () => this.markAsUnsaved() });
518-
this.ui.setupResourceManagement(callbacks);
650+
this.ui.setupUrlManagement({
651+
markAsUnsaved: () => {
652+
this.markAsUnsaved();
653+
this._debouncedSidebarSync();
654+
}
655+
});
656+
this.ui.setupResourceManagement({
657+
...callbacks,
658+
markAsUnsaved: () => {
659+
this.markAsUnsaved();
660+
this._debouncedSidebarSync();
661+
}
662+
});
519663

520664
// Add both change and input events for better responsiveness
521665
const handleChange = () => {
522666
this.markAsDirty();
523667
if (this.state.isAutosaveEnabled) {
524668
this._debouncedSave();
525669
}
670+
// Sync sidebar changes to header
671+
this._debouncedSidebarSync();
526672
};
527673

528674
// Form inputs that should trigger change detection
@@ -586,12 +732,17 @@ class ScriptEditor {
586732
sidebarIconBtns.forEach(btn => btn.classList.remove('active'));
587733
this.elements.sidebarPanels.forEach(panel => panel.classList.remove('active'));
588734
this.elements.sidebarContentArea.style.display = 'none';
589-
this.elements.sidebarContentArea.style.width = '0';
590735

591736
sidebarIconBtns.forEach(btn => {
592737
btn.addEventListener('click', () => {
593-
const section = btn.dataset.section;
738+
const section = btn.getAttribute('data-section');
739+
740+
// Skip if this is the generate header button (no data-section)
741+
if (!section) return;
742+
594743
const panel = document.getElementById(`${section}-panel`);
744+
if (!panel) return;
745+
595746
const isCurrentlyActive = btn.classList.contains('active');
596747

597748
if (isCurrentlyActive) {
@@ -969,6 +1120,43 @@ class ScriptEditor {
9691120
this.ui.showStatusMessage('Export failed', 'error');
9701121
}
9711122
}
1123+
1124+
/**
1125+
* Generate Tampermonkey-style header and insert at top of code editor
1126+
*/
1127+
generateTampermonkeyHeader() {
1128+
try {
1129+
const scriptData = this.gatherScriptData();
1130+
const metadata = buildTampermonkeyMetadata(scriptData);
1131+
1132+
// Get current code content
1133+
const currentCode = this.codeEditorManager.getValue();
1134+
1135+
// Check if there's already a userscript header
1136+
const existingHeaderMatch = currentCode.match(/\/\/ ==UserScript==[\s\S]*?\/\/ ==\/UserScript==/);
1137+
1138+
let newCode;
1139+
if (existingHeaderMatch) {
1140+
// Replace existing header
1141+
newCode = currentCode.replace(existingHeaderMatch[0], metadata);
1142+
this.ui.showStatusMessage('Metadata updated', 'success');
1143+
} else {
1144+
// Insert header at the beginning
1145+
newCode = metadata + '\n\n' + currentCode;
1146+
this.ui.showStatusMessage('Metadata generated', 'success');
1147+
}
1148+
1149+
// Set the new code content
1150+
this.codeEditorManager.setValue(newCode);
1151+
1152+
// Mark as dirty to indicate changes
1153+
this.markAsDirty();
1154+
1155+
} catch (err) {
1156+
console.error('Generate header failed:', err);
1157+
this.ui.showStatusMessage('Failed to generate header', 'error');
1158+
}
1159+
}
9721160
}
9731161

9741162
// Main init for editor

0 commit comments

Comments
 (0)