Kone base64 autodecoder Rebuilt

Auto-decode Base64/Braille encoded links in Kone.gg, with a unified settings panel, a searchable link summary, drag-to-decode, and lightweight DOM watching over both the post body and comments.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name           Kone base64 autodecoder Rebuilt
// @name:ko        코네 자동 디코더 (개선판)
// @version        3.12.0
// @author         OMNI7 (Original code by 'Laria')
// @match          https://kone.gg/*
// @description    Auto-decode Base64/Braille encoded links in Kone.gg, with a unified settings panel, a searchable link summary, drag-to-decode, and lightweight DOM watching over both the post body and comments.
// @description:ko 코네 내 Base64/점자로 인코딩된 링크를 자동으로 복호화합니다. 통합 설정 패널, 검색 가능한 링크 요약, 드래그 디코딩, 본문·댓글을 모두 감시하는 가벼운 DOM 감시를 제공합니다.
// @icon           https://www.google.com/s2/favicons?sz=64&domain=kone.gg
// @require        https://cdn.jsdelivr.net/npm/sweetalert2@11
// @license        MIT
// @encoding       utf-8
// @run-at         document-end
// @grant          GM.getValue
// @grant          GM.setValue
// @grant          GM.deleteValue
// @grant          GM.registerMenuCommand
// @grant          GM.unregisterMenuCommand
// @grant          GM.setClipboard
// @grant          GM.openInTab
// @namespace https://greasyfork.org/users/1575179
// ==/UserScript==

/*
  Kone base64 autodecoder — rebuilt by OMNI7.
  Based on "Arca base64 autodecoder" by 'Laria' (https://greasyfork.org/ko/scripts/482577).
*/

const regexEncodedPrefixDef = [
    /(aHR0cDovL|aHR0cHM6Ly|bWVnYS5ue|a2lvLmFj)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(YUhSMGNEb3ZM|YUhSMGNITTZMe|YldWbllTVX|YTIsdmJtR)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(WVVoU01HTkViM1pN|WVVoU01HTklUVFpNZ|V1d4V1JteF|V1RJeHNXWm)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(V1ZWb1UwMUhUa1ZpTTFwT|V1ZWb1UwMUhUa2xVVkZwTl|VmpkNFYxSn|VjFSSmVIT)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(VjFaV2IxVXdNVWhVYTFacFRURndU|VjFaV2IxVXdNVWhVYTJ4VlZrWndUb)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(VmpGYVYySXhWWGROVldoVllURmFjRlJVUm5kV|VmpGYVYySXhWWGROVldoVllUSjRWbFpyV25kVW)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(Vm1wR1lWWXlTWGhXV0dST1ZsZG9WbGxVUm1GalJsSlZVbTVrV|Vm1wR1lWWXlTWGhXV0dST1ZsZG9WbGxVU2pSV2JGcHlWMjVrVl)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(Vm0xd1IxbFdXWGxUV0doWFYwZFNUMVpzWkc5V2JHeFZVbTFHYWxKc1NsWlZiVFZyV|Vm0xd1IxbFdXWGxUV0doWFYwZFNUMVpzWkc5V2JHeFZVMnBTVjJKR2NIbFdNalZyVm)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(Vm0weGQxSXhiRmRYV0d4VVYwZG9XRll3WkZOVU1WcHpXa2M1VjJKSGVGWlZiVEZIWVd4S2MxTnNXbFppVkZaeV|Vm0weGQxSXhiRmRYV0d4VVYwZG9XRll3WkZOVU1WcHpXa2M1VjJKSGVGWlZNbkJUVmpKS1IyTkliRmROYWxaeVZt)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJsbDNXa1pPVlUxV2NIcFhhMk0xVmpKS1NHVkdXbFppVkVaSVdWZDRTMk14VG5OWGJGcHBWa1phZ|Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJsbDNXa1pPVlUxV2NIcFhhMk0xVmpKS1NHVkdXbFpOYmtKVVZtcEtTMUl5VGtsaVJtUk9ZV3hhZVZad)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(Vm0wd2QyVkhVWGhUV0docFVtMVNXVll3WkRSV1ZsbDNXa2M1V0ZKc2JETlhhMXBQVmxVeFYyTkljRmhoTWsweFZtcEtTMU5IVmtkWGJGcHBWa1ZhU1ZkV1pEUlRNazE0Vkc1T1dHSkdjSEJXYTFwaF|Vm0wd2QyVkhVWGhUV0docFVtMVNXVll3WkRSV1ZsbDNXa2M1V0ZKc2JETlhhMXBQVmxVeFYyTkljRmhoTWsweFZtcEtTMU5IVmtkWGJGcE9ZbXRLVlZadGNFdFRNVWw1Vkd0c2FWSnRVazlaVjNoaFpWWmFk)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
];
const MAX_DECODE_DEPTH = regexEncodedPrefixDef.length;

const abadInternalDB = {
  decodedLink: {}, decodedList: [], hostnameSetRaw: new Set(), hostnameSet: [],
  internalDB: { dragDecodingEnable: false, swal2Enable: false },
};

const abadConstDB = {
  regInvalid: /[^\w\+\/=]/,
};

abadConstDB.SWAL2Title = `<div class="abad-brand"><span class="abad-brand__icon">🔓</span><span class="abad-brand__name">${('name:ko' in GM.info.script) ? GM.info.script['name:ko'] : GM.info.script.name}</span><span class="abad-brand__version">v${GM.info.script.version}</span></div>`;


