Adds a few additional features to the HLTV forums such as extended forums sidebar or sorting of the sidebar by comments or creation date.
// ==UserScript== // @name HLTV Forum Enhancements // @namespace plennhar-hltv-forum-enhancements // @version 0.1.1 // @description Adds a few additional features to the HLTV forums such as extended forums sidebar or sorting of the sidebar by comments or creation date. // @author Plennhar // @match https://www.hltv.org/* // @grant GM_xmlhttpRequest // @grant GM.setValue // @grant GM.getValue // @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js // @license GPL-3.0-or-later // ==/UserScript== // SPDX-FileCopyrightText: 2024 Plennhar // SPDX-License-Identifier: GPL-3.0-or-later (function() { 'use strict'; console.log("Initializing script"); const forums = { 'https://www.hltv.org/forums/offtopic': 'red', 'https://www.hltv.org/forums/counterstrike': '#ffae00', 'https://www.hltv.org/forums/fantasy': '#633da0', 'https://www.hltv.org/forums/betting': 'darkgreen', 'https://www.hltv.org/forums/hardware': 'silver', 'https://www.hltv.org/forums/bugs': '#3d6ea0' }; function fetchPosts(url) { console.log(`Requesting data from ${url}`); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { console.log(`Received response from ${url}`); const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, 'text/html'); resolve({ doc, url }); }, onerror: function(error) { console.error(`Failed to fetch data from ${url}`, error); reject(error); } }); }); } function addPostsToSidebar(doc, url) { console.log(`Processing document for ${url}`); const threads = $(doc).find('.forumthreads tr.tablerow').toArray(); console.log(`Found ${threads.length} threads on ${url}`); const boxShadowColor = forums[url] || '#ffae00'; // Default color threads.forEach(thread => { const threadLink = $(thread).find('.name a'); const threadHref = threadLink.attr('href'); const threadTitle = threadLink.text(); const threadReplies = $(thread).find('.replies').text(); const threadId = threadHref.split('/').pop(); const existingThread = $(`a[href="${threadHref}"]`); if (existingThread.length === 0) { console.log(`Adding thread ${threadId} to the sidebar`); $('.activitylist').append( `<a href="${threadHref}" class="col-box activity a-reset" style="box-shadow: inset 2px 0 0 0 ${boxShadowColor};"><span class="topic a-default">${threadTitle}</span>${threadReplies}</a>` ); } else { // If a match is found, change the border color to the one defined in forums console.log(`Thread ${threadId} is already in the sidebar, updating border color.`); existingThread.css('box-shadow', `inset 2px 0 0 0 ${boxShadowColor}`); } }); } function checkForSidebar() { console.log("Checking for sidebar..."); const sidebar = document.querySelector('.activitylist'); if (sidebar) { console.log("Sidebar found"); return sidebar; } else { console.log("Sidebar not found, retrying..."); return null; } } function waitForSidebar() { return new Promise((resolve) => { const interval = setInterval(() => { const sidebar = checkForSidebar(); if (sidebar) { clearInterval(interval); resolve(sidebar); } }, 300); }); } function updateCheckboxStates(checkboxes) { const selectedCount = checkboxes.find('input:checked').length; checkboxes.find('input').each(function() { if (selectedCount >= 2 && !$(this).is(':checked')) { $(this).prop('disabled', true).closest('label').css('color', '#999'); } else { $(this).prop('disabled', false).closest('label').css('color', ''); } }); } function createPreferencesModal() { const modal = $('<div>').addClass('preferences-modal').css({ position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', backgroundColor: '#333', padding: '20px', zIndex: '10000', display: 'none', color: 'white', borderRadius: '5px' }); const title = $('<h2>').text('Fetch extra forum threads to sidebar').css({ marginBottom: '10px', fontSize: '18px' }); const checkboxes = $('<div>').css({ display: 'grid', gridTemplateColumns: '1fr 1fr', gridGap: '10px' }); Object.keys(forums).forEach(url => { const label = $('<label>').text(url.split('/').pop().replace(/-/g, ' ')).css({ cursor: 'pointer' }); const checkbox = $('<input>') .attr('type', 'checkbox') .attr('value', url) .css({ marginRight: '5px' }); label.prepend(checkbox); checkboxes.append(label); }); checkboxes.on('change', 'input', function() { updateCheckboxStates(checkboxes); }); const saveButton = $('<button>').text('Save').css({ marginTop: '15px', padding: '5px 10px', backgroundColor: '#ffae00', color: 'white', border: 'none', cursor: 'pointer' }).on('click', async () => { const selectedForums = []; checkboxes.find('input:checked').each(function() { selectedForums.push($(this).val()); }); await GM.setValue('selectedForums', selectedForums); const removeNewsMatches = $('#removeNewsMatches').prop('checked'); const changeUpvoteStyle = $('#changeUpvoteStyle').prop('checked'); await GM.setValue('removeNewsMatches', removeNewsMatches); await GM.setValue('changeUpvoteStyle', changeUpvoteStyle); modal.fadeOut(); console.log("Preferences saved:", selectedForums); applyEnhancements(removeNewsMatches, changeUpvoteStyle); location.reload(); }); const enhancementsSection = $('<div>').css({ marginTop: '20px' }); const enhancementsTitle = $('<div>').text('Enhancements').css({ fontSize: '18px', fontWeight: 'bold', marginBottom: '10px' }); const removeNewsMatchesCheckbox = $('<div>').css({ marginBottom: '10px' }).append( $('<input type="checkbox" id="removeNewsMatches">').prop('checked', true), $('<label for="removeNewsMatches">Remove news and matches from sidebar</label>') ); const changeUpvoteStyleCheckbox = $('<div>').append( $('<input type="checkbox" id="changeUpvoteStyle">').prop('checked', true), $('<label for="changeUpvoteStyle">Change +1 of upvoted comments to green</label>') ); enhancementsSection.append(enhancementsTitle, removeNewsMatchesCheckbox, changeUpvoteStyleCheckbox); modal.append(title, checkboxes, enhancementsSection, saveButton); $('body').append(modal); GM.getValue('selectedForums', []).then(selectedForums => { checkboxes.find('input').each(function() { if (selectedForums.includes($(this).val())) { $(this).prop('checked', true); } }); updateCheckboxStates(checkboxes); }); GM.getValue('removeNewsMatches', true).then(savedRemoveNewsMatches => { $('#removeNewsMatches').prop('checked', savedRemoveNewsMatches); applyEnhancements(savedRemoveNewsMatches); }); GM.getValue('changeUpvoteStyle', true).then(savedChangeUpvoteStyle => { $('#changeUpvoteStyle').prop('checked', savedChangeUpvoteStyle); applyEnhancements(undefined, savedChangeUpvoteStyle); }); return modal; } function applyEnhancements(removeNewsMatches, changeUpvoteStyle) { if (removeNewsMatches) { removeNewsMatchesScript(); } if (changeUpvoteStyle) { changeUpvoteStyleScript(); } } function removeNewsMatchesScript() { (function() { 'use strict'; function cleanRecentActivity() { const recentActivityLinks = document.querySelectorAll('.activitylist a'); recentActivityLinks.forEach(link => { if (link.href.match(/\/matches\/|\/news\//)) { link.style.display = 'none'; } }); } window.addEventListener('load', cleanRecentActivity); const observer = new MutationObserver(cleanRecentActivity); observer.observe(document.querySelector('.activitylist'), { childList: true, subtree: true }); })(); } function changeUpvoteStyleScript() { (function() { 'use strict'; let styledElements = new Set(); function changeStyles() { let elements = document.querySelectorAll('.plus-button.active'); elements.forEach(element => { if (!styledElements.has(element)) { element.style.backgroundColor = 'darkgreen'; element.style.color = 'lightgreen'; element.style.fontWeight = 'bold'; styledElements.add(element); } }); styledElements.forEach(element => { if (!element.classList.contains('active')) { element.style.backgroundColor = ''; element.style.color = ''; element.style.fontWeight = ''; styledElements.delete(element); } }); } const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { changeStyles(); } }); }); observer.observe(document.documentElement, { attributes: true, subtree: true, attributeFilter: ['class'] }); window.addEventListener('load', changeStyles); })(); } function createCogwheel() { const cogwheel = $('<div>').addClass('nav-cogwheel').css({ position: 'relative', display: 'inline-block', cursor: 'pointer', marginLeft: '20px', color: '#ffae00' }).html('<i class="fa fa-cog"></i>'); cogwheel.on('click', () => { $('.preferences-modal').fadeIn(); }); $('#navBarContainerFull .user-wrap').append(cogwheel); $(document).on('click', function(event) { const modal = $('.preferences-modal'); if (!$(event.target).closest(modal).length && !$(event.target).closest(cogwheel).length) { modal.fadeOut(); } }); const closeButton = $('<span>').html('×').css({ position: 'absolute', top: '10px', right: '15px', fontSize: '20px', color: '#fff', cursor: 'pointer' }).on('click', function() { $('.preferences-modal').fadeOut(); }); $('.preferences-modal').prepend(closeButton); } async function main() { console.log("Starting main function"); const preferencesModal = createPreferencesModal(); createCogwheel(); await waitForSidebar(); console.log("Sidebar is available, fetching posts..."); const selectedForums = await GM.getValue('selectedForums', []); for (const forum of selectedForums) { const { doc, url } = await fetchPosts(forum); addPostsToSidebar(doc, url); } } main(); (function() { 'use strict'; function sortForums(order) { let forumContainer = document.querySelector('.col-box-con .activitylist'); if (!forumContainer) return; let forums = Array.from(forumContainer.querySelectorAll('a')); if (order === 'type') { forums.sort((a, b) => { let typeA = a.getAttribute('href').split('/')[1]; let typeB = b.getAttribute('href').split('/')[1]; let numA = parseInt(a.getAttribute('href').match(/\d+/)[0]); let numB = parseInt(b.getAttribute('href').match(/\d+/)[0]); if (typeA === typeB) { return numB - numA; } else { let typeOrder = { 'news': 1, 'matches': 2, 'forums': 3 }; return typeOrder[typeA] - typeOrder[typeB]; } }); } else { forums.sort((a, b) => { let commentsA = extractCommentCount(a); let commentsB = extractCommentCount(b); return commentsB - commentsA; }); } forumContainer.innerHTML = ''; forums.forEach(forum => forumContainer.appendChild(forum)); } function extractCommentCount(element) { let text = element.innerHTML.trim(); let match = text.match(/<\/span>(\d+)$/); return match ? parseInt(match[1]) : 0; } function resetForums() { location.reload(); } function createToggleButton() { let recentActivityTitle = document.querySelector('.recent-activity h1'); if (!recentActivityTitle) return; let toggleButton = document.createElement('button'); toggleButton.style.marginRight = '10px'; toggleButton.style.border = 'none'; toggleButton.style.background = 'none'; toggleButton.style.cursor = 'pointer'; toggleButton.title = 'Sort order: Default'; let icon = document.createElement('span'); icon.innerHTML = '💬'; icon.style.color = ''; toggleButton.appendChild(icon); let currentState = localStorage.getItem('forumSortOrder') || 'default'; updateButtonIcon(currentState, icon); toggleButton.addEventListener('click', () => { let newState; if (currentState === 'default') { newState = 'high'; } else if (currentState === 'high') { newState = 'type'; } else { newState = 'default'; } currentState = newState; localStorage.setItem('forumSortOrder', newState); updateButtonIcon(newState, icon); setTimeout(() => { if (newState === 'high') { sortForums('high'); } else if (newState === 'type') { sortForums('type'); } else { resetForums(); } }, 500); }); recentActivityTitle.insertBefore(toggleButton, recentActivityTitle.firstChild); } function updateButtonIcon(state, icon) { if (state === 'high') { icon.innerHTML = '🔽'; icon.style.color = ''; icon.title = 'Sort order: High to Low'; } else if (state === 'type') { icon.innerHTML = '📅'; icon.style.color = ''; icon.title = 'Sort order: Creation Time (New to Old)'; } else { icon.innerHTML = '💬'; icon.style.color = ''; icon.title = 'Sort order: Default (Posts with New Comments First)'; } } window.addEventListener('load', () => { createToggleButton(); let savedOrder = localStorage.getItem('forumSortOrder'); setTimeout(() => { if (savedOrder === 'high') { sortForums('high'); } else if (savedOrder === 'type') { sortForums('type'); } }, 1000); }); document.addEventListener('click', (event) => { let modal = document.querySelector('.forum-preferences-modal'); let menuWrap = document.querySelector('.forum-preferences-menu-wrap'); if (modal && !modal.contains(event.target) && !menuWrap.contains(event.target)) { modal.classList.add('hidden'); } }); })(); })();