// ==UserScript==
// @name SkipCut PowerTools
// @name:en SkipCut PowerTools
// @name:nl SkipCut PowerTools
// @name:es Herramientas Avanzadas de SkipCut
// @name:fr Outils Avancés SkipCut
// @name:de SkipCut PowerTools
// @name:zh-CN SkipCut 高级工具
// @name:ja SkipCut パワーツール
// @name:ru Инструменты SkipCut
// @name:pt Ferramentas Avançadas SkipCut
// @name:it Strumenti Avanzati SkipCut
// @name:ko SkipCut 파워툴
// @namespace https://greasyfork.org/users/1197317-opus-x
// @version 1.04
// @description SkipCut PowerTools – Minimal/Full UI + Fast Invidious Buttons
// @description:en SkipCut PowerTools – Minimal/Full UI + Fast Invidious Buttons
// @description:nl SkipCut PowerTools – Minimale/Volledige UI + Snelle Invidious Knoppen
// @description:es Herramientas Avanzadas de SkipCut – Interfaz Mínima/Completa + Botones Rápidos para Invidious
// @description:fr Outils Avancés SkipCut – Interface Minimale/Complète + Boutons Rapides pour Invidious
// @description:de SkipCut PowerTools – Minimales/Volles UI + Schnelle Invidious-Schaltflächen
// @description:zh-CN SkipCut 高级工具 - 极简/完整用户界面 + 快速 Invidious 按钮
// @description:ja SkipCut パワーツール - ミニマル/フル UI + 高速 Invidious ボタン
// @description:ru Инструменты SkipCut – Минимальный/Полный интерфейс + Быстрые кнопки для Invidious
// @description:pt Ferramentas Avançadas SkipCut – Interface Mínima/Completa + Botões Rápidos para Invidious
// @description:it Strumenti Avanzati SkipCut – Interfaccia Minimale/Completa + Pulsanti Rapidi per Invidious
// @description:ko SkipCut 파워툴 - 최소/완전 UI + 빠른 Invidious 버튼
// @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';
// ---------------------------
// Localization
// ---------------------------
const translations = {
'en': {
openInvidious: 'Open Invidious',
refreshMirrors: 'Refresh mirrors',
minimalUI: 'Minimal UI',
fullUI: 'Full UI',
checkingMirrors: 'Checking mirrors…',
reChecking: 'Re-checking…',
invidiousOK: 'Invidious OK',
noMirrors: 'No mirrors available'
},
'nl': {
openInvidious: 'Open Invidious',
refreshMirrors: 'Spiegels vernieuwen',
minimalUI: 'Minimale UI',
fullUI: 'Volledige UI',
checkingMirrors: 'Spiegels controleren…',
reChecking: 'Opnieuw controleren…',
invidiousOK: 'Invidious OK',
noMirrors: 'Geen spiegels beschikbaar'
},
'es': {
openInvidious: 'Abrir Invidious',
refreshMirrors: 'Actualizar espejos',
minimalUI: 'Interfaz Mínima',
fullUI: 'Interfaz Completa',
checkingMirrors: 'Comprobando espejos…',
reChecking: 'Volviendo a comprobar…',
invidiousOK: 'Invidious OK',
noMirrors: 'No hay espejos disponibles'
},
'fr': {
openInvidious: 'Ouvrir Invidious',
refreshMirrors: 'Actualiser les miroirs',
minimalUI: 'Interface Minimale',
fullUI: 'Interface Complète',
checkingMirrors: 'Vérification des miroirs…',
reChecking: 'Vérification en cours…',
invidiousOK: 'Invidious OK',
noMirrors: 'Aucun miroir disponible'
},
'de': {
openInvidious: 'Invidious öffnen',
refreshMirrors: 'Spiegel aktualisieren',
minimalUI: 'Minimales UI',
fullUI: 'Volles UI',
checkingMirrors: 'Spiegel werden überprüft…',
reChecking: 'Erneute Überprüfung…',
invidiousOK: 'Invidious OK',
noMirrors: 'Keine Spiegel verfügbar'
},
'zh-CN': {
openInvidious: '打开 Invidious',
refreshMirrors: '刷新镜像',
minimalUI: '极简界面',
fullUI: '完整界面',
checkingMirrors: '正在检查镜像…',
reChecking: '正在重新检查…',
invidiousOK: 'Invidious 正常',
noMirrors: '没有可用的镜像'
},
'ja': {
openInvidious: 'Invidious を開く',
refreshMirrors: 'ミラーを更新',
minimalUI: 'ミニマル UI',
fullUI: 'フル UI',
checkingMirrors: 'ミラーを確認中…',
reChecking: '再確認中…',
invidiousOK: 'Invidious OK',
noMirrors: '利用可能なミラーがありません'
},
'ru': {
openInvidious: 'Открыть Invidious',
refreshMirrors: 'Обновить зеркала',
minimalUI: 'Минимальный интерфейс',
fullUI: 'Полный интерфейс',
checkingMirrors: 'Проверка зеркал…',
reChecking: 'Повторная проверка…',
invidiousOK: 'Invidious OK',
noMirrors: 'Зеркала недоступны'
},
'pt': {
openInvidious: 'Abrir Invidious',
refreshMirrors: 'Atualizar espelhos',
minimalUI: 'Interface Mínima',
fullUI: 'Interface Completa',
checkingMirrors: 'Verificando espelhos…',
reChecking: 'Verificando novamente…',
invidiousOK: 'Invidious OK',
noMirrors: 'Nenhum espelho disponível'
},
'it': {
openInvidious: 'Apri Invidious',
refreshMirrors: 'Aggiorna mirror',
minimalUI: 'Interfaccia Minimale',
fullUI: 'Interfaccia Completa',
checkingMirrors: 'Controllo dei mirror…',
reChecking: 'Ricontrollo in corso…',
invidiousOK: 'Invidious OK',
noMirrors: 'Nessun mirror disponibile'
},
'ko': {
openInvidious: 'Invidious 열기',
refreshMirrors: '미러 새로고침',
minimalUI: '최소 UI',
fullUI: '완전 UI',
checkingMirrors: '미러 확인 중…',
reChecking: '다시 확인 중…',
invidiousOK: 'Invidious OK',
noMirrors: '사용 가능한 미러 없음'
}
};
// Determine user language (fallback to English)
const userLang = (navigator.language || navigator.userLanguage || 'en').split('-')[0];
const lang = translations[userLang] ? userLang : 'en';
const t = translations[lang];
// ---------------------------
// Mirror lists
// ---------------------------
const INVIDIOUS_MIRRORS = [
"https://yewtu.be",
"https://inv.tux.pizza",
"https://invidious.privacydev.net",
"https://invidious.protokolla.fi",
"https://inv.nadeko.net",
"https://invidious.nerdvpn.de",
"https://invidious.f5.si"
];
// ---------------------------
// 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;
// Create the <style> tag if it doesn't exist yet
if (!minimalStyleEl) {
minimalStyleEl = document.createElement('style');
minimalStyleEl.id = 'sc-minimal-style';
document.head.appendChild(minimalStyleEl);
}
// Minimal Mode active → adjust layout
if (enable) {
minimalStyleEl.textContent = `
/* Hide unnecessary sections */
.nav-menu, .hero-section, .input-section,
#bmc-wbtn, .trending-container,
.features-highlight, .testimonials-section,
.infographic-section, .faq-section,
.featured-section, .footer-container,
.mobile-menu-toggle, .mob-nav-link,
.ybug-launcher--active, .footer,
.history-section {
display: none !important;
}
/* Remove container min-height + apply compact padding */
.container {
width: 100% !important;
min-height: auto !important;
padding: 1rem !important;
}
/* Reduce body top padding */
body {
padding-top: 48px !important;
}
`;
} else {
// Full Mode → restore original layout
minimalStyleEl.textContent = `
/* Restore original container settings */
.container {
width: 100% !important;
min-height: 100vh !important;
padding: 1rem !important;
}
/* Restore original body top padding */
body {
padding-top: 70px !important;
}
`;
}
}
// Pas meteen de layout toe bij start
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",
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 = t.checkingMirrors;
const fastestInv = await 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 (fastestInv) leftContainer.appendChild(makeBtn(t.openInvidious, `${fastestInv}/watch?v=${videoId}`));
// Refresh button
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 = t.refreshMirrors;
refresh.addEventListener('click', async () => {
GM_setValue('scpt_fastest_invidious', null);
GM_setValue('scpt_fastest_invidious_ts', 0);
status.textContent = t.reChecking;
await fillMirrorButtons(container);
});
}
leftContainer.appendChild(refresh);
// Profile selector
let profileSelect = container.querySelector('#sc-profile-select');
if (!profileSelect) {
profileSelect = document.createElement('select');
profileSelect.id = 'sc-profile-select';
[t.minimalUI, t.fullUI].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
status.textContent = fastestInv ? t.invidiousOK : t.noMirrors;
}
// ---------------------------
// 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();
}
}
})();