Arca base64 autodecoder

auto decode Base64 encoded link in Arca.live

当前为 2024-01-25 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            Arca base64 autodecoder
// @name:ko         아카라이브 Base64 자동 디코더
// @version         1.212
// @author          Laria
// @match           https://arca.live/b/*/*
// @description     auto decode Base64 encoded link in Arca.live
// @description:ko  아카라이브 내 Base64로 인코딩된 링크를 자동으로 복호화합니다.
// @icon            https://www.google.com/s2/favicons?sz=64&domain=arca.live
// @require         https://cdn.jsdelivr.net/npm/sweetalert2@11
// @license         MIT
// @encoding        utf-8
// @run-at          document-end
// @supportURL      https://greasyfork.org/ko/scripts/482577-arca-base64-autodecoder
// @namespace       https://greasyfork.org/users/1235854
// @grant           GM.getValue
// @grant           GM.setValue
// @grant           GM.deleteValue
// @grant           GM.registerMenuCommand
// @grant           GM.unregisterMenuCommand
// @grant           GM.setClipboard
// ==/UserScript==

/*
 * == Change log ==
 * 1.0 - Release
 * 1.1 - Invalid character update (replace -> replaceAll)
 * 1.11 - Improved show multiple links
 * 1.12 - Show Single links Bugfix
 * 1.13 - Bugfix 1.12
 * 1.14 - Base64 add padding func
 * 1.15 - Add annotation, display improvements
 * 1.16 - Display improved - CSS applied
 * 1.17 - var safe, max_iter defined (~7, def:3)
 * 1.18 - auto update check, log system
 * 1.20 - add menu(base64 depth, user-drag auto decoding, hide encoded link, update notify)
 * 1.201 - base64 depth extends - 11, temporary disable - drag auto decoding
 * 1.202 - improve encoded link click callback, feature block in edit mode, enable drag auto decoding
 * 1.203 - add menu(restore defaults)
 * 1.204 - set update check interval -> 1day(86400), seperate localparameter
 * 1.205 - url chk add(write), code stabilization
 * 1.206 - add menu(expand menu), newline, encoded link copy function, show url hostname
 * 1.207 - show total decoded count on article top, update link fix/improve redirection, update chk interval modify(86400 -> 21600)
 * 1.21 - window alert/confirm -> swal2 gui
 * 1.211 - version fix
 * 1.212 - remove unavailble function
*/

/*
 * == TODO ==
 * auto decoding newline/space
 * detect channel => specific decoding
 * show warning message(redirection)
*/

//base64 encoded(http:/*, https:/*) string prefix
const regexEncodedPrefixDef = [
    /(aHR0cDovL|aHR0cHM6Ly)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 1 time
    /(YUhSMGNEb3ZM|YUhSMGNITTZMe)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 2 time
    /(WVVoU01HTkViM1pN|WVVoU01HTklUVFpNZ)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 3 time
    /(V1ZWb1UwMUhUa1ZpTTFwT|V1ZWb1UwMUhUa2xVVkZwTl)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 4 time
    /(VjFaV2IxVXdNVWhVYTFacFRURndU|VjFaV2IxVXdNVWhVYTJ4VlZrWndUb)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 5 time
    /(VmpGYVYySXhWWGROVldoVllURmFjRlJVUm5kV|VmpGYVYySXhWWGROVldoVllUSjRWbFpyV25kVW)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 6 time
    /(Vm1wR1lWWXlTWGhXV0dST1ZsZG9WbGxVUm1GalJsSlZVbTVrV|Vm1wR1lWWXlTWGhXV0dST1ZsZG9WbGxVU2pSV2JGcHlWMjVrVl)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 7 time
    /(Vm0xd1IxbFdXWGxUV0doWFYwZFNUMVpzWkc5V2JHeFZVbTFHYWxKc1NsWlZiVFZyV|Vm0xd1IxbFdXWGxUV0doWFYwZFNUMVpzWkc5V2JHeFZVMnBTVjJKR2NIbFdNalZyVm)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 8 time
    /(Vm0weGQxSXhiRmRYV0d4VVYwZG9XRll3WkZOVU1WcHpXa2M1VjJKSGVGWlZiVEZIWVd4S2MxTnNXbFppVkZaeV|Vm0weGQxSXhiRmRYV0d4VVYwZG9XRll3WkZOVU1WcHpXa2M1VjJKSGVGWlZNbkJUVmpKS1IyTkliRmROYWxaeVZt)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 9 time
    /(Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJsbDNXa1pPVlUxV2NIcFhhMk0xVmpKS1NHVkdXbFppVkVaSVdWZDRTMk14VG5OWGJGcHBWa1phZ|Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJsbDNXa1pPVlUxV2NIcFhhMk0xVmpKS1NHVkdXbFpOYmtKVVZtcEtTMUl5VGtsaVJtUk9ZV3hhZVZad)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 10 time
    /(Vm0wd2QyVkhVWGhUV0docFVtMVNXVll3WkRSV1ZsbDNXa2M1V0ZKc2JETlhhMXBQVmxVeFYyTkljRmhoTWsweFZtcEtTMU5IVmtkWGJGcHBWa1ZhU1ZkV1pEUlRNazE0Vkc1T1dHSkdjSEJXYTFwaF|Vm0wd2QyVkhVWGhUV0docFVtMVNXVll3WkRSV1ZsbDNXa2M1V0ZKc2JETlhhMXBQVmxVeFYyTkljRmhoTWsweFZtcEtTMU5IVmtkWGJGcE9ZbXRLVlZadGNFdFRNVWw1Vkd0c2FWSnRVazlaVjNoaFpWWmFk)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //encoding 11 time
];

//TODO
const regexEncodedPrefixNewline1 = [
    /(Cmh0dHA6L|Cmh0dHBzOi8)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 1 time
    /(Q21oMGRIQTZM|Q21oMGRIQnpPaT)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 2 time
    /(UTIxb01HUklRVFpN|aaaa)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 3 time
    /(VVRJeGIwMUhVa2xSVkZwT|aaaa)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 4 time
    /(VlZSSmVHSXdNVWhWYTJ4U1ZrWndU|aaaa)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 5 time
    /(VmxaU1NtVkhTWGROVldoV1lUSjRVMVpyV25kV|aaaa)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 6 time
    /(Vm14YVUxTnRWa2hUV0dST1ZsZG9WMWxVU2pSVk1WcHlWMjVrV|aaaa)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 7 time
    /(Vm0xNFlWVXhUblJXYTJoVVYwZFNUMVpzWkc5V01XeFZVMnBTVmsxV2NIbFdNalZyV|aaaa)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 8 time
    /(Vm0weE5GbFdWWGhVYmxKWFlUSm9WVll3WkZOVU1WcHpXa2M1VjAxWGVGWlZNbkJUVm1zeFYyTkliRmROYWxaeV|aaaa)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 9 time
    /(Vm0wd2VFNUdiRmRXV0doVllteEtXRmxVU205V1ZsbDNXa1pPVlUxV2NIcFhhMk0xVmpBeFdHVkdXbFpOYmtKVVZtMXplRll5VGtsaVJtUk9ZV3hhZV|aaaa)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 10 time
    /(Vm0wd2QyVkZOVWRpUm1SWFYwZG9WbGx0ZUV0WFJteFZVMjA1VjFac2JETlhhMXBQVmxVeFYyTkljRmhoTWsweFZtcEJlRmRIVmtkWGJGcE9ZbXRLVlZadE1YcGxSbGw1Vkd0c2FWSnRVazlaVjNoaFpW|aaaa)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 newline, encoding 11 time
];

