Душа_Чата
|
Сообщение #1
2 апреля 2023 в 23:41
|
Гонщик
1 |
Предлагаю желающим воспользоваться этим скриптом для чата, который расширяет его возможности. Распишу попунктно что он умеет делать. 1. Просматривать количество текущих пользователей в чате. 2. Уведомлять о поступивших сообщениях звуковым сигналом или голосовым зачитыванием. 3. Уведомлять о личных сообщениях. Требуется настройка возможных вариаций вашего никнейма, который может быть использован при обращении к вам. Настройка происходит в const mentionKeywords внутри кода. Первое значение myNickname в массиве mentionKeywords не нужно менять. Менять необходимо последующие значения в кавычках. Вы так-же можете ничего не указывать кроме обязательного myNickname. Важно, чтобы в таком случае после myNickname не стояло запятой. Как только вы добавляете вариации слов, после myNickname обязательно должна стоять запятая и слова оборачиваете в кавычки. Пример с дополнительными словами возможного к вам обращения. const mentionKeywords = [ // Actual nickname myNickname, // Possible nickname keywords 'Душа', 'Панчер' ]; Пример того, когда вам не нужно заполнять дополнительные слова. Обязательное остаётся. const mentionKeywords = [ // Actual nickname myNickname ]; 4. Возможность подсвечивать в чате слова обращения к вам, которые были занесены в mentionKeywords. 5. Можно ограничивать звуковые оповещения и голосовое зачитывание только для личных сообщений переключателем режима. (все), (только личные). 6. Удалять нежелательные сообщения в чате единичным выделением правой кнопкой мыши или же множественным выделением с зажатой правой кнопкой мыши и нажатием на кнопку Delete. 7. Отправлять картинки в чат ссылкой, после чего происходит конвертация ссылки в кликабельное превью, после чего при клике на превью левой кнопкой мыши открывается картинка в крупном масштабе. С зажатой средней клавишей мыши можно водить изображение по экрану, а прокручивая колесо мыши масштабировать. 8. Отправлять ютуб ссылки в чат, после чего оно конвертируется в iframe, в следствии чего можно проигрывать видео ролики прямо в чате не переходя в новую вкладку. 9. Оповещать о всех зашедших и вышедших пользователях тултипами без сигналов. 10. Оповещать о всех зашедших и вышедших отслеживаемых пользователей тултипами и звуко-голосовым оповещением. Добавление отслеживаемых пользователей происходит в const usersToTrack внутри кода. // Define the users to track and notify with popup and audio const usersToTrack = [ { name: 'Даниэль', gender: 'male', pronunciation: 'Даниэль' }, // ------- 01 { name: 'певец', gender: 'male', pronunciation: 'Певец' }, // ----------- 02 { name: 'Баристарх', gender: 'male', pronunciation: 'Баристарх' }, // --- 03 { name: 'madinko', gender: 'female', pronunciation: 'Мадинко' }, // ----- 04 { name: 'Переборыч', gender: 'male', pronunciation: 'Переборыч' }, // --- 05 { name: 'Advisor', gender: 'male', pronunciation: 'Адвайзер' }, // ------ 06 { name: 'Хеопс', gender: 'male', pronunciation: 'Хеопс' }, // ----------- 07 { name: 'Рустамко', gender: 'male', pronunciation: 'Рустамко' }, // ----- 08 { name: 'ExpLo1t', gender: 'female', pronunciation: 'Эксплоит' }, // ---- 09 { name: 'инфо-пчелы', gender: 'male', pronunciation: 'Инфо-Пчёлы' }, // - 10 { name: 'Razmontana', gender: 'male', pronunciation: 'Размонтана' } // -- 11 ]; 11. Безотказная прокрутка чата, которая умудряется прокручивать даже таких гигантов как превью изображения и ютуб iframe. Дефолтный функционал сайта не справляется с ними. 12. Переключение между вкладками чата внутри заезда по горячей клавишей Tab с запоминанием настройки текущей активной вкладки и автоматическим восстановлением с последующими заездами. 13. Нижний отступ для каждого последнего сообщения или последнего сообщения из группы сообщений одного и того же пользователя для создания группирования и повышения читабельности чата. СКАЧАТЬ ИМЕННО ТУТСкрипт для чатаДля улучшенного экспириенса, установите тёмную тему. Тёмная темаДополнительные примечания. Для работы сигналов (beep), необходимо разрешить звуки (AudioContext API). Для этого, у кого браузер firefox, необходимо произвести подобные настройки. Ввести в браузерной строке about:config -> Enter и найти соответствующий ключ как на скрине и выставить ему значение 1 В некоторых случаях цифра 1 не всегда работает. Поэтому попробуйте выставить цифру 0. Перезагрузите браузер и вновь зайдите. После этого точно должен заработать оповещающий сигнал (beep). Для работы голосового движка, вам необходимо как минимум быть на windows машине. Так-же должен быть установлен русский языковой пакет. После установки языкового пакета, вам возможно будет необходимо так-же задать разрешение воспроизводить голоса в браузере. Если у вас опять же браузер firefox, то похожим методом как с (AudioContext API). Для лучшего понимания как, что и почему, лучше ознакомиться прочтением всех постов данной темы. В них расписаны как добавление новых функций так и объяснение как с ними работать. Последний раз отредактировано 14 мая 2023 в 19:45 пользователем Душа_Чата
|
Душа_Чата
|
Сообщение #2
3 апреля 2023 в 15:06
|
Гонщик
1 |
Добавлена возможность отключать звук зачитывания сообщения переключив тем самым на звуковой сигнал и наоборот. Кнопка ведёт себя как переключатель между режимами оповещения о новых последних сообщениях. А главное, что кнопка запоминает данную последнюю настройку пользователем, так как информация о состоянии кнопки сохраняется в браузерном localStorage как newMessageIsMuted (false или true). Обновлённый код скрытый текст… // ==UserScript== // @name KG_Chat_Users_Tracker // @namespace http://tampermonkey.net/ // @version 0.1 // @description Count how much users are in chat and notify who entered and left the chat // @author Patcher // @match *://klavogonki.ru/g* // @grant none // ==/UserScript==
(function () { // SOUND NOTIFICATION // Note values and their corresponding frequencies // C0 to B8 const notesToFrequency = {}; for (let i = 0; i < 88; i++) { const note = i - 48; const frequency = Math.pow(2, (note - 9) / 12) * 440; notesToFrequency[i] = frequency; }
// List of notes to play for "User Left" && "User Entered" && "New Messages" const userEnteredNotes = [48, 60]; // C4, C5 const userLeftNotes = [60, 48]; // C5, C4 const newMessageNotes = [65];
// Volume and duration settings const volumeEntered = 0.35; const volumeLeft = 0.35; const volumeNewMessage = 0.35; const duration = 80; const fadeTime = 30;
// Function to play a beep given a list of notes and a volume async function playBeep(notes, volume) { const context = new AudioContext(); for (const note of notes) { if (note === 0) { // Rest note await new Promise((resolve) => setTimeout(resolve, duration)); } else { // Play note const oscillator = context.createOscillator(); const gain = context.createGain(); oscillator.connect(gain); oscillator.frequency.value = notesToFrequency[note]; oscillator.type = "triangle";
// Create low pass filter to cut frequencies below 250Hz const lowPassFilter = context.createBiquadFilter(); lowPassFilter.type = 'lowpass'; lowPassFilter.frequency.value = 250; oscillator.connect(lowPassFilter);
// Create high pass filter to cut frequencies above 16kHz const highPassFilter = context.createBiquadFilter(); highPassFilter.type = 'highpass'; highPassFilter.frequency.value = 16000; lowPassFilter.connect(highPassFilter);
gain.connect(context.destination); gain.gain.setValueAtTime(0, context.currentTime); gain.gain.linearRampToValueAtTime(volume, context.currentTime + fadeTime / 1000); oscillator.start(context.currentTime); oscillator.stop(context.currentTime + duration * 0.001); gain.gain.setValueAtTime(volume, context.currentTime + (duration - fadeTime) / 1000); gain.gain.linearRampToValueAtTime(0, context.currentTime + duration / 1000); await new Promise((resolve) => setTimeout(resolve, duration)); } } }
// Text to speech function textToSpeech(text) { // Replace underscores with spaces and match only letters const lettersOnly = text.replace(/_/g, ' ').replace(/[^a-zA-Zа-яА-Я ]/g, '');
const utterance = new SpeechSynthesisUtterance(lettersOnly); utterance.lang = 'ru-RU'; utterance.voice = speechSynthesis.getVoices().find((voice) => voice.name === 'Microsoft Pavel - Russian (Russia)');
speechSynthesis.speak(utterance); }
// Define the users to track and notify with popup and audio const usersToTrack = [ { name: 'Даниэль', gender: 'male' }, { name: 'певец', gender: 'male' }, { name: 'ВеликийИнка', gender: 'male' }, { name: 'madinko', gender: 'female' }, { name: 'Переборыч', gender: 'male' }, { name: 'Advisor', gender: 'male' }, { name: 'Хеопс', gender: 'male' } ];
const verbs = { male: { enter: 'зашёл', leave: 'вышел' }, female: { enter: 'зашла', leave: 'вышла' } };
function getUserGender(userName) { const user = usersToTrack.find((user) => user.name === userName); return user ? user.gender : null; }
// Functions to play beep for user entering and leaving function userEntered(user) { playBeep(userEnteredNotes, volumeEntered); const userGender = getUserGender(user); const action = verbs[userGender].enter; const message = `${user} ${action}`; setTimeout(() => { textToSpeech(message); }, 300); }
function userLeft(user) { playBeep(userLeftNotes, volumeLeft); const userGender = getUserGender(user); const action = verbs[userGender].leave; const message = `${user} ${action}`; setTimeout(() => { textToSpeech(message); }, 300); }
// POPUPS // Define the function to generate HSL color with user parameters for hue, saturation, lightness function getHSLColor(hue, saturation, lightness) { // Set default value for hue if (typeof hue === 'undefined') { hue = 180; } // Set default value for saturation if (typeof saturation === 'undefined') { saturation = 50; } // Set default value for lightness if (typeof lightness === 'undefined') { lightness = 50; } var color = `hsl(${hue},${saturation}%,${lightness}%)`; return color; }
// Reference for the existing popup let previousPopup = null;
function showUserAction(user, action, presence) { const userPopup = document.createElement('div'); userPopup.classList.add('userPopup'); userPopup.innerText = `${user} ${action}`;
// Set the initial styles for the user popup userPopup.style.position = 'fixed'; userPopup.style.right = '-100%'; userPopup.style.transform = 'translateY(-50%)'; userPopup.style.opacity = '0'; userPopup.style.color = presence ? getHSLColor(100, 50, 50) : getHSLColor(0, 50, 70); // fontColor green && red userPopup.style.backgroundColor = presence ? getHSLColor(100, 50, 10) : getHSLColor(0, 50, 15); // backgroundColor green && red userPopup.style.border = presence ? `1px solid ${getHSLColor(100, 50, 25)}` : `1px solid ${getHSLColor(0, 50, 40)}`; // borderColor green && red userPopup.style.setProperty('border-radius', '4px 0 0 4px', 'important'); userPopup.style.padding = '8px 16px'; userPopup.style.display = 'flex'; userPopup.style.alignItems = 'center';
// Append the user popup to the body document.body.appendChild(userPopup);
// Calculate the width and height of the user popup const popupWidth = userPopup.offsetWidth; const popupHeight = userPopup.offsetHeight; const verticalOffset = 2;
// Set the position of the user popup relative to the previous popup let topPosition = '30vh'; if (previousPopup !== null) { const previousPopupPosition = previousPopup.getBoundingClientRect(); topPosition = `calc(${previousPopupPosition.bottom}px + ${popupHeight}px / 2 + ${verticalOffset}px)`; } userPopup.style.top = topPosition; userPopup.style.right = `-${popupWidth}px`;
// Animate the user popup onto the screen userPopup.style.transition = 'all 0.3s ease-in-out'; userPopup.style.right = '0'; userPopup.style.opacity = '1';
// Store a reference to the current popup previousPopup = userPopup;
// Hide the user popup after a short delay setTimeout(() => { userPopup.style.transition = 'all 0.3s ease-in-out'; userPopup.style.right = `-${popupWidth}px`; userPopup.style.opacity = '0'; setTimeout(() => { document.body.removeChild(userPopup); // Clear the reference to the previous popup if (previousPopup === userPopup) { previousPopup = null; } }, 300); }, 5000); }
// FUNCTIONALITY // Define references to retrieve and create const userList = document.querySelector('.userlist-content'); const userCount = document.createElement('div'); userCount.classList.add('user-count'); userCount.style.filter = 'grayscale(100%)'; userCount.innerHTML = '0'; document.body.appendChild(userCount);
// Initialize variables to keep track of the current and previous users let currentUsers = []; let previousUsers = []; let hasObservedChanges = false; let prevUserCountValue = 0;
// Initialize variables for the user count animation let currentTextContent = []; let isAnimating = false;
// Mutation observer to track all the users with only graphical popup notification // Also play notification sound "Left" or "Entered" if the one of them is identical from "usersToTrack" array // Create a mutation observer to detect when the user list is modified const observeUsers = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { // Retrieve all users textContent from userList ins elements const newUserList = Array.from(userList.children).map(child => child.textContent);
// Find new users and left users const newUsers = newUserList.filter(user => !currentUsers.includes(user)); const leftUsers = currentUsers.filter(user => !newUserList.includes(user));
// Retrieve fresh user count length const userCountValue = newUserList.length; // Retrieve the counter element const userCount = document.querySelector('.user-count');
// Update grayscale filter userCount.style.filter = userCountValue > 0 ? 'none' : 'grayscale(100%)';
// Check if the user count animation needs to be started if (currentTextContent.length === 0 && newUserList.length > 0 && !isAnimating) { isAnimating = true; const actualUserCount = newUserList.length; const speed = 20; // Change the speed here (in milliseconds) let count = 0; const userCountIncrement = () => { if (count <= actualUserCount) { const progress = count / actualUserCount; const grayscale = 100 - progress * 100; userCount.innerHTML = `${count++}`; userCount.style.filter = `grayscale(${grayscale}%)`; setTimeout(userCountIncrement, speed); } else { currentTextContent = Array.from(userList.children).map(child => child.textContent); userCount.style.filter = 'none'; userCount.classList.add('pulse'); setTimeout(() => { userCount.classList.remove('pulse'); isAnimating = false; // set isAnimating to false after the animation }, 1000); } }; setTimeout(userCountIncrement, speed); } // Animation END
// Check only after the animation is end if (!isAnimating) { // Check if the user count has changed and add pulse animation if (userCountValue !== prevUserCountValue) { userCount.classList.add('pulse'); // Updating the counter element value userCount.innerHTML = userCountValue; setTimeout(() => { userCount.classList.remove('pulse'); }, 1000); } }
// Log new and left users if (hasObservedChanges) { newUsers.forEach((newUser) => { if (!previousUsers.includes(newUser)) { const userGender = getUserGender(newUser) || 'male'; // use 'male' as default const action = verbs[userGender].enter; showUserAction(newUser, action, true); if (usersToTrack.some(user => user.name === newUser)) { userEntered(newUser, userGender); } } });
leftUsers.forEach((leftUser) => { const userGender = getUserGender(leftUser) || 'male'; // use 'male' as default const action = verbs[userGender].leave; showUserAction(leftUser, action, false); if (usersToTrack.some(user => user.name === leftUser)) { userLeft(leftUser, userGender); } }); } else { hasObservedChanges = true; }
// Update the previous users and user count previousUsers = currentUsers; currentUsers = newUserList; prevUserCountValue = userCountValue;
} }); });
// Start observing users const config = { childList: true }; observeUsers.observe(userList, config);
// STYLIZATION const styles = ` @import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');
.user-count { font-family: 'Orbitron', sans-serif; font-size: 24px; color: #83cf40; position: fixed; top: 130px; right: 24px; background-color: #2b4317; width: 48px; height: 48px; display: flex; justify-content: center; align-items: center; border: 1px solid #4b7328; transition: filter 0.2s ease-in-out; }
.pulse { animation-name: pulse; animation-duration: 1s; animation-iteration-count: 1; }
@keyframes pulse { 0% { filter: brightness(1); } 50% { filter: brightness(1.5); } 100% { filter: brightness(1); } } `;
const styleElement = document.createElement('style'); styleElement.textContent = styles; document.head.appendChild(styleElement);
// EVERY NEW MESSAGE READER // Avoid reading on load page to read the messages normally on stable presence let isInitialized = false;
// create a mutation observer to watch for new messages being added const newMessagesObserver = new MutationObserver(mutations => { for (let mutation of mutations) { if (mutation.type === 'childList') { for (let node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'P') { // read the text content of the new message and speak it const latestMessageTextContent = localStorage.getItem('latestMessageTextContent'); const newMessageTextContent = getLatestMessageTextContent();
// Get the sound switcher element and check if it's muted or not const soundSwitcher = document.querySelector('#unmuted, #muted'); const isMuted = soundSwitcher && soundSwitcher.id === 'muted';
// If not muted, speak the new message and update the latest message content in local storage if (!isMuted && isInitialized && newMessageTextContent && newMessageTextContent !== latestMessageTextContent) { const utterance = new SpeechSynthesisUtterance(newMessageTextContent); utterance.lang = 'ru-RU'; utterance.voice = speechSynthesis.getVoices().find(voice => voice.name === 'Microsoft Pavel - Russian (Russia)'); speechSynthesis.speak(utterance); localStorage.setItem('latestMessageTextContent', newMessageTextContent); }
// If it's the first time, update the latest message content in local storage and set isInitialized to true if (!isInitialized) { localStorage.setItem('latestMessageTextContent', newMessageTextContent); isInitialized = true; }
// If it's muted, play the beep sound for the new message if (isMuted) { playBeep(newMessageNotes, volumeNewMessage); } } } } } });
// function to get the cleaned text content of the latest message function getLatestMessageTextContent() { const message = document.querySelector('.messages-content div p:last-child'); if (!message) { return null; } const textNodes = [...message.childNodes].filter(node => node.nodeType === Node.TEXT_NODE); const text = textNodes.map(node => node.textContent).join('').trim(); const time = message.querySelector('.time'); const username = message.querySelector('.username'); const timeText = time ? time.textContent : ''; const usernameText = username ? username.textContent : ''; return text.replace(timeText, '').replace(usernameText, '').trim(); }
// observe changes to the messages container element const messagesContainer = document.querySelector('.messages-content div'); newMessagesObserver.observe(messagesContainer, { childList: true, subtree: true });
// SOUND GRAPHICAL SWITCHER // New message sound notification graphical switcher as a button const chatButtonsPanel = document.querySelector('.chat .messages table td:nth-child(3)'); // Avoid panel squeezing chatButtonsPanel.style.minWidth = '75px'; const soundSwitcher = document.createElement('div');
// Check if the user has previously muted the sound const newMessageIsMuted = localStorage.getItem('newMessageIsMuted') === 'true';
soundSwitcher.classList.add('chat-opt-btn'); soundSwitcher.id = newMessageIsMuted ? 'muted' : 'unmuted'; soundSwitcher.addEventListener('click', function () { if (this.id === 'unmuted') { this.id = 'muted'; localStorage.setItem('newMessageIsMuted', 'true'); } else { this.id = 'unmuted'; localStorage.setItem('newMessageIsMuted', 'false'); } updateSoundIcon(); });
const soundIcon = document.createElement('span'); soundIcon.classList.add('sound-icon'); soundIcon.textContent = newMessageIsMuted ? '
|
Душа_Чата
|
Сообщение #3
3 апреля 2023 в 16:39
|
Гонщик
1 |
Сделан небольшой фикс скрипта. Когда кнопка находится в состоянии мута с простым звуковым оповещением, теперь не будет сигналить после каждое перезагрузки страницы лишь в том случае, если в чате последнее сообщение осталось всё тоже неизменное после множества других перезагрузок страницы. Будет сигналить, если последнее сообщение отличительное с последнего посещения страницы с чатом. Так вы будете знать о том, что новые сообщения в чате появились. скрытый текст… // ==UserScript== // @name KG_Chat_Users_Tracker // @namespace http://tampermonkey.net/ // @version 0.1 // @description Count how much users are in chat and notify who entered and left the chat // @author Patcher // @match *://klavogonki.ru/g* // @grant none // ==/UserScript==
(function () { // SOUND NOTIFICATION // Note values and their corresponding frequencies // C0 to B8 const notesToFrequency = {}; for (let i = 0; i < 88; i++) { const note = i - 48; const frequency = Math.pow(2, (note - 9) / 12) * 440; notesToFrequency[i] = frequency; }
// List of notes to play for "User Left" && "User Entered" && "New Messages" const userEnteredNotes = [48, 60]; // C4, C5 const userLeftNotes = [60, 48]; // C5, C4 const newMessageNotes = [65];
// Volume and duration settings const volumeEntered = 0.35; const volumeLeft = 0.35; const volumeNewMessage = 0.35; const duration = 80; const fadeTime = 30;
// Function to play a beep given a list of notes and a volume async function playBeep(notes, volume) { const context = new AudioContext(); for (const note of notes) { if (note === 0) { // Rest note await new Promise((resolve) => setTimeout(resolve, duration)); } else { // Play note const oscillator = context.createOscillator(); const gain = context.createGain(); oscillator.connect(gain); oscillator.frequency.value = notesToFrequency[note]; oscillator.type = "triangle";
// Create low pass filter to cut frequencies below 250Hz const lowPassFilter = context.createBiquadFilter(); lowPassFilter.type = 'lowpass'; lowPassFilter.frequency.value = 250; oscillator.connect(lowPassFilter);
// Create high pass filter to cut frequencies above 16kHz const highPassFilter = context.createBiquadFilter(); highPassFilter.type = 'highpass'; highPassFilter.frequency.value = 16000; lowPassFilter.connect(highPassFilter);
gain.connect(context.destination); gain.gain.setValueAtTime(0, context.currentTime); gain.gain.linearRampToValueAtTime(volume, context.currentTime + fadeTime / 1000); oscillator.start(context.currentTime); oscillator.stop(context.currentTime + duration * 0.001); gain.gain.setValueAtTime(volume, context.currentTime + (duration - fadeTime) / 1000); gain.gain.linearRampToValueAtTime(0, context.currentTime + duration / 1000); await new Promise((resolve) => setTimeout(resolve, duration)); } } }
// Text to speech function textToSpeech(text) { // Replace underscores with spaces and match only letters const lettersOnly = text.replace(/_/g, ' ').replace(/[^a-zA-Zа-яА-Я ]/g, '');
const utterance = new SpeechSynthesisUtterance(lettersOnly); utterance.lang = 'ru-RU'; utterance.voice = speechSynthesis.getVoices().find((voice) => voice.name === 'Microsoft Pavel - Russian (Russia)');
speechSynthesis.speak(utterance); }
// Define the users to track and notify with popup and audio const usersToTrack = [ { name: 'Даниэль', gender: 'male' }, { name: 'певец', gender: 'male' }, { name: 'ВеликийИнка', gender: 'male' }, { name: 'madinko', gender: 'female' }, { name: 'Переборыч', gender: 'male' }, { name: 'Advisor', gender: 'male' }, { name: 'Хеопс', gender: 'male' } ];
const verbs = { male: { enter: 'зашёл', leave: 'вышел' }, female: { enter: 'зашла', leave: 'вышла' } };
function getUserGender(userName) { const user = usersToTrack.find((user) => user.name === userName); return user ? user.gender : null; }
// Functions to play beep for user entering and leaving function userEntered(user) { playBeep(userEnteredNotes, volumeEntered); const userGender = getUserGender(user); const action = verbs[userGender].enter; const message = `${user} ${action}`; setTimeout(() => { textToSpeech(message); }, 300); }
function userLeft(user) { playBeep(userLeftNotes, volumeLeft); const userGender = getUserGender(user); const action = verbs[userGender].leave; const message = `${user} ${action}`; setTimeout(() => { textToSpeech(message); }, 300); }
// POPUPS // Define the function to generate HSL color with user parameters for hue, saturation, lightness function getHSLColor(hue, saturation, lightness) { // Set default value for hue if (typeof hue === 'undefined') { hue = 180; } // Set default value for saturation if (typeof saturation === 'undefined') { saturation = 50; } // Set default value for lightness if (typeof lightness === 'undefined') { lightness = 50; } var color = `hsl(${hue},${saturation}%,${lightness}%)`; return color; }
// Reference for the existing popup let previousPopup = null;
function showUserAction(user, action, presence) { const userPopup = document.createElement('div'); userPopup.classList.add('userPopup'); userPopup.innerText = `${user} ${action}`;
// Set the initial styles for the user popup userPopup.style.position = 'fixed'; userPopup.style.right = '-100%'; userPopup.style.transform = 'translateY(-50%)'; userPopup.style.opacity = '0'; userPopup.style.color = presence ? getHSLColor(100, 50, 50) : getHSLColor(0, 50, 70); // fontColor green && red userPopup.style.backgroundColor = presence ? getHSLColor(100, 50, 10) : getHSLColor(0, 50, 15); // backgroundColor green && red userPopup.style.border = presence ? `1px solid ${getHSLColor(100, 50, 25)}` : `1px solid ${getHSLColor(0, 50, 40)}`; // borderColor green && red userPopup.style.setProperty('border-radius', '4px 0 0 4px', 'important'); userPopup.style.padding = '8px 16px'; userPopup.style.display = 'flex'; userPopup.style.alignItems = 'center';
// Append the user popup to the body document.body.appendChild(userPopup);
// Calculate the width and height of the user popup const popupWidth = userPopup.offsetWidth; const popupHeight = userPopup.offsetHeight; const verticalOffset = 2;
// Set the position of the user popup relative to the previous popup let topPosition = '30vh'; if (previousPopup !== null) { const previousPopupPosition = previousPopup.getBoundingClientRect(); topPosition = `calc(${previousPopupPosition.bottom}px + ${popupHeight}px / 2 + ${verticalOffset}px)`; } userPopup.style.top = topPosition; userPopup.style.right = `-${popupWidth}px`;
// Animate the user popup onto the screen userPopup.style.transition = 'all 0.3s ease-in-out'; userPopup.style.right = '0'; userPopup.style.opacity = '1';
// Store a reference to the current popup previousPopup = userPopup;
// Hide the user popup after a short delay setTimeout(() => { userPopup.style.transition = 'all 0.3s ease-in-out'; userPopup.style.right = `-${popupWidth}px`; userPopup.style.opacity = '0'; setTimeout(() => { document.body.removeChild(userPopup); // Clear the reference to the previous popup if (previousPopup === userPopup) { previousPopup = null; } }, 300); }, 5000); }
// FUNCTIONALITY // Define references to retrieve and create const userList = document.querySelector('.userlist-content'); const userCount = document.createElement('div'); userCount.classList.add('user-count'); userCount.style.filter = 'grayscale(100%)'; userCount.innerHTML = '0'; document.body.appendChild(userCount);
// Initialize variables to keep track of the current and previous users let currentUsers = []; let previousUsers = []; let hasObservedChanges = false; let prevUserCountValue = 0;
// Initialize variables for the user count animation let currentTextContent = []; let isAnimating = false;
// Mutation observer to track all the users with only graphical popup notification // Also play notification sound "Left" or "Entered" if the one of them is identical from "usersToTrack" array // Create a mutation observer to detect when the user list is modified const observeUsers = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { // Retrieve all users textContent from userList ins elements const newUserList = Array.from(userList.children).map(child => child.textContent);
// Find new users and left users const newUsers = newUserList.filter(user => !currentUsers.includes(user)); const leftUsers = currentUsers.filter(user => !newUserList.includes(user));
// Retrieve fresh user count length const userCountValue = newUserList.length; // Retrieve the counter element const userCount = document.querySelector('.user-count');
// Update grayscale filter userCount.style.filter = userCountValue > 0 ? 'none' : 'grayscale(100%)';
// Check if the user count animation needs to be started if (currentTextContent.length === 0 && newUserList.length > 0 && !isAnimating) { isAnimating = true; const actualUserCount = newUserList.length; const speed = 20; // Change the speed here (in milliseconds) let count = 0; const userCountIncrement = () => { if (count <= actualUserCount) { const progress = count / actualUserCount; const grayscale = 100 - progress * 100; userCount.innerHTML = `${count++}`; userCount.style.filter = `grayscale(${grayscale}%)`; setTimeout(userCountIncrement, speed); } else { currentTextContent = Array.from(userList.children).map(child => child.textContent); userCount.style.filter = 'none'; userCount.classList.add('pulse'); setTimeout(() => { userCount.classList.remove('pulse'); isAnimating = false; // set isAnimating to false after the animation }, 1000); } }; setTimeout(userCountIncrement, speed); } // Animation END
// Check only after the animation is end if (!isAnimating) { // Check if the user count has changed and add pulse animation if (userCountValue !== prevUserCountValue) { userCount.classList.add('pulse'); // Updating the counter element value userCount.innerHTML = userCountValue; setTimeout(() => { userCount.classList.remove('pulse'); }, 1000); } }
// Log new and left users if (hasObservedChanges) { newUsers.forEach((newUser) => { if (!previousUsers.includes(newUser)) { const userGender = getUserGender(newUser) || 'male'; // use 'male' as default const action = verbs[userGender].enter; showUserAction(newUser, action, true); if (usersToTrack.some(user => user.name === newUser)) { userEntered(newUser, userGender); } } });
leftUsers.forEach((leftUser) => { const userGender = getUserGender(leftUser) || 'male'; // use 'male' as default const action = verbs[userGender].leave; showUserAction(leftUser, action, false); if (usersToTrack.some(user => user.name === leftUser)) { userLeft(leftUser, userGender); } }); } else { hasObservedChanges = true; }
// Update the previous users and user count previousUsers = currentUsers; currentUsers = newUserList; prevUserCountValue = userCountValue;
} }); });
// Start observing users const config = { childList: true }; observeUsers.observe(userList, config);
// STYLIZATION const styles = ` @import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');
.user-count { font-family: 'Orbitron', sans-serif; font-size: 24px; color: #83cf40; position: fixed; top: 130px; right: 24px; background-color: #2b4317; width: 48px; height: 48px; display: flex; justify-content: center; align-items: center; border: 1px solid #4b7328; transition: filter 0.2s ease-in-out; }
.pulse { animation-name: pulse; animation-duration: 1s; animation-iteration-count: 1; }
@keyframes pulse { 0% { filter: brightness(1); } 50% { filter: brightness(1.5); } 100% { filter: brightness(1); } } `;
const styleElement = document.createElement('style'); styleElement.textContent = styles; document.head.appendChild(styleElement);
// EVERY NEW MESSAGE READER // Avoid reading on load page to read the messages normally on stable presence let isInitialized = false;
// create a mutation observer to watch for new messages being added const newMessagesObserver = new MutationObserver(mutations => { for (let mutation of mutations) { if (mutation.type === 'childList') { for (let node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'P') { // read the text content of the new message and speak it const latestMessageTextContent = localStorage.getItem('latestMessageTextContent'); const newMessageTextContent = getLatestMessageTextContent();
// Get the sound switcher element and check if it's muted or not const soundSwitcher = document.querySelector('#unmuted, #muted'); const isMuted = soundSwitcher && soundSwitcher.id === 'muted';
// If not muted, speak the new message and update the latest message content in local storage if (!isMuted && isInitialized && newMessageTextContent && newMessageTextContent !== latestMessageTextContent) { const utterance = new SpeechSynthesisUtterance(newMessageTextContent); utterance.lang = 'ru-RU'; utterance.voice = speechSynthesis.getVoices().find(voice => voice.name === 'Microsoft Pavel - Russian (Russia)'); speechSynthesis.speak(utterance); localStorage.setItem('latestMessageTextContent', newMessageTextContent); }
// If it's the first time, update the latest message content in local storage and set isInitialized to true if (!isInitialized) { localStorage.setItem('latestMessageTextContent', newMessageTextContent); isInitialized = true; }
// If is muted, play the beep sound for the new message if (isMuted && isInitialized && newMessageTextContent && newMessageTextContent !== latestMessageTextContent) { playBeep(newMessageNotes, volumeNewMessage); localStorage.setItem('latestMessageTextContent', newMessageTextContent); } } } } } });
// function to get the cleaned text content of the latest message function getLatestMessageTextContent() { const message = document.querySelector('.messages-content div p:last-child'); if (!message) { return null; } const textNodes = [...message.childNodes].filter(node => node.nodeType === Node.TEXT_NODE); const text = textNodes.map(node => node.textContent).join('').trim(); const time = message.querySelector('.time'); const username = message.querySelector('.username'); const timeText = time ? time.textContent : ''; const usernameText = username ? username.textContent : ''; return text.replace(timeText, '').replace(usernameText, '').trim(); }
// observe changes to the messages container element const messagesContainer = document.querySelector('.messages-content div'); newMessagesObserver.observe(messagesContainer, { childList: true, subtree: true });
// SOUND GRAPHICAL SWITCHER // New message sound notification graphical switcher as a button const chatButtonsPanel = document.querySelector('.chat .messages table td:nth-child(3)'); // Avoid panel squeezing chatButtonsPanel.style.minWidth = '75px'; const soundSwitcher = document.createElement('div');
// Check if the user has previously muted the sound const newMessageIsMuted = localStorage.getItem('newMessageIsMuted') === 'true';
soundSwitcher.classList.add('chat-opt-btn'); soundSwitcher.id = newMessageIsMuted ? 'muted' : 'unmuted'; soundSwitcher.addEventListener('click', function () { if (this.id === 'unmuted') { this.id = 'muted'; localStorage.setItem('newMessageIsMuted', 'true'); } else { this.id = 'unmuted'; localStorage.setItem('newMessageIsMuted', 'false'); } updateSoundIcon(); });
const soundIcon = document.createElement('span'); soundIcon.classList.add('sound-icon'); soundIcon.textContent = newMessageIsMuted ? '
|
Душа_Чата
|
Сообщение #4
3 апреля 2023 в 20:28
|
Гонщик
1 |
Добавляю версию скрипта, где кнопка динамика для переключения режимов зачитывания последнего сообщения и оповещения звуковым сопровождением оформлены в соответствии с другими кнопками чата как реализовано в тёмной теме для расширения stylus. Иконки стали линейными и обладают разными цветами. Зелёный и красный. Код скрытый текст… // ==UserScript== // @name KG_Chat_Users_Tracker // @namespace http://tampermonkey.net/ // @version 0.1 // @description Count how much users are in chat and notify who entered and left the chat // @author Patcher // @match *://klavogonki.ru/g* // @grant none // ==/UserScript==
(function () { // SOUND NOTIFICATION // Note values and their corresponding frequencies // C0 to B8 const notesToFrequency = {}; for (let i = 0; i < 88; i++) { const note = i - 48; const frequency = Math.pow(2, (note - 9) / 12) * 440; notesToFrequency[i] = frequency; }
// List of notes to play for "User Left" && "User Entered" && "New Messages" const userEnteredNotes = [48, 60]; // C4, C5 const userLeftNotes = [60, 48]; // C5, C4 const newMessageNotes = [65];
// Volume and duration settings const volumeEntered = 0.35; const volumeLeft = 0.35; const volumeNewMessage = 0.35; const duration = 80; const fadeTime = 30;
// Function to play a beep given a list of notes and a volume async function playBeep(notes, volume) { const context = new AudioContext(); for (const note of notes) { if (note === 0) { // Rest note await new Promise((resolve) => setTimeout(resolve, duration)); } else { // Play note const oscillator = context.createOscillator(); const gain = context.createGain(); oscillator.connect(gain); oscillator.frequency.value = notesToFrequency[note]; oscillator.type = "triangle";
// Create low pass filter to cut frequencies below 250Hz const lowPassFilter = context.createBiquadFilter(); lowPassFilter.type = 'lowpass'; lowPassFilter.frequency.value = 250; oscillator.connect(lowPassFilter);
// Create high pass filter to cut frequencies above 16kHz const highPassFilter = context.createBiquadFilter(); highPassFilter.type = 'highpass'; highPassFilter.frequency.value = 16000; lowPassFilter.connect(highPassFilter);
gain.connect(context.destination); gain.gain.setValueAtTime(0, context.currentTime); gain.gain.linearRampToValueAtTime(volume, context.currentTime + fadeTime / 1000); oscillator.start(context.currentTime); oscillator.stop(context.currentTime + duration * 0.001); gain.gain.setValueAtTime(volume, context.currentTime + (duration - fadeTime) / 1000); gain.gain.linearRampToValueAtTime(0, context.currentTime + duration / 1000); await new Promise((resolve) => setTimeout(resolve, duration)); } } }
// Text to speech function textToSpeech(text) { // Replace underscores with spaces and match only letters const lettersOnly = text.replace(/_/g, ' ').replace(/[^a-zA-Zа-яА-Я ]/g, '');
const utterance = new SpeechSynthesisUtterance(lettersOnly); utterance.lang = 'ru-RU'; utterance.voice = speechSynthesis.getVoices().find((voice) => voice.name === 'Microsoft Pavel - Russian (Russia)');
speechSynthesis.speak(utterance); }
// Define the users to track and notify with popup and audio const usersToTrack = [ { name: 'Даниэль', gender: 'male' }, { name: 'певец', gender: 'male' }, { name: 'ВеликийИнка', gender: 'male' }, { name: 'madinko', gender: 'female' }, { name: 'Переборыч', gender: 'male' }, { name: 'Advisor', gender: 'male' }, { name: 'Хеопс', gender: 'male' } ];
const verbs = { male: { enter: 'зашёл', leave: 'вышел' }, female: { enter: 'зашла', leave: 'вышла' } };
function getUserGender(userName) { const user = usersToTrack.find((user) => user.name === userName); return user ? user.gender : null; }
// Functions to play beep for user entering and leaving function userEntered(user) { playBeep(userEnteredNotes, volumeEntered); const userGender = getUserGender(user); const action = verbs[userGender].enter; const message = `${user} ${action}`; setTimeout(() => { textToSpeech(message); }, 300); }
function userLeft(user) { playBeep(userLeftNotes, volumeLeft); const userGender = getUserGender(user); const action = verbs[userGender].leave; const message = `${user} ${action}`; setTimeout(() => { textToSpeech(message); }, 300); }
// POPUPS // Define the function to generate HSL color with user parameters for hue, saturation, lightness function getHSLColor(hue, saturation, lightness) { // Set default value for hue if (typeof hue === 'undefined') { hue = 180; } // Set default value for saturation if (typeof saturation === 'undefined') { saturation = 50; } // Set default value for lightness if (typeof lightness === 'undefined') { lightness = 50; } var color = `hsl(${hue},${saturation}%,${lightness}%)`; return color; }
// Reference for the existing popup let previousPopup = null;
function showUserAction(user, action, presence) { const userPopup = document.createElement('div'); userPopup.classList.add('userPopup'); userPopup.innerText = `${user} ${action}`;
// Set the initial styles for the user popup userPopup.style.position = 'fixed'; userPopup.style.right = '-100%'; userPopup.style.transform = 'translateY(-50%)'; userPopup.style.opacity = '0'; userPopup.style.color = presence ? getHSLColor(100, 50, 50) : getHSLColor(0, 50, 70); // fontColor green && red userPopup.style.backgroundColor = presence ? getHSLColor(100, 50, 10) : getHSLColor(0, 50, 15); // backgroundColor green && red userPopup.style.border = presence ? `1px solid ${getHSLColor(100, 50, 25)}` : `1px solid ${getHSLColor(0, 50, 40)}`; // borderColor green && red userPopup.style.setProperty('border-radius', '4px 0 0 4px', 'important'); userPopup.style.padding = '8px 16px'; userPopup.style.display = 'flex'; userPopup.style.alignItems = 'center';
// Append the user popup to the body document.body.appendChild(userPopup);
// Calculate the width and height of the user popup const popupWidth = userPopup.offsetWidth; const popupHeight = userPopup.offsetHeight; const verticalOffset = 2;
// Set the position of the user popup relative to the previous popup let topPosition = '30vh'; if (previousPopup !== null) { const previousPopupPosition = previousPopup.getBoundingClientRect(); topPosition = `calc(${previousPopupPosition.bottom}px + ${popupHeight}px / 2 + ${verticalOffset}px)`; } userPopup.style.top = topPosition; userPopup.style.right = `-${popupWidth}px`;
// Animate the user popup onto the screen userPopup.style.transition = 'all 0.3s ease-in-out'; userPopup.style.right = '0'; userPopup.style.opacity = '1';
// Store a reference to the current popup previousPopup = userPopup;
// Hide the user popup after a short delay setTimeout(() => { userPopup.style.transition = 'all 0.3s ease-in-out'; userPopup.style.right = `-${popupWidth}px`; userPopup.style.opacity = '0'; setTimeout(() => { document.body.removeChild(userPopup); // Clear the reference to the previous popup if (previousPopup === userPopup) { previousPopup = null; } }, 300); }, 5000); }
// FUNCTIONALITY // Define references to retrieve and create const userList = document.querySelector('.userlist-content'); const userCount = document.createElement('div'); userCount.classList.add('user-count'); userCount.style.filter = 'grayscale(100%)'; userCount.innerHTML = '0'; document.body.appendChild(userCount);
// Initialize variables to keep track of the current and previous users let currentUsers = []; let previousUsers = []; let hasObservedChanges = false; let prevUserCountValue = 0;
// Initialize variables for the user count animation let currentTextContent = []; let isAnimating = false;
// Mutation observer to track all the users with only graphical popup notification // Also play notification sound "Left" or "Entered" if the one of them is identical from "usersToTrack" array // Create a mutation observer to detect when the user list is modified const observeUsers = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { // Retrieve all users textContent from userList ins elements const newUserList = Array.from(userList.children).map(child => child.textContent);
// Find new users and left users const newUsers = newUserList.filter(user => !currentUsers.includes(user)); const leftUsers = currentUsers.filter(user => !newUserList.includes(user));
// Retrieve fresh user count length const userCountValue = newUserList.length; // Retrieve the counter element const userCount = document.querySelector('.user-count');
// Update grayscale filter userCount.style.filter = userCountValue > 0 ? 'none' : 'grayscale(100%)';
// Check if the user count animation needs to be started if (currentTextContent.length === 0 && newUserList.length > 0 && !isAnimating) { isAnimating = true; const actualUserCount = newUserList.length; const speed = 20; // Change the speed here (in milliseconds) let count = 0; const userCountIncrement = () => { if (count <= actualUserCount) { const progress = count / actualUserCount; const grayscale = 100 - progress * 100; userCount.innerHTML = `${count++}`; userCount.style.filter = `grayscale(${grayscale}%)`; setTimeout(userCountIncrement, speed); } else { currentTextContent = Array.from(userList.children).map(child => child.textContent); userCount.style.filter = 'none'; userCount.classList.add('pulse'); setTimeout(() => { userCount.classList.remove('pulse'); isAnimating = false; // set isAnimating to false after the animation }, 1000); } }; setTimeout(userCountIncrement, speed); } // Animation END
// Check only after the animation is end if (!isAnimating) { // Check if the user count has changed and add pulse animation if (userCountValue !== prevUserCountValue) { userCount.classList.add('pulse'); // Updating the counter element value userCount.innerHTML = userCountValue; setTimeout(() => { userCount.classList.remove('pulse'); }, 1000); } }
// Log new and left users if (hasObservedChanges) { newUsers.forEach((newUser) => { if (!previousUsers.includes(newUser)) { const userGender = getUserGender(newUser) || 'male'; // use 'male' as default const action = verbs[userGender].enter; showUserAction(newUser, action, true); if (usersToTrack.some(user => user.name === newUser)) { userEntered(newUser, userGender); } } });
leftUsers.forEach((leftUser) => { const userGender = getUserGender(leftUser) || 'male'; // use 'male' as default const action = verbs[userGender].leave; showUserAction(leftUser, action, false); if (usersToTrack.some(user => user.name === leftUser)) { userLeft(leftUser, userGender); } }); } else { hasObservedChanges = true; }
// Update the previous users and user count previousUsers = currentUsers; currentUsers = newUserList; prevUserCountValue = userCountValue;
} }); });
// Start observing users const config = { childList: true }; observeUsers.observe(userList, config);
// STYLIZATION const styles = ` @import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');
.user-count { font-family: 'Orbitron', sans-serif; font-size: 24px; color: #83cf40; position: fixed; top: 130px; right: 24px; background-color: #2b4317; width: 48px; height: 48px; display: flex; justify-content: center; align-items: center; border: 1px solid #4b7328; transition: filter 0.2s ease-in-out; }
.pulse { animation-name: pulse; animation-duration: 1s; animation-iteration-count: 1; }
@keyframes pulse { 0% { filter: brightness(1); } 50% { filter: brightness(1.5); } 100% { filter: brightness(1); } } `;
const styleElement = document.createElement('style'); styleElement.textContent = styles; document.head.appendChild(styleElement);
// EVERY NEW MESSAGE READER // Avoid reading on load page to read the messages normally on stable presence let isInitialized = false;
// create a mutation observer to watch for new messages being added const newMessagesObserver = new MutationObserver(mutations => { for (let mutation of mutations) { if (mutation.type === 'childList') { for (let node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'P') { // read the text content of the new message and speak it const latestMessageTextContent = localStorage.getItem('latestMessageTextContent'); const newMessageTextContent = getLatestMessageTextContent();
// Get the sound switcher element and check if it's muted or not const soundSwitcher = document.querySelector('#unmuted, #muted'); const isMuted = soundSwitcher && soundSwitcher.id === 'muted';
// If not muted, speak the new message and update the latest message content in local storage if (!isMuted && isInitialized && newMessageTextContent && newMessageTextContent !== latestMessageTextContent) { const utterance = new SpeechSynthesisUtterance(newMessageTextContent); utterance.lang = 'ru-RU'; utterance.voice = speechSynthesis.getVoices().find(voice => voice.name === 'Microsoft Pavel - Russian (Russia)'); speechSynthesis.speak(utterance); localStorage.setItem('latestMessageTextContent', newMessageTextContent); }
// If it's the first time, update the latest message content in local storage and set isInitialized to true if (!isInitialized) { localStorage.setItem('latestMessageTextContent', newMessageTextContent); isInitialized = true; }
// If is muted, play the beep sound for the new message if (isMuted && isInitialized && newMessageTextContent && newMessageTextContent !== latestMessageTextContent) { playBeep(newMessageNotes, volumeNewMessage); localStorage.setItem('latestMessageTextContent', newMessageTextContent); } } } } } });
// function to get the cleaned text content of the latest message function getLatestMessageTextContent() { const message = document.querySelector('.messages-content div p:last-child'); if (!message) { return null; } const textNodes = [...message.childNodes].filter(node => node.nodeType === Node.TEXT_NODE); const text = textNodes.map(node => node.textContent).join('').trim(); const time = message.querySelector('.time'); const username = message.querySelector('.username'); const timeText = time ? time.textContent : ''; const usernameText = username ? username.textContent : ''; return text.replace(timeText, '').replace(usernameText, '').trim(); }
// observe changes to the messages container element const messagesContainer = document.querySelector('.messages-content div'); newMessagesObserver.observe(messagesContainer, { childList: true, subtree: true });
// SOUND GRAPHICAL SWITCHER // Button SVG icons muted and unmuted representation const iconSoundMuted = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="hsl(355, 80%, 65%)" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-x"> <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> <line x1="23" y1="9" x2="17" y2="15"></line> <line x1="17" y1="9" x2="23" y2="15"></line> </svg>`; const iconSoundUnmuted = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="hsl(80, 80%, 40%)" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-2"> <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path> </svg>`; // New message sound notification graphical switcher as a button const chatButtonsPanel = document.querySelector('.chat .messages table td:nth-child(3)'); // Avoid panel squeezing chatButtonsPanel.style.minWidth = '105px'; const soundSwitcher = document.createElement('div');
// Check if the user has previously muted the sound const newMessageIsMuted = localStorage.getItem('newMessageIsMuted') === 'true';
soundSwitcher.classList.add('chat-opt-btn'); soundSwitcher.id = newMessageIsMuted ? 'muted' : 'unmuted'; soundSwitcher.addEventListener('click', function () { if (this.id === 'unmuted') { this.id = 'muted'; localStorage.setItem('newMessageIsMuted', 'true'); } else { this.id = 'unmuted'; localStorage.setItem('newMessageIsMuted', 'false'); } updateSoundIcon(); });
const soundIcon = document.createElement('span'); soundIcon.classList.add('sound-icon'); // iconSoundMuted >
|
Вова_10
|
Сообщение #5
3 апреля 2023 в 20:39
|
Виртуоз
32 |
Ссылку на скрипт дайте, пожалуйста, чтобы его установить
|
Душа_Чата
|
Сообщение #6
3 апреля 2023 в 21:29
|
Гонщик
1 |
Правки правки правки и ещё раз правки. Нет предела совершенству. Правки больше касаемо оптимизации и справления некоторых ошибок. А исправлено вот что: 1. После каждой перезагрузки страницы один раз мелькает дефолтный женской голос, так как объявляется voice API не совсем корректно и не очень хорошо с точки зрения оптимизированного кода. Так-же в прошлых версиях было объявлено два разных голосовых процессора, когда сейчас объединилось в одну функцию и объявление всех голосовых настроен назначаются единожды после загрузки страницы, нежели как раньше по замечанию изменений в DOM дереве, каждый раз голосовой движок создавался с нуля, что сказывается на производительность. А вот и сама функция и вынесенные глобально объявления голосового процессора. скрытый текст… // define the voice for text to speech const voice = speechSynthesis.getVoices().find((voice) => voice.name === 'Microsoft Pavel - Russian (Russia)');
// define the utterance object const utterance = new SpeechSynthesisUtterance(); utterance.lang = 'ru-RU'; utterance.voice = voice;
// Text to speech function function textToSpeech(text) { // Replace underscores with spaces and match only letters const lettersOnly = text.replace(/_/g, ' ').replace(/[^a-zA-Zа-яА-Я ]/g, '');
// set the text content of the utterance utterance.text = lettersOnly;
// speak the utterance speechSynthesis.speak(utterance); } Теперь одна функция работает как с оповещениями о зашедших и вышедших пользователях, так и читает последние сообщения в чате всегда одним мужским голосом (Microsoft Pavel). Было бы очень здорово, чтобы голос читался голосовым процессором по типу нейронки, но там всё такое платное. Может быть кто подскажет, как быть в данной ситуации, чтобы улучить качество голоса. Может быть кто знает что-нибудь бесплатное и качественней для зачитывания текста? Код скрытый текст… // ==UserScript== // @name KG_Chat_Users_Tracker // @namespace http://tampermonkey.net/ // @version 0.1 // @description Count how much users are in chat and notify who entered and left the chat // @author Patcher // @match *://klavogonki.ru/g* // @grant none // ==/UserScript==
(function () { // SOUND NOTIFICATION // Note values and their corresponding frequencies // C0 to B8 const notesToFrequency = {}; for (let i = 0; i < 88; i++) { const note = i - 48; const frequency = Math.pow(2, (note - 9) / 12) * 440; notesToFrequency[i] = frequency; }
// List of notes to play for "User Left" && "User Entered" && "New Messages" const userEnteredNotes = [48, 60]; // C4, C5 const userLeftNotes = [60, 48]; // C5, C4 const newMessageNotes = [65];
// Volume and duration settings const volumeEntered = 0.35; const volumeLeft = 0.35; const volumeNewMessage = 0.35; const duration = 80; const fadeTime = 30;
// Function to play a beep given a list of notes and a volume async function playBeep(notes, volume) { const context = new AudioContext(); for (const note of notes) { if (note === 0) { // Rest note await new Promise((resolve) => setTimeout(resolve, duration)); } else { // Play note const oscillator = context.createOscillator(); const gain = context.createGain(); oscillator.connect(gain); oscillator.frequency.value = notesToFrequency[note]; oscillator.type = "triangle";
// Create low pass filter to cut frequencies below 250Hz const lowPassFilter = context.createBiquadFilter(); lowPassFilter.type = 'lowpass'; lowPassFilter.frequency.value = 250; oscillator.connect(lowPassFilter);
// Create high pass filter to cut frequencies above 16kHz const highPassFilter = context.createBiquadFilter(); highPassFilter.type = 'highpass'; highPassFilter.frequency.value = 16000; lowPassFilter.connect(highPassFilter);
gain.connect(context.destination); gain.gain.setValueAtTime(0, context.currentTime); gain.gain.linearRampToValueAtTime(volume, context.currentTime + fadeTime / 1000); oscillator.start(context.currentTime); oscillator.stop(context.currentTime + duration * 0.001); gain.gain.setValueAtTime(volume, context.currentTime + (duration - fadeTime) / 1000); gain.gain.linearRampToValueAtTime(0, context.currentTime + duration / 1000); await new Promise((resolve) => setTimeout(resolve, duration)); } } }
// define the voice for text to speech const voice = speechSynthesis.getVoices().find((voice) => voice.name === 'Microsoft Pavel - Russian (Russia)');
// define the utterance object const utterance = new SpeechSynthesisUtterance(); utterance.lang = 'ru-RU'; utterance.voice = voice;
// Text to speech function function textToSpeech(text) { // Replace underscores with spaces and match only letters const lettersOnly = text.replace(/_/g, ' ').replace(/[^a-zA-Zа-яА-Я ]/g, '');
// set the text content of the utterance utterance.text = lettersOnly;
// speak the utterance speechSynthesis.speak(utterance); }
// Define the users to track and notify with popup and audio const usersToTrack = [ { name: 'Даниэль', gender: 'male' }, { name: 'певец', gender: 'male' }, { name: 'ВеликийИнка', gender: 'male' }, { name: 'madinko', gender: 'female' }, { name: 'Переборыч', gender: 'male' }, { name: 'Advisor', gender: 'male' }, { name: 'Хеопс', gender: 'male' } ];
const verbs = { male: { enter: 'зашёл', leave: 'вышел' }, female: { enter: 'зашла', leave: 'вышла' } };
function getUserGender(userName) { const user = usersToTrack.find((user) => user.name === userName); return user ? user.gender : null; }
// Functions to play beep for user entering and leaving function userEntered(user) { playBeep(userEnteredNotes, volumeEntered); const userGender = getUserGender(user); const action = verbs[userGender].enter; const message = `${user} ${action}`; setTimeout(() => { textToSpeech(message); }, 300); }
function userLeft(user) { playBeep(userLeftNotes, volumeLeft); const userGender = getUserGender(user); const action = verbs[userGender].leave; const message = `${user} ${action}`; setTimeout(() => { textToSpeech(message); }, 300); }
// POPUPS // Define the function to generate HSL color with user parameters for hue, saturation, lightness function getHSLColor(hue, saturation, lightness) { // Set default value for hue if (typeof hue === 'undefined') { hue = 180; } // Set default value for saturation if (typeof saturation === 'undefined') { saturation = 50; } // Set default value for lightness if (typeof lightness === 'undefined') { lightness = 50; } var color = `hsl(${hue},${saturation}%,${lightness}%)`; return color; }
// Reference for the existing popup let previousPopup = null;
function showUserAction(user, action, presence) { const userPopup = document.createElement('div'); userPopup.classList.add('userPopup'); userPopup.innerText = `${user} ${action}`;
// Set the initial styles for the user popup userPopup.style.position = 'fixed'; userPopup.style.right = '-100%'; userPopup.style.transform = 'translateY(-50%)'; userPopup.style.opacity = '0'; userPopup.style.color = presence ? getHSLColor(100, 50, 50) : getHSLColor(0, 50, 70); // fontColor green && red userPopup.style.backgroundColor = presence ? getHSLColor(100, 50, 10) : getHSLColor(0, 50, 15); // backgroundColor green && red userPopup.style.border = presence ? `1px solid ${getHSLColor(100, 50, 25)}` : `1px solid ${getHSLColor(0, 50, 40)}`; // borderColor green && red userPopup.style.setProperty('border-radius', '4px 0 0 4px', 'important'); userPopup.style.padding = '8px 16px'; userPopup.style.display = 'flex'; userPopup.style.alignItems = 'center';
// Append the user popup to the body document.body.appendChild(userPopup);
// Calculate the width and height of the user popup const popupWidth = userPopup.offsetWidth; const popupHeight = userPopup.offsetHeight; const verticalOffset = 2;
// Set the position of the user popup relative to the previous popup let topPosition = '30vh'; if (previousPopup !== null) { const previousPopupPosition = previousPopup.getBoundingClientRect(); topPosition = `calc(${previousPopupPosition.bottom}px + ${popupHeight}px / 2 + ${verticalOffset}px)`; } userPopup.style.top = topPosition; userPopup.style.right = `-${popupWidth}px`;
// Animate the user popup onto the screen userPopup.style.transition = 'all 0.3s ease-in-out'; userPopup.style.right = '0'; userPopup.style.opacity = '1';
// Store a reference to the current popup previousPopup = userPopup;
// Hide the user popup after a short delay setTimeout(() => { userPopup.style.transition = 'all 0.3s ease-in-out'; userPopup.style.right = `-${popupWidth}px`; userPopup.style.opacity = '0'; setTimeout(() => { document.body.removeChild(userPopup); // Clear the reference to the previous popup if (previousPopup === userPopup) { previousPopup = null; } }, 300); }, 5000); }
// FUNCTIONALITY // Define references to retrieve and create const userList = document.querySelector('.userlist-content'); const userCount = document.createElement('div'); userCount.classList.add('user-count'); userCount.style.filter = 'grayscale(100%)'; userCount.innerHTML = '0'; document.body.appendChild(userCount);
// Initialize variables to keep track of the current and previous users let currentUsers = []; let previousUsers = []; let hasObservedChanges = false; let prevUserCountValue = 0;
// Initialize variables for the user count animation let currentTextContent = []; let isAnimating = false;
// Mutation observer to track all the users with only graphical popup notification // Also play notification sound "Left" or "Entered" if the one of them is identical from "usersToTrack" array // Create a mutation observer to detect when the user list is modified const observeUsers = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { // Retrieve all users textContent from userList ins elements const newUserList = Array.from(userList.children).map(child => child.textContent);
// Find new users and left users const newUsers = newUserList.filter(user => !currentUsers.includes(user)); const leftUsers = currentUsers.filter(user => !newUserList.includes(user));
// Retrieve fresh user count length const userCountValue = newUserList.length; // Retrieve the counter element const userCount = document.querySelector('.user-count');
// Update grayscale filter userCount.style.filter = userCountValue > 0 ? 'none' : 'grayscale(100%)';
// Check if the user count animation needs to be started if (currentTextContent.length === 0 && newUserList.length > 0 && !isAnimating) { isAnimating = true; const actualUserCount = newUserList.length; const speed = 20; // Change the speed here (in milliseconds) let count = 0; const userCountIncrement = () => { if (count <= actualUserCount) { const progress = count / actualUserCount; const grayscale = 100 - progress * 100; userCount.innerHTML = `${count++}`; userCount.style.filter = `grayscale(${grayscale}%)`; setTimeout(userCountIncrement, speed); } else { currentTextContent = Array.from(userList.children).map(child => child.textContent); userCount.style.filter = 'none'; userCount.classList.add('pulse'); setTimeout(() => { userCount.classList.remove('pulse'); isAnimating = false; // set isAnimating to false after the animation }, 1000); } }; setTimeout(userCountIncrement, speed); } // Animation END
// Check only after the animation is end if (!isAnimating) { // Check if the user count has changed and add pulse animation if (userCountValue !== prevUserCountValue) { userCount.classList.add('pulse'); // Updating the counter element value userCount.innerHTML = userCountValue; setTimeout(() => { userCount.classList.remove('pulse'); }, 1000); } }
// Log new and left users if (hasObservedChanges) { newUsers.forEach((newUser) => { if (!previousUsers.includes(newUser)) { const userGender = getUserGender(newUser) || 'male'; // use 'male' as default const action = verbs[userGender].enter; showUserAction(newUser, action, true); if (usersToTrack.some(user => user.name === newUser)) { userEntered(newUser, userGender); } } });
leftUsers.forEach((leftUser) => { const userGender = getUserGender(leftUser) || 'male'; // use 'male' as default const action = verbs[userGender].leave; showUserAction(leftUser, action, false); if (usersToTrack.some(user => user.name === leftUser)) { userLeft(leftUser, userGender); } }); } else { hasObservedChanges = true; }
// Update the previous users and user count previousUsers = currentUsers; currentUsers = newUserList; prevUserCountValue = userCountValue;
} }); });
// Start observing users const config = { childList: true }; observeUsers.observe(userList, config);
// STYLIZATION const styles = ` @import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');
.user-count { font-family: 'Orbitron', sans-serif; font-size: 24px; color: #83cf40; position: fixed; top: 130px; right: 24px; background-color: #2b4317; width: 48px; height: 48px; display: flex; justify-content: center; align-items: center; border: 1px solid #4b7328; transition: filter 0.2s ease-in-out; }
.pulse { animation-name: pulse; animation-duration: 1s; animation-iteration-count: 1; }
@keyframes pulse { 0% { filter: brightness(1); } 50% { filter: brightness(1.5); } 100% { filter: brightness(1); } } `;
const styleElement = document.createElement('style'); styleElement.textContent = styles; document.head.appendChild(styleElement);
// EVERY NEW MESSAGE READER // Avoid reading on load page to read the messages normally on stable presence let isInitialized = false;
// create a mutation observer to watch for new messages being added const newMessagesObserver = new MutationObserver(mutations => { for (let mutation of mutations) { if (mutation.type === 'childList') { for (let node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'P') { // read the text content of the new message and speak it const latestMessageTextContent = localStorage.getItem('latestMessageTextContent'); const newMessageTextContent = getLatestMessageTextContent();
// Get the sound switcher element and check if it's muted or not const soundSwitcher = document.querySelector('#unmuted, #muted'); const isMuted = soundSwitcher && soundSwitcher.id === 'muted';
// If not muted, speak the new message and update the latest message content in local storage if (!isMuted && isInitialized && newMessageTextContent && newMessageTextContent !== latestMessageTextContent) { textToSpeech(newMessageTextContent); localStorage.setItem('latestMessageTextContent', newMessageTextContent); }
// If it's the first time, update the latest message content in local storage and set isInitialized to true if (!isInitialized) { localStorage.setItem('latestMessageTextContent', newMessageTextContent); isInitialized = true; }
// If is muted, play the beep sound for the new message if (isMuted && isInitialized && newMessageTextContent && newMessageTextContent !== latestMessageTextContent) { playBeep(newMessageNotes, volumeNewMessage); localStorage.setItem('latestMessageTextContent', newMessageTextContent); } } } } } });
// function to get the cleaned text content of the latest message function getLatestMessageTextContent() { const message = document.querySelector('.messages-content div p:last-child'); if (!message) { return null; } const textNodes = [...message.childNodes].filter(node => node.nodeType === Node.TEXT_NODE); const text = textNodes.map(node => node.textContent).join('').trim(); const time = message.querySelector('.time'); const username = message.querySelector('.username'); const timeText = time ? time.textContent : ''; const usernameText = username ? username.textContent : ''; return text.replace(timeText, '').replace(usernameText, '').trim(); }
// observe changes to the messages container element const messagesContainer = document.querySelector('.messages-content div'); newMessagesObserver.observe(messagesContainer, { childList: true, subtree: true });
// SOUND GRAPHICAL SWITCHER // Button SVG icons muted and unmuted representation const iconSoundMuted = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="hsl(355, 80%, 65%)" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-x"> <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> <line x1="23" y1="9" x2="17" y2="15"></line> <line x1="17" y1="9" x2="23" y2="15"></line> </svg>`; const iconSoundUnmuted = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="hsl(80, 80%, 40%)" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-2"> <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path> </svg>`; // New message sound notification graphical switcher as a button const chatButtonsPanel = document.querySelector('.chat .messages table td:nth-child(3)'); // Avoid panel squeezing chatButtonsPanel.style.minWidth = '105px'; const soundSwitcher = document.createElement('div');
// Check if the user has previously muted the sound const newMessageIsMuted = localStorage.getItem('newMessageIsMuted') === 'true';
soundSwitcher.classList.add('chat-opt-btn'); soundSwitcher.id = newMessageIsMuted ? 'muted' : 'unmuted'; soundSwitcher.addEventListener('click', function () { if (this.id === 'unmuted') { this.id = 'muted'; localStorage.setItem('newMessageIsMuted', 'true'); } else { this.id = 'unmuted'; localStorage.setItem('newMessageIsMuted', 'false'); } updateSoundIcon(); });
const soundIcon = document.createElement('span'); soundIcon.classList.add('sound-icon'); // iconSoundMuted >
|
Душа_Чата
|
Сообщение #7
3 апреля 2023 в 21:35
|
Гонщик
1 |
Вова_10 Вова_10 писал(а): Ссылку на скрипт дайте, пожалуйста, чтобы его установить Это очень просто. Необходимо для начала скачать расширение для браузера tampermonkeyА далее в настройках расширения на странице клавогонок создать новый скрипт нажав на эту кнопку Далее откроется окно с полем куда нужно с заменой всего что там увидишь самой последней версией кода из хайда И сохраняешь Ctrl + S
|
Душа_Чата
|
Сообщение #8
4 апреля 2023 в 01:14
|
Гонщик
1 |
Добавлена функция подсветки ников в списке чата для незабаненных и забаненных пользователей. Незабаненные светятся зелёным, а забаненные красным. Всё оптимально смотрится на тёмном фоне. Для тех кто сидит с дефолтной светлой темы, то придётся ковырнуть для этого цвета и сделать их темнее. Ковырять придётся эту функцию Менять нужно эти строки anchor.style.setProperty('color', '#83cf40', 'important'); anchor.style.setProperty('text-shadow', '0 0 1px #83cf40', 'important'); anchor.style.setProperty('color', '#ff8080', 'important'); anchor.style.setProperty('text-shadow', '0 0 1px #ff8080', 'important'); скрытый текст… // Function to highlight users from 'usersToTrack' array in the userlist // Also order online users at the top of the list // And offline users at the bottom of the list function highlightTrackingUsers() { // Select all ins elements from the userlist const insElements = document.querySelectorAll('.userlist-content ins');
// Iterate over the ins elements and check if they contain an anchor element for (const ins of insElements) { const anchor = ins.querySelector('a'); if (anchor) { // Retrieve the username from the anchor textContent const name = anchor.textContent.trim(); // Find the user in 'usersToTrack' array by their name const userToTrack = usersToTrack.find(user => user.name === name); // If the user is found and not revoked, set their anchor text color to green if (userToTrack && !ins.classList.contains('revoked')) { anchor.style.setProperty('color', '#83cf40', 'important'); anchor.style.setProperty('text-shadow', '0 0 1px #83cf40', 'important'); // If the tracked user is not already at the beginning of the list, move them there if (ins.parentNode.firstChild !== ins) { ins.parentNode.insertBefore(ins, ins.parentNode.firstChild); } } // If the user is found and is revoked, set their anchor text color to a red else if (userToTrack && ins.classList.contains('revoked')) { anchor.style.setProperty('color', '#ff8080', 'important'); anchor.style.setProperty('text-shadow', '0 0 1px #ff8080', 'important'); // If the tracked user is not already at the end of the list, move them there if (ins.parentNode.lastChild !== ins) { ins.parentNode.insertBefore(ins, null); } } } } } скрытый текст… // ==UserScript== // @name KG_Chat_Users_Tracker // @namespace http://tampermonkey.net/ // @version 0.1 // @description Count how much users are in chat and notify who entered and left the chat // @author Patcher // @match *://klavogonki.ru/g* // @grant none // ==/UserScript==
(function () { // SOUND NOTIFICATION // Note values and their corresponding frequencies // C0 to B8 const notesToFrequency = {}; for (let i = 0; i < 88; i++) { const note = i - 48; const frequency = Math.pow(2, (note - 9) / 12) * 440; notesToFrequency[i] = frequency; }
// List of notes to play for "User Left" && "User Entered" && "New Messages" const userEnteredNotes = [48, 60]; // C4, C5 const userLeftNotes = [60, 48]; // C5, C4 const newMessageNotes = [65];
// Volume and duration settings const volumeEntered = 0.35; const volumeLeft = 0.35; const volumeNewMessage = 0.35; const duration = 80; const fadeTime = 10;
// Function to play a beep given a list of notes and a volume async function playBeep(notes, volume) { const context = new AudioContext(); for (const note of notes) { if (note === 0) { // Rest note await new Promise((resolve) => setTimeout(resolve, duration)); } else { // Play note const oscillator = context.createOscillator(); const gain = context.createGain(); oscillator.connect(gain); oscillator.frequency.value = notesToFrequency[note]; oscillator.type = "triangle";
// Create low pass filter to cut frequencies below 250Hz const lowPassFilter = context.createBiquadFilter(); lowPassFilter.type = 'lowpass'; lowPassFilter.frequency.value = 250; oscillator.connect(lowPassFilter);
// Create high pass filter to cut frequencies above 16kHz const highPassFilter = context.createBiquadFilter(); highPassFilter.type = 'highpass'; highPassFilter.frequency.value = 16000; lowPassFilter.connect(highPassFilter);
gain.connect(context.destination); gain.gain.setValueAtTime(0, context.currentTime); gain.gain.linearRampToValueAtTime(volume, context.currentTime + fadeTime / 1000); oscillator.start(context.currentTime); oscillator.stop(context.currentTime + duration * 0.001); gain.gain.setValueAtTime(volume, context.currentTime + (duration - fadeTime) / 1000); gain.gain.linearRampToValueAtTime(0, context.currentTime + duration / 1000); await new Promise((resolve) => setTimeout(resolve, duration)); } } }
// define the voice for text to speech const voice = speechSynthesis.getVoices().find((voice) => voice.name === 'Microsoft Pavel - Russian (Russia)');
// define the utterance object const utterance = new SpeechSynthesisUtterance(); utterance.lang = 'ru-RU'; utterance.voice = voice;
// Text to speech function function textToSpeech(text) { // Replace underscores with spaces and match only letters const lettersOnly = text.replace(/_/g, ' ').replace(/[^a-zA-Zа-яА-Я ]/g, '');
// set the text content of the utterance utterance.text = lettersOnly;
// speak the utterance speechSynthesis.speak(utterance); }
// Define the users to track and notify with popup and audio const usersToTrack = [ { name: 'Даниэль', gender: 'male' }, { name: 'певец', gender: 'male' }, { name: 'ВеликийИнка', gender: 'male' }, { name: 'madinko', gender: 'female' }, { name: 'Переборыч', gender: 'male' }, { name: 'Advisor', gender: 'male' }, { name: 'Хеопс', gender: 'male' }, { name: 'Рустамко', gender: 'male' } ];
const verbs = { male: { enter: 'зашёл', leave: 'вышел' }, female: { enter: 'зашла', leave: 'вышла' } };
function getUserGender(userName) { const user = usersToTrack.find((user) => user.name === userName); return user ? user.gender : null; }
// Functions to play beep for user entering and leaving function userEntered(user) { playBeep(userEnteredNotes, volumeEntered); const userGender = getUserGender(user); const action = verbs[userGender].enter; const message = `${user} ${action}`; setTimeout(() => { textToSpeech(message); }, 300); }
function userLeft(user) { playBeep(userLeftNotes, volumeLeft); const userGender = getUserGender(user); const action = verbs[userGender].leave; const message = `${user} ${action}`; setTimeout(() => { textToSpeech(message); }, 300); }
// POPUPS // Define the function to generate HSL color with user parameters for hue, saturation, lightness function getHSLColor(hue, saturation, lightness) { // Set default value for hue if (typeof hue === 'undefined') { hue = 180; } // Set default value for saturation if (typeof saturation === 'undefined') { saturation = 50; } // Set default value for lightness if (typeof lightness === 'undefined') { lightness = 50; } var color = `hsl(${hue},${saturation}%,${lightness}%)`; return color; }
// Reference for the existing popup let previousPopup = null;
function showUserAction(user, action, presence) { const userPopup = document.createElement('div'); userPopup.classList.add('userPopup'); userPopup.innerText = `${user} ${action}`;
// Set the initial styles for the user popup userPopup.style.position = 'fixed'; userPopup.style.right = '-100%'; userPopup.style.transform = 'translateY(-50%)'; userPopup.style.opacity = '0'; userPopup.style.color = presence ? getHSLColor(100, 50, 50) : getHSLColor(0, 50, 70); // fontColor green && red userPopup.style.backgroundColor = presence ? getHSLColor(100, 50, 10) : getHSLColor(0, 50, 15); // backgroundColor green && red userPopup.style.border = presence ? `1px solid ${getHSLColor(100, 50, 25)}` : `1px solid ${getHSLColor(0, 50, 40)}`; // borderColor green && red userPopup.style.setProperty('border-radius', '4px 0 0 4px', 'important'); userPopup.style.padding = '8px 16px'; userPopup.style.display = 'flex'; userPopup.style.alignItems = 'center';
// Append the user popup to the body document.body.appendChild(userPopup);
// Calculate the width and height of the user popup const popupWidth = userPopup.offsetWidth; const popupHeight = userPopup.offsetHeight; const verticalOffset = 2;
// Set the position of the user popup relative to the previous popup let topPosition = '30vh'; if (previousPopup !== null) { const previousPopupPosition = previousPopup.getBoundingClientRect(); topPosition = `calc(${previousPopupPosition.bottom}px + ${popupHeight}px / 2 + ${verticalOffset}px)`; } userPopup.style.top = topPosition; userPopup.style.right = `-${popupWidth}px`;
// Animate the user popup onto the screen userPopup.style.transition = 'all 0.3s ease-in-out'; userPopup.style.right = '0'; userPopup.style.opacity = '1';
// Store a reference to the current popup previousPopup = userPopup;
// Hide the user popup after a short delay setTimeout(() => { userPopup.style.transition = 'all 0.3s ease-in-out'; userPopup.style.right = `-${popupWidth}px`; userPopup.style.opacity = '0'; setTimeout(() => { document.body.removeChild(userPopup); // Clear the reference to the previous popup if (previousPopup === userPopup) { previousPopup = null; } }, 300); }, 5000); }
// FUNCTIONALITY // Function to highlight users from 'usersToTrack' array in the userlist // Also order online users at the top of the list // And offline users at the bottom of the list function highlightTrackingUsers() { // Select all ins elements from the userlist const insElements = document.querySelectorAll('.userlist-content ins');
// Iterate over the ins elements and check if they contain an anchor element for (const ins of insElements) { const anchor = ins.querySelector('a'); if (anchor) { // Retrieve the username from the anchor textContent const name = anchor.textContent.trim(); // Find the user in 'usersToTrack' array by their name const userToTrack = usersToTrack.find(user => user.name === name); // If the user is found and not revoked, set their anchor text color to green if (userToTrack && !ins.classList.contains('revoked')) { anchor.style.setProperty('color', '#83cf40', 'important'); anchor.style.setProperty('text-shadow', '0 0 1px #83cf40', 'important'); // If the tracked user is not already at the beginning of the list, move them there if (ins.parentNode.firstChild !== ins) { ins.parentNode.insertBefore(ins, ins.parentNode.firstChild); } } // If the user is found and is revoked, set their anchor text color to a red else if (userToTrack && ins.classList.contains('revoked')) { anchor.style.setProperty('color', '#ff8080', 'important'); anchor.style.setProperty('text-shadow', '0 0 1px #ff8080', 'important'); // If the tracked user is not already at the end of the list, move them there if (ins.parentNode.lastChild !== ins) { ins.parentNode.insertBefore(ins, null); } } } } }
// Define references to retrieve and create const userList = document.querySelector('.userlist-content'); const userCount = document.createElement('div'); userCount.classList.add('user-count'); userCount.style.filter = 'grayscale(100%)'; userCount.innerHTML = '0'; document.body.appendChild(userCount);
// Initialize variables to keep track of the current and previous users let currentUsers = []; let previousUsers = []; let hasObservedChanges = false; let prevUserCountValue = 0;
// Initialize variables for the user count animation let currentTextContent = []; let isAnimating = false;
// Mutation observer to track all the users with only graphical popup notification // Also play notification sound "Left" or "Entered" if the one of them is identical from "usersToTrack" array // Create a mutation observer to detect when the user list is modified const observeUsers = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { // Retrieve all users textContent from userList ins elements const newUserList = Array.from(userList.children).map(child => child.textContent);
// Find new users and left users const newUsers = newUserList.filter(user => !currentUsers.includes(user)); const leftUsers = currentUsers.filter(user => !newUserList.includes(user));
// Retrieve fresh user count length const userCountValue = newUserList.length; // Retrieve the counter element const userCount = document.querySelector('.user-count');
// Update grayscale filter userCount.style.filter = userCountValue > 0 ? 'none' : 'grayscale(100%)';
// Check if the user count animation needs to be started if (currentTextContent.length === 0 && newUserList.length > 0 && !isAnimating) { isAnimating = true; const actualUserCount = newUserList.length; const speed = 20; // Change the speed here (in milliseconds) let count = 0; const userCountIncrement = () => { if (count <= actualUserCount) { const progress = count / actualUserCount; const grayscale = 100 - progress * 100; userCount.innerHTML = `${count++}`; userCount.style.filter = `grayscale(${grayscale}%)`; setTimeout(userCountIncrement, speed); } else { currentTextContent = Array.from(userList.children).map(child => child.textContent); userCount.style.filter = 'none'; userCount.classList.add('pulse'); setTimeout(() => { userCount.classList.remove('pulse'); isAnimating = false; // set isAnimating to false after the animation }, 1000); } }; setTimeout(userCountIncrement, speed); } // Animation END
// Check only after the animation is end if (!isAnimating) { // Check if the user count has changed and add pulse animation if (userCountValue !== prevUserCountValue) { userCount.classList.add('pulse'); // Updating the counter element value userCount.innerHTML = userCountValue; setTimeout(() => { userCount.classList.remove('pulse'); }, 1000); } }
// Log new and left users if (hasObservedChanges) { newUsers.forEach((newUser) => { if (!previousUsers.includes(newUser)) { const userGender = getUserGender(newUser) || 'male'; // use 'male' as default const action = verbs[userGender].enter; showUserAction(newUser, action, true); if (usersToTrack.some(user => user.name === newUser)) { userEntered(newUser, userGender); } } });
leftUsers.forEach((leftUser) => { const userGender = getUserGender(leftUser) || 'male'; // use 'male' as default const action = verbs[userGender].leave; showUserAction(leftUser, action, false); if (usersToTrack.some(user => user.name === leftUser)) { userLeft(leftUser, userGender); } });
// Highlight tracking users in chat user list what are registered as tracked highlightTrackingUsers();
} else { hasObservedChanges = true; }
// Update the previous users and user count previousUsers = currentUsers; currentUsers = newUserList; prevUserCountValue = userCountValue;
} }); });
// Start observing users const config = { childList: true }; observeUsers.observe(userList, config);
// STYLIZATION const styles = ` @import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');
.user-count { font-family: 'Orbitron', sans-serif; font-size: 24px; color: #83cf40; position: fixed; top: 130px; right: 24px; background-color: #2b4317; width: 48px; height: 48px; display: flex; justify-content: center; align-items: center; border: 1px solid #4b7328; transition: filter 0.2s ease-in-out; }
.pulse { animation-name: pulse; animation-duration: 1s; animation-iteration-count: 1; }
@keyframes pulse { 0% { filter: brightness(1); } 50% { filter: brightness(1.5); } 100% { filter: brightness(1); } } `;
const styleElement = document.createElement('style'); styleElement.textContent = styles; document.head.appendChild(styleElement);
// EVERY NEW MESSAGE READER // Avoid reading on load page to read the messages normally on stable presence let isInitialized = false;
// create a mutation observer to watch for new messages being added const newMessagesObserver = new MutationObserver(mutations => { for (let mutation of mutations) { if (mutation.type === 'childList') { for (let node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'P') { // read the text content of the new message and speak it const latestMessageTextContent = localStorage.getItem('latestMessageTextContent'); const newMessageTextContent = getLatestMessageTextContent();
// Get the sound switcher element and check if it's muted or not const soundSwitcher = document.querySelector('#unmuted, #muted'); const isMuted = soundSwitcher && soundSwitcher.id === 'muted';
// If not muted, speak the new message and update the latest message content in local storage if (!isMuted && isInitialized && newMessageTextContent && newMessageTextContent !== latestMessageTextContent) { textToSpeech(newMessageTextContent); localStorage.setItem('latestMessageTextContent', newMessageTextContent); }
// If it's the first time, update the latest message content in local storage and set isInitialized to true if (!isInitialized) { localStorage.setItem('latestMessageTextContent', newMessageTextContent); isInitialized = true; }
// If is muted, play the beep sound for the new message if (isMuted && isInitialized && newMessageTextContent && newMessageTextContent !== latestMessageTextContent) { playBeep(newMessageNotes, volumeNewMessage); localStorage.setItem('latestMessageTextContent', newMessageTextContent); } } } } } });
// function to get the cleaned text content of the latest message function getLatestMessageTextContent() { const message = document.querySelector('.messages-content div p:last-child'); if (!message) { return null; } const textNodes = [...message.childNodes].filter(node => node.nodeType === Node.TEXT_NODE); const text = textNodes.map(node => node.textContent).join('').trim(); const time = message.querySelector('.time'); const username = message.querySelector('.username'); const timeText = time ? time.textContent : ''; const usernameText = username ? username.textContent : ''; return text.replace(timeText, '').replace(usernameText, '').trim(); }
// observe changes to the messages container element const messagesContainer = document.querySelector('.messages-content div'); newMessagesObserver.observe(messagesContainer, { childList: true, subtree: true });
// SOUND GRAPHICAL SWITCHER // Button SVG icons muted and unmuted representation const iconSoundMuted = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="hsl(355, 80%, 65%)" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-x"> <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> <line x1="23" y1="9" x2="17" y2="15"></line> <line x1="17" y1="9" x2="23" y2="15"></line> </svg>`; const iconSoundUnmuted = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="hsl(80, 80%, 40%)" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-2"> <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path> </svg>`; // New message sound notification graphical switcher as a button const chatButtonsPanel = document.querySelector('.chat .messages table td:nth-child(3)'); // Avoid panel squeezing chatButtonsPanel.style.minWidth = '105px'; const soundSwitcher = document.createElement('div');
// Check if the user has previously muted the sound const newMessageIsMuted = localStorage.getItem('newMessageIsMuted') === 'true';
soundSwitcher.classList.add('chat-opt-btn'); soundSwitcher.id = newMessageIsMuted ? 'muted' : 'unmuted'; soundSwitcher.addEventListener('click', function () { if (this.id === 'unmuted') { this.id = 'muted'; localStorage.setItem('newMessageIsMuted', 'true'); } else { this.id = 'unmuted'; localStorage.setItem('newMessageIsMuted', 'false'); } updateSoundIcon(); });
const soundIcon = document.createElement('span'); soundIcon.classList.add('sound-icon'); // iconSoundMuted >
|
Душа_Чата
|
Сообщение #9
4 апреля 2023 в 01:21
|
Гонщик
1 |
Только что заметил, что в хайде помещается не весь код стоило мне его раскрыть. Поэтому придётся мне сохранять код на сторонних сервисах, только однажды может так случиться, что код через какое-то время испарится. Через какое время точно не известно. Всё зависит от сервиса. codepaste.xyz
|
Душа_Чата
|
Сообщение #10
4 апреля 2023 в 06:02
|
Гонщик
1 |
Ну чтож. Рад представить вам очередную классную возможность видеть в чате ссылки изображений не только как текстовые ссылки по которым необходимо кликать и переходить в другую вкладку, но и видеть миниатюры, причём легко настраиваемые по изначальной величине, а так-же они кросплатформенные, зависимые от ширины вьюпорта. По клику миниатюры показывается крупное изображение и по клику на само крупное изображение или куда-то за пределы, то крупная картинка скрывается восвояси. Конвертация ссылки в картинку происходит только на тот момент, когда скрипт замечает в сообщении хоть одну ссылку с расширением картинки, а не проверят каждое сообщение, если там нет самой ссылки на картинку. Как это работает, можно посмотреть по ссылке: видео демонстрацияКод codepaste.xyz
|
певец
|
Сообщение #11
4 апреля 2023 в 08:40
|
Супермен
18 |
Демонстрации нужно в первом посте выкладывать))) а не кучу кода непонятную для 99% здешних хомячков))
|
Душа_Чата
|
Сообщение #12
4 апреля 2023 в 16:14
|
Гонщик
1 |
певец, тот кто ищет, тот всегда найдёт.
|
Душа_Чата
|
Сообщение #13
4 апреля 2023 в 17:48
|
Гонщик
1 |
Пофиксил фриз в чате из-за функции подсветки пользователей. Убрал логику сортировки. Чересчур нагружает. Оставил лишь подсветку. Так-же помимо правки фриза добавлена ещё одна фича. Длинные ссылки, порою громадные соразмерно пятиэтажного дома на весь чат, занимают большое полезное пространство, да и очень бросается в глаза. Поэтому, принято решение сократить их простого английского слова image с соответствующим расширением изображения на конце (jpg, jpeg, png или gif). Сама длинная ссылка сохранется в title ссылки. Увидеть её вы сможете при наведении курсором на ссылку. Это для тех, кто хочет быть уверенным, что ссылка не вредоносная, хотя в браузерах уже включена такая возможность отображения подписей к ссылкам где-то в низу окна браузера. Миниатюрам добавлены внутренние и внешние отступы, чтобы было немного воздуха вокруг элементов. Визуализация Код codepaste.xyz Последний раз отредактировано 4 апреля 2023 в 17:54 пользователем elasez_uyefot_2
|
Душа_Чата
|
Сообщение #14
4 апреля 2023 в 23:33
|
Гонщик
1 |
1. "Теперь можно также закрыть полноэкранный просмотр изображения в чате, нажав кнопку 'Esc'." 2. "Проблема исправлена, вызванная ложным срабатыванием, которое говорило о том, что пользователь покинул чат, когда он на самом деле просто на короткое время исчезал из списка чата на доли миллисекунд." Код
|
Душа_Чата
|
Сообщение #15
5 апреля 2023 в 03:01
|
Гонщик
1 |
1. "Теперь чат сворачивается и разворачивается по сочетанию горячих клавиш Ctrl + Space, тем самым счётчик пользователей обнуляется, когда чат свернут и восстанавливает свою работу, когда чат разворачивается. Так-же покуда чат скрыт, то никаких уведомлений о зашедших и вышедших пользователях, а так-же о новых сообщениях. Полное молчание." 2. "Добавлена поддержка конвертирования ютуб ссылок в видео фреймы. Это означает, что теперь вы можете смотреть видео прямо из чата, не переходя в другую вкладку оставаясь в фокусе с чатом." Код
|
Душа_Чата
|
Сообщение #16
5 апреля 2023 в 16:18
|
Гонщик
1 |
Пофиксил функцию конвертации ссылок ютуба. Теперь добавлена поддержка и ссылок с параметром ?v= Код
|
Душа_Чата
|
Сообщение #17
5 апреля 2023 в 21:20
|
Гонщик
1 |
1. Исправлена ошибка, когда конвертер ютубовских ссылок пытался конвертировать одну из ссылок скинутых в чат другим пользователем на некий ютуб канал. Соответственно данная ссылка не валидная ссылка на ютуб видео ряд, поэтому сужен круг поиска конкретно только на валидные полные и сокращённые ютуб видео ссылки. 2. Создание и удаление просмотра изображений из чата в полном размере теперь появляются плавно и плавно затухают вместе с затемняющей нижележащей подложкой, чтобы создавать благоприятный эффект для глаз. 3. Ранее кнопка переключения режимов включала в себе такие как: Голос и сигнал. Теперь же включён выбор полного отключения любого звука о новом сообщении. (voice, beep, silence). 4. Добавлена поддержка конвертации картинок в чате в форате webp. 5. Теперь конвертируются все ссылки содержащие изображения не только лишь в том случае, когда расширение изображения находились в конце ссылки, но и в том случае, когда расширение картинки может находиться где-то в центре самой картинки, когда после самого расширения могут быть описаны ещё какие-то дополнительные параметры генерируемые сервисом предоставляющий доступ к картинке. 6. При наведении на кнопку смены режима оповещения о новых сообщениях теперь обзавелись заголовком, коротко описывающий тип режима. Какое у него назначение. Код (834 строк) Последний раз отредактировано 5 апреля 2023 в 21:42 пользователем elasez_uyefot_2
|
Душа_Чата
|
Сообщение #18
5 апреля 2023 в 22:47
|
Гонщик
1 |
1. Добавлена возможность приближать и отдалять полноформатное изображение прокручиванием колеса мыши, а так-же передвигать изображение зажатым колесом мыши во вьюпорте браузера. 2. Скрипт был переименован KG_Chat_Empowerment так как возможности юзер скрипта переросли обычное звание счётчика пользователей в чате. 3. Фикс. Некорректное имя сокращённого изображения в чате, в том случае, когда расширение изображения не находится в конце ссылки, а где-то в середине меж данными. Сокращение создавалось так. image.png..... и далее ещё какие-то параметры по типу высоты ширины и так далее. ДемонстрацияКод (920 строк) Последний раз отредактировано 6 апреля 2023 в 03:14 пользователем elasez_uyefot_2
|
Душа_Чата
|
Сообщение #19
6 апреля 2023 в 02:47
|
Гонщик
1 |
1. Добавилась возможность удалять нежелательные сообщения на время их видимости в чате, чтобы при каждой перезагрузке страницы удалённые сообщения более не подгружались, так как некоторые сообщения редко но метко могут вызывать раздражение. Учитывая, что теперь в чате имеется возможность отображать картинки и видеофреймы, то будет не лишним такая возможность удалять их из виду. Как только сообщение при последующих обновлениях страницы более не будет подгружено в чате из-за лимита чата в количестве 20 сообщений, то информация о значении удалённого сообщения будет подчищена из хранилища "localStorage", чтобы не захламлять ключ "deletedChatMessagesContent". В этом ключе будет храниться только актуальная удалённая информация и автоматически подчищаться. В конечном итоге, когда в чате более не будет подгружено ни единого удалённого сообщения после перезагрузки страницы, то и ключ будет пустым. Магия. 2. Для удаления сообщения необходимо навести на желаемое сообщение и правой кнопкой мыши вызвать соответствующую кнопку "Delete". Так-же создаётся красное выделение сообщения, чтобы визуально понимать правильное ли сообщение было выделено. Визуальный фидбэк. Если вы в течении одной секунды не нацелились на кнопку, то кнопка пропадёт вместе с выделением сообщения. Если же вы в течении секунды нацелились на кнопку, то кнопка и выделение будут сохранены до тех пор, пока вы опять не уберёте курсор с кнопи, что опять запустит цикл в ожидание одной секунды, после чего произойдёт удаление кнопки и выделения. 3. Так-же имеется кнопка просмотра истории удалённых сообщений кнопкой переключатель. Она имеет 3 значения (Hidden, Show, Hide). Hidden отображается изначально после каждой перезагрузки страницы в том случае, если у вас имеются актуальные удалённые сообщения. Кнопка не отображается вовсе, если удалённых сообщений нет. После нажатия кнопки с подписью Hidden, она переименуется в Hide, что в свою очередь покажет скрытые сообщения. И после нажатия на кнопку с подписью Hide, предпросматриваемые удалённые сообщения снова скроются и кнопка обзаведётся подписью Show. Что означает Показать. Теперь при повторных нажатиях так и будет колебаться между Show и Hide до тех пор, пока вы снова не обновите страницу где в итоге опять будет подпись Hidden. ДемонстрацияКод (1213 строк) Последний раз отредактировано 7 апреля 2023 в 03:46 пользователем Душа_Чата
|
Душа_Чата
|
Сообщение #20
7 апреля 2023 в 23:24
|
Гонщик
1 |
1. Теперь удаление сообщений происходит намного быстрее и удобнее засчёт множественных выделений. Раньше для удаления была доступна лишь одна возможность, удалять сообщение одно за другим по одному. Это нудно и долго, и поэтому сразу же возникла мысль реализовать более удобную версию. Теперь достаточно Зажать правую кнопку мышку и провести курсором по сообщениям, которые хочется захватить для последущего удаления. Как только необходимые сообщения были выделены, успейте в течении одной секунды перевести курсор мыши на кнопку, чтобы удержать её в фокусе, так как выделение сбросится, если в течении ограниченного времени вы не успеете навести курсор. Выделенные сообщения одним движением руки теперь легко удаляются всего одним нажатием кнопки "Delete". Демонстрация 1Демонстрация 2Код (1299 строк)Версия кода устроенная на new Map() constructorКод (1319 строк) Последний раз отредактировано 8 апреля 2023 в 10:22 пользователем Душа_Чата
|