Greasy Fork is available in English.
Simplify hashtags on X/Twitter & Bluesky: group in collapsible bundles, insert/copy, combine bundles in a pop-up, and share via export/import.
当前为
// ==UserScript==
// @name Hashtag Bundler for X/Twitter and Bluesky
// @namespace https://codymkw.nekoweb.org/
// @version 2.3
// @description Simplify hashtags on X/Twitter & Bluesky: group in collapsible bundles, insert/copy, combine bundles in a pop-up, and share via export/import.
// @author Cody
// @match https://twitter.com/*
// @match https://x.com/*
// @match https://bsky.app/*
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function () {
'use strict';
let PANEL_ID, STORAGE_KEY;
const hostname = window.location.hostname;
if (hostname === 'twitter.com' || hostname === 'x.com') {
PANEL_ID = 'twitter-hashtag-panel';
STORAGE_KEY = 'twitterHashtagBundles';
} else if (hostname === 'bsky.app') {
PANEL_ID = 'bluesky-hashtag-panel';
STORAGE_KEY = 'blueskyHashtagBundles';
} else return;
const COMBINED_KEY = STORAGE_KEY + '_combined';
let panelVisible = false;
const loadData = () => JSON.parse(GM_getValue(STORAGE_KEY, '{}'));
const saveData = (d) => GM_setValue(STORAGE_KEY, JSON.stringify(d));
const loadCombined = () => JSON.parse(GM_getValue(COMBINED_KEY, '{}'));
const saveCombined = (d) => GM_setValue(COMBINED_KEY, JSON.stringify(d));
const el = (tag, props = {}, children = []) => {
const e = document.createElement(tag);
Object.assign(e, props);
children.forEach(c => e.appendChild(c));
return e;
};
const createPanel = () => {
if (document.getElementById(PANEL_ID)) return;
const panel = el('div', {
id: PANEL_ID,
style: `
position: fixed; right: 20px; bottom: 20px;
width: 280px; background: #1f1f1f; color: white;
border-radius: 12px; padding: 10px; font-family: sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,0.4); z-index: 99999;
`
});
const style = document.createElement("style");
style.textContent = `
#bundle-container::-webkit-scrollbar { width: 6px; }
#bundle-container::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
#bundle-container::-webkit-scrollbar-track { background: #222; }
`;
document.head.appendChild(style);
const header = el('div', { style: 'font-weight:bold;margin-bottom:6px;display:flex;justify-content:space-between;align-items:center;cursor:pointer;' });
const title = el('span', { textContent: 'Hashtag Bundles' });
const toggle = el('span', { textContent: '▼', style: 'font-size:14px;' });
header.append(title, toggle);
const container = el('div', { id: 'bundle-container', style: 'overflow-y:auto; max-height:300px; padding-right:3px;' });
panel.appendChild(header);
panel.appendChild(container);
document.body.appendChild(panel);
let collapsed = false;
header.onclick = () => {
collapsed = !collapsed;
container.style.display = collapsed ? 'none' : 'block';
toggle.textContent = collapsed ? '▲' : '▼';
};
const render = () => {
container.innerHTML = '';
const data = loadData();
// New Bundle Button
const addBtn = el('button', { textContent:'➕ New Bundle', style:'width:100%;padding:8px;margin-bottom:6px;border:none;border-radius:6px;background:#5865F2;color:white;font-weight:bold;cursor:pointer;' });
addBtn.onclick = () => {
const name = prompt('Bundle name?'); if(!name) return;
const tags = prompt('Enter hashtags separated by spaces — "#" optional:'); if(!tags) return;
data[name] = tags.split(/\s+/).filter(Boolean).map(t=>t.startsWith('#')?t:'#'+t);
saveData(data); render();
};
container.appendChild(addBtn);
// Combine Bundles Button (create new combined bundle)
const combineBtn = el('button', { textContent:'🔗 Combine Bundles', style:'width:100%;padding:8px;margin-bottom:6px;border:none;border-radius:6px;background:#9b59b6;color:white;font-weight:bold;cursor:pointer;' });
combineBtn.onclick = () => createCombinedBundle(data);
container.appendChild(combineBtn);
// Show Combined Bundles Button (pop-up)
const showCombinedBtn = el('button', { textContent:'📂 Show Combined Bundles', style:'width:100%;padding:8px;margin-bottom:6px;border:none;border-radius:6px;background:#8e44ad;color:white;font-weight:bold;cursor:pointer;' });
showCombinedBtn.onclick = () => openCombinedPopup();
container.appendChild(showCombinedBtn);
// Export / Import
const ioControls = el('div',{style:'display:flex;gap:6px;margin-bottom:10px;'});
const makeIOBtn = (label,bg) => el('button',{textContent:label,style:`flex:1;padding:8px;border:none;border-radius:6px;background:${bg};color:white;font-weight:bold;cursor:pointer;`});
const exportBtn = makeIOBtn('Export','#4CAF50');
const importBtn = makeIOBtn('Import','#2196F3');
exportBtn.onclick = () => {
const blob = new Blob([JSON.stringify(loadData(),null,2)],{type:'application/json'});
const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download=`${STORAGE_KEY}.json`; a.click(); URL.revokeObjectURL(url);
};
importBtn.onclick = () => {
const input = document.createElement('input'); input.type='file'; input.accept='application/json';
input.onchange = (e)=>{ const file=e.target.files[0]; if(!file) return;
const reader=new FileReader(); reader.onload=(ev)=>{ try { const imported=JSON.parse(ev.target.result); saveData({...loadData(),...imported}); render(); alert('Import OK!'); } catch{ alert('Invalid JSON.'); } }; reader.readAsText(file);
}; input.click();
};
ioControls.append(exportBtn,importBtn); container.appendChild(ioControls);
// Render Bundles
Object.keys(data).forEach(bundleName => {
const entry = el('div',{style:'background:#262626;border-radius:8px;margin-bottom:8px;padding:6px 8px;'});
const row = el('div',{style:'display:flex;justify-content:space-between;align-items:center;cursor:pointer;'});
const nameEl = el('span',{textContent:bundleName,style:'font-weight:bold;font-size:14px;'});
const arrow = el('span',{textContent:'▶',style:'font-size:13px;transition:0.2s;'});
row.append(nameEl,arrow); entry.appendChild(row);
const inner = el('div',{style:'display:none;margin-top:6px;padding-top:6px;border-top:1px solid #444;font-size:12px;color:#ccc;'});
const tagText = el('div',{textContent:data[bundleName].join(' '),style:'overflow-wrap:anywhere;margin-bottom:6px;'});
const controls = el('div',{style:'display:flex;gap:6px;'});
const makeBtn = (label,bg) => el('button',{textContent:label,style:`padding:4px 6px;border:none;border-radius:4px;background:${bg};color:white;cursor:pointer;font-size:11px;`});
const insertBtn = makeBtn('Insert','#2196F3'); const copyBtn=makeBtn('Copy','#4CAF50'); const editBtn=makeBtn('Edit','#555'); const delBtn=makeBtn('Delete','#c0392b');
insertBtn.onclick=()=>{ const composer=document.querySelector('div[contenteditable="true"]'); if(!composer) return alert('Composer not found.'); composer.focus(); document.execCommand('insertText',false,data[bundleName].join(' ')+' '); insertBtn.textContent='Inserted!'; setTimeout(()=>insertBtn.textContent='Insert',800); };
copyBtn.onclick=async()=>{ await navigator.clipboard.writeText(data[bundleName].join(' ')).catch(()=>alert('Copy error')); copyBtn.textContent='Copied!'; setTimeout(()=>copyBtn.textContent='Copy',800); };
editBtn.onclick=()=>{ const newTags=prompt('Edit hashtags separated by spaces — "#" optional:',data[bundleName].join(' ')); if(!newTags) return; data[bundleName]=newTags.split(/\s+/).filter(Boolean).map(t=>t.startsWith('#')?t:'#'+t); saveData(data); render(); };
delBtn.onclick=()=>{ if(!confirm(`Delete "${bundleName}"?`)) return; delete data[bundleName]; saveData(data); render(); };
controls.append(insertBtn,copyBtn,editBtn,delBtn); inner.append(tagText,controls); entry.appendChild(inner);
row.onclick=()=>{ const show=inner.style.display==='none'; inner.style.display=show?'block':'none'; arrow.style.transform=show?'rotate(90deg)':'rotate(0deg)'; };
container.appendChild(entry);
});
};
render(); panelVisible=true;
};
const removePanel = () => { const p=document.getElementById(PANEL_ID); if(p)p.remove(); panelVisible=false; };
// Create Combined Bundle via prompts
const createCombinedBundle = (data) => {
const bundleNames = Object.keys(data); if(!bundleNames.length) return alert('No bundles to combine.');
const selected=prompt(`Enter bundle names to combine, separated by commas:\nAvailable: ${bundleNames.join(', ')}`); if(!selected) return;
const names=selected.split(',').map(n=>n.trim()).filter(Boolean); if(!names.every(n=>bundleNames.includes(n))) return alert('Invalid bundle names.');
const combinedTags=[...new Set(names.flatMap(n=>data[n]))];
const newName=prompt('Name for the new combined bundle?'); if(!newName) return;
const combinedData=loadCombined(); combinedData[newName]={tags:combinedTags,sources:names}; saveCombined(combinedData);
alert(`Combined bundle "${newName}" created!`);
};
// Open centered pop-up for combined bundles
const openCombinedPopup = () => {
const combinedData=loadCombined(); if(!Object.keys(combinedData).length) return alert('No combined bundles yet.');
if(document.getElementById('combined-popup')) return;
const popup=el('div',{id:'combined-popup',style:'position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:320px;max-height:400px;background:#1f1f1f;color:white;border-radius:12px;padding:12px;overflow-y:auto;box-shadow:0 2px 12px rgba(0,0,0,0.5);z-index:100000;'});
const header=el('div',{style:'display:flex;justify-content:space-between;font-weight:bold;margin-bottom:8px;'}); const title=el('span',{textContent:'Combined Bundles'}); const closeBtn=el('span',{textContent:'✖',style:'cursor:pointer'}); closeBtn.onclick=()=>popup.remove(); header.append(title,closeBtn); popup.appendChild(header);
Object.keys(combinedData).forEach(name=>{
const combo=combinedData[name];
const entry=el('div',{style:'background:#262626;border-radius:8px;padding:6px;margin-bottom:6px;'});
const nameEl=el('div',{textContent:`${name} (from: ${combo.sources.join(', ')})`, style:'font-weight:bold;font-size:13px;margin-bottom:4px;'});
const tagText=el('div',{textContent:combo.tags.join(' '),style:'overflow-wrap:anywhere;font-size:12px;margin-bottom:4px;'});
const controls=el('div',{style:'display:flex;gap:6px;'});
const makeBtn=(text,bg,onClick)=>el('button',{textContent:text,style:`flex:1;padding:4px;border-radius:4px;background:${bg};color:white;cursor:pointer;font-size:11px;`,onclick:onClick});
const insertBtn=makeBtn('Insert','#2196F3',()=>{const c=document.querySelector('div[contenteditable="true"]'); if(c) document.execCommand('insertText',false,combo.tags.join(' ')+' ');});
const copyBtn=makeBtn('Copy','#4CAF50',async()=>{await navigator.clipboard.writeText(combo.tags.join(' ')).catch(()=>alert('Copy error'));});
const delBtn=makeBtn('Delete','#c0392b',()=>{delete combinedData[name]; saveCombined(combinedData); entry.remove();});
controls.append(insertBtn,copyBtn,delBtn); entry.append(nameEl,tagText,controls); popup.appendChild(entry);
});
document.body.appendChild(popup);
};
const observer = new MutationObserver(()=>{ const composer=document.querySelector('div[contenteditable="true"]'); if(composer && !panelVisible) createPanel(); else if(!composer && panelVisible) removePanel(); });
observer.observe(document.body,{childList:true,subtree:true});
})();