Reddit Customizer Pro

Floating customization panel with drag, keyboard shortcuts, and preset profiles

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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