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