Kone base64 autodecoder

auto decode Base64 encoded link in Kone.gg

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           Kone base64 autodecoder
// @name:ko        코네 Base64 자동 디코더
// @version        1.1.5
// @author         OMNI7 (Original code by 'Laria')
// @match          https://kone.gg/*
// @description    auto decode Base64 encoded link in Kone.gg
// @description:ko 코네 내 Base64로 인코딩된 링크를 자동으로 복호화합니다.
// @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
// @namespace https://greasyfork.org/users/1575179
// ==/UserScript==
 
/* CREDITS:
  This script is a modified version of "Arca base64 autodecoder" originally created by 'Laria'.
  Original Script: https://greasyfork.org/ko/scripts/482577
 
  Special thanks to 'Laria' for the original code!
*/
 
const regexEncodedPrefixDef = [
    /(aHR0cDovL|aHR0cHM6Ly|bWVnYS5ue|a2lvLmFj)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(YUhSMGNEb3ZM|YUhSMGNITTZMe|YldWbllTVX|YTIsdmJtR)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
    /(WVVoU01HTkViM1pN|WVVoU01HTklUVFpNZ|V1d4V1JteF|V1RJeHNXWn)([\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,
];
 
let abadInternalDB = {
  encodedLink: {}, decodedLink: {}, decodedList: [], hostnameSetRaw: new Set(), hostnameSet: [],
  internalDB: { autoDecodingMaximum: 11, totlaDecodedCount: 0, dragDecodingEnable: false, swal2Enable: false },
};
 
const abadConstDB = {
  regInvalid: /[^\w\+\/=]/,
  logPrompt: { default: '['+GM.info.script.name+']', decodeManager: '['+GM.info.script.name+'-DEC]', paramManager: '['+GM.info.script.name+'-PAR]' },
  SWAL2Title: `<span style="font-size: 82.5%;">${('name:ko' in GM.info.script)?GM.info.script['name:ko']:GM.info.script.name}</span><i style="font-size: 40%;"> V ${GM.info.script.version}</i>`,
};
 
let hindex = 0;
let lastSelected = document;
let lastSelectedTime = Date.now();
let lastUrl = location.href;
 
let localParameter = {
  'autodecode': { 'param_name': 'autodecode', 'value': true, 'def_value': true },
  'basedepth': { 'param_name': 'basedepth', 'value': 3, 'def_value': 3 },
  'enclinkhide': { 'param_name': 'enclinkhide', 'value': false, 'def_value': false },
  'declinkhide': { 'param_name': 'declinkhide', 'value': false, 'def_value': false },
  'draggable': { 'param_name': 'draggable', 'value': false, 'def_value': false },
  'expandmenu': { 'param_name': 'expandmenu', 'value': true, 'def_value': true },
};
 
let menuStructure = {
  'autodecode': { 'param_name': localParameter.autodecode, 'name': '🗝 자동 디코딩 [상태]', 'desc': '페이지 접속 시 자동으로 디코딩하는 기능을 켜고 끕니다.', 'id': -1, 'func': menuFunctionAutodecode, 'visible': true },
  'basedepth': { 'param_name': localParameter.basedepth, 'name': '🎛 base64 깊이 조절하기', 'desc': '자동 base64 디코딩 깊이를 조절할 수 있습니다.', 'id': -1, 'func': menuFunctionBasedepth, 'visible': true },
  'enclinkhide': { 'param_name': localParameter.enclinkhide, 'name': '🔗 인코딩된 코드 [보이기/숨기기]', 'desc': '', 'id': -1, 'func': menuFunctionEnchide, 'visible': true },
  'declinkhide': { 'param_name': localParameter.declinkhide, 'name': '🔗 디코딩된 코드 [보이기/숨기기]', 'desc': '', 'id': -1, 'func': menuFunctionDechide, 'visible': true },
  'draggable': { 'param_name': localParameter.draggable, 'name': '🖱 드래그 시 자동 디코딩 [켜기/끄기]', 'desc': '', 'id': -1, 'func': menuFunctionDraggable, 'visible': true },
  'resetdefaults': { 'param_name': null, 'name': '🛠 스크립트 기본값 초기화', 'desc': '', 'id': -1, 'func': menuFunctionRstDefaults, 'visible': true },
  'expandmenu': { 'param_name': localParameter.expandmenu, 'name': '⚙️ 스크립트 메뉴 [축소/확장]', 'desc': '', 'id': -1, 'func': menuFunctionChangeExpandMode, 'visible': true },
};
 
let _eventHandlers = {};
const addListener = (node, event, handler, capture = false) => {
  if (!(event in _eventHandlers)) _eventHandlers[event] = [];
  _eventHandlers[event].push({ node: node, handler: handler, capture: capture });
  node.addEventListener(event, handler, capture);
};
 
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
function getLocation(href) {
  var match = href.toString().match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/);
  return match && { href: href, hostname: match[3] };
}
function createElemID() { return 'abad_'+self.crypto.randomUUID(); }
function base64AddPadding(str) { return str + Array((4 - str.length % 4) % 4 + 1).join('='); }
 
const Base64 = {
  _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
  decode : function (input) {
    let output = ""; let chr1, chr2, chr3; let enc1, enc2, enc3, enc4; let i = 0;
    input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
    while (i < input.length) {
      enc1 = this._keyStr.indexOf(input.charAt(i++)); enc2 = this._keyStr.indexOf(input.charAt(i++));
      enc3 = this._keyStr.indexOf(input.charAt(i++)); enc4 = this._keyStr.indexOf(input.charAt(i++));
      chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4;
      output = output + String.fromCharCode(chr1);
      if (enc3 != 64) output = output + String.fromCharCode(chr2);
      if (enc4 != 64) output = output + String.fromCharCode(chr3);
    }
    return this._utf8_decode(output);
  },
  _utf8_decode : function (utftext) {
    let string = ""; let i = 0; let c = 0, c2 = 0, c3 = 0;
    while (i < utftext.length) {
      c = utftext.charCodeAt(i);
      if (c < 128) { string += String.fromCharCode(c); i++; }
      else if ((c > 191) && (c < 224)) { c2 = utftext.charCodeAt(i+1); string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); i += 2; }
      else { c2 = utftext.charCodeAt(i+1); c3 = utftext.charCodeAt(i+2); string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); i += 3; }
    }
    return string;
  }
};
 
