Mobile Base64 Text Decoder

Base64 decoder with movable toggle button, mobile touch support, toast, Tampermonkey menu, color picker, and gesture support

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Mobile Base64 Text Decoder
// @version      1.6
// @description  Base64 decoder with movable toggle button, mobile touch support, toast, Tampermonkey menu, color picker, and gesture support
// @match        *://*/*
// @license      MIT
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-end
// @noframes
// @namespace https://greasyfork.org/users/1548859
// ==/UserScript==

;(function () {
  'use strict'
  if (window.top !== window.self) return

  const BTN_ID = 'b64-mobile-toggle-btn'
  if (document.getElementById(BTN_ID)) return

  const DEFAULT_PREFS = { autoCopy: false, toastDuration: 1200, colorIndex: 0 }
  const prefs = Object.assign({}, DEFAULT_PREFS, GM_getValue('b64MobileDecoderPrefs', {}))
  const menuIds = {}

  // ==============================
  // 🎨 CSS Styles
  // ==============================
  GM_addStyle(`
.b64-toggle-btn {
  position: fixed;
  width: 40px;
  height: 40px;
  border-radius: 20px;
  background: rgba(0,0,0,0.6);
  color: #fff;
  font-size: 16px;
  line-height: 40px;
  text-align: center;
  cursor: pointer;
  max-height: 300px;
  overflow: auto;
  z-index: 2147483647;
  user-select: none;
  transition: background 0.2s, box-shadow 0.2s;
  pointer-events: auto;
}
.b64-toggle-btn.active {
  background: var(--accent-color, rgba(0,128,255,0.8));
  box-shadow: 0 0 8px 2px var(--accent-color, rgba(0,128,255,0.8));
}
.b64-toast {
  position: fixed;
  padding: 6px 12px;
  background: rgba(0,0,0,0.8);
  color: var(--accent-color, #0f0);
  font-size: 12px;
  border-radius: 4px;
  opacity: 0;
  transition: opacity 0.3s;
  z-index: 2147483647;
}
.b64-toast.show {
  opacity: 1;
}
.b64-float {
  position: absolute;
  padding: 10px;
  background: rgba(0,0,0,0.8);
  color: #fff;
  border-radius: 5px;
  font-size: 14px;
  z-index: 2147483647;
  display: none;
  max-width: 80%;
  word-wrap: break-word;
  box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  user-select: none;
  cursor: pointer;
}
.b64-color-overlay {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.7);
  z-index: 2147483647;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  padding: 20px;
}
.b64-color-swatch {
  width: 40px; height: 40px;
  border-radius: 20px;
  margin: 10px;
  border: 2px solid #fff;
  cursor: pointer;
}
.b64-color-done {
  flex-basis: 100%;
  margin-top: 20px;
  padding: 10px 20px;
  background: #fff;
  color: #000;
  border-radius: 6px;
  font-weight: bold;
  text-align: center;
  cursor: pointer;
}
`)

  // ==============================
  // 🧱 UI Elements
  // ==============================
  const toggleBtn = document.createElement('div')
  toggleBtn.id = BTN_ID
  toggleBtn.className = 'b64-toggle-btn'
  toggleBtn.textContent = 'B64'
  document.body.appendChild(toggleBtn)

  const toast = document.createElement('div')
  toast.className = 'b64-toast'
  document.body.appendChild(toast)

  let floatingWin = null
  function getFloating() {
    if (floatingWin) return floatingWin
    const win = document.createElement('div')
    win.className = 'b64-float'
    document.body.appendChild(win)
    win.addEventListener('click', e => {
      e.stopPropagation()
      if (decodedText) {
        copyToClipboard(decodedText)
        showToast('Copied ✓')
      }
    })
    floatingWin = win
    return win
  }

  function showToast(msg) {
    const color = COLORS[prefs.colorIndex %COLORS.length];
    toast.style.color = color;
    toast.textContent = msg
    toast.style.display = 'block'
    const btnRect = toggleBtn.getBoundingClientRect()
    const toastW = toast.offsetWidth
    const left = btnRect.left + (btnRect.width - toastW) / 2
    const top = btnRect.bottom + 8
    toast.style.left = `${left}px`
    toast.style.top = `${top}px`
    toast.classList.add('show')
    setTimeout(() => {
      toast.classList.remove('show')
      toast.style.display = 'none'
    }, prefs.toastDuration)
  }

  // ==============================
  // 🎨 Color Palette & Initial Color
  // ==============================
  const COLORS = [
    'rgba(0,124,62,0.8)', // green
    'rgba(0,128,255,0.8)', // blue
    'rgba(255,64,64,0.8)', // red
    'rgba(255,165,0,0.8)', // orange
    'rgba(128,0,255,0.8)' // purple
  ]

  function applyUIColor() {
    const color = COLORS[prefs.colorIndex % COLORS.length]
    toggleBtn.style.setProperty('--accent-color', color)
  }

  applyUIColor()

  // Double-tap gesture to cycle colors
  let lastTap = 0
  toggleBtn.addEventListener('touchend', e => {
    const now = Date.now()
    if (now - lastTap < 300) {
      prefs.colorIndex = (prefs.colorIndex + 1) % COLORS.length
      GM_setValue('b64MobileDecoderPrefs', prefs)
      applyUIColor()
      showToast('Color changed')
      e.preventDefault()
    }
    lastTap = now
  }, { passive: false })


  // ==============================
  // 🛠️ Tampermonkey Menu
  // ==============================
  function registerMenu() {
    if (typeof GM_registerMenuCommand !== 'function') return

    // Unregister old commands to prevent duplicates
    if (menuIds.toggleBtn) GM_unregisterMenuCommand(menuIds.toggleBtn)
    if (menuIds.autoCopy) GM_unregisterMenuCommand(menuIds.autoCopy)
    if (menuIds.toastDuration) GM_unregisterMenuCommand(menuIds.toastDuration)
    if (menuIds.resetPos) GM_unregisterMenuCommand(menuIds.resetPos)
    if (menuIds.changeColor) GM_unregisterMenuCommand(menuIds.changeColor)

    // Toggle button visibility
    menuIds.toggleBtn = GM_registerMenuCommand(
      toggleBtn.style.display === 'none' ? 'Show B64' : 'Hide B64',
      () => {
        toggleBtn.style.display = toggleBtn.style.display === 'none' ? '' : 'none'
        registerMenu()
      }
    )

    // Auto-copy toggle
    menuIds.autoCopy = GM_registerMenuCommand(
      prefs.autoCopy ? 'Auto-Copy: ON' : 'Auto-Copy: OFF',
      () => {
        prefs.autoCopy = !prefs.autoCopy
        GM_setValue('b64MobileDecoderPrefs', prefs)
        showToast(`Auto-Copy ${prefs.autoCopy ? 'Enabled' : 'Disabled'}`)
        registerMenu()
      }
    )

    // Toast duration
    menuIds.toastDuration = GM_registerMenuCommand(
      'Set Toast Duration (ms)',
      () => {
        const v = parseInt(prompt('Toast duration in ms:', prefs.toastDuration), 10)
        if (!isNaN(v) && v >= 200) {
          prefs.toastDuration = v
          GM_setValue('b64MobileDecoderPrefs', prefs)
          showToast(`Toast: ${v}ms`)
        } else {
          showToast('Invalid duration')
        }
        registerMenu()
      }
    )

    // Reset button position
    menuIds.resetPos = GM_registerMenuCommand(
      'Restore Button Position',
      () => {
        setBtnPosition(null, true)
        showToast('Position reset')
        registerMenu()
      }
    )

    // Change button color
    menuIds.changeColor = GM_registerMenuCommand(
      'Change Button Color',
      () => showColorPickerOverlay()
    )
  }

  // ==============================
  // 🎨 Color Picker Overlay (Menu + Gesture)
  // ==============================
  function showColorPickerOverlay() {
    if (document.querySelector('.b64-color-overlay')) return

    const overlay = document.createElement('div')
    overlay.className = 'b64-color-overlay'

    COLORS.forEach((c, i) => {
      const swatch = document.createElement('div')
      swatch.className = 'b64-color-swatch'
      swatch.style.background = c
      swatch.addEventListener('click', () => {
        prefs.colorIndex = i
        GM_setValue('b64MobileDecoderPrefs', prefs)
        applyUIColor()
        showToast('Color selected')
        overlay.remove()
      })
      overlay.appendChild(swatch)
    })

    const doneBtn = document.createElement('div')
    doneBtn.className = 'b64-color-done'
    doneBtn.textContent = 'Done'
    doneBtn.addEventListener('click', () => overlay.remove())
    overlay.appendChild(doneBtn)

    document.body.appendChild(overlay)
  }

  // Register menu at startup
  registerMenu()


// ==============================
  // 📍 Button Positioning & Dragging
  // ==============================
  let isActive = false
  let dragging = false
  let startX = 0
  let startY = 0
  let origX = 0
  let origY = 0
  const DRAG_THRESHOLD = 8
  const savedPos = GM_getValue('b64MobileBtnPos', {})

  function setBtnPosition(saved, clearPrefs) {
    toggleBtn.style.right = 'auto'
    toggleBtn.style.bottom = 'auto'
    const btnW = toggleBtn.offsetWidth
    const btnH = toggleBtn.offsetHeight
    const dw = document.documentElement.clientWidth
    const dh = document.documentElement.clientHeight

    if (saved && typeof saved.x === 'number' && typeof saved.y === 'number') {
      let x = Math.min(Math.max(0, saved.x), dw - btnW)
      let y = Math.min(Math.max(0, saved.y), dh - btnH)
      toggleBtn.style.left = x + 'px'
      toggleBtn.style.top = y + 'px'
    } else {
      toggleBtn.style.left = (dw - btnW - 20) + 'px'
      toggleBtn.style.top = (dh - btnH - 20) + 'px'
      if (clearPrefs) GM_setValue('b64MobileBtnPos', {})
    }
  }

  setBtnPosition(savedPos, false)

  toggleBtn.addEventListener('touchstart', e => {
    const touch = e.touches[0]
    startX = touch.clientX
    startY = touch.clientY
    const r = toggleBtn.getBoundingClientRect()
    origX = r.left
    origY = r.top
    dragging = !isActive // can drag only if inactive
    e.preventDefault()
  }, { passive: false })

  window.addEventListener('touchmove', e => {
    if (!dragging) return
    const touch = e.touches[0]
    const dx = touch.clientX - startX
    const dy = touch.clientY - startY
    if (Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) return

    const btnW = toggleBtn.offsetWidth
    const btnH = toggleBtn.offsetHeight
    const maxX = document.documentElement.clientWidth - btnW
    const maxY = document.documentElement.clientHeight - btnH
    const newX = Math.min(Math.max(0, origX + dx), maxX)
    const newY = Math.min(Math.max(0, origY + dy), maxY)

    toggleBtn.style.left = newX + 'px'
    toggleBtn.style.top = newY + 'px'
  }, { passive: false })

  window.addEventListener('touchend', e => {
    const touch = e.changedTouches[0]
    const dx = touch.clientX - startX
    const dy = touch.clientY - startY
    dragging = false
    if (Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) {
      isActive = !isActive
      toggleBtn.classList.toggle('active', isActive)
      showToast(isActive ? 'Decoder ON' : 'Decoder OFF')
    } else {
      GM_setValue('b64MobileBtnPos', {
        x: parseInt(toggleBtn.style.left, 10),
        y: parseInt(toggleBtn.style.top, 10)
      })
    }
  }, { passive: false })

  // ==============================
  // 📋 Clipboard Copying
  // ==============================
  function copyToClipboard(s) {
    if (navigator.clipboard?.writeText) {
      navigator.clipboard.writeText(s).catch(() => fallbackCopy(s))
    } else fallbackCopy(s)
  }

  function fallbackCopy(s) {
    const ta = document.createElement('textarea')
    ta.value = s
    ta.style.position = 'fixed'
    ta.style.opacity = '0'
    document.body.appendChild(ta)
    ta.select()
    document.execCommand('copy')
    document.body.removeChild(ta)
  }

  // ==============================
  // 🔍 Base64 Detection & Decoding
  // ==============================
  let decodeTimer = null
  let decodedText = ''

  function isLikelyBase64(s) {
    const c = s.trim().replace(/\s+/g, '').replace(/[-_]/g, m => m === '-' ? '+' : '/')
    if (!/^[A-Za-z0-9+/=]+$/.test(c) || c.length % 4 !== 0) return false
    const p = c.indexOf('=')
    if (p > -1) {
      const cnt = c.length - p
      if (cnt > 2 || p !== c.length - cnt) return false
    }
    return true
  }

  function decodeBase64(s) {
    try {
      const norm = s.trim().replace(/\s+/g, '').replace(/\u200B/g, '').replace(/[-_]/g, m => m === '-' ? '+' : '/')
      const bin = atob(norm)
      const bytes = Uint8Array.from(bin, ch => ch.charCodeAt(0))
      const out = new TextDecoder('utf-8').decode(bytes)
      const bad = [...out].filter(ch => {
        const code = ch.charCodeAt(0)
        return code < 32 || code > 126
      }).length
      return bad / out.length > 0.2 ? null : out
    } catch {
      return null
    }
  }

  // ==============================
  // 🧠 Selection Monitoring
  // ==============================
  document.addEventListener('selectionchange', () => {
    if (!isActive) return
    clearTimeout(decodeTimer)
    const txt = window.getSelection().toString().trim().replace(/\u200B/g, '')
    if (!txt || !isLikelyBase64(txt)) {
      if (floatingWin) floatingWin.style.display = 'none'
      return
    }
    decodeTimer = setTimeout(() => {
      const sel = window.getSelection()
      if (!sel.rangeCount) return
      const out = decodeBase64(txt)
      if (!out) return
      decodedText = out
      const win = getFloating()
      win.textContent = `Decoded: ${out}`
      win.style.display = 'block'
      if (prefs.autoCopy) {
        copyToClipboard(out)
        showToast('Copied ✓')
      }
      const r = sel.getRangeAt(0).getBoundingClientRect()
      let x = r.left + scrollX
      let y = r.bottom + scrollY + 5
      const w = win.offsetWidth
      const h = win.offsetHeight
      x = Math.min(Math.max(scrollX + 10, x), document.documentElement.clientWidth + scrollX - w - 10)
      if (y + h > innerHeight + scrollY - 10) y = r.top + scrollY - h - 5
      win.style.left = x + 'px'
      win.style.top = y + 'px'
    }, 200)
  })

  // ==============================
  // 🚫 Touch Dismiss Floating Window
  // ==============================
  document.addEventListener('touchstart', e => {
    if (floatingWin && !floatingWin.contains(e.target)) {
      floatingWin.style.display = 'none'
    }
  }, { passive: false })

  // ==============================
  // 📱 Orientation-Aware Resize Handler
  // ==============================
  let lastScreenWidth = document.documentElement.clientWidth
  let resizeTimer = null

  window.addEventListener('resize', () => {
    clearTimeout(resizeTimer)
    resizeTimer = setTimeout(() => {
      const btnW = toggleBtn.offsetWidth
      const btnH = toggleBtn.offsetHeight
      const newScreenWidth = document.documentElement.clientWidth
      const newScreenHeight = document.documentElement.clientHeight
      let x = parseInt(toggleBtn.style.left, 10)
      let y = parseInt(toggleBtn.style.top, 10)
      if (isNaN(x) || isNaN(y)) return

      // Clamp to screen bounds to prevent off-screen in landscape
      x = Math.min(Math.max(0, x), newScreenWidth - btnW)
      y = Math.min(Math.max(0, y), newScreenHeight - btnH)

      toggleBtn.style.left = x + 'px'
      toggleBtn.style.top = y + 'px'
      toggleBtn.classList.toggle('active', isActive)
      GM_setValue('b64MobileBtnPos', { x, y })

      lastScreenWidth = newScreenWidth
    }, 100)
  })

  // ==============================
  // 📐 Mobile Viewport Optimization
  // ==============================
  const meta = document.createElement('meta')
  meta.name = 'viewport'
  meta.content = 'width=device-width, initial-scale=1.0'
  document.head.appendChild(meta)

})();