//TODO
const regexEncodedPrefixNewline2 = [
    /(CgpodHRwOi8|CgpodHRwczov)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 1 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 2 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 3 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 4 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 5 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 6 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 7 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 8 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 9 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 10 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 newline, encoding 11 time
];

//TODO
const regexEncodedPrefixSpace1 = [
    /(IGh0dHA6L|IGh0dHBzOi8)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 1 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 2 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 3 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 4 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 5 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 6 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 7 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 8 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 9 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 10 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 1 space, encoding 11 time
];

//TODO
const regexEncodedPrefixSpace2 = [
    /(ICBodHRwOi8|ICBodHRwczov)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 1 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 2 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 3 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 4 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 5 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 6 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 7 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 8 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 9 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 10 time
    /(|)(\w|=|\+|\/)*(?=[^\+=\w\/])/g, //with 2 space, encoding 11 time
];

//auto decoding maximum
const autoDecodingMaximum = Math.min(regexEncodedPrefixDef.length, regexEncodedPrefixNewline1.length, regexEncodedPrefixNewline2.length, regexEncodedPrefixSpace1.length, regexEncodedPrefixSpace2.length);

//regex prefix - drag
const regInvalid = /[^\w\+\/=]/;

//update check interval (sec, def:1 day(86400))
const updateInterval = 21600;

//update chk, fail->false
let updateAvailble = true;

//auto drag decoding enable status
let draggableActivated = false;

//sweetalert2
let modalUIEnabled = false;

//encoded link list, [uuid]: [encoded link]
let encodedList = {};

//total decode count
let hindex = 0;

//drag function comparison
let lastSelected = document;
let lastSelectedTime = Date.now();

//domain - end chk
const deniedURLSuffix = ['/write', '/edit'];

//logging prefix, param
const logPromptDEF = '['+GM.info.script.name+']';
const logPromptDEC = '['+GM.info.script.name+'-DEC]';
const logPromptUPD = '['+GM.info.script.name+'-UPD]';
const logPromptPARAM = '['+GM.info.script.name+'-PAR]';

//script local parameter
let localParameter = {
  'lastupdate': {
    'param_name': 'lastupdate',
    'value': 0,
    'def_value': 0,
  },
  'basedepth': {
    'param_name': 'basedepth',
    'value': 3,
    'def_value': 3,
  },
  'enclinkhide': {
    'param_name': 'enclinkhide',
    'value': false,
    'def_value': false,
  },
  'draggable': {
    'param_name': 'draggable',
    'value': false,
    'def_value': false,
  },
  'updatechk': {
    'param_name': 'chkupd',
    'value': true,
    'def_value': true,
  },
  'extlinkwarn': {
    'param_name': 'extlinkwarn',
    'value': true,
    'def_value': true,
  },
  'appliedchannel': {
    'param_name': 'appliedchannel',
    'value': [],
    'def_value': [],
  },
  'expandmenu': {
    'param_name': 'expandmenu',
    'value': true,
    'def_value': true,
  },
};

//script menu structure
let menuStructure = {
  'basedepth': {
    'param_name': localParameter.basedepth,
    'name': '🎛 base64 깊이 조절하기 - 현재 값 : 알수없음',
    'desc': '자동 base64 디코딩 깊이를 조절할 수 있습니다.',
    'id': -1,
    'func': menuFunctionBasedepth,
    'visible': true,
  },
  'enclinkhide': {
    'param_name': localParameter.enclinkhide,
    'name': '🔗 인코딩된 링크 [보이기/숨기기]',
    'desc': '자동 base64 디코딩 전 인코딩된 링크를 항상 보이게 할지 설정할 수 있습니다.',
    'id': -1,
    'func': menuFunctionEnchide,
    'visible': true,
  },
  'extlinkwarn': {
    'param_name': localParameter.extlinkwarn,
    'name': '❗️ 외부 링크 경고 [보이기/숨기기]',
    'desc': '디코딩된 링크 클릭 시 외부링크에 대한 경고 메시지 표시 여부를 설정할 수 있습니다.',
    'id': -1,
    'func': menuFunctionNotAvailable,
    'visible': false,
  },
  'draggable': {
    'param_name': localParameter.draggable,
    'name': '🖱 드래그 시 자동 디코딩 [켜기/끄기]',
    'desc': '드래그 시 자동으로 드래그한 부분을 base64로 디코딩할지 설정할 수 있습니다.',
    'id': -1,
    'func': menuFunctionDraggable,
    'visible': true,
  },
  'appliedchannel': {
    'param_name': localParameter.appliedchannel,
    'name': '🏷 이 채널에서 자동 디코딩 [켜기/끄기]',
    'desc': '현재 보고있는 채널에서 자동 디코딩 기능 여부를 설정할 수 있습니다.',
    'id': -1,
    'func': menuFunctionNotAvailable,
    'visible': false,
  },
  'updatechk': {
    'param_name': localParameter.updatechk,
    'name': '🔄 업데이트 알림 [켜기/끄기]',
    'desc': '새 버전이 나올 시 업데이트 확인 알림을 띄울지 여부를 설정할 수 있습니다.',
    'id': -1,
    'func': menuFunctionUpdateCheck,
    'visible': true,
  },
  'resetdefaults': {
    'param_name': null,
    'name': '🛠 스크립트 기본값 초기화',
    'desc': '스크립트의 사용자 설정을 초기화하고 설치 상태로 되돌립니다.',
    'id': -1,
    'func': menuFunctionRstDefaults,
    'visible': true,
  },

  //proto
  'prototype': {
    'param_name': null,
    'name': '🔤 확장패널 메뉴 제목',
    'desc': '확장패널 설명 내용.',
    'id': -1,
    'func': menuFunctionNotAvailable,
    'visible': false,
  },
  //default
  'expandmenu': {
    'param_name': localParameter.expandmenu,
    'name': '⚙️ 스크립트 메뉴 [축소/확장]',
    'desc': '스크립트 설정 메뉴를 확장하거나 축소할 수 있습니다.',
    'id': -1,
    'func': menuFunctionChangeExpandMode,
    'visible': true,
  },
};

function getLocation(href) {
    var match = href.toString().match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/);
    return match && {
        href: href,
        protocol: match[1],
        host: match[2],
        hostname: match[3],
        port: match[4],
        pathname: match[5],
        search: match[6],
        hash: match[7]
    };
}

//element id - random uuid
function createElemID() {
  return 'abad_'+self.crypto.randomUUID();
}

//auto add padding - add '=' padding in base64 encoded string
function base64AddPadding(str) {
    return str + Array((4 - str.length % 4) % 4 + 1).join('=');
}

//base64 decode
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;

      //last bits
      output = output + String.fromCharCode(chr1);
      if (enc3 != 64) { //=
        output = output + String.fromCharCode(chr2);
      }
      if (enc4 != 64) { //==
        output = output + String.fromCharCode(chr3);
      }
    }

    output = Base64._utf8_decode(output);
    return output;
  },
  // private method for UTF-8 decoding
  _utf8_decode : function (utftext) {
    let string = "";
    let i = 0;
    let c = 0;
    let c1 = 0;
    let c2 = 0;
    let 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;
  }
};

