Душа_Чата
|
Сообщение #1
2 апреля 2023 в 23:41
|
Маньяк
2 |
Предлагаю желающим воспользоваться этим скриптом для чата, который расширяет его возможности. СКАЧАТЬ ИМЕННО ТУТGithubGreaseForktДля улучшенного экспириенса, установите тёмную тему. Тёмная темаПервоначальный взгляд на общий вид. С этим скриптом вы сможете ↓ → 1. Просматривать количество текущих пользователей в чате. → 2. Настраивать звуковые оповещения по иконке динамика.Кнопка может быть в трёх состояниях. 1. Состояние, когда не происходит никаких звуковых уведомлений.2. Состояние, когда голосом уведомляется о зашедших и вышедших пользователях в чат (из чата). Проигрывается звуковой сигнал о поступившем сообщении в чат.3. Состояние, когда помимо уведомления голосом о захождении и выхода пользователя, также зачитывается голосом каждое поступившее в чат сообщение вместо звукового сигнала.P.S. В настройках данной кнопки скрыта ещё одна настройка скорости и тембра озвучиваемого голоса при голосовых оповещениях и зачитывании сообщений. Для того, чтобы настроить скорость, необходимо зажать клавиш Ctrl и нажимать ЛКМ по кнопке и тем самым будет уменьшаться скорость, нажимая ПКМ, вы будете увеличивать скорость зачитывания. Изначальная скорость выставлена в 1.5. Для того, чтобы настроить тембр, необходимо зажать клавишу Alt и аналогичным образом изменять значение как и со скоростью. Изначальное значение тембра 1.0. Также внутри кода имеются дополнительные тонкие настройки 1. Громкость голоса относительно выставленной громкости в настройках системы, а именно 80% от 100% системной громкости.const voiceVolume = 0.8; 2. Громкость звукового оповещения о новом сообщении или же звук оповещения о зашедшем или вышедшем пользователе. 20% от системной 100% громкости.const beepVolume = 0.2; 3. Продолжительность длительности звукового сигнала или связки групповых сигналов в некоторых случаях в миллисекундах.const duration = 80; Остальные настройки не рекомендуется менять, но если вы экспериментатор и знаток кода, то можете пощупать. На ваше усмотрение. Всё в ваших руках. → 3. Устанавливать рамки на оповещения о новых сообщениях.Кнопка может быть в двух состояниях. * (1) Состояние, когда уведомления о новых сообщениях или зачитывание происходит от всех пользователей в чате.* (2) Состояние, когда уведомления о новых сообщениях или зачитывание, происходит от пользователей, которые к вам обратились по никнейму или же по словам, заданные в массив (внутри кода), называемые алиасами (aliases).как слова ваших предыдущих никнеймов или других слов возможно упоминающих о вас.Вот пример того, как в чате происходит обращение к вам. В данном случае слово обращения (никнейм) обрамляется в зелёный прямоугольник. Также в этот же прямоугольник включаются и слова алиасы (aliases). Это личные к вам сообщения, которые озвучиваются другим типом звукового оповещения в сравнении со звуком обычных сообщений не обращённые к вам. → 4. Просматривать историю пользователей, которые заходили в чат, на момент, пока вы тоже присутствовали в чате.Также вы могли заметить, что возле данной кнопки в левом нижнем углу находится элемент с цифровым значением, который отображает колличество сохранённых пользователей в КЭШ, которые при открытии, будут представлены в КЭШ панели. Это достаточно обширная интеграция, которая нуждается в поэтапном описании. Когда вы кликаете по кнопке с иконкой базы данных, то открывается так называемая КЭШ (Cache) панель, где она представлена в таком виде. Внутри этой панели начнём разбираться по порядку. Первое на что стоит обратить внимание, так это на возможность настройки автоматического сброса КЭШ, который используется функционалом скрипта и необходим для корректной работы кастомного списка пользователей в чате. Для настройки необходимо кликнуть на время оранжевого цвета указанного стрелкой, напротив которого описание Threshold и в появившемся всплывающем окне, указать время в формате (HH, HH:mm, или HH:mm:ss), что означает [Только часы, Часы + минуты, Часы + минуты + секунды]. Рядом с кликабельным значением времени также присутствует ещё элемент Countdown, где в реальном времени отображается счётчик, показывающий сколько осталось времении, до автоматического сброса КЭШ. Строка поиска пользователей в стиле Fuzzy Finder. В самом верхнем правом углу находятся две кнопки. Первая кнопка с корзинкой необходима для удаления КЭШ вручную, если в этом будет необходимость, чтобы актуализировать КЭШ пользователей. Вторая кнопка с крестиком закрывает КЭШ панель. P.S. КЭШ панель, это графическое представление сохранённых всех пользователей со всеми необходимыми данными, такими как: 1. Ссылка на самую актуальную пользовательскую аватарку2. Никнейм пользователя3. Справа от никнейма цифровое значение, отображающее колличество перезаходов в чат пользователей, на момент пока вы тоже присутствовали в чате, чтобы зарегистрировать данное действие. Как ещё можно заметить, что некоторые значения обрамлены в зелёный легко заметный прямоугольник.Это сделано только для тех пользователей, которые внесены в список отслеживаемых пользователей, которые голосом уведомляющтся о их захождении и выходе. Остальные цифры, никак не обрамлённые, это самые обычные рядовые пользователи зашедшие в чат.4. Ниже дата регистрации пользователя, а при наведении курсором мыши на время регистрации, происходит конвертация во время нахождения пользователем на сайте в целом, в общем.5. Ещё ниже полоска данных о лучшем рекорде (иконка ракеты), рейтинговом уровне (иконка звезды), колличестве машинок в гараже (иконка машины), колличестве друзей (иконка рукопожатия).Все эти элементы кликабельны и ведут на соответствующий раздел связанный с конкретным выбранным пользователем с которым ведётся манипуляция. Опять же, повторюсь, что все эти данные в основном используются для корректной работы кастомного списка пользователей чата, но графическое представление также считается уместным для понимания о том, какие всё-таки пользователи могли сегодня побывать в чате и получить быструю информацию по каждому из них. Весь этот список пользователей хранится в вашем браузере в localStorage https://developer.mozilla.org/en-US/docs/We...ow/localStorage, после первой успешной подгрузки этих данных с API сайта. Необходимо это для того, чтобы не запрашивать каждый раз всю эту информацию с сайта, как только вы заходите на страницу с чатом. Делается это для того, чтобы в последующие сессии, список пользователей в чате прогружался молниеносно и не создавать излишнюю нагрузку на сайт частыми запросами. По мере того, как вы находитесь в чате, в него будут поступать всё новые и новые посетители, которые с первичной загрузкой не были подгружены в localStorage (КЭШ) и будут подгружены единожды туда же и сохранённые данные аналогично будут использованы в последующие загрузки и перезагрузки страницы с чатом. → 5. Просматривать на лету колличество введённых символов в поле для ввода чата.С каждым напечатанным символом, увеличивается числовое значение. Также меняется цветовое оформление по мере увеличения колличества символов. Изначальное сероватое состояние со значения 0, от 90 до 100 плавно окрашивается в жёлтую гамму, со 190 до 200 в оранжевую, с 250 окончательно уходит в красную. Альтернативный метод отслеживания колличества введённых символов также происходит в самом чате. Он оформлен в подвижном состоянии и двигается всегда за последним введённым символом. Данный бегунок длинны сообщения также эффектно появляется и скрывается с определённой анимацией. По мере ввода символов отображается цифровое значение со стрелкой вперёд. По мере удаления цифровое значение со стрелкой назад. Цветовое оформление аналогично первому решению в самой панели кнопок. → 6. Пользоваться обновлённым списком чата с куда более современным подходом оформления.1. Тут можно заметить изначально более крупные аватарки с большим разрешением, как в профильных аватарках с уклоном на мониторы большего разрешения. При наведении курсором мыши на аватарку, они плавно увеличиваются вдвое и плавно уменьшаются при уведении фокуса с аватарки.2. Сортировка пользователей происходит по рангам с цветовым обозначением и их группированием в неразграничённые группы. Возрастание ранга начинается снизу вверх, то-есть самый низший ранг будет в самом низу, а самый наивысший ранг в самом верху.3. С правой стороны от ника имеются отображения ГОЛУБОЙ звезды для отслеживаемых пользователей.Отслеживаемые пользователи добавляются внутри кода. Добавлять их нужно одной целой корректно оформлённой строкой с запятой в конце. В запятой нет необходимости, если это последняя строка. Пример строки кода: { name: 'Даниэль', gender: 'male', pronunciation: 'Даниэль' }, // ------------ 01 name, это свойство где указывается никнейм пользователя, в gender указывается пол пользователя, а в pronunciation это то, как именно будет произнесён голосом зашедший или вышедший из чата пользователь чата. Вы можете скопировать весь этот блок кода и корректировать под себя, в зависимости от того, каких пользователей вы хотели бы отслеживать. Быть в курсе о их присутствии и отсутствии в чате. 4. Отображение пользователя незабаненным и в бане графически оформлено иным образом. Иконка для перехода в профиль окрашивается в зелёный цвет для тех кто не в бане, а красным для тех кто в бане. На gif скриншоте правда можно увидеть только зелёные иконки незабаненных пользователей, так как именно в этот момент забаненных не оказалось.5. Раньше справа от ника ещё отображалась иконка модератора в виде жёлтого щита, но в последнее время эта иконка больше не показывается, так как в штатном списке чата этой иконки также не отображается. Позже необоходимо будет придумать, как реализовать этот момент, чтобы вернуть эту иконку модераторам. → 7. Отправлять в чат ютуб ссылки с последующей конвертацией в проигрыватель.Так что открывать ютуб ссылки в новую вкладку теперь нет необходимости. Видео можно развернуть в полный экран не выходя из чата. → 8. Отправлять в чат ссылки на картинки с последующей конвертацией в превью.Кликнув на превью можно открыть картинку на весь экран. Картинку можно масштабировать прокручивая колесо мыши. Картинку можно трансформировать (перемещать) по экрану зажав среднюю кноку мыши и двигать курсор мыши по экрану. Куда курсор, туда и картинка. Если в текущем списке сообщений чата больше одной картинки, то открыв одну из картинок, вы можете стрелками влево и вправо переключаться между ними. Чтобы закрыть открытую картинку, вы можете кликнуть по самой картинке, за её пределами или же нажать клавишу ESC. P.S. Важно подметить, что конвертируются не все картинки, а только картинки с доверенным доменом. Этот список также можно редактировать, если вам не хватает того, что включено в список. → 9. Просматривать абсолютно всех зашедших и вышедших пользователей во всплывающих плашках, которые появляются слева от чата из-за пределов вьюпорта.Пользователи, которые зашли, отображены в зелёной гамме, которые вышли в красной. → 10. Просматривать историю зашедших и вышедших (отслеживаемых) пользователей чата прямо в списке сообщений.Сделано это таким образом, чтобы как-то разграничить визуальные уведомления от простых и отслеживаемых пользователей. Так как отслеживаемые в приоритете, следы их присутствия сохраняются и список сообщений чат стал отличным для этого местом (убежищем). Всё аналогично как и с неотслеживаемыми пользователями. Внутри элемента фиксируется никнейм, иконка о конкретном событии (зашёл или вышел), время когда произошло событие. → 11. Забавная плюшка. Можно удалять элементы формирующие историю событий о заходе и выходе из чата отслеживаемых пользователей в списке сообщений чата.Для этого необоходимо кликнуть быстро два раза левой кнопкой мыши по одному из элементов и элементы начнут удаляться с самого низа до самого верха, при этом чат по мере их удаления будет прокручиваться вверх и как только все элементы будут удалены, чат снова прокрутится к последним (актуальным) сообщениям. P.S. Конечная прокрутка вниз бывает не всегда срабатывает, так что это скорее фича, а не баг. Алгоритм с вами играет, забавляет вас. → 12. Список сообщений чата группируется в блоки сообщений.Сообщение или блок сообщений конкретного пользователя разграничивается отступами сверху и снизу, чтобы повысить читабельность и не сваливать всё в одну кучу (сплошную кашу). → 13. Возможность удалять вручную неугодные сообщения, а также их восстанавливать.Способы удаления сообщений: 1. Единичное удаление сообщения. Для этого необходимо навести курсор мыши на сообщение и правой кнопкой мыши вызвать кнопку удаления (Delete). Нажать на кнопку и сообщение будет удалено, а точнее скрыто. Фактически оно не удаляется из вёрстки, чтобы его можно было вновь показать при необходимости.2. Удаление сообщений целое группой. Для этого необходимо с зажатой правой кнопкой мыши создать целое выделение. Выдаление создаётся красным фоновым цветом. Отжимая правую кнопку мыши, вновь появляется кнопка (Delete), по которой нажимая удаляются все выделенные сообщения.3. Удаление сообщений единичной выборкой. Для этого необходимо за короткий промежуток времени успеть правой кнопкой выделить все неугодные сообщения и удалить их аналогично.А наглядно происходит это именно так. Чтобы вновь показать удалённые сообщения, необходимо кликнуть по появившейся кнопке Скрытые (Hidden). Сообщения отобразятся и кнопка поменяет окрас и свою надпись на Скрыть (Hide). После скрытия сообщений кнопка окрасится в иной тон и поменяет надпись на Показать (Show). Чтобы восстановить удалённые сообщения, необходимо зажать клавишу Ctrl и навести на кнопку, которая окрасится в белобрысый тон и поменяет надпись на Восстановить (Restore). Нажав на неё с ударженной клавишей Ctrl сообщения восстановятся. → 14. Система автоматического удаления часто отправляемых сообщений в чат.Как только пользователь начинает отправлять в чат сообщений с интервалом меньше 400 миллисекунд, все его сообщения из чата пропадают. Настроить эту чувствительность можно внутри кода. const timeDifferenceThreshold = 400; Также пользователь может попасть в окончательный бан и его сообщения перестанут отображаться, если он превысит этот лимит больше 10 раз. Это тоже можно настроить. const thresholdMaxTries = 10; P.S. Имеется ещё один алгоритм удаления похожих сообщений, но он временно отключён и нуждается в доработке, так как он зачастую удаляет абсолютно не похожие сообщения. → 15. Скрывать чат по сочетанию горячих клавиш Ctrl + Space.Этот метод удаления лишь скрывает чат из виду сохраняя его работоспособность для того, чтобы алгоритм продолжал свою работу по отслеживанию пользователей и другую необходимую работу. Рекомендуется использовать именно этот способ скрытия чата из вида. P.S. Хочу подметить тот факт, что фокус в строку ввода восстанавливается автоматически после раскрытия чата или же обновления страницы, переключении между вкладками комнаты с игровой страницы. → 16. Продолжать читать сообщения чата даже после его скрытия сочетанием клавиш Ctrl + Space.Каждое сообщение отображатеся снизу вверх в виде всплывающих элементов. Отображение максимального колличества сообщений можно настроить внутри кода. const maxPopupMessagesCount = 10; Сообщения появляются и исчезают с приятной анимацией. Пользователь чата изначально получает свою одну цветовую гамму для каждого сообщения и использует её в текущей сессии до перезагрузки страницы с чатом. Каждый пользователь со своей уникальной сгенерированной цветовой гаммой для улучшенного визуального разграничения сообщений. → 17. Активация строки ввода в чате после получения бана.Если вас забанили в чате, то поле для ввода становится не активным и текст вы больше вводить не можете. Здесь же поле вновь становится доступным для ввода текста, но отправлять сообщения вы можете только в шепталку. Кнопка отправки сообщения окрашивается в другой цвет и меняется иконка охарактеризовывает тем самым, что общий чат не доступен. → 18. Отправлять в чате более длинные сообщения.Чат в штатном состоянии позволяет отправлять не больше 300 символов. Здесь же алгоритм вам позволяет отправлять длинные сообщения до 1000 символов. Как только ваше сообщений станет длиннее 300 символов, оно разобьётся на части и с рандомным интервалом между 500 - 1000 миллисекунд отправится в чате в порядке очереди, причём каждая отправляемая часть сообщения не превысит допустимые 300 символов. P.S. Не злоупотребляйте данным функционалом с целью навредить проекту. Предпочтительно печатать сообщения собственноручно, если вдруг вы сегодня оказались многословным и 300 символов в одно сообщение для вас оказалось не достаточным. → 19. На странице игры вы можете переключаться между общей и игровой комнатой чата по клавише Tab.Важно подметить. Что алгоритм запоминает последнее переключение вкладки чата и с последующими играми вы останетесь в той комнате, в которую переключились в последний раз. Так что если вы предпочитаете сидеть всегда в общем чате, то вас автоматически переключит в общий чат. Так это работает. Запоминание последней настройки. → 20. Автоматическое восстановление связи с чатом.Если у вас например открыта вкладка с общим чатом и вы решили открыть другую вкладку с действующей игрой и открытым чатом, то в первой вкладке произойдёт потеря связи с общим чатом. Как только вы закроете вкладку с игрой и вернётесь в первую вкладку с общим чатом, то произойдёт автоматическое обновление страницы. Таким образом чат снова станет доступным. Это избавляет от необходимости вручную обновлять страницу. Примечание. Чтобы страница обновилась сама, необходимо, чтобы вкладка была активной. Это означает, что вы должны находиться именно на этой странице с открытой вкладкой. → 21. Восстановление введённого сообщения в поле для ввода чата, если случайно, непредвиденно обновилась страница по разным причинам.Для работы звуковых оповещений, необходимо разрешить звуки (AudioContext API). Для этого, у кого браузер firefox, необходимо произвести подобные настройки. Ввести в браузерной строке about:config -> Enter и найти соответствующий ключ как на скрине и выставить ему значение 1 В некоторых случаях цифра 1 не всегда работает. Поэтому попробуйте выставить цифру 0. Перезагрузите браузер и вновь зайдите. После этого точно должен заработать оповещающий звуковой сигнал. Для работы голосового движка, вам необходимо как минимум быть на windows машине. Так-же должен быть установлен русский языковой пакет. После установки языкового пакета, вам возможно будет необходимо так-же задать разрешение воспроизводить голоса в браузере. Если у вас опять же браузер firefox, то похожим методом как с (AudioContext API). Последний раз отредактировано 29 октября 2024 в 05:47 пользователем Душа_Чата
|
Душа_Чата
|
Сообщение #2
3 апреля 2023 в 15:06
|
Маньяк
2 |
Добавлена возможность отключать звук зачитывания сообщения переключив тем самым на звуковой сигнал и наоборот. Кнопка ведёт себя как переключатель между режимами оповещения о новых последних сообщениях. А главное, что кнопка запоминает данную последнюю настройку пользователем, так как информация о состоянии кнопки сохраняется в браузерном 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
|
Маньяк
2 |
Сделан небольшой фикс скрипта. Когда кнопка находится в состоянии мута с простым звуковым оповещением, теперь не будет сигналить после каждое перезагрузки страницы лишь в том случае, если в чате последнее сообщение осталось всё тоже неизменное после множества других перезагрузок страницы. Будет сигналить, если последнее сообщение отличительное с последнего посещения страницы с чатом. Так вы будете знать о том, что новые сообщения в чате появились. скрытый текст… // ==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
|
Маньяк
2 |
Добавляю версию скрипта, где кнопка динамика для переключения режимов зачитывания последнего сообщения и оповещения звуковым сопровождением оформлены в соответствии с другими кнопками чата как реализовано в тёмной теме для расширения 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
|
Супермен
37 |
Ссылку на скрипт дайте, пожалуйста, чтобы его установить
|
Душа_Чата
|
Сообщение #6
3 апреля 2023 в 21:29
|
Маньяк
2 |
Правки правки правки и ещё раз правки. Нет предела совершенству. Правки больше касаемо оптимизации и справления некоторых ошибок. А исправлено вот что: 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
|
Маньяк
2 |
Вова_10 Вова_10 писал(а): Ссылку на скрипт дайте, пожалуйста, чтобы его установить Это очень просто. Необходимо для начала скачать расширение для браузера tampermonkeyА далее в настройках расширения на странице клавогонок создать новый скрипт нажав на эту кнопку Далее откроется окно с полем куда нужно с заменой всего что там увидишь самой последней версией кода из хайда И сохраняешь Ctrl + S
|
Душа_Чата
|
Сообщение #8
4 апреля 2023 в 01:14
|
Маньяк
2 |
Добавлена функция подсветки ников в списке чата для незабаненных и забаненных пользователей. Незабаненные светятся зелёным, а забаненные красным. Всё оптимально смотрится на тёмном фоне. Для тех кто сидит с дефолтной светлой темы, то придётся ковырнуть для этого цвета и сделать их темнее. Ковырять придётся эту функцию Менять нужно эти строки 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
|
Маньяк
2 |
Только что заметил, что в хайде помещается не весь код стоило мне его раскрыть. Поэтому придётся мне сохранять код на сторонних сервисах, только однажды может так случиться, что код через какое-то время испарится. Через какое время точно не известно. Всё зависит от сервиса. codepaste.xyz
|
Душа_Чата
|
Сообщение #10
4 апреля 2023 в 06:02
|
Маньяк
2 |
Ну чтож. Рад представить вам очередную классную возможность видеть в чате ссылки изображений не только как текстовые ссылки по которым необходимо кликать и переходить в другую вкладку, но и видеть миниатюры, причём легко настраиваемые по изначальной величине, а так-же они кросплатформенные, зависимые от ширины вьюпорта. По клику миниатюры показывается крупное изображение и по клику на само крупное изображение или куда-то за пределы, то крупная картинка скрывается восвояси. Конвертация ссылки в картинку происходит только на тот момент, когда скрипт замечает в сообщении хоть одну ссылку с расширением картинки, а не проверят каждое сообщение, если там нет самой ссылки на картинку. Как это работает, можно посмотреть по ссылке: видео демонстрацияКод codepaste.xyz
|
Пьяный_Качок
|
Сообщение #11
4 апреля 2023 в 08:40
|
Супермен
21 |
Демонстрации нужно в первом посте выкладывать))) а не кучу кода непонятную для 99% здешних хомячков))
|
Душа_Чата
|
Сообщение #12
4 апреля 2023 в 16:14
|
Маньяк
2 |
певец, тот кто ищет, тот всегда найдёт.
|
Душа_Чата
|
Сообщение #13
4 апреля 2023 в 17:48
|
Маньяк
2 |
Пофиксил фриз в чате из-за функции подсветки пользователей. Убрал логику сортировки. Чересчур нагружает. Оставил лишь подсветку. Так-же помимо правки фриза добавлена ещё одна фича. Длинные ссылки, порою громадные соразмерно пятиэтажного дома на весь чат, занимают большое полезное пространство, да и очень бросается в глаза. Поэтому, принято решение сократить их простого английского слова image с соответствующим расширением изображения на конце (jpg, jpeg, png или gif). Сама длинная ссылка сохранется в title ссылки. Увидеть её вы сможете при наведении курсором на ссылку. Это для тех, кто хочет быть уверенным, что ссылка не вредоносная, хотя в браузерах уже включена такая возможность отображения подписей к ссылкам где-то в низу окна браузера. Миниатюрам добавлены внутренние и внешние отступы, чтобы было немного воздуха вокруг элементов. Визуализация Код codepaste.xyz Последний раз отредактировано 4 апреля 2023 в 17:54 пользователем elasez_uyefot_2
|
Душа_Чата
|
Сообщение #14
4 апреля 2023 в 23:33
|
Маньяк
2 |
1. "Теперь можно также закрыть полноэкранный просмотр изображения в чате, нажав кнопку 'Esc'." 2. "Проблема исправлена, вызванная ложным срабатыванием, которое говорило о том, что пользователь покинул чат, когда он на самом деле просто на короткое время исчезал из списка чата на доли миллисекунд." Код
|
Душа_Чата
|
Сообщение #15
5 апреля 2023 в 03:01
|
Маньяк
2 |
1. "Теперь чат сворачивается и разворачивается по сочетанию горячих клавиш Ctrl + Space, тем самым счётчик пользователей обнуляется, когда чат свернут и восстанавливает свою работу, когда чат разворачивается. Так-же покуда чат скрыт, то никаких уведомлений о зашедших и вышедших пользователях, а так-же о новых сообщениях. Полное молчание." 2. "Добавлена поддержка конвертирования ютуб ссылок в видео фреймы. Это означает, что теперь вы можете смотреть видео прямо из чата, не переходя в другую вкладку оставаясь в фокусе с чатом." Код
|
Душа_Чата
|
Сообщение #16
5 апреля 2023 в 16:18
|
Маньяк
2 |
Пофиксил функцию конвертации ссылок ютуба. Теперь добавлена поддержка и ссылок с параметром ?v= Код
|
Душа_Чата
|
Сообщение #17
5 апреля 2023 в 21:20
|
Маньяк
2 |
1. Исправлена ошибка, когда конвертер ютубовских ссылок пытался конвертировать одну из ссылок скинутых в чат другим пользователем на некий ютуб канал. Соответственно данная ссылка не валидная ссылка на ютуб видео ряд, поэтому сужен круг поиска конкретно только на валидные полные и сокращённые ютуб видео ссылки. 2. Создание и удаление просмотра изображений из чата в полном размере теперь появляются плавно и плавно затухают вместе с затемняющей нижележащей подложкой, чтобы создавать благоприятный эффект для глаз. 3. Ранее кнопка переключения режимов включала в себе такие как: Голос и сигнал. Теперь же включён выбор полного отключения любого звука о новом сообщении. (voice, beep, silence). 4. Добавлена поддержка конвертации картинок в чате в форате webp. 5. Теперь конвертируются все ссылки содержащие изображения не только лишь в том случае, когда расширение изображения находились в конце ссылки, но и в том случае, когда расширение картинки может находиться где-то в центре самой картинки, когда после самого расширения могут быть описаны ещё какие-то дополнительные параметры генерируемые сервисом предоставляющий доступ к картинке. 6. При наведении на кнопку смены режима оповещения о новых сообщениях теперь обзавелись заголовком, коротко описывающий тип режима. Какое у него назначение. Код (834 строк) Последний раз отредактировано 5 апреля 2023 в 21:42 пользователем elasez_uyefot_2
|
Душа_Чата
|
Сообщение #18
5 апреля 2023 в 22:47
|
Маньяк
2 |
1. Добавлена возможность приближать и отдалять полноформатное изображение прокручиванием колеса мыши, а так-же передвигать изображение зажатым колесом мыши во вьюпорте браузера. 2. Скрипт был переименован KG_Chat_Empowerment так как возможности юзер скрипта переросли обычное звание счётчика пользователей в чате. 3. Фикс. Некорректное имя сокращённого изображения в чате, в том случае, когда расширение изображения не находится в конце ссылки, а где-то в середине меж данными. Сокращение создавалось так. image.png..... и далее ещё какие-то параметры по типу высоты ширины и так далее. ДемонстрацияКод (920 строк) Последний раз отредактировано 6 апреля 2023 в 03:14 пользователем elasez_uyefot_2
|
Душа_Чата
|
Сообщение #19
6 апреля 2023 в 02:47
|
Маньяк
2 |
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
|
Маньяк
2 |
1. Теперь удаление сообщений происходит намного быстрее и удобнее засчёт множественных выделений. Раньше для удаления была доступна лишь одна возможность, удалять сообщение одно за другим по одному. Это нудно и долго, и поэтому сразу же возникла мысль реализовать более удобную версию. Теперь достаточно Зажать правую кнопку мышку и провести курсором по сообщениям, которые хочется захватить для последущего удаления. Как только необходимые сообщения были выделены, успейте в течении одной секунды перевести курсор мыши на кнопку, чтобы удержать её в фокусе, так как выделение сбросится, если в течении ограниченного времени вы не успеете навести курсор. Выделенные сообщения одним движением руки теперь легко удаляются всего одним нажатием кнопки "Delete". Демонстрация 1Демонстрация 2Код (1299 строк)Версия кода устроенная на new Map() constructorКод (1319 строк) Последний раз отредактировано 8 апреля 2023 в 10:22 пользователем Душа_Чата
|