Orion iOS Eruda Console Toggle

Persistent, stronger Eruda launcher for Orion iOS with always-on mode and blocker-resilient loading fallbacks.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         Orion iOS Eruda Console Toggle
// @namespace    https://littlelooney.example/orion-eruda
// @version      2026-05-24.2
// @description  Persistent, stronger Eruda launcher for Orion iOS with always-on mode and blocker-resilient loading fallbacks.
// @author       LittleLooney + OpenAI
// @match        *://*/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/eruda.min.js
// @grant        none
// @run-at       document-start
// ==/UserScript==

(() => {
  'use strict';

  const STORAGE_KEY = 'orion-eruda-toggle-position';
  const AUTO_KEY = 'orion-eruda-auto-open';
  const BTN_ID = 'orion-eruda-toggle-btn';
  const CDN_FALLBACKS = [
    'https://cdn.jsdelivr.net/npm/[email protected]/eruda.min.js',
    'https://unpkg.com/[email protected]/eruda.min.js'
  ];

  let erudaReady = false;
  let erudaVisible = false;

  function loadPosition() {
    try {
      const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
      if (typeof saved.left === 'number' && typeof saved.top === 'number') return saved;
    } catch {}
    return { left: 14, top: 86 };
  }

  function savePosition(left, top) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ left, top }));
  }

  function isAutoOpenEnabled() {
    return localStorage.getItem(AUTO_KEY) !== '0';
  }

  function setAutoOpenEnabled(enabled) {
    localStorage.setItem(AUTO_KEY, enabled ? '1' : '0');
  }

  function clamp(value, min, max) {
    return Math.min(Math.max(value, min), max);
  }

  function updateButtonStyle(btn, pos) {
    const maxLeft = Math.max(8, window.innerWidth - btn.offsetWidth - 8);
    const maxTop = Math.max(8, window.innerHeight - btn.offsetHeight - 8);
    const left = clamp(pos.left, 8, maxLeft);
    const top = clamp(pos.top, 8, maxTop);
    btn.style.left = `${left}px`;
    btn.style.top = `${top}px`;
    savePosition(left, top);
  }

  function loadScript(src) {
    return new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = src;
      script.async = true;
      script.onload = resolve;
      script.onerror = () => reject(new Error(`Failed to load ${src}`));
      (document.head || document.documentElement).appendChild(script);
    });
  }

  async function ensureEruda() {
    if (!window.eruda) {
      for (const src of CDN_FALLBACKS) {
        try {
          await loadScript(src);
          if (window.eruda) break;
        } catch {}
      }
    }
    if (!window.eruda) throw new Error('Eruda unavailable (possibly blocked by network/content blocker).');

    if (!erudaReady) {
      window.eruda.init({ useShadowDom: false, autoScale: true });
      erudaReady = true;
    }
  }

  function setButtonState(btn, text) {
    btn.textContent = text;
    btn.setAttribute('aria-label', text);
  }

  async function openEruda(btn) {
    await ensureEruda();
    window.eruda.show();
    erudaVisible = true;
    setButtonState(btn, isAutoOpenEnabled() ? 'Console: ON • AUTO' : 'Console: ON');
  }

  function closeEruda(btn) {
    if (window.eruda) window.eruda.hide();
    erudaVisible = false;
    setButtonState(btn, isAutoOpenEnabled() ? 'Console: OFF • AUTO' : 'Console: OFF');
  }

  async function toggleEruda(btn) {
    btn.disabled = true;
    try {
      if (erudaVisible) closeEruda(btn); else await openEruda(btn);
    } catch (err) {
      console.error('[Orion Eruda]', err);
      setButtonState(btn, 'Console: ERR');
      setTimeout(() => setButtonState(btn, erudaVisible ? 'Console: ON' : 'Console: OFF'), 1800);
    } finally {
      btn.disabled = false;
    }
  }

  function addAutoToggle(btn) {
    btn.addEventListener('contextmenu', (e) => {
      e.preventDefault();
      const next = !isAutoOpenEnabled();
      setAutoOpenEnabled(next);
      if (erudaVisible) setButtonState(btn, next ? 'Console: ON • AUTO' : 'Console: ON');
      else setButtonState(btn, next ? 'Console: OFF • AUTO' : 'Console: OFF');
    });
  }

  function makeDraggable(btn) {
    let dragging = false, moved = false, startX = 0, startY = 0, originLeft = 0, originTop = 0;
    const onMove = (x, y) => {
      if (!dragging) return;
      const dx = x - startX, dy = y - startY;
      moved = moved || Math.abs(dx) > 4 || Math.abs(dy) > 4;
      updateButtonStyle(btn, { left: originLeft + dx, top: originTop + dy });
    };

    btn.addEventListener('touchstart', (e) => {
      const t = e.touches[0]; if (!t) return;
      dragging = true; moved = false; startX = t.clientX; startY = t.clientY;
      originLeft = parseFloat(btn.style.left) || 0; originTop = parseFloat(btn.style.top) || 0;
    }, { passive: true });
    btn.addEventListener('touchmove', (e) => { const t = e.touches[0]; if (t) onMove(t.clientX, t.clientY); }, { passive: true });
    btn.addEventListener('touchend', () => { dragging = false; }, { passive: true });
    btn.addEventListener('mousedown', (e) => {
      dragging = true; moved = false; startX = e.clientX; startY = e.clientY;
      originLeft = parseFloat(btn.style.left) || 0; originTop = parseFloat(btn.style.top) || 0; e.preventDefault();
    });
    window.addEventListener('mousemove', (e) => onMove(e.clientX, e.clientY));
    window.addEventListener('mouseup', () => { dragging = false; });

    btn.addEventListener('click', (e) => {
      if (moved) { e.preventDefault(); e.stopPropagation(); moved = false; return; }
      toggleEruda(btn);
    });
  }

  function attachKeyboardShortcut(btn) {
    window.addEventListener('keydown', (e) => {
      if (e.altKey && e.key.toLowerCase() === 'd') {
        e.preventDefault();
        toggleEruda(btn);
      }
    });
  }

  function injectUi() {
    if (document.getElementById(BTN_ID)) return document.getElementById(BTN_ID);
    const btn = document.createElement('button');
    btn.id = BTN_ID;
    btn.type = 'button';
    setButtonState(btn, isAutoOpenEnabled() ? 'Console: OFF • AUTO' : 'Console: OFF');
    btn.title = 'Tap: toggle console. Drag: move. Long-press/right-click: toggle AUTO mode.';
    Object.assign(btn.style, { position:'fixed', zIndex:'2147483647', padding:'10px 12px', borderRadius:'12px', border:'1px solid rgba(255,255,255,0.25)', background:'rgba(16,16,20,0.85)', color:'#f5f5f5', fontFamily:'-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif', fontSize:'13px', fontWeight:'600', boxShadow:'0 6px 24px rgba(0,0,0,0.25)', cursor:'pointer', userSelect:'none', WebkitUserSelect:'none', opacity:'0.95' });
    (document.documentElement || document.body).appendChild(btn);
    updateButtonStyle(btn, loadPosition());
    makeDraggable(btn);
    attachKeyboardShortcut(btn);
    addAutoToggle(btn);
    window.addEventListener('resize', () => updateButtonStyle(btn, { left: parseFloat(btn.style.left) || 8, top: parseFloat(btn.style.top) || 8 }));
    return btn;
  }

  async function boot() {
    const btn = injectUi();
    if (isAutoOpenEnabled()) {
      try { await openEruda(btn); } catch (e) { console.warn('[Orion Eruda] Auto-open failed', e); }
    }
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true });
  else boot();
})();