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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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);
      }
    }
  });
})();