LZT Article Summarizer

Утилита, которая сокращает текст темы, экономя ваше время на прочтение.

目前為 2024-11-03 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         LZT Article Summarizer
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Утилита, которая сокращает текст темы, экономя ваше время на прочтение.
// @author       @planetus (lolz)
// @match        https://lolz.live/threads/*
// @match        https://zelenka.guru/threads/*
// @match        https://lolz.guru/threads/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zelenka.guru
// @license      MIT
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    const defaultSettings = {
        minTextLength: 200,
        maxSummaryLength: 500,
        maxKeyPoints: 5,
        buttonColor: '#228e5d',
        summaryBgColor: '#091e15',
        summaryTextColor: '#ecf0f1',
        animationSpeed: 300
    };

    let settings = Object.assign({}, defaultSettings, GM_getValue('lztSummarizerSettings', {}));

    function saveSettings() {
        GM_setValue('lztSummarizerSettings', settings);
        updateStyles();
    }

    function clearSettings() {
        GM_setValue('lztSummarizerSettings', {});
        settings = Object.assign({}, defaultSettings);
        updateStyles();
    }

    function createSettingsMenu() {
        const settingsHTML = `
            <div id="lzt-settings-overlay" class="lzt-settings-overlay">
                <div id="lzt-settings" class="lzt-settings">
                    <h2 class="lzt-settings-title">Настройки LZT Summarizer</h2>
                    <div class="lzt-settings-group">
                        <label for="minTextLength">Минимальная длина текста для резюме:</label>
                        <input type="number" id="minTextLength" value="${settings.minTextLength}">
                    </div>
                    <div class="lzt-settings-group">
                        <label for="maxSummaryLength">Максимальная длина резюме:</label>
                        <input type="number" id="maxSummaryLength" value="${settings.maxSummaryLength}">
                    </div>
                    <div class="lzt-settings-group">
                        <label for="maxKeyPoints">Максимальное количество ключевых моментов:</label>
                        <input type="number" id="maxKeyPoints" value="${settings.maxKeyPoints}">
                    </div>
                    <div class="lzt-settings-group">
                        <label for="buttonColor">Цвет кнопки:</label>
                        <input type="color" id="buttonColor" value="${settings.buttonColor}">
                    </div>
                    <div class="lzt-settings-group">
                        <label for="summaryBgColor">Цвет фона резюме:</label>
                        <input type="color" id="summaryBgColor" value="${settings.summaryBgColor}">
                    </div>
                    <div class="lzt-settings-group">
                        <label for="summaryTextColor">Цвет текста резюме:</label>
                        <input type="color" id="summaryTextColor" value="${settings.summaryTextColor}">
                    </div>
                    <div class="lzt-settings-group">
                        <label for="animationSpeed">Скорость анимации (мс):</label>
                        <input type="number" id="animationSpeed" value="${settings.animationSpeed}">
                    </div>
                    <div class="lzt-settings-buttons">
                        <button id="saveSettings" class="lzt-settings-button lzt-settings-save">Сохранить</button>
                        <button id="clearSettings" class="lzt-settings-button lzt-settings-clear">Очистить всё</button>
                        <button id="closeSettings" class="lzt-settings-button lzt-settings-close">Закрыть</button>
                    </div>
                </div>
            </div>
        `;

        const settingsDiv = document.createElement('div');
        settingsDiv.innerHTML = settingsHTML;
        document.body.appendChild(settingsDiv);

        const overlay = document.getElementById('lzt-settings-overlay');
        const settingsPanel = document.getElementById('lzt-settings');

        setTimeout(() => {
            overlay.style.opacity = '1';
            settingsPanel.style.transform = 'translate(-50%, -50%) scale(1)';
        }, 50);

        document.getElementById('saveSettings').addEventListener('click', function() {
            settings.minTextLength = parseInt(document.getElementById('minTextLength').value);
            settings.maxSummaryLength = parseInt(document.getElementById('maxSummaryLength').value);
            settings.maxKeyPoints = parseInt(document.getElementById('maxKeyPoints').value);
            settings.buttonColor = document.getElementById('buttonColor').value;
            settings.summaryBgColor = document.getElementById('summaryBgColor').value;
            settings.summaryTextColor = document.getElementById('summaryTextColor').value;
            settings.animationSpeed = parseInt(document.getElementById('animationSpeed').value);
            saveSettings();
            closeSettingsMenu();
        });

        document.getElementById('clearSettings').addEventListener('click', function() {
            if (confirm('Вы уверены, что хотите сбросить все настройки?')) {
                clearSettings();
                closeSettingsMenu();
                alert('Настройки сброшены до значений по умолчанию.');
            }
        });

        document.getElementById('closeSettings').addEventListener('click', closeSettingsMenu);

        function closeSettingsMenu() {
            overlay.style.opacity = '0';
            settingsPanel.style.transform = 'translate(-50%, -50%) scale(0.9)';
            setTimeout(() => {
                settingsDiv.remove();
            }, 300);
        }
    }

    function updateStyles() {
        GM_addStyle(`
            .lzt-summary-button {
                background: linear-gradient(45deg, ${settings.buttonColor}, ${lightenDarkenColor(settings.buttonColor, -20)});
                border: none;
                color: white;
                padding: 10px 15px;
                text-align: center;
                text-decoration: none;
                display: inline-block;
                font-size: 14px;
                user-select: none;
                margin: 10px 0;
                cursor: pointer;
                border-radius: 4px;
                transition: all ${settings.animationSpeed}ms ease;
                box-shadow: 0 2px 4px rgba(0,0,0,0.1);
                animation: fadeInButton ${settings.animationSpeed}ms ease-out;
            }
            .lzt-summary-button:hover {
                background: linear-gradient(45deg, ${lightenDarkenColor(settings.buttonColor, -20)}, ${lightenDarkenColor(settings.buttonColor, -40)});
                transform: translateY(-2px);
                box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            }
            .lzt-summary-button:active {
                transform: translateY(0);
            }
            .lzt-summary {
                position: relative;
                z-index: 99;
                background-color: ${settings.summaryBgColor};
                border-left: 4px solid ${settings.buttonColor};
                color: ${settings.summaryTextColor};
                padding: 20px;
                margin: 15px 0;
                border-radius: 8px;
                font-size: 14px;
                line-height: 1.6;
                animation: fadeIn ${settings.animationSpeed}ms ease-out;
                box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            }
            @keyframes fadeIn {
                from { opacity: 0; transform: translateY(-10px); }
                to { opacity: 1; transform: translateY(0); }
            }
            @keyframes fadeInButton {
                from {
                    opacity: 0;
                    transform: translateY(10px);
                }
                to {
                    opacity: 1;
                    transform: translateY(0);
                }
            }
            .lzt-summary-title {
                color: ${settings.buttonColor};
                font-weight: bold;
                margin-bottom: 15px;
                font-size: 18px;
            }
            .lzt-summary-content {
                margin-top: 10px;
            }
            .lzt-key-points {
                margin-top: 20px;
                padding-top: 15px;
                border-top: 1px solid rgba(255,255,255,0.2);
            }
            .lzt-key-points-title {
                color: ${settings.buttonColor};
                font-weight: bold;
                margin-bottom: 10px;
            }
            .lzt-key-point {
                margin: 8px 0;
                padding-left: 20px;
                position: relative;
            }
            .lzt-key-point:before {
                content: "•";
                color: ${settings.buttonColor};
                position: absolute;
                left: 0;
                font-size: 18px;
            }
            .lzt-popup {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%) scale(0.9);
                background-color: ${settings.summaryBgColor};
                padding: 30px;
                border-radius: 12px;
                box-shadow: 0 10px 30px rgba(0,0,0,0.3);
                z-index: 999999;
                width: 90%;
                max-width: 800px;
                max-height: 90vh;
                overflow-y: auto;
                opacity: 0;
                transition: all ${settings.animationSpeed}ms ease;
            }
            .lzt-popup.show {
                opacity: 1;
                transform: translate(-50%, -50%) scale(1);
            }
            .lzt-popup-close {
                position: absolute;
                top: 15px;
                right: 20px;
                font-size: 28px;
                color: ${settings.summaryTextColor};
                cursor: pointer;
                transition: color ${settings.animationSpeed}ms ease;
            }
            .lzt-popup-close:hover {
                color: ${settings.buttonColor};
            }
            .lzt-original-content {
                transition: opacity ${settings.animationSpeed}ms ease, height ${settings.animationSpeed}ms ease;
                overflow: hidden;
            }
            .lzt-overlay {
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                background-color: rgba(0, 0, 0, 0.7);
                backdrop-filter: blur(8px);
                z-index: 99998;
                opacity: 0;
                transition: opacity ${settings.animationSpeed}ms ease;
            }
            .lzt-overlay.show {
                opacity: 1;
            }
            .lzt-settings-overlay {
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                background-color: rgba(0, 0, 0, 0.8);
                z-index: 10000;
                display: flex;
                justify-content: center;
                align-items: center;
                opacity: 0;
                backdrop-filter: blur(8px);
                transition: opacity ${settings.animationSpeed}ms ease;
            }
            .lzt-settings {
                background-color: #303030b0;
                padding: 30px;
                border-radius: 12px;
                box-shadow: 0 10px 30px rgba(0,0,0,0.3);
                width: 90%;
                max-width: 500px;
                transform: translate(-50%, -50%) scale(0.9);
                transition: transform ${settings.animationSpeed}ms ease;
                position: fixed;
                top: 50%;
                left: 50%;
            }
            .lzt-settings-title {
                color: #ecf0f1;
                font-size: 24px;
                margin-bottom: 20px;
                text-align: center;
            }
            .lzt-settings-group {
                margin-bottom: 15px;
            }
            .lzt-settings-group label {
                display: block;
                color: #bdc3c7;
                margin-bottom: 5px;
            }
            .lzt-settings-group input {
                width: 100%;
                padding: 8px;
                border: none;
                border-radius: 4px;
                background-color: #303030;
                color: #ecf0f1;
                font-size: 14px;
            }
            .lzt-settings-group input[type="color"] {
                height: 40px;
                cursor: pointer;
            }
            .lzt-settings-buttons {
               display: flex;
               gap: 5px;
               justify-content: space-between;
               margin-top: 20px;
            }
            .lzt-settings-button {
                padding: 10px 20px;
                border: none;
                border-radius: 4px;
                font-size: 15px;
                cursor: pointer;
                transition: background-color ${settings.animationSpeed}ms ease;
            }
           .lzt-settings-save {
               display: block;
               width: -webkit-fill-available;
               background-color: #2ecc7161;
               color: white;
            }
            .lzt-settings-save:hover {
               background-color: #27ae60;
            }
            .lzt-settings-clear {
              display: block;
              width: -webkit-fill-available;
              background-color: #f39c1291;
              color: white;
             }
            .lzt-settings-clear:hover {
              background-color: #d35400;
            }
            .lzt-settings-close {
              display: block;
              width: -webkit-fill-available;
              background-color: #e74c3cde;
              color: white;
            }
            .lzt-settings-close:hover {
              background-color: #c0392b;
            }
        `);
    }

    function lightenDarkenColor(col, amt) {
        let usePound = false;
        if (col[0] == "#") {
            col = col.slice(1);
            usePound = true;
        }
        let num  = parseInt(col,16);
        let r = (num >> 16) + amt;
        if (r > 255) r = 255;
        else if  (r < 0) r = 0;
        let b = ((num >> 8) & 0x00FF) + amt;
        if (b > 255) b = 255;
        else if  (b < 0) b = 0;
        let g = (num &   0x0000FF) + amt;
        if (g > 255) g = 255;
        else if (g < 0) g = 0;
        return (usePound?"#":"") + (g | (b << 8) | (r << 16)).toString(16).padStart(6, '0');
    }

    function extractKeyPoints(text) {
        const sentences = text.match(/[^\.!\?]+[\.!\?]+/g) || [];
        const keyPoints = [];

        const indicators = [
            'важно', 'главное', 'необходимо', 'следует', 'нужно',
            'основной', 'ключевой', 'рекомендуется', 'обратите внимание',
            'в первую очередь', 'самое важное', 'ключевая идея', 'существенно',
            'критически', 'принципиально', 'фундаментально', 'приоритетно'
        ];

        sentences.forEach(sentence => {
            const lowercaseSentence = sentence.toLowerCase();
            if (indicators.some(indicator => lowercaseSentence.includes(indicator)) ||
                (sentence.length > 30 && sentence.length < 200 && /[0-9]/.test(sentence)) ||
                /^[•\-]/.test(sentence.trim())) {
                keyPoints.push(sentence.trim());
            }
        });

        const startIndicators = ['ключевые моменты', 'основные пункты', 'главные идеи'];
        let foundKeyPointSection = false;

        sentences.forEach(sentence => {
            const lowercaseSentence = sentence.toLowerCase().trim();
            if (startIndicators.some(indicator => lowercaseSentence.startsWith(indicator))) {
                foundKeyPointSection = true;
            }
            if (foundKeyPointSection && /^[•\-]/.test(sentence.trim())) {
                keyPoints.push(sentence.trim());
            }
        });

        return [...new Set(keyPoints)].slice(0, settings.maxKeyPoints);
    }

    function summarizeText(text) {
        text = text.replace(/\s+/g, ' ').trim();
        const sentences = text.match(/[^\.!\?]+[\.!\?]+/g) || [];

        if (sentences.length === 0) {
            return '';
        }

        const sentenceScores = sentences.map((sentence, index) => {
            const words = sentence.toLowerCase().split(' ');
            let score = 0;

            const importantWords = ['важно', 'главное', 'необходимо', 'ключевой', 'существенно', 'критически', 'спонсор', 'проект'];
            if (words.some(word => importantWords.includes(word))) {
                score += 5;
            }

            if (sentence.length > 30 && sentence.length < 200) {
                score += 2;
            }

            if (/[0-9]/.test(sentence)) {
                score += 2;
            }

            if (index < 5) {
                score += 3;
            }

            if (index > 0) {
                const prevSentence = sentences[index - 1].toLowerCase();
                const commonWords = words.filter(word => prevSentence.includes(word) && word.length > 3);
                score += commonWords.length;
            }

            return { sentence, score, index };
        });

        sentenceScores.sort((a, b) => b.score - a.score);

        let summary = '';
        let currentLength = 0;
        let usedIndexes = new Set();

        for (const { sentence, index } of sentenceScores) {
            if (currentLength + sentence.length <= settings.maxSummaryLength && !usedIndexes.has(index)) {
                if (summary && Math.abs(index - Array.from(usedIndexes).pop()) > 3) {
                    continue;
                }
                summary += sentence + ' ';
                currentLength += sentence.length;
                usedIndexes.add(index);
            }
            if (currentLength >= settings.maxSummaryLength) break;
        }

        return summary.trim();
    }

    function createPopup(content) {
        const overlay = document.createElement('div');
        overlay.className = 'lzt-overlay';
        document.body.appendChild(overlay);

        const popup = document.createElement('div');
        popup.className = 'lzt-popup';
        popup.innerHTML = `
            <div class="lzt-popup-close">&times;</div>
            ${content}
        `;

        document.body.appendChild(popup);

        setTimeout(() => {
            overlay.classList.add('show');
            popup.classList.add('show');
        }, 50);

        const closePopup = () => {
            overlay.classList.remove('show');
            popup.classList.remove('show');
            setTimeout(() => {
                overlay.remove();
                popup.remove();
            }, settings.animationSpeed);
        };

        popup.querySelector('.lzt-popup-close').addEventListener('click', closePopup);
        overlay.addEventListener('click', closePopup);
    }

