Altcoinstalks Editor + Image Uploader

Toggle SCEditor with native preview + upload images to hostmeme.com for Altcoinstalks

// ==UserScript==
// @name         Altcoinstalks Editor + Image Uploader
// @namespace    Royal Cap
// @version      1.0
// @description  Toggle SCEditor with native preview + upload images to hostmeme.com for Altcoinstalks
// @match        https://www.altcoinstalks.com/index.php?*
// @run-at       document-end
// @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 = 'alt_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 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) {}
        }
        textarea.value = normalizeImgSizeAttrs(textarea.value);
      });
    }

    if (lastEnabled) createEditor(textarea, ui);

    // Try repeatedly until the button appears
    const interval = setInterval(() => {
      if (addUploadButton()) clearInterval(interval);
    }, 1000);
  }

  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 overrideImgBBCode() {
    if (!window.sceditor || !sceditor.formats || !sceditor.formats.bbcode) return;

    sceditor.formats.bbcode.set('img', {
      tags: { img: { width: null, height: null, src: null } },
      isInline: true,
      allowsEmpty: false,
      format: function (element, content) {
        if (!element || !element.getAttribute) return content;
        const src = element.getAttribute('src') || '';
        const w = element.getAttribute('width');
        const h = element.getAttribute('height');
        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]`;
      },
      html: function (token, attrs, content) {
        let w = attrs.width || null;
        let h = attrs.height || null;
        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();
        return `<img src="${safeSrc}"${wAttr}${hAttr} />`;
      },
      quoteType: sceditor.BBCodeParser.QuoteType.never
    });
  }

  function createEditor(textarea, ui) {
    try {
      overrideImgBBCode();

      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();
          textarea.value = normalizeImgSizeAttrs(textarea.value);
          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"]') ||
      document.querySelector('textarea[name="post"]') ||
      document.querySelector('textarea')
    );
  }

  function tryGetBool(key, defVal) {
    try { return !!GM_getValue(key, defVal); } catch { return defVal; }
  }

  function addUploadButton() {
    let postBtn =
      document.querySelector("input[name='post'], input[name='postmodify'], input[value='Post'], input[value='Save']");

    if (!postBtn || document.getElementById("uploadImageBtn")) return false;

    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 height=${data.height}]${data.url}[/img]`;

            const textarea = findTextarea();
            const inst = textarea ? getInstance(textarea) : null;
            if (inst) {
              inst.insert(bbcode);
              inst.updateOriginal();
              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();
    });

    return true;
  }

})();