SkipCut PowerTools

SkipCut PowerTools – Minimal/Full UI + Fast Piped, Invidious & FreeTube Buttons

目前為 2025-08-25 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         SkipCut PowerTools
// @namespace    https://greasyfork.org/users/1197317-opus-x
// @version      1.0
// @description  SkipCut PowerTools – Minimal/Full UI + Fast Piped, Invidious & FreeTube Buttons
// @author       Opus-X
// @license      MIT
// @match        https://skipcut.com/*
// @match        https://www.skipcut.com/*
// @run-at       document-start
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      *
// ==/UserScript==

(function () {
    'use strict';

    // ---------------------------
    // Mirror lists
    // ---------------------------
    const INVIDIOUS_MIRRORS = [
        "https://yewtu.be",
        "https://inv.tux.pizza",
        "https://invidious.privacydev.net",
        "https://iv.ggtyler.dev",
        "https://invidious.protokolla.fi"
    ];

    const PIPED_MIRRORS = [
        "https://piped.video",
        "https://piped.mha.fi",
        "https://piped.lunar.icu",
        "https://piped.projectsegfau.lt",
        "https://watch.leptons.xyz"
    ];

    // ---------------------------
    // Config
    // ---------------------------
    const PING_TIMEOUT_MS = 2500;
    const MIRROR_CACHE_TTL_MS = 6 * 60 * 60 * 1000;

    const urlParams = new URLSearchParams(location.search);
    const hasVideo = urlParams.has('v');
    const videoId = urlParams.get('v');

    // ---------------------------
    // Minimal Layout Toggle
    // ---------------------------
    const MINIMAL_KEY = 'sc_minimal_layout';
    let minimalMode = GM_getValue(MINIMAL_KEY, true);
    let minimalStyleEl;

    function applyMinimalLayout(enable) {
        if (!hasVideo) return;
        if (!minimalStyleEl) {
            minimalStyleEl = document.createElement('style');
            minimalStyleEl.id = 'sc-minimal-style';
            document.head.appendChild(minimalStyleEl);
        }
        minimalStyleEl.textContent = enable ? `
            .nav-menu, .hero-section, .input-section,
            #bmc-wbtn, .trending-container,
            .features-highlight, .testimonials-section,
            .infographic-section, .faq-section,
            .featured-section, .footer-container,
            .ybug-launcher--active, .history-section {
                display: none !important;
            }
        ` : '';
    }
    applyMinimalLayout(minimalMode);

    // ---------------------------
    // Styles
    // ---------------------------
    GM_addStyle(`
        #sc-powertools {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 6px 0;
            margin: 10px 0;
            gap: 10px;
            flex-wrap: wrap;
        }
        #sc-powertools .sc-left {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
            align-items: center;
        }
        #sc-powertools .sc-btn {
            background: #222;
            color: #fff;
            padding: 6px 12px;
            border-radius: 6px;
            border: none;
            cursor: pointer;
            font-size: 13px;
            text-decoration: none;
            transition: background 0.2s ease-in-out;
        }
        #sc-powertools .sc-btn:hover { background: #444; }
        #sc-powertools .sc-btn[disabled] { opacity: 0.55; cursor: not-allowed; }
        #sc-profile-select {
            background:#333;
            color:#fff;
            padding:6px 10px;
            border-radius:6px;
            border:none;
            cursor:pointer;
            font-size:13px;
            margin-left:auto;
        }
        .sc-status {
            font-size: 13px;
            color: #777;
            margin-left: 5px;
        }
    `);

    // ---------------------------
    // Fastest mirror detection
    // ---------------------------
    function pingMirror(baseUrl) {
        return new Promise(resolve => {
            const started = performance.now();
            GM_xmlhttpRequest({
                method: "HEAD", // faster than GET
                url: baseUrl.replace(/\/+$/, "") + "/favicon.ico",
                timeout: PING_TIMEOUT_MS,
                onload: (res) => {
                    if (res.status === 200) {
                        resolve({ url: baseUrl, time: performance.now() - started });
                    } else resolve(null);
                },
                onerror: () => resolve(null),
                ontimeout: () => resolve(null)
            });
        });
    }

    async function pickFastestMirror(kind, list) {
        const cacheKey = `scpt_fastest_${kind}`;
        const tsKey = `${cacheKey}_ts`;
        const now = Date.now();
        const cached = GM_getValue(cacheKey, null);
        const cachedTs = GM_getValue(tsKey, 0);

        if (cached && (now - cachedTs) < MIRROR_CACHE_TTL_MS) return cached;

        const checks = await Promise.all(list.map(pingMirror));
        const working = checks.filter(Boolean).sort((a, b) => a.time - b.time);
        const fastest = working.length ? working[0].url : null;

        GM_setValue(cacheKey, fastest);
        GM_setValue(tsKey, now);
        return fastest;
    }

    // ---------------------------
    // Main container creation
    // ---------------------------
    function insertMirrorButtonsContainer() {
        if (!hasVideo || document.getElementById('sc-powertools')) return null;
        const videoInfo = document.querySelector('.video-info');
        if (!videoInfo) return null;

        const container = document.createElement('div');
        container.id = 'sc-powertools';

        const leftContainer = document.createElement('div');
        leftContainer.className = 'sc-left';
        container.appendChild(leftContainer);

        videoInfo.parentNode.insertBefore(container, videoInfo);
        return container;
    }

    // ---------------------------
    // Fill buttons & dropdown
    // ---------------------------
    async function fillMirrorButtons(container) {
        const leftContainer = container.querySelector('.sc-left');
        leftContainer.innerHTML = ''; // Reset buttons on refresh

        // Status text
        let status = container.querySelector('.sc-status');
        if (!status) {
            status = document.createElement('span');
            status.className = 'sc-status';
            leftContainer.appendChild(status);
        }
        status.textContent = 'Checking mirrors…';

        const [fastestPiped, fastestInv] = await Promise.all([
            pickFastestMirror('piped', PIPED_MIRRORS),
            pickFastestMirror('invidious', INVIDIOUS_MIRRORS)
        ]);

        const makeBtn = (label, href) => {
            const a = document.createElement('a');
            a.className = 'sc-btn sc-mirror-btn';
            a.textContent = label;
            a.href = href;
            a.target = '_blank';
            a.rel = 'noopener noreferrer';
            return a;
        };

        if (fastestPiped) leftContainer.appendChild(makeBtn('Open Piped', `${fastestPiped}/watch?v=${videoId}`));
        if (fastestInv) leftContainer.appendChild(makeBtn('Open Invidious', `${fastestInv}/watch?v=${videoId}`));
        if (videoId) leftContainer.appendChild(makeBtn('Open FreeTube', `freetube://${videoId}`));

        // Refresh button (only created once)
        let refresh = container.querySelector('.sc-refresh-btn');
        if (!refresh) {
            refresh = document.createElement('button');
            refresh.className = 'sc-btn sc-mirror-btn sc-refresh-btn';
            refresh.textContent = 'Refresh mirrors';
            refresh.addEventListener('click', async () => {
                GM_setValue('scpt_fastest_piped', null);
                GM_setValue('scpt_fastest_piped_ts', 0);
                GM_setValue('scpt_fastest_invidious', null);
                GM_setValue('scpt_fastest_invidious_ts', 0);
                status.textContent = 'Re-checking…';
                await fillMirrorButtons(container);
            });
        }
        leftContainer.appendChild(refresh);

        // Always keep the profile selector on the right
        let profileSelect = container.querySelector('#sc-profile-select');
        if (!profileSelect) {
            profileSelect = document.createElement('select');
            profileSelect.id = 'sc-profile-select';
            ['Minimal UI', 'Full UI'].forEach((p, i) => {
                const o = document.createElement('option');
                o.value = i;
                o.textContent = p;
                profileSelect.appendChild(o);
            });
            profileSelect.value = minimalMode ? '0' : '1';
            profileSelect.addEventListener('change', e => {
                minimalMode = e.target.value === '0';
                GM_setValue(MINIMAL_KEY, minimalMode);
                applyMinimalLayout(minimalMode);
            });
            container.appendChild(profileSelect);
        }

        // Update status text based on results
        const parts = [];
        if (fastestPiped) parts.push('Piped OK');
        if (fastestInv) parts.push('Invidious OK');
        status.textContent = parts.join(' • ') || 'No mirrors available';
    }

    // ---------------------------
    // Bootstrap when ready
    // ---------------------------
    function bootWhenReady() {
        if (!hasVideo) return;
        const tryInit = () => {
            if (document.getElementById('sc-powertools')) return false;
            const container = insertMirrorButtonsContainer();
            if (container) { fillMirrorButtons(container); return true; }
            return false;
        };
        if (tryInit()) return;
        const mo = new MutationObserver(() => { if (tryInit()) mo.disconnect(); });
        mo.observe(document.documentElement, { childList: true, subtree: true });
    }

    if (hasVideo) {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', bootWhenReady, { once: true });
        } else {
            bootWhenReady();
        }
    }

})();