function getTextContent(element) {
        let text = '';
        function extractText(node) {
            if (node.nodeType === Node.TEXT_NODE) {
                text += node.textContent.trim() + ' ';
            } else if (node.nodeType === Node.ELEMENT_NODE) {
                if (node.classList.contains('bbCodeBlock') ||
                    node.tagName === 'IMG' ||
                    node.tagName === 'SCRIPT' ||
                    node.tagName === 'STYLE' ||
                    node.classList.contains('messageTextEndMarker') ||
                    node.classList.contains('lzt-summary-button')) {
                    return;
                }
                node.childNodes.forEach(extractText);
            }
        }
        extractText(element);
        return text.replace(/\s+/g, ' ').trim();
    }

function addSummaryButton(postElement) {
        if (!postElement.classList.contains('firstPost')) return;

        if (postElement.querySelector('.lzt-summary-button')) return;

        const contentElement = postElement.querySelector('.messageContent');
        if (!contentElement) return;

        const fullText = getTextContent(contentElement);

        console.log('Длина текста:', fullText.length);
        console.log('Текстовое содержимое:', fullText);

        if (fullText.length < settings.minTextLength) {
            console.log('Слишком короткий текст');
            return;
        }

        const summaryButton = document.createElement('button');
        summaryButton.textContent = '📝 Показать резюме';
        summaryButton.className = 'lzt-summary-button';
        summaryButton.style.opacity = '0';

        let summaryElement = null;
        let isProcessing = false;

        summaryButton.addEventListener('click', function(e) {
            e.preventDefault();
            if (isProcessing) return;
            isProcessing = true;

            if (summaryElement) {
                summaryElement.style.display = summaryElement.style.display === 'none' ? 'block' : 'none';
                summaryButton.textContent = summaryElement.style.display === 'none' ? '📝 Показать резюме' : '❌ Скрыть резюме';

                contentElement.style.height = summaryElement.style.display === 'none' ? 'auto' : '0';
                contentElement.style.opacity = summaryElement.style.display === 'none' ? '1' : '0';
                isProcessing = false;
            } else {
                const summary = summarizeText(fullText);
                const keyPoints = extractKeyPoints(fullText);

                summaryElement = document.createElement('div');
                summaryElement.className = 'lzt-summary';

                let summaryHTML = `
                    <div class="lzt-summary-title">📌 Краткое содержание</div>
                    <div class="lzt-summary-content">${summary || 'Не удалось создать краткое содержание.'}</div>
                `;

                if (keyPoints.length > 0) {
                    summaryHTML += `
                        <div class="lzt-key-points">
                            <div class="lzt-key-points-title">🔑 Ключевые моменты:</div>
                            ${keyPoints.map(point => `<div class="lzt-key-point">${point}</div>`).join('')}
                        </div>
                    `;
                } else {
                    summaryHTML += `
                        <div class="lzt-key-points">
                            <div class="lzt-key-points-title">🔑 Ключевые моменты:</div>
                            <div class="lzt-key-point">Не удалось выделить ключевые моменты.</div>
                        </div>
                    `;
                }

                summaryElement.innerHTML = summaryHTML;
                contentElement.parentNode.insertBefore(summaryElement, contentElement.nextSibling);
                summaryButton.textContent = '❌ Скрыть резюме';

                contentElement.style.height = '0';
                contentElement.style.opacity = '0';

                setTimeout(() => {
                    isProcessing = false;
                }, settings.animationSpeed);
            }
        });

        contentElement.parentNode.insertBefore(summaryButton, contentElement);

        setTimeout(() => {
            summaryButton.style.opacity = '1';
        }, 100);
    }

    function summarizeMainArticle() {
        const firstPost = document.querySelector('li.message.firstPost');
        if (!firstPost) {
            alert('Не удалось найти основную статью на странице.');
            return;
        }

        const contentElement = firstPost.querySelector('.messageContent');
        if (!contentElement) {
            alert('Не удалось найти содержимое статьи.');
            return;
        }

        const fullText = getTextContent(contentElement);
        const summary = summarizeText(fullText);
        const keyPoints = extractKeyPoints(fullText);

        let popupContent = `
            <div class="lzt-summary-title">📝 Краткое содержание статьи</div>
            <div class="lzt-summary-content">${summary || 'Не удалось создать краткое содержание.'}</div>
        `;

        if (keyPoints.length > 0) {
            popupContent += `
                <div class="lzt-key-points">
                    <div class="lzt-key-points-title">🔑 Ключевые моменты:</div>
                    ${keyPoints.map(point => `<div class="lzt-key-point">${point}</div>`).join('')}
                </div>
            `;
        } else {
            popupContent += `
                <div class="lzt-key-points">
                    <div class="lzt-key-points-title">🔑 Ключевые моменты:</div>
                    <div class="lzt-key-point">Не удалось выделить ключевые моменты.</div>
                </div>
            `;
        }

        createPopup(popupContent);
    }

    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    GM_registerMenuCommand("LZT Резюме статьи", debounce(summarizeMainArticle, 300));
    GM_registerMenuCommand("Настройки LZT Summarizer", debounce(createSettingsMenu, 300));

    updateStyles();

    window.addEventListener('load', function() {
        const firstPost = document.querySelector('li.message.firstPost');
        if (firstPost) {
            addSummaryButton(firstPost);
        }
    });

    const observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(function(node) {
                    if (node.nodeType === 1 && node.matches('li.message.firstPost')) {
                        addSummaryButton(node);
                    }
                });
            }
        });
    });

    observer.observe(document.body, { childList: true, subtree: true });
})();