function injectStyles() {
  if (document.getElementById('abad-styles')) return;
  const style = document.createElement('style');
  style.id = 'abad-styles';
  style.textContent = `
:root {
  --abad-radius: 14px; --abad-radius-sm: 10px;
  --abad-bg: #ffffff; --abad-bg-soft: #f8fafc; --abad-bg-hover: #eef2f6; --abad-border: #e2e8f0;
  --abad-text: #0f172a; --abad-text-muted: #64748b;
  --abad-primary: #059669; --abad-primary-soft: #d1fae5; --abad-primary-text: #047857;
  --abad-accent: #db2777;
  --abad-shadow: 0 10px 28px rgba(15,23,42,.12), 0 2px 6px rgba(15,23,42,.06);
  --abad-shadow-sm: 0 2px 10px rgba(15,23,42,.08);
}
html.dark, html.abad-dark {
  --abad-bg: #1e293b; --abad-bg-soft: #172033; --abad-bg-hover: #263349; --abad-border: #334155;
  --abad-text: #f1f5f9; --abad-text-muted: #94a3b8;
  --abad-primary: #34d399; --abad-primary-soft: rgba(52,211,153,.16); --abad-primary-text: #6ee7b7;
  --abad-accent: #f472b6;
  --abad-shadow: 0 14px 34px rgba(0,0,0,.5), 0 2px 6px rgba(0,0,0,.3);
  --abad-shadow-sm: 0 2px 12px rgba(0,0,0,.35);
}
.abad-brand { display:flex; align-items:center; justify-content:center; gap:9px; font-family: inherit; }
.abad-brand__icon { font-size:22px; line-height:1; }
.abad-brand__name { font-size:18px; font-weight:800; color: var(--abad-text); }
.abad-brand__version { font-size:12.5px; font-weight:700; color: var(--abad-primary-text); background: var(--abad-primary-soft); padding:2px 9px; border-radius:999px; }
.abad-card { display:flex; align-items:center; gap:12px; background: var(--abad-bg); border:1px solid var(--abad-border); border-radius: var(--abad-radius); padding:14px 18px; margin:14px 0; box-shadow: var(--abad-shadow-sm); cursor:pointer; transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; font-family: inherit; }
.abad-card:hover { transform: translateY(-1px); box-shadow: var(--abad-shadow); border-color: var(--abad-primary); }
.abad-card__icon { flex:none; width:44px; height:44px; border-radius:50%; background: var(--abad-primary-soft); color: var(--abad-primary-text); display:flex; align-items:center; justify-content:center; font-size:20px; }
.abad-card__body { flex:1; min-width:0; }
.abad-card__title { font-weight:700; font-size:16.5px; color: var(--abad-text); }
.abad-card__title b { color: var(--abad-primary-text); }
.abad-card__meta { font-size:14px; color: var(--abad-text-muted); margin-top:3px; }
.abad-card__chevron { flex:none; color: var(--abad-text-muted); font-size:15px; }
.abad-fab { position:fixed; right:20px; bottom:20px; z-index:9999; display:none; align-items:center; gap:8px; background: linear-gradient(135deg, var(--abad-primary), #0d9488); color:#fff; font-size:15px; font-weight:700; padding:11px 18px; border-radius:999px; box-shadow: var(--abad-shadow); cursor:pointer; user-select:none; transition: transform .15s ease; font-family: inherit; }
.abad-fab:hover { transform: translateY(-2px) scale(1.02); }
.abad-fab__count { background: rgba(255,255,255,.25); border-radius:999px; padding:1px 9px; font-size:14px; }
.abad-generated { display:inline; }
.abad-decoded-text { display:inline; padding:0.05em 0.35em; border-radius:0.35em; background: var(--abad-primary-soft); color: var(--abad-text); word-break:break-word; font: inherit; line-height:inherit; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
.abad-link { color: var(--abad-primary-text); font-weight:700; text-decoration:underline; text-underline-offset:0.15em; }
.abad-link:hover { color: var(--abad-accent); }
.abad-drag-container { display:inline; }
.abad-drag-result { color: var(--abad-primary-text); font-weight:700; cursor:pointer; background: var(--abad-primary-soft); border-radius:0.35em; padding:0 0.2em; font: inherit; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
.abad-drag-result a { cursor:pointer; }
.abad-mobile-btn { position:fixed; bottom:25px; left:25px; z-index:10000; width:54px; height:54px; border-radius:50%; background: linear-gradient(135deg, var(--abad-primary), #0d9488); color:#fff; font-size:24px; display:none; align-items:center; justify-content:center; box-shadow: var(--abad-shadow); cursor:pointer; user-select:none; }
.abad-swal-popup:not(.swal2-toast) { border-radius:18px !important; font-family: inherit !important; font-size:16px !important; box-shadow: var(--abad-shadow) !important; background: var(--abad-bg) !important; color: var(--abad-text) !important; }
.abad-swal-popup.swal2-toast { border-radius:12px !important; font-family: inherit !important; padding:0.7em 1em !important; box-shadow: var(--abad-shadow-sm) !important; background: var(--abad-bg) !important; color: var(--abad-text) !important; }
.abad-swal-popup .swal2-html-container { color: var(--abad-text); }
.abad-swal-popup.swal2-toast .swal2-html-container { font-size: 13.5px; margin: 0 0.5em; }
.abad-btn { font-size:15px; font-weight:600; padding:9px 20px; border-radius:999px; border:none; cursor:pointer; margin:4px; transition: transform .1s ease, opacity .1s ease; }
.abad-btn:hover { transform: translateY(-1px); }
.abad-btn:active { transform: translateY(0); opacity:.85; }
.abad-btn--primary { background: var(--abad-primary); color:#fff; }
.abad-btn--secondary { background: var(--abad-accent); color:#fff; }
.abad-btn--ghost { background: var(--abad-bg-soft); color: var(--abad-text-muted); border:1px solid var(--abad-border); }
.abad-search-input { width:100%; box-sizing:border-box; padding:10px 14px; margin-bottom:10px; border:1px solid var(--abad-border); border-radius:10px; background: var(--abad-bg-soft); color: var(--abad-text); font-size:15px; outline:none; transition: border-color .12s ease; }
.abad-search-input:focus { border-color: var(--abad-primary); }
.abad-list { overflow:auto; width:100%; height:280px; border-radius:12px; background: var(--abad-bg-soft); border:1px solid var(--abad-border); padding:6px; }
.abad-list-row { display:flex; align-items:center; gap:8px; padding:9px 10px; border-radius:8px; }
.abad-list-row:hover { background: var(--abad-bg-hover); }
.abad-list-row__favicon { width:17px; height:17px; border-radius:3px; flex:none; }
.abad-list-row__link { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color: var(--abad-text); text-decoration:none; font-size:14.5px; font-weight:500; }
.abad-list-row__link:hover { color: var(--abad-primary-text); text-decoration:underline; }
.abad-list-row__goto { flex:none; font-size:13px; font-weight:700; color: var(--abad-primary-text); background: var(--abad-primary-soft); padding:4px 10px; border-radius:999px; text-decoration:none; cursor:pointer; }
.abad-list-empty { text-align:center; color: var(--abad-text-muted); padding:40px 0; font-size:15px; }
.abad-list-header { display:flex; align-items:center; justify-content:space-between; font-size:15px; margin-bottom:8px; text-align:left; }
.abad-list-count { font-size:14px; color: var(--abad-text-muted); }
.abad-settings { text-align:left; font-size:15.5px; }
.abad-settings-row { display:flex; align-items:center; justify-content:space-between; padding:11px 4px; border-bottom:1px solid var(--abad-border); gap:10px; }
.abad-settings-row:last-of-type { border-bottom:none; }
.abad-settings-label { display:flex; align-items:center; gap:9px; color: var(--abad-text); font-weight:600; }
.abad-settings-label__icon { font-size:18px; }
.abad-switch { position:relative; width:42px; height:24px; flex:none; display:inline-block; }
.abad-switch input { position:absolute; opacity:0; width:100%; height:100%; margin:0; cursor:pointer; z-index:1; }
.abad-switch__track { position:absolute; inset:0; background: var(--abad-border); border-radius:999px; transition: background .15s ease; pointer-events:none; }
.abad-switch__thumb { position:absolute; top:2px; left:2px; width:20px; height:20px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.3); transition: transform .15s ease; pointer-events:none; }
.abad-switch input:checked + .abad-switch__track { background: var(--abad-primary); }
.abad-switch input:checked ~ .abad-switch__thumb { transform: translateX(18px); }
.abad-depth-block { margin-top:8px; padding-top:12px; text-align:left; }
.abad-depth-value { color: var(--abad-primary-text); font-weight:800; }
.abad-range { width:100%; accent-color: var(--abad-primary); margin-top:8px; }
.abad-hint { margin-top:12px; font-size:13.5px; color: var(--abad-text-muted); background: var(--abad-bg-soft); border-radius:10px; padding:9px 11px; text-align:left; }
.abad-hint b { color: var(--abad-text); }
.abad-code-preview { margin-top:12px; text-align:left; font-size:14.5px; }
.abad-code-preview__box { overflow-y:auto; overflow-wrap:anywhere; margin:6px 0; padding:10px; width:100%; height:150px; background: var(--abad-bg-soft); border:1px solid var(--abad-border); border-radius:10px; box-sizing:border-box; cursor:text; user-select:text; color: var(--abad-text); }
.abad-timer-note { font-size:13px; color: var(--abad-text-muted); }
`;
  document.head.appendChild(style);
}

function syncDarkClass() {
  const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
  document.documentElement.classList.toggle('abad-dark', !!prefersDark && !document.documentElement.classList.contains('light'));
}

function swalFire(options) {
  return Swal.fire(Object.assign({
    buttonsStyling: false,
    customClass: {
      popup: 'abad-swal-popup',
      confirmButton: 'abad-btn abad-btn--primary',
      cancelButton: 'abad-btn abad-btn--ghost',
      denyButton: 'abad-btn abad-btn--secondary',
    },
  }, options));
}

function toastPosition() {
  return window.matchMedia('(max-width: 768px)').matches ? 'top' : 'top-end';
}

function showToast(message, icon = 'success', timer = 2200) {
  return swalFire({
    icon,
    html: `<b>${message}</b>`,
    toast: true,
    position: toastPosition(),
    timer,
    timerProgressBar: true,
    showConfirmButton: false,
  });
}

let hindex = 0;
let lastUrl = location.href;

const localParameter = {
  'autodecode': { 'param_name': 'autodecode', 'value': true,  'def_value': true },
  'basedepth':  { 'param_name': 'basedepth',  'value': 3,     'def_value': 3 },
  'draggable':  { 'param_name': 'draggable',  'value': false, 'def_value': false },
  'expandmenu': { 'param_name': 'expandmenu', 'value': true,  'def_value': true },
};