const scrollToTarget = function(id_tmp, target) {
  let targetElem = document.getElementById(id_tmp);
  if (!targetElem) {
      const pc = document.querySelector("main #post_content");
      if (pc && pc.shadowRoot) targetElem = pc.shadowRoot.getElementById(id_tmp);
  }
  if (!targetElem) return;
 
  if (abadInternalDB.internalDB.swal2Enable) {
    Swal.close();
    Swal.fire({
      icon: 'success', title: abadConstDB.SWAL2Title,
      html: `<b style="font-size: 82.5%;">${(target==undefined)?'해당':target} 위치로 이동했습니다.</b>`, toast: true, position: 'top-end', timer: 3500, timerProgressBar: true, showConfirmButton: false
    });
  }
  targetElem.style.background = '#06ff004f';
  window.scrollTo({top:window.pageYOffset + targetElem.getBoundingClientRect().top - (window.innerHeight / 2), behavior:'smooth'});
  sleep(2750).then(() => { targetElem.style.background = null; targetElem.style.transition = "all 1s"; });
};
 
function copyToClipboard(target, cont) {
  let msgHeader = '';
  if (cont != undefined) msgHeader = `${cont}이(가) `;
  if (target == undefined) {
    console.warn('Copy target does not exist');
  } else {
    try {
      GM.setClipboard(target);
      if (abadInternalDB.internalDB.swal2Enable) {
        let timerInterval;
        Swal.fire({
          title: abadConstDB.SWAL2Title,
          html: `<b>${msgHeader}클립보드로 복사되었습니다.</b><br><div style="margin-top: 15px; text-align:left; font-size:72.5%">또는 아래 코드를 복사:<div style="overflow-y:auto; overflow-wrap: anywhere; margin: 5px 0; padding: 5px; width:100%; height:150px; background-color: #e6e6e6; border-radius: 4px; box-sizing: border-box; cursor: text; user-select: text;">${target}</div></div>`,
          confirmButtonText: '확인',
          icon: 'success',
          timer: 3000,
          timerProgressBar: true,
          footer: `<span id="footer" style="font-size: 82.5%;">&nbsp;</span>`,
          didOpen: (modal) => {
            let autoClose = true;
 
            const handleMouseEnter = () => {
              autoClose = false;
              Swal.stopTimer();
              const footer = modal.querySelector("#footer");
              if (footer) footer.innerHTML = `<i style="font-size: 82.5%;">창에서 마우스를 떼면 일정시간 후 자동으로 닫힙니다.</i>`;
            };
 
            const handleMouseLeave = () => {
              autoClose = true;
              Swal.resumeTimer();
            };
 
            modal.onmouseenter = handleMouseEnter;
            modal.onmouseleave = handleMouseLeave;
 
            if (modal.matches(':hover')) {
              handleMouseEnter();
            }
 
            timerInterval = setInterval(() => {
              if (autoClose) {
                let timeLeft = Swal.getTimerLeft();
                let seconds = isNaN(Math.floor(timeLeft / 1000)) ? '0' : Math.floor(timeLeft / 1000);
                const footer = modal.querySelector("#footer");
                if (footer) footer.innerHTML = `<i style="font-size: 82.5%;">약 ${seconds}초 후 창이 자동으로 닫힙니다.</i>`;
              }
            }, 100);
          },
          willClose: () => { clearInterval(timerInterval); },
        });
      } else {
        window.alert(msgHeader + '클립보드로 복사되었습니다.');
      }
    } catch (e) {
      console.warn('Copy failed:', e);
    }
  }
}
 
function showEncodedLink(event) {
  const self = event.currentTarget;
 
  if (abadInternalDB.encodedLink.hasOwnProperty(self.id)) {
    const data = abadInternalDB.encodedLink[self.id];
 
    if (data.isEnabled) {
        let sel = window.getSelection();
        let pc = document.querySelector("main #post_content");
        let shadowSel = (pc && pc.shadowRoot && typeof pc.shadowRoot.getSelection === 'function') ? pc.shadowRoot.getSelection() : null;
 
        if ((sel && sel.toString().length > 0) || (shadowSel && shadowSel.toString().length > 0)) {
            return;
        }
    }
 
    event.preventDefault();
    event.stopPropagation();
 
    const rawLink = data.raw;
    const label = data.label || '인코딩된';
 
    if (!data.isEnabled) {
      self.innerHTML = rawLink.replace(/\r?\n/g, '<br>');
      self.style.color = '#4758bc';
      self.style.fontWeight = 'normal';
      self.style.textDecoration = 'none';
      self.title = `${label} 코드입니다. 클릭 시 내용이 복사됩니다.`;
      data.isEnabled = true;
    } else {
      copyToClipboard(rawLink, `${label} 코드`);
    }
  }
}
 