//encoded link click callback
function showEncodedLink(event) {
  const self = event.currentTarget;
  //check already clicked
  if (encodedList.hasOwnProperty(self.id)) {
    window.console.log(logPromptDEC, 'show encoded link -', encodedList[self.id]);
    self.innerHTML = encodedList[self.id];
    self.style.color = 'rgb(71 88 188)';
    self.title = '디코딩 전 인코딩된 링크입니다, 클릭 시 내용이 복사됩니다.';
    delete encodedList[self.id];
  } else {
    window.console.log(logPromptDEF, 'copy link to clipboard -', self.innerHTML);
    try {
      GM.setClipboard(self.innerHTML);
      if (modalUIEnabled) {
        Swal.fire({
          title: logPromptDEF,
          text: '인코딩된 코드가 클립보드로 복사되었습니다.',
          icon: 'success',
        });
      } else {
        window.alert(logPromptDEF+'\n인코딩된 코드가 클립보드로 복사되었습니다.');
      }
    } catch (e) {
      window.console.warn(logPromptDEC, 'error occured link copy:', e);
      if (modalUIEnabled) {
        Swal.fire({
          title: logPromptDEF,
          text: '코드 복사 실패.',
          icon: 'error',
        });
      } else {
        window.alert(logPromptDEF+'\n코드 복사 실패.');
      }
    }
  }
  return;
}

//link area
function createEncodedLink(src) {
  return '<span style="font-size: 87.5%;color: rgb(71 188 115);">[ ' + src.toString() + ' ]</span>';
}

//encoded link element
function createMaskEncodedLink(src) {
  const uuid = createElemID();
  encodedList[uuid] = src;
  return '<span id="' + uuid.toString() + '" title="클릭 시 디코딩 전 인코딩된 링크를 표시합니다.">' + '클릭 시 인코딩된 코드 보기' + '</span>';
}

//link creation
function createLink(src, index, url, depth, hidelink = false) {
  //n번째 링크 (base64 깊이: 0) [ ABCDEF= / 클릭시 원본~ ]
  return '<a href="'+url+'" title="'+url+' (새 창으로 열기)" target="_blank" rel="external nofollow noopener noreferrer">'+index.toString()+'번째 링크 (base64 깊이: '+depth.toString()+') <span style="font-size: 77.5%;">('+getLocation(url).hostname+')</span></a> '+(hidelink?createEncodedLink(createMaskEncodedLink(src)):createEncodedLink(src))+'';
}

//decode & generate
function replacerGen(numIter) {
  return function(source) {
    try {
      let rstring = ""; //return msg
      window.console.log('\n'+logPromptDEC,'No.',(hindex+1),'encoded link:\n', source.toString()); //source

      //decode
      let converted = Base64.decode(base64AddPadding(source));
      //attempt to decode nested base64 encoded string
      for (let i=0; i<numIter; i++) {
          converted = Base64.decode(base64AddPadding(converted));
      }
      hindex++;

      //remove invalid string - �
      converted = decodeURI(encodeURI(converted).replaceAll('%00', ''));
      window.console.log(logPromptDEC,'No.',hindex,'decode completed (depth:',numIter+1,'):\n',converted.toString()); //converted

      //trim
      converted = converted.trim();

      //split by new line
      converted = converted.split(/\r?\n/);
      //single component
      if (converted.length == 2 && converted[converted.length-1] == '') {
        rstring += createLink(source, hindex, converted[0], numIter+1, !localParameter.enclinkhide.value);
      //multiple component
      } else if (converted.length > 1) {
        rstring += createEncodedLink(localParameter.enclinkhide.value?source.toString():createMaskEncodedLink(source.toString()));

        let nindex = 1;
        const hindexPrev = hindex;
        converted.forEach(function(i) {
          if (i != '') {
            rstring += '<br><span style="margin-left:2px;">└ </span>' + createLink('<span style="color: rgb(71 188 115);" title="자동으로 분할된 '+nindex.toString()+'번째 링크입니다.">링크 자동 분할 : '+nindex.toString()+'번째</span>', hindex, i, numIter+1);
            hindex++;
            nindex++;
          }
        });
        //apply last components
        hindex--;
        nindex--;

        window.console.log(logPromptDEC,'No.',hindexPrev,'- splitted total :', nindex);
        rstring = '<span style="color: rgb(232 62 140);"><b><i>분할된 링크 총 '+nindex.toString()+'개</i></b></span> ' + rstring;
      } else rstring += createLink(source, hindex, converted, numIter+1, !localParameter.enclinkhide.value);
      return rstring;
    } catch(e) {
      window.console.warn('\n'+logPromptDEC,'error occured during decoding:', e);
      window.console.warn(logPromptDEC,'base64 decode fail:', source);
    }
    return '<span style="color: rgb(255 0 0);">[ base64 변환 실패: '+source.toString()+' ]</span>';
  };
}

//user drag event
function selClicked(event) {
  const sel = document.getSelection().toString();
  if (!sel.match(regInvalid) && sel.length >= 10 && lastSelectedTime + 200 < Date.now()) {
    try {
      window.console.log(logPromptDEC,'live match -',sel.toString());
      let converted = decodeURI(encodeURI(Base64.decode(base64AddPadding(sel))).replaceAll('%00', ''));
      window.console.log(logPromptDEC,'converted -',converted.toString());
      this.innerHTML = this.innerHTML.replace(sel, converted)+' ';
    } catch (e) {
      return;
    } finally {
      this.removeEventListener('click', selClicked);
    }
  }
}

//user drag activate
function activateDragDecoding() {
  if (draggableActivated) {
    window.console.log(logPromptDEF,'USR-Drag already enabled.');
    return;
  }
  draggableActivated = true;
  window.console.log(logPromptDEF,'USR-Drag enabled.');
  document.addEventListener('selectionchange', function() {
    let sel = document.getSelection().anchorNode;
    if (sel) {
      sel = sel.parentElement;
      if (sel != lastSelected) {
        lastSelected.removeEventListener('click', selClicked);
        sel.addEventListener('click', selClicked);
        lastSelected = sel;
        lastSelectedTime = Date.now();
      }
    }
  });
}

