Utility tools for old Reddit - works alongside filter scripts
// ==UserScript==
// @name Old Reddit GearTools
// @namespace http://tampermonkey.net/
// @version 1.2.1
// @description Utility tools for old Reddit - works alongside filter scripts
// @author Crates
// @license MIT
// @match https://old.reddit.com/*
// @match https://www.reddit.com/*
// @grant GM_setClipboard
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_notification
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// ========== EARLY STYLE INJECTION (before DOM loads) ==========
// Inject critical styles immediately to prevent flicker
let sidebarHidden = GM_getValue('sidebarHidden', true);
let subCssDisabled = GM_getValue('subCssDisabled', false);
function updateEarlyStyles() {
const earlyStyleEl = document.getElementById('pwr-early-styles');
if (earlyStyleEl) {
earlyStyleEl.textContent = `
${sidebarHidden ? `
.side {
display: none !important;
}
.content {
margin-right: 10px !important;
}
` : ''}
${subCssDisabled ? `
link[title="applied_subreddit_stylesheet"] {
display: none !important;
}
` : ''}
`;
}
}
const earlyStyles = `
${sidebarHidden ? `
.side {
display: none !important;
}
.content {
margin-right: 10px !important;
}
` : ''}
${subCssDisabled ? `
link[title="applied_subreddit_stylesheet"] {
display: none !important;
}
` : ''}
`;
// Inject styles as early as possible
const earlyStyleEl = document.createElement('style');
earlyStyleEl.id = 'pwr-early-styles';
earlyStyleEl.textContent = earlyStyles;
(document.head || document.documentElement).appendChild(earlyStyleEl);
// Disable subreddit CSS immediately if setting is on
if (subCssDisabled) {
// Use MutationObserver to catch and disable stylesheet as soon as it's added
const cssObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.tagName === 'LINK' && node.title === 'applied_subreddit_stylesheet') {
node.disabled = true;
node.dataset.pwrDisabled = 'true';
}
});
});
});
cssObserver.observe(document.documentElement, { childList: true, subtree: true });
// Store observer to disconnect later
window.pwrCssObserver = cssObserver;
}
// ========== WAIT FOR DOM TO CONTINUE ==========
function onDOMReady(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn);
} else {
fn();
}
}
onDOMReady(function() {
// Only run on old reddit
if (!document.querySelector('#header-bottom-left')) return;
// Disconnect early CSS observer if it exists
if (window.pwrCssObserver) {
window.pwrCssObserver.disconnect();
}
// ========== STYLES ==========
const styles = `
#pwr-tools-trig {
cursor: pointer;
color: #369;
}
#pwr-tools-trig:hover {
text-decoration: underline;
}
#pwr-tools-trig svg {
vertical-align: middle;
margin-top: -2px;
}
#pwr-tools-dropdown {
display: none;
position: absolute;
top: 100%;
left: 0;
background: #fff;
border: 1px solid #c7c7c7;
border-radius: 0 0 3px 3px;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
z-index: 10001;
min-width: 220px;
font-size: 12px;
padding: 0;
}
#pwr-tools-dropdown.active {
display: block;
}
.pwr-tools-header {
background: #f6f7f8;
border-bottom: 1px solid #c7c7c7;
padding: 8px 12px;
font-weight: bold;
color: #333;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.pwr-tools-item {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
color: #333;
border-bottom: 1px solid #ededed;
transition: background 0.15s;
}
.pwr-tools-item:last-child {
border-bottom: none;
}
.pwr-tools-item:hover {
background: #f0f7ff;
}
.pwr-tools-item svg {
margin-right: 10px;
flex-shrink: 0;
color: #666;
}
.pwr-tools-item:hover svg {
color: #369;
}
.pwr-tools-item-text {
flex: 1;
}
.pwr-tools-item-title {
font-weight: 500;
color: #333;
}
.pwr-tools-item-desc {
font-size: 10px;
color: #888;
margin-top: 2px;
}
.pwr-tools-item.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pwr-tools-item.disabled:hover {
background: #fff;
}
.pwr-tools-toast {
position: fixed;
bottom: 20px;
right: 20px;
background: #333;
color: #fff;
padding: 12px 20px;
border-radius: 4px;
font-size: 13px;
z-index: 100000;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
animation: pwr-toast-in 0.3s ease;
}
.pwr-tools-toast.success {
background: #5a9e5a;
}
.pwr-tools-toast.error {
background: #c44;
}
@keyframes pwr-toast-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.pwr-tools-li {
position: relative;
}
/* Modal styles */
.pwr-tools-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 100001;
display: flex;
align-items: center;
justify-content: center;
}
.pwr-tools-modal {
background: #fff;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
max-width: 500px;
width: 90%;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.pwr-tools-modal-header {
padding: 15px 20px;
border-bottom: 1px solid #c7c7c7;
font-weight: bold;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
}
.pwr-tools-modal-close {
cursor: pointer;
font-size: 20px;
color: #888;
line-height: 1;
}
.pwr-tools-modal-close:hover {
color: #333;
}
.pwr-tools-modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.pwr-tools-modal textarea {
width: 100%;
height: 200px;
font-family: monospace;
font-size: 11px;
border: 1px solid #c7c7c7;
border-radius: 3px;
padding: 10px;
resize: vertical;
}
.pwr-tools-modal-footer {
padding: 15px 20px;
border-top: 1px solid #c7c7c7;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.pwr-tools-btn {
padding: 8px 16px;
border: 1px solid #c7c7c7;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
background: #f6f7f8;
}
.pwr-tools-btn:hover {
background: #eee;
}
.pwr-tools-btn-primary {
background: #369;
color: #fff;
border-color: #369;
}
.pwr-tools-btn-primary:hover {
background: #2a5a8a;
}
.pwr-tools-count {
background: #369;
color: #fff;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
margin-left: 8px;
}
/* NSFW Blur styles */
.pwr-nsfw-blurred a.thumbnail img {
filter: blur(15px) !important;
transition: filter 0.2s;
}
.pwr-nsfw-blurred a.thumbnail:hover img {
filter: blur(5px) !important;
}
.pwr-nsfw-blur-content {
filter: blur(20px);
transition: filter 0.2s;
}
.pwr-nsfw-blur-content:hover {
filter: blur(3px);
}
.pwr-tools-active {
background: #5a9e5a;
color: #fff;
font-size: 9px;
padding: 2px 5px;
border-radius: 3px;
margin-left: 6px;
font-weight: normal;
}
/* Sidebar hidden styles */
.pwr-sidebar-hidden .side {
display: none !important;
}
.pwr-sidebar-hidden .content {
margin-right: 10px !important;
}
/* Disable sub CSS indicator */
.pwr-nocss-active link[rel="stylesheet"][href*="reddit.com/r/"],
.pwr-nocss-active style[data-subreddit],
.pwr-nocss-active .stylesheet-customize-container {
display: none !important;
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = styles;
document.head.appendChild(styleEl);
// ========== ICONS ==========
const icons = {
gear: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>`,
video: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="23 7 16 12 23 17 23 7"></polygon>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
</svg>`,
image: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>`,
link: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>`,
expand: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 3 21 3 21 9"></polyline>
<polyline points="9 21 3 21 3 15"></polyline>
<line x1="21" y1="3" x2="14" y2="10"></line>
<line x1="3" y1="21" x2="10" y2="14"></line>
</svg>`,
collapse: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 14 10 14 10 20"></polyline>
<polyline points="20 10 14 10 14 4"></polyline>
<line x1="14" y1="10" x2="21" y2="3"></line>
<line x1="3" y1="21" x2="10" y2="14"></line>
</svg>`,
download: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>`,
eye: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>`,
eyeOff: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>`,
sidebar: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="15" y1="3" x2="15" y2="21"></line>
</svg>`,
load: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="1 4 1 10 7 10"></polyline>
<path d="M3.51 15a9 9 0 1 0 .49-3.5"></path>
</svg>`,
css: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
<line x1="14" y1="4" x2="10" y2="20"></line>
</svg>`
};
// ========== UTILITIES ==========
function escapeHtml(s) {
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function showToast(message, type = 'info') {
const existing = document.querySelector('.pwr-tools-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = `pwr-tools-toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
function getVisiblePosts() {
// Get all posts, respecting any filter that may have hidden some
const posts = document.querySelectorAll('#siteTable > .thing.link');
return Array.from(posts).filter(post => {
const style = window.getComputedStyle(post);
return style.display !== 'none' && style.visibility !== 'hidden';
});
}
function copyToClipboard(text) {
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(text);
return true;
}
// Fallback
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
return success;
}
function showModal(title, content, buttons = []) {
const overlay = document.createElement('div');
overlay.className = 'pwr-tools-modal-overlay';
const modal = document.createElement('div');
modal.className = 'pwr-tools-modal';
modal.innerHTML = `
<div class="pwr-tools-modal-header">
<span>${title}</span>
<span class="pwr-tools-modal-close">×</span>
</div>
<div class="pwr-tools-modal-body">${content}</div>
<div class="pwr-tools-modal-footer"></div>
`;
const footer = modal.querySelector('.pwr-tools-modal-footer');
buttons.forEach(btn => {
const button = document.createElement('button');
button.className = `pwr-tools-btn ${btn.primary ? 'pwr-tools-btn-primary' : ''}`;
button.textContent = btn.text;
button.onclick = () => {
if (btn.onClick) btn.onClick(modal);
if (btn.close !== false) overlay.remove();
};
footer.appendChild(button);
});
modal.querySelector('.pwr-tools-modal-close').onclick = () => overlay.remove();
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
overlay.appendChild(modal);
document.body.appendChild(overlay);
return modal;
}
// ========== TOOL FUNCTIONS ==========
// Tool 1: Copy Video URLs (Redgifs, Imgur, Gfycat, etc.)
function copyVideoUrls() {
const posts = getVisiblePosts();
const videoUrls = [];
const videoPatterns = [
/redgifs\.com/i,
/gfycat\.com/i,
/imgur\.com.*\.(gifv|mp4|gif)/i,
/v\.redd\.it/i,
/streamable\.com/i,
/streamja\.com/i,
/streamff\.com/i,
/dubz\.co/i,
/streamwo\.com/i
];
posts.forEach(post => {
const link = post.querySelector('a.title');
if (!link) return;
const href = link.href;
if (videoPatterns.some(p => p.test(href))) {
videoUrls.push({
title: link.textContent.trim().substring(0, 60),
url: href
});
}
// Also check for embedded v.redd.it
const dataUrl = post.getAttribute('data-url');
if (dataUrl && /v\.redd\.it/i.test(dataUrl) && !videoUrls.find(v => v.url === dataUrl)) {
videoUrls.push({
title: link.textContent.trim().substring(0, 60),
url: dataUrl
});
}
});
if (videoUrls.length === 0) {
showToast('No video URLs found in visible posts', 'error');
return;
}
const urlText = videoUrls.map(v => v.url).join('\n');
const listHtml = videoUrls.map(v =>
`<div style="margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #eee;">
<div style="font-size:11px;color:#666;margin-bottom:2px;">${escapeHtml(v.title)}...</div>
<div style="font-family:monospace;font-size:10px;word-break:break-all;">${escapeHtml(v.url)}</div>
</div>`
).join('');
showModal(
`Video URLs Found (${videoUrls.length})`,
`<div style="max-height:300px;overflow-y:auto;margin-bottom:10px;">${listHtml}</div>
<textarea readonly style="width:100%;height:100px;font-size:10px;">${urlText}</textarea>`,
[
{ text: 'Cancel' },
{ text: 'Copy All', primary: true, onClick: () => {
copyToClipboard(urlText);
showToast(`Copied ${videoUrls.length} video URLs!`, 'success');
}}
]
);
}
// Tool 2: Copy Image URLs
function copyImageUrls() {
const posts = getVisiblePosts();
const imageUrls = [];
const imagePatterns = [
/i\.redd\.it/i,
/imgur\.com.*\.(jpg|jpeg|png|gif)$/i,
/i\.imgur\.com/i,
/preview\.redd\.it/i
];
posts.forEach(post => {
const link = post.querySelector('a.title');
if (!link) return;
const href = link.href;
const dataUrl = post.getAttribute('data-url') || '';
// Check main link
if (imagePatterns.some(p => p.test(href)) || /\.(jpg|jpeg|png|gif|webp)$/i.test(href)) {
imageUrls.push({
title: link.textContent.trim().substring(0, 60),
url: href
});
}
// Check data-url
else if (imagePatterns.some(p => p.test(dataUrl)) || /\.(jpg|jpeg|png|gif|webp)$/i.test(dataUrl)) {
imageUrls.push({
title: link.textContent.trim().substring(0, 60),
url: dataUrl
});
}
});
if (imageUrls.length === 0) {
showToast('No image URLs found in visible posts', 'error');
return;
}
const urlText = imageUrls.map(i => i.url).join('\n');
showModal(
`Image URLs Found (${imageUrls.length})`,
`<textarea readonly style="width:100%;height:250px;font-size:10px;">${urlText}</textarea>`,
[
{ text: 'Cancel' },
{ text: 'Copy All', primary: true, onClick: () => {
copyToClipboard(urlText);
showToast(`Copied ${imageUrls.length} image URLs!`, 'success');
}}
]
);
}
// Tool 3: Copy All Post Links
function copyAllPostLinks() {
const posts = getVisiblePosts();
const links = [];
posts.forEach(post => {
const titleLink = post.querySelector('a.title');
const commentsLink = post.querySelector('a.comments');
if (titleLink) {
links.push({
title: titleLink.textContent.trim().substring(0, 80),
contentUrl: titleLink.href,
commentsUrl: commentsLink ? commentsLink.href : null
});
}
});
if (links.length === 0) {
showToast('No posts found', 'error');
return;
}
const format = `Title | Content URL | Comments URL\n${'='.repeat(80)}\n` +
links.map(l => `${l.title}\n Content: ${l.contentUrl}\n Comments: ${l.commentsUrl || 'N/A'}`).join('\n\n');
showModal(
`Post Links (${links.length})`,
`<textarea readonly style="width:100%;height:300px;font-size:10px;">${format}</textarea>`,
[
{ text: 'Cancel' },
{ text: 'Copy URLs Only', onClick: () => {
const urlsOnly = links.map(l => l.contentUrl).join('\n');
copyToClipboard(urlsOnly);
showToast(`Copied ${links.length} content URLs!`, 'success');
}},
{ text: 'Copy All', primary: true, onClick: () => {
copyToClipboard(format);
showToast(`Copied ${links.length} post details!`, 'success');
}}
]
);
}
// Tool 4: Expand/Collapse All Previews
function toggleAllPreviews() {
const posts = getVisiblePosts();
const expanders = [];
posts.forEach(post => {
// Find expando button - check it exists and is visible
const expando = post.querySelector('.expando-button');
if (expando &&
!expando.classList.contains('hidden') &&
expando.offsetParent !== null) {
expanders.push(expando);
}
});
if (expanders.length === 0) {
showToast('No expandable content found', 'error');
return;
}
// Determine current state - if most are collapsed, expand; otherwise collapse
const collapsedCount = expanders.filter(btn => btn.classList.contains('collapsed')).length;
const shouldExpand = collapsedCount > expanders.length / 2;
let toggled = 0;
// Store scroll position
const scrollPos = window.scrollY;
expanders.forEach(btn => {
const isCollapsed = btn.classList.contains('collapsed');
if ((shouldExpand && isCollapsed) || (!shouldExpand && !isCollapsed)) {
// Simple click - view property doesn't work in userscript sandbox
btn.click();
toggled++;
}
});
// Restore scroll position after a brief delay
requestAnimationFrame(() => {
window.scrollTo(window.scrollX, scrollPos);
});
showToast(`${shouldExpand ? 'Expanded' : 'Collapsed'} ${toggled} previews`, 'success');
}
// Tool 6: Blur NSFW Content
let nsfwBlurred = GM_getValue('nsfwBlurred', false);
function applyNsfwBlur() {
const posts = document.querySelectorAll('#siteTable > .thing.link');
posts.forEach(post => {
const isNsfw = post.classList.contains('over18') || post.querySelector('.nsfw-stamp');
const thumbnail = post.querySelector('a.thumbnail img');
const expando = post.querySelector('.expando');
if (isNsfw) {
if (nsfwBlurred) {
post.classList.add('pwr-nsfw-blurred');
if (thumbnail) thumbnail.style.filter = 'blur(15px)';
if (expando) expando.classList.add('pwr-nsfw-blur-content');
} else {
post.classList.remove('pwr-nsfw-blurred');
if (thumbnail) thumbnail.style.filter = '';
if (expando) expando.classList.remove('pwr-nsfw-blur-content');
}
}
});
}
function toggleNsfwBlur() {
nsfwBlurred = !nsfwBlurred;
GM_setValue('nsfwBlurred', nsfwBlurred);
applyNsfwBlur();
const count = document.querySelectorAll('.pwr-nsfw-blurred').length;
showToast(nsfwBlurred ? `NSFW blur enabled (${count} posts)` : 'NSFW blur disabled', 'success');
// Update the menu item indicator
updateNsfwMenuItem();
}
function updateNsfwMenuItem() {
const nsfwItem = document.querySelector('[data-tool="nsfw"]');
if (nsfwItem) {
const title = nsfwItem.querySelector('.pwr-tools-item-title');
if (title) {
title.innerHTML = nsfwBlurred ? 'Blur NSFW <span class="pwr-tools-active">ON</span>' : 'Blur NSFW';
}
}
}
// Tool 7: Toggle Sidebar
// sidebarHidden is declared at top level for early style injection
function applySidebarState() {
const body = document.body;
if (sidebarHidden) {
body.classList.add('pwr-sidebar-hidden');
} else {
body.classList.remove('pwr-sidebar-hidden');
}
// Update early styles
updateEarlyStyles();
}
function toggleSidebar() {
sidebarHidden = !sidebarHidden;
GM_setValue('sidebarHidden', sidebarHidden);
applySidebarState();
showToast(sidebarHidden ? 'Sidebar hidden' : 'Sidebar visible', 'success');
// Update the menu item indicator
updateSidebarMenuItem();
}
function updateSidebarMenuItem() {
const sidebarItem = document.querySelector('[data-tool="sidebar"]');
if (sidebarItem) {
const title = sidebarItem.querySelector('.pwr-tools-item-title');
if (title) {
title.innerHTML = sidebarHidden ? 'Toggle Sidebar <span class="pwr-tools-active">HIDDEN</span>' : 'Toggle Sidebar';
}
}
}
// Tool 8: Disable Subreddit CSS
// subCssDisabled is declared at top level for early style injection
function applySubCssState() {
const body = document.body;
if (subCssDisabled) {
body.classList.add('pwr-nocss-active');
// Also remove subreddit stylesheet links directly
document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
if (link.href && link.href.includes('/r/') && link.href.includes('stylesheet')) {
link.disabled = true;
link.dataset.pwrDisabled = 'true';
}
});
// Handle the main subreddit stylesheet
const subStylesheet = document.querySelector('link[title="applied_subreddit_stylesheet"]');
if (subStylesheet) {
subStylesheet.disabled = true;
subStylesheet.dataset.pwrDisabled = 'true';
}
} else {
body.classList.remove('pwr-nocss-active');
// Re-enable stylesheets
document.querySelectorAll('link[data-pwr-disabled="true"]').forEach(link => {
link.disabled = false;
delete link.dataset.pwrDisabled;
});
}
// Update early styles
updateEarlyStyles();
}
function toggleSubCss() {
subCssDisabled = !subCssDisabled;
GM_setValue('subCssDisabled', subCssDisabled);
applySubCssState();
showToast(subCssDisabled ? 'Subreddit CSS disabled' : 'Subreddit CSS enabled', 'success');
// Update the menu item indicator
updateSubCssMenuItem();
}
function updateSubCssMenuItem() {
const cssItem = document.querySelector('[data-tool="subcss"]');
if (cssItem) {
const title = cssItem.querySelector('.pwr-tools-item-title');
if (title) {
title.innerHTML = subCssDisabled ? 'Disable Sub CSS <span class="pwr-tools-active">OFF</span>' : 'Disable Sub CSS';
}
}
}
// Watch for new posts loaded dynamically (RES infinite scroll, etc.)
const observer = new MutationObserver((mutations) => {
if (nsfwBlurred) {
applyNsfwBlur();
}
});
observer.observe(document.body, { childList: true, subtree: true });
// Tool 5: Quick Stats for Visible Posts
function showQuickStats() {
const posts = getVisiblePosts();
const stats = {
total: posts.length,
images: 0,
videos: 0,
links: 0,
selfPosts: 0,
totalScore: 0,
totalComments: 0,
domains: {}
};
posts.forEach(post => {
// Score
const score = post.querySelector('.score.unvoted');
if (score) {
const val = parseInt(score.textContent);
if (!isNaN(val)) stats.totalScore += val;
}
// Comments
const comments = post.querySelector('a.comments');
if (comments) {
const match = comments.textContent.match(/(\d+)/);
if (match) stats.totalComments += parseInt(match[1]);
}
// Type
if (post.classList.contains('self')) {
stats.selfPosts++;
} else {
const dataUrl = post.getAttribute('data-url') || '';
const titleHref = post.querySelector('a.title')?.href || '';
if (/v\.redd\.it|redgifs|gfycat|streamable/i.test(dataUrl + titleHref)) {
stats.videos++;
} else if (/i\.redd\.it|imgur|preview\.redd\.it/i.test(dataUrl + titleHref) ||
/\.(jpg|jpeg|png|gif|webp)$/i.test(dataUrl + titleHref)) {
stats.images++;
} else {
stats.links++;
}
}
// Domain
const domain = post.querySelector('.domain a');
if (domain) {
const d = domain.textContent.replace(/[()]/g, '');
stats.domains[d] = (stats.domains[d] || 0) + 1;
}
});
const topDomains = Object.entries(stats.domains)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([d, c]) => `${d}: ${c}`)
.join('<br>');
showModal(
'Quick Stats for Visible Posts',
`<table style="width:100%;border-collapse:collapse;">
<tr><td style="padding:8px;border-bottom:1px solid #eee;"><strong>Total Posts</strong></td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.total}</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid #eee;">📷 Images</td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.images}</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid #eee;">🎬 Videos</td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.videos}</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid #eee;">🔗 Links</td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.links}</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid #eee;">📝 Self Posts</td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.selfPosts}</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid #eee;"><strong>Total Score</strong></td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.totalScore.toLocaleString()}</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid #eee;"><strong>Total Comments</strong></td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.totalComments.toLocaleString()}</td></tr>
<tr><td colspan="2" style="padding:12px 8px 4px;"><strong>Top Domains</strong></td></tr>
<tr><td colspan="2" style="padding:4px 8px 8px;font-size:11px;color:#666;">${topDomains || 'None'}</td></tr>
</table>`,
[{ text: 'Close', primary: true }]
);
}
// Tool 9: Load N Posts
function loadAndExpand() {
// Don't run on comment pages
if (document.querySelector('.commentarea')) {
showToast('Not available on comment pages', 'error');
return;
}
const defaultTarget = 200;
let cancelRequested = false;
showModal(
'Load Posts',
`<div style="margin-bottom:12px;font-size:13px;color:#555;">
Loads posts via infinite scroll until the target count is reached.
</div>
<div style="display:flex;align-items:center;gap:10px;">
<label style="font-size:12px;font-weight:bold;color:#333;white-space:nowrap;">Target posts:</label>
<input id="pwr-load-target" type="number" min="25" max="2000" step="25" value="${defaultTarget}"
style="width:90px;padding:6px 8px;border:1px solid #c7c7c7;border-radius:3px;font-size:13px;">
</div>
<div id="pwr-load-status" style="margin-top:12px;font-size:12px;color:#888;min-height:18px;"></div>`,
[
{ text: 'Close', close: false, onClick: (modal) => {
cancelRequested = true;
modal.closest('.pwr-tools-modal-overlay')?.remove();
}},
{ text: 'Load Posts', primary: true, close: false, onClick: (modal) => {
const input = modal.querySelector('#pwr-load-target');
const target = Math.max(25, Math.min(2000, parseInt(input.value) || defaultTarget));
const statusEl = modal.querySelector('#pwr-load-status');
const loadBtn = modal.querySelector('.pwr-tools-btn-primary');
const closeBtn = modal.querySelector('button:not(.pwr-tools-btn-primary)');
// Swap Load button for Cancel during loading
loadBtn.disabled = true;
loadBtn.style.display = 'none';
closeBtn.textContent = 'Cancel';
statusEl.style.color = '#369';
runLoadAndExpand(target, statusEl, () => cancelRequested, () => {
closeBtn.textContent = cancelRequested ? 'Close' : 'Done';
loadBtn.style.display = '';
loadBtn.disabled = false;
loadBtn.textContent = 'Load Again';
if (statusEl.style.color !== 'rgb(204, 68, 68)') {
statusEl.style.color = '#555';
}
});
}}
]
);
}
function runLoadAndExpand(target, statusEl, isCancelled, onDone) {
// We fetch up to BATCH_SIZE pages concurrently. Since each Reddit page URL
// requires the `after` token from the previous response, we can't fire all
// requests at once — but we can chain them: fetch pages 1-4 in sequence with
// no gap between them, collect their docs, append in order, then immediately
// fire the next batch of 4. This keeps 1 request always in flight and
// saturates the pipeline without hammering Reddit too hard.
const BATCH_SIZE = 4;
const siteTable = document.querySelector('.sitetable.linklisting');
if (!siteTable) {
if (statusEl) statusEl.textContent = 'Could not find post list.';
onDone();
return;
}
function updateStatus(msg) {
if (statusEl) statusEl.textContent = msg;
}
function getPostCount() {
return document.querySelectorAll('.thing.link').length;
}
async function fetchPage(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
return new DOMParser().parseFromString(html, 'text/html');
}
// Append posts from a parsed doc. Returns next page URL or null.
// Does NOT restore scroll — posts append at the bottom and page grows naturally.
function appendFromDoc(doc) {
doc.querySelectorAll('.thing.link').forEach(post => {
const clone = post.cloneNode(true);
clone.removeAttribute('data-numbered');
siteTable.appendChild(clone);
});
const newNextHref = doc.querySelector('.next-button a')?.href || null;
const liveNextBtn = document.querySelector('.next-button a');
if (liveNextBtn && newNextHref) {
liveNextBtn.href = newNextHref;
} else if (liveNextBtn && !newNextHref) {
liveNextBtn.closest('.next-button')?.remove();
}
// Let FilterTools/AutoTools observers process the new posts
window.dispatchEvent(new Event('scroll'));
return newNextHref;
}
// Fetch a chain of up to `count` pages sequentially (no gaps),
// firing each request the moment the previous URL is known.
// Returns array of parsed docs in order, plus the final next URL.
async function fetchBatch(startUrl, count) {
const docs = [];
let url = startUrl;
for (let i = 0; i < count && url; i++) {
const doc = await fetchPage(url);
docs.push(doc);
url = doc.querySelector('.next-button a')?.href || null;
// Fire next fetch immediately — no await gap between requests
}
return { docs, nextUrl: url };
}
async function run() {
let nextUrl = document.querySelector('.next-button a')?.href || null;
if (!nextUrl) {
updateStatus(`Already at end — ${getPostCount()} posts loaded.`);
onDone();
return;
}
while (nextUrl && getPostCount() < target) {
if (isCancelled()) {
updateStatus(`Cancelled — ${getPostCount()} posts loaded.`);
statusEl.style.color = '#888';
onDone();
return;
}
updateStatus(`Loading... ${getPostCount()} / ${target} posts`);
const remaining = target - getPostCount();
const batchCount = Math.min(BATCH_SIZE, Math.ceil(remaining / 25));
try {
const { docs, nextUrl: discovered } = await fetchBatch(nextUrl, batchCount);
// Append all pages from this batch in order
let lastNext = null;
for (const doc of docs) {
lastNext = appendFromDoc(doc);
}
nextUrl = discovered || lastNext;
} catch (err) {
console.warn('[GearTools] Load error:', err);
updateStatus(`Done! Loaded ${getPostCount()} posts (fetch error).`);
onDone();
return;
}
}
updateStatus(`Done! Loaded ${getPostCount()} posts.`);
onDone();
}
run().catch(err => {
console.error('[GearTools] runLoad failed:', err);
if (statusEl) statusEl.textContent = 'Error during loading.';
onDone();
});
}
// ========== BUILD UI ==========
function buildToolsMenu() {
const tabMenu = document.querySelector('.tabmenu');
if (!tabMenu) return;
// Find the filter trigger (pwr-trig) to place our icon next to it
const filterTrig = document.getElementById('pwr-trig');
const filterLi = filterTrig ? filterTrig.closest('li') : null;
// Create our tools tab
const toolsLi = document.createElement('li');
toolsLi.className = 'pwr-tools-li';
const toolsTrig = document.createElement('a');
toolsTrig.href = '#';
toolsTrig.className = 'choice';
toolsTrig.id = 'pwr-tools-trig';
toolsTrig.title = 'Reddit Tools';
toolsTrig.innerHTML = icons.gear;
// Create dropdown
const dropdown = document.createElement('div');
dropdown.id = 'pwr-tools-dropdown';
dropdown.innerHTML = `
<div class="pwr-tools-header">Tools</div>
<div class="pwr-tools-item" data-tool="videos">
${icons.video}
<div class="pwr-tools-item-text">
<div class="pwr-tools-item-title">Copy Video URLs</div>
<div class="pwr-tools-item-desc">Redgifs, Gfycat, Streamable, v.redd.it</div>
</div>
</div>
<div class="pwr-tools-item" data-tool="images">
${icons.image}
<div class="pwr-tools-item-text">
<div class="pwr-tools-item-title">Copy Image URLs</div>
<div class="pwr-tools-item-desc">i.redd.it, Imgur, direct images</div>
</div>
</div>
<div class="pwr-tools-item" data-tool="links">
${icons.link}
<div class="pwr-tools-item-text">
<div class="pwr-tools-item-title">Copy All Post Links</div>
<div class="pwr-tools-item-desc">Export all visible post URLs</div>
</div>
</div>
<div class="pwr-tools-item" data-tool="expand">
${icons.expand}
<div class="pwr-tools-item-text">
<div class="pwr-tools-item-title">Toggle All Previews</div>
<div class="pwr-tools-item-desc">Expand or collapse all media</div>
</div>
</div>
<div class="pwr-tools-item" data-tool="loadexpand">
${icons.load}
<div class="pwr-tools-item-text">
<div class="pwr-tools-item-title">Load N Posts</div>
<div class="pwr-tools-item-desc">Scroll-load posts to a target count</div>
</div>
</div>
<div class="pwr-tools-item" data-tool="stats">
${icons.download}
<div class="pwr-tools-item-text">
<div class="pwr-tools-item-title">Quick Stats</div>
<div class="pwr-tools-item-desc">View stats for visible posts</div>
</div>
</div>
<div class="pwr-tools-item" data-tool="nsfw">
${icons.eyeOff}
<div class="pwr-tools-item-text">
<div class="pwr-tools-item-title">Blur NSFW</div>
<div class="pwr-tools-item-desc">Toggle blur on NSFW thumbnails & content</div>
</div>
</div>
<div class="pwr-tools-item" data-tool="sidebar">
${icons.sidebar}
<div class="pwr-tools-item-text">
<div class="pwr-tools-item-title">Toggle Sidebar</div>
<div class="pwr-tools-item-desc">Show or hide the sidebar</div>
</div>
</div>
<div class="pwr-tools-item" data-tool="subcss">
${icons.css}
<div class="pwr-tools-item-text">
<div class="pwr-tools-item-title">Disable Sub CSS</div>
<div class="pwr-tools-item-desc">Toggle subreddit custom styles</div>
</div>
</div>
`;
// Tool actions
dropdown.querySelectorAll('.pwr-tools-item').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const tool = item.dataset.tool;
dropdown.classList.remove('active');
switch (tool) {
case 'videos': copyVideoUrls(); break;
case 'images': copyImageUrls(); break;
case 'links': copyAllPostLinks(); break;
case 'expand': toggleAllPreviews(); break;
case 'loadexpand': loadAndExpand(); break;
case 'stats': showQuickStats(); break;
case 'nsfw': toggleNsfwBlur(); break;
case 'sidebar': toggleSidebar(); break;
case 'subcss': toggleSubCss(); break;
}
});
});
// Toggle dropdown
toolsTrig.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Close FilterTools dropdown if open
const filterMenu = document.getElementById('orfs-menu');
if (filterMenu) filterMenu.classList.remove('active');
dropdown.classList.toggle('active');
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!toolsLi.contains(e.target)) {
dropdown.classList.remove('active');
}
});
toolsLi.appendChild(toolsTrig);
toolsLi.appendChild(dropdown);
// Insert after filter if it exists, otherwise at the end
if (filterLi) {
filterLi.insertAdjacentElement('afterend', toolsLi);
} else {
tabMenu.appendChild(toolsLi);
}
}
// ========== INIT ==========
function init() {
buildToolsMenu();
// Apply sidebar state (hidden by default)
applySidebarState();
setTimeout(updateSidebarMenuItem, 100);
// Apply sub CSS state (disabled by default)
applySubCssState();
setTimeout(updateSubCssMenuItem, 100);
// Apply NSFW blur if it was previously enabled
if (nsfwBlurred) {
applyNsfwBlur();
// Small delay to ensure DOM is ready for menu update
setTimeout(updateNsfwMenuItem, 100);
}
}
// Run init
init();
}); // end onDOMReady
})();