function showDecodeSummary(event) {
  if (abadInternalDB.internalDB.swal2Enable) {
    const decodedLinkListWrapper = createElemID();
    Swal.fire({
      title: abadConstDB.SWAL2Title,
      html: `<b>이 페이지에서 디코딩된 링크 목록</b><br><div style="margin-top: 15; text-align:left; font-size:80%"><div id="${decodedLinkListWrapper}" style="overflow: auto; margin: 10 0 10; width:100%; height:250px; background-color: #e6e6e6;">불러오는중...</div></div>`,
      confirmButtonText: '닫기',
      didOpen: (modal) => {
        Swal.showLoading();
        sleep(100).then(() => {
          modal.querySelector('#'+decodedLinkListWrapper).innerHTML = '';
          Object.keys(abadInternalDB.decodedLink).forEach(function(targetRaw) {
            const target = abadInternalDB.decodedLink[targetRaw];
            let cont = document.createElement("p");
            cont.style.marginBottom = '0.3rem'; cont.style.whiteSpace = 'nowrap';
 
            const elemGotoLocation = document.createElement("a");
            elemGotoLocation.id = createElemID(); elemGotoLocation.innerHTML = `[<u>클릭 시 해당 위치로 이동</u>]`; elemGotoLocation.href = "javascript:void(0);";
 
            const contLink = document.createElement("a");
            contLink.href = target.href; contLink.target = "_blank"; contLink.innerHTML = `&gt; ${target.no}번째 링크 (${target.hostname})`;
 
            cont.appendChild(contLink); cont.appendChild(document.createTextNode(" - ")); cont.appendChild(elemGotoLocation);
            modal.querySelector('#'+decodedLinkListWrapper).appendChild(cont);
            addListener(elemGotoLocation, 'click', function() { scrollToTarget(target.id, `${target.no}번째 링크`); });
          });
          Swal.hideLoading();
        });
      }
    });
  }
}
 
function createEncodedLink(src) { return `<span style="font-size: 87.5%;color: #47bc73 !important;">[ ${src.toString()} ]</span>`; }
function createMaskEncodedLink(src, decodedSrc, genMode, uuid) {
  const uuidDec = createElemID();
  let encPart = '';
  let decPart = '';
 
  if (localParameter.enclinkhide.value) {
    abadInternalDB.encodedLink[uuid] = { type: genMode, raw: src, isEnabled: true, label: '인코딩된' };
    encPart = `<a id="${uuid}" style="cursor:pointer; color:#4758bc; font-weight:normal; text-decoration:none;" title="인코딩된 코드입니다. 클릭 시 내용이 복사됩니다.">${src.replace(/\r?\n/g, '<br>')}</a>`;
  } else {
    abadInternalDB.encodedLink[uuid] = { type: genMode, raw: src, isEnabled: false, label: '인코딩된' };
    encPart = `<a id="${uuid}" style="cursor:pointer; text-decoration:underline;" title="클릭 시 인코딩된 코드를 표시합니다.">클릭 시 인코딩된 코드 보기</a>`;
  }
 
  if (localParameter.declinkhide.value) {
    abadInternalDB.encodedLink[uuidDec] = { type: genMode, raw: decodedSrc, isEnabled: true, label: '디코딩된' };
    decPart = `<a id="${uuidDec}" style="cursor:pointer; color:#4758bc; font-weight:normal; text-decoration:none;" title="디코딩된 코드입니다. 클릭 시 내용이 복사됩니다.">${decodedSrc.replace(/\r?\n/g, '<br>')}</a>`;
  } else {
    abadInternalDB.encodedLink[uuidDec] = { type: genMode, raw: decodedSrc, isEnabled: false, label: '디코딩된' };
    decPart = `<a id="${uuidDec}" style="cursor:pointer; text-decoration:underline;" title="클릭 시 디코딩된 코드를 표시합니다.">클릭 시 디코딩된 코드 보기</a>`;
  }
 
  return `${encPart} <span style="color:#aaa; cursor:default; text-decoration:none; margin: 0 4px;">|</span> ${decPart}`;
}
 
function createLink(src, decodedSrc, index, url, depth, genMode, uuid, parentuuid, isSplitChild = false) {
  abadInternalDB.hostnameSetRaw.add(url.hostname);
 
  let extraUI = "";
  if (isSplitChild) {
    extraUI = createEncodedLink(src);
  } else {
    extraUI = createEncodedLink(createMaskEncodedLink(src, decodedSrc, genMode, parentuuid));
  }
 
  return `<a id="${uuid}" href="${url.href}" title="${url.href} (새 창으로 열기)" target="_blank" rel="external nofollow noopener noreferrer">${index.toString()}번째 링크 (base64 깊이: ${depth.toString()}) <span style="font-size: 77.5%;">(${url.hostname})</span></a> ${extraUI}`;
}
 
