Skip to content

Commit 7b26dd0

Browse files
committed
Export scripts
1 parent 5de7507 commit 7b26dd0

12 files changed

Lines changed: 402 additions & 89 deletions

Public/dashboard.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ <h1>CodeTweak</h1>
172172
<th scope="col">Name</th>
173173
<th scope="col">Author</th>
174174
<th scope="col">Website Icons</th>
175+
<th scope="col">Icon</th>
175176
<th scope="col">Run At</th>
176177
<th scope="col">Version</th>
177178
<th scope="col">Actions</th>
@@ -348,8 +349,8 @@ <h2>Browse Greasy Fork Scripts</h2>
348349
</div>
349350

350351
<script src="utils/favicon.js" defer></script>
351-
<script src="dashboard/dashboard-ui.js" defer></script>
352-
<script src="dashboard/dashboard-logic.js" defer></script>
352+
<script type="module" src="dashboard/dashboard-ui.js" defer></script>
353+
<script type="module" src="dashboard/dashboard-logic.js" defer></script>
353354
<script type="module" src="dashboard/dashboard-greasyfork.js" defer></script>
354355
<script type="module" src="dashboard.js" defer></script>
355356
</body>

Public/dashboard.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import { setupGreasyfork } from "./dashboard/dashboard-greasyfork.js";
2+
import {
3+
loadScripts,
4+
loadSettings,
5+
saveSettings,
6+
filterScripts,
7+
} from "./dashboard/dashboard-logic.js";
8+
import { setupTabs } from "./dashboard/dashboard-ui.js";
29

