A hub to manage and run bookmarklets. Ctrl+Alt+B to open panel. Import/Export JSON and Drag & Drop added. FIXES ADDING BOOKMARKLETS.
// ==UserScript==
// @name Bookmarklet Hub
// @namespace http://tampermonkey.net/
// @version 1.91
// @description A hub to manage and run bookmarklets. Ctrl+Alt+B to open panel. Import/Export JSON and Drag & Drop added. FIXES ADDING BOOKMARKLETS.
// @author Dijon The Mustard and *A LOT* of ChatGPT
// @license MIT
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const PANEL_ID = 'bookmarklet-hub-panel';
const STORAGE_KEY = 'bookmarkletHubData';
let bookmarklets = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
let draggedItem = null;
function saveBookmarklets() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarklets));
}
const fontLink = document.createElement('link');
fontLink.href = 'https://fonts.googleapis.com/css2?family=Outfit:wght@400;600&display=swap';
fontLink.rel = 'stylesheet';
document.head.appendChild(fontLink);
const style = document.createElement('style');
style.textContent = `
.bm-folder-items {
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 0.5s ease, opacity 0.5s ease;
}
.bm-folder-items.open {
max-height: 1000px;
opacity: 1;
}
.hub-button {
background: transparent;
color: #00c853;
border: 1px solid #00c853;
border-radius: 4px;
padding: 4px 8px;
font-weight: bold;
font-size: 14px;
cursor: pointer;
margin-right: 6px;
}
.hub-button:hover {
background-color: #00c85311;
}
.bookmarklet-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
cursor: grab;
}
.bookmarklet-item.dragging {
opacity: 0.5;
}
.folder-summary {
cursor: pointer;
font-weight: bold;
margin-top: 8px;
display: flex;
align-items: center;
}
.folder-summary span {
color: #ffd83d;
margin-right: 4px;
}
.folder-summary.drag-over {
background-color: rgba(255, 216, 61, 0.1);
}
`;
document.head.appendChild(style);
function createPanel() {
if (document.getElementById(PANEL_ID)) return;
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.style.position = 'fixed';
panel.style.top = '10px';
panel.style.left = '10px';
panel.style.zIndex = 99999;
panel.style.padding = '12px';
panel.style.background = '#222';
panel.style.color = 'white';
panel.style.border = '1px solid #444';
panel.style.borderRadius = '10px';
panel.style.boxShadow = '0 0 15px rgba(0,0,0,0.6)';
panel.style.maxWidth = '340px';
panel.style.fontFamily = "'Outfit', sans-serif";
panel.style.cursor = 'move';
panel.innerHTML = `
<div id="bookmarklet-hub-header" style="margin-bottom:10px; font-weight:600; font-size:22px; cursor:move; text-align: center; letter-spacing: 1px; color: #ffd83d;">
>> Bookmarklet Hub <<
<button id="close-hub" style="
float:right;
background:transparent;
color:#ff0033;
border:1px solid #ff0033;
border-radius:6px;
padding:0 8px;
cursor:pointer;
font-weight:bold;
font-size:14px;
">✕</button>
</div>
<input id="search-bar" type="text" placeholder="Search bookmarklets..." style="width: 100%; padding: 6px 8px; margin-bottom: 12px; border-radius: 4px; border: none; font-size: 14px;" />
<ul id="bookmarklet-list" style="list-style:none; padding-left:0; max-height:300px; overflow-y: auto;"></ul>
<hr style="margin:12px 0; border:1px solid #999;">
<div style="margin-bottom: 10px;">
<input id="bm-name" placeholder="Name" style="width:45%; margin:6px 0; padding:4px; border-radius:4px; border:none;" />
<input id="bm-code" placeholder="Code (no javascript:)" style="width:100%; margin-bottom:6px; padding:4px; border-radius:4px; border:none;" />
<input id="bm-folder" placeholder="Folder (optional)" style="width:100%; margin-bottom:6px; padding:4px; border-radius:4px; border:none;" />
<button id="add-bookmarklet" style="
background: transparent;
background-clip: text;
-webkit-background-clip: text;
color: #00c853;
border: none;
padding:6px 12px;
font-weight:bold;
font-size:14px;
cursor:pointer;
">+ Add Bookmarklet</button>
</div>
<hr style="margin:12px 0; border:1px solid #999;">
<div style="display: flex; gap: 8px; margin-top: 10px;">
<button id="import-bookmarklets" class="hub-button">Import JSON</button>
<button id="export-bookmarklets" class="hub-button">Export JSON</button>
</div>
`;
document.body.appendChild(panel);
let isDraggingPanel = false;
let panelOffsetX = 0;
let panelOffsetY = 0;
const header = panel.querySelector('#bookmarklet-hub-header');
header.addEventListener('mousedown', (e) => {
isDraggingPanel = true;
panelOffsetX = e.clientX - panel.offsetLeft;
panelOffsetY = e.clientY - panel.offsetTop;
document.body.style.userSelect = 'none';
});
document.addEventListener('mouseup', () => {
isDraggingPanel = false;
document.body.style.userSelect = '';
});
document.addEventListener('mousemove', (e) => {
if (isDraggingPanel) {
panel.style.left = (e.clientX - panelOffsetX) + 'px';
panel.style.top = (e.clientY - panelOffsetY) + 'px';
}
});
panel.querySelector('#close-hub').onclick = () => panel.remove();
panel.querySelector('#add-bookmarklet').onclick = () => {
const name = panel.querySelector('#bm-name').value.trim();
const code = panel.querySelector('#bm-code').value.trim();
const folder = panel.querySelector('#bm-folder').value.trim() || 'Root';
if (!name || !code) return;
bookmarklets.push({ name, code, folder });
saveBookmarklets();
renderList();
};
panel.querySelector('#search-bar').addEventListener('input', function() {
const query = this.value.toLowerCase();
renderList(query);
});
panel.querySelector('#import-bookmarklets').addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'application/json';
fileInput.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const importedData = JSON.parse(e.target.result);
if (Array.isArray(importedData)) {
bookmarklets = importedData;
saveBookmarklets();
renderList();
alert('Bookmarklets imported successfully!');
} else {
alert('Invalid JSON format.');
}
} catch (error) {
alert('Error parsing JSON file.');
}
};
reader.readAsText(file);
}
};
fileInput.click();
});
panel.querySelector('#export-bookmarklets').addEventListener('click', () => {
const jsonString = JSON.stringify(bookmarklets, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'bookmarklets.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
renderList();
}
function renderList(query = '') {
const list = document.querySelector('#bookmarklet-list');
if (!list) return;
list.innerHTML = '';
const folders = {};
bookmarklets.forEach((bm, index) => { // Use the top-level bookmarklets array
const folder = bm.folder || 'Root';
if (!folders[folder]) folders[folder] = [];
folders[folder].push({ ...bm, _index: index });
});
Object.entries(folders).forEach(([folderName, items]) => {
const folderEl = document.createElement('li');
const summary = document.createElement('div');
summary.classList.add('folder-summary');
summary.innerHTML = `<span aria-hidden="true">⮞</span> ${folderName}`;
let isExpanded = false;
const itemList = document.createElement('ul');
itemList.classList.add('bm-folder-items');
itemList.style.listStyle = 'none';
itemList.style.paddingLeft = '12px';
summary.onclick = () => {
isExpanded = !isExpanded;
itemList.classList.toggle('open', isExpanded);
summary.querySelector('span').textContent = isExpanded ? '⮟' : '⮞';
};
summary.addEventListener('dragover', (e) => {
e.preventDefault();
if (draggedItem && draggedItem.folder !== folderName) {
summary.classList.add('drag-over');
}
});
summary.addEventListener('dragleave', () => {
summary.classList.remove('drag-over');
});
summary.addEventListener('drop', (e) => {
e.preventDefault();
summary.classList.remove('drag-over');
if (draggedItem && draggedItem.folder !== folderName) {
const oldIndex = bookmarklets.findIndex(bm => bm.name === draggedItem.name && bm.code === draggedItem.code && bm.folder === draggedItem.folder);
if (oldIndex !== -1) {
bookmarklets[oldIndex].folder = folderName;
saveBookmarklets();
renderList();
}
draggedItem = null;
}
});
items.forEach((bm) => {
if (query && !bm.name.toLowerCase().includes(query)) return;
const li = document.createElement('li');
li.classList.add('bookmarklet-item');
li.draggable = true;
li.dataset.index = bm._index;
const controls = document.createElement('div');
controls.style.display = 'flex';
controls.style.alignItems = 'center';
const runBtn = document.createElement('button');
runBtn.textContent = '⮞';
runBtn.style.background = 'transparent';
runBtn.style.color = '#00c853';
runBtn.style.border = '1px solid #00c853';
runBtn.style.borderRadius = '4px';
runBtn.style.marginRight = '6px';
runBtn.style.padding = '2px 6px';
runBtn.style.cursor = 'pointer';
runBtn.onclick = () => {
try {
const fn = new Function(bm.code);
fn();
} catch (e) {
alert('Error running bookmarklet: ' + e);
}
};
controls.appendChild(runBtn);
const nameSpan = document.createElement('span');
nameSpan.textContent = bm.name;
nameSpan.style.flexGrow = '1';
controls.appendChild(nameSpan);
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '✕';
deleteBtn.style.background = 'transparent';
deleteBtn.style.color = '#d50000';
deleteBtn.style.border = '1px solid #d50000';
deleteBtn.style.borderRadius = '4px';
deleteBtn.style.padding = '2px 6px';
deleteBtn.style.cursor = 'pointer';
deleteBtn.onclick = () => {
bookmarklets.splice(bookmarklets.findIndex(b => b._index === bm._index), 1);
saveBookmarklets();
renderList();
};
controls.appendChild(deleteBtn);
li.appendChild(controls);
itemList.appendChild(li);
li.addEventListener('dragstart', (e) => {
draggedItem = { name: bm.name, code: bm.code, folder: bm.folder, index: bm._index };
li.classList.add('dragging');
e.dataTransfer.setData('text/plain', ''); // Required for Firefox
});
li.addEventListener('dragend', () => {
li.classList.remove('dragging');
draggedItem = null;
});
});
// Open the folder if there are any matching items
if (query) {
const hasMatch = items.some(bm => bm.name.toLowerCase().includes(query));
if (hasMatch) {
itemList.classList.add('open');
summary.querySelector('span').textContent = '⮟';
}
}
folderEl.appendChild(summary);
folderEl.appendChild(itemList);
list.appendChild(folderEl);
});
}
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.altKey && e.key === 'b') {
e.preventDefault();
createPanel();
}
});
})();