const menuStructure = {
  'quickpanel':    { 'name': '⚡ 빠른 설정 (전체 보기)', 'desc': '모든 옵션을 한 화면에서 확인하고 변경합니다.', 'id': -1, 'func': openQuickSettingsPanel, 'visible': true },
  'autodecode':    { 'name': '🗝 자동 디코딩 [상태]', 'desc': '페이지 접속 시 자동으로 디코딩하는 기능을 켜고 끕니다.', 'id': -1, 'func': menuFunctionAutodecode, 'visible': true },
  'basedepth':     { 'name': '🎛 base64 깊이 조절하기', 'desc': '자동 base64 디코딩 깊이를 조절할 수 있습니다.', 'id': -1, 'func': menuFunctionBasedepth, 'visible': true },
  'draggable':     { 'name': '🖱 드래그 시 자동 디코딩 [켜기/끄기]', 'desc': '', 'id': -1, 'func': menuFunctionDraggable, 'visible': true },
  'resetdefaults': { 'name': '🛠 스크립트 기본값 초기화', 'desc': '', 'id': -1, 'func': menuFunctionRstDefaults, 'visible': true },
  'expandmenu':    { 'name': '⚙️ 스크립트 메뉴 [축소/확장]', 'desc': '', 'id': -1, 'func': menuFunctionChangeExpandMode, 'visible': true },
};

const addListener = (node, event, handler, capture = false) => node.addEventListener(event, handler, capture);
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }

function getLocation(href) {
  const match = href.toString().match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/);
  return match && { href, hostname: match[3] };
}

function createElemID() {
  try {
    if (self.crypto && typeof self.crypto.randomUUID === 'function') return 'abad_' + self.crypto.randomUUID();
  } catch (_) {}
  return 'abad_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
}

function faviconFor(hostname) { return `https://www.google.com/s2/favicons?sz=32&domain=${hostname}`; }

function getPostContainer() {
  return document.querySelector('#post-article') || document.querySelector('main #post_content');
}
function getPostRoot() {
  const pc = getPostContainer();
  return (pc && pc.shadowRoot) ? pc.shadowRoot : pc;
}
function getCommentRoot() {
  return document.querySelector('#post-comment');
}

function getActiveSelection() {
  const root = getPostRoot();
  if (root && typeof root.getSelection === 'function') {
    const shadowSel = root.getSelection();
    if (shadowSel && shadowSel.toString().trim().length > 0) return shadowSel;
  }
  return window.getSelection();
}

function toElement(node) {
  if (!node) return null;
  return node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
}

function neutralizeFlexParent(el) {
  const p = el && el.parentElement;
  if (!p || p.classList.contains('abad-flex-neutralized')) return;
  const d = getComputedStyle(p).display;
  if (d === 'flex' || d === 'inline-flex' || d === 'grid' || d === 'inline-grid') {
    p.style.setProperty('display', 'block', 'important');
    p.classList.add('abad-flex-neutralized');
  }
}

function isWithinDecodableArea(node) {
  const el = toElement(node);
  if (!el || typeof el.closest !== 'function') return false;
  if (el.isContentEditable || el.closest('textarea, input')) return false;
  if (el.closest('.abad-generated, .abad-drag-container, .abad-drag-result')) return false;
  return !!(el.closest('#post-article') || el.closest('#post-comment'));
}

