Bitcointalk Editor + Image Uploader

Toggle SCEditor with native preview + upload images to hostmeme.com and insert BBCode automatically

ของเมื่อวันที่ 22-08-2025 ดู เวอร์ชันล่าสุด

// ==UserScript==
// @name         Bitcointalk Editor + Image Uploader
// @namespace    Royal Cap
// @version      1.0
// @description  Toggle SCEditor with native preview + upload images to hostmeme.com and insert BBCode automatically
// @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);

  waitForTextarea().then(init).catch(() => {});

  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) {}
        }
      });
    }

    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 };
  }

  function createEditor(textarea, ui) {
    try {
      sceditor.create(textarea, {
        format: 'bbcode',
        style: THEME_CONTENT,
        autoExpand: true,
        autofocus: true,
        enablePasteFiltering: true,
        autoUpdate: true
      });

      const inst = getInstance(textarea);

      inst.bind('valuechanged', () => {
        try {
          inst.updateOriginal();
          const previewEl = document.querySelector('#preview_body');
          if (previewEl) {
            previewEl.innerHTML = inst.fromBBCode(inst.val(), 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.styleSheets].some(s => s.href && s.href.includes(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) {
            const bbcode = `[img]${data.url}[/img]`;
            const textarea = document.querySelector("textarea[name='message']");
            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 });
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。