function replacerGen(numIter, genMode) {
  return function(source) {
    const isBraille = /[\u2800-\u28FF]/.test(source);
    try {
      let rstring = "";
      let converted = "";
 
      if (isBraille) {
          const decodeBraille = (str) => {
              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 numMap = {
                  '⠁': '1', '⠃': '2', '⠉': '3', '⠙': '4', '⠑': '5', '⠋': '6', '⠛': '7', '⠓': '8', '⠊': '9', '⠚': '0'
              };
 
              let result = '';
              let isNum = false;
              let isCaps = false;
              let 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 && numMap[char]) { result += numMap[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;
          };
          converted = decodeBraille(source).trim();
      } else {
          converted = Base64.decode(base64AddPadding(source));
          for (let i=0; i<numIter; i++) converted = Base64.decode(base64AddPadding(converted));
      }
 
      hindex++;
 
      converted = decodeURI(encodeURI(converted).replaceAll('%00', ''));
      converted = converted.trim();
 
      converted = converted.split(/\r?\n/).map(item => {
          let trimmed = item.trim();
          if (trimmed.startsWith('mega.nz') || trimmed.startsWith('kio.ac')) return 'https://' + trimmed;
          return trimmed;
      });
 
      const registerDecodedLink = function(_target, _uuid, _parentuuid) {
        abadInternalDB.decodedLink[_uuid] = { id: _uuid, no: hindex, type: genMode, hostname: _target.hostname, title: _target.href, href: _target.href, srcid: _parentuuid };
        abadInternalDB.decodedList.push(_uuid);
      };
 
      if (converted.length == 1) {
        const uuid = createElemID(); const parentuuid = createElemID();
        const url_t = getLocation(converted[0]);
        if(!url_t) throw new Error("Invalid URL");
        registerDecodedLink(url_t, uuid, parentuuid);
 
        rstring += createLink(source, converted[0], hindex, url_t, numIter+1, genMode, uuid, parentuuid, false);
      } else {
        const parentuuid = createElemID();
 
        rstring += createEncodedLink(createMaskEncodedLink(source.toString(), converted.join('\n'), genMode, parentuuid));
 
        let nindex = 1;
        converted.forEach(function(i) {
          if (i != '') {
            const uuid = createElemID(); const url_t = getLocation(i);
            if(url_t) {
                registerDecodedLink(url_t, uuid, parentuuid);
 
                rstring += `<br><span style="margin-left:2px;">└ </span>${createLink(`<span style="color: #47bc73; font-weight: bold;">링크 자동 분할 : ${nindex.toString()}번째</span>`, i, hindex, url_t, numIter+1, genMode, uuid, parentuuid, true)}`;
 
                hindex++; nindex++;
            }
          }
        });
        hindex--;
        rstring = `<span style="color: #e83e8c;"><b><i>분할된 링크 총 ${nindex-1}개</i></b></span> ${rstring}`;
      }
      return rstring;
    } catch(e) { return `<span style="color: #ff0000;">[ ${isBraille ? '점자' : 'base64'} 변환 실패: ${source.toString()}]</span>`; }
  };
}
 
function tryDragDecode(event) {
  let pc = document.querySelector("main #post_content");
  let selection = null;
 
  if (pc && pc.shadowRoot && typeof pc.shadowRoot.getSelection === 'function') {
      let shadowSel = pc.shadowRoot.getSelection();
      if (shadowSel && shadowSel.rangeCount > 0 && shadowSel.toString().trim().length > 0) {
          selection = shadowSel;
      }
  }
 
  if (!selection) {
      let winSel = window.getSelection();
      if (winSel && winSel.rangeCount > 0 && winSel.toString().trim().length > 0) {
          selection = winSel;
      }
  }
 
  if (!selection) return;
  const selText = selection.toString().trim();
 
  const isBraille = /^[\u2800-\u28FF\s]+$/.test(selText);
 
  if ((isBraille || !selText.match(abadConstDB.regInvalid)) && selText.length >= 4) {
    try {
      let converted = '';
 
      if (isBraille) {
          const decodeBraille = (str) => {
              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 numMap = {
                  '⠁': '1', '⠃': '2', '⠉': '3', '⠙': '4', '⠑': '5', '⠋': '6', '⠛': '7', '⠓': '8', '⠊': '9', '⠚': '0'
              };
 
              let result = '';
              let isNum = false;
              let isCaps = false;
              let 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 && numMap[char]) { result += numMap[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;
          };
 
          converted = decodeBraille(selText).trim();
      } else {
          converted = decodeURI(encodeURI(Base64.decode(base64AddPadding(selText))).replaceAll('%00', ''));
          converted = converted.trim();
      }
 
      if (!converted || converted === selText) return;
 
      if (event) {
          event.preventDefault();
          event.stopPropagation();
          event.stopImmediatePropagation();
      }
 
      const anchorNode = selection.anchorNode;
 
      const 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.color = '#198754';
                  a.style.textDecoration = 'underline';
                  a.textContent = part;
                  a.title = '클릭 시 새 창으로 이동';
 
                  a.addEventListener('click', function(e) {
                      e.stopPropagation();
                  });
                  container.appendChild(a);
              } else {
                  container.appendChild(document.createTextNode(part));
              }
          });
      };
 
      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 = document.createElement('span');
              wrapper.style.color = '#198754';
              wrapper.style.fontWeight = 'bold';
              wrapper.style.cursor = 'pointer';
              wrapper.title = '드래그 하여 디코딩 된 결과입니다. (텍스트 클릭 시 원본 복구)';
              wrapper.className = 'abad-generated';
 
              populateWithLinks(wrapper, converted);
 
              wrapper.addEventListener('click', function(e) {
                  let sel = window.getSelection();
                  let pc = document.querySelector("main #post_content");
                  let shadowSel = (pc && pc.shadowRoot && typeof pc.shadowRoot.getSelection === 'function') ? pc.shadowRoot.getSelection() : null;
 
                  if ((sel && sel.toString().length > 0) || (shadowSel && shadowSel.toString().length > 0)) {
                      return;
                  }
 
                  e.preventDefault();
                  e.stopPropagation();
                  container.replaceWith(anchorNode);
              });

              wrapper.addEventListener('mousedown', function(e) {
                  if (e.detail > 1) {
                      e.preventDefault();
                  }
              });
 
              container.appendChild(wrapper);
              if (afterText) container.appendChild(document.createTextNode(afterText));
 
              const parent = anchorNode.parentNode;
              anchorNode.replaceWith(container);
 
              if (parent) {
                  const mo = new MutationObserver((mutations) => {
                      let restored = false;
                      parent.childNodes.forEach(n => {
                          if (n !== container && n.nodeType === Node.TEXT_NODE && n.nodeValue.includes(selText)) {
                              restored = true;
                          }
                      });
                      if (restored) {
                          container.remove();
                          mo.disconnect();
                      }
                  });
                  mo.observe(parent, { childList: true, characterData: true, subtree: true });
              }
 
              selection.removeAllRanges();
              return;
          }
      }
 
      const range = selection.getRangeAt(0);
      const parent = range.commonAncestorContainer;
      const extractedNodes = range.extractContents();
 
      const resultNode = document.createElement('span');
      resultNode.style.color = '#198754';
      resultNode.style.fontWeight = 'bold';
      resultNode.style.cursor = 'pointer';
      resultNode.title = '드래그 하여 디코딩 된 결과입니다. (텍스트 클릭 시 원본 복구)';
      resultNode.className = 'abad-generated';
 
      populateWithLinks(resultNode, converted);
 
      resultNode.addEventListener('click', function(e) {
          let sel = window.getSelection();
          let pc = document.querySelector("main #post_content");
          let shadowSel = (pc && pc.shadowRoot && typeof pc.shadowRoot.getSelection === 'function') ? pc.shadowRoot.getSelection() : null;
 
          if ((sel && sel.toString().length > 0) || (shadowSel && shadowSel.toString().length > 0)) {
              return;
          }
 
          e.preventDefault();
          e.stopPropagation();
          this.replaceWith(extractedNodes);
      });

      resultNode.addEventListener('mousedown', function(e) {
          if (e.detail > 1) {
              e.preventDefault();
          }
      });
 
      range.insertNode(resultNode);
 
      const actualParent = parent.nodeType === Node.TEXT_NODE ? parent.parentNode : parent;
      if (actualParent) {
          const mo = new MutationObserver(() => {
              let restored = false;
              actualParent.childNodes.forEach(n => {
                  if (n !== resultNode && n.nodeType === Node.TEXT_NODE && n.nodeValue.includes(selText)) {
                      restored = true;
                  }
              });
              if (restored) {
                  resultNode.remove();
                  mo.disconnect();
              }
          });
          mo.observe(actualParent, { childList: true, characterData: true, subtree: true });
      }
 
      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')) 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.innerHTML = '🔒';
      mobileBtn.title = '선택한 텍스트 디코딩';
      mobileBtn.style.cssText = `
          position: fixed;
          bottom: 25px;
          left: 25px;
          font-size: 24px;
          background: #198754;
          border: 2px solid #ddd;
          border-radius: 50%;
          width: 50px;
          height: 50px;
          display: none;
          align-items: center;
          justify-content: center;
          box-shadow: 0 4px 10px rgba(0,0,0,0.3);
          z-index: 10000;
          cursor: pointer;
          user-select: none;
      `;
      document.body.appendChild(mobileBtn);

      document.addEventListener('selectionchange', () => {
          if (!abadInternalDB.internalDB.dragDecodingEnable) {
              mobileBtn.style.display = 'none';
              return;
          }

          let pc = document.querySelector("main #post_content");
          let selection = null;

          if (pc && pc.shadowRoot && typeof pc.shadowRoot.getSelection === 'function') {
              let shadowSel = pc.shadowRoot.getSelection();
              if (shadowSel && shadowSel.toString().trim().length > 0) {
                  selection = shadowSel;
              }
          }

          if (!selection) {
              let winSel = window.getSelection();
              if (winSel && winSel.toString().trim().length > 0) {
                  selection = winSel;
              }
          }

          if (selection) {
              const selText = selection.toString().trim();
              const isBraille = /^[\u2800-\u28FF\s]+$/.test(selText);
              
              if ((isBraille || !selText.match(abadConstDB.regInvalid)) && selText.length >= 4) {
                  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 });
  }
}
 
