장점은 내가 갖고있는지 여부랑, 파일명을 같이 저장해서 db에 두면 뭐라고 저장해놨는지 보기 편하다는 점이랑
AI작품같은 경우 들어가도 VPN안키면 설명 이미지가 안보이는데 얘는 그런거 무시하고 바로 볼 수 있어서 편함
이미지 없는 복구글 뭐였는지 기억이 안나서 만들었는데 생각보다 반응도 빠릿빠릿하고 편해서 공유해봄
---
스크립트 등록 방법 :
Tempermonkey - 새 스크립트 만들기
아래 펼쳐서 싹다 붙여넣기
// ==UserScript==
// @name RJ Search Helper
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Search for selected text in a local database and show formatted results
// @author FLAG
// @match *://kone.gg/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// ==/UserScript==
(function () {
'use strict';
// CSS for the popup and settings
const styles = `
#ark-result-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6);
z-index: 9999;
display: none;
justify-content: center;
align-items: center;
}
#ark-search-popup {
position: relative;
background: #222;
color: #fff;
padding: 20px;
padding-top: 40px;
border-radius: 10px;
box-shadow: 0 6px 25px rgba(0,0,0,0.5);
font-family: 'Inter', sans-serif;
font-size: 16px;
width: 640px;
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
border: 1px solid #444;
}
#ark-search-popup .result-item { margin-bottom: 8px; line-height: 1.5; border-bottom: 1px solid #333; padding-bottom: 6px; }
#ark-search-popup .close-btn { position: absolute; top: 5px; right: 12px; cursor: pointer; color: #888; font-size: 24px; z-index: 10001; }
#ark-search-popup .close-btn:hover { color: #fff; }
#ark-image-container {
display: flex;
overflow-x: auto;
gap: 12px;
margin-top: 15px;
padding-bottom: 8px;
}
#ark-image-container img {
height: 384px;
border-radius: 6px;
border: 1px solid #444;
flex-shrink: 0;
cursor: pointer;
}
#ark-image-container::-webkit-scrollbar { height: 8px; }
#ark-image-container::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; }
#ark-settings-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.8);
z-index: 10001;
display: none;
justify-content: center;
align-items: center;
}
#ark-settings-modal {
background: #1e1e1e;
padding: 20px;
border-radius: 10px;
width: 80%;
max-width: 600px;
color: #eee;
}
#ark-db-input {
width: 100%;
height: 300px;
background: #111;
color: #0f0;
border: 1px solid #333;
padding: 10px;
font-family: monospace;
margin: 10px 0;
border-radius: 5px;
}
.ark-btn {
background: #00d1ff;
color: #000;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
}
.ark-btn:hover { background: #00b8e6; }
#ark-toast {
position: fixed;
bottom: 50px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 209, 255, 0.9);
color: #000;
padding: 10px 20px;
border-radius: 20px;
font-weight: bold;
font-size: 14px;
z-index: 11000;
display: none;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
pointer-events: none;
}
.ark-sync-section {
margin-bottom: 20px;
background: #222;
padding: 15px;
border-radius: 8px;
border: 1px solid #333;
}
.ark-sync-section label {
display: flex;
align-items: center;
cursor: pointer;
font-weight: bold;
color: #00d1ff;
}
#ark-sync-controls {
margin-top: 12px;
display: flex;
align-items: center;
gap: 12px;
padding-left: 24px;
}
#ark-filename {
font-size: 13px;
color: #888;
font-style: italic;
}
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
// Initial Elements
const resultOverlay = document.createElement('div');
resultOverlay.id = 'ark-result-overlay';
document.body.appendChild(resultOverlay);
const popup = document.createElement('div');
popup.id = 'ark-search-popup';
resultOverlay.appendChild(popup);
const toast = document.createElement('div');
toast.id = 'ark-toast';
document.body.appendChild(toast);
function showToast(message) {
toast.innerText = message;
toast.style.display = 'block';
setTimeout(() => {
toast.style.display = 'none';
}, 2000);
}
const overlay = document.createElement('div');
overlay.id = 'ark-settings-overlay';
overlay.innerHTML = `
<div id="ark-settings-modal">
<h3 style="margin-top: 0; color: #00d1ff;">⚙️ 데이터 베이스 설정</h3>
<div class="ark-sync-section">
<label>
<input type="checkbox" id="ark-sync-toggle" style="width: 18px; height: 18px; margin-right: 10px;">
로컬 파일과 동기화 (Live Sync)
</label>
<div id="ark-sync-controls">
<button class="ark-btn" id="ark-select-file" style="font-size: 12px; padding: 5px 12px;">파일 선택</button>
<span id="ark-filename">선택된 파일 없음</span>
</div>
</div>
<p style="margin-bottom: 5px;">가상 데이터베이스 (로컬 동기화 비활성화 시 사용):</p>
<textarea id="ark-db-input" placeholder="텍스트를 여기에 붙여넣어주세요..."></textarea>
<div style="text-align: right; margin-top: 10px;">
<button class="ark-btn" id="ark-save-db">저장</button>
<button class="ark-btn" id="ark-close-settings" style="background: #444; color: #fff; margin-left: 10px;">닫기</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// IndexedDB for FileHandle persistence
async function getDBHandle() {
return new Promise((resolve) => {
const request = indexedDB.open("ArkDB", 1);
request.onupgradeneeded = e => e.target.result.createObjectStore("handles");
request.onsuccess = e => {
const db = e.target.result;
const tx = db.transaction("handles", "readonly");
const store = tx.objectStore("handles");
const getReq = store.get("local_file");
getReq.onsuccess = () => resolve(getReq.result);
getReq.onerror = () => resolve(null);
};
request.onerror = () => resolve(null);
});
}
async function setDBHandle(handle) {
return new Promise((resolve) => {
const request = indexedDB.open("ArkDB", 1);
request.onsuccess = e => {
const db = e.target.result;
const tx = db.transaction("handles", "readwrite");
const store = tx.objectStore("handles");
store.put(handle, "local_file");
tx.oncomplete = () => resolve(true);
};
});
}
// Database access
async function getDatabase() {
const syncEnabled = GM_getValue('ark_sync_enabled', false);
if (syncEnabled) {
const handle = await getDBHandle();
if (handle) {
try {
// Check for permission (shows prompt if needed)
if (await handle.queryPermission() !== 'granted') {
if (await handle.requestPermission() !== 'granted') {
throw new Error('Permission denied');
}
}
const file = await handle.getFile();
const text = await file.text();
return text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
} catch (err) {
console.error('Local file read failed:', err);
showToast('로컬 파일을 읽을 수 없습니다. 설정을 확인해주세요.');
}
}
}
const raw = GM_getValue('ark_db', '');
return raw.split('\n').map(line => line.trim()).filter(line => line.length > 0);
}
function saveDatabase(text) {
GM_setValue('ark_db', text);
showToast('저장되었습니다.');
overlay.style.display = 'none';
}
// Search logic
async function search(query) {
if (!query) return { results: [], rjCode: null };
const match = query.match(/(RJ|ST)\d+/i);
const searchToken = match ? match[0].toUpperCase() : query.trim();
if (!searchToken) return { results: [], rjCode: null };
const db = await getDatabase();
const results = db.filter(line => line.toUpperCase().includes(searchToken));
return { results, rjCode: searchToken.startsWith('RJ') ? searchToken : null };
}
function getDLSiteImages(rjCode) {
if (!rjCode) return [];
const numStr = rjCode.substring(2);
const num = parseInt(numStr);
const folderNum = (Math.floor(num / 1000) + 1) * 1000;
const folder = "RJ" + folderNum.toString().padStart(numStr.length, '0');
const baseUrl = `https://img.dlsite.jp/modpub/images2/work/doujin/${folder}/`;
// 5 images as requested (Main + 4 samples)
const suffixes = ["_img_main", "_img_smp1", "_img_smp2", "_img_smp3", "_img_smp4"];
return suffixes.map(s => `${baseUrl}${rjCode.toUpperCase()}${s}.webp`);
}
// Result formatting
function formatResult(line) {
let prefix = "";
const trimmedLine = line.trim();
const hasBrackets = /\[.*\]$/.test(trimmedLine);
const hasMibun = trimmedLine.includes("미번");
if (hasBrackets && hasMibun) {
prefix = "★@ ";
} else if (hasBrackets) {
prefix = "★ ";
} else if (hasMibun) {
prefix = "@ ";
}
return prefix + line;
}
// UI Logic
function showPopup(data) {
const { results, rjCode } = data;
popup.innerHTML = `
<span class="close-btn">×</span>
<h3 style="margin-top: 0;">Search Results</h3>
<div id="ark-popup-content"></div>
`;
const contentArea = popup.querySelector('#ark-popup-content');
if (results.length === 0) {
contentArea.innerHTML = `<div style="color: #ff4d4d; font-weight: bold;">현재 저장소에 없습니다!</div>`;
} else {
contentArea.innerHTML = `<div style="color: #00d1ff; font-weight: bold; margin-bottom: 10px;">파일을 발견했습니다!</div>`;
results.forEach(res => {
const div = document.createElement('div');
div.className = 'result-item';
div.innerText = formatResult(res);
contentArea.appendChild(div);
});
}
// Add Images if RJ code exists
if (rjCode) {
const imgContainer = document.createElement('div');
imgContainer.id = 'ark-image-container';
// Add horizontal scroll on mouse wheel
imgContainer.addEventListener('wheel', (e) => {
if (e.deltaY !== 0) {
e.preventDefault();
imgContainer.scrollLeft += e.deltaY;
}
});
const images = getDLSiteImages(rjCode);
images.forEach(url => {
const img = document.createElement('img');
img.src = url;
img.onerror = () => img.remove();
img.onclick = () => window.open(url, '_blank');
imgContainer.appendChild(img);
});
contentArea.appendChild(imgContainer);
}
resultOverlay.style.display = 'flex';
popup.querySelector('.close-btn').addEventListener('click', () => {
resultOverlay.style.display = 'none';
});
}
// Events
window.addEventListener('keydown', (e) => {
const selection = window.getSelection().toString().trim();
if (e.shiftKey && (e.key === 'Z' || e.key === 'z')) {
if (selection) {
search(selection).then(data => {
showPopup(data);
});
}
}
if (e.shiftKey && (e.key === 'C' || e.key === 'c')) {
if (selection) {
try {
const decoded = atob(selection);
GM_setClipboard(decoded);
showToast('복호화 성공!');
console.log('Decoded and copied to clipboard:', decoded);
} catch (err) {
console.error('Base64 decode failed:', err);
}
}
}
});
// Close popup on click outside
window.addEventListener('mousedown', (e) => {
if (e.target === resultOverlay) {
resultOverlay.style.display = 'none';
}
if (!overlay.contains(e.target) && e.target !== overlay) {
// This handles the settings overlay click outside if needed,
// but the logic was a bit mixed. Let's focus on search overlay.
}
});
// Close settings overlay on click outside
overlay.addEventListener('mousedown', (e) => {
if (e.target === overlay) {
overlay.style.display = 'none';
}
});
// Settings Menu
GM_registerMenuCommand("Update Database", async () => {
document.getElementById('ark-db-input').value = GM_getValue('ark_db', '');
const syncToggle = document.getElementById('ark-sync-toggle');
const syncEnabled = GM_getValue('ark_sync_enabled', false);
syncToggle.checked = syncEnabled;
const syncControls = document.getElementById('ark-sync-controls');
syncControls.style.display = syncEnabled ? 'flex' : 'none';
const handle = await getDBHandle();
if (handle) {
document.getElementById('ark-filename').innerText = handle.name;
}
overlay.style.display = 'flex';
});
document.getElementById('ark-sync-toggle').addEventListener('change', (e) => {
const enabled = e.target.checked;
GM_setValue('ark_sync_enabled', enabled);
document.getElementById('ark-sync-controls').style.display = enabled ? 'flex' : 'none';
});
const selectFileBtn = document.getElementById('ark-select-file');
if (selectFileBtn) {
selectFileBtn.addEventListener('click', async () => {
const picker = window.showOpenFilePicker || (typeof unsafeWindow !== 'undefined' ? unsafeWindow.showOpenFilePicker : null);
if (!picker) {
alert('현재 브라우저에서 로컬 파일 동기화 기능을 지원하지 않거나, 보안 연결(HTTPS)이 아닙니다. 크롬/엣지 브라우저를 사용해주세요.');
return;
}
try {
const [handle] = await picker({
types: [{ description: 'Text Files', accept: { 'text/plain': ['.txt'] } }],
multiple: false
});
await setDBHandle(handle);
document.getElementById('ark-filename').innerText = handle.name;
showToast('연결되었습니다: ' + handle.name);
} catch (err) {
console.error('File selection failed:', err);
if (err.name !== 'AbortError') {
showToast('파일 선택 중 오류가 발생했습니다.');
}
}
});
}
document.getElementById('ark-save-db').addEventListener('click', () => {
const text = document.getElementById('ark-db-input').value;
saveDatabase(text);
});
document.getElementById('ark-close-settings').addEventListener('click', () => {
overlay.style.display = 'none';
});
})();
---
버그나 수정필요한 부분있으면 말좀
---
업데이트
v0.2
탭 이동 (정보 -> 유틸)
로컬 파일 동기화 기능 추가 (Update Database 누르고 설정 가능)
화면 밖으로 팝업이 생기는 이슈 수정
Shift + C 단축키에 base64 변환 후 클립보드 복사 기능 추가
