JanitorAI Export beta

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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