Our TikTok Control Panel

The ultimate tool with an adaptive theme and video loop control.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        El Nostre Tauler de Control per a TikTok
// @name:es     Nuestro Panel de Control para TikTok
// @name:en     Our TikTok Control Panel
// @namespace   El nostre espai privat a TikTok.
// @namespace:es  Nuestro espacio privado en TikTok.
// @namespace:en  Our private space on TikTok.
// @match       *://www.tiktok.com/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @version     9.4 (Rhythm Control)
// @author      Anna (originals by Margu, @dmtri)
// @description Eina definitiva amb tema adaptable i control de bucles de vídeo.
// @description:es Herramienta definitiva con tema adaptable y control de bucles de vídeo.
// @description:en The ultimate tool with an adaptive theme and video loop control.
// @license     MIT
// ==/UserScript==

(function() {
    'use strict';

    const App = {
        // --- MAPA D'OBJECTIUS ---
        elementMap: {
            mainNav: { name: 'Menú principal esquerra', selector: '.e9sj7gd7.css-5f57fe-5e6d46e3--SubMainNavContentContainer', category: 'General' },
            footerNav: { name: 'Peu de pàgina del menú', selector: '.e9sj7gd8.css-42z4cf-5e6d46e3--SubMainNavFooterContainer', category: 'General' },
            topRightButtons: { name: 'Botons Missatges/Safata', selector: '.e10tfxq61.css-9novpx-5e6d46e3--DivFixedTopContainer', category: 'General' },
            uploadButton: { name: 'Botó de Pujar vídeo', selector: '.e18za1nb0.css-6w6pkg-5e6d46e3--DivTriggerButtonWrapper', category: 'General' },
            searchBar: { name: 'Barra de cerca', selector: '.e3pk5p611.css-13qhhpy-5e6d46e3--DivSearchBarContainer > .e17qpoe50.css-1bb483g-5e6d46e3--DivSearchFormContainer', category: 'General'},
            miniPlayer: { name: 'Mini reproductor (baix)', selector: '.e3pk5p610.css-1fdepl7-5e6d46e3--DivMiniPlayerContainer', category: 'General'},
            sideBar: { name: 'Barra lateral del vídeo', selector: '.e1196arh5.css-10pqo95-DivScrollingContentContainer', category: 'Panell de Vídeo' },
            actionButtons: { name: 'Panell (Like, Comentaris...)', selector: '.e11c9bxt0.css-adn7ap-5e6d46e3--DivButtonPanelWrapper', category: 'Panell de Vídeo' },
            effectsButton: { name: 'Botó d\'Efectes', selector: 'div.e1s4651v1.css-tvlexx-5e6d46e3--StyledTUXTooltip.TUXTooltip-reference:nth-of-type(5)', category: 'Panell de Vídeo' },
            shareButton: { name: 'Botó de Compartir', selector: '.e1roo5iv7.css-b5d1i1-5e6d46e3--DivBtnWrapper', category: 'Panell de Vídeo'},
            likedVideosTab: { name: 'Pestanya de "M\'agrada"', selector: '.ej9r3wt4.css-750edx-5e6d46e3--PLike', category: 'Perfil d\'Usuari' },
            repostTab: { name: 'Pestanya de "Reposts"', selector: '.e19uy6s72.css-1pkoxxg-PRepost', category: 'Perfil d\'Usuari' },
            privateAndCollectionTabs: { name: 'Pestanyes Privat/Col·lecció', selector: '.e178qcw41.css-4fwrfl-5e6d46e3--DivTabItemContainer', category: 'Perfil d\'Usuari'},
            sortByOldest: { name: 'Ordenar per més antic', selector: 'button.TUXSegmentedControl-item.TUXUnstyledButton:nth-of-type(3)', category: 'Perfil d\'Usuari' },
            stopVideoLoops: { name: 'Aturar bucles de vídeo', category: 'Reproductor' } // Aquesta no té selector, és una funció
        },

        state: {},
        processedVideos: new WeakSet(),

        async init() {
            this.state = JSON.parse(await GM_getValue('tiktokControlPanelSettings_v5', '{"stopVideoLoops": true}'));
            this.injectStyles();
            this.applyVisibilityRules();
            this.buildUI();
            this.initThemeDetector();
            this.initMasterObserver();
            console.log('[Tauler de Control TikTok] Sistema de control total operatiu.');
        },

        async toggleVisibility(key) {
            this.state[key] = !this.state[key];
            await GM_setValue('tiktokControlPanelSettings_v5', JSON.stringify(this.state));
            window.location.reload();
        },
        
        applyVisibilityRules() {
            const selectorsToHide = Object.keys(this.state)
                .filter(key => this.state[key] && this.elementMap[key] && this.elementMap[key].selector)
                .map(key => this.elementMap[key].selector);

            if (selectorsToHide.length > 0) {
                const style = document.createElement('style');
                style.id = 'anna-tiktok-visibility-rules';
                style.textContent = `${selectorsToHide.join(',\n')} { display: none !important; }`;
                document.head.appendChild(style);
            }
        },
        
        initThemeDetector() {
            this.panelElement = document.getElementById('anna-control-panel');
            this.themeQuery = window.matchMedia('(prefers-color-scheme: dark)');
            this.applyTheme();
            this.themeQuery.addEventListener('change', this.applyTheme.bind(this));
        },

        applyTheme() {
            this.panelElement.classList.toggle('acp-dark-theme', this.themeQuery.matches);
            this.panelElement.classList.toggle('acp-light-theme', !this.themeQuery.matches);
        },

        buildUI() {
            const panel = document.createElement('div');
            panel.id = 'anna-control-panel';
            const categories = {};
            for (const key in this.elementMap) {
                const element = this.elementMap[key];
                if (!categories[element.category]) categories[element.category] = '';
                const isHidden = this.state[key];
                categories[element.category] += `
                    <div class="acp-switch-container">
                        <label for="switch-${key}" class="acp-label">${element.name}</label>
                        <label class="acp-switch">
                            <input type="checkbox" id="switch-${key}" data-key="${key}" ${isHidden ? 'checked' : ''}>
                            <span class="acp-slider"></span>
                        </label>
                    </div>
                `;
            }
            let categoriesHTML = '';
            for (const categoryName in categories) {
                categoriesHTML += `<div class="acp-category"><div class="acp-category-title">${categoryName}</div>${categories[categoryName]}</div>`;
            }
            panel.innerHTML = `<div class="acp-header">El Nostre Tauler</div><div class="acp-body">${categoriesHTML}</div>`;
            document.body.appendChild(panel);
            this.addUIEventListeners();
        },

        addUIEventListeners() {
            document.getElementById('anna-control-panel').querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
                checkbox.addEventListener('change', (e) => this.toggleVisibility(e.target.dataset.key));
            });
        },
        
        // --- OBSERVADOR MESTRE (Guardià + Control de Bucles) ---
        initMasterObserver() {
            const errorSelector = '.emuynwa0.css-1tttox1-DivErrorContainer';
            const observer = new MutationObserver(() => {
                // Funció del Guardià d'Errors
                if (document.querySelector(errorSelector)) {
                    observer.disconnect();
                    window.location.reload();
                }
                // Funció de Control de Bucles
                if (this.state.stopVideoLoops) {
                    document.querySelectorAll("video").forEach(this.makeVideoNonLooping.bind(this));
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        },

        // --- NOVA FUNCIÓ: CONTROL DE BUCLES DE VÍDEO ---
        makeVideoNonLooping(vid) {
            if (this.processedVideos.has(vid)) return;
            this.processedVideos.add(vid);
            vid.removeAttribute("loop");

            let timeouts = [];
            const clearTimeouts = () => {
                timeouts.forEach(clearTimeout);
                timeouts = [];
            };

            const setupTimeout = () => {
                clearTimeouts();
                if (isNaN(vid.duration) || vid.duration === Infinity || vid.paused) return;
                
                const remainingTime = (vid.duration - vid.currentTime - 0.05) * 1000;
                if (remainingTime > 0) {
                    timeouts.push(setTimeout(() => vid.pause(), remainingTime));
                }
            };
            
            vid.addEventListener('loadedmetadata', setupTimeout);
            vid.addEventListener('seeked', setupTimeout);
            vid.addEventListener('play', setupTimeout);
            vid.addEventListener("pause", clearTimeouts);
            vid.addEventListener("ended", () => setTimeout(() => vid.pause(), 10));
        },

        injectStyles() {
            GM_addStyle(`
                #anna-control-panel {
                    position: fixed; bottom: 10px; right: 10px; width: 320px;
                    border-radius: 8px; z-index: 10000; font-family: sans-serif;
                    transform: translateX(calc(100% - 35px)); transition: transform 0.3s ease-in-out, background-color 0.3s, border-color 0.3s;
                }
                #anna-control-panel:hover { transform: translateX(0); }
                .acp-dark-theme { background-color: rgba(22, 24, 35, 0.9); border: 1px solid rgba(255, 255, 255, 0.2); color: #fff; backdrop-filter: blur(5px); }
                .acp-dark-theme .acp-header { background-color: rgba(255, 255, 255, 0.1); }
                .acp-dark-theme .acp-category-title { border-bottom-color: rgba(255, 255, 255, 0.2); }
                .acp-light-theme { background-color: rgba(255, 255, 255, 0.9); border: 1px solid rgba(0, 0, 0, 0.1); color: #000; backdrop-filter: blur(5px); }
                .acp-light-theme .acp-header { background-color: rgba(0, 0, 0, 0.05); }
                .acp-light-theme .acp-category-title { border-bottom-color: rgba(0, 0, 0, 0.1); }
                .acp-light-theme .acp-label { color: #333; }
                .acp-header { padding: 10px; font-weight: bold; text-align: center; border-top-left-radius: 8px; border-top-right-radius: 8px; }
                .acp-body { padding: 15px; }
                .acp-category { margin-bottom: 15px; }
                .acp-category-title { font-size: 1em; font-weight: bold; color: #fe2c55; margin-bottom: 10px; padding-bottom: 5px; border-bottom-width: 1px; border-bottom-style: solid; }
                .acp-switch-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
                .acp-label { font-size: 14px; }
                .acp-switch { position: relative; display: inline-block; width: 40px; height: 20px; }
                .acp-switch input { opacity: 0; width: 0; height: 0; }
                .acp-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 20px; }
                .acp-light-theme .acp-slider { background-color: #ccc; }
                .acp-dark-theme .acp-slider { background-color: #4a4a4a; }
                .acp-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 0 2px rgba(0,0,0,0.3); }
                input:checked + .acp-slider { background-color: #fe2c55; }
                input:checked + .acp-slider:before { transform: translateX(20px); }
            `);
        }
    };

    if (document.body) {
        App.init();
    } else {
        document.addEventListener('DOMContentLoaded', () => App.init());
    }
})();