//update check
function checkForUpdate() {
  if (!updateAvailble || !localParameter.updatechk.value) {
    window.console.log(logPromptUPD,'updchk skipped.');
    return;
  }
  const currentTime = Math.floor(new Date().getTime() / 1000);
  if (currentTime - localParameter.lastupdate.value < updateInterval) {
    window.console.log(logPromptUPD,'updchk already done in '+updateInterval+' sec.. skip updchk');
    return;
  }
  try {
    GM.setValue(localParameter.lastupdate.param_name, currentTime);
  } catch(e) {
    window.console.error(logPromptUPD,'last upd time write fail -', e);
    return;
  }

  window.console.log(logPromptUPD,'checking for update...');

  const svrMetadataLink = 'https://update.greasyfork.org/scripts/482577/Arca%20base64%20autodecoder.meta.js';
  const scriptLink = 'https://greasyfork.org/ko/scripts/482577-arca-base64-autodecoder';
  fetch(svrMetadataLink)
  .then(response => response.text())
  .then(data => {
    //extract version from greaskyfork script
    const match = data.match(/@version\s+(\d+\.\d+)/);
    if (match) {
      const tar_version = parseFloat(match[1]);
      const cur_version = parseFloat(GM.info.script.version);

      const openUpdateLink = () => {
        window.console.log(logPromptUPD,'opening source url..');
        if(window.open(scriptLink) == null) {
          window.console.log(logPromptUPD,'popup block detected..');
          if (modalUIEnabled) {
            Swal.fire({
              title: logPromptDEF,
              html: '<b>팝업 차단</b>이 설정된 것으로 보입니다.<br>차단을 해제해주세요..',
              icon: 'warning',
              timer: 15000,
              timerProgressBar: true,
              toast: true,
            });
          } else {
            window.alert(logPromptDEF+'\n팝업 차단이 설정된 것으로 보입니다, 차단을 해제해주세요..');
          }
        } else {
          if (modalUIEnabled) {
            Swal.fire({
              title: logPromptDEF,
              html: '<i>업데이트 후 새로고침해야 적용됩니다.</i>',
              icon: 'info',
              timer: 15000,
              timerProgressBar: true,
              toast: true,
            });
          } else {
            window.alert(logPromptDEF+'\n업데이트 후 새로고침해야 적용됩니다.');
          }
        }
      };

      //new version detected
      if (tar_version > cur_version) {
        window.console.log(logPromptUPD,'new version available. ('+cur_version+' -> '+tar_version+')');


        if (modalUIEnabled) {
            //y/n dialog
            Swal.fire({
              title: logPromptDEF,
              html: '<strong>새로운 버전이 감지되었습니다. 업데이트를 권장합니다.</strong><br>( 기존버전 : '+cur_version+', 새로운 버전 : '+tar_version+' )<br>(변경사항은 아카라이브 게시글을 참고해주세요.)<br><br><i>"알림 끄기"를 누르면 앞으로 업데이트 알림을 띄우지 않습니다.</i>',
              icon: 'info',
              showDenyButton: true,
              confirmButtonColor: '#3085d6',
              denyButtonColor: '#d33',
              confirmButtonText: '업데이트',
              denyButtonText: '알림 끄기',
              timer: 20000,
              timerProgressBar: true,
              didOpen: (modal) => {
                  modal.onmouseenter = Swal.stopTimer;
                  modal.onmouseleave = Swal.resumeTimer;
              }
            }).then((result) => {
              if (result.isConfirmed) {

                //get extension env
                if (!GM.info.scriptWillUpdate) {
                  window.console.log(logPromptUPD,'extension not allowed auto update..');
                  Swal.fire({
                    title: logPromptDEF,
                    html: '<b>주의!</b><br>스크립트 내용 변경 등으로 인해 확장프로그램 내 자동 업데이트가 꺼져있는 것 같습니다.<br>업데이트 시 기존 스크립트에 덮어쓰게 되어 기존 내용이 손실될 수 있습니다.<br>이 점 확인 후 업데이트 바랍니다.<br><br><i>(계속하려면 확인, 취소하려면 취소를 눌러주세요.)</i>',
                    icon: 'warning',
                    showCancelButton: true,
                    confirmButtonColor: '#3085d6',
                    cancelButtonColor: '#d33',
                    confirmButtonText: '확인',
                    cancelButtonText: '취소',
                    timer: 10000,
                    timerProgressBar: true,
                    didOpen: (modal) => {
                        modal.onmouseenter = Swal.stopTimer;
                        modal.onmouseleave = Swal.resumeTimer;
                    }
                  }).then((result) => {
                    if (result.isConfirmed) {
                      openUpdateLink();
                     } else {
                      window.console.log(logPromptUPD,"user canceled.");
                    }
                  });
                } else {
                  openUpdateLink();
                }
              } else if (result.isDenied){
                window.console.log(logPromptPARAM,'updatechk change',true.toString(),'to',false.toString());
                try {
                  GM.setValue(localParameter.updatechk.param_name, false);
                  localParameter.updatechk.value = false;
                  window.console.log(logPromptPARAM,"updatechk change successful");
                  menuStructureUpdate();
                  Swal.fire({
                    icon: 'success',
                    title: logPromptDEF,
                    text: '앞으로 업데이트 알림을 띄우지 않습니다.',
                    toast: true,
                    position: 'center',
                    timer: 3000,
                    timerProgressBar: true,
                    confirmButtonText: '확인',
                  });
                } catch(e) {
                  localParameter.updatechk.value = true;
                  window.console.error(logPromptPARAM,"updatechk change fail -", e);
                  Swal.fire({
                    title: logPromptDEF,
                    html: '파라미터 변경 중 문제 발생, 브라우저 로그를 확인해주세요..',
                    icon: 'error',
                  });
                }
              } else {
                window.console.log(logPromptUPD,"time out");
              }
            });
        } else {
          //y/n dialog
          if (window.confirm(logPromptDEF+'\n새로운 버전이 감지되었습니다. 업데이트를 권장합니다.\n( 기존버전 : '+cur_version+', 새로운 버전 : '+tar_version+' )\n(변경사항은 아카라이브 게시글을 참고해주세요.)\n\n취소를 누르면 앞으로 업데이트 알림을 띄우지 않습니다.')) {
            //get extension env
            if (!GM.info.scriptWillUpdate) {
              window.console.log(logPromptUPD,'extension not allowed auto update..');
              if (window.confirm(logPromptDEF+'\n주의! 스크립트 내용 변경 등으로 인해 확장프로그램 내 자동 업데이트가 꺼져있는 것 같습니다.\n업데이트 시 기존 스크립트에 덮어쓰게 되어 기존 내용이 손실될 수 있습니다.\n이 점 확인 후 업데이트 바랍니다.\n\n(계속하려면 확인, 취소하려면 취소를 눌러주세요.)')) {
                openUpdateLink();
              } else {
                window.console.log(logPromptUPD,"user canceled.");
              }
            } else {
              openUpdateLink();
            }
          } else {
            window.console.log(logPromptPARAM,'updatechk change',true.toString(),'to',false.toString());
            try {
              GM.setValue(localParameter.updatechk.param_name, false);
              localParameter.updatechk.value = false;
              window.console.log(logPromptPARAM,"updatechk change successful");
              menuStructureUpdate();
              window.alert(logPromptDEF+'\n앞으로 업데이트 알림을 띄우지 않습니다.');
            } catch(e) {
              localParameter.updatechk.value = true;
              window.console.error(logPromptPARAM,"updatechk change fail -", e);
              window.alert(logPromptDEF+'\n파라미터 변경 중 문제 발생, 브라우저 로그를 확인해주세요..');
            }
          }
        }
      } else {
        window.console.log(logPromptUPD,'latest version', cur_version, 'detected. (eth:',tar_version,')');
      }
    } else {
      window.console.error(logPromptUPD,'unable to extract version..');
    }
  })
  .catch(error => {
    updateAvailble = false;
    window.console.error(logPromptUPD,'link unreachable.. -', error);
    //next try
    try {
      GM.setValue(localParameter.updatechk.param_name, true);
    } catch (_) {}
  });
  updateAvailble = false;
}