async function menuStructureUpdate(fistRun = false) {
  localParameter.basedepth.value = Math.min(localParameter.basedepth.value, 11);
  menuStructure.autodecode.name = '🗝 자동 디코딩 '+(localParameter.autodecode.value ? '끄기' : '켜기');
  menuStructure.basedepth.name = '🎛 base64 깊이 조절하기 - 현재 값 : '+localParameter.basedepth.value+'회';
  menuStructure.enclinkhide.name = '🔗 인코딩된 링크 '+(localParameter.enclinkhide.value?'숨기기':'보이기');
  menuStructure.declinkhide.name = '🔗 디코딩된 링크 '+(localParameter.declinkhide.value?'숨기기':'보이기');
  menuStructure.draggable.name = '🖱 드래그 시 자동 디코딩 '+(localParameter.draggable.value?'끄기':'켜기');
  menuStructure.expandmenu.name = '⚙️ 스크립트 메뉴 '+(localParameter.expandmenu.value?'축소':'확장');
 
  menuStructure.enclinkhide.visible = localParameter.autodecode.value;
  menuStructure.declinkhide.visible = localParameter.autodecode.value;
  menuStructure.basedepth.visible = localParameter.autodecode.value;
 
  if (!fistRun) {
    for (let i of Object.keys(menuStructure)) {
      try { await GM.unregisterMenuCommand(menuStructure[i].id); } catch(_) {}
    }
  }
 
  try {
    if(localParameter.expandmenu.value) {
      for (let 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.expandmenu.id = await GM.registerMenuCommand(menuStructure.expandmenu.name, menuStructure.expandmenu.func, {title:menuStructure.expandmenu.desc});
    }
  } catch(e) {}
}
 
function menuFuncSubPageReload(showmsg) {
  if (abadInternalDB.internalDB.swal2Enable) {
    Swal.fire({
      title: abadConstDB.SWAL2Title,
      html: `<b>${showmsg}</b><br><br><i>> 반영을 위해 사이트 새로고침이 필요합니다.<br>사이트를 새로고침할까요?</i>`,
      icon: 'info',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      confirmButtonText: '새로고침',
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) window.location.reload(true);
    });
  }
}
 