function escapeHtml(str) {
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function looksLikeRealText(str) {
  if (!str || str.length < 2 || str.includes('\uFFFD')) return false;
  let printable = 0;
  for (const ch of str) {
    const code = ch.codePointAt(0);
    if (code === 0x09 || code === 0x0A || code === 0x0D || code >= 0x20) printable++;
  }
  if (printable / str.length <= 0.9) return false;
  return /[가-힣ㄱ-ㆎ\s]/.test(str) || /^https?:\/\//i.test(str.trim());
}

const Base64 = {
  decode(input) {
    const cleaned = input.replace(/[^A-Za-z0-9+/=]/g, '').replace(/=+$/, '');
    if (!cleaned) return null;
    const padded = cleaned + '='.repeat((4 - cleaned.length % 4) % 4);
    try {
      const binaryString = window.atob(padded);
      const bytes = new Uint8Array(binaryString.length);
      for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
      return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
    } catch (e) {
      return null;
    }
  }
};

const brailleMap = {
  '⠁': 'a', '⠃': 'b', '⠉': 'c', '⠙': 'd', '⠑': 'e', '⠋': 'f', '⠛': 'g', '⠓': 'h', '⠊': 'i', '⠚': 'j',
  '⠅': 'k', '⠇': 'l', '⠍': 'm', '⠝': 'n', '⠕': 'o', '⠏': 'p', '⠟': 'q', '⠗': 'r', '⠎': 's', '⠞': 't',
  '⠥': 'u', '⠧': 'v', '⠺': 'w', '⠭': 'x', '⠽': 'y', '⠵': 'z'
};
const brailleNumMap = {
  '⠁': '1', '⠃': '2', '⠉': '3', '⠙': '4', '⠑': '5', '⠋': '6', '⠛': '7', '⠓': '8', '⠊': '9', '⠚': '0'
};

function decodeBraille(str) {
  let result = '';
  let isNum = false, isCaps = false, isCapsLock = false;
  for (let i = 0; i < str.length; i++) {
    const char = str[i];
    if (char === '⠸') {
      if (str[i + 1] === '⠌') { result += '/'; i++; continue; }
      if (str[i + 1] === '⠹') { result += '#'; i++; continue; }
    }
    if (char === '⠠') {
      isNum = false;
      if (str[i + 1] === '⠠') { isCapsLock = true; i++; continue; }
      else if (str[i + 1] === '⠄') { isCapsLock = false; i++; continue; }
      else { isCaps = true; continue; }
    }
    if (char === '⠼') { isNum = true; isCaps = false; isCapsLock = false; continue; }
    if (char === '⠰') { isNum = false; continue; }
    if (char === '⠲') { result += '.'; isNum = false; continue; }
    if (char === '⠒') { result += ':'; isNum = false; continue; }
    if (char === '⠤') { result += '-'; isNum = false; continue; }
    if (char === '⠐' && str[i + 1] === '⠶') { result += '='; i++; isNum = false; continue; }
    if (char === '⠖') { result += '+'; isNum = false; continue; }
    if (char === '⠌') { result += '/'; isNum = false; continue; }
    if (char === '⠹') { result += '#'; isNum = false; continue; }
    if (char === '⠶') { result += '='; isNum = false; continue; }
    if (char === '⠀') { result += ' '; isNum = false; continue; }
    if (isNum && brailleNumMap[char]) { result += brailleNumMap[char]; continue; }
    if (brailleMap[char]) {
      let c = brailleMap[char];
      if (isCaps || isCapsLock) { c = c.toUpperCase(); isCaps = false; }
      result += c;
      isNum = false;
      continue;
    }
    if (char >= '⠀' && char <= '⣿') continue;
    result += char;
    isNum = false;
  }
  return result;
}

function scrollToTarget(id_tmp, target) {
  const root = getPostRoot();
  const targetElem = document.getElementById(id_tmp) || (root && root.getElementById && root.getElementById(id_tmp));
  if (!targetElem) return;
  if (abadInternalDB.internalDB.swal2Enable) {
    Swal.close();
    showToast(`${(target === undefined) ? '해당' : target} 위치로 이동했습니다.`, 'success', 3500);
  }
  targetElem.classList.remove('abad-flash');
  void targetElem.offsetWidth;
  targetElem.classList.add('abad-flash');
  window.scrollTo({ top: window.pageYOffset + targetElem.getBoundingClientRect().top - (window.innerHeight / 2), behavior: 'smooth' });
  sleep(2600).then(() => { targetElem.classList.remove('abad-flash'); });
};

function copyToClipboard(target, cont) {
  const msgHeader = cont != undefined ? `${cont}이(가) ` : '';
  if (target == undefined) { console.warn('Copy target does not exist'); return; }
  try {
    GM.setClipboard(target);
    if (abadInternalDB.internalDB.swal2Enable) {
      let timerInterval;
      swalFire({
        title: abadConstDB.SWAL2Title,
        html: `<b>${msgHeader}클립보드로 복사되었습니다.</b><div class=\"abad-code-preview\">또는 아래 코드를 복사:<div class=\"abad-code-preview__box\">${escapeHtml(target)}</div></div>`,
        confirmButtonText: '확인', icon: 'success', timer: 3000, timerProgressBar: true,
        footer: '<span id="footer" class="abad-timer-note">&nbsp;</span>',
        didOpen: (modal) => {
          let autoClose = true;
          const handleMouseEnter = () => {
            autoClose = false; Swal.stopTimer();
            const footer = modal.querySelector('#footer');
            if (footer) footer.textContent = '창에서 마우스를 떼면 일정시간 후 자동으로 닫힙니다.';
          };
          const handleMouseLeave = () => { autoClose = true; Swal.resumeTimer(); };
          modal.onmouseenter = handleMouseEnter;
          modal.onmouseleave = handleMouseLeave;
          if (modal.matches(':hover')) handleMouseEnter();
          timerInterval = setInterval(() => {
            if (autoClose) {
              const timeLeft = Swal.getTimerLeft();
              const seconds = isNaN(Math.floor(timeLeft / 1000)) ? '0' : Math.floor(timeLeft / 1000);
              const footer = modal.querySelector('#footer');
              if (footer) footer.textContent = `약 ${seconds}초 후 창이 자동으로 닫힙니다.`;
            }
          }, 100);
        },
        willClose: () => { clearInterval(timerInterval); },
      });
    } else {
      window.alert(msgHeader + '클립보드로 복사되었습니다.');
    }
  } catch (e) { console.warn('Copy failed:', e); }
}


function showDecodeSummary() {
  if (!abadInternalDB.internalDB.swal2Enable) return;
  const listWrapperId = createElemID();
  const searchInputId = createElemID();
  swalFire({
    title: abadConstDB.SWAL2Title,
    width: 560,
    html: `
      <div class="abad-list-header">
        <b>디코딩된 링크 목록</b>
        <span class="abad-list-count">총 ${abadInternalDB.decodedList.length}개</span>
      </div>
      <input id="${searchInputId}" type="text" class="abad-search-input" placeholder="🔍 호스트명으로 검색 (예: mega.nz)">
      <div id="${listWrapperId}" class="abad-list">불러오는중...</div>
    `,
    showCancelButton: true,
    confirmButtonText: '📋 전체 복사',
    cancelButtonText: '닫기',
    showDenyButton: true,
    denyButtonText: '🚀 전체 열기',
    didOpen: (modal) => {
      Swal.showLoading();
      const listEl = modal.querySelector('#' + listWrapperId);
      const searchEl = modal.querySelector('#' + searchInputId);
      const renderList = (filterText) => {
        listEl.innerHTML = '';
        const needle = (filterText || '').trim().toLowerCase();
        abadInternalDB.decodedList.forEach((uuid) => {
          const target = abadInternalDB.decodedLink[uuid];
          if (!target) return;
          if (needle && !target.hostname.toLowerCase().includes(needle)) return;
          const row = document.createElement('div');
          row.className = 'abad-list-row';
          const favicon = document.createElement('img');
          favicon.src = faviconFor(target.hostname);
          favicon.className = 'abad-list-row__favicon';
          favicon.onerror = () => { favicon.style.display = 'none'; };
          const contLink = document.createElement('a');
          contLink.href = target.href;
          contLink.target = '_blank';
          contLink.rel = 'external nofollow noopener noreferrer';
          contLink.textContent = `${target.no}번째 링크 · ${target.hostname}`;
          contLink.className = 'abad-list-row__link';
          const gotoBtn = document.createElement('a');
          gotoBtn.textContent = '이동';
          gotoBtn.href = 'javascript:void(0);';
          gotoBtn.className = 'abad-list-row__goto';
          addListener(gotoBtn, 'click', () => scrollToTarget(target.id, `${target.no}번째 링크`));
          row.appendChild(favicon);
          row.appendChild(contLink);
          row.appendChild(gotoBtn);
          listEl.appendChild(row);
        });
        if (!listEl.childElementCount) {
          listEl.innerHTML = '<div class="abad-list-empty">일치하는 링크가 없습니다.</div>';
        }
      };
      addListener(searchEl, 'input', () => renderList(searchEl.value));
      sleep(80).then(() => { renderList(''); Swal.hideLoading(); });
    },
  }).then((result) => {
    const allLinks = abadInternalDB.decodedList.map(id => abadInternalDB.decodedLink[id]).filter(Boolean);
    if (result.isConfirmed) {
      copyToClipboard(allLinks.map(l => l.href).join('\n'), `링크 ${allLinks.length}개`);
    } else if (result.isDenied) {
      if (allLinks.length === 0) return;
      const openAll = () => allLinks.forEach(l => { try { GM.openInTab(l.href, { active: false, insert: true }); } catch (e) { window.open(l.href, '_blank'); } });
      if (allLinks.length > 5) {
        swalFire({
          title: abadConstDB.SWAL2Title,
          html: `<b>${allLinks.length}개의 링크를 새 탭으로 모두 여시겠습니까?</b>`,
          icon: 'warning', showCancelButton: true, confirmButtonText: '모두 열기', cancelButtonText: '취소',
        }).then(r => { if (r.isConfirmed) openAll(); });
      } else {
        openAll();
      }
    }
  });
}

function toHttpsCandidate(text) {
  if (text.startsWith('mega.nz') || text.startsWith('kio.ac')) return 'https://' + text;
  return text;
}

const BASE64_DETECT_SOURCE = `[A-Za-z0-9+/]{12,}={0,2}`;
const BASE64_EMBED_SOURCE = `[A-Za-z0-9+/]{8,}={0,2}`;

function fullyDecodeEmbedded(text) {
  let current = text;
  for (let pass = 0; pass < MAX_DECODE_DEPTH + 2; pass++) {
    const regex = new RegExp(BASE64_EMBED_SOURCE, 'g');
    let match, out = '', lastIndex = 0, changed = false;
    while ((match = regex.exec(current))) {
      const raw = match[0];
      let accepted = null;
      const decoded = Base64.decode(raw);
      if (decoded !== null) {
        const candidate = decodeURI(encodeURI(decoded).replaceAll('%00', '')).trim();
        if (looksLikeRealText(candidate)) accepted = candidate;
      }
      out += current.slice(lastIndex, match.index);
      out += accepted !== null ? accepted : raw;
      if (accepted !== null) changed = true;
      lastIndex = match.index + raw.length;
    }
    out += current.slice(lastIndex);
    current = out;
    if (!changed) break;
  }
  return current;
}

function decodesToRealText(sourceStr) {
  const decoded = Base64.decode(sourceStr);
  if (decoded === null) return false;
  const preview = decodeURI(encodeURI(decoded).replaceAll('%00', '')).trim();
  if (looksLikeRealText(preview)) return true;
  const fully = fullyDecodeEmbedded(preview);
  return fully !== preview && looksLikeRealText(fully);
}

function splitTrailingPunctuation(url) {
  let end = url.length;
  while (end > 0) {
    const ch = url[end - 1];
    if ('.,!?;:]'.includes(ch)) { end--; continue; }
    if (ch === ')') {
      const body = url.slice(0, end - 1);
      const opens = (body.match(/\(/g) || []).length;
      const closes = (body.match(/\)/g) || []).length;
      if (closes >= opens) { end--; continue; }
    }
    break;
  }
  return { link: url.slice(0, end), trailing: url.slice(end) };
}


function linkifyDecodedLine(line, genMode) {
  const urlRegex = /https?:\/\/[^\s<>"']+/g;
  let out = '', lastIndex = 0, match;
  while ((match = urlRegex.exec(line))) {
    const { link: trimmedLink, trailing: trailingPunct } = splitTrailingPunctuation(match[0]);
    const url_t = getLocation(trimmedLink);
    out += escapeHtml(line.slice(lastIndex, match.index));
    if (url_t) {
      if (genMode === 'article') {
        hindex++;
        const uuid = createElemID();
        abadInternalDB.hostnameSetRaw.add(url_t.hostname);
        abadInternalDB.decodedLink[uuid] = { id: uuid, no: hindex, type: genMode, hostname: url_t.hostname, href: url_t.href };
        abadInternalDB.decodedList.push(uuid);
        out += `<a id="${uuid}" class="abad-link" href="${escapeHtml(url_t.href)}" title="${escapeHtml(url_t.href)} (새 창으로 열기)" target="_blank" rel="external nofollow noopener noreferrer">${escapeHtml(trimmedLink)}</a>${escapeHtml(trailingPunct)}`;
      } else {
        out += `<a class="abad-link" href="${escapeHtml(url_t.href)}" title="${escapeHtml(url_t.href)} (새 창으로 열기)" target="_blank" rel="external nofollow noopener noreferrer">${escapeHtml(trimmedLink)}</a>${escapeHtml(trailingPunct)}`;
      }
    } else {
      out += escapeHtml(match[0]);
    }
    lastIndex = match.index + match[0].length;
  }
  out += escapeHtml(line.slice(lastIndex));
  return out;
}

function replacerGen(genMode) {
  return function (source) {
    const isBraille = /[⠀-⣿]/.test(source);
    try {
      let converted;
      if (isBraille) {
        converted = decodeBraille(source).trim();
        if (!converted) return null;
      } else {
        converted = Base64.decode(source);
        if (converted === null) return null;
      }
      converted = decodeURI(encodeURI(converted).replaceAll('%00', '')).trim();
      if (!converted || converted.includes('\uFFFD')) return null;
      const lines = converted.split(/\r?\n/).map(line => line.trim()).filter(line => line !== '');
      if (!lines.length) return null;
      const resolvedLines = lines.map(line => toHttpsCandidate(fullyDecodeEmbedded(line).trim()));
      const rendered = resolvedLines.map(line => linkifyDecodedLine(line, genMode)).join('<br>');
      return `<span class="abad-decoded-text">${rendered}</span>`;
    } catch (e) {
      return null;
    }
  };
}

function populateWithLinks(container, text) {
  const urlPattern = /(https?:\/\/[^\s]+|(?:[a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?)/g;
  const parts = text.split(urlPattern);
  parts.forEach(part => {
    if (!part) return;
    if (/^(https?:\/\/|(?:[a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,})/.test(part)) {
      const a = document.createElement('a');
      a.href = /^https?:\/\//.test(part) ? part : 'https://' + part;
      a.target = '_blank';
      a.rel = 'noreferrer';
      a.style.textDecoration = 'underline';
      a.textContent = part;
      a.title = '클릭 시 새 창으로 이동';
      container.appendChild(a);
    } else {
      container.appendChild(document.createTextNode(part));
    }
  });
}

function isFocusedInFormField() {
  const active = document.activeElement;
  return !!(active && (active.tagName === 'TEXTAREA' || active.tagName === 'INPUT' || active.isContentEditable));
}

function isDecodableSelection(selText) {
  if (!selText || selText.length < 4) return false;
  const isBraille = /^[⠀-⣿\s]+$/.test(selText);
  return isBraille || !selText.match(abadConstDB.regInvalid);
}

const RESTORE_OBSERVER_TTL_MS = 60000;

function attachRestore(decodedNode, restoreFn, observerParent, selText) {
  decodedNode.title = '드래그 하여 디코딩 된 결과입니다. (링크가 아닌 텍스트를 클릭하면 원본 복구)';
  decodedNode.addEventListener('mousedown', (e) => { if (e.detail > 1) e.preventDefault(); });
  decodedNode.addEventListener('click', function (e) {
    const t = toElement(e.target);
    if (t && t.closest && t.closest('a')) return;
    const sel = getActiveSelection();
    if (sel && !sel.isCollapsed && sel.toString().trim().length > 0) return;
    e.preventDefault();
    e.stopPropagation();
    restoreFn();
  });
  if (!observerParent) return;
  let ttlTimer = null;
  const mo = new MutationObserver(() => {
    let restored = false;
    observerParent.childNodes.forEach(n => {
      if (n !== decodedNode && n.nodeType === Node.TEXT_NODE && n.nodeValue.includes(selText)) restored = true;
    });
    if (restored) { decodedNode.remove(); mo.disconnect(); if (ttlTimer) clearTimeout(ttlTimer); }
  });
  mo.observe(observerParent, { childList: true, characterData: true, subtree: true });
  ttlTimer = setTimeout(() => { try { mo.disconnect(); } catch (_) {} }, RESTORE_OBSERVER_TTL_MS);
}

function tryDragDecode(event) {
  if (isFocusedInFormField()) return;
  const selection = getActiveSelection();
  if (!selection || selection.toString().trim().length === 0) return;
  if (!isWithinDecodableArea(selection.anchorNode)) return;
  const selText = selection.toString().trim();
  const isBraille = /^[⠀-⣿\s]+$/.test(selText);
  if (!isDecodableSelection(selText)) return;
  try {
    let converted;
    if (isBraille) {
      converted = decodeBraille(selText).trim();
    } else {
      const decoded = Base64.decode(selText);
      if (decoded === null) return;
      converted = decodeURI(encodeURI(decoded).replaceAll('%00', '')).trim();
    }
    if (!converted || converted === selText) return;
    converted = fullyDecodeEmbedded(converted).trim();
    if (!converted || converted === selText) return;
    if (event) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); }
    const makeDecodedNode = () => {
      const node = document.createElement('span');
      node.className = 'abad-generated abad-drag-result';
      populateWithLinks(node, converted);
      return node;
    };
    const anchorNode = selection.anchorNode;
    if (anchorNode && anchorNode.nodeType === Node.TEXT_NODE) {
      const originalText = anchorNode.textContent;
      const startIdx = originalText.indexOf(selText);
      if (startIdx !== -1) {
        const beforeText = originalText.substring(0, startIdx);
        const afterText = originalText.substring(startIdx + selText.length);
        const container = document.createElement('span');
        container.className = 'abad-drag-container';
        if (beforeText) container.appendChild(document.createTextNode(beforeText));
        const wrapper = makeDecodedNode();
        container.appendChild(wrapper);
        if (afterText) container.appendChild(document.createTextNode(afterText));
        const parent = anchorNode.parentNode;
        anchorNode.replaceWith(container);
        neutralizeFlexParent(container);
        attachRestore(wrapper, () => container.replaceWith(anchorNode), parent, selText);
        selection.removeAllRanges();
        return;
      }
    }
    const range = selection.getRangeAt(0);
    const parent = range.commonAncestorContainer;
    const extractedNodes = range.extractContents();
    const resultNode = makeDecodedNode();
    range.insertNode(resultNode);
    neutralizeFlexParent(resultNode);
    const actualParent = parent.nodeType === Node.TEXT_NODE ? parent.parentNode : parent;
    attachRestore(resultNode, () => resultNode.replaceWith(extractedNodes), actualParent, selText);
    selection.removeAllRanges();
  } catch (e) {}
}

function activateDragDecoding() {
  if (abadInternalDB.internalDB.dragDecodingEnable) return;
  abadInternalDB.internalDB.dragDecodingEnable = true;
  document.addEventListener('mouseup', function (e) {
    if (e.target && e.target.closest && e.target.closest('#abad-mobile-decode-btn, .abad-generated, .abad-drag-container, .abad-drag-result')) return;
    setTimeout(() => { if (abadInternalDB.internalDB.dragDecodingEnable) tryDragDecode(e); }, 10);
  }, true);
  const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
  if (isTouchDevice) {
    const mobileBtn = document.createElement('div');
    mobileBtn.id = 'abad-mobile-decode-btn';
    mobileBtn.className = 'abad-mobile-btn';
    mobileBtn.innerHTML = '🔓';
    mobileBtn.title = '선택한 텍스트 디코딩';
    document.body.appendChild(mobileBtn);
    document.addEventListener('selectionchange', () => {
      if (!abadInternalDB.internalDB.dragDecodingEnable || isFocusedInFormField()) { mobileBtn.style.display = 'none'; return; }
      const selection = getActiveSelection();
      if (selection.toString().trim().length > 0 && isWithinDecodableArea(selection.anchorNode)) {
        if (isDecodableSelection(selection.toString().trim())) { mobileBtn.style.display = 'flex'; return; }
      }
      mobileBtn.style.display = 'none';
    });
    const onMobileBtnClick = (e) => { e.preventDefault(); e.stopPropagation(); tryDragDecode(e); mobileBtn.style.display = 'none'; };
    mobileBtn.addEventListener('mousedown', onMobileBtnClick);
    mobileBtn.addEventListener('touchstart', onMobileBtnClick, { passive: false });
  }
}


function buildDepthSliderHTML(ids, value) {
  return `
    <div class="abad-depth-block">
      🎛 base64 디코딩 깊이: <span class="abad-depth-value" id="${ids.depthLabel}">${value}</span>회
      <input type="range" id="${ids.depth}" class="abad-range" min="1" max="${MAX_DECODE_DEPTH}" step="1" value="${value}">
    </div>
    <div class="abad-hint" id="${ids.warn}" style="display:none;">값을 너무 크게 지정하면 브라우저 성능에 영향을 줄 수 있습니다.</div>`;
}

function wireDepthSlider(modal, ids) {
  const rangeEl = modal.querySelector('#' + ids.depth);
  const labelEl = modal.querySelector('#' + ids.depthLabel);
  const warnEl = modal.querySelector('#' + ids.warn);
  const updateWarn = (v) => { warnEl.style.display = v > 7 ? 'block' : 'none'; };
  updateWarn(rangeEl.value);
  rangeEl.addEventListener('input', (e) => {
    labelEl.textContent = e.target.value;
    updateWarn(e.target.value);
  });
}


function openQuickSettingsPanel() {
  if (!abadInternalDB.internalDB.swal2Enable) return;
  const ids = {
    autodecode: createElemID(), draggable: createElemID(),
    expandmenu: createElemID(), depth: createElemID(), depthLabel: createElemID(), warn: createElemID(),
  };
  const before = {
    autodecode: localParameter.autodecode.value,
    draggable: localParameter.draggable.value,
    expandmenu: localParameter.expandmenu.value,
    basedepth: localParameter.basedepth.value,
  };
  const toggleRow = (id, icon, label, checked) => `
    <div class="abad-settings-row">
      <span class="abad-settings-label"><span class="abad-settings-label__icon">${icon}</span>${label}</span>
      <label class="abad-switch">
        <input type="checkbox" id="${id}" ${checked ? 'checked' : ''}>
        <span class="abad-switch__track"></span>
        <span class="abad-switch__thumb"></span>
      </label>
    </div>`;
  swalFire({
    title: abadConstDB.SWAL2Title,
    width: 480,
    html: `
      <div class="abad-settings">
        ${toggleRow(ids.autodecode, '🗝', '자동 디코딩 사용', before.autodecode)}
        ${toggleRow(ids.draggable, '🖱', '드래그 시 자동 디코딩', before.draggable)}
        ${toggleRow(ids.expandmenu, '⚙️', '메뉴 항목 모두 표시', before.expandmenu)}
        ${buildDepthSliderHTML(ids, before.basedepth)}
        <div class="abad-hint">단축키 · <b>Alt+L</b> 디코딩 목록 &nbsp;|&nbsp; <b>Alt+K</b> 이번 세션만 드래그 디코딩 토글</div>
      </div>
    `,
    showCancelButton: true,
    confirmButtonText: '저장',
    cancelButtonText: '취소',
    didOpen: (modal) => wireDepthSlider(modal, ids),
    preConfirm: () => {
      const modal = Swal.getPopup();
      return {
        autodecode: modal.querySelector('#' + ids.autodecode).checked,
        draggable: modal.querySelector('#' + ids.draggable).checked,
        expandmenu: modal.querySelector('#' + ids.expandmenu).checked,
        basedepth: parseInt(modal.querySelector('#' + ids.depth).value, 10),
      };
    },
  }).then(async (result) => {
    if (!result.isConfirmed) return;
    const after = result.value;
    localParameter.autodecode.value = after.autodecode;
    localParameter.draggable.value = after.draggable;
    localParameter.expandmenu.value = after.expandmenu;
    localParameter.basedepth.value = after.basedepth;
    await Promise.all([
      GM.setValue(localParameter.autodecode.param_name, after.autodecode),
      GM.setValue(localParameter.draggable.param_name, after.draggable),
      GM.setValue(localParameter.expandmenu.param_name, after.expandmenu),
      GM.setValue(localParameter.basedepth.param_name, after.basedepth),
    ]);
    menuStructureUpdate();
    const needsReload =
      (before.autodecode !== after.autodecode) ||
      (before.basedepth !== after.basedepth && before.autodecode);
    if (needsReload) {
      menuFuncSubPageReload('설정이 변경되었습니다.');
      return;
    }
    if (after.draggable && !before.draggable) activateDragDecoding();
    if (after.autodecode && !before.autodecode) executeDecoder();
    showToast('설정이 저장되었습니다.', 'success', 1800);
  });
}

async function menuStructureUpdate(firstRun = false) {
  localParameter.basedepth.value = Math.min(localParameter.basedepth.value, MAX_DECODE_DEPTH);
  menuStructure.autodecode.name = '🗝 자동 디코딩 ' + (localParameter.autodecode.value ? '끄기' : '켜기');
  menuStructure.basedepth.name = '🎛 base64 깊이 조절하기 - 현재 값 : ' + localParameter.basedepth.value + '회';
  menuStructure.draggable.name = '🖱 드래그 시 자동 디코딩 ' + (localParameter.draggable.value ? '끄기' : '켜기');
  menuStructure.expandmenu.name = '⚙️ 스크립트 메뉴 ' + (localParameter.expandmenu.value ? '축소' : '확장');
  menuStructure.basedepth.visible = localParameter.autodecode.value;
  if (!firstRun) {
    for (const i of Object.keys(menuStructure)) {
      try { await GM.unregisterMenuCommand(menuStructure[i].id); } catch (_) {}
    }
  }
  try {
    if (localParameter.expandmenu.value) {
      for (const i of Object.keys(menuStructure)) {
        if (menuStructure[i].visible) {
          menuStructure[i].id = await GM.registerMenuCommand(menuStructure[i].name, menuStructure[i].func, { title: menuStructure[i].desc });
        }
      }
    } else {
      menuStructure.quickpanel.id = await GM.registerMenuCommand(menuStructure.quickpanel.name, menuStructure.quickpanel.func, { title: menuStructure.quickpanel.desc });
      menuStructure.expandmenu.id = await GM.registerMenuCommand(menuStructure.expandmenu.name, menuStructure.expandmenu.func, { title: menuStructure.expandmenu.desc });
    }
  } catch (e) {}
}

function menuFuncSubPageReload(showmsg) {
  if (!abadInternalDB.internalDB.swal2Enable) return;
  swalFire({
    title: abadConstDB.SWAL2Title,
    html: `<b>${showmsg}</b><div class=\"abad-hint\">반영을 위해 사이트 새로고침이 필요합니다. 새로고침할까요?</div>`,
    icon: 'info', showCancelButton: true,
    confirmButtonText: '새로고침', cancelButtonText: '취소',
  }).then((result) => { if (result.isConfirmed) window.location.reload(); });
}


function menuFunctionAutodecode() {
  menuStructureUpdate();
  const currentState = localParameter.autodecode.value;
  if (!abadInternalDB.internalDB.swal2Enable) return;
  swalFire({
    title: abadConstDB.SWAL2Title,
    html: `<b>자동 디코딩 기능을 ${(currentState ? '끄시' : '켜시')}겠습니까?</b>`,
    icon: 'question', showCancelButton: true,
    confirmButtonText: (currentState ? '끄기' : '켜기'), cancelButtonText: '취소',
  }).then((result) => {
    if (!result.isConfirmed) return;
    const targetState = !currentState;
    localParameter.autodecode.value = targetState;
    GM.setValue(localParameter.autodecode.param_name, targetState).then(() => {
      menuStructureUpdate();
      if (targetState) {
        executeDecoder();
        showToast('자동 디코딩 기능이 활성화되었습니다.');
      } else {
        menuFuncSubPageReload('자동 디코딩 기능이 해제되었습니다. 원본 상태로 되돌리기 위해 페이지를 새로고침합니다.');
      }
    });
  });
}

function menuFunctionBasedepth() {
  menuStructureUpdate();
  const previousValue = localParameter.basedepth.value;
  if (!abadInternalDB.internalDB.swal2Enable) return;
  const ids = { depth: createElemID(), depthLabel: createElemID(), warn: createElemID() };
  swalFire({
    title: abadConstDB.SWAL2Title,
    html: `
      <div class="abad-settings">
        <b>Base64 자동 디코딩 중첩 횟수를 얼마로 지정할까요?</b>
        <div class="abad-hint">인코딩을 인코딩한 것을 여러 번 반복한 것을 자동으로 풀어냅니다. (지정 가능한 범위: 1~${MAX_DECODE_DEPTH}, 기본값 3)</div>
        ${buildDepthSliderHTML(ids, previousValue)}
      </div>
    `,
    showCancelButton: true,
    confirmButtonText: '변경',
    cancelButtonText: '취소',
    didOpen: (modal) => wireDepthSlider(modal, ids),
    preConfirm: () => parseInt(Swal.getPopup().querySelector('#' + ids.depth).value, 10),
  }).then((result) => {
    if (!result.isConfirmed) return;
    const targetValue = result.value;
    if (targetValue === previousValue) {
      showToast('기존 값과 동일합니다.', 'info', 1600);
      return;
    }
    localParameter.basedepth.value = targetValue;
    try {
      GM.setValue(localParameter.basedepth.param_name, targetValue);
      menuFuncSubPageReload(`자동 디코딩 중첩 횟수가 ${previousValue}에서 ${targetValue}(으)로 변경이 완료되었습니다.`);
    } catch (e) { localParameter.basedepth.value = previousValue; }
    finally { menuStructureUpdate(); }
  });
}


function menuFunctionDraggable() {
  menuStructureUpdate();
  const currentState = localParameter.draggable.value;
  if (!abadInternalDB.internalDB.swal2Enable) return;
  const alreadyActiveThisSession = abadInternalDB.internalDB.dragDecodingEnable;
  swalFire({
    title: abadConstDB.SWAL2Title,
    html: `<b>드래그 시 자동 디코딩을 ${(currentState ? '비' : '')}활성화 하시겠습니까?</b><div class="abad-hint">앞으로 인코딩된 부분을 드래그${(currentState ? '해도 자동으로 디코딩되지 않습' : ' 시 Base64로 인코딩된 것으로 판단되면 자동으로 디코딩을 시도합')}니다.</div>`,
    icon: 'question', showDenyButton: true, showCancelButton: !currentState && !alreadyActiveThisSession,
    confirmButtonText: (currentState ? '비활성화' : '활성화'),
    denyButtonText: '취소', cancelButtonText: alreadyActiveThisSession ? undefined : '이번에만 활성화',
  }).then((result) => {
    if (result.isConfirmed) {
      const targetState = !currentState;
      localParameter.draggable.value = targetState;
      try {
        GM.setValue(localParameter.draggable.param_name, targetState);
        if (targetState) {
          activateDragDecoding();
          showToast('앞으로 드래그 시 자동 디코딩을 진행합니다.');
        } else {
          menuFuncSubPageReload('앞으로 드래그 해도 반응하지 않습니다.');
        }
      } catch (e) { localParameter.draggable.value = currentState; }
      finally { menuStructureUpdate(); }
    } else if (result.isDismissed && result.dismiss === Swal.DismissReason.cancel && !alreadyActiveThisSession) {
      try {
        activateDragDecoding();
        showToast('드래그 시 자동 디코딩이 활성화되었습니다. <span class="abad-timer-note">(새로고침 시 자동으로 비활성화)</span>');
      } catch (e) {}
    }
  });
}

function menuFunctionChangeExpandMode() {
  menuStructureUpdate();
  const currentState = localParameter.expandmenu.value;
  if (!abadInternalDB.internalDB.swal2Enable) return;
  swalFire({
    title: abadConstDB.SWAL2Title,
    html: `<b>메뉴에 나타나는 항목을 ${(currentState ? '줄일' : '늘릴')}까요?</b><div class="abad-hint">앞으로 세부설정 메뉴가 ${(currentState ? '숨겨' : '보여')}집니다.</div>`,
    icon: 'question', showCancelButton: true,
    confirmButtonText: (currentState ? '줄이기' : '표시하기'), cancelButtonText: '취소',
  }).then((result) => {
    if (!result.isConfirmed) return;
    const targetState = !currentState;
    localParameter.expandmenu.value = targetState;
    try {
      GM.setValue(localParameter.expandmenu.param_name, targetState);
      showToast(`앞으로 세부설정 메뉴가 ${(targetState ? '보여' : '숨겨')}집니다.`);
    } catch (e) { localParameter.expandmenu.value = currentState; }
    finally { menuStructureUpdate(); }
  });
}


function menuFunctionRstDefaults() {
  menuStructureUpdate();
  if (!abadInternalDB.internalDB.swal2Enable) return;
  swalFire({
    title: abadConstDB.SWAL2Title,
    html: '<b>정말 스크립트 설정을 기본값으로 초기화하시겠습니까?</b><div class="abad-hint">초기화 완료 후 자동으로 새로고침됩니다.</div>',
    icon: 'warning', showCancelButton: true,
    focusCancel: true, confirmButtonText: '초기화 진행', cancelButtonText: '취소',
    showLoaderOnConfirm: true, timer: 10000, timerProgressBar: true,
    didOpen: (modal) => { modal.onmouseenter = Swal.stopTimer; modal.onmouseleave = Swal.resumeTimer; },
  }).then((result) => {
    if (!result.isConfirmed) return;
    swalFire({
      title: abadConstDB.SWAL2Title,
      html: '<b>설정값을 제거중입니다, 잠시만 기다려주세요..</b>',
      footer: '<span class="abad-timer-note">1분 이내로 이 창이 사라지지 않으면 수동으로 새로고침해주세요.</span>',
      didOpen: () => { Swal.showLoading(); },
      showConfirmButton: false, allowOutsideClick: false, allowEscapeKey: false, allowEnterKey: false,
    });
    try {
      Object.keys(menuStructure).forEach((i) => { try { GM.unregisterMenuCommand(menuStructure[i].id); } catch (_) {} });
      for (const i of Object.keys(localParameter)) GM.deleteValue(localParameter[i].param_name);
      sleep(250).then(() => {
        swalFire({
          title: abadConstDB.SWAL2Title,
          html: '<b>설정값이 모두 제거되었습니다.</b><div class="abad-hint">확인 후 현재 창이 자동으로 새로고침됩니다.</div>',
          footer: '<span class="abad-timer-note">비정상적으로 동작 시 스크립트를 재설치해주세요.</span>',
          icon: 'success', confirmButtonText: '확인',
          didOpen: () => { Swal.hideLoading(); },
        }).then(() => { window.location.reload(); });
      });
    } catch (e) { Swal.close(); }
  });
}

function wrapMatch(node, start, length, html, genMode) {
  const afterStart = node.splitText(start);
  const afterEnd = afterStart.splitText(length);
  const wrapper = document.createElement('span');
  wrapper.className = 'abad-generated';
  wrapper.innerHTML = html;
  afterStart.replaceWith(wrapper);
  neutralizeFlexParent(wrapper);
  processTextNode(afterEnd, genMode);
  return true;
}

function processTextNode(node, genMode) {
  genMode = genMode || 'article';
  const text = node.textContent;
  if (!text || !text.trim()) return false;

  const brailleRegex = /[⠀-⣿]{4,}/g;
  const bMatch = brailleRegex.exec(text);
  if (bMatch) {
    const sourceStr = bMatch[0];
    const html = replacerGen(genMode)(sourceStr);
    if (html !== null) return wrapMatch(node, bMatch.index, sourceStr.length, html, genMode);
  }

  for (let i = 0; i < localParameter.basedepth.value; i++) {
    const regex = new RegExp(regexEncodedPrefixDef[i].source, regexEncodedPrefixDef[i].flags);
    const match = regex.exec(text);
    if (!match) continue;
    let start = match.index;
    while (start > 0 && /[A-Za-z0-9+/=]/.test(text[start - 1])) start--;
    const sourceStr = text.slice(start, match.index + match[0].length);
    const html = replacerGen(genMode)(sourceStr);
    if (html === null) continue;
    return wrapMatch(node, start, sourceStr.length, html, genMode);
  }

  const genericRegex = new RegExp(BASE64_DETECT_SOURCE, 'g');
  let gMatch;
  while ((gMatch = genericRegex.exec(text))) {
    const sourceStr = gMatch[0];
    if (!decodesToRealText(sourceStr)) continue;
    const html = replacerGen(genMode)(sourceStr);
    if (html === null) continue;
    return wrapMatch(node, gMatch.index, sourceStr.length, html, genMode);
  }

  return false;
}

function hasSummaryBox() {
  if (document.getElementById('abad_summary_box')) return true;
  const root = getPostRoot();
  return !!(root && root.getElementById && root.getElementById('abad_summary_box'));
}

let _floatingBadge = null;
let _summaryIntersectionObserver = null;
function ensureFloatingBadge(summaryBoxEl) {
  if (!_floatingBadge) {
    _floatingBadge = document.createElement('div');
    _floatingBadge.id = 'abad_floating_badge';
    _floatingBadge.className = 'abad-fab';
    _floatingBadge.addEventListener('click', showDecodeSummary);
    document.body.appendChild(_floatingBadge);
  }
  _floatingBadge.innerHTML = `🔓 디코딩된 링크 <span class=\"abad-fab__count\">${abadInternalDB.decodedList.length}</span>`;
  if (_summaryIntersectionObserver) _summaryIntersectionObserver.disconnect();
  _summaryIntersectionObserver = new IntersectionObserver((entries) => {
    entries.forEach(entry => { _floatingBadge.style.display = entry.isIntersecting ? 'none' : 'flex'; });
  }, { threshold: 0 });
  _summaryIntersectionObserver.observe(summaryBoxEl);
}


function executeDecoder() {
  if (!localParameter.autodecode.value) return;
  const rootDefs = [
    { root: getPostRoot(), mode: 'article' },
    { root: getCommentRoot(), mode: 'comment' },
  ].filter(rd => rd.root);
  if (!rootDefs.length) return;
  let processedAny = false;
  const textTagNames = 'p, span, div, a, li, h1, h2, h3, h4, h5, h6, em, strong, td, code, pre, blockquote';
  rootDefs.forEach(({ root, mode }) => {
    const elements = Array.from(root.querySelectorAll(textTagNames));
    elements.push(root);
    elements.forEach(el => {
      if (typeof el.closest === 'function' && el.closest('.abad-generated')) return;
      const textNodes = Array.from(el.childNodes).filter(n => n.nodeType === Node.TEXT_NODE);
      textNodes.forEach(node => { if (processTextNode(node, mode)) processedAny = true; });
    });
  });

  if (processedAny && hindex > 0 && !hasSummaryBox()) {
    abadInternalDB.hostnameSet = Array.from(abadInternalDB.hostnameSetRaw).sort();
    const result = document.createElement('div');
    result.id = 'abad_summary_box';
    const result_box = document.createElement('div');
    result_box.className = 'abad-card';
    result_box.title = '클릭 시 디코딩된 링크를 한번에 볼 수 있습니다.';
    result_box.innerHTML =
      '<div class="abad-card__icon">🔓</div>' +
      '<div class="abad-card__body">' +
      `<div class="abad-card__title">총 <b>${hindex}개</b>의 링크가 자동 디코딩되었습니다</div>` +
      `<div class="abad-card__meta">감지된 사이트 ${abadInternalDB.hostnameSet.length}개 · 클릭하여 전체 목록 보기 · 단축키 Alt+L</div>` +
      '</div>' +
      '<div class="abad-card__chevron">›</div>';
    result_box.addEventListener('click', showDecodeSummary);
    result.appendChild(result_box);

    const insertTarget = getPostContainer();
    const contentWrapper = insertTarget ? insertTarget.querySelector(':scope > div.relative.min-h-60') : null;
    if (contentWrapper && contentWrapper.parentNode) {
      contentWrapper.parentNode.insertBefore(result, contentWrapper);
    } else if (insertTarget && insertTarget.shadowRoot) {
      insertTarget.shadowRoot.insertBefore(result, insertTarget.shadowRoot.firstChild);
    } else if (insertTarget && insertTarget.parentNode) {
      insertTarget.parentNode.insertBefore(result, insertTarget);
    } else {
      const fallbackTarget = document.querySelector('main');
      if (fallbackTarget && fallbackTarget.parentNode) fallbackTarget.parentNode.insertBefore(result, fallbackTarget);
    }
    ensureFloatingBadge(result_box);
  }
}

function debounce(func, delay) {
  let timer;
  return function (...args) { clearTimeout(timer); timer = setTimeout(() => func(...args), delay); };
}

let _contentObservers = [];
function attachContentObserver(debouncedExecute) {
  _contentObservers.forEach(function (o) { try { o.disconnect(); } catch (_) {} });
  _contentObservers = [];
  var targets = [getPostRoot(), getCommentRoot()].filter(Boolean);
  var observeTargets = targets.length ? targets : [document.body];
  observeTargets.forEach(function (t) {
    var mo = new MutationObserver(debouncedExecute);
    mo.observe(t, { childList: true, subtree: true });
    _contentObservers.push(mo);
  });
  return getPostRoot() || getCommentRoot() || document.body;
}

(async () => {
  injectStyles();
  syncDarkClass();
  try {
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncDarkClass);
  } catch (_) {}

  if (window.Swal) {
    const styleSA2 = document.createElement('style');
    styleSA2.textContent = '.swal2-container { z-index: 2400; }';
    document.head.appendChild(styleSA2);
    abadInternalDB.internalDB.swal2Enable = true;
  }

  for (const i of Object.keys(localParameter)) {
    localParameter[i].value = await GM.getValue(localParameter[i].param_name, localParameter[i].def_value);
  }

  menuStructureUpdate(true);
  if (localParameter.draggable.value) activateDragDecoding();

  let observedTarget = null;
  const debouncedExecute = debounce(() => {
    if (lastUrl !== location.href) {
      lastUrl = location.href;
      hindex = 0;
      abadInternalDB.decodedLink = {};
      abadInternalDB.decodedList = [];
      abadInternalDB.hostnameSetRaw.clear();
      const root = getPostRoot();
      const oldBox = document.getElementById('abad_summary_box') || (root && root.getElementById && root.getElementById('abad_summary_box'));
      if (oldBox) oldBox.remove();
      if (_floatingBadge) _floatingBadge.style.display = 'none';
    }
    executeDecoder();
    const currentSig = (getPostRoot() ? '1' : '0') + '|' + (getCommentRoot() ? '1' : '0');
    if (currentSig !== observedTarget) { observedTarget = currentSig; attachContentObserver(debouncedExecute); }
  }, 400);

  attachContentObserver(debouncedExecute);
  observedTarget = (getPostRoot() ? '1' : '0') + '|' + (getCommentRoot() ? '1' : '0');

  new MutationObserver(debouncedExecute).observe(document.body, { childList: true, subtree: false });

  debouncedExecute();

  document.addEventListener('keydown', (e) => {
    if (!e.altKey) return;
    if (e.key === 'l' || e.key === 'L') { e.preventDefault(); showDecodeSummary(); }
    if (e.key === 'k' || e.key === 'K') {
      e.preventDefault();
      if (!abadInternalDB.internalDB.dragDecodingEnable) {
        activateDragDecoding();
        showToast('드래그 시 자동 디코딩이 이번 세션에 한해 활성화되었습니다.', 'success', 1800);
      } else {
        showToast('이미 활성화되어 있습니다. (완전히 끄려면 새로고침이 필요합니다)', 'info', 1800);
      }
    }
  });
})();