Greasy Fork is available in English.

JanitorAI Export beta

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

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

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