Skip to content

Commit 249a35d

Browse files
committed
Bulk export and how to menu
1 parent 7b26dd0 commit 249a35d

6 files changed

Lines changed: 261 additions & 25 deletions

File tree

Public/dashboard.html

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ <h2>General Settings</h2>
252252
<button type="reset" id="resetSettingsBtn" class="secondary">
253253
Reset to Defaults
254254
</button>
255+
<button type="button" id="exportAllBtn" class="secondary">
256+
Export All Scripts
257+
</button>
255258
</div>
256259
</div>
257260
</form>
@@ -263,29 +266,45 @@ <h2>General Settings</h2>
263266
role="tabpanel"
264267
aria-labelledby="about-tab-btn"
265268
>
266-
<div class="settings-section">
267-
<h2>About CodeTweak</h2>
268-
<p>
269-
CodeTweak is a powerful browser extension that allows you to
270-
inject custom JavaScript code into specific websites based on your
271-
preferences.
272-
</p>
273-
<div class="version-pill">Version 1.0.0</div>
269+
<div class="about-container">
270+
<aside class="about-sidebar" role="navigation" aria-label="About navigation">
271+
<button class="about-nav active" data-section="getting-started">Getting Started</button>
272+
<button class="about-nav" data-section="installing">Installing Scripts</button>
273+
<button class="about-nav" data-section="managing">Managing Scripts</button>
274+
<button class="about-nav" data-section="creating">Creating Scripts</button>
275+
<button class="about-nav" data-section="exporting">Exporting Scripts</button>
276+
</aside>
277+
278+
<div class="about-content">
279+
<div class="about-section active" id="getting-started">
280+
<h2>Getting Started</h2>
281+
<p>CodeTweak lets you run custom JavaScript ("user&nbsp;scripts") on the sites you choose. Click <em>New Script</em> on the dashboard, write or paste code, pick the target sites and save – that's it!</p>
282+
</div>
283+
284+
<div class="about-section" id="installing">
285+
<h2>Installing Scripts</h2>
286+
<h3>Greasy Fork (1-click)</h3>
287+
<p>Click <em>Browse Scripts</em> to open the Greasy&nbsp;Fork dialog, search for a script and press <em>Install</em>.</p>
288+
<h3>Drag &amp; Drop</h3>
289+
<p>Drag any <code>.js</code> or text file onto the dashboard to import it.</p>
290+
<h3>Paste Code</h3>
291+
<p>Open the editor, paste a script and click <em>Save</em>.</p>
292+
<h3>In-app Greasy Fork search</h3>
293+
<p>Use the search field inside the Greasy&nbsp;Fork modal to find scripts without leaving CodeTweak.</p>
294+
</div>
274295

275-
<h3>How to Use</h3>
276-
<ol>
277-
<li>Create a script by clicking the "New Script" button</li>
278-
<li>Specify the target website(s) where the script should run</li>
279-
<li>Write your JavaScript code in the editor</li>
280-
<li>
281-
Choose when the script should run (page start, DOM ready, page
282-
load, or when a specific element is loaded)
283-
</li>
284-
<li>
285-
Save your script and it will automatically run when you visit
286-
the specified websites
287-
</li>
288-
</ol>
296+
<div class="about-section" id="managing">
297+
<h2>Managing Scripts</h2>
298+
<p>Use the <em>Scripts</em> table to enable/disable, edit, update, export or delete each script. Filters above the table help locate scripts quickly.</p>
299+
<p>The badge on the extension icon shows how many scripts are active on the current tab.</p>
300+
</div>
301+
302+
<div class="about-section" id="creating">
303+
<h2>Creating Scripts</h2>
304+
<p>The built-in editor supports syntax highlighting and lets you opt-in to Greasemonkey (GM) APIs such as <code>GM_getValue</code>, <code>GM_openInTab</code> and <code>GM_notification</code>. Toggle the checkboxes to inject the APIs you need.</p>
305+
<p>You can also declare <code>@resource</code> and <code>@require</code> headers – the editor will fetch and bundle them automatically.</p>
306+
</div>
307+
</div>
289308
</div>
290309
</section>
291310
</main>

Public/dashboard.js

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
/* global showNotification chrome */
12
import { setupGreasyfork } from "./dashboard/dashboard-greasyfork.js";
23
import {
34
loadScripts,
45
loadSettings,
56
saveSettings,
67
filterScripts,
78
} from "./dashboard/dashboard-logic.js";
8-
import { setupTabs } from "./dashboard/dashboard-ui.js";
9+
import { setupTabs, setupAboutNav } from "./dashboard/dashboard-ui.js";
10+
import { parseUserScriptMetadata } from "./utils/metadataParser.js";
911