//menu update
function menuStructureUpdate(fistRun = false) {
  //pre process
  localParameter.basedepth.value = localParameter.basedepth.value > autoDecodingMaximum ? autoDecodingMaximum : localParameter.basedepth.value;

  //update menu name
  menuStructure.basedepth.name = '🎛 base64 깊이 조절하기 - 현재 값 : '+localParameter.basedepth.value+'회';
  menuStructure.enclinkhide.name = '🔗 인코딩된 링크 '+(localParameter.enclinkhide.value?'숨기기':'보이기');
  menuStructure.draggable.name = '🖱 드래그 시 자동 디코딩 '+(localParameter.draggable.value?'끄기':'켜기');
  menuStructure.updatechk.name = '🔄 업데이트 알림 '+(localParameter.updatechk.value?'끄기':'켜기');

  menuStructure.extlinkwarn.name = '❗️ 외부 링크 경고 '+(localParameter.extlinkwarn.value?'숨기기':'보이기');
  menuStructure.appliedchannel.name = '🏷 이 채널에서 자동 디코딩 [켜기/끄기]';

  menuStructure.expandmenu.name = '⚙️ 스크립트 메뉴 '+(localParameter.expandmenu.value?'축소':'확장');

  //remove exist menu cmd
  if (!fistRun) {
    Object.keys(menuStructure).forEach(function(i) {
      try {
        GM.unregisterMenuCommand(menuStructure[i].id);
      } catch(_) {}
    });
  }
  //monkey menu cmd register
  try {
    //all menu expanded
    if(localParameter.expandmenu.value) {
      Object.keys(menuStructure).forEach(function(i) {
        if (menuStructure[i].visible) {
          menuStructure[i].id = GM.registerMenuCommand(menuStructure[i].name, menuStructure[i].func, {title:menuStructure[i].desc});
        } else {
          //if invisible -> use default parameter
          if (localParameter.hasOwnProperty(i)) {
            localParameter[i].value = localParameter[i].def_value;
          }
        }
      });
    //simple menu
    } else {
      menuStructure.expandmenu.id = GM.registerMenuCommand(menuStructure.expandmenu.name, menuStructure.expandmenu.func, {title:menuStructure.expandmenu.desc});
    }
    window.console.log(logPromptPARAM,'ext opt pannel',(fistRun?'registered':'reloaded'));
  } catch(e) {
    window.console.error(logPromptPARAM,'err - ext opt pannel',(fistRun?'register':'reload'),'- ', e);
    Object.keys(menuStructure).forEach(function(i) {
      try {
        GM.unregisterMenuCommand(menuStructure[i].id);
      } catch(_) {}
    });
    try { GM.registerMenuCommand('ⓘ 메뉴 추가 실패, 브라우저 로그 참고', () => {
      if (modalUIEnabled) {
        Swal.fire({
          title: logPromptDEF,
          html: '메뉴 추가 도중 문제가 발생했습니다.<br><i>브라우저 로그를 확인해주세요..</i>',
          icon: 'error',
          timer: 5000,
          timerProgressBar: true,
        });
      } else {
        window.alert(logPromptDEF+'\n메뉴 추가 도중 문제가 발생했습니다, 브라우저 로그를 확인해주세요..');
      }
    }); } catch(_) {}
  }
}

function menuFuncSubPageReload(showmsg) {
  if (modalUIEnabled) {
    Swal.fire({
      title: logPromptDEF,
      html: ((showmsg==undefined)?'':(showmsg+'<br><br>'))+'> 반영을 위해 사이트 새로고침이 필요합니다, 사이트를 새로고침할까요?',
      icon: 'info',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      confirmButtonText: '새로고침',
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) {
        window.location.reload(true);
      } else {
        window.console.log(logPromptDEF, 'page reload canceled');
      }
    });
  } else {
    if(window.confirm(logPromptDEF+'\n'+((showmsg==undefined)?'':(showmsg+'\n\n'))+'> 반영을 위해 사이트 새로고침이 필요합니다, 사이트를 새로고침할까요?')) {
      window.location.reload(true);
    }
  }
}

function menuFunctionBasedepth() {
  menuStructureUpdate();
  const previousValue = localParameter.basedepth.value;
  const str_common_1 = ' ( 지정 가능한 범위: 1~'+autoDecodingMaximum.toString()+' )';

  if (modalUIEnabled) {
    Swal.fire({
      title: logPromptDEF,
      icon: "question",
      input: "range",
      html: 'Base64 자동 디코딩 중첩 횟수를 얼마로 지정할까요?<br><i>(인코딩을 인코딩한 것을 여러번 반복한걸 자동으로 풀어냅니다.)</i><br>현재 값: '+previousValue.toString()+'회,'+(previousValue == 3 ? '' : ' 기본값: 3회,')+str_common_1,
      inputAttributes: {
        min: "1",
        max: autoDecodingMaximum.toString(),
        step: "1"
      },
      footer: '<i>(값을 너무 크게 지정하면 컴퓨터 성능에 영향을 줄 수 있습니다.)</i>',
      inputValue: previousValue,
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      confirmButtonText: '변경',
      cancelButtonText: '취소',
      inputValidator: (value) => {
        return new Promise((resolve) => {
          if (value == previousValue) {
            resolve('기존값과 동일합니다, 현재 값: '+previousValue+'회');
          } else {
            resolve();
          }
        });
      },
    }).then((result) => {
      if (result.isConfirmed) {
        const targetValue = parseInt(result.value);
        window.console.log(logPromptPARAM,'basedepth change',previousValue.toString(),'to',targetValue.toString());
        localParameter.basedepth.value = targetValue;
        try {
          GM.setValue(localParameter.basedepth.param_name, targetValue);
          window.console.log(logPromptPARAM,"basedepth change successful");
          menuFuncSubPageReload('값이 '+previousValue.toString()+'에서 '+targetValue.toString()+'으로 변경이 완료되었습니다.');
        } catch(e) {
          localParameter.basedepth.value = previousValue;
          window.console.error(logPromptPARAM,"basedepth change fail -", e);
          Swal.fire({
            title: logPromptDEF,
            html: '파라미터 변경 중 문제 발생, 브라우저 로그를 확인해주세요..',
            icon: 'error',
          });
        } finally {
          menuStructureUpdate();
        }
      } else {
        window.console.log(logPromptDEF,'basedepth change canceled.');
      }
    });
  } else {
    while (true) {
      const input = window.prompt(logPromptDEF+'\nBase64 자동 디코딩 중첩 횟수를 얼마로 지정할까요?\n(인코딩을 인코딩한 것을 여러번 반복한걸 자동으로 풀어냅니다.)\n현재 값: '+previousValue.toString()+'회,'+(previousValue == 3 ? '' : ' 기본값: 3회,')+str_common_1+'\n\n(값을 너무 크게 지정하면 컴퓨터 성능에 영향을 줄 수 있습니다.)', previousValue);
      if (input == null) {
        window.console.log(logPromptDEF,'basedepth change canceled.');
        break;
      }
      if (!isNaN(input)) {
        const targetValue = parseInt(input);
        if (targetValue == previousValue) {
          window.alert(logPromptDEF+'\n동일한 값을 입력했습니다, 현재 값: '+previousValue+'회');
        } else if (targetValue >= 1 && targetValue <= autoDecodingMaximum) {
          window.console.log(logPromptPARAM,'basedepth change',previousValue.toString(),'to',targetValue.toString());
          localParameter.basedepth.value = targetValue;
          try {
            GM.setValue(localParameter.basedepth.param_name, targetValue);
            window.console.log(logPromptPARAM,"basedepth change successful");
            menuFuncSubPageReload('값이 '+previousValue.toString()+'에서 '+targetValue.toString()+'으로 변경이 완료되었습니다.');
          } catch(e) {
            localParameter.basedepth.value = previousValue;
            window.console.error(logPromptPARAM,"basedepth change fail -", e);
            window.alert(logPromptDEF+'\n파라미터 변경 중 문제 발생, 브라우저 로그를 확인해주세요..');
          } finally {
            menuStructureUpdate();
            break;
          }
        } else {
          window.alert(logPromptDEF+'\n'+targetValue+'(으)로 설정할 수 없습니다.\n범위를 초과하였습니다..'+str_common_1);
        }
      } else {
        window.alert(logPromptDEF+'\n'+input+'은(는)숫자가 아닙니다.\n숫자만 입력해주세요..'+str_common_1);
      }
    }
  }
}

