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