TikTok Enhancer Plus

Safely enhance TikTok web: download videos (with blob fallback), audio-only, dark mode, auto loop/mute/scroll, UI tweaks, and settings gear menu — by Eliminater74

// ==UserScript==
// @name         TikTok Enhancer Plus
// @namespace    https://greasyfork.org/en/users/123456-eliminater74
// @version      2.1
// @description  Safely enhance TikTok web: download videos (with blob fallback), audio-only, dark mode, auto loop/mute/scroll, UI tweaks, and settings gear menu — by Eliminater74
// @author       Eliminater74
// @license      MIT
// @match        https://www.tiktok.com/*
// @icon         https://www.tiktok.com/favicon.ico
// @grant        GM_download
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    const SETTINGS_KEY = 'tiktokEnhancerSettings';
    const defaultSettings = {
        darkMode: false,
        autoMute: false,
        autoLoop: false,
        autoScroll: false,
        removeAds: true,
        wideMode: false
    };
    const config = { ...defaultSettings, ...JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}') };
    const saveSettings = () => localStorage.setItem(SETTINGS_KEY, JSON.stringify(config));

    function createMenu() {
        const menu = document.createElement('div');
        menu.id = 'tiktok-enhancer-menu';
        menu.style = `
            position: fixed;
            bottom: 70px;
            right: 20px;
            background: #222;
            color: white;
            padding: 10px;
            border-radius: 10px;
            z-index: 999999;
            font-family: sans-serif;
            box-shadow: 0 0 10px #000;
            display: none;
        `;
        menu.innerHTML = `
            <label><input type="checkbox" id="toggle-dark"> Dark Mode</label><br>
            <label><input type="checkbox" id="toggle-mute"> Auto Mute</label><br>
            <label><input type="checkbox" id="toggle-loop"> Auto Loop</label><br>
            <label><input type="checkbox" id="toggle-scroll"> Auto Scroll</label><br>
            <label><input type="checkbox" id="toggle-ads"> Remove Ads</label><br>
            <label><input type="checkbox" id="toggle-wide"> Wide Mode</label><br><hr>
            <button id="save-settings">💾 Save</button>
            <button id="load-settings">📂 Load</button>
        `;
        document.body.appendChild(menu);

        const gear = document.createElement('div');
        gear.innerText = '⚙️';
        gear.style = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            font-size: 24px;
            z-index: 999998;
            cursor: pointer;
            background: #333;
            color: white;
            padding: 5px 10px;
            border-radius: 50%;
            box-shadow: 0 0 8px #000;
        `;
        gear.onclick = () => menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
        document.body.appendChild(gear);

        for (const key in defaultSettings) {
            const el = document.getElementById(`toggle-${key.replace('auto', '').toLowerCase()}`);
            if (el) {
                el.checked = config[key];
                el.addEventListener('change', () => {
                    config[key] = el.checked;
                    saveSettings();
                    applySettings();
                });
            }
        }

        document.getElementById('save-settings').onclick = () => {
            localStorage.setItem(SETTINGS_KEY + '_backup', JSON.stringify(config));
            alert('Settings saved!');
        };

        document.getElementById('load-settings').onclick = () => {
            const backup = JSON.parse(localStorage.getItem(SETTINGS_KEY + '_backup') || '{}');
            Object.assign(config, backup);
            saveSettings();
            location.reload();
        };
    }

    function applySettings() {
        document.documentElement.style.filter = config.darkMode ? 'invert(1) hue-rotate(180deg)' : '';
        [...document.images].forEach(img => img.style.filter = config.darkMode ? 'invert(1) hue-rotate(180deg)' : '');

        if (config.removeAds) {
            setInterval(() => {
                document.querySelectorAll('[data-e2e*="sponsored"], [data-testid*="trending"]').forEach(el => el.remove());
            }, 1500);
        }

        if (config.wideMode) {
            const style = document.createElement('style');
            style.id = 'wide-mode-style';
            style.innerText = `main > div { max-width: 100vw !important; padding: 0 !important; }`;
            document.head.appendChild(style);
        } else {
            const existing = document.getElementById('wide-mode-style');
            if (existing) existing.remove();
        }
    }

    function injectDownload(video) {
        if (!video || video.dataset.enhanced) return;
        video.dataset.enhanced = 'true';

        const interval = setInterval(() => {
            if (video.readyState === 4 && !video.paused) {
                clearInterval(interval);

                if (config.autoMute) video.muted = true;
                if (config.autoScroll) video.addEventListener('ended', () => window.scrollBy(0, window.innerHeight), { once: true });
                if (config.autoLoop) video.addEventListener('ended', () => { video.currentTime = 0; video.play(); });

                const src = video.currentSrc || video.src;
                if (!src) return;

                const existing = document.querySelector('#tiktok-download-btn');
                if (existing) existing.remove();

                const btn = document.createElement('div');
                btn.id = 'tiktok-download-btn';
                btn.style = `
                    position: fixed;
                    bottom: 80px;
                    left: 20px;
                    z-index: 999999;
                `;

                const dl = document.createElement('button');
                dl.textContent = '⬇ Download';
                dl.onclick = () => {
                    if (src.startsWith('blob:')) {
                        alert('This video is streamed using a blob URL. Direct download is not currently possible.');
                    } else {
                        GM_download(src, `TikTok_${Date.now()}.mp4`);
                    }
                };
                dl.style = `background:red;color:white;padding:6px 12px;border:none;border-radius:6px;cursor:pointer;margin-right:5px;`;

                const audio = document.createElement('button');
                audio.textContent = '🎵 Audio';
                audio.onclick = () => {
                    alert('Audio-only download not yet implemented. Coming soon!');
                };
                audio.style = `background:#333;color:white;padding:6px 12px;border:none;border-radius:6px;cursor:pointer;`;

                btn.append(dl, audio);
                document.body.appendChild(btn);
            }
        }, 1000);
    }

    function monitorVideos() {
        const observer = new MutationObserver(() => {
            document.querySelectorAll('video').forEach(injectDownload);
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function keyShortcuts() {
        document.addEventListener('keydown', e => {
            const vid = document.querySelector('video');
            if (!vid) return;
            if (e.key === 'd') GM_download(vid.src || vid.currentSrc, `TikTok_${Date.now()}.mp4`);
            if (e.key === 'm') vid.muted = !vid.muted;
            if (e.key === 't') {
                config.darkMode = !config.darkMode;
                saveSettings();
                applySettings();
            }
        });
    }

    createMenu();
    applySettings();
    monitorVideos();
    keyShortcuts();
})();