Auto-decode Base64/Braille encoded links in Kone.gg, with a unified settings panel, a searchable link summary, drag-to-decode, and lightweight DOM watching over both the post body and comments.
// ==UserScript==
// @name Kone base64 autodecoder Rebuilt
// @name:ko 코네 자동 디코더 (개선판)
// @version 3.12.0
// @author OMNI7 (Original code by 'Laria')
// @match https://kone.gg/*
// @description Auto-decode Base64/Braille encoded links in Kone.gg, with a unified settings panel, a searchable link summary, drag-to-decode, and lightweight DOM watching over both the post body and comments.
// @description:ko 코네 내 Base64/점자로 인코딩된 링크를 자동으로 복호화합니다. 통합 설정 패널, 검색 가능한 링크 요약, 드래그 디코딩, 본문·댓글을 모두 감시하는 가벼운 DOM 감시를 제공합니다.
// @icon https://www.google.com/s2/favicons?sz=64&domain=kone.gg
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @license MIT
// @encoding utf-8
// @run-at document-end
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @grant GM.setClipboard
// @grant GM.openInTab
// @namespace https://greasyfork.org/users/1575179
// ==/UserScript==
/*
Kone base64 autodecoder — rebuilt by OMNI7.
Based on "Arca base64 autodecoder" by 'Laria' (https://greasyfork.org/ko/scripts/482577).
*/
const regexEncodedPrefixDef = [
/(aHR0cDovL|aHR0cHM6Ly|bWVnYS5ue|a2lvLmFj)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
/(YUhSMGNEb3ZM|YUhSMGNITTZMe|YldWbllTVX|YTIsdmJtR)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
/(WVVoU01HTkViM1pN|WVVoU01HTklUVFpNZ|V1d4V1JteF|V1RJeHNXWm)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
/(V1ZWb1UwMUhUa1ZpTTFwT|V1ZWb1UwMUhUa2xVVkZwTl|VmpkNFYxSn|VjFSSmVIT)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
/(VjFaV2IxVXdNVWhVYTFacFRURndU|VjFaV2IxVXdNVWhVYTJ4VlZrWndUb)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
/(VmpGYVYySXhWWGROVldoVllURmFjRlJVUm5kV|VmpGYVYySXhWWGROVldoVllUSjRWbFpyV25kVW)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
/(Vm1wR1lWWXlTWGhXV0dST1ZsZG9WbGxVUm1GalJsSlZVbTVrV|Vm1wR1lWWXlTWGhXV0dST1ZsZG9WbGxVU2pSV2JGcHlWMjVrVl)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
/(Vm0xd1IxbFdXWGxUV0doWFYwZFNUMVpzWkc5V2JHeFZVbTFHYWxKc1NsWlZiVFZyV|Vm0xd1IxbFdXWGxUV0doWFYwZFNUMVpzWkc5V2JHeFZVMnBTVjJKR2NIbFdNalZyVm)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
/(Vm0weGQxSXhiRmRYV0d4VVYwZG9XRll3WkZOVU1WcHpXa2M1VjJKSGVGWlZiVEZIWVd4S2MxTnNXbFppVkZaeV|Vm0weGQxSXhiRmRYV0d4VVYwZG9XRll3WkZOVU1WcHpXa2M1VjJKSGVGWlZNbkJUVmpKS1IyTkliRmROYWxaeVZt)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
/(Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJsbDNXa1pPVlUxV2NIcFhhMk0xVmpKS1NHVkdXbFppVkVaSVdWZDRTMk14VG5OWGJGcHBWa1phZ|Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJsbDNXa1pPVlUxV2NIcFhhMk0xVmpKS1NHVkdXbFpOYmtKVVZtcEtTMUl5VGtsaVJtUk9ZV3hhZVZad)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
/(Vm0wd2QyVkhVWGhUV0docFVtMVNXVll3WkRSV1ZsbDNXa2M1V0ZKc2JETlhhMXBQVmxVeFYyTkljRmhoTWsweFZtcEtTMU5IVmtkWGJGcHBWa1ZhU1ZkV1pEUlRNazE0Vkc1T1dHSkdjSEJXYTFwaF|Vm0wd2QyVkhVWGhUV0docFVtMVNXVll3WkRSV1ZsbDNXa2M1V0ZKc2JETlhhMXBQVmxVeFYyTkljRmhoTWsweFZtcEtTMU5IVmtkWGJGcE9ZbXRLVlZadGNFdFRNVWw1Vkd0c2FWSnRVazlaVjNoaFpWWmFk)([\w\-+\/=]*)(?=[^\+=\w\/]|$)/g,
];
const MAX_DECODE_DEPTH = regexEncodedPrefixDef.length;
const abadInternalDB = {
decodedLink: {}, decodedList: [], hostnameSetRaw: new Set(), hostnameSet: [],
internalDB: { dragDecodingEnable: false, swal2Enable: false },
};
const abadConstDB = {
regInvalid: /[^\w\+\/=]/,
};
abadConstDB.SWAL2Title = `<div class="abad-brand"><span class="abad-brand__icon">🔓</span><span class="abad-brand__name">${('name:ko' in GM.info.script) ? GM.info.script['name:ko'] : GM.info.script.name}</span><span class="abad-brand__version">v${GM.info.script.version}</span></div>`;
function injectStyles() {
if (document.getElementById('abad-styles')) return;
const style = document.createElement('style');
style.id = 'abad-styles';
style.textContent = `
:root {
--abad-radius: 14px; --abad-radius-sm: 10px;
--abad-bg: #ffffff; --abad-bg-soft: #f8fafc; --abad-bg-hover: #eef2f6; --abad-border: #e2e8f0;
--abad-text: #0f172a; --abad-text-muted: #64748b;
--abad-primary: #059669; --abad-primary-soft: #d1fae5; --abad-primary-text: #047857;
--abad-accent: #db2777;
--abad-shadow: 0 10px 28px rgba(15,23,42,.12), 0 2px 6px rgba(15,23,42,.06);
--abad-shadow-sm: 0 2px 10px rgba(15,23,42,.08);
}
html.dark, html.abad-dark {
--abad-bg: #1e293b; --abad-bg-soft: #172033; --abad-bg-hover: #263349; --abad-border: #334155;
--abad-text: #f1f5f9; --abad-text-muted: #94a3b8;
--abad-primary: #34d399; --abad-primary-soft: rgba(52,211,153,.16); --abad-primary-text: #6ee7b7;
--abad-accent: #f472b6;
--abad-shadow: 0 14px 34px rgba(0,0,0,.5), 0 2px 6px rgba(0,0,0,.3);
--abad-shadow-sm: 0 2px 12px rgba(0,0,0,.35);
}
.abad-brand { display:flex; align-items:center; justify-content:center; gap:9px; font-family: inherit; }
.abad-brand__icon { font-size:22px; line-height:1; }
.abad-brand__name { font-size:18px; font-weight:800; color: var(--abad-text); }
.abad-brand__version { font-size:12.5px; font-weight:700; color: var(--abad-primary-text); background: var(--abad-primary-soft); padding:2px 9px; border-radius:999px; }
.abad-card { display:flex; align-items:center; gap:12px; background: var(--abad-bg); border:1px solid var(--abad-border); border-radius: var(--abad-radius); padding:14px 18px; margin:14px 0; box-shadow: var(--abad-shadow-sm); cursor:pointer; transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; font-family: inherit; }
.abad-card:hover { transform: translateY(-1px); box-shadow: var(--abad-shadow); border-color: var(--abad-primary); }
.abad-card__icon { flex:none; width:44px; height:44px; border-radius:50%; background: var(--abad-primary-soft); color: var(--abad-primary-text); display:flex; align-items:center; justify-content:center; font-size:20px; }
.abad-card__body { flex:1; min-width:0; }
.abad-card__title { font-weight:700; font-size:16.5px; color: var(--abad-text); }
.abad-card__title b { color: var(--abad-primary-text); }
.abad-card__meta { font-size:14px; color: var(--abad-text-muted); margin-top:3px; }
.abad-card__chevron { flex:none; color: var(--abad-text-muted); font-size:15px; }
.abad-fab { position:fixed; right:20px; bottom:20px; z-index:9999; display:none; align-items:center; gap:8px; background: linear-gradient(135deg, var(--abad-primary), #0d9488); color:#fff; font-size:15px; font-weight:700; padding:11px 18px; border-radius:999px; box-shadow: var(--abad-shadow); cursor:pointer; user-select:none; transition: transform .15s ease; font-family: inherit; }
.abad-fab:hover { transform: translateY(-2px) scale(1.02); }
.abad-fab__count { background: rgba(255,255,255,.25); border-radius:999px; padding:1px 9px; font-size:14px; }
.abad-generated { display:inline; }
.abad-decoded-text { display:inline; padding:0.05em 0.35em; border-radius:0.35em; background: var(--abad-primary-soft); color: var(--abad-text); word-break:break-word; font: inherit; line-height:inherit; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
.abad-link { color: var(--abad-primary-text); font-weight:700; text-decoration:underline; text-underline-offset:0.15em; }
.abad-link:hover { color: var(--abad-accent); }
.abad-drag-container { display:inline; }
.abad-drag-result { color: var(--abad-primary-text); font-weight:700; cursor:pointer; background: var(--abad-primary-soft); border-radius:0.35em; padding:0 0.2em; font: inherit; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
.abad-drag-result a { cursor:pointer; }
.abad-mobile-btn { position:fixed; bottom:25px; left:25px; z-index:10000; width:54px; height:54px; border-radius:50%; background: linear-gradient(135deg, var(--abad-primary), #0d9488); color:#fff; font-size:24px; display:none; align-items:center; justify-content:center; box-shadow: var(--abad-shadow); cursor:pointer; user-select:none; }
.abad-swal-popup:not(.swal2-toast) { border-radius:18px !important; font-family: inherit !important; font-size:16px !important; box-shadow: var(--abad-shadow) !important; background: var(--abad-bg) !important; color: var(--abad-text) !important; }
.abad-swal-popup.swal2-toast { border-radius:12px !important; font-family: inherit !important; padding:0.7em 1em !important; box-shadow: var(--abad-shadow-sm) !important; background: var(--abad-bg) !important; color: var(--abad-text) !important; }
.abad-swal-popup .swal2-html-container { color: var(--abad-text); }
.abad-swal-popup.swal2-toast .swal2-html-container { font-size: 13.5px; margin: 0 0.5em; }
.abad-btn { font-size:15px; font-weight:600; padding:9px 20px; border-radius:999px; border:none; cursor:pointer; margin:4px; transition: transform .1s ease, opacity .1s ease; }
.abad-btn:hover { transform: translateY(-1px); }
.abad-btn:active { transform: translateY(0); opacity:.85; }
.abad-btn--primary { background: var(--abad-primary); color:#fff; }
.abad-btn--secondary { background: var(--abad-accent); color:#fff; }
.abad-btn--ghost { background: var(--abad-bg-soft); color: var(--abad-text-muted); border:1px solid var(--abad-border); }
.abad-search-input { width:100%; box-sizing:border-box; padding:10px 14px; margin-bottom:10px; border:1px solid var(--abad-border); border-radius:10px; background: var(--abad-bg-soft); color: var(--abad-text); font-size:15px; outline:none; transition: border-color .12s ease; }
.abad-search-input:focus { border-color: var(--abad-primary); }
.abad-list { overflow:auto; width:100%; height:280px; border-radius:12px; background: var(--abad-bg-soft); border:1px solid var(--abad-border); padding:6px; }
.abad-list-row { display:flex; align-items:center; gap:8px; padding:9px 10px; border-radius:8px; }
.abad-list-row:hover { background: var(--abad-bg-hover); }
.abad-list-row__favicon { width:17px; height:17px; border-radius:3px; flex:none; }
.abad-list-row__link { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color: var(--abad-text); text-decoration:none; font-size:14.5px; font-weight:500; }
.abad-list-row__link:hover { color: var(--abad-primary-text); text-decoration:underline; }
.abad-list-row__goto { flex:none; font-size:13px; font-weight:700; color: var(--abad-primary-text); background: var(--abad-primary-soft); padding:4px 10px; border-radius:999px; text-decoration:none; cursor:pointer; }
.abad-list-empty { text-align:center; color: var(--abad-text-muted); padding:40px 0; font-size:15px; }
.abad-list-header { display:flex; align-items:center; justify-content:space-between; font-size:15px; margin-bottom:8px; text-align:left; }
.abad-list-count { font-size:14px; color: var(--abad-text-muted); }
.abad-settings { text-align:left; font-size:15.5px; }
.abad-settings-row { display:flex; align-items:center; justify-content:space-between; padding:11px 4px; border-bottom:1px solid var(--abad-border); gap:10px; }
.abad-settings-row:last-of-type { border-bottom:none; }
.abad-settings-label { display:flex; align-items:center; gap:9px; color: var(--abad-text); font-weight:600; }
.abad-settings-label__icon { font-size:18px; }
.abad-switch { position:relative; width:42px; height:24px; flex:none; display:inline-block; }
.abad-switch input { position:absolute; opacity:0; width:100%; height:100%; margin:0; cursor:pointer; z-index:1; }
.abad-switch__track { position:absolute; inset:0; background: var(--abad-border); border-radius:999px; transition: background .15s ease; pointer-events:none; }
.abad-switch__thumb { position:absolute; top:2px; left:2px; width:20px; height:20px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.3); transition: transform .15s ease; pointer-events:none; }
.abad-switch input:checked + .abad-switch__track { background: var(--abad-primary); }
.abad-switch input:checked ~ .abad-switch__thumb { transform: translateX(18px); }
.abad-depth-block { margin-top:8px; padding-top:12px; text-align:left; }
.abad-depth-value { color: var(--abad-primary-text); font-weight:800; }
.abad-range { width:100%; accent-color: var(--abad-primary); margin-top:8px; }
.abad-hint { margin-top:12px; font-size:13.5px; color: var(--abad-text-muted); background: var(--abad-bg-soft); border-radius:10px; padding:9px 11px; text-align:left; }
.abad-hint b { color: var(--abad-text); }
.abad-code-preview { margin-top:12px; text-align:left; font-size:14.5px; }
.abad-code-preview__box { overflow-y:auto; overflow-wrap:anywhere; margin:6px 0; padding:10px; width:100%; height:150px; background: var(--abad-bg-soft); border:1px solid var(--abad-border); border-radius:10px; box-sizing:border-box; cursor:text; user-select:text; color: var(--abad-text); }
.abad-timer-note { font-size:13px; color: var(--abad-text-muted); }
`;
document.head.appendChild(style);
}
function syncDarkClass() {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.toggle('abad-dark', !!prefersDark && !document.documentElement.classList.contains('light'));
}
function swalFire(options) {
return Swal.fire(Object.assign({
buttonsStyling: false,
customClass: {
popup: 'abad-swal-popup',
confirmButton: 'abad-btn abad-btn--primary',
cancelButton: 'abad-btn abad-btn--ghost',
denyButton: 'abad-btn abad-btn--secondary',
},
}, options));
}
function toastPosition() {
return window.matchMedia('(max-width: 768px)').matches ? 'top' : 'top-end';
}
function showToast(message, icon = 'success', timer = 2200) {
return swalFire({
icon,
html: `<b>${message}</b>`,
toast: true,
position: toastPosition(),
timer,
timerProgressBar: true,
showConfirmButton: false,
});
}
let hindex = 0;
let lastUrl = location.href;
const localParameter = {
'autodecode': { 'param_name': 'autodecode', 'value': true, 'def_value': true },
'basedepth': { 'param_name': 'basedepth', 'value': 3, 'def_value': 3 },
'draggable': { 'param_name': 'draggable', 'value': false, 'def_value': false },
'expandmenu': { 'param_name': 'expandmenu', 'value': true, 'def_value': true },
};
const menuStructure = {
'quickpanel': { 'name': '⚡ 빠른 설정 (전체 보기)', 'desc': '모든 옵션을 한 화면에서 확인하고 변경합니다.', 'id': -1, 'func': openQuickSettingsPanel, 'visible': true },
'autodecode': { 'name': '🗝 자동 디코딩 [상태]', 'desc': '페이지 접속 시 자동으로 디코딩하는 기능을 켜고 끕니다.', 'id': -1, 'func': menuFunctionAutodecode, 'visible': true },
'basedepth': { 'name': '🎛 base64 깊이 조절하기', 'desc': '자동 base64 디코딩 깊이를 조절할 수 있습니다.', 'id': -1, 'func': menuFunctionBasedepth, 'visible': true },
'draggable': { 'name': '🖱 드래그 시 자동 디코딩 [켜기/끄기]', 'desc': '', 'id': -1, 'func': menuFunctionDraggable, 'visible': true },
'resetdefaults': { 'name': '🛠 스크립트 기본값 초기화', 'desc': '', 'id': -1, 'func': menuFunctionRstDefaults, 'visible': true },
'expandmenu': { 'name': '⚙️ 스크립트 메뉴 [축소/확장]', 'desc': '', 'id': -1, 'func': menuFunctionChangeExpandMode, 'visible': true },
};
const addListener = (node, event, handler, capture = false) => node.addEventListener(event, handler, capture);
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
function getLocation(href) {
const match = href.toString().match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/);
return match && { href, hostname: match[3] };
}
function createElemID() {
try {
if (self.crypto && typeof self.crypto.randomUUID === 'function') return 'abad_' + self.crypto.randomUUID();
} catch (_) {}
return 'abad_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
}
function faviconFor(hostname) { return `https://www.google.com/s2/favicons?sz=32&domain=${hostname}`; }
function getPostContainer() {
return document.querySelector('#post-article') || document.querySelector('main #post_content');
}
function getPostRoot() {
const pc = getPostContainer();
return (pc && pc.shadowRoot) ? pc.shadowRoot : pc;
}
function getCommentRoot() {
return document.querySelector('#post-comment');
}
function getActiveSelection() {
const root = getPostRoot();
if (root && typeof root.getSelection === 'function') {
const shadowSel = root.getSelection();
if (shadowSel && shadowSel.toString().trim().length > 0) return shadowSel;
}
return window.getSelection();
}
function toElement(node) {
if (!node) return null;
return node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
}
function neutralizeFlexParent(el) {
const p = el && el.parentElement;
if (!p || p.classList.contains('abad-flex-neutralized')) return;
const d = getComputedStyle(p).display;
if (d === 'flex' || d === 'inline-flex' || d === 'grid' || d === 'inline-grid') {
p.style.setProperty('display', 'block', 'important');
p.classList.add('abad-flex-neutralized');
}
}
function isWithinDecodableArea(node) {
const el = toElement(node);
if (!el || typeof el.closest !== 'function') return false;
if (el.isContentEditable || el.closest('textarea, input')) return false;
if (el.closest('.abad-generated, .abad-drag-container, .abad-drag-result')) return false;
return !!(el.closest('#post-article') || el.closest('#post-comment'));
}
function escapeHtml(str) {
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
function looksLikeRealText(str) {
if (!str || str.length < 2 || str.includes('\uFFFD')) return false;
let printable = 0;
for (const ch of str) {
const code = ch.codePointAt(0);
if (code === 0x09 || code === 0x0A || code === 0x0D || code >= 0x20) printable++;
}
if (printable / str.length <= 0.9) return false;
return /[가-힣ㄱ-ㆎ\s]/.test(str) || /^https?:\/\//i.test(str.trim());
}
const Base64 = {
decode(input) {
const cleaned = input.replace(/[^A-Za-z0-9+/=]/g, '').replace(/=+$/, '');
if (!cleaned) return null;
const padded = cleaned + '='.repeat((4 - cleaned.length % 4) % 4);
try {
const binaryString = window.atob(padded);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch (e) {
return null;
}
}
};
const brailleMap = {
'⠁': 'a', '⠃': 'b', '⠉': 'c', '⠙': 'd', '⠑': 'e', '⠋': 'f', '⠛': 'g', '⠓': 'h', '⠊': 'i', '⠚': 'j',
'⠅': 'k', '⠇': 'l', '⠍': 'm', '⠝': 'n', '⠕': 'o', '⠏': 'p', '⠟': 'q', '⠗': 'r', '⠎': 's', '⠞': 't',
'⠥': 'u', '⠧': 'v', '⠺': 'w', '⠭': 'x', '⠽': 'y', '⠵': 'z'
};
const brailleNumMap = {
'⠁': '1', '⠃': '2', '⠉': '3', '⠙': '4', '⠑': '5', '⠋': '6', '⠛': '7', '⠓': '8', '⠊': '9', '⠚': '0'
};
function decodeBraille(str) {
let result = '';
let isNum = false, isCaps = false, isCapsLock = false;
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (char === '⠸') {
if (str[i + 1] === '⠌') { result += '/'; i++; continue; }
if (str[i + 1] === '⠹') { result += '#'; i++; continue; }
}
if (char === '⠠') {
isNum = false;
if (str[i + 1] === '⠠') { isCapsLock = true; i++; continue; }
else if (str[i + 1] === '⠄') { isCapsLock = false; i++; continue; }
else { isCaps = true; continue; }
}
if (char === '⠼') { isNum = true; isCaps = false; isCapsLock = false; continue; }
if (char === '⠰') { isNum = false; continue; }
if (char === '⠲') { result += '.'; isNum = false; continue; }
if (char === '⠒') { result += ':'; isNum = false; continue; }
if (char === '⠤') { result += '-'; isNum = false; continue; }
if (char === '⠐' && str[i + 1] === '⠶') { result += '='; i++; isNum = false; continue; }
if (char === '⠖') { result += '+'; isNum = false; continue; }
if (char === '⠌') { result += '/'; isNum = false; continue; }
if (char === '⠹') { result += '#'; isNum = false; continue; }
if (char === '⠶') { result += '='; isNum = false; continue; }
if (char === '⠀') { result += ' '; isNum = false; continue; }
if (isNum && brailleNumMap[char]) { result += brailleNumMap[char]; continue; }
if (brailleMap[char]) {
let c = brailleMap[char];
if (isCaps || isCapsLock) { c = c.toUpperCase(); isCaps = false; }
result += c;
isNum = false;
continue;
}
if (char >= '⠀' && char <= '⣿') continue;
result += char;
isNum = false;
}
return result;
}
function scrollToTarget(id_tmp, target) {
const root = getPostRoot();
const targetElem = document.getElementById(id_tmp) || (root && root.getElementById && root.getElementById(id_tmp));
if (!targetElem) return;
if (abadInternalDB.internalDB.swal2Enable) {
Swal.close();
showToast(`${(target === undefined) ? '해당' : target} 위치로 이동했습니다.`, 'success', 3500);
}
targetElem.classList.remove('abad-flash');
void targetElem.offsetWidth;
targetElem.classList.add('abad-flash');
window.scrollTo({ top: window.pageYOffset + targetElem.getBoundingClientRect().top - (window.innerHeight / 2), behavior: 'smooth' });
sleep(2600).then(() => { targetElem.classList.remove('abad-flash'); });
};
function copyToClipboard(target, cont) {
const msgHeader = cont != undefined ? `${cont}이(가) ` : '';
if (target == undefined) { console.warn('Copy target does not exist'); return; }
try {
GM.setClipboard(target);
if (abadInternalDB.internalDB.swal2Enable) {
let timerInterval;
swalFire({
title: abadConstDB.SWAL2Title,
html: `<b>${msgHeader}클립보드로 복사되었습니다.</b><div class=\"abad-code-preview\">또는 아래 코드를 복사:<div class=\"abad-code-preview__box\">${escapeHtml(target)}</div></div>`,
confirmButtonText: '확인', icon: 'success', timer: 3000, timerProgressBar: true,
footer: '<span id="footer" class="abad-timer-note"> </span>',
didOpen: (modal) => {
let autoClose = true;
const handleMouseEnter = () => {
autoClose = false; Swal.stopTimer();
const footer = modal.querySelector('#footer');
if (footer) footer.textContent = '창에서 마우스를 떼면 일정시간 후 자동으로 닫힙니다.';
};
const handleMouseLeave = () => { autoClose = true; Swal.resumeTimer(); };
modal.onmouseenter = handleMouseEnter;
modal.onmouseleave = handleMouseLeave;
if (modal.matches(':hover')) handleMouseEnter();
timerInterval = setInterval(() => {
if (autoClose) {
const timeLeft = Swal.getTimerLeft();
const seconds = isNaN(Math.floor(timeLeft / 1000)) ? '0' : Math.floor(timeLeft / 1000);
const footer = modal.querySelector('#footer');
if (footer) footer.textContent = `약 ${seconds}초 후 창이 자동으로 닫힙니다.`;
}
}, 100);
},
willClose: () => { clearInterval(timerInterval); },
});
} else {
window.alert(msgHeader + '클립보드로 복사되었습니다.');
}
} catch (e) { console.warn('Copy failed:', e); }
}
function showDecodeSummary() {
if (!abadInternalDB.internalDB.swal2Enable) return;
const listWrapperId = createElemID();
const searchInputId = createElemID();
swalFire({
title: abadConstDB.SWAL2Title,
width: 560,
html: `
<div class="abad-list-header">
<b>디코딩된 링크 목록</b>
<span class="abad-list-count">총 ${abadInternalDB.decodedList.length}개</span>
</div>
<input id="${searchInputId}" type="text" class="abad-search-input" placeholder="🔍 호스트명으로 검색 (예: mega.nz)">
<div id="${listWrapperId}" class="abad-list">불러오는중...</div>
`,
showCancelButton: true,
confirmButtonText: '📋 전체 복사',
cancelButtonText: '닫기',
showDenyButton: true,
denyButtonText: '🚀 전체 열기',
didOpen: (modal) => {
Swal.showLoading();
const listEl = modal.querySelector('#' + listWrapperId);
const searchEl = modal.querySelector('#' + searchInputId);
const renderList = (filterText) => {
listEl.innerHTML = '';
const needle = (filterText || '').trim().toLowerCase();
abadInternalDB.decodedList.forEach((uuid) => {
const target = abadInternalDB.decodedLink[uuid];
if (!target) return;
if (needle && !target.hostname.toLowerCase().includes(needle)) return;
const row = document.createElement('div');
row.className = 'abad-list-row';
const favicon = document.createElement('img');
favicon.src = faviconFor(target.hostname);
favicon.className = 'abad-list-row__favicon';
favicon.onerror = () => { favicon.style.display = 'none'; };
const contLink = document.createElement('a');
contLink.href = target.href;
contLink.target = '_blank';
contLink.rel = 'external nofollow noopener noreferrer';
contLink.textContent = `${target.no}번째 링크 · ${target.hostname}`;
contLink.className = 'abad-list-row__link';
const gotoBtn = document.createElement('a');
gotoBtn.textContent = '이동';
gotoBtn.href = 'javascript:void(0);';
gotoBtn.className = 'abad-list-row__goto';
addListener(gotoBtn, 'click', () => scrollToTarget(target.id, `${target.no}번째 링크`));
row.appendChild(favicon);
row.appendChild(contLink);
row.appendChild(gotoBtn);
listEl.appendChild(row);
});
if (!listEl.childElementCount) {
listEl.innerHTML = '<div class="abad-list-empty">일치하는 링크가 없습니다.</div>';
}
};
addListener(searchEl, 'input', () => renderList(searchEl.value));
sleep(80).then(() => { renderList(''); Swal.hideLoading(); });
},
}).then((result) => {
const allLinks = abadInternalDB.decodedList.map(id => abadInternalDB.decodedLink[id]).filter(Boolean);
if (result.isConfirmed) {
copyToClipboard(allLinks.map(l => l.href).join('\n'), `링크 ${allLinks.length}개`);
} else if (result.isDenied) {
if (allLinks.length === 0) return;
const openAll = () => allLinks.forEach(l => { try { GM.openInTab(l.href, { active: false, insert: true }); } catch (e) { window.open(l.href, '_blank'); } });
if (allLinks.length > 5) {
swalFire({
title: abadConstDB.SWAL2Title,
html: `<b>${allLinks.length}개의 링크를 새 탭으로 모두 여시겠습니까?</b>`,
icon: 'warning', showCancelButton: true, confirmButtonText: '모두 열기', cancelButtonText: '취소',
}).then(r => { if (r.isConfirmed) openAll(); });
} else {
openAll();
}
}
});
}
function toHttpsCandidate(text) {
if (text.startsWith('mega.nz') || text.startsWith('kio.ac')) return 'https://' + text;
return text;
}
const BASE64_DETECT_SOURCE = `[A-Za-z0-9+/]{12,}={0,2}`;
const BASE64_EMBED_SOURCE = `[A-Za-z0-9+/]{8,}={0,2}`;
function fullyDecodeEmbedded(text) {
let current = text;
for (let pass = 0; pass < MAX_DECODE_DEPTH + 2; pass++) {
const regex = new RegExp(BASE64_EMBED_SOURCE, 'g');
let match, out = '', lastIndex = 0, changed = false;
while ((match = regex.exec(current))) {
const raw = match[0];
let accepted = null;
const decoded = Base64.decode(raw);
if (decoded !== null) {
const candidate = decodeURI(encodeURI(decoded).replaceAll('%00', '')).trim();
if (looksLikeRealText(candidate)) accepted = candidate;
}
out += current.slice(lastIndex, match.index);
out += accepted !== null ? accepted : raw;
if (accepted !== null) changed = true;
lastIndex = match.index + raw.length;
}
out += current.slice(lastIndex);
current = out;
if (!changed) break;
}
return current;
}
function decodesToRealText(sourceStr) {
const decoded = Base64.decode(sourceStr);
if (decoded === null) return false;
const preview = decodeURI(encodeURI(decoded).replaceAll('%00', '')).trim();
if (looksLikeRealText(preview)) return true;
const fully = fullyDecodeEmbedded(preview);
return fully !== preview && looksLikeRealText(fully);
}
function splitTrailingPunctuation(url) {
let end = url.length;
while (end > 0) {
const ch = url[end - 1];
if ('.,!?;:]'.includes(ch)) { end--; continue; }
if (ch === ')') {
const body = url.slice(0, end - 1);
const opens = (body.match(/\(/g) || []).length;
const closes = (body.match(/\)/g) || []).length;
if (closes >= opens) { end--; continue; }
}
break;
}
return { link: url.slice(0, end), trailing: url.slice(end) };
}
function linkifyDecodedLine(line, genMode) {
const urlRegex = /https?:\/\/[^\s<>"']+/g;
let out = '', lastIndex = 0, match;
while ((match = urlRegex.exec(line))) {
const { link: trimmedLink, trailing: trailingPunct } = splitTrailingPunctuation(match[0]);
const url_t = getLocation(trimmedLink);
out += escapeHtml(line.slice(lastIndex, match.index));
if (url_t) {
if (genMode === 'article') {
hindex++;
const uuid = createElemID();
abadInternalDB.hostnameSetRaw.add(url_t.hostname);
abadInternalDB.decodedLink[uuid] = { id: uuid, no: hindex, type: genMode, hostname: url_t.hostname, href: url_t.href };
abadInternalDB.decodedList.push(uuid);
out += `<a id="${uuid}" class="abad-link" href="${escapeHtml(url_t.href)}" title="${escapeHtml(url_t.href)} (새 창으로 열기)" target="_blank" rel="external nofollow noopener noreferrer">${escapeHtml(trimmedLink)}</a>${escapeHtml(trailingPunct)}`;
} else {
out += `<a class="abad-link" href="${escapeHtml(url_t.href)}" title="${escapeHtml(url_t.href)} (새 창으로 열기)" target="_blank" rel="external nofollow noopener noreferrer">${escapeHtml(trimmedLink)}</a>${escapeHtml(trailingPunct)}`;
}
} else {
out += escapeHtml(match[0]);
}
lastIndex = match.index + match[0].length;
}
out += escapeHtml(line.slice(lastIndex));
return out;
}
function replacerGen(genMode) {
return function (source) {
const isBraille = /[⠀-⣿]/.test(source);
try {
let converted;
if (isBraille) {
converted = decodeBraille(source).trim();
if (!converted) return null;
} else {
converted = Base64.decode(source);
if (converted === null) return null;
}
converted = decodeURI(encodeURI(converted).replaceAll('%00', '')).trim();
if (!converted || converted.includes('\uFFFD')) return null;
const lines = converted.split(/\r?\n/).map(line => line.trim()).filter(line => line !== '');
if (!lines.length) return null;
const resolvedLines = lines.map(line => toHttpsCandidate(fullyDecodeEmbedded(line).trim()));
const rendered = resolvedLines.map(line => linkifyDecodedLine(line, genMode)).join('<br>');
return `<span class="abad-decoded-text">${rendered}</span>`;
} catch (e) {
return null;
}
};
}
function populateWithLinks(container, text) {
const urlPattern = /(https?:\/\/[^\s]+|(?:[a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?)/g;
const parts = text.split(urlPattern);
parts.forEach(part => {
if (!part) return;
if (/^(https?:\/\/|(?:[a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,})/.test(part)) {
const a = document.createElement('a');
a.href = /^https?:\/\//.test(part) ? part : 'https://' + part;
a.target = '_blank';
a.rel = 'noreferrer';
a.style.textDecoration = 'underline';
a.textContent = part;
a.title = '클릭 시 새 창으로 이동';
container.appendChild(a);
} else {
container.appendChild(document.createTextNode(part));
}
});
}
function isFocusedInFormField() {
const active = document.activeElement;
return !!(active && (active.tagName === 'TEXTAREA' || active.tagName === 'INPUT' || active.isContentEditable));
}
function isDecodableSelection(selText) {
if (!selText || selText.length < 4) return false;
const isBraille = /^[⠀-⣿\s]+$/.test(selText);
return isBraille || !selText.match(abadConstDB.regInvalid);
}
const RESTORE_OBSERVER_TTL_MS = 60000;
function attachRestore(decodedNode, restoreFn, observerParent, selText) {
decodedNode.title = '드래그 하여 디코딩 된 결과입니다. (링크가 아닌 텍스트를 클릭하면 원본 복구)';
decodedNode.addEventListener('mousedown', (e) => { if (e.detail > 1) e.preventDefault(); });
decodedNode.addEventListener('click', function (e) {
const t = toElement(e.target);
if (t && t.closest && t.closest('a')) return;
const sel = getActiveSelection();
if (sel && !sel.isCollapsed && sel.toString().trim().length > 0) return;
e.preventDefault();
e.stopPropagation();
restoreFn();
});
if (!observerParent) return;
let ttlTimer = null;
const mo = new MutationObserver(() => {
let restored = false;
observerParent.childNodes.forEach(n => {
if (n !== decodedNode && n.nodeType === Node.TEXT_NODE && n.nodeValue.includes(selText)) restored = true;
});
if (restored) { decodedNode.remove(); mo.disconnect(); if (ttlTimer) clearTimeout(ttlTimer); }
});
mo.observe(observerParent, { childList: true, characterData: true, subtree: true });
ttlTimer = setTimeout(() => { try { mo.disconnect(); } catch (_) {} }, RESTORE_OBSERVER_TTL_MS);
}
function tryDragDecode(event) {
if (isFocusedInFormField()) return;
const selection = getActiveSelection();
if (!selection || selection.toString().trim().length === 0) return;
if (!isWithinDecodableArea(selection.anchorNode)) return;
const selText = selection.toString().trim();
const isBraille = /^[⠀-⣿\s]+$/.test(selText);
if (!isDecodableSelection(selText)) return;
try {
let converted;
if (isBraille) {
converted = decodeBraille(selText).trim();
} else {
const decoded = Base64.decode(selText);
if (decoded === null) return;
converted = decodeURI(encodeURI(decoded).replaceAll('%00', '')).trim();
}
if (!converted || converted === selText) return;
converted = fullyDecodeEmbedded(converted).trim();
if (!converted || converted === selText) return;
if (event) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); }
const makeDecodedNode = () => {
const node = document.createElement('span');
node.className = 'abad-generated abad-drag-result';
populateWithLinks(node, converted);
return node;
};
const anchorNode = selection.anchorNode;
if (anchorNode && anchorNode.nodeType === Node.TEXT_NODE) {
const originalText = anchorNode.textContent;
const startIdx = originalText.indexOf(selText);
if (startIdx !== -1) {
const beforeText = originalText.substring(0, startIdx);
const afterText = originalText.substring(startIdx + selText.length);
const container = document.createElement('span');
container.className = 'abad-drag-container';
if (beforeText) container.appendChild(document.createTextNode(beforeText));
const wrapper = makeDecodedNode();
container.appendChild(wrapper);
if (afterText) container.appendChild(document.createTextNode(afterText));
const parent = anchorNode.parentNode;
anchorNode.replaceWith(container);
neutralizeFlexParent(container);
attachRestore(wrapper, () => container.replaceWith(anchorNode), parent, selText);
selection.removeAllRanges();
return;
}
}
const range = selection.getRangeAt(0);
const parent = range.commonAncestorContainer;
const extractedNodes = range.extractContents();
const resultNode = makeDecodedNode();
range.insertNode(resultNode);
neutralizeFlexParent(resultNode);
const actualParent = parent.nodeType === Node.TEXT_NODE ? parent.parentNode : parent;
attachRestore(resultNode, () => resultNode.replaceWith(extractedNodes), actualParent, selText);
selection.removeAllRanges();
} catch (e) {}
}
function activateDragDecoding() {
if (abadInternalDB.internalDB.dragDecodingEnable) return;
abadInternalDB.internalDB.dragDecodingEnable = true;
document.addEventListener('mouseup', function (e) {
if (e.target && e.target.closest && e.target.closest('#abad-mobile-decode-btn, .abad-generated, .abad-drag-container, .abad-drag-result')) return;
setTimeout(() => { if (abadInternalDB.internalDB.dragDecodingEnable) tryDragDecode(e); }, 10);
}, true);
const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
if (isTouchDevice) {
const mobileBtn = document.createElement('div');
mobileBtn.id = 'abad-mobile-decode-btn';
mobileBtn.className = 'abad-mobile-btn';
mobileBtn.innerHTML = '🔓';
mobileBtn.title = '선택한 텍스트 디코딩';
document.body.appendChild(mobileBtn);
document.addEventListener('selectionchange', () => {
if (!abadInternalDB.internalDB.dragDecodingEnable || isFocusedInFormField()) { mobileBtn.style.display = 'none'; return; }
const selection = getActiveSelection();
if (selection.toString().trim().length > 0 && isWithinDecodableArea(selection.anchorNode)) {
if (isDecodableSelection(selection.toString().trim())) { mobileBtn.style.display = 'flex'; return; }
}
mobileBtn.style.display = 'none';
});
const onMobileBtnClick = (e) => { e.preventDefault(); e.stopPropagation(); tryDragDecode(e); mobileBtn.style.display = 'none'; };
mobileBtn.addEventListener('mousedown', onMobileBtnClick);
mobileBtn.addEventListener('touchstart', onMobileBtnClick, { passive: false });
}
}
function buildDepthSliderHTML(ids, value) {
return `
<div class="abad-depth-block">
🎛 base64 디코딩 깊이: <span class="abad-depth-value" id="${ids.depthLabel}">${value}</span>회
<input type="range" id="${ids.depth}" class="abad-range" min="1" max="${MAX_DECODE_DEPTH}" step="1" value="${value}">
</div>
<div class="abad-hint" id="${ids.warn}" style="display:none;">값을 너무 크게 지정하면 브라우저 성능에 영향을 줄 수 있습니다.</div>`;
}
function wireDepthSlider(modal, ids) {
const rangeEl = modal.querySelector('#' + ids.depth);
const labelEl = modal.querySelector('#' + ids.depthLabel);
const warnEl = modal.querySelector('#' + ids.warn);
const updateWarn = (v) => { warnEl.style.display = v > 7 ? 'block' : 'none'; };
updateWarn(rangeEl.value);
rangeEl.addEventListener('input', (e) => {
labelEl.textContent = e.target.value;
updateWarn(e.target.value);
});
}
function openQuickSettingsPanel() {
if (!abadInternalDB.internalDB.swal2Enable) return;
const ids = {
autodecode: createElemID(), draggable: createElemID(),
expandmenu: createElemID(), depth: createElemID(), depthLabel: createElemID(), warn: createElemID(),
};
const before = {
autodecode: localParameter.autodecode.value,
draggable: localParameter.draggable.value,
expandmenu: localParameter.expandmenu.value,
basedepth: localParameter.basedepth.value,
};
const toggleRow = (id, icon, label, checked) => `
<div class="abad-settings-row">
<span class="abad-settings-label"><span class="abad-settings-label__icon">${icon}</span>${label}</span>
<label class="abad-switch">
<input type="checkbox" id="${id}" ${checked ? 'checked' : ''}>
<span class="abad-switch__track"></span>
<span class="abad-switch__thumb"></span>
</label>
</div>`;
swalFire({
title: abadConstDB.SWAL2Title,
width: 480,
html: `
<div class="abad-settings">
${toggleRow(ids.autodecode, '🗝', '자동 디코딩 사용', before.autodecode)}
${toggleRow(ids.draggable, '🖱', '드래그 시 자동 디코딩', before.draggable)}
${toggleRow(ids.expandmenu, '⚙️', '메뉴 항목 모두 표시', before.expandmenu)}
${buildDepthSliderHTML(ids, before.basedepth)}
<div class="abad-hint">단축키 · <b>Alt+L</b> 디코딩 목록 | <b>Alt+K</b> 이번 세션만 드래그 디코딩 토글</div>
</div>
`,
showCancelButton: true,
confirmButtonText: '저장',
cancelButtonText: '취소',
didOpen: (modal) => wireDepthSlider(modal, ids),
preConfirm: () => {
const modal = Swal.getPopup();
return {
autodecode: modal.querySelector('#' + ids.autodecode).checked,
draggable: modal.querySelector('#' + ids.draggable).checked,
expandmenu: modal.querySelector('#' + ids.expandmenu).checked,
basedepth: parseInt(modal.querySelector('#' + ids.depth).value, 10),
};
},
}).then(async (result) => {
if (!result.isConfirmed) return;
const after = result.value;
localParameter.autodecode.value = after.autodecode;
localParameter.draggable.value = after.draggable;
localParameter.expandmenu.value = after.expandmenu;
localParameter.basedepth.value = after.basedepth;
await Promise.all([
GM.setValue(localParameter.autodecode.param_name, after.autodecode),
GM.setValue(localParameter.draggable.param_name, after.draggable),
GM.setValue(localParameter.expandmenu.param_name, after.expandmenu),
GM.setValue(localParameter.basedepth.param_name, after.basedepth),
]);
menuStructureUpdate();
const needsReload =
(before.autodecode !== after.autodecode) ||
(before.basedepth !== after.basedepth && before.autodecode);
if (needsReload) {
menuFuncSubPageReload('설정이 변경되었습니다.');
return;
}
if (after.draggable && !before.draggable) activateDragDecoding();
if (after.autodecode && !before.autodecode) executeDecoder();
showToast('설정이 저장되었습니다.', 'success', 1800);
});
}
async function menuStructureUpdate(firstRun = false) {
localParameter.basedepth.value = Math.min(localParameter.basedepth.value, MAX_DECODE_DEPTH);
menuStructure.autodecode.name = '🗝 자동 디코딩 ' + (localParameter.autodecode.value ? '끄기' : '켜기');
menuStructure.basedepth.name = '🎛 base64 깊이 조절하기 - 현재 값 : ' + localParameter.basedepth.value + '회';
menuStructure.draggable.name = '🖱 드래그 시 자동 디코딩 ' + (localParameter.draggable.value ? '끄기' : '켜기');
menuStructure.expandmenu.name = '⚙️ 스크립트 메뉴 ' + (localParameter.expandmenu.value ? '축소' : '확장');
menuStructure.basedepth.visible = localParameter.autodecode.value;
if (!firstRun) {
for (const i of Object.keys(menuStructure)) {
try { await GM.unregisterMenuCommand(menuStructure[i].id); } catch (_) {}
}
}
try {
if (localParameter.expandmenu.value) {
for (const i of Object.keys(menuStructure)) {
if (menuStructure[i].visible) {
menuStructure[i].id = await GM.registerMenuCommand(menuStructure[i].name, menuStructure[i].func, { title: menuStructure[i].desc });
}
}
} else {
menuStructure.quickpanel.id = await GM.registerMenuCommand(menuStructure.quickpanel.name, menuStructure.quickpanel.func, { title: menuStructure.quickpanel.desc });
menuStructure.expandmenu.id = await GM.registerMenuCommand(menuStructure.expandmenu.name, menuStructure.expandmenu.func, { title: menuStructure.expandmenu.desc });
}
} catch (e) {}
}
function menuFuncSubPageReload(showmsg) {
if (!abadInternalDB.internalDB.swal2Enable) return;
swalFire({
title: abadConstDB.SWAL2Title,
html: `<b>${showmsg}</b><div class=\"abad-hint\">반영을 위해 사이트 새로고침이 필요합니다. 새로고침할까요?</div>`,
icon: 'info', showCancelButton: true,
confirmButtonText: '새로고침', cancelButtonText: '취소',
}).then((result) => { if (result.isConfirmed) window.location.reload(); });
}
function menuFunctionAutodecode() {
menuStructureUpdate();
const currentState = localParameter.autodecode.value;
if (!abadInternalDB.internalDB.swal2Enable) return;
swalFire({
title: abadConstDB.SWAL2Title,
html: `<b>자동 디코딩 기능을 ${(currentState ? '끄시' : '켜시')}겠습니까?</b>`,
icon: 'question', showCancelButton: true,
confirmButtonText: (currentState ? '끄기' : '켜기'), cancelButtonText: '취소',
}).then((result) => {
if (!result.isConfirmed) return;
const targetState = !currentState;
localParameter.autodecode.value = targetState;
GM.setValue(localParameter.autodecode.param_name, targetState).then(() => {
menuStructureUpdate();
if (targetState) {
executeDecoder();
showToast('자동 디코딩 기능이 활성화되었습니다.');
} else {
menuFuncSubPageReload('자동 디코딩 기능이 해제되었습니다. 원본 상태로 되돌리기 위해 페이지를 새로고침합니다.');
}
});
});
}
function menuFunctionBasedepth() {
menuStructureUpdate();
const previousValue = localParameter.basedepth.value;
if (!abadInternalDB.internalDB.swal2Enable) return;
const ids = { depth: createElemID(), depthLabel: createElemID(), warn: createElemID() };
swalFire({
title: abadConstDB.SWAL2Title,
html: `
<div class="abad-settings">
<b>Base64 자동 디코딩 중첩 횟수를 얼마로 지정할까요?</b>
<div class="abad-hint">인코딩을 인코딩한 것을 여러 번 반복한 것을 자동으로 풀어냅니다. (지정 가능한 범위: 1~${MAX_DECODE_DEPTH}, 기본값 3)</div>
${buildDepthSliderHTML(ids, previousValue)}
</div>
`,
showCancelButton: true,
confirmButtonText: '변경',
cancelButtonText: '취소',
didOpen: (modal) => wireDepthSlider(modal, ids),
preConfirm: () => parseInt(Swal.getPopup().querySelector('#' + ids.depth).value, 10),
}).then((result) => {
if (!result.isConfirmed) return;
const targetValue = result.value;
if (targetValue === previousValue) {
showToast('기존 값과 동일합니다.', 'info', 1600);
return;
}
localParameter.basedepth.value = targetValue;
try {
GM.setValue(localParameter.basedepth.param_name, targetValue);
menuFuncSubPageReload(`자동 디코딩 중첩 횟수가 ${previousValue}에서 ${targetValue}(으)로 변경이 완료되었습니다.`);
} catch (e) { localParameter.basedepth.value = previousValue; }
finally { menuStructureUpdate(); }
});
}
function menuFunctionDraggable() {
menuStructureUpdate();
const currentState = localParameter.draggable.value;
if (!abadInternalDB.internalDB.swal2Enable) return;
const alreadyActiveThisSession = abadInternalDB.internalDB.dragDecodingEnable;
swalFire({
title: abadConstDB.SWAL2Title,
html: `<b>드래그 시 자동 디코딩을 ${(currentState ? '비' : '')}활성화 하시겠습니까?</b><div class="abad-hint">앞으로 인코딩된 부분을 드래그${(currentState ? '해도 자동으로 디코딩되지 않습' : ' 시 Base64로 인코딩된 것으로 판단되면 자동으로 디코딩을 시도합')}니다.</div>`,
icon: 'question', showDenyButton: true, showCancelButton: !currentState && !alreadyActiveThisSession,
confirmButtonText: (currentState ? '비활성화' : '활성화'),
denyButtonText: '취소', cancelButtonText: alreadyActiveThisSession ? undefined : '이번에만 활성화',
}).then((result) => {
if (result.isConfirmed) {
const targetState = !currentState;
localParameter.draggable.value = targetState;
try {
GM.setValue(localParameter.draggable.param_name, targetState);
if (targetState) {
activateDragDecoding();
showToast('앞으로 드래그 시 자동 디코딩을 진행합니다.');
} else {
menuFuncSubPageReload('앞으로 드래그 해도 반응하지 않습니다.');
}
} catch (e) { localParameter.draggable.value = currentState; }
finally { menuStructureUpdate(); }
} else if (result.isDismissed && result.dismiss === Swal.DismissReason.cancel && !alreadyActiveThisSession) {
try {
activateDragDecoding();
showToast('드래그 시 자동 디코딩이 활성화되었습니다. <span class="abad-timer-note">(새로고침 시 자동으로 비활성화)</span>');
} catch (e) {}
}
});
}
function menuFunctionChangeExpandMode() {
menuStructureUpdate();
const currentState = localParameter.expandmenu.value;
if (!abadInternalDB.internalDB.swal2Enable) return;
swalFire({
title: abadConstDB.SWAL2Title,
html: `<b>메뉴에 나타나는 항목을 ${(currentState ? '줄일' : '늘릴')}까요?</b><div class="abad-hint">앞으로 세부설정 메뉴가 ${(currentState ? '숨겨' : '보여')}집니다.</div>`,
icon: 'question', showCancelButton: true,
confirmButtonText: (currentState ? '줄이기' : '표시하기'), cancelButtonText: '취소',
}).then((result) => {
if (!result.isConfirmed) return;
const targetState = !currentState;
localParameter.expandmenu.value = targetState;
try {
GM.setValue(localParameter.expandmenu.param_name, targetState);
showToast(`앞으로 세부설정 메뉴가 ${(targetState ? '보여' : '숨겨')}집니다.`);
} catch (e) { localParameter.expandmenu.value = currentState; }
finally { menuStructureUpdate(); }
});
}
function menuFunctionRstDefaults() {
menuStructureUpdate();
if (!abadInternalDB.internalDB.swal2Enable) return;
swalFire({
title: abadConstDB.SWAL2Title,
html: '<b>정말 스크립트 설정을 기본값으로 초기화하시겠습니까?</b><div class="abad-hint">초기화 완료 후 자동으로 새로고침됩니다.</div>',
icon: 'warning', showCancelButton: true,
focusCancel: true, confirmButtonText: '초기화 진행', cancelButtonText: '취소',
showLoaderOnConfirm: true, timer: 10000, timerProgressBar: true,
didOpen: (modal) => { modal.onmouseenter = Swal.stopTimer; modal.onmouseleave = Swal.resumeTimer; },
}).then((result) => {
if (!result.isConfirmed) return;
swalFire({
title: abadConstDB.SWAL2Title,
html: '<b>설정값을 제거중입니다, 잠시만 기다려주세요..</b>',
footer: '<span class="abad-timer-note">1분 이내로 이 창이 사라지지 않으면 수동으로 새로고침해주세요.</span>',
didOpen: () => { Swal.showLoading(); },
showConfirmButton: false, allowOutsideClick: false, allowEscapeKey: false, allowEnterKey: false,
});
try {
Object.keys(menuStructure).forEach((i) => { try { GM.unregisterMenuCommand(menuStructure[i].id); } catch (_) {} });
for (const i of Object.keys(localParameter)) GM.deleteValue(localParameter[i].param_name);
sleep(250).then(() => {
swalFire({
title: abadConstDB.SWAL2Title,
html: '<b>설정값이 모두 제거되었습니다.</b><div class="abad-hint">확인 후 현재 창이 자동으로 새로고침됩니다.</div>',
footer: '<span class="abad-timer-note">비정상적으로 동작 시 스크립트를 재설치해주세요.</span>',
icon: 'success', confirmButtonText: '확인',
didOpen: () => { Swal.hideLoading(); },
}).then(() => { window.location.reload(); });
});
} catch (e) { Swal.close(); }
});
}
function wrapMatch(node, start, length, html, genMode) {
const afterStart = node.splitText(start);
const afterEnd = afterStart.splitText(length);
const wrapper = document.createElement('span');
wrapper.className = 'abad-generated';
wrapper.innerHTML = html;
afterStart.replaceWith(wrapper);
neutralizeFlexParent(wrapper);
processTextNode(afterEnd, genMode);
return true;
}
function processTextNode(node, genMode) {
genMode = genMode || 'article';
const text = node.textContent;
if (!text || !text.trim()) return false;
const brailleRegex = /[⠀-⣿]{4,}/g;
const bMatch = brailleRegex.exec(text);
if (bMatch) {
const sourceStr = bMatch[0];
const html = replacerGen(genMode)(sourceStr);
if (html !== null) return wrapMatch(node, bMatch.index, sourceStr.length, html, genMode);
}
for (let i = 0; i < localParameter.basedepth.value; i++) {
const regex = new RegExp(regexEncodedPrefixDef[i].source, regexEncodedPrefixDef[i].flags);
const match = regex.exec(text);
if (!match) continue;
let start = match.index;
while (start > 0 && /[A-Za-z0-9+/=]/.test(text[start - 1])) start--;
const sourceStr = text.slice(start, match.index + match[0].length);
const html = replacerGen(genMode)(sourceStr);
if (html === null) continue;
return wrapMatch(node, start, sourceStr.length, html, genMode);
}
const genericRegex = new RegExp(BASE64_DETECT_SOURCE, 'g');
let gMatch;
while ((gMatch = genericRegex.exec(text))) {
const sourceStr = gMatch[0];
if (!decodesToRealText(sourceStr)) continue;
const html = replacerGen(genMode)(sourceStr);
if (html === null) continue;
return wrapMatch(node, gMatch.index, sourceStr.length, html, genMode);
}
return false;
}
function hasSummaryBox() {
if (document.getElementById('abad_summary_box')) return true;
const root = getPostRoot();
return !!(root && root.getElementById && root.getElementById('abad_summary_box'));
}
let _floatingBadge = null;
let _summaryIntersectionObserver = null;
function ensureFloatingBadge(summaryBoxEl) {
if (!_floatingBadge) {
_floatingBadge = document.createElement('div');
_floatingBadge.id = 'abad_floating_badge';
_floatingBadge.className = 'abad-fab';
_floatingBadge.addEventListener('click', showDecodeSummary);
document.body.appendChild(_floatingBadge);
}
_floatingBadge.innerHTML = `🔓 디코딩된 링크 <span class=\"abad-fab__count\">${abadInternalDB.decodedList.length}</span>`;
if (_summaryIntersectionObserver) _summaryIntersectionObserver.disconnect();
_summaryIntersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => { _floatingBadge.style.display = entry.isIntersecting ? 'none' : 'flex'; });
}, { threshold: 0 });
_summaryIntersectionObserver.observe(summaryBoxEl);
}
function executeDecoder() {
if (!localParameter.autodecode.value) return;
const rootDefs = [
{ root: getPostRoot(), mode: 'article' },
{ root: getCommentRoot(), mode: 'comment' },
].filter(rd => rd.root);
if (!rootDefs.length) return;
let processedAny = false;
const textTagNames = 'p, span, div, a, li, h1, h2, h3, h4, h5, h6, em, strong, td, code, pre, blockquote';
rootDefs.forEach(({ root, mode }) => {
const elements = Array.from(root.querySelectorAll(textTagNames));
elements.push(root);
elements.forEach(el => {
if (typeof el.closest === 'function' && el.closest('.abad-generated')) return;
const textNodes = Array.from(el.childNodes).filter(n => n.nodeType === Node.TEXT_NODE);
textNodes.forEach(node => { if (processTextNode(node, mode)) processedAny = true; });
});
});
if (processedAny && hindex > 0 && !hasSummaryBox()) {
abadInternalDB.hostnameSet = Array.from(abadInternalDB.hostnameSetRaw).sort();
const result = document.createElement('div');
result.id = 'abad_summary_box';
const result_box = document.createElement('div');
result_box.className = 'abad-card';
result_box.title = '클릭 시 디코딩된 링크를 한번에 볼 수 있습니다.';
result_box.innerHTML =
'<div class="abad-card__icon">🔓</div>' +
'<div class="abad-card__body">' +
`<div class="abad-card__title">총 <b>${hindex}개</b>의 링크가 자동 디코딩되었습니다</div>` +
`<div class="abad-card__meta">감지된 사이트 ${abadInternalDB.hostnameSet.length}개 · 클릭하여 전체 목록 보기 · 단축키 Alt+L</div>` +
'</div>' +
'<div class="abad-card__chevron">›</div>';
result_box.addEventListener('click', showDecodeSummary);
result.appendChild(result_box);
const insertTarget = getPostContainer();
const contentWrapper = insertTarget ? insertTarget.querySelector(':scope > div.relative.min-h-60') : null;
if (contentWrapper && contentWrapper.parentNode) {
contentWrapper.parentNode.insertBefore(result, contentWrapper);
} else if (insertTarget && insertTarget.shadowRoot) {
insertTarget.shadowRoot.insertBefore(result, insertTarget.shadowRoot.firstChild);
} else if (insertTarget && insertTarget.parentNode) {
insertTarget.parentNode.insertBefore(result, insertTarget);
} else {
const fallbackTarget = document.querySelector('main');
if (fallbackTarget && fallbackTarget.parentNode) fallbackTarget.parentNode.insertBefore(result, fallbackTarget);
}
ensureFloatingBadge(result_box);
}
}
function debounce(func, delay) {
let timer;
return function (...args) { clearTimeout(timer); timer = setTimeout(() => func(...args), delay); };
}
let _contentObservers = [];
function attachContentObserver(debouncedExecute) {
_contentObservers.forEach(function (o) { try { o.disconnect(); } catch (_) {} });
_contentObservers = [];
var targets = [getPostRoot(), getCommentRoot()].filter(Boolean);
var observeTargets = targets.length ? targets : [document.body];
observeTargets.forEach(function (t) {
var mo = new MutationObserver(debouncedExecute);
mo.observe(t, { childList: true, subtree: true });
_contentObservers.push(mo);
});
return getPostRoot() || getCommentRoot() || document.body;
}
(async () => {
injectStyles();
syncDarkClass();
try {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncDarkClass);
} catch (_) {}
if (window.Swal) {
const styleSA2 = document.createElement('style');
styleSA2.textContent = '.swal2-container { z-index: 2400; }';
document.head.appendChild(styleSA2);
abadInternalDB.internalDB.swal2Enable = true;
}
for (const i of Object.keys(localParameter)) {
localParameter[i].value = await GM.getValue(localParameter[i].param_name, localParameter[i].def_value);
}
menuStructureUpdate(true);
if (localParameter.draggable.value) activateDragDecoding();
let observedTarget = null;
const debouncedExecute = debounce(() => {
if (lastUrl !== location.href) {
lastUrl = location.href;
hindex = 0;
abadInternalDB.decodedLink = {};
abadInternalDB.decodedList = [];
abadInternalDB.hostnameSetRaw.clear();
const root = getPostRoot();
const oldBox = document.getElementById('abad_summary_box') || (root && root.getElementById && root.getElementById('abad_summary_box'));
if (oldBox) oldBox.remove();
if (_floatingBadge) _floatingBadge.style.display = 'none';
}
executeDecoder();
const currentSig = (getPostRoot() ? '1' : '0') + '|' + (getCommentRoot() ? '1' : '0');
if (currentSig !== observedTarget) { observedTarget = currentSig; attachContentObserver(debouncedExecute); }
}, 400);
attachContentObserver(debouncedExecute);
observedTarget = (getPostRoot() ? '1' : '0') + '|' + (getCommentRoot() ? '1' : '0');
new MutationObserver(debouncedExecute).observe(document.body, { childList: true, subtree: false });
debouncedExecute();
document.addEventListener('keydown', (e) => {
if (!e.altKey) return;
if (e.key === 'l' || e.key === 'L') { e.preventDefault(); showDecodeSummary(); }
if (e.key === 'k' || e.key === 'K') {
e.preventDefault();
if (!abadInternalDB.internalDB.dragDecodingEnable) {
activateDragDecoding();
showToast('드래그 시 자동 디코딩이 이번 세션에 한해 활성화되었습니다.', 'success', 1800);
} else {
showToast('이미 활성화되어 있습니다. (완전히 끄려면 새로고침이 필요합니다)', 'info', 1800);
}
}
});
})();