JanitorAI Export beta

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Advertisement:

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

Advertisement:

// ==UserScript==
// @name         JanitorAI Export beta
// @name:zh-CN   JanitorAI 剧情导出与校对工具 beta
// @namespace    https://greasyfork.org/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';
    });
})();