// ==UserScript== // @name PerplexityTools - Floating Copy Code & Navigation Buttons (AFU IT) // @namespace http://tampermonkey.net/ // @version 1.0 // @description Adds floating copy button and navigation buttons // @author AFU IT // @match https://www.perplexity.ai/* // @license MIT // @grant none // @downloadURL https://update.greasyfork.icu/scripts/535221/PerplexityTools%20-%20Floating%20Copy%20Code%20%20Navigation%20Buttons%20%28AFU%20IT%29.user.js // @updateURL https://update.greasyfork.icu/scripts/535221/PerplexityTools%20-%20Floating%20Copy%20Code%20%20Navigation%20Buttons%20%28AFU%20IT%29.meta.js // ==/UserScript== (function() { 'use strict'; const CHECK_INTERVAL = 2000; // Check every 2 seconds const LONG_PRESS_DURATION = 1000; // 1 second for long press const originalFetch = window.fetch; // Variables to track long press let upButtonTimer = null; let downButtonTimer = null; let isUpButtonLongPress = false; let isDownButtonLongPress = false; // Helper function to scroll to the previous question function scrollToPreviousQuestion() { if (isUpButtonLongPress) return; // Skip if this is triggered by a long press const queryBlocks = Array.from(document.querySelectorAll('.group.relative.mb-1.flex.items-end.gap-0\\.5')); if (!queryBlocks.length) return; // Get all blocks positions const positions = queryBlocks.map(block => { const rect = block.getBoundingClientRect(); return { element: block, top: rect.top, bottom: rect.bottom }; }); // Sort by vertical position positions.sort((a, b) => a.top - b.top); // Find the first block above the middle of the viewport const viewportMiddle = window.innerHeight / 2; let targetBlock = null; for (let i = positions.length - 1; i >= 0; i--) { if (positions[i].top < viewportMiddle) { if (i > 0) { targetBlock = positions[i - 1].element; } else { // If we're at the first question, scroll to top window.scrollTo({ top: 0, behavior: 'smooth' }); return; } break; } } // If we found a target block, scroll to it at the top of the viewport if (targetBlock) { targetBlock.scrollIntoView({ behavior: 'smooth', block: "start" }); } else if (positions.length > 0) { // If no suitable block found, go to the first one positions[0].element.scrollIntoView({ behavior: 'smooth', block: "start" }); } } // Helper function to scroll to the next question function scrollToNextQuestion() { if (isDownButtonLongPress) return; // Skip if this is triggered by a long press const queryBlocks = Array.from(document.querySelectorAll('.group.relative.mb-1.flex.items-end.gap-0\\.5')); if (!queryBlocks.length) return; // Get all blocks positions const positions = queryBlocks.map(block => { const rect = block.getBoundingClientRect(); return { element: block, top: rect.top, bottom: rect.bottom }; }); // Sort by vertical position positions.sort((a, b) => a.top - b.top); // Find the first block below the middle of the viewport const viewportMiddle = window.innerHeight / 2; let targetBlock = null; for (let i = 0; i < positions.length; i++) { if (positions[i].top > viewportMiddle) { targetBlock = positions[i].element; break; } } // If we found a target block, scroll to it at the top of the viewport if (targetBlock) { targetBlock.scrollIntoView({ behavior: 'smooth', block: "start" }); } else if (positions.length > 0) { // If no suitable block found, try to find the Related section const relatedSection = document.querySelector('.default.font-display.text-lg.font-medium:has(.fa-new-thread)'); if (relatedSection) { relatedSection.scrollIntoView({ behavior: 'smooth', block: "start" }); } else { // Or go to the bottom window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); } } } // Helper function to scroll to the top of the page function scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); } // Helper function to scroll to the bottom of the page function scrollToBottom() { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); } // Floating buttons functionality function addFloatingButtons() { // Find all pre elements that don't already have our buttons const codeBlocks = document.querySelectorAll('pre:not(.buttons-added)'); codeBlocks.forEach(block => { // Mark this block as processed block.classList.add('buttons-added'); // Create the copy button with Perplexity's styling const copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.className = 'focus-visible:bg-offsetPlus dark:focus-visible:bg-offsetPlusDark hover:bg-offsetPlus text-textOff dark:text-textOffDark hover:text-textMain dark:hover:bg-offsetPlusDark dark:hover:text-textMainDark font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-out font-sans select-none items-center relative group/button justify-center text-center items-center rounded-full cursor-pointer active:scale-[0.97] active:duration-150 active:ease-outExpo origin-center whitespace-nowrap inline-flex text-sm h-8 aspect-square'; copyBtn.style.cssText = ` position: sticky; top: 95px; right: 40px; float: right; z-index: 100; margin-right: 5px; `; copyBtn.innerHTML = `
`; copyBtn.addEventListener('click', () => { const code = block.querySelector('code').innerText; navigator.clipboard.writeText(code); // Visual feedback const originalHTML = copyBtn.innerHTML; copyBtn.innerHTML = `
`; setTimeout(() => { copyBtn.innerHTML = originalHTML; }, 2000); }); // Create the up arrow button const upBtn = document.createElement('button'); upBtn.type = 'button'; upBtn.className = 'focus-visible:bg-offsetPlus dark:focus-visible:bg-offsetPlusDark hover:bg-offsetPlus text-textOff dark:text-textOffDark hover:text-textMain dark:hover:bg-offsetPlusDark dark:hover:text-textMainDark font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-out font-sans select-none items-center relative group/button justify-center text-center items-center rounded-full cursor-pointer active:scale-[0.97] active:duration-150 active:ease-outExpo origin-center whitespace-nowrap inline-flex text-sm h-8 aspect-square'; upBtn.style.cssText = ` position: sticky; top: 95px; right: 40px; float: right; z-index: 100; margin-right: 5px; `; upBtn.innerHTML = `
`; // Add long press functionality to up button upBtn.addEventListener('mousedown', () => { isUpButtonLongPress = false; upButtonTimer = setTimeout(() => { isUpButtonLongPress = true; scrollToTop(); // Visual feedback for long press upBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background setTimeout(() => { upBtn.style.backgroundColor = ''; }, 500); }, LONG_PRESS_DURATION); }); upBtn.addEventListener('mouseup', () => { clearTimeout(upButtonTimer); if (!isUpButtonLongPress) { scrollToPreviousQuestion(); } }); upBtn.addEventListener('mouseleave', () => { clearTimeout(upButtonTimer); }); upBtn.addEventListener('touchstart', (e) => { isUpButtonLongPress = false; upButtonTimer = setTimeout(() => { isUpButtonLongPress = true; scrollToTop(); // Visual feedback for long press upBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background setTimeout(() => { upBtn.style.backgroundColor = ''; }, 500); }, LONG_PRESS_DURATION); e.preventDefault(); // Prevent default touch behavior }, { passive: false }); upBtn.addEventListener('touchend', (e) => { clearTimeout(upButtonTimer); if (!isUpButtonLongPress) { scrollToPreviousQuestion(); } e.preventDefault(); }, { passive: false }); upBtn.addEventListener('touchcancel', () => { clearTimeout(upButtonTimer); }); // Create the down arrow button const downBtn = document.createElement('button'); downBtn.type = 'button'; downBtn.className = 'focus-visible:bg-offsetPlus dark:focus-visible:bg-offsetPlusDark hover:bg-offsetPlus text-textOff dark:text-textOffDark hover:text-textMain dark:hover:bg-offsetPlusDark dark:hover:text-textMainDark font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-out font-sans select-none items-center relative group/button justify-center text-center items-center rounded-full cursor-pointer active:scale-[0.97] active:duration-150 active:ease-outExpo origin-center whitespace-nowrap inline-flex text-sm h-8 aspect-square'; downBtn.style.cssText = ` position: sticky; top: 95px; right: 40px; float: right; z-index: 100; margin-right: 5px; `; downBtn.innerHTML = `
`; // Add long press functionality to down button downBtn.addEventListener('mousedown', () => { isDownButtonLongPress = false; downButtonTimer = setTimeout(() => { isDownButtonLongPress = true; scrollToBottom(); // Visual feedback for long press downBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background setTimeout(() => { downBtn.style.backgroundColor = ''; }, 500); }, LONG_PRESS_DURATION); }); downBtn.addEventListener('mouseup', () => { clearTimeout(downButtonTimer); if (!isDownButtonLongPress) { scrollToNextQuestion(); } }); downBtn.addEventListener('mouseleave', () => { clearTimeout(downButtonTimer); }); downBtn.addEventListener('touchstart', (e) => { isDownButtonLongPress = false; downButtonTimer = setTimeout(() => { isDownButtonLongPress = true; scrollToBottom(); // Visual feedback for long press downBtn.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; // Light blue background setTimeout(() => { downBtn.style.backgroundColor = ''; }, 500); }, LONG_PRESS_DURATION); e.preventDefault(); // Prevent default touch behavior }, { passive: false }); downBtn.addEventListener('touchend', (e) => { clearTimeout(downButtonTimer); if (!isDownButtonLongPress) { scrollToNextQuestion(); } e.preventDefault(); }, { passive: false }); downBtn.addEventListener('touchcancel', () => { clearTimeout(downButtonTimer); }); // Insert the buttons at the beginning of the pre element block.insertBefore(downBtn, block.firstChild); block.insertBefore(upBtn, block.firstChild); block.insertBefore(copyBtn, block.firstChild); }); } // Function to periodically check for new code blocks function checkForCodeBlocks() { addFloatingButtons(); } // Initial setup function init() { // Set up interval for checking code blocks setInterval(checkForCodeBlocks, CHECK_INTERVAL); // Initial check for code blocks setTimeout(checkForCodeBlocks, 1000); } // Initialize init(); // Listen for URL changes (for single-page apps) let lastUrl = window.location.href; new MutationObserver(() => { if (lastUrl !== window.location.href) { lastUrl = window.location.href; setTimeout(() => { addFloatingButtons(); }, 1000); // Check after URL change } }).observe(document, { subtree: true, childList: true }); })();