SkipCut PowerTools – Minimal/Full UI + Fast Piped, Invidious & FreeTube Buttons
目前為
// ==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();
}
}
})();