Floating customization panel with drag, keyboard shortcuts, and preset profiles
// ==UserScript==
// @name Reddit Customizer Pro
// @namespace https://greatest.deepsurf.us/users/YOUR_USER_ID
// @version 3.0.0
// @description Floating customization panel with drag, keyboard shortcuts, and preset profiles
// @author YourNameOrHandle
// @match https://*.reddit.com/*
// @match https://*.old.reddit.com/*
// @grant none
// @run-at document-idle
// @license MIT
// @supportURL https://greatest.deepsurf.us/scripts/SCRIPT_ID/feedback
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'reddit_customizer_v3';
const DEFAULT_SETTINGS = {
fontSize: '14', compactMode: false, hideSidebar: false, hideTopNav: false,
hideAds: true, hideChat: true, hideAwards: false, maxWidth: '960',
thumbnailSize: 'medium', hideCommentScores: false, highlightOP: true,
disableAnimations: false, customScrollbar: true, bgImage: '', bgOverlayOpacity: '0.85',
accentColor: '#ff4500', panelTransparency: '0.95', customCSS: ''
};
let state = loadState();
let panel = null, btn = null, styleEl = null;
let observer = null, updateTimer = null;
// ─── State Management ──────────────────────────────────────────────
function getDefaultState() {
return {
settings: { ...DEFAULT_SETTINGS },
presets: { "Default": { ...DEFAULT_SETTINGS } },
activePreset: "Default",
positions: { fab: { x: window.innerWidth - 68, y: window.innerHeight - 68 }, panel: { x: window.innerWidth - 360, y: window.innerHeight - 140 } }
};
}
function loadState() {
try {
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY));
const def = getDefaultState();
return { ...def, ...stored, settings: { ...def.settings, ...(stored?.settings || {}) } };
} catch { return getDefaultState(); }
}
function saveState() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function applyPreset(name) {
if (!state.presets[name]) return;
state.settings = { ...state.presets[name] };
state.activePreset = name;
saveState();
applyStyles();
if (panel) populatePanel();
}
function saveCurrentAsPreset(name) {
name = name.trim();
if (!name) return alert("Preset name cannot be empty.");
state.presets[name] = { ...state.settings };
state.activePreset = name;
saveState();
populatePanel();
}
function deletePreset(name) {
if (name === "Default") return alert("Cannot delete Default preset.");
if (!confirm(`Delete preset "${name}"?`)) return;
delete state.presets[name];
if (state.activePreset === name) applyPreset("Default");
saveState();
populatePanel();
}
// ─── CSS Engine ────────────────────────────────────────────────────
function generateCSS() {
const s = state.settings;
const thumb = { small: '60px', medium: '100px', large: '140px' }[s.thumbnailSize] || '100px';
let css = `
:root { --rc-font: ${s.fontSize}px; --rc-accent: ${s.accentColor}; --rc-panel-bg: rgba(22, 22, 23, ${s.panelTransparency}); }
body, .shreddit-post, .Post, .comment, .scrollerItem, .md { font-size: var(--rc-font) !important; }
`;
if (s.maxWidth !== '100%') css += `@media(min-width:1024px){shreddit-app,main,.main-container{max-width:${s.maxWidth}px!important;margin:0 auto!important;}}`;
if (s.compactMode) css += `shreddit-post,.Post,.scrollerItem{padding:6px 8px!important;margin-bottom:2px!important;}shreddit-comment,.comment{margin-bottom:4px!important;}.flex{padding:4px 0!important;}`;
if (s.hideSidebar) css += `shreddit-sidebar,.sidebar,.right-sidebar,.side{display:none!important;}`;
if (s.hideTopNav) css += `shreddit-header,header,.shreddit-header{display:none!important;}`;
if (s.hideAds) css += `shreddit-promoted,.promotedlink,.promoted,[data-ad-location],[data-promoted],.scrollerItem[data-testid="ad-post"]{display:none!important;}`;
if (s.hideChat) css += `chat-app,.chat-widget,[data-testid="chat-widget"],#chat-widget{display:none!important;}`;
if (s.hideAwards) css += `.award-icon,.awards,shreddit-awards{display:none!important;}`;
if (s.hideCommentScores) css += `score-tag,.comment-score,.score{display:none!important;}`;
if (s.highlightOP) css += `shreddit-comment[op="true"],.comment.op{border-left:3px solid var(--rc-accent)!important;background:rgba(255,69,0,0.05)!important;}`;
css += `.post-image-container,.Post .post-thumbnail{width:${thumb}!important;height:${thumb}!important;min-height:${thumb}!important;}`;
if (s.disableAnimations) css += `*,*::before,*::after{animation-duration:0.001ms!important;transition-duration:0.001ms!important;scroll-behavior:auto!important;}`;
if (s.customScrollbar) css += `::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#161617}::-webkit-scrollbar-thumb{background:#444;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--rc-accent)}`;
if (s.bgImage.trim()) css += `body{background-image:url('${s.bgImage}')!important;background-size:cover!important;background-attachment:fixed!important;background-position:center!important;}shreddit-app,main,.main-container{background:rgba(20,20,21,${s.bgOverlayOpacity})!important;}`;
if (s.customCSS.trim()) css += `\n/* User CSS */\n${s.customCSS.trim()}\n`;
return css;
}
function applyStyles() {
if (!styleEl) { styleEl = document.createElement('style'); styleEl.id = 'rc-styles'; document.head.appendChild(styleEl); }
styleEl.textContent = generateCSS();
}
// ─── UI Builder ────────────────────────────────────────────────────
function createButton() {
btn = document.createElement('button');
btn.id = 'rc-fab';
btn.innerHTML = '⚙️';
btn.title = 'Open Customizer (Alt+C)';
btn.style.cssText = `position:fixed;width:48px;height:48px;background:${state.settings.accentColor};color:#fff;border:none;border-radius:50%;font-size:22px;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,0.3);z-index:999999;transition:transform 0.2s;display:flex;align-items:center;justify-content:center;`;
document.body.appendChild(btn);
makeDraggable(btn, 'fab');
btn.addEventListener('click', togglePanel);
btn.addEventListener('mouseenter', () => btn.style.transform = 'scale(1.1)');
btn.addEventListener('mouseleave', () => btn.style.transform = 'scale(1)');
}
function createPanel() {
panel = document.createElement('div');
panel.id = 'rc-panel';
panel.style.cssText = `position:fixed;width:340px;max-width:92vw;max-height:80vh;background:var(--rc-panel-bg);color:#d7dadc;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,0.4);padding:14px;z-index:999998;display:none;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;border:1px solid #333;backdrop-filter:blur(8px);user-select:none;`;
const presetOptions = Object.keys(state.presets).map(p => `<option value="${p}" ${p===state.activePreset?'selected':''}>${p}</option>`).join('');
panel.innerHTML = `
<div id="rc-drag-handle" style="cursor:grab;padding:0 0 8px 0;border-bottom:1px solid #333;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;color:${state.settings.accentColor};font-size:17px;">Reddit Customizer</h3>
<button id="rc-close" style="background:none;border:none;color:#888;cursor:pointer;font-size:20px;padding:2px 6px;border-radius:4px;">✕</button>
</div>
<div style="max-height:65vh;overflow-y:auto;padding-right:4px;scrollbar-width:thin;scrollbar-color:#444 transparent;">
<!-- Presets -->
<section style="margin-bottom:12px;background:#222;padding:8px;border-radius:8px;">
<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px;">
<select id="rc-preset-sel" style="flex:1;background:#272729;color:#d7dadc;border:1px solid #444;border-radius:4px;padding:4px;font-size:12px;">${presetOptions}</select>
<button id="rc-save-preset" title="Save current as preset" style="background:#333;color:#aaa;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;">💾</button>
<button id="rc-del-preset" title="Delete preset" style="background:#333;color:#aaa;border:none;padding:4px 6px;border-radius:4px;cursor:pointer;font-size:11px;">🗑️</button>
</div>
<div style="font-size:10px;color:#666;text-align:center;">⌨️ Shortcuts: <b>Alt+C</b> Toggle | <b>Alt+S</b> Save | <b>Alt+R</b> Reset</div>
</section>
<section style="margin-bottom:12px;"><h4 style="margin:0 0 6px 0;font-size:11px;color:#777;text-transform:uppercase;">Appearance</h4>
<label style="display:block;margin-bottom:6px;font-size:12px;">Font Size: <span id="rc-fs-val">${state.settings.fontSize}px</span><input type="range" id="rc-fontsize" min="12" max="20" value="${state.settings.fontSize}" style="width:100%;margin-top:3px;"></label>
<label style="display:block;margin-bottom:6px;font-size:12px;">Accent Color<input type="color" id="rc-accent" value="${state.settings.accentColor}" style="width:100%;height:28px;border:none;border-radius:6px;cursor:pointer;background:#272729;margin-top:2px;"></label>
<label style="display:block;margin-bottom:6px;font-size:12px;">Content Width (px)<input type="number" id="rc-maxwidth" value="${state.settings.maxWidth}" min="600" max="1400" style="width:100%;background:#272729;color:#d7dadc;border:1px solid #444;border-radius:4px;padding:4px;margin-top:2px;"></label>
<label style="display:block;margin-bottom:6px;font-size:12px;">Panel Opacity<input type="range" id="rc-paneltrans" min="0.7" max="1" step="0.05" value="${state.settings.panelTransparency}" style="width:100%;margin-top:3px;"></label>
</section>
<section style="margin-bottom:12px;"><h4 style="margin:0 0 6px 0;font-size:11px;color:#777;text-transform:uppercase;">Layout & Content</h4>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;cursor:pointer;"><input type="checkbox" id="rc-compact" ${state.settings.compactMode?'checked':''}> Compact Mode</label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;cursor:pointer;"><input type="checkbox" id="rc-sidebar" ${state.settings.hideSidebar?'checked':''}> Hide Sidebar</label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;cursor:pointer;"><input type="checkbox" id="rc-topnav" ${state.settings.hideTopNav?'checked':''}> Hide Top Nav</label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;cursor:pointer;"><input type="checkbox" id="rc-ads" ${state.settings.hideAds?'checked':''}> Hide Ads</label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;cursor:pointer;"><input type="checkbox" id="rc-chat" ${state.settings.hideChat?'checked':''}> Hide Chat</label>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;"><input type="checkbox" id="rc-awards" ${state.settings.hideAwards?'checked':''}> Hide Awards</label>
</section>
<section style="margin-bottom:10px;"><h4 style="margin:0 0 6px 0;font-size:11px;color:#777;text-transform:uppercase;">Comments & Polish</h4>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;cursor:pointer;"><input type="checkbox" id="rc-hidescores" ${state.settings.hideCommentScores?'checked':''}> Hide Comment Scores</label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;cursor:pointer;"><input type="checkbox" id="rc-op" ${state.settings.highlightOP?'checked':''}> Highlight OP</label>
<label style="display:block;margin-bottom:6px;font-size:12px;">Thumbnails<select id="rc-thumb" style="width:100%;background:#272729;color:#d7dadc;border:1px solid #444;border-radius:4px;padding:4px;margin-top:2px;"><option value="small" ${state.settings.thumbnailSize==='small'?'selected':''}>Small</option><option value="medium" ${state.settings.thumbnailSize==='medium'?'selected':''}>Medium</option><option value="large" ${state.settings.thumbnailSize==='large'?'selected':''}>Large</option></select></label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;cursor:pointer;"><input type="checkbox" id="rc-animations" ${state.settings.disableAnimations?'checked':''}> Disable Animations</label>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;cursor:pointer;"><input type="checkbox" id="rc-scrollbar" ${state.settings.customScrollbar?'checked':''}> Custom Scrollbar</label>
<label style="display:block;margin-bottom:6px;font-size:12px;">Background URL<input type="text" id="rc-bgimg" value="${state.settings.bgImage}" placeholder="https://..." style="width:100%;background:#272729;color:#d7dadc;border:1px solid #444;border-radius:4px;padding:4px;font-size:11px;margin-top:2px;"></label>
</section>
<section><label style="display:block;margin-bottom:4px;font-size:12px;">Custom CSS</label><textarea id="rc-customcss" style="width:100%;height:60px;background:#272729;color:#d7dadc;border:1px solid #444;border-radius:6px;padding:6px;font-family:monospace;font-size:11px;resize:vertical;">${state.settings.customCSS}</textarea></section>
</div>
<div style="display:flex;gap:6px;margin-top:8px;padding-top:8px;border-top:1px solid #333;">
<button id="rc-apply" style="flex:1;background:${state.settings.accentColor};color:white;border:none;padding:7px;border-radius:6px;cursor:pointer;font-weight:500;font-size:13px;">Apply & Save</button>
<button id="rc-reset" style="background:#343536;color:#aaa;border:none;padding:7px;border-radius:6px;cursor:pointer;font-size:12px;">Reset</button>
</div>
`;
document.body.appendChild(panel);
makeDraggable(panel, 'panel');
populatePanel();
}
function populatePanel() {
if (!panel) return;
const q = (id) => panel.querySelector(`#${id}`);
q('rc-fontsize').value = state.settings.fontSize; q('rc-fs-val').textContent = `${state.settings.fontSize}px`;
q('rc-accent').value = state.settings.accentColor; q('rc-maxwidth').value = state.settings.maxWidth;
q('rc-paneltrans').value = state.settings.panelTransparency; q('rc-compact').checked = state.settings.compactMode;
q('rc-sidebar').checked = state.settings.hideSidebar; q('rc-topnav').checked = state.settings.hideTopNav;
q('rc-ads').checked = state.settings.hideAds; q('rc-chat').checked = state.settings.hideChat;
q('rc-awards').checked = state.settings.hideAwards; q('rc-hidescores').checked = state.settings.hideCommentScores;
q('rc-op').checked = state.settings.highlightOP; q('rc-thumb').value = state.settings.thumbnailSize;
q('rc-animations').checked = state.settings.disableAnimations; q('rc-scrollbar').checked = state.settings.customScrollbar;
q('rc-bgimg').value = state.settings.bgImage; q('rc-customcss').value = state.settings.customCSS;
// Preset dropdown
const sel = q('rc-preset-sel');
sel.innerHTML = Object.keys(state.presets).map(p => `<option value="${p}" ${p===state.activePreset?'selected':''}>${p}</option>`).join('');
updatePanelTheme();
}
function togglePanel() {
if (!panel) createPanel();
panel.style.display = panel.style.display === 'block' ? 'none' : 'block';
if (panel.style.display === 'block') populatePanel();
}
function updatePanelTheme() {
if (!panel) return;
panel.querySelector('h3').style.color = state.settings.accentColor;
panel.querySelector('#rc-apply').style.background = state.settings.accentColor;
}
// ─── Drag Engine ───────────────────────────────────────────────────
function makeDraggable(el, storageKey) {
let isDragging = false, offsetX, offsetY;
const pos = state.positions[storageKey];
if (pos) { el.style.left = pos.x + 'px'; el.style.top = pos.y + 'px'; }
const onStart = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT' || e.target.closest('button') || (storageKey==='panel' && !e.target.closest('#rc-drag-handle'))) return;
isDragging = true;
const cX = e.touches ? e.touches[0].clientX : e.clientX;
const cY = e.touches ? e.touches[0].clientY : e.clientY;
const rect = el.getBoundingClientRect();
offsetX = cX - rect.left; offsetY = cY - rect.top;
el.style.zIndex = 999999; el.style.cursor = 'grabbing';
e.preventDefault();
};
const onMove = (e) => {
if (!isDragging) return;
const cX = e.touches ? e.touches[0].clientX : e.clientX;
const cY = e.touches ? e.touches[0].clientY : e.clientY;
let newX = cX - offsetX, newY = cY - offsetY;
// Clamp to viewport
const w = window.innerWidth, h = window.innerHeight;
const ew = el.offsetWidth, eh = el.offsetHeight;
newX = Math.max(0, Math.min(w - ew, newX));
newY = Math.max(0, Math.min(h - eh, newY));
el.style.left = newX + 'px'; el.style.top = newY + 'px';
e.preventDefault();
};
const onEnd = () => {
if (!isDragging) return;
isDragging = false; el.style.zIndex = 999998; el.style.cursor = 'grab';
const rect = el.getBoundingClientRect();
state.positions[storageKey] = { x: rect.left, y: rect.top };
saveState();
};
el.style.cursor = 'grab';
el.addEventListener('mousedown', onStart); el.addEventListener('touchstart', onStart, { passive: false });
document.addEventListener('mousemove', onMove); document.addEventListener('touchmove', onMove, { passive: false });
document.addEventListener('mouseup', onEnd); document.addEventListener('touchend', onEnd);
}
// ─── Keyboard Shortcuts ────────────────────────────────────────────
document.addEventListener('keydown', (e) => {
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName) && e.target.id !== 'rc-customcss') return;
if (e.altKey) {
if (e.key.toLowerCase() === 'c') { e.preventDefault(); togglePanel(); }
else if (e.key.toLowerCase() === 's') { e.preventDefault(); const n = prompt("Save current settings as preset name:"); saveCurrentAsPreset(n); }
else if (e.key.toLowerCase() === 'r') { e.preventDefault(); resetCurrentSettings(); }
}
});
// ─── Event Binding ─────────────────────────────────────────────────
function bindEvents() {
if (!panel) return;
const q = (id) => panel.querySelector(`#${id}`);
q('rc-fontsize').addEventListener('input', e => q('rc-fs-val').textContent = `${e.target.value}px`);
q('rc-accent').addEventListener('input', e => updatePanelTheme(e.target.value));
q('rc-close').addEventListener('click', () => panel.style.display = 'none');
q('rc-preset-sel').addEventListener('change', e => applyPreset(e.target.value));
q('rc-save-preset').addEventListener('click', () => { const n = prompt("Preset name:"); saveCurrentAsPreset(n); });
q('rc-del-preset').addEventListener('click', () => deletePreset(state.activePreset));
q('rc-apply').addEventListener('click', () => {
state.settings.fontSize = q('rc-fontsize').value; state.settings.accentColor = q('rc-accent').value;
state.settings.maxWidth = q('rc-maxwidth').value; state.settings.panelTransparency = q('rc-paneltrans').value;
state.settings.compactMode = q('rc-compact').checked; state.settings.hideSidebar = q('rc-sidebar').checked;
state.settings.hideTopNav = q('rc-topnav').checked; state.settings.hideAds = q('rc-ads').checked;
state.settings.hideChat = q('rc-chat').checked; state.settings.hideAwards = q('rc-awards').checked;
state.settings.hideCommentScores = q('rc-hidescores').checked; state.settings.highlightOP = q('rc-op').checked;
state.settings.thumbnailSize = q('rc-thumb').value; state.settings.disableAnimations = q('rc-animations').checked;
state.settings.customScrollbar = q('rc-scrollbar').checked; state.settings.bgImage = q('rc-bgimg').value.trim();
state.settings.customCSS = q('rc-customcss').value;
saveState(); applyStyles(); updatePanelTheme(); panel.style.display = 'none';
});
q('rc-reset').addEventListener('click', resetCurrentSettings);
document.addEventListener('click', e => {
if (panel?.style.display === 'block' && !panel.contains(e.target) && e.target !== btn) panel.style.display = 'none';
});
}
function resetCurrentSettings() {
state.settings = { ...DEFAULT_SETTINGS };
state.activePreset = "Default";
saveState(); applyStyles(); populatePanel(); btn.style.background = state.settings.accentColor;
}
// ─── Initialization ────────────────────────────────────────────────
function init() {
createButton();
createPanel();
bindEvents();
applyStyles();
observer = new MutationObserver(() => { clearTimeout(updateTimer); updateTimer = setTimeout(applyStyles, 200); });
observer.observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();