310
function initDashboard() {
411
const elements = {

Public/dashboard/dashboard-greasyfork.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ async function importGreasyforkScript(codeUrl) {
163163
...(metadata.resources && { resources: metadata.resources }),
164164
...(metadata.requires && { requires: metadata.requires }),
165165
...(metadata.license && { license: metadata.license }),
166+
...(metadata.icon && { icon: metadata.icon }),
166167
};
167168
Object.keys(metadata.gmApis).forEach((apiFlag) => {
168169
scriptData[apiFlag] = metadata.gmApis[apiFlag];

Public/dashboard/dashboard-logic.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
buildTampermonkeyMetadata,
3+
extractMetadataBlock,
4+
} from "../utils/metadataParser.js";
5+
16
async function loadScripts(elements, state) {
27
try {
38
const { scripts = [] } = await chrome.storage.local.get("scripts");
@@ -268,3 +273,57 @@ function applyTheme(isDark) {
268273
body.classList.add("light-theme");
269274
}
270275
}
276+
277+
// Export current script in Tampermonkey format (.user.js)
278+
function exportScript(script) {
279+
try {
280+
const code = script.code || "";
281+
const hasMetadata = !!extractMetadataBlock(code);
282+
const metadata = hasMetadata ? "" : buildTampermonkeyMetadata(script);
283+
const content = hasMetadata ? code : `${metadata}\n\n${code}`;
284+
285+
const fileNameSafe = (script.name || "script")
286+
.replace(/[^a-z0-9_-]+/gi, "_")
287+
.replace(/_{2,}/g, "_")
288+
.replace(/^_|_$/g, "") || "script";
289+
290+
const blob = new Blob([content], {
291+
type: "text/javascript;charset=utf-8",
292+
});
293+
const url = URL.createObjectURL(blob);
294+
const a = document.createElement("a");
295+
a.href = url;
296+
a.download = `${fileNameSafe}.user.js`;
297+
document.body.appendChild(a);
298+
a.click();
299+
document.body.removeChild(a);
300+
URL.revokeObjectURL(url);
301+
302+
showNotification("Script exported", "success");
303+
} catch (error) {
304+
console.error("Export failed:", error);
305+
showNotification("Export failed", "error");
306+
}
307+
}
308+
309+
// Attach functions to global so dashboard-ui can use them
310+
window.exportScript = exportScript;
311+
window.toggleScript = toggleScript;
312+
window.editScript = editScript;
313+
window.deleteScript = deleteScript;
314+
window.checkForUpdates = checkForUpdates;
315+
window.refreshDashboard = refreshDashboard;
316+
317+
export {
318+
loadScripts,
319+
filterScripts,
320+
toggleScript,
321+
editScript,
322+
deleteScript,
323+
loadSettings,
324+
saveSettings,
325+
checkForUpdates,
326+
refreshDashboard,
327+
applyTheme,
328+
exportScript,
329+
};

Public/dashboard/dashboard-ui.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ function createScriptRow(script) {
9393
// Favicon cell
9494
row.appendChild(createFaviconCell(script));
9595

96+
// Icon cell
97+
row.appendChild(createIconCell(script));
98+
9699
// Run At cell
97100
const runAtCell = document.createElement("td");
98101
const timingInfo = document.createElement("div");
@@ -187,6 +190,29 @@ function createFaviconCell(script) {
187190
return faviconCell;
188191
}
189192

193+
function createIconCell(script) {
194+
const iconCell = document.createElement("td");
195+
const container = document.createElement("div");
196+
container.className = "icon-container";
197+
198+
if (script.icon) {
199+
const img = document.createElement("img");
200+
img.src = script.icon;
201+
img.alt = "";
202+
img.className = "script-icon";
203+
img.onerror = () => {
204+
img.remove();
205+
container.textContent = "N/A";
206+
};
207+
container.appendChild(img);
208+
} else {
209+
container.textContent = "-";
210+
}
211+
212+
iconCell.appendChild(container);
213+
return iconCell;
214+
}
215+
190216
function createFaviconWrapper(hostname) {
191217
const faviconWrapper = document.createElement("div");
192218
faviconWrapper.className = "favicon-wrapper";
@@ -271,6 +297,15 @@ function createActionsCell(script) {
271297
});
272298
}
273299

300+
actions.push({
301+
icon: `<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
302+
<polyline points="12 5 12 19" />
303+
<polyline points="5 12 12 19 19 12" />
304+
</svg>`,
305+
title: "Export Script",
306+
handler: () => window.exportScript && window.exportScript(script),
307+
});
308+
274309
actions.push({
275310
icon: `<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
276311
<polyline points="3 6 5 6 21 6" />
@@ -364,3 +399,17 @@ function escapeHtml(unsafe) {
364399
.replace(/"/g, "&quot;")
365400
.replace(/'/g, "&#039;");
366401
}
402+
403+
// Expose helpers for other modules
404+
window.updateWebsiteFilterOptions = updateWebsiteFilterOptions;
405+
window.updateScriptsList = updateScriptsList;
406+
window.showNotification = showNotification;
407+
window.escapeHtml = escapeHtml;
408+
409+
export {
410+
setupTabs,
411+
updateWebsiteFilterOptions,
412+
updateScriptsList,
413+
showNotification,
414+
escapeHtml,
415+
};

Public/editor.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@
109109
<label for="scriptLicense" class="form-label">License</label>
110110
<input type="text" id="scriptLicense" class="form-input" placeholder="e.g., MIT, GPL-3.0, CC-BY-4.0">
111111
</div>
112+
<div class="form-group">
113+
<label for="scriptIcon" class="form-label">Icon URL</label>
114+
<input type="url" id="scriptIcon" class="form-input" placeholder="https://example.com/icon.png">
115+
</div>
112116
</div>
113117
</div>
114118
<!-- GM API Access -->

Public/editor.js

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
FormValidator
77
} from "./utils/editor_managers.js";
88
import { CodeEditorManager } from "./utils/editor_settings.js";
9+
import { buildTampermonkeyMetadata, parseUserScriptMetadata } from "./utils/metadataParser.js";
910

1011
class ScriptEditor {
1112
constructor() {
@@ -164,6 +165,7 @@ class ScriptEditor {
164165
"scriptName",
165166
"scriptAuthor",
166167
"scriptLicense",
168+
"scriptIcon",
167169
"targetUrl",
168170
"runAt",
169171
"scriptVersion",
@@ -324,6 +326,7 @@ class ScriptEditor {
324326
if (metadata.namespace) scriptData.namespace = metadata.namespace;
325327
if (metadata.runAt) scriptData.runAt = metadata.runAt;
326328
if (metadata.license) scriptData.license = metadata.license;
329+
if (metadata.icon) scriptData.icon = metadata.icon;
327330

328331
// Handle matches and includes
329332
if (metadata.matches?.length) {
@@ -373,53 +376,22 @@ class ScriptEditor {
373376
if (!importData) return;
374377

375378
const { code } = importData;
376-
377-
// Parse metadata block
378-
const metaMatch = code.match(/==UserScript==([\s\S]*?)==\/UserScript==/);
379-
const metadata = {};
380-
if (metaMatch) {
381-
metaMatch[1].split("\n").forEach((line) => {
382-
const m = line.match(/@(\w+)\s+(.+)/);
383-
if (m) {
384-
const [, key, value] = m;
385-
if (key === "match" || key === "include") {
386-
metadata.matches = metadata.matches || [];
387-
metadata.matches.push(value.trim());
388-
} else if (key === "grant") {
389-
metadata.grants = metadata.grants || [];
390-
metadata.grants.push(value.trim());
391-
} else if (key === "require") {
392-
metadata.requires = metadata.requires || [];
393-
metadata.requires.push(value.trim());
394-
} else {
395-
metadata[key] = value.trim();
396-
}
397-
}
398-
});
399-
}
400-
401-
const scriptObj = {
402-
name: metadata.name || "Imported Script",
403-
author: metadata.author || "Anonymous",
404-
description: metadata.description || "",
405-
version: metadata.version || this.config.DEFAULT_VERSION,
406-
targetUrls: metadata.matches || ["*://*/*"],
407-
runAt: metadata.runAt || "document_end",
408-
code,
409-
requires: metadata.requires || [],
410-
};
411-
412-
this.populateFormWithScript(scriptObj);
413-
379+
380+
// Parse metadata using shared utility for full support
381+
const metadata = parseUserScriptMetadata(code);
382+
383+
// Delegate to existing import handler for form population
384+
this.handleScriptImport({ code, ...metadata });
385+
414386
// Mark as unsaved draft for user review (but DO NOT autosave)
415387
this.state.hasUnsavedChanges = true;
416388
this.ui.updateScriptStatus(true);
417-
389+
418390
// Clean up storage
419391
await chrome.storage.local.remove(key);
420392
} catch (err) {
421-
console.error("Failed to load imported script:", err);
422-
this.ui.showStatusMessage("Failed to load imported script", "error");
393+
console.error('Error loading imported script:', err);
394+
this.ui.showStatusMessage('Failed to load imported script', 'error');
423395
}
424396
}
425397

@@ -554,7 +526,7 @@ class ScriptEditor {
554526

555527
// Form field change listeners for autosave
556528
const formFields = [
557-
'scriptName', 'scriptAuthor', 'scriptVersion', 'scriptDescription', 'scriptLicense',
529+
'scriptName', 'scriptAuthor', 'scriptVersion', 'scriptDescription', 'scriptLicense', 'scriptIcon',
558530
'runAt', 'waitForSelector', 'targetUrl'
559531
];
560532

@@ -649,6 +621,7 @@ class ScriptEditor {
649621
script.version || this.config.DEFAULT_VERSION;
650622
this.elements.scriptDescription.value = script.description || "";
651623
this.elements.scriptLicense.value = script.license || "";
624+
this.elements.scriptIcon.value = script.icon || "";
652625
this.codeEditorManager.setValue(script.code || "");
653626

654627
script.targetUrls?.forEach((url) => this.ui.addUrlToList(url));
@@ -726,6 +699,7 @@ class ScriptEditor {
726699
this.elements.scriptVersion.value.trim() || this.config.DEFAULT_VERSION,
727700
description: this.elements.scriptDescription.value.trim(),
728701
license: this.elements.scriptLicense?.value.trim() || "",
702+
icon: this.elements.scriptIcon?.value.trim() || "",
729703
code: this.codeEditorManager.getValue(),
730704
enabled: true,
731705
updatedAt: new Date().toISOString(),
@@ -912,6 +886,37 @@ class ScriptEditor {
912886
console.warn("Initial background connection failed:", error);
913887
}
914888
}
889+
890+
/**
891+
* Export current script in classic Tampermonkey format (.user.js)
892+
*/
893+
exportScript() {
894+
try {
895+
const scriptData = this.gatherScriptData();
896+
const metadata = buildTampermonkeyMetadata(scriptData);
897+
const content = `${metadata}\n\n${scriptData.code}`;
898+
899+
const fileNameSafe = (scriptData.name || 'script')
900+
.replace(/[^a-z0-9_-]+/gi, '_')
901+
.replace(/_{2,}/g, '_')
902+
.replace(/^_|_$/g, '') || 'script';
903+
904+
const blob = new Blob([content], { type: 'text/javascript;charset=utf-8' });
905+
const url = URL.createObjectURL(blob);
906+
const a = document.createElement('a');
907+
a.href = url;
908+
a.download = `${fileNameSafe}.user.js`;
909+
document.body.appendChild(a);
910+
a.click();
911+
document.body.removeChild(a);
912+
URL.revokeObjectURL(url);
913+
914+
this.ui.showStatusMessage('Script exported', 'success');
915+
} catch (err) {
916+
console.error('Export failed:', err);
917+
this.ui.showStatusMessage('Export failed', 'error');
918+
}
919+
}
915920
}
916921

917922
// Main init for editor

Public/popup.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,16 @@ document.addEventListener("DOMContentLoaded", async () => {
126126
const info = document.createElement("div");
127127
info.className = "script-info";
128128

129+
// Icon
130+
if (script.icon) {
131+
const iconImg = document.createElement("img");
132+
iconImg.src = script.icon;
133+
iconImg.alt = "";
134+
iconImg.className = "script-icon";
135+
iconImg.onerror = () => iconImg.remove();
136+
item.appendChild(iconImg);
137+
}
138+
129139
const name = document.createElement("div");
130140
name.className = "script-name";
131141
name.textContent = script.name;

Public/styles/dashboard.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,20 @@ input:checked + .slider:before {
374374
background: var(--bg-primary);
375375
}
376376

377+
.icon-container {
378+
display: flex;
379+
align-items: center;
380+
justify-content: center;
381+
}
382+
383+
.script-icon {
384+
width: 18px;
385+
height: 18px;
386+
border-radius: var(--radius-sm);
387+
object-fit: contain;
388+
background: var(--bg-primary);
389+
}
390+
377391
.favicon-fallback {
378392
width: 18px;
379393
height: 18px;

0 commit comments

Comments
 (0)