Greasy Fork is available in English.
Fixes text selection while dragging. Added message counter and bilingual UI.
// ==UserScript==
// @name JanitorAI Export beta
// @name:zh-CN JanitorAI 剧情导出与校对工具 beta
// @namespace https://greatest.deepsurf.us/zh-CN/users/1593463-nander
// @license CC BY-NC-SA 4.0
// @version 23.7.1
// @description Fixes text selection while dragging. Added message counter and bilingual UI.
// @description:zh-CN 修复拖拽时文本被选中的问题。增加对话计数器、双语UI及狙击式校对。
// @author Gemini & User
// @match https://janitorai.com/chats/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
const VERSION = "v23.7";
let chatLog = [];
let sourceNode = null;
let forceNewline = GM_getValue('forceNewline', false);
let showTrigger = GM_getValue('showTrigger', true);
let lang = GM_getValue('lang', 'zh');
const i18n = {
zh: { title: "排序矩阵 " + VERSION, hint: "点 A,再点 B 即可排序", btnMatrix: "排序矩阵", btnEnter: "Enter换行: ", btnHide: "彻底隐藏", btnLang: "English", btnExport: "导出存档", btnClose: "关闭", locked: "源:锁定" },
en: { title: "Sniper Matrix " + VERSION, hint: "Click A, then B to reorder", btnMatrix: "Matrix", btnEnter: "Enter Newline: ", btnHide: "Hide UI", btnLang: "中文", btnExport: "Export", btnClose: "Close", locked: "Src: Locked" }
};
// --- 核心捕获与渲染逻辑 ---
function capture() {
const msgs = document.querySelectorAll('div[class*="_messageBody_"], li[class*="_message_"]');
let newCaptured = false;
msgs.forEach(el => {
const text = el.innerText.trim();
if (text.length < 2) return;
const finger = text.slice(0, 50) + text.length;
if (chatLog.find(i => i.id === finger)) return;
const nameEl = el.closest('li')?.querySelector('div[class*="_nameText_"]') || el.parentElement.querySelector('div[class*="_nameText_"]');
let role = nameEl ? nameEl.innerText.trim() : "User";
if (text.includes("You wake up after tossing")) role = "Intro";
chatLog.push({ id: finger, role, content: text });
newCaptured = true;
});
if (newCaptured) updateCounter();
}
function updateCounter() {
const badge = document.getElementById('v23-counter');
if (badge) {
badge.innerText = chatLog.length > 99 ? "99+" : chatLog.length;
badge.style.display = chatLog.length > 0 ? "flex" : "none";
}
}
// --- UI 系统 (优化拖拽体验) ---
function initUI() {
if (document.getElementById('v23-main-wrap')) return;
const mainWrap = document.createElement('div');
mainWrap.id = 'v23-main-wrap';
const pos = GM_getValue('capsulePos', { top: 20, left: 20 });
// 核心修复:-webkit-user-select: none 防止拖拽时选中背景文字
mainWrap.style = `position:fixed; top:${pos.top}px; left:${pos.left}px; z-index:2147483647; display:${showTrigger?'block':'none'}; padding: 10px 180px 30px 10px; margin: -10px; -webkit-user-select:none; user-select:none;`;
const trigger = document.createElement('div');
trigger.id = "v23-trigger";
// 修正 Cursor: grab 更有拖拽感
trigger.style = "width:16px; height:16px; background:#ffcc00; border-radius:3px; cursor:grab; box-shadow:0 0 10px rgba(255,204,0,0.8); position:relative; z-index:2;";
const badge = document.createElement('div');
badge.id = "v23-counter";
badge.style = "position:absolute; top:-6px; right:-8px; background:#f44; color:#fff; font-size:9px; font-weight:bold; min-width:12px; height:12px; border-radius:6px; display:none; align-items:center; justify-content:center; padding:0 2px; border:1px solid #111; pointer-events:none;";
const panel = document.createElement('div');
panel.id = 'v23-panel';
panel.style = "display:none; position:absolute; left:30px; top:10px; background:#111; border:1px solid #ffcc00; padding:10px; border-radius:8px; width:140px; box-shadow:0 0 20px rgba(0,0,0,1); z-index:3; cursor:default;";
const updatePanel = () => {
const curT = i18n[lang];
panel.innerHTML = `
<button id="p-matrix" style="width:100%; padding:6px; background:#ffcc00; color:#000; border:none; border-radius:4px; font-size:11px; font-weight:bold; cursor:pointer; margin-bottom:6px;">${curT.btnMatrix} (${chatLog.length})</button>
<button id="p-ent" style="width:100%; padding:6px; background:${forceNewline?'#00ffcc':'#333'}; color:${forceNewline?'#000':'#fff'}; border:none; border-radius:4px; font-size:11px; cursor:pointer; margin-bottom:6px;">${curT.btnEnter}${forceNewline?'ON':'OFF'}</button>
<button id="p-lang" style="width:100%; padding:6px; background:#444; color:#fff; border:none; border-radius:4px; font-size:11px; cursor:pointer; margin-bottom:6px;">${curT.btnLang}</button>
<button id="p-hide" style="width:100%; padding:4px; background:transparent; color:#f44; border:1px solid #f44; border-radius:4px; font-size:10px; cursor:pointer; margin-bottom:8px;">${curT.btnHide}</button>
<div style="font-size:9px; color:#555; text-align:center; border-top:1px solid #222; padding-top:4px; font-family:monospace;">Janitor Sniper ${VERSION}</div>
`;
document.getElementById('p-matrix').onclick = (e) => { e.stopPropagation(); showPreview(); };
document.getElementById('p-ent').onclick = (e) => { e.stopPropagation(); forceNewline = !forceNewline; GM_setValue('forceNewline', forceNewline); updatePanel(); };
document.getElementById('p-lang').onclick = (e) => { e.stopPropagation(); lang = (lang==='zh'?'en':'zh'); GM_setValue('lang', lang); updatePanel(); };
document.getElementById('p-hide').onclick = (e) => { e.stopPropagation(); showTrigger = false; GM_setValue('showTrigger', false); mainWrap.style.display = 'none'; };
};
mainWrap.onmouseenter = () => { panel.style.display = 'block'; updatePanel(); };
mainWrap.onmouseleave = () => { panel.style.display = 'none'; };
// 拖拽逻辑修复
let dragging = false, offset = { x: 0, y: 0 };
trigger.onmousedown = (e) => {
e.preventDefault(); // 关键:阻止默认行为,防止蓝屏选中
dragging = false;
trigger.style.cursor = 'grabbing';
offset.x = e.clientX - mainWrap.offsetLeft;
offset.y = e.clientY - mainWrap.offsetTop;
const move = (ev) => {
dragging = true;
mainWrap.style.left = (ev.clientX - offset.x) + 'px';
mainWrap.style.top = (ev.clientY - offset.y) + 'px';
};
const up = () => {
trigger.style.cursor = 'grab';
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', up);
GM_setValue('capsulePos', { top: parseInt(mainWrap.style.top), left: parseInt(mainWrap.style.left) });
};
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', up);
};
trigger.onclick = (e) => { e.stopPropagation(); if (!dragging) showPreview(); };
trigger.appendChild(badge);
mainWrap.appendChild(trigger);
mainWrap.appendChild(panel);
(document.body || document.documentElement).appendChild(mainWrap);
updateCounter();
}
// --- 矩阵预览与导出逻辑 (同前) ---
function showPreview() {
if (document.getElementById('v23-overlay')) return;
const t = i18n[lang];
const overlay = document.createElement('div');
overlay.id = "v23-overlay";
overlay.style = "position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(5,5,5,0.98);z-index:2147483647;overflow-y:auto;padding:40px;color:#e0e0e0;font-family:sans-serif;backdrop-filter:blur(10px);";
overlay.innerHTML = `<div style="max-width:900px;margin:0 auto;"><h1 style="color:#ffcc00;border-bottom:2px solid #ffcc00;padding-bottom:10px;display:flex;justify-content:space-between;"><span>${t.title}</span></h1><div id="drag-container"></div><div style="position:fixed;bottom:30px;right:50px;display:flex;gap:15px;"><button id="final-export" style="padding:15px 40px;background:#ffcc00;color:#000;font-weight:bold;cursor:pointer;border:none;border-radius:30px;">${t.btnExport}</button><button onclick="this.closest('#v23-overlay').remove()" style="padding:15px 25px;background:#333;color:#fff;border:none;border-radius:30px;cursor:pointer;">${t.btnClose}</button></div></div>`;
document.body.appendChild(overlay);
renderList(document.getElementById('drag-container'));
document.getElementById('final-export').onclick = () => {
let md = `# RP Archive\n\n`;
chatLog.forEach(item => md += `### **${item.role}**\n\n${item.content}\n\n---\n\n`);
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([md], { type: 'text/markdown' }));
a.download = `Janitor_Archive_${VERSION}.md`; a.click();
};
}
function renderList(container) {
container.innerHTML = '';
const t = i18n[lang];
chatLog.forEach((item) => {
const card = document.createElement('div');
card.className = "drag-item"; card.dataset.id = item.id;
const isUser = item.role === "nana";
card.style = `background:${item.role === "Intro" ? '#2a1b3d' : (isUser ? '#1e2a38' : '#2d1e1e')};border-left:5px solid ${isUser ? '#3498db' : '#e74c3c'};margin-bottom:10px;padding:15px;border-radius:8px;cursor:crosshair;`;
card.innerHTML = `<div style="font-weight:bold;margin-bottom:5px;font-size:11px;color:#ffcc00;display:flex;justify-content:space-between;"><span>${item.role}</span><span class="status-tag"></span></div><div style="font-size:14px;">${item.content}</div>`;
card.onclick = (e) => {
e.stopPropagation();
if (!sourceNode) { sourceNode = card; card.style.outline = "2px solid #ffcc00"; card.querySelector('.status-tag').innerText = t.locked; }
else if (sourceNode === card) { sourceNode = null; renderList(container); }
else {
const sIdx = chatLog.findIndex(i => i.id === sourceNode.dataset.id);
const itemToMove = chatLog.splice(sIdx, 1)[0];
const tIdx = chatLog.findIndex(i => i.id === card.dataset.id);
chatLog.splice(tIdx + 1, 0, itemToMove);
sourceNode = null; renderList(container);
}
};
container.appendChild(card);
});
}
setInterval(() => { capture(); initUI(); }, 2000);
document.addEventListener('keydown', (e) => {
if (forceNewline && e.key === 'Enter' && !e.ctrlKey && !e.altKey && !e.shiftKey) {
const t = e.target;
if (t.tagName === 'TEXTAREA' || t.getAttribute('contenteditable') === 'true') e.stopPropagation();
}
}, true);
GM_registerMenuCommand("👁️ Wake up Tool / 唤醒工具", () => {
showTrigger = true; GM_setValue('showTrigger', true);
const w = document.getElementById('v23-main-wrap');
if(w) w.style.display = 'block';
});
})();