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