pip anywhere

(cick the pip button inside of your userscript manager menu) pip with full custom controls on any site seek bar, buffer indicator, volume, speed, skip, keyboard shortcuts, auto-hide hud. gracefully falls back to legacy pip on unsupported browsers.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name            pip anywhere
// @namespace       https://minoa.cat/
// @version         2.1.1
// @description     (cick the pip button inside of your userscript manager menu) pip with full custom controls on any site seek bar, buffer indicator, volume, speed, skip, keyboard shortcuts, auto-hide hud. gracefully falls back to legacy pip on unsupported browsers.
// @author          minoa
// @license         MIT
// @homepageURL     https://greasyfork.org/scripts/pip-anywhere
// @supportURL      https://github.com/M2noa/pip-anywhere/issues
// @match           *://*/*
// @grant           GM_registerMenuCommand
// @grant           GM_unregisterMenuCommand
// @grant           GM_getValue
// @grant           GM_setValue
// @noframes
// ==/UserScript==

;(function () {
  'use strict'

  // whether document pip is available in this browser (chrome 116+)
  const DPIP_SUPPORTED = 'documentPictureInPicture' in window

  // speed steps to cycle through
  const SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 4]

  // storage key
  const KEY_MODE = 'pip_mode'

  // --- persistent mode preference ---
  // 'advanced' = document pip with custom hud
  // 'basic'    = native video.requestPictureInPicture()
  // if dpip isnt supported we always use basic regardless

  let mode = GM_getValue(KEY_MODE, 'advanced')

  // menu command handles so we can re-register on toggle
  let menuPipId    = null
  let menuToggleId = null

  // --- session state ---

  let pinned    = null  // video locked for this page session
  let pipWin    = null  // documentPictureInPicture window reference
  let rafId     = null  // requestAnimationFrame handle for progress loop
  let hideTimer = null  // auto-hide timeout handle

  // saved dom position so we can restore the video element after pip closes
  let savedParent = null
  let savedNext   = null
  let savedStyles = null

  // --- video discovery ---

  function getVideos() {
    return Array.from(document.querySelectorAll('video')).filter(
      v => v.readyState > 0 && v.offsetParent !== null
    )
  }

  function getPlaying() {
    return getVideos().filter(v => !v.paused && !v.ended)
  }

  function elementCenter(el) {
    const r = el.getBoundingClientRect()
    return [r.left + r.width / 2, r.top + r.height / 2]
  }

  function closestToPoint(mx, my, list) {
    let best = null
    let bd   = Infinity
    for (const v of list) {
      const [cx, cy] = elementCenter(v)
      const d = Math.hypot(cx - mx, cy - my)
      if (d < bd) { bd = d; best = v }
    }
    return best
  }

  // --- utils ---

  function fmtTime(s) {
    s = Math.floor(s || 0)
    const h   = Math.floor(s / 3600)
    const m   = Math.floor((s % 3600) / 60)
    const sec = s % 60
    if (h > 0) {
      return h + ':' + String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0')
    }
    return m + ':' + String(sec).padStart(2, '0')
  }

  function makeSvg(path, size) {
    size = size || 16
    return (
      '<svg width="' + size + '" height="' + size +
      '" viewBox="0 0 24 24" fill="currentColor">' + path + '</svg>'
    )
  }

  const ICON = {
    play:  makeSvg('<path d="M8 5v14l11-7z"/>'),
    pause: makeSvg('<path d="M6 19h4V5H6zm8-14v14h4V5z"/>'),
    mute:  makeSvg('<path d="M16.5 12A4.5 4.5 0 0014 7.97v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.796 8.796 0 0021 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06A8.99 8.99 0 0017.73 18l1.98 2L21 18.73 4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>'),
    vol:   makeSvg('<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>'),
    back:  makeSvg('<path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z"/>'),
    fwd:   makeSvg('<path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/>'),
    close: makeSvg('<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>')
  }

  // --- selection mode ---
  // when multiple videos are playing, outline the closest one to the mouse
  // and let the user click to confirm which one to pip

  let selecting  = false
  let selHovered = null

  function enterSelectMode(candidates, cb) {
    if (selecting) return
    selecting = true

    const prevCursor = document.documentElement.style.cursor
    document.documentElement.style.cursor = 'crosshair'

    function hl(v) {
      v.style.setProperty('outline', '2px solid #0af', 'important')
      v.style.setProperty('outline-offset', '-2px', 'important')
    }
    function unhl(v) {
      if (!v) return
      v.style.removeProperty('outline')
      v.style.removeProperty('outline-offset')
    }
    function onMove(e) {
      const next = closestToPoint(e.clientX, e.clientY, candidates)
      if (next === selHovered) return
      unhl(selHovered)
      selHovered = next
      if (selHovered) hl(selHovered)
    }
    function onKey(e) {
      if (e.key === 'Escape') cleanup()
    }
    function onClick(e) {
      e.stopPropagation()
      e.preventDefault()
      const target = selHovered
      cleanup()
      cb(target)
    }
    function cleanup() {
      selecting = false
      unhl(selHovered)
      selHovered = null
      document.documentElement.style.cursor = prevCursor
      document.removeEventListener('mousemove', onMove)
      document.removeEventListener('click', onClick, true)
      document.removeEventListener('keydown', onKey)
    }

    document.addEventListener('mousemove', onMove)
    document.addEventListener('click', onClick, true)
    document.addEventListener('keydown', onKey)
  }

  // --- styles injected into the document pip window ---

  const HUD_CSS = [
    '* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; -webkit-user-select: none; }',
    'body {',
    '  background: #000; width: 100vw; height: 100vh;',
    '  overflow: hidden; display: flex;',
    '  align-items: center; justify-content: center;',
    '}',
    '#wrap { position: relative; width: 100%; height: 100%; }',
    'video {',
    '  width: 100% !important; height: 100% !important;',
    '  max-width: 100% !important; max-height: 100% !important;',
    '  object-fit: contain; display: block;',
    '}',
    '#hud {',
    '  position: absolute; bottom: 0; left: 0; right: 0;',
    '  padding: 36px 12px 10px;',
    '  background: linear-gradient(transparent, rgba(0,0,0,0.85));',
    '  transition: opacity 0.2s ease;',
    '}',
    '#hud.hidden { opacity: 0; pointer-events: none; }',
    '#prog-wrap {',
    '  position: relative; height: 4px;',
    '  background: rgba(255,255,255,0.18); border-radius: 2px;',
    '  margin-bottom: 9px; cursor: pointer;',
    '  transition: height 0.12s, margin-bottom 0.12s;',
    '}',
    '#prog-wrap:hover { height: 6px; margin-bottom: 7px; }',
    '#buf {',
    '  position: absolute; left: 0; top: 0; height: 100%;',
    '  background: rgba(255,255,255,0.22); border-radius: 2px; pointer-events: none;',
    '}',
    '#fill {',
    '  position: absolute; left: 0; top: 0; height: 100%;',
    '  background: #0af; border-radius: 2px; pointer-events: none;',
    '}',
    '#prog-thumb {',
    '  position: absolute; top: 50%; transform: translate(-50%, -50%);',
    '  width: 12px; height: 12px; background: #fff; border-radius: 50%;',
    '  pointer-events: none; opacity: 0; transition: opacity 0.15s;',
    '}',
    '#prog-wrap:hover #prog-thumb { opacity: 1; }',
    '#prog-tip {',
    '  position: absolute; bottom: 14px; transform: translateX(-50%);',
    '  background: rgba(0,0,0,0.75); color: #fff;',
    '  font-size: 10px; font-family: ui-monospace, monospace;',
    '  padding: 2px 5px; border-radius: 3px;',
    '  pointer-events: none; white-space: nowrap;',
    '  opacity: 0; transition: opacity 0.1s;',
    '}',
    '#prog-wrap:hover #prog-tip { opacity: 1; }',
    '#btns { display: flex; align-items: center; gap: 3px; }',
    'button {',
    '  background: none; border: none; color: #fff; cursor: pointer;',
    '  padding: 4px 6px; border-radius: 4px; line-height: 1;',
    '  opacity: 0.82; transition: opacity 0.12s, background 0.12s; flex-shrink: 0;',
    '}',
    'button:hover  { opacity: 1; background: rgba(255,255,255,0.12); }',
    'button:active { background: rgba(255,255,255,0.2); }',
    '#time {',
    '  font-size: 11px; color: rgba(255,255,255,0.78);',
    '  white-space: nowrap; padding: 0 4px;',
    '  font-family: ui-monospace, monospace;',
    '}',
    '.spacer { flex: 1; }',
    '#vol-wrap { display: flex; align-items: center; gap: 4px; }',
    'input[type=range] {',
    '  -webkit-appearance: none; appearance: none;',
    '  width: 64px; height: 3px;',
    '  background: rgba(255,255,255,0.28); border-radius: 2px;',
    '  outline: none; cursor: pointer;',
    '}',
    'input[type=range]::-webkit-slider-thumb {',
    '  -webkit-appearance: none; appearance: none;',
    '  width: 11px; height: 11px; background: #fff; border-radius: 50%;',
    '}',
    '#speed-btn {',
    '  font-variant-numeric: tabular-nums;',
    '  min-width: 36px; text-align: center; font-size: 11px;',
    '}',
    'svg { display: block; }',
  ].join('\n')

  // --- build and wire up the hud inside the pip window ---

  function buildHud(doc, video) {
    const wrap = doc.createElement('div')
    wrap.id = 'wrap'

    const hud = doc.createElement('div')
    hud.id = 'hud'
    hud.innerHTML = [
      '<div id="prog-wrap">',
        '<div id="buf"></div>',
        '<div id="fill"></div>',
        '<div id="prog-thumb"></div>',
        '<div id="prog-tip">0:00</div>',
      '</div>',
      '<div id="btns">',
        '<button id="btn-back"  title="back 10s">'      + ICON.back  + '</button>',
        '<button id="btn-pp"    title="play / pause">'  + (video.paused ? ICON.play : ICON.pause) + '</button>',
        '<button id="btn-fwd"   title="forward 10s">'   + ICON.fwd   + '</button>',
        '<span id="time">' + fmtTime(video.currentTime) + ' / ' + fmtTime(video.duration) + '</span>',
        '<div class="spacer"></div>',
        '<div id="vol-wrap">',
          '<button id="btn-mute" title="mute">' + (video.muted ? ICON.mute : ICON.vol) + '</button>',
          '<input type="range" id="vol-slider" min="0" max="1" step="0.02" value="' + video.volume + '">',
        '</div>',
        '<button id="speed-btn" title="cycle speed">' + video.playbackRate + 'x</button>',
        '<button id="btn-close" title="close pip">'   + ICON.close + '</button>',
      '</div>',
    ].join('')

    wrap.appendChild(video)
    wrap.appendChild(hud)
    doc.body.appendChild(wrap)

    const elPp      = hud.querySelector('#btn-pp')
    const elBack    = hud.querySelector('#btn-back')
    const elFwd     = hud.querySelector('#btn-fwd')
    const elMute    = hud.querySelector('#btn-mute')
    const elVolSldr = hud.querySelector('#vol-slider')
    const elSpeed   = hud.querySelector('#speed-btn')
    const elClose   = hud.querySelector('#btn-close')
    const elFill    = hud.querySelector('#fill')
    const elBuf     = hud.querySelector('#buf')
    const elThumb   = hud.querySelector('#prog-thumb')
    const elTip     = hud.querySelector('#prog-tip')
    const elProgW   = hud.querySelector('#prog-wrap')
    const elTime    = hud.querySelector('#time')

    // raf loop - syncs seek bar, buffer, time, and button icon states
    function tick() {
      if (!video || video.readyState < 1) {
        rafId = requestAnimationFrame(tick)
        return
      }
      const dur = video.duration || 0
      const pct = dur ? Math.min(1, video.currentTime / dur) : 0
      const pctStr = (pct * 100).toFixed(3) + '%'

      elFill.style.width  = pctStr
      elThumb.style.left  = pctStr
      elTime.textContent  = fmtTime(video.currentTime) + ' / ' + fmtTime(dur)

      if (video.buffered.length && dur) {
        elBuf.style.width =
          (video.buffered.end(video.buffered.length - 1) / dur * 100).toFixed(3) + '%'
      }

      elPp.innerHTML   = video.paused ? ICON.play : ICON.pause
      elMute.innerHTML = (video.muted || video.volume === 0) ? ICON.mute : ICON.vol

      rafId = requestAnimationFrame(tick)
    }
    rafId = requestAnimationFrame(tick)

    // play/pause
    elPp.addEventListener('click', () => video.paused ? video.play() : video.pause())

    // skip buttons
    elBack.addEventListener('click', () => {
      video.currentTime = Math.max(0, video.currentTime - 10)
    })
    elFwd.addEventListener('click', () => {
      video.currentTime = Math.min(video.duration || 0, video.currentTime + 10)
    })

    // mute toggle
    elMute.addEventListener('click', () => {
      video.muted     = !video.muted
      elVolSldr.value = video.muted ? 0 : video.volume
    })

    // volume slider
    elVolSldr.addEventListener('input', () => {
      video.volume = parseFloat(elVolSldr.value)
      video.muted  = video.volume === 0
    })

    // speed cycle
    elSpeed.addEventListener('click', () => {
      const cur               = SPEEDS.indexOf(video.playbackRate)
      const next              = SPEEDS[(cur + 1) % SPEEDS.length]
      video.playbackRate      = next
      elSpeed.textContent     = next + 'x'
    })

    // seek bar - click to jump, drag to scrub
    let scrubbing = false

    function seekFromEvent(e) {
      const r   = elProgW.getBoundingClientRect()
      const pct = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width))
      video.currentTime = pct * (video.duration || 0)
    }

    // tooltip on hover showing time at cursor position
    elProgW.addEventListener('mousemove', e => {
      const r         = elProgW.getBoundingClientRect()
      const pct       = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width))
      elTip.textContent = fmtTime(pct * (video.duration || 0))
      elTip.style.left  = (pct * 100).toFixed(3) + '%'
    })

    elProgW.addEventListener('mousedown', e => { scrubbing = true; seekFromEvent(e) })
    doc.addEventListener('mousemove',     e => { if (scrubbing) seekFromEvent(e) })
    doc.addEventListener('mouseup',       ()  => { scrubbing = false })

    // close button
    elClose.addEventListener('click', () => pipWin && pipWin.close())

    // auto-hide hud after 3s of no mouse activity
    function showHud() {
      hud.classList.remove('hidden')
      clearTimeout(hideTimer)
      hideTimer = setTimeout(() => { hud.classList.add('hidden') }, 3000)
    }
    doc.addEventListener('mousemove', showHud)
    doc.addEventListener('click',     showHud)
    showHud()

    // keyboard shortcuts when pip window has focus
    doc.addEventListener('keydown', e => {
      if (e.ctrlKey || e.metaKey || e.altKey) return
      const dur = video.duration || 0

      switch (e.key) {
        case ' ':
        case 'k':
          e.preventDefault()
          video.paused ? video.play() : video.pause()
          break
        case 'ArrowLeft':
        case 'j':
          video.currentTime = Math.max(0, video.currentTime - 10)
          break
        case 'ArrowRight':
        case 'l':
          video.currentTime = Math.min(dur, video.currentTime + 10)
          break
        case 'ArrowUp':
          e.preventDefault()
          video.volume    = Math.min(1, +(video.volume + 0.1).toFixed(2))
          elVolSldr.value = video.volume
          break
        case 'ArrowDown':
          e.preventDefault()
          video.volume    = Math.max(0, +(video.volume - 0.1).toFixed(2))
          elVolSldr.value = video.volume
          break
        case 'm':
          video.muted = !video.muted
          break
        case 'Home':
          e.preventDefault()
          video.currentTime = 0
          break
        case 'End':
          e.preventDefault()
          video.currentTime = dur
          break
        case '>':
        case '.': {
          const ni            = Math.min(SPEEDS.length - 1, SPEEDS.indexOf(video.playbackRate) + 1)
          video.playbackRate  = SPEEDS[ni]
          elSpeed.textContent = SPEEDS[ni] + 'x'
          break
        }
        case '<':
        case ',': {
          const ni            = Math.max(0, SPEEDS.indexOf(video.playbackRate) - 1)
          video.playbackRate  = SPEEDS[ni]
          elSpeed.textContent = SPEEDS[ni] + 'x'
          break
        }
        // 0-9 to jump to 0%-90% through the video (youtube-style)
        case '0': case '1': case '2': case '3': case '4':
        case '5': case '6': case '7': case '8': case '9':
          video.currentTime = dur * (parseInt(e.key, 10) / 10)
          break
      }
      showHud()
    })
  }

  // --- document pip (advanced mode) ---

  async function openDocumentPip(video) {
    if (pipWin) { pipWin.close(); pipWin = null }
    if (document.pictureInPictureElement) {
      await document.exitPictureInPicture().catch(() => {})
    }

    // remember the video's original dom position for restoration on close
    savedParent = video.parentNode
    savedNext   = video.nextSibling
    savedStyles = {
      width:     video.style.width,
      height:    video.style.height,
      position:  video.style.position,
      maxWidth:  video.style.maxWidth,
      maxHeight: video.style.maxHeight,
    }

    pinned = video

    // size window to match video aspect ratio
    const vw   = video.videoWidth  || 640
    const vh   = video.videoHeight || 360
    const winH = 360
    const winW = Math.round(winH * (vw / vh))

    let win
    try {
      win = await window.documentPictureInPicture.requestWindow({
        width:                  winW,
        height:                 winH,
        disallowReturnToOpener: false,
      })
    } catch (err) {
      // browser denied or csp blocked - fall back to legacy
      console.warn('[pip-anywhere] document pip failed, falling back to legacy:', err)
      openLegacyPip(video)
      return
    }

    pipWin = win

    const style       = win.document.createElement('style')
    style.textContent = HUD_CSS
    win.document.head.appendChild(style)

    buildHud(win.document, video)

    // restore video to its original dom position when the pip window closes.
    // wrapped in tiered try/catch because sites like cineby mutate the player
    // dom while pip is open, leaving savedParent/savedNext in stale states.
    win.addEventListener('pagehide', () => {
      if (rafId) { cancelAnimationFrame(rafId); rafId = null }
      clearTimeout(hideTimer)
      pipWin = null

      if (savedStyles) Object.assign(video.style, savedStyles)

      try {
        // tier 1: ideal - put it back exactly where it was
        if (savedParent && savedParent.isConnected) {
          if (savedNext && savedNext.isConnected && savedNext.parentNode === savedParent) {
            savedParent.insertBefore(video, savedNext)
          } else {
            savedParent.appendChild(video)
          }
        } else {
          // tier 2: parent is gone - try to find a new home via a fresh query
          // sites that rebuild the player will have a new container in the dom
          const newParent = document.querySelector(
            'video') ? null : document.body
          if (newParent) newParent.appendChild(video)
          // if a fresh video already exists in the dom the site recreated the
          // player itself, so we just let the element sit detached - it'll be
          // garbage collected and the site's player is already working fine
        }
      } catch (e) {
        // tier 3: something still went wrong (race, shadow dom, etc.)
        // swallow silently - the site will handle its own player state
        try { document.body.appendChild(video) } catch (_) {}
      }

      savedParent = savedNext = savedStyles = null
    })
  }

  // --- legacy pip (basic mode / dpip not supported) ---

  async function openLegacyPip(video) {
    pinned = video
    if (document.pictureInPictureElement === video) return
    await video.requestPictureInPicture().catch(() => {})
  }

  // --- dispatch to the right pip implementation ---

  function openPip(video) {
    if (mode === 'advanced' && DPIP_SUPPORTED) {
      openDocumentPip(video)
    } else {
      openLegacyPip(video)
    }
  }

  // --- main pip action ---

  async function triggerPip() {
    // reuse pinned video for the rest of the session
    if (pinned && document.body.contains(pinned)) {
      openPip(pinned)
      return
    }

    const all = getVideos()
    if (!all.length) return

    if (all.length === 1) { openPip(all[0]); return }

    const active = getPlaying()
    if (active.length === 1) { openPip(active[0]); return }
    if (active.length > 1)  { enterSelectMode(active, openPip); return }

    // nothing playing - pick the biggest visible video, probably the main player
    const biggest = all.reduce((a, b) => {
      const ra = a.getBoundingClientRect()
      const rb = b.getBoundingClientRect()
      return ra.width * ra.height >= rb.width * rb.height ? a : b
    })
    openPip(biggest)
  }

  // --- menu command labels ---

  function pipLabel() {
    if (!DPIP_SUPPORTED) return 'PiP'
    return mode === 'advanced' ? 'PiP  [Advanced]' : 'PiP  [Basic]'
  }

  function toggleLabel() {
    // only show the toggle if the browser actually supports document pip
    if (!DPIP_SUPPORTED) return null
    return mode === 'advanced' ? 'Switch to Basic PiP' : 'Switch to Advanced PiP'
  }

  function registerMenuCommands() {
    if (menuPipId    != null) { try { GM_unregisterMenuCommand(menuPipId)    } catch (e) {} }
    if (menuToggleId != null) { try { GM_unregisterMenuCommand(menuToggleId) } catch (e) {} }

    menuPipId = GM_registerMenuCommand(pipLabel(), triggerPip)

    const label = toggleLabel()
    if (label) menuToggleId = GM_registerMenuCommand(label, toggleMode)
  }

  function toggleMode() {
    mode = mode === 'advanced' ? 'basic' : 'advanced'
    GM_setValue(KEY_MODE, mode)
    registerMenuCommands()
  }

  // boot
  registerMenuCommands()
})()