function menuFunctionEnchide() {
  menuStructureUpdate();
  const currentState = localParameter.enclinkhide.value;
  if (modalUIEnabled) {
    Swal.fire({
      title: logPromptDEF,
      html: '<b>디코딩 시 인코딩된 링크를 '+(currentState?'숨기시':'표시하')+'겠습니까?</b><br><br><i>(앞으로 디코딩 전 인코딩된 링크를<br>"'+(currentState?'클릭 시 기존링크 보기':'aHR0cHM6Ly9hcmNhLmx..')+'"와 같은 형태로 보여줍니다.)</i>',
      icon: 'question',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      cancelButtonColor: '#d33',
      confirmButtonText: '네',
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) {
        const targetState = !currentState;
        window.console.log(logPromptPARAM,'enchide change',currentState.toString(),'to',targetState.toString());
        localParameter.enclinkhide.value = targetState;
        try {
          GM.setValue(localParameter.enclinkhide.param_name, targetState);
          window.console.log(logPromptPARAM,"updatechk change successful");
          Swal.fire({
            icon: 'success',
            title: logPromptDEF,
            text: '앞으로 인코딩된 링크를 '+(targetState?'표시합':'숨깁')+'니다.',
            toast: true,
            position: 'center',
            timer: 1500,
            timerProgressBar: true,
            confirmButtonText: '확인',
          });
        } catch(e) {
          localParameter.enclinkhide.value = currentState;
          window.console.error(logPromptPARAM,"enchide change fail -", e);
          Swal.fire({
            title: logPromptDEF,
            html: '파라미터 변경 중 문제 발생.<br><i>브라우저 로그를 확인해주세요..</i>',
            icon: 'error',
          });
        } finally {
          menuStructureUpdate();
        }
      } else {
        window.console.log(logPromptDEF,'enchide change canceled.');
      }
    });
  } else {
    if (window.confirm(logPromptDEF+'\n디코딩 시 인코딩된 링크를 '+(currentState?'숨기시':'표시하')+'겠습니까?\n\n(앞으로 디코딩 전 인코딩된 링크를\n"'+(currentState?'클릭 시 기존링크 보기':'aHR0cHM6Ly9hcmNhLmx..')+'"와 같은 형태로 보여줍니다.)')) {
      const targetState = !currentState;
      window.console.log(logPromptPARAM,'enchide change',currentState.toString(),'to',targetState.toString());
      localParameter.enclinkhide.value = targetState;
      try {
        GM.setValue(localParameter.enclinkhide.param_name, targetState);
        window.console.log(logPromptPARAM,"updatechk change successful");
        if (targetState) {
          menuFuncSubPageReload('앞으로 인코딩된 링크를 표시합니다.');
        } else {
          window.alert(logPromptDEF+'\n앞으로 인코딩된 링크를 숨깁니다.');
        }
      } catch(e) {
        localParameter.enclinkhide.value = currentState;
        window.console.error(logPromptPARAM,"enchide change fail -", e);
        window.alert(logPromptDEF+'\n파라미터 변경 중 문제 발생, 브라우저 로그를 확인해주세요..');
      } finally {
        menuStructureUpdate();
      }
    } else {
      window.console.log(logPromptDEF,'enchide change canceled.');
    }
  }
}

function menuFunctionDraggable() {
  menuStructureUpdate();
  const currentState = localParameter.draggable.value;
  if (modalUIEnabled) {
    Swal.fire({
      title: logPromptDEF,
      html: '<b>드래그 시 자동 디코딩을 '+(currentState?'비':'')+'활성화 하시겠습니까?</b><br><br><i>(앞으로 인코딩된 부분을 드래그'+(currentState?'해도 자동으로 디코딩되지 않습':' 시 Base64로 인코딩된것으로 판단 되면 자동으로 디코딩을 시도합')+'니다.)</i>'+(currentState?'':'<br><br><i>(이 기능은 작동이 불안정할 수 있습니다.)</i>'),
      icon: 'question',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      cancelButtonColor: '#d33',
      confirmButtonText: '네',
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) {
        const targetState = !currentState;
        window.console.log(logPromptPARAM,'draggable change',currentState.toString(),'to',targetState.toString());
        localParameter.draggable.value = targetState;
        try {
          GM.setValue(localParameter.draggable.param_name, targetState);
          window.console.log(logPromptPARAM,"draggable change successful");
          if (targetState) {
            try {
              activateDragDecoding();
              Swal.fire({
                icon: 'success',
                title: logPromptDEF,
                text: '앞으로 드래그 시 자동 디코딩을 진행합니다.',
                toast: true,
                position: 'center',
                timer: 1500,
                timerProgressBar: true,
                confirmButtonText: '확인',
              });
            } catch(e) {
              window.console.error(logPromptDEF,"draggable activate fail -", e);
              Swal.fire({
                title: logPromptDEF,
                html: '드래그 시 자동 디코딩 활성화 중 문제가 발생했습니다.<br><i>브라우저 로그를 확인해주세요..</i><br><br><i>새로고침이 필요합니다..</i>',
                icon: 'error',
              });
            }
          } else {
            menuFuncSubPageReload('앞으로 드래그 해도 반응하지 않습니다.');
          }
        } catch(e) {
          localParameter.draggable.value = currentState;
          window.console.error(logPromptPARAM,"draggable change fail -", e);
          Swal.fire({
            title: logPromptDEF,
            html: '파라미터 변경 중 문제 발생.<br><i>브라우저 로그를 확인해주세요..</i>',
            icon: 'error',
          });
        } finally {
          menuStructureUpdate();
        }
      } else {
        window.console.log(logPromptDEF,'draggable change canceled.');
      }
    });
  } else {
    if (window.confirm(logPromptDEF+'\n드래그 시 자동 디코딩을 '+(currentState?'비':'')+'활성화 하시겠습니까?\n\n(앞으로 인코딩된 부분을 드래그'+(currentState?'해도 자동으로 디코딩되지 않습':' 시 Base64로 인코딩된것으로\n판단 되면 자동으로 디코딩을 시도합')+'니다.)\n\n(이 기능은 작동이 불안정할 수 있습니다.)')) {
      const targetState = !currentState;
      window.console.log(logPromptPARAM,'draggable change',currentState.toString(),'to',targetState.toString());
      localParameter.draggable.value = targetState;
      try {
        GM.setValue(localParameter.draggable.param_name, targetState);
        window.console.log(logPromptPARAM,"draggable change successful");
        if (targetState) {
          try {
            activateDragDecoding();
            window.alert(logPromptDEF+'\n앞으로 드래그 시 자동 디코딩을 진행합니다.');
          } catch(e) {
            window.console.error(logPromptDEF,"draggable activate fail -", e);
            window.alert(logPromptDEF+'\n드래그 시 자동 디코딩 활성화 중 문제가 발생했습니다, 브라우저 로그를 확인해주세요..\n새로고침이 필요합니다..');
          }
        } else {
          menuFuncSubPageReload('앞으로 드래그 해도 반응하지 않습니다.');
        }
      } catch(e) {
        localParameter.draggable.value = currentState;
        window.console.error(logPromptPARAM,"draggable change fail -", e);
        window.alert(logPromptDEF+'\n파라미터 변경 중 문제 발생, 브라우저 로그를 확인해주세요..');
      } finally {
        menuStructureUpdate();
      }
    } else {
      window.console.log(logPromptDEF,'draggable change canceled.');
    }
  }
}

