JanitorAI Export beta

Fixes text selection while dragging. Added message counter and bilingual UI.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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';
    });
})();