function menuFunctionAutodecode() {
  menuStructureUpdate();
  const currentState = localParameter.autodecode.value;
 
  if (abadInternalDB.internalDB.swal2Enable) {
    Swal.fire({
      title: abadConstDB.SWAL2Title,
      html: `<b>자동 디코딩 기능을 ${(currentState ? '끄시' : '켜시')}겠습니까?</b>`,
      icon: 'question',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      cancelButtonColor: '#d33',
      confirmButtonText: (currentState ? '끄기' : '켜기'),
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) {
        const targetState = !currentState;
        localParameter.autodecode.value = targetState;
 
        GM.setValue(localParameter.autodecode.param_name, targetState).then(() => {
          menuStructureUpdate();
 
          if (targetState) {
            executeDecoder();
            Swal.fire({
              icon: 'success',
              title: abadConstDB.SWAL2Title,
              html: `<b style="font-size: 77.5%">자동 디코딩 기능이 활성화되었습니다.</b>`,
              toast: true,
              position: 'center',
              timer: 2000,
              timerProgressBar: true,
              showConfirmButton: false
            });
          } else {
            menuFuncSubPageReload('자동 디코딩 기능이 해제되었습니다. 원본 상태로 되돌리기 위해 페이지를 새로고침합니다.');
          }
        });
      }
    });
  }
}
 
function menuFunctionBasedepth() {
  menuStructureUpdate();
  const previousValue = localParameter.basedepth.value;
  const str_common_1 = ' ( 지정 가능한 범위: 1~'+abadInternalDB.internalDB.autoDecodingMaximum.toString()+' )';
 
  if (abadInternalDB.internalDB.swal2Enable) {
    const slideHandler = function(event) {
      const target = Swal.getPopup().querySelector("#footer");
      if (event.target.value > 7) {
        target.style.display = 'block';
        target.innerHTML = `<i>(값을 너무 크게 지정하면 브라우저 성능에 영향을 줄 수 있습니다.)</i>`;
      } else { target.style.display = 'none'; }
    };
    Swal.fire({
      title: abadConstDB.SWAL2Title,
      icon: "question",
      input: "range",
      html: `<b>Base64 자동 디코딩 중첩 횟수를 얼마로 지정할까요?</b><div style = "font-size: 75%; margin: 1em auto 1em"><i>(인코딩을 인코딩한 것을 여러번 반복한 것을 자동으로 풀어냅니다.)</i></div><span style = "font-size: 87.5%;">현재 값: ${previousValue.toString()}회,${(previousValue == 3 ? '' : ' 기본값: 3회,')}${str_common_1}`,
      inputAttributes: { min: "1", max: abadInternalDB.internalDB.autoDecodingMaximum.toString(), step: "1" },
      footer: `<i id="footer">${(previousValue > 7)?'(값을 너무 크게 지정하면 브라우저 성능에 영향을 줄 수 있습니다.)':''}</i>`,
      inputValue: previousValue,
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      confirmButtonText: '변경',
      cancelButtonText: '취소',
      inputValidator: (value) => {
        return new Promise((resolve) => {
          if (value == previousValue) { resolve(`기존값과 동일합니다, 현재 값: ${previousValue}회`); } else { resolve(); }
        });
      },
      didOpen: (modal) => { modal.querySelector(".swal2-range").firstChild.addEventListener('input', slideHandler, false); },
      willClose: (modal) => { modal.querySelector(".swal2-range").firstChild.removeEventListener('input', slideHandler); },
    }).then((result) => {
      if (result.isConfirmed) {
        const targetValue = parseInt(result.value);
        localParameter.basedepth.value = targetValue;
        try {
          GM.setValue(localParameter.basedepth.param_name, targetValue);
          menuFuncSubPageReload('자동 디코딩 중첩 횟수가 '+previousValue.toString()+'에서 '+targetValue.toString()+'(으)로<br>변경이 완료되었습니다.');
        } catch(e) { localParameter.basedepth.value = previousValue; }
        finally { menuStructureUpdate(); }
      }
    });
  }
}
 
function menuFunctionEnchide() {
  menuStructureUpdate();
  const currentState = localParameter.enclinkhide.value;
  if (abadInternalDB.internalDB.swal2Enable) {
    Swal.fire({
      title: abadConstDB.SWAL2Title,
      html: `<b>인코딩된 코드를 ${(currentState?'숨기시':'표시하')}겠습니까?</i>`,
      icon: 'question',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      cancelButtonColor: '#d33',
      confirmButtonText: (currentState?'숨기기':'표시하기'),
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) {
        const targetState = !currentState;
        localParameter.enclinkhide.value = targetState;
        try {
          GM.setValue(localParameter.enclinkhide.param_name, targetState);
          menuFuncSubPageReload('앞으로 인코딩된 코드를 '+(targetState?'표시합':'숨깁')+'니다.');
        } catch(e) { localParameter.enclinkhide.value = currentState; }
        finally { menuStructureUpdate(); }
      }
    });
  } else {
    const targetState = !currentState;
    localParameter.enclinkhide.value = targetState;
    try { GM.setValue(localParameter.enclinkhide.param_name, targetState); } catch(e) {}
    location.reload();
  }
}
 
function menuFunctionDechide() {
  menuStructureUpdate();
  const currentState = localParameter.declinkhide.value;
  if (abadInternalDB.internalDB.swal2Enable) {
    Swal.fire({
      title: abadConstDB.SWAL2Title,
      html: `<b>디코딩된 코드를 ${(currentState?'숨기시':'표시하')}겠습니까?</i>`,
      icon: 'question',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      cancelButtonColor: '#d33',
      confirmButtonText: (currentState?'숨기기':'표시하기'),
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) {
        const targetState = !currentState;
        localParameter.declinkhide.value = targetState;
        try {
          GM.setValue(localParameter.declinkhide.param_name, targetState);
          menuFuncSubPageReload('앞으로 디코딩된 코드를 '+(targetState?'표시합':'숨깁')+'니다.');
        } catch(e) { localParameter.declinkhide.value = currentState; }
        finally { menuStructureUpdate(); }
      }
    });
  } else {
    const targetState = !currentState;
    localParameter.declinkhide.value = targetState;
    try { GM.setValue(localParameter.declinkhide.param_name, targetState); } catch(e) {}
    location.reload();
  }
}
 