function menuFunctionUpdateCheck() {
  menuStructureUpdate();
  const currentState = localParameter.updatechk.value;
  if (modalUIEnabled) {
    Swal.fire({
      title: logPromptDEF,
      html: '<b>업데이트 알림을 '+(currentState?'끄':'켜')+'시겠습니까?</b><br><br><i>(앞으로 업데이트가 있'+(currentState?'어도 알려주지 않습':'으면 자동으로 알려줍')+'니다.)</i>',
      icon: 'question',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      cancelButtonColor: '#d33',
      confirmButtonText: '네',
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) {
        const targetState = !currentState;
        window.console.log(logPromptPARAM,'updatechk change',currentState.toString(),'to',targetState.toString());
        localParameter.updatechk.value = targetState;
        try {
          GM.setValue(localParameter.updatechk.param_name, targetState);
          window.console.log(logPromptPARAM,"updatechk change successful");
          Swal.fire({
            icon: 'success',
            title: logPromptDEF,
            text: '앞으로 업데이트'+(targetState?'가 존재하면':'')+' 알림을 띄'+(targetState?'웁':'우지 않습')+'니다.',
            toast: true,
            position: 'center',
            timer: 1500,
            timerProgressBar: true,
            confirmButtonText: '확인',
          });
        } catch(e) {
          localParameter.updatechk.value = currentState;
          window.console.error(logPromptPARAM,"updatechk change fail -", e);
          Swal.fire({
            title: logPromptDEF,
            html: '파라미터 변경 중 문제 발생.<br><i>브라우저 로그를 확인해주세요..</i>',
            icon: 'error',
          });
        } finally {
          menuStructureUpdate();
        }
      } else {
        window.console.log(logPromptDEF,'updatechk change canceled.');
      }
    });
  } else {
    if (window.confirm(logPromptDEF+'\n업데이트 알림을 '+(currentState?'끄':'켜')+'시겠습니까?\n\n(앞으로 업데이트가 있'+(currentState?'어도 알려주지 않습':'으면 자동으로 알려줍')+'니다.)')) {
      const targetState = !currentState;
      window.console.log(logPromptPARAM,'updatechk change',currentState.toString(),'to',targetState.toString());
      localParameter.updatechk.value = targetState;
      try {
        GM.setValue(localParameter.updatechk.param_name, targetState);
        window.console.log(logPromptPARAM,"updatechk change successful");
        window.alert(logPromptDEF+'\n앞으로 업데이트'+(targetState?'가 존재하면':'')+' 알림을 띄'+(targetState?'웁':'우지 않습')+'니다.');
      } catch(e) {
        localParameter.updatechk.value = currentState;
        window.console.error(logPromptPARAM,"updatechk change fail -", e);
        window.alert(logPromptDEF+'\n파라미터 변경 중 문제 발생, 브라우저 로그를 확인해주세요..');
      } finally {
        menuStructureUpdate();
      }
    } else {
      window.console.log(logPromptDEF,'updatechk change canceled.');
    }
  }
}

function menuFunctionChangeExpandMode() {
  menuStructureUpdate();
  const currentState = localParameter.expandmenu.value;
  if (modalUIEnabled) {
    Swal.fire({
      title: logPromptDEF,
      html: '<b>메뉴에 나타나는 항목을 '+(currentState?'줄일':'늘릴')+'까요?</b><br><br><i>(앞으로 세부설정 메뉴가 '+(currentState?'숨겨':'보여')+'집니다.)</i>',
      icon: 'question',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      cancelButtonColor: '#d33',
      confirmButtonText: '네',
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) {
        const targetState = !currentState;
        window.console.log(logPromptPARAM,'menuexpand change',currentState.toString(),'to',targetState.toString());
        localParameter.expandmenu.value = targetState;
        try {
          GM.setValue(localParameter.expandmenu.param_name, targetState);
          window.console.log(logPromptPARAM,"menuexpand change successful");
          Swal.fire({
            icon: 'success',
            title: logPromptDEF,
            text: '앞으로 세부설정 메뉴가 '+(targetState?'보여':'숨겨')+'집니다.',
            toast: true,
            position: 'center',
            timer: 1500,
            timerProgressBar: true,
            confirmButtonText: '확인',
          });
        } catch(e) {
          localParameter.expandmenu.value = currentState;
          window.console.error(logPromptPARAM,"menuexpand change fail -", e);
          Swal.fire({
            title: logPromptDEF,
            html: '파라미터 변경 중 문제 발생.<br><i>브라우저 로그를 확인해주세요..</i>',
            icon: 'error',
          });
        } finally {
          menuStructureUpdate();
        }
      } else {
        window.console.log(logPromptDEF,'menuexpand change canceled.');
      }
    });
  } else {
    if (window.confirm(logPromptDEF+'\n메뉴에 나타나는 항목을 '+(currentState?'줄일':'늘릴')+'까요?\n\n(앞으로 세부설정 메뉴가 '+(currentState?'숨겨':'보여')+'집니다.)')) {
      const targetState = !currentState;
      window.console.log(logPromptPARAM,'menuexpand change',currentState.toString(),'to',targetState.toString());
      localParameter.expandmenu.value = targetState;
      try {
        GM.setValue(localParameter.expandmenu.param_name, targetState);
        window.console.log(logPromptPARAM,"menuexpand change successful");
        window.alert(logPromptDEF+'\n앞으로 세부설정 메뉴가 '+(targetState?'보여':'숨겨')+'집니다.');
      } catch(e) {
        localParameter.expandmenu.value = currentState;
        window.console.error(logPromptPARAM,"menuexpand change fail -", e);
        window.alert(logPromptDEF+'\n파라미터 변경 중 문제 발생, 브라우저 로그를 확인해주세요..');
      } finally {
        menuStructureUpdate();
      }
    } else {
      window.console.log(logPromptDEF,'menuexpand change canceled.');
    }
  }
}

function menuFunctionRstDefaults() {
  menuStructureUpdate();
  if (modalUIEnabled) {
    Swal.fire({
      title: logPromptDEF,
      html: '<b>정말 스크립트 설정을 기본값으로 초기화하시겠습니까?</b><br><br><i>(초기화 완료 후 자동으로 새로고침됩니다.)</i>',
      icon: 'question',
      showCancelButton: true,
      confirmButtonColor: '#3085d6',
      cancelButtonColor: '#d33',
      confirmButtonText: '네',
      cancelButtonText: '취소',
    }).then((result) => {
      if (result.isConfirmed) {
        window.console.log(logPromptPARAM, 'remove all settings..');
        Swal.fire({
          title: logPromptDEF,
          html: "설정값을 제거중입니다, 잠시만 기다려주세요..",
          didOpen: () => {
            Swal.showLoading();
          },
          showConfirmButton: false,
          allowOutsideClick: false,
          allowEscapeKey: false,
          allowEnterKey: false,
        });
        try {
          for (const i of Object.keys(localParameter)) {
            console.log(logPromptPARAM, 'try to remove -', localParameter[i].param_name);
            GM.deleteValue(localParameter[i].param_name);
          }
          Swal.close();
          window.console.log(logPromptPARAM, 'all parameter removed.');
          Swal.fire({
            title: logPromptDEF,
            html: '<b>설정값이 모두 제거되었습니다.</b><br><br><i>(확인 후 현재 창이 자동으로 새로고침됩니다.)</i>',
            icon: 'info',
            confirmButtonColor: '#3085d6',
            confirmButtonText: '확인',
            didOpen: () => {
              Swal.hideLoading();
            },
          }).then((result) => {
            window.location.reload(true);
          });
        } catch(e) {
          window.console.error(logPromptPARAM,'err - get sc parameter - ', e);
          Swal.close();
          Swal.fire({
            title: logPromptDEF,
            didOpen: () => {
              Swal.hideLoading();
            },
            html: '<b>경고! 파라미터 초기화 도중 문제가 발생했습니다.</b><br><i>브라우저 로그를 참고해주세요..</i>',
            icon: 'error',
          });
        }
      } else {
        window.console.log(logPromptDEF,'settings restore canceled.');
      }
    });
  } else {
    if (window.confirm(logPromptDEF+'\n정말 스크립트 설정을 기본값으로 초기화하시겠습니까?\n\n(초기화 완료 후 자동으로 새로고침됩니다.)')) {
      try {
        window.console.log(logPromptPARAM, 'remove all settings..');
        for (const i of Object.keys(localParameter)) {
          console.log(logPromptPARAM, 'try to remove -', localParameter[i].param_name);
          GM.deleteValue(localParameter[i].param_name);
        }
        window.console.log(logPromptPARAM, 'all parameter removed.');
        window.alert(logPromptDEF+'\n설정값이 모두 제거되었습니다.\n\n(확인 후 현재 창이 자동으로 새로고침됩니다.)');
        window.location.reload(true);
      } catch(e) {
        window.console.error(logPromptPARAM,'err - get sc parameter - ', e);
        window.alert(logPromptDEF+'\n경고! 파라미터 초기화 도중 문제가 발생했습니다. 브라우저 로그를 참고해주세요..');
      }
    } else {
      window.console.log(logPromptDEF,'settings restore canceled.');
    }
  }
}

