您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Toggle SCEditor with native preview + upload images to hostmeme.com and keep [img width=.. height=..] for Bitcointalk
// ==UserScript== // @name Bitcointalk Editor + Image Uploader // @namespace Royal Cap // @version 1.1 // @description Toggle SCEditor with native preview + upload images to hostmeme.com and keep [img width=.. height=..] for Bitcointalk // @match https://bitcointalk.org/index.php?* // @run-at document-start // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @require https://cdn.jsdelivr.net/npm/sceditor@3/minified/sceditor.min.js // @require https://cdn.jsdelivr.net/npm/sceditor@3/minified/formats/bbcode.min.js // @license MIT // ==/UserScript== (function () { 'use strict'; const THEME_UI = 'https://cdn.jsdelivr.net/npm/sceditor@3/minified/themes/default.min.css'; const THEME_CONTENT = 'https://cdn.jsdelivr.net/npm/sceditor@3/minified/themes/content/default.min.css'; const STORAGE_KEY = 'btc_sceditor_enabled'; GM_addStyle(` .tm-sce-toolbar{display:flex;gap:.5rem;align-items:center;margin:8px 0 6px 0;} .tm-sce-btn{cursor:pointer;padding:.35rem .6rem;border:1px solid #aaa;border-radius:8px;background:#f4f4f4;font:inherit} .tm-sce-btn:hover{background:#e9e9e9} .tm-sce-badge{font-size:.85em;color:#444} `); addStylesheetOnce(THEME_UI); // --- Core: wait for the forum textarea and boot waitForTextarea().then(init).catch(() => {}); // --- Normalize any [img=WxH] to [img width=W height=H] function normalizeImgSizeAttrs(bbcode) { return bbcode.replace(/\[img=(\d+)x(\d+)\]([^\[]+?)\[\/img\]/gi, '[img width=$1 height=$2]$3[/img]'); } function init(textarea) { if (textarea.dataset.tmSceReady) return; textarea.dataset.tmSceReady = '1'; const ui = buildUI(); textarea.parentElement.insertBefore(ui.toolbar, textarea); const lastEnabled = tryGetBool(STORAGE_KEY, false); ui.toggleBtn.addEventListener('click', () => { if (textarea.dataset.tmSceEnabled === '1') { destroyEditor(textarea, ui); GM_setValue(STORAGE_KEY, false); } else { createEditor(textarea, ui); GM_setValue(STORAGE_KEY, true); } }); const form = textarea.closest('form'); if (form) { form.addEventListener('submit', () => { const inst = getInstance(textarea); if (inst) { try { inst.updateOriginal(); } catch (e) {} } // Final safety pass on submit textarea.value = normalizeImgSizeAttrs(textarea.value); }); } if (lastEnabled) createEditor(textarea, ui); // Add Image Upload Button beside "Post" button addUploadButton(); } function buildUI() { const toolbar = document.createElement('div'); toolbar.className = 'tm-sce-toolbar'; const toggleBtn = document.createElement('button'); toggleBtn.type = 'button'; toggleBtn.className = 'tm-sce-btn'; toggleBtn.textContent = 'Enable Editor'; const badge = document.createElement('span'); badge.className = 'tm-sce-badge'; badge.textContent = '(native textarea)'; toolbar.append(toggleBtn, badge); return { toolbar, toggleBtn, badge }; } // --- The important part: override SCEditor’s img BBCode to force width/height attributes function overrideImgBBCode() { if (!window.sceditor || !sceditor.formats || !sceditor.formats.bbcode) return; // Define an img tag that always outputs [img width=.. height=..] and understands [img=WxH] on input sceditor.formats.bbcode.set('img', { // Match any <img> and capture width/height where present tags: { img: { width: null, height: null, src: null } }, isInline: true, allowsEmpty: false, // HTML -> BBCode format: function (element/* HTMLElement */, content/* string */) { if (!element || !element.getAttribute) return content; const src = element.getAttribute('src') || ''; const w = element.getAttribute('width'); const h = element.getAttribute('height'); // Always prefer attribute form for Bitcointalk const parts = []; if (w) parts.push('width=' + w); if (h) parts.push('height=' + h); const attr = parts.length ? ' ' + parts.join(' ') : ''; return `[img${attr}]${src}[/img]`; }, // BBCode -> HTML html: function (token, attrs, content) { // Accept either [img width=.. height=..] or [img=WxH] let w = attrs.width || null; let h = attrs.height || null; // If writer used [img=WxH], parse it and convert to attributes const def = attrs.defaultattr || attrs.defaultAttr || null; if ((!w || !h) && def && /^(\d+)x(\d+)$/i.test(def)) { const m = String(def).match(/^(\d+)x(\d+)$/i); if (m) { w = w || m[1]; h = h || m[2]; } } const wAttr = w ? ` width="${w}"` : ''; const hAttr = h ? ` height="${h}"` : ''; const safeSrc = String(content || '').trim(); // Self-closing img is fine for the WYSIWYG surface return `<img src="${safeSrc}"${wAttr}${hAttr} />`; }, // Never quote numeric attrs in the output BBCode quoteType: sceditor.BBCodeParser.QuoteType.never }); } function createEditor(textarea, ui) { try { // Important: override must happen before create() overrideImgBBCode(); sceditor.create(textarea, { format: 'bbcode', style: THEME_CONTENT, autoExpand: true, autofocus: true, enablePasteFiltering: true, autoUpdate: true }); const inst = getInstance(textarea); // Keep preview in sync and normalize any legacy [img=WxH] inst.bind('valuechanged', () => { try { // First sync to the original inst.updateOriginal(); // Then normalize original BBCode to attribute style textarea.value = normalizeImgSizeAttrs(textarea.value); // Update Bitcointalk’s native preview if present const previewEl = document.querySelector('#preview_body'); if (previewEl) { previewEl.innerHTML = inst.fromBBCode(textarea.value, true); } } catch (e) {} }); textarea.dataset.tmSceEnabled = '1'; ui.toggleBtn.textContent = 'Disable Editor'; ui.badge.textContent = '(Editor active)'; } catch (err) { console.error('[SCEditor Toggle] Failed:', err); alert('Could not initialize SCEditor.'); } } function destroyEditor(textarea, ui) { const inst = getInstance(textarea); if (inst) { try { inst.updateOriginal(); } catch (e) {} try { inst.unbind('valuechanged'); inst.destroy(); } catch (e) {} } textarea.dataset.tmSceEnabled = '0'; ui.toggleBtn.textContent = 'Enable Editor'; ui.badge.textContent = '(native textarea)'; } function getInstance(textarea) { try { return sceditor.instance(textarea); } catch { return null; } } function addStylesheetOnce(href) { if (document.querySelector(`link[rel="stylesheet"][href="${href}"]`)) return; const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = href; document.documentElement.appendChild(link); } function waitForTextarea() { return new Promise((resolve, reject) => { const direct = findTextarea(); if (direct) return resolve(direct); const obs = new MutationObserver(() => { const ta = findTextarea(); if (ta) { obs.disconnect(); resolve(ta); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); setTimeout(() => { obs.disconnect(); reject(new Error('No textarea found')); }, 8000); }); } function findTextarea() { return document.querySelector('textarea[name="message"]'); } function tryGetBool(key, defVal) { try { return !!GM_getValue(key, defVal); } catch { return defVal; } } // ========================= // IMAGE UPLOADER SECTION // ========================= function addUploadButton() { const postBtn = document.querySelector("input[name='post']"); if (!postBtn || document.getElementById("uploadImageBtn")) return; const uploadBtn = document.createElement("button"); uploadBtn.id = "uploadImageBtn"; uploadBtn.innerText = "Upload Image"; uploadBtn.type = "button"; uploadBtn.style.marginLeft = "10px"; uploadBtn.style.padding = "5px 10px"; postBtn.parentNode.insertBefore(uploadBtn, postBtn.nextSibling); uploadBtn.addEventListener("click", () => { const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.onchange = async () => { const file = input.files[0]; if (!file) return; const formData = new FormData(); formData.append("image", file); uploadBtn.innerText = "Uploading..."; try { const response = await fetch("https://hostmeme.com/bitcointalk.php", { method: "POST", body: formData, }); const data = await response.json(); if (data.success && data.url && data.width && data.height) { // Note: width first, then height to match Bitcointalk usage const bbcode = `[img width=${data.width} height=${data.height}]${data.url}[/img]`; // If editor is active, insert through SCEditor to keep preview synced const textarea = document.querySelector("textarea[name='message']"); const inst = textarea ? getInstance(textarea) : null; if (inst) { inst.insert(bbcode); try { inst.updateOriginal(); } catch (e) {} textarea.value = normalizeImgSizeAttrs(textarea.value); } else if (textarea) { textarea.value += `\n${bbcode}\n`; } } else { alert("Upload failed: " + (data.error || "Unknown error")); } } catch (err) { alert("Upload error: " + err.message); } finally { uploadBtn.innerText = "Upload Image"; } }; input.click(); }); } // Observer in case form loads dynamically const observer = new MutationObserver(addUploadButton); observer.observe(document.body, { childList: true, subtree: true }); })();