1012
function initDashboard() {
1113
const elements = {
@@ -14,6 +16,8 @@ function initDashboard() {
1416
scriptsList: document.getElementById("scriptsList"),
1517
emptyState: document.getElementById("emptyState"),
1618
saveSettingsBtn: document.getElementById("saveSettingsBtn"),
19+
resetSettingsBtn: document.getElementById("resetSettingsBtn"),
20+
exportAllBtn: document.getElementById("exportAllBtn"),
1721
tabs: document.querySelector(".tabs"),
1822
tabContents: document.querySelectorAll(".tab-content"),
1923
emptyStateCreateBtn: document.getElementById("emptyStateCreateBtn"),
@@ -47,7 +51,21 @@ function initDashboard() {
4751
loadScripts(elements, state);
4852
loadSettings(elements.settings);
4953
setupTabs(elements.tabs, elements.tabContents);
54+
// about tab sidebar
55+
setupAboutNav(document.querySelector("#about-tab .about-container"));
5056
setupGreasyfork(elements.greasyfork);
57+
setupFileDragAndDrop();
58+
59+
// Listen for runtime messages to reflect updates from other parts of the extension
60+
chrome.runtime.onMessage.addListener((message) => {
61+
if (message.action === "scriptsUpdated") {
62+
showNotification("Dashboard refreshed", "success");
63+
// reload scripts while preserving filters
64+
loadScripts(elements, state);
65+
} else if (message.action === "settingsUpdated") {
66+
showNotification("Settings updated", "success");
67+
}
68+
});
5169
}
5270

5371
function setupEventListeners(elements, state) {
@@ -63,6 +81,13 @@ function setupEventListeners(elements, state) {
6381
saveSettings(elements.settings)
6482
);
6583

84+
elements.resetSettingsBtn?.addEventListener("click", () => {
85+
showNotification("Settings reset to defaults", "success");
86+
});
87+
88+
// bulk export
89+
elements.exportAllBtn?.addEventListener("click", exportAllScripts);
90+
6691
elements.filters.scriptSearch?.addEventListener(
6792
"input",
6893
debounce(() => filterScripts(elements, state), 300)
@@ -80,6 +105,111 @@ function setupEventListeners(elements, state) {
80105
elements.filters.runAtFilter?.addEventListener("change", filterChangeHandler);
81106
}
82107

108+
function setupFileDragAndDrop() {
109+
const prevent = (e) => {
110+
e.preventDefault();
111+
e.stopPropagation();
112+
};
113+
114+
["dragenter", "dragover"].forEach((evt) => {
115+
document.addEventListener(evt, prevent, false);
116+
});
117+
118+
document.addEventListener(
119+
"drop",
120+
async (e) => {
121+
prevent(e);
122+
const files = Array.from(e.dataTransfer?.files || []);
123+
if (!files.length) return;
124+
125+
const validFiles = files.filter((f) => {
126+
const ext = f.name.split(".").pop().toLowerCase();
127+
return ["js", "txt"].includes(ext);
128+
});
129+
130+
if (!validFiles.length) {
131+
showNotification("Only .js and .txt files are supported", "warning");
132+
return;
133+
}
134+
135+
try {
136+
const { scripts = [] } = await chrome.storage.local.get("scripts");
137+
for (const file of validFiles) {
138+
const code = await file.text();
139+
const metadata = parseUserScriptMetadata(code);
140+
141+
const scriptData = {
142+
name:
143+
metadata.name ||
144+
file.name.replace(/\.(txt|js)$/i, "") ||
145+
"Imported Script",
146+
author: metadata.author || "Anonymous",
147+
description: metadata.description || "",
148+
version: metadata.version || "1.0.0",
149+
targetUrls: metadata.matches || ["*://*/*"],
150+
code,
151+
runAt: metadata.runAt || "document_end",
152+
enabled: true,
153+
id: crypto.randomUUID(),
154+
createdAt: Date.now(),
155+
updatedAt: Date.now(),
156+
...(metadata.resources && { resources: metadata.resources }),
157+
...(metadata.requires && { requires: metadata.requires }),
158+
...(metadata.license && { license: metadata.license }),
159+
...(metadata.icon && { icon: metadata.icon }),
160+
};
161+
162+
if (metadata.gmApis) {
163+
Object.keys(metadata.gmApis).forEach((flag) => {
164+
scriptData[flag] = metadata.gmApis[flag];
165+
});
166+
}
167+
168+
scripts.push(scriptData);
169+
}
170+
171+
await chrome.storage.local.set({ scripts });
172+
chrome.runtime.sendMessage({ action: "scriptsUpdated" });
173+
showNotification("Scripts imported successfully", "success");
174+
175+
// Refresh dashboard view
176+
window.location.reload();
177+
} catch (err) {
178+
console.error("Import failed:", err);
179+
showNotification("Failed to import scripts", "error");
180+
}
181+
},
182+
false
183+
);
184+
}
185+
186+
async function exportAllScripts() {
187+
console.log("Exporting all scripts");
188+
try {
189+
const { scripts = [] } = await chrome.storage.local.get("scripts");
190+
if (!scripts.length) {
191+
showNotification("No scripts to export", "warning");
192+
return;
193+
}
194+
for (const script of scripts) {
195+
const blob = new Blob([script.code], { type: "text/javascript" });
196+
const url = URL.createObjectURL(blob);
197+
const safeName = (script.name || "script")
198+
.replace(/[^a-z0-9\- _]/gi, "_")
199+
.replace(/\s+/g, "_");
200+
chrome.downloads.download({
201+
url,
202+
filename: `CodeTweak Export/${safeName}.user.js`,
203+
saveAs: false,
204+
});
205+
}
206+
showNotification("All scripts exported", "success");
207+
} catch (err) {
208+
console.error("Export failed", err);
209+
showNotification("Failed to export scripts", "error");
210+
}
211+
}
212+
83213
function debounce(func, delay) {
84214
let timeout;
85215
return function () {

Public/dashboard/dashboard-logic.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ async function loadSettings(settingsElements) {
125125
enableAllScripts: true,
126126
showNotifications: true,
127127
confirmBeforeRunning: false,
128-
darkMode: true,
128+
// follow system theme by default
129+
darkMode: window.matchMedia("(prefers-color-scheme: dark)").matches,
129130
};
130131

131132
// Apply defaults and update storage if needed

Public/dashboard/dashboard-ui.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,10 +396,45 @@ function escapeHtml(unsafe) {
396396
.replace(/&/g, "&amp;")
397397
.replace(/</g, "&lt;")
398398
.replace(/>/g, "&gt;")
399-
.replace(/"/g, "&quot;")
399+
.replace(/\"/g, "&quot;")
400400
.replace(/'/g, "&#039;");
401401
}
402402

403+
// ----------------------------------------------
404+
// About Tab helpers
405+
// ----------------------------------------------
406+
function setupAboutNav(aboutContainer) {
407+
if (!aboutContainer) return;
408+
409+
const navButtons = aboutContainer.querySelectorAll(".about-nav");
410+
const sections = aboutContainer.querySelectorAll(".about-section");
411+
412+
// ensure exporting section exists
413+
const contentEl = aboutContainer.querySelector(".about-content");
414+
if (contentEl && !contentEl.querySelector("#exporting")) {
415+
contentEl.insertAdjacentHTML(
416+
"beforeend",
417+
`<div class="about-section" id="exporting">
418+
<h2>Exporting Scripts</h2>
419+
<p>Select the export icon in the <em>Scripts</em> table to download a script as a standalone <code>.user.js</code> file for sharing or backups.</p>
420+
</div>`
421+
);
422+
}
423+
424+
navButtons.forEach((btn) => {
425+
btn.addEventListener("click", () => {
426+
navButtons.forEach((b) => {
427+
b.classList.remove("active");
428+
});
429+
sections.forEach((s) => s.classList.remove("active"));
430+
431+
btn.classList.add("active");
432+
const targetId = btn.dataset.section;
433+
aboutContainer.querySelector(`#${targetId}`)?.classList.add("active");
434+
});
435+
});
436+
}
437+
403438
// Expose helpers for other modules
404439
window.updateWebsiteFilterOptions = updateWebsiteFilterOptions;
405440
window.updateScriptsList = updateScriptsList;
@@ -408,6 +443,7 @@ window.escapeHtml = escapeHtml;
408443

409444
export {
410445
setupTabs,
446+
setupAboutNav,
411447
updateWebsiteFilterOptions,
412448
updateScriptsList,
413449
showNotification,

Public/manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"contextMenus",
1414
"clipboardWrite",
1515
"notifications",
16+
"downloads",
1617
"offscreen"
1718
],
1819
"host_permissions": [

Public/styles/dashboard.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,55 @@ input:checked + .slider:before {
755755
color: var(--text-primary);
756756
}
757757

758+
.about-container {
759+
display: flex;
760+
gap: var(--space-lg);
761+
align-items: flex-start;
762+
}
763+
764+
.about-sidebar {
765+
display: flex;
766+
flex-direction: column;
767+
gap: var(--space-xs);
768+
}
769+
770+
.about-nav {
771+
padding: var(--space-sm) var(--space-md);
772+
border: 1px solid var(--border-secondary);
773+
background: var(--bg-secondary);
774+
border-radius: var(--radius-md);
775+
color: var(--text-secondary);
776+
cursor: pointer;
777+
transition: var(--transition-all);
778+
font-size: var(--text-sm);
779+
text-align: left;
780+
}
781+
782+
.about-nav:hover {
783+
background: var(--bg-hover);
784+
color: var(--primary);
785+
}
786+
787+
.about-nav.active {
788+
background: var(--primary);
789+
border-color: var(--primary);
790+
color: #fff;
791+
}
792+
793+
.about-content {
794+
flex: 1;
795+
min-width: 0;
796+
}
797+
798+
.about-section {
799+
display: none;
800+
}
801+
802+
.about-section.active {
803+
display: block;
804+
animation: fadeIn var(--transition-slow) ease;
805+
}
806+
758807
@keyframes slideIn {
759808
from {
760809
transform: translateX(100%);

0 commit comments

Comments
 (0)