function menuFunctionNotAvailable() {
  window.console.log(logPromptDEF,'unavailable function clicked');
  if (modalUIEnabled) {
      Swal.fire({
        title: logPromptDEF,
        html: '현재 사용할 수 없는 기능입니다..<br><br><i>(구현되지 않았거나 버그로 인해 일시적으로<br>현재버전에서 비활성화된 기능입니다.)</i>',
        icon: 'error',
        timer: 5000,
        timerProgressBar: true,
      });
  } else {
    window.alert(logPromptDEF+'\n현재 사용할 수 없는 기능입니다..');
  }
}

//main
(async () => {
  'use strict';

  //chk browser env
  if (((window.navigator.language || window.navigator.userLanguage) != 'ko-KR')) {
    window.console.warn('Warning! this script support only korean language..');
  }

  window.console.log(logPromptDEF,'V',GM.info.script.version,'pre processing..');

  //Sweet Alert2 chk
  if (window.Swal != undefined) {
    const styleSA2 = document.createElement('style');
    styleSA2.textContent = '.swal2-container { z-index: 2400; }';
    document.head.appendChild(styleSA2);
    modalUIEnabled = true;
    window.console.log(logPromptDEF,'SA2 loaded');
  }

  //check edit mode
  if (window.location.pathname.match(/\/b\/.*?\/(write|edit)/)) {
    window.console.log(logPromptDEF,'write/edit mode detected, function disabled.');
    try {
      GM.registerMenuCommand("작성/수정 모드에서는 동작하지 않음", ()=>{
        if (modalUIEnabled) {
          Swal.fire({
            title: logPromptDEF,
            html: '작성 또는 수정모드에서는 동작하지 않습니다..',
            icon: 'error',
            timer: 5000,
            timerProgressBar: true,
          });
        } else {
          window.alert(logPromptDEF+'\n작성 또는 수정모드에서는 동작하지 않습니다..');
        }
      }, {title:'작성 또는 수정모드에서는 동작하지 않습니다.'});
    } catch(_) {}
    return;
  }
  /*
  const URLSuffix = window.location.pathname.match(/([/][a-z0-9_-]*[\/]?)$/g);
  if (URLSuffix != null) {
    if (deniedURLSuffix.some(str => str == URLSuffix[0])) {
      window.console.log(logPromptDEF,'write/edit mode detected, function disabled.');
      try {GM.registerMenuCommand("작성/수정 모드에서는 동작하지 않음", ()=>{window.alert(logPromptDEF+'\n작성 또는 수정모드에서는 동작하지 않습니다..');}, {title:'작성 또는 수정모드에서는 동작하지 않습니다.'});} catch(_) {}
      return;
    }
  }*/

  window.console.log(logPromptDEF,'abad enabled');

  //load parameter
  try {
    for (const i of Object.keys(localParameter)) {
      localParameter[i].value = await GM.getValue(localParameter[i].param_name, localParameter[i].def_value);
    }
    window.console.log(logPromptPARAM, 'sc parameter load completed.');
  } catch(e) {
    window.console.error(logPromptPARAM,'err - get sc parameter - ', e);
  }

  //apply parameter and register monkey menu command
  menuStructureUpdate(true);

  //chk update
  await checkForUpdate();

  //drag auto decoding
  if (localParameter.draggable.value) {
    activateDragDecoding();
  }

  window.console.log(logPromptDEF,'script ready');
  //main procedure

  //article
  let article = document.getElementsByClassName("article-content")[0];
  if (article != undefined) {
    for (let i=0; i<localParameter.basedepth.value; i++) {
      article.innerHTML = article.innerHTML.replaceAll(regexEncodedPrefixDef[i], replacerGen(i));
    }
  } else window.console.warn(logPromptDEF,'article not found.');
  const decoded_article = hindex;

  //comment
  let comments = document.getElementsByClassName("list-area");
  if (article != undefined) {
    if (comments.length != 0) {
      for (let i=0; i<localParameter.basedepth.value; i++) {
        comments[0].innerHTML = comments[0].innerHTML.replaceAll(regexEncodedPrefixDef[i], replacerGen(i));
      }
    }
  } else window.console.warn(logPromptDEF,'comments not found.');
  const decoded_comment = hindex - decoded_article;

  //show result on article top
  if (decoded_article+decoded_comment>0) {
    let result = document.createElement("div");
    result.id = createElemID();
    result.class = 'btn';
    result.style.marginTop = '10px';
    result.style.marginBottom = '10px';
    result.style.paddingTop = '7px';

    let result_box = document.createElement("span");
    result_box.style.border = '1px solid rgb(104 179 255)';
    result_box.style.padding = '7px 15px';

    let result_in = '<span style="color: rgb(232 62 140);">';
    if (decoded_article+decoded_comment>0) {
      result_in += '총 '+(decoded_article+decoded_comment)+'개의 링크가 자동 디코딩되었습니다. <span style="font-size: 75%;">( '+((decoded_article>0)?('게시글: '+decoded_article+'개'+((decoded_comment>0)?' / ':'')):'')+((decoded_comment>0)?('댓글: '+decoded_comment+'개'):'')+' )</span>';
    } else {//not use
      result_in += '<span style="font-size: 75%;"><i>이 게시글 또는 댓글에서 Base64로 인코딩 된 링크가 감지되지 않았습니다..</i></span>';
    }
    result_in += '</span>';
    result_box.innerHTML = result_in;
    result.appendChild(result_box);
    result.appendChild(document.createElement("hr"));
    article.prepend(result);
  }
  window.console.log(logPromptDEC,'total',hindex,'link decode task completed. (article:', decoded_article, ', comment:', decoded_comment, ')');

  //add event listner - click, show original encoded link
  if (!localParameter.enclinkhide.value) {
    Object.keys(encodedList).forEach(function(i) {
      document.getElementById(i).addEventListener('click', showEncodedLink); //, { once : true }
    });
  }

})();