function menuFunctionDraggable() {
  menuStructureUpdate();
  const currentState = localParameter.draggable.value;
  if (abadInternalDB.internalDB.swal2Enable) {
    Swal.fire({
      title: abadConstDB.SWAL2Title,
      html: `<b>드래그 시 자동 디코딩을 ${(currentState?'비':'')}활성화 하시겠습니까?</b><br><br><i>(앞으로 인코딩된 부분을 드래그${(currentState?'해도<br>자동으로 디코딩되지 않습':' 시 Base64로 인코딩된<br>것으로 판단 되면 자동으로 디코딩을 시도합')}니다.)</i>`,
      icon: 'question',
      showDenyButton: true,
      showCancelButton: !currentState,
      confirmButtonColor: '#3085d6',
      confirmButtonText: (currentState?'비활성화':'활성화'),
      denyButtonText: '취소',
      cancelButtonText: '이번에만 활성화',
    }).then((result) => {
      if (result.isConfirmed) {
        const targetState = !currentState;
        localParameter.draggable.value = targetState;
        try {
          GM.setValue(localParameter.draggable.param_name, targetState);
          if (targetState) {
            activateDragDecoding();
            Swal.fire({ icon: 'success', title: abadConstDB.SWAL2Title, html: `<b style="font-size: 77.5%">앞으로 드래그 시 자동 디코딩을 진행합니다.</b>`, toast: true, position: 'center', timer: 2000, timerProgressBar: true, showConfirmButton: false });
          } else { menuFuncSubPageReload('앞으로 드래그 해도 반응하지 않습니다.'); }
        } catch(e) { localParameter.draggable.value = currentState; }
        finally { menuStructureUpdate(); }
      } else if (result.isDismissed && result.dismiss === Swal.DismissReason.cancel) {
        try {
          activateDragDecoding();
          Swal.fire({ icon: 'success', title: abadConstDB.SWAL2Title, html: `<b style="font-size: 77.5%">드래그 시 자동 디코딩이 활성화되었습니다.</b><br><i style="font-size: 67.5%">새로고침 시 자동으로 비활성화됩니다.</i>`, toast: true, position: 'center', timer: 2000, timerProgressBar: true, showConfirmButton: false });
        } catch(e) {}
      }
    });
  }
}
 
function menuFunctionChangeExpandMode() {
  menuStructureUpdate();
  const currentState = localParameter.expandmenu.value;
  if (abadInternalDB.internalDB.swal2Enable) {
    Swal.fire({
      title: abadConstDB.SWAL2Title,
      html: `<b>메뉴에 나타나는 항목을 ${(currentState?'줄일':'늘릴')}까요?</b><br><br><i>(앞으로 세부설정 메뉴가 ${(currentState?'숨겨':'보여')}집니다.)</i>`,
      icon: 'question',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      cancelButtonColor: '#d33',
      confirmButtonText: (currentState?'줄이기':'표시하기'),
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) {
        const targetState = !currentState;
        localParameter.expandmenu.value = targetState;
        try {
          GM.setValue(localParameter.expandmenu.param_name, targetState);
          Swal.fire({ icon: 'success', title: abadConstDB.SWAL2Title, html: `<b style="font-size: 87.5%">앞으로 세부설정 메뉴가 ${(targetState?'보여':'숨겨')}집니다.</b>`, toast: true, position: 'center', timer: 2000, timerProgressBar: true, showConfirmButton: false });
        } catch(e) { localParameter.expandmenu.value = currentState; }
        finally { menuStructureUpdate(); }
      }
    });
  }
}
 
function menuFunctionRstDefaults() {
  menuStructureUpdate();
  if (abadInternalDB.internalDB.swal2Enable) {
    Swal.fire({
      title: abadConstDB.SWAL2Title,
      html: `<b>정말 스크립트 설정을 기본값으로 초기화하시겠습니까?</b><br><br><i>(초기화 완료 후 자동으로 새로고침됩니다.)</i>`,
      icon: 'warning',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      cancelButtonColor: '#d33',
      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) {
        Swal.fire({
          title: abadConstDB.SWAL2Title,
          html: `<b>설정값을 제거중입니다, 잠시만 기다려주세요..</b>`,
          footer: `<i>1분 이내로 이 창이 사라지지 않으면 수동으로 새로고침해주세요.</i>`,
          didOpen: () => { Swal.showLoading(); },
          showConfirmButton: false, allowOutsideClick: false, allowEscapeKey: false, allowEnterKey: false,
        });
        try {
          Object.keys(menuStructure).forEach(function(i) { try { GM.unregisterMenuCommand(menuStructure[i].id); } catch(_) {} });
          for (const i of Object.keys(localParameter)) { GM.deleteValue(localParameter[i].param_name); }
          sleep(250).then(() => {
            Swal.fire({
              title: abadConstDB.SWAL2Title,
              html: `<b>설정값이 모두 제거되었습니다.</b><br><br><i>(확인 후 현재 창이 자동으로 새로고침됩니다.)</i>`,
              footer: `<i style="font-size: 82.5%;">비정상적으로 동작 시 스크립트를 재설치해주세요.</i>`,
              icon: 'success',
              confirmButtonColor: '#3085d6',
              confirmButtonText: '확인',
              didOpen: () => { Swal.hideLoading(); },
            }).then(() => { window.location.reload(true); });
          });
        } catch(e) { Swal.close(); }
      }
    });
  }
}
 
function processTextNode(node) {
    let text = node.textContent;
    if (!text || !text.trim()) return false;
 
    let brailleRegex = /[\u2800-\u28FF]{4,}/g;
    let bMatch = brailleRegex.exec(text);
    if (bMatch) {
        let sourceStr = bMatch[0];
        let start = bMatch.index;
 
        let afterStart = node.splitText(start);
        let afterEnd = afterStart.splitText(sourceStr.length);
 
        let htmlString = replacerGen(0, 'article')(sourceStr);
 
        let wrapper = document.createElement('span');
        wrapper.className = 'abad-generated';
        wrapper.innerHTML = htmlString;
 
        afterStart.replaceWith(wrapper);
 
        wrapper.querySelectorAll('[id^="abad_"]').forEach(el => {
            if (abadInternalDB.encodedLink.hasOwnProperty(el.id)) {
                el.addEventListener('click', showEncodedLink);
            }
        });
 
        processTextNode(afterEnd);
        return true;
    }
 
    for (let i = 0; i < localParameter.basedepth.value; i++) {
        let regex = new RegExp(regexEncodedPrefixDef[i].source, regexEncodedPrefixDef[i].flags);
        let match = regex.exec(text);
        if (match) {
            let sourceStr = match[0];
            let start = match.index;
 
            let afterStart = node.splitText(start);
            let afterEnd = afterStart.splitText(sourceStr.length);
 
            let htmlString = replacerGen(i, 'article')(sourceStr);
 
            let wrapper = document.createElement('span');
            wrapper.className = 'abad-generated';
            wrapper.innerHTML = htmlString;
 
            afterStart.replaceWith(wrapper);
 
            wrapper.querySelectorAll('[id^="abad_"]').forEach(el => {
                if (abadInternalDB.encodedLink.hasOwnProperty(el.id)) {
                    el.addEventListener('click', showEncodedLink);
                }
            });
 
            processTextNode(afterEnd);
            return true;
        }
    }
    return false;
}
 
function hasSummaryBox() {
    if (document.getElementById('abad_summary_box')) return true;
    const pc = document.querySelector("main #post_content");
    if (pc && pc.shadowRoot && pc.shadowRoot.getElementById('abad_summary_box')) return true;
    return false;
}
 
function executeDecoder() {
    if (!localParameter.autodecode.value) return;
 
    let containers = [];
    let pc = document.querySelector("main #post_content");
    if (pc && pc.shadowRoot) containers.push(pc.shadowRoot);
    else if (pc) containers.push(pc);
    document.querySelectorAll('main .grow.flex .grow.grid, main .md\\:items-center').forEach(e => containers.push(e));
 
    let processedAny = false;
 
    containers.forEach(container => {
        const textTagNames = 'p, span, div, a, li, h1, h2, h3, h4, h5, h6, em, strong, td, code, pre, blockquote';
        const elements = Array.from(container.querySelectorAll(textTagNames));
        elements.push(container);
 
        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)) processedAny = true;
            });
        });
    });
 
    if (processedAny && hindex > 0 && !hasSummaryBox()) {
        abadInternalDB.hostnameSet = Array.from(abadInternalDB.hostnameSetRaw).sort();
 
        let result = document.createElement("div");
        result.id = 'abad_summary_box';
        result.style.margin = '10px 0';
        result.style.paddingTop = '3px';
 
        let result_box = document.createElement("span");
        result_box.style.cursor = 'pointer';
        let result_in = '<div style="color: #e83e8c; border: 1.5px solid #68b3ff; padding: 7px 15px 5px 15px; border-radius: 5px; background: #fff;">';
        result_box.title = '클릭 시 디코딩된 링크를 한번에 볼 수 있습니다.';
        result_in += `총 <b>${hindex}개</b>의 링크가 자동 디코딩되었습니다.<br><span style="font-size: 90%; color: #666;">( 감지된 사이트 종류: ${abadInternalDB.hostnameSet.length}개 )</span>`;
        result_in += `<p style="margin-top: 5px; margin-bottom: 0px; font-size: 13px; color: #888;"><i>(클릭하여 전체 목록 보기)</i></p>`;
        result_in += '</div>';
        result_box.innerHTML = result_in;
        result_box.addEventListener('click', showDecodeSummary);
 
        result.appendChild(result_box);
        result.appendChild(document.createElement("hr"));
 
        let insertTarget = document.querySelector("main #post_content");
        if (insertTarget && insertTarget.shadowRoot) {
            insertTarget.shadowRoot.insertBefore(result, insertTarget.shadowRoot.firstChild);
        } else if (insertTarget && insertTarget.parentNode) {
            insertTarget.parentNode.insertBefore(result, insertTarget);
        } else {
            let fallbackTarget = document.querySelector('main .grow.flex .grow.grid') || document.querySelector('main');
            if (fallbackTarget && fallbackTarget.parentNode) {
                fallbackTarget.parentNode.insertBefore(result, fallbackTarget);
            }
        }
    }
}
 
function debounce(func, delay) {
    let timer;
    return function() { clearTimeout(timer); timer = setTimeout(func, delay); };
}
 
(async () => {
  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();
 
  executeDecoder();
 
  const debouncedExecute = debounce(() => {
      if (lastUrl !== location.href) {
          lastUrl = location.href;
          hindex = 0;
          abadInternalDB.decodedLink = {};
          abadInternalDB.encodedLink = {};
          abadInternalDB.hostnameSetRaw.clear();
          const pc = document.querySelector("main #post_content");
          let oldBox = document.getElementById('abad_summary_box');
          if (!oldBox && pc && pc.shadowRoot) oldBox = pc.shadowRoot.getElementById('abad_summary_box');
          if (oldBox) oldBox.remove();
      }
      executeDecoder();
  }, 400);
 
  const observer = new MutationObserver(debouncedExecute);
  observer.observe(document.body, { childList: true, subtree: true });
})();