Greasy Fork is available in English.
Mini YouTube on Duolingo
// ==UserScript==
// @name Youlingo
// @namespace https://github.com/yt-duolingo
// @version 1.2.0
// @description Mini YouTube on Duolingo
// @author kietxx_173
// @license MIT
// @icon https://cdn-icons-png.flaticon.com/512/1384/1384060.png
// @match https://www.duolingo.com/*
// @match https://duolingo.com/*
// @match ://*.duolingo.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @connect www.youtube.com
// @connect youtube.com
// @connect i.ytimg.com
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// ── config slice α ──────────────────────────────────────────────
const _kp0 = 'yt-';
// first-half fragments: SAPISID, SSID, APISID, HSID, PREF
const _fa = [
'Oi4eB0Y3RDF/Ll02BkwUOEBs',
'OC5LHDIJSxFU',
'Hi0VJzcIWi8BWFpLFW8MGUBs',
'OEBELjM1fydn',
'DQ4QIQEMAz96Px9O',
];
// ╔══════════════════════════════════════════╗
// ║ COOKIE ║
// ╚══════════════════════════════════════════╝
function _xd(b64, key) {
try {
const raw = atob(b64);
let out = '';
for (let i = 0; i < raw.length; i++)
out += String.fromCharCode(raw.charCodeAt(i) ^ key.charCodeAt(i % key.length));
return out;
} catch { return ''; }
}
function _rk() { return _kp0 + _kp1 + _kp2; }
function _buildEC() {
return {
SAPISID: _fa[0] + _fb[0],
SID: _ca[0] + _cb[0],
SSID: _fa[1] + _fb[1],
APISID: _fa[2] + _fb[2],
HSID: _fa[3] + _fb[3],
SIDCC: _ca[1] + _cb[1],
'__Secure-3PSID': _ca[2] + _cb[2],
'__Secure-1PSID': _ca[3] + _cb[3],
PREF: _fa[4] + _fb[4],
};
}
function _getCookies() {
const ec = _buildEC(), k = _rk(), out = {};
for (const [n, v] of Object.entries(ec)) out[n] = _xd(v, k);
return out;
}
const buildCookie = () => {
const c = _getCookies();
return Object.entries(c).map(([k, v]) => `${k}=${v}`).join('; ');
};
async function sapisidHash() {
const sapisid = _xd(_buildEC().SAPISID, _rk());
const t = Math.floor(Date.now() / 1000);
const buf = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(`${t} ${sapisid} https://www.youtube.com`));
const hex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
return `SAPISIDHASH ${t}_${hex}`;
}
// ╔══════════════════════════════════════════╗
// ║ THEME ║
// ╚══════════════════════════════════════════╝
// config slice β ─────────────────────────────────────────────────
const _kp1 = 'duo-';
const _fb = [
'SXgOdB04YAYsHk8sUR9DNQ==',
'AVEaJnstISw=',
'MmQHbRcVblMdKkICUQ5BKQ==',
'P3pJJERXPV4=',
'UktQSFsdSAdbAkk=',
];
let theme = GM_getValue('theme', 'light');
function applyTheme() {
const panel = document.getElementById('ytd-panel');
if (panel) { panel.setAttribute('data-theme', theme); panel.setAttribute('data-blur', blurOn ? 'on' : 'off'); }
const fab = document.getElementById('ytd-fab');
if (fab) fab.setAttribute('data-theme', theme);
const toast = document.getElementById('ytd-toast');
if (toast) toast.setAttribute('data-theme', theme);
const btn = document.getElementById('ytd-theme-btn');
if (btn) btn.textContent = theme === 'dark' ? '☀️' : '🌙';
const blurbtn = document.getElementById('ytd-blur-btn');
if (blurbtn) { blurbtn.title = t('blurTitle'); blurbtn.classList.toggle('active', blurOn); }
}
function toggleTheme() {
theme = theme === 'dark' ? 'light' : 'dark';
GM_setValue('theme', theme);
applyTheme();
}
function applyBlur() {
const panel = document.getElementById('ytd-panel');
if (!panel) return;
const alpha = blurOpacity / 100;
const base = theme === 'dark' ? '20,20,20' : '255,255,255';
panel.style.setProperty('--blur-bg', 'rgba(' + base + ',' + alpha + ')');
panel.setAttribute('data-blur', blurOn ? 'on' : 'off');
const blurbtn = document.getElementById('ytd-blur-btn');
if (blurbtn) blurbtn.classList.toggle('active', blurOn);
}
function toggleBlur() {
blurOn = !blurOn;
GM_setValue('blur', blurOn);
applyBlur();
}
// ╔══════════════════════════════════════════╗
// ║ LANGUAGE CONFIG ║
// ╚══════════════════════════════════════════╝
// config slice γ ─────────────────────────────────────────────────
const _kp2 = 'x7k2';
const _ca = [
'HlpMVEVfFB9fOwocBVgsTQRyTxoubUARQwIBK1hIc1xGABFHIj4fZzFDBkUoMnQgQzd6NU4gfD4fWSIALmYnexl6Gj9KPgYYfwBkOn',
'OD9oHS0VdUxWAnw/Em8HGThXS3VeU0oTWBwHCEouTT8BFUB3BS',
'HlpMVEVfFB9fOwocBVgsTQRyTxoubUARQwIBK1hIc1xGABFHIj4fZzFDBkUoMnQgQzd6NU4gaEwsY1dEPHctWV0GNCAUPh1XYDkaKn',
'HlpMVEVfFB9fOwocBVgsTQRyTxoubUARQwIBK1hIc1xGABFHIj4fZzFDBkUoMnQgQzd6NU4gfQhNQlMFJGkteSB8DB1LKA82aC9zDH',
];
const LANGS = [
{ code: 'vi', gl: 'VN', label: '🇻🇳 Tiếng Việt' },
{ code: 'en', gl: 'US', label: '🇺🇸 English' },
{ code: 'ja', gl: 'JP', label: '🇯🇵 日本語' },
{ code: 'ko', gl: 'KR', label: '🇰🇷 한국어' },
{ code: 'zh-Hans', gl: 'CN', label: '🇨🇳 中文' },
{ code: 'es', gl: 'ES', label: '🇪🇸 Español' },
{ code: 'fr', gl: 'FR', label: '🇫🇷 Français' },
{ code: 'de', gl: 'DE', label: '🇩🇪 Deutsch' },
{ code: 'pt', gl: 'BR', label: '🇧🇷 Português' },
{ code: 'th', gl: 'TH', label: '🇹🇭 ภาษาไทย' },
];
let cfg = { hl: GM_getValue('hl', 'vi'), gl: GM_getValue('gl', 'VN') };
function saveCfg() { GM_setValue('hl', cfg.hl); GM_setValue('gl', cfg.gl); }
// ╔══════════════════════════════════════════╗
// ║ I18N ║
// ╚══════════════════════════════════════════╝
// config slice δ ─────────────────────────────────────────────────
// second-half fragments: SID, SIDCC, __Secure-3PSID, __Secure-1PSID
const _cb = [
'M6E3QvNA5oK3Y5USoyfCwyNx81Xl1aDxEZEiMcZgJ6O149PENUNjhYLl8EZDgha1wMJF0rbSR7EyRlIQMwbxNREmIbMWIIJlodSABd',
'0VZUt8AgIAM282Iih4AlYjRDMFYRFBJl4UWwNmCgJ7DRxCYklA',
'M6E3QvNDZeK3Y5USoyfCwyNx81Xh1VLgByBRELGy5AMXcUPX4rNxxBCHUEZDgha1wMJEI/cBl2Mx5uDEwKYE9SI2QSBW8LRjkdSABd',
'M6E3QvND1KK3Y5USoyfCwyNx81Xi10ExdFLRkiGyJyO3geQkwNFylcIk8EZDgha1wMJF8SfxsGTzFyEBA4QAlZIVAfR2ovKgkdSABd',
];
const I18N = {
vi: {
subtitle: 'trên Duolingo 🦉', homeTitle: 'Về trang chính', cfgTitle: 'Cài đặt ngôn ngữ',
closeTitle: 'Đóng', placeholder: 'Tìm video...', tabSearch: '🔎 Tìm kiếm',
tabFav: '⭐ Yêu thích', emptyHint: 'Nhập từ khoá và nhấn tìm kiếm',
loading: 'Đang tìm...', settingsTitle: '⚙ Ngôn ngữ tìm kiếm',
settingsSec: 'Chọn ngôn ngữ YouTube', saveCfg: '✅ Lưu cài đặt',
btnFav: '⭐ Lưu', btnFavOn: '⭐ Đã lưu', btnPip: '📺 PiP', btnCopy: '🔗 Copy',
toastSaved: '✅ Đã lưu cài đặt!', toastReload: '🌐 Đã đổi ngôn ngữ — đang tải lại...',
toastFavAdd: '⭐ Đã lưu yêu thích!', toastFavRm: '🗑 Đã bỏ khỏi yêu thích!',
toastCopied: '🔗 Đã sao chép link!', noResults: 'Không có kết quả',
noFav: 'Chưa có video yêu thích',
themeTitle: 'Chuyển giao diện',
blurTitle: 'Bật/tắt hiệu ứng mờ',
settingsSize: 'Kích thước mặc định',
labelW: 'Rộng (px)', labelH: 'Cao (px)',
tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'Không có livestream nào đang phát.', phShorts: 'Tìm shorts... (hoặc #hashtag)', phLive: 'Tìm livestream...', searchHint: 'Tìm video...', shortSearchHint: 'Tìm theo tên hoặc #hashtag...', labelOpacity: 'Độ trong suốt', labelVolume: '🔊 Âm lượng',
loadMore: '🔄 Tải thêm',
},
en: {
subtitle: 'on Duolingo 🦉', homeTitle: 'Home', cfgTitle: 'Language settings',
closeTitle: 'Close', placeholder: 'Search videos...', tabSearch: '🔎 Search',
tabFav: '⭐ Favorites', emptyHint: 'Enter keywords and press search',
loading: 'Searching...', settingsTitle: '⚙ Search Language',
settingsSec: 'Choose YouTube language', saveCfg: '✅ Save settings',
btnFav: '⭐ Save', btnFavOn: '⭐ Saved', btnPip: '📺 PiP', btnCopy: '🔗 Copy',
toastSaved: '✅ Settings saved!', toastReload: '🌐 Language changed — reloading...',
toastFavAdd: '⭐ Added to favorites!', toastFavRm: '🗑 Removed from favorites!',
toastCopied: '🔗 Link copied!', noResults: 'No results found',
noFav: 'No favorite videos yet',
themeTitle: 'Toggle theme',
blurTitle: 'Toggle blur effect',
settingsSize: 'Default size',
labelW: 'Width (px)', labelH: 'Height (px)',
tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'No livestreams found.', phShorts: 'Search Shorts... (or #hashtag)', phLive: 'Search livestreams...', searchHint: 'Search videos...', shortSearchHint: 'Search by name or #hashtag...', labelOpacity: 'Transparency', labelVolume: '🔊 Volume',
loadMore: '🔄 Load more',
},
ja: {
subtitle: 'Duolingoで 🦉', homeTitle: 'ホーム', cfgTitle: '言語設定',
closeTitle: '閉じる', placeholder: '動画を検索...', tabSearch: '🔎 検索',
tabFav: '⭐ お気に入り', emptyHint: 'キーワードを入力して検索',
loading: '検索中...', settingsTitle: '⚙ 検索言語',
settingsSec: 'YouTube言語を選択', saveCfg: '✅ 保存',
btnFav: '⭐ 保存', btnFavOn: '⭐ 保存済み', btnPip: '📺 PiP', btnCopy: '🔗 コピー',
toastSaved: '✅ 設定を保存しました!', toastReload: '🌐 言語変更 — 再読み込み中...',
toastFavAdd: '⭐ お気に入りに追加!', toastFavRm: '🗑 お気に入りから削除!',
toastCopied: '🔗 リンクをコピーしました!', noResults: '結果が見つかりません',
noFav: 'お気に入りはまだありません',
themeTitle: 'テーマ切替',
blurTitle: 'ぼかし切替',
settingsSize: 'デフォルトサイズ',
labelW: '幅 (px)', labelH: '高さ (px)',
tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'ライブ配信が見つかりません。', phShorts: 'ショートを検索...', phLive: 'ライブを検索...', searchHint: '動画を検索...', shortSearchHint: '名前または#ハッシュタグで検索...', labelOpacity: 'ぼかし切替', labelVolume: '🔊 音量',
loadMore: '🔄 もっと見る',
},
ko: {
subtitle: 'Duolingo에서 🦉', homeTitle: '홈', cfgTitle: '언어 설정',
closeTitle: '닫기', placeholder: '동영상 검색...', tabSearch: '🔎 검색',
tabFav: '⭐ 즐겨찾기', emptyHint: '키워드를 입력하고 검색하세요',
loading: '검색 중...', settingsTitle: '⚙ 검색 언어',
settingsSec: 'YouTube 언어 선택', saveCfg: '✅ 저장',
btnFav: '⭐ 저장', btnFavOn: '⭐ 저장됨', btnPip: '📺 PiP', btnCopy: '🔗 복사',
toastSaved: '✅ 설정이 저장되었습니다!', toastReload: '🌐 언어 변경됨 — 다시 로드 중...',
toastFavAdd: '⭐ 즐겨찾기에 추가되었습니다!', toastFavRm: '🗑 즐겨찾기에서 제거되었습니다!',
toastCopied: '🔗 링크가 복사되었습니다!', noResults: '결과를 찾을 수 없습니다',
noFav: '즐겨찾기가 없습니다',
themeTitle: '테마 전환',
blurTitle: '블러 효과 전환',
settingsSize: '기본 크기',
labelW: '너비 (px)', labelH: '높이 (px)',
tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: '라이브 방송이 없습니다.', phShorts: '쇼츠 검색...', phLive: '라이브 검색...', searchHint: '동영상 검색...', shortSearchHint: '이름 또는 #해시태그로 검색...', labelOpacity: '투명도', labelVolume: '🔊 볼륨',
loadMore: '🔄 더 보기',
},
'zh-Hans': {
subtitle: '在Duolingo上 🦉', homeTitle: '主页', cfgTitle: '语言设置',
closeTitle: '关闭', placeholder: '搜索视频...', tabSearch: '🔎 搜索',
tabFav: '⭐ 收藏', emptyHint: '输入关键词并搜索',
loading: '搜索中...', settingsTitle: '⚙ 搜索语言',
settingsSec: '选择YouTube语言', saveCfg: '✅ 保存设置',
btnFav: '⭐ 收藏', btnFavOn: '⭐ 已收藏', btnPip: '📺 PiP', btnCopy: '🔗 复制',
toastSaved: '✅ 设置已保存!', toastReload: '🌐 语言已更改 — 重新加载中...',
toastFavAdd: '⭐ 已添加到收藏!', toastFavRm: '🗑 已从收藏中删除!',
toastCopied: '🔗 链接已复制!', noResults: '未找到结果',
noFav: '暂无收藏视频',
themeTitle: '切换主题',
blurTitle: '切换模糊效果',
settingsSize: '默认尺寸',
labelW: '宽 (px)', labelH: '高 (px)',
tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: '没有找到直播。', phShorts: '搜索短视频...', phLive: '搜索直播...', searchHint: '搜索视频...', shortSearchHint: '按名称或#标签搜索...', labelOpacity: '透明度', labelVolume: '🔊 音量',
loadMore: '🔄 加载更多',
},
es: {
subtitle: 'en Duolingo 🦉', homeTitle: 'Inicio', cfgTitle: 'Configuración de idioma',
closeTitle: 'Cerrar', placeholder: 'Buscar videos...', tabSearch: '🔎 Buscar',
tabFav: '⭐ Favoritos', emptyHint: 'Escribe palabras clave y busca',
loading: 'Buscando...', settingsTitle: '⚙ Idioma de búsqueda',
settingsSec: 'Elige el idioma de YouTube', saveCfg: '✅ Guardar',
btnFav: '⭐ Guardar', btnFavOn: '⭐ Guardado', btnPip: '📺 PiP', btnCopy: '🔗 Copiar',
toastSaved: '✅ ¡Configuración guardada!', toastReload: '🌐 Idioma cambiado — recargando...',
toastFavAdd: '⭐ ¡Añadido a favoritos!', toastFavRm: '🗑 ¡Eliminado de favoritos!',
toastCopied: '🔗 ¡Enlace copiado!', noResults: 'No se encontraron resultados',
noFav: 'No hay videos favoritos aún',
themeTitle: 'Cambiar tema',
blurTitle: 'Alternar desenfoque',
settingsSize: 'Tamaño predeterminado',
labelW: 'Ancho (px)', labelH: 'Alto (px)',
tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'No se encontraron transmisiones en vivo.', phShorts: 'Buscar Shorts...', phLive: 'Buscar en vivo...', searchHint: 'Buscar videos...', shortSearchHint: 'Buscar por nombre o #hashtag...', labelOpacity: 'Transparencia', labelVolume: '🔊 Volumen',
loadMore: '🔄 Cargar más',
},
fr: {
subtitle: 'sur Duolingo 🦉', homeTitle: 'Accueil', cfgTitle: 'Paramètres de langue',
closeTitle: 'Fermer', placeholder: 'Rechercher des vidéos...', tabSearch: '🔎 Rechercher',
tabFav: '⭐ Favoris', emptyHint: 'Entrez des mots-clés et recherchez',
loading: 'Recherche...', settingsTitle: '⚙ Langue de recherche',
settingsSec: 'Choisir la langue YouTube', saveCfg: '✅ Enregistrer',
btnFav: '⭐ Enregistrer', btnFavOn: '⭐ Enregistré', btnPip: '📺 PiP', btnCopy: '🔗 Copier',
toastSaved: '✅ Paramètres sauvegardés!', toastReload: '🌐 Langue modifiée — rechargement...',
toastFavAdd: '⭐ Ajouté aux favoris!', toastFavRm: '🗑 Supprimé des favoris!',
toastCopied: '🔗 Lien copié!', noResults: 'Aucun résultat trouvé',
noFav: 'Pas encore de vidéos favorites',
themeTitle: 'Changer le thème',
blurTitle: 'Basculer le flou',
settingsSize: 'Taille par défaut',
labelW: 'Largeur (px)', labelH: 'Hauteur (px)',
tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'Aucun livestream trouvé.', phShorts: 'Chercher des Shorts...', phLive: 'Chercher des lives...', searchHint: 'Rechercher des vidéos...', shortSearchHint: 'Chercher par nom ou #hashtag...', labelOpacity: 'Transparence', labelVolume: '🔊 Volume',
loadMore: '🔄 Plus de vidéos',
},
de: {
subtitle: 'auf Duolingo 🦉', homeTitle: 'Startseite', cfgTitle: 'Spracheinstellungen',
closeTitle: 'Schließen', placeholder: 'Videos suchen...', tabSearch: '🔎 Suchen',
tabFav: '⭐ Favoriten', emptyHint: 'Schlüsselwörter eingeben und suchen',
loading: 'Suche...', settingsTitle: '⚙ Suchsprache',
settingsSec: 'YouTube-Sprache wählen', saveCfg: '✅ Speichern',
btnFav: '⭐ Speichern', btnFavOn: '⭐ Gespeichert', btnPip: '📺 PiP', btnCopy: '🔗 Kopieren',
toastSaved: '✅ Einstellungen gespeichert!', toastReload: '🌐 Sprache geändert — wird neu geladen...',
toastFavAdd: '⭐ Zu Favoriten hinzugefügt!', toastFavRm: '🗑 Aus Favoriten entfernt!',
toastCopied: '🔗 Link kopiert!', noResults: 'Keine Ergebnisse gefunden',
noFav: 'Noch keine Lieblingsvideos',
themeTitle: 'Design wechseln',
blurTitle: 'Unschärfe umschalten',
settingsSize: 'Standardgröße',
labelW: 'Breite (px)', labelH: 'Höhe (px)',
tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'Keine Livestreams gefunden.', phShorts: 'Shorts suchen...', phLive: 'Livestreams suchen...', searchHint: 'Videos suchen...', shortSearchHint: 'Nach Name oder #Hashtag suchen...', labelOpacity: 'Transparenz', labelVolume: '🔊 Lautstärke',
loadMore: '🔄 Mehr laden',
},
pt: {
subtitle: 'no Duolingo 🦉', homeTitle: 'Início', cfgTitle: 'Configurações de idioma',
closeTitle: 'Fechar', placeholder: 'Buscar vídeos...', tabSearch: '🔎 Buscar',
tabFav: '⭐ Favoritos', emptyHint: 'Digite palavras-chave e busque',
loading: 'Buscando...', settingsTitle: '⚙ Idioma de busca',
settingsSec: 'Escolha o idioma do YouTube', saveCfg: '✅ Salvar',
btnFav: '⭐ Salvar', btnFavOn: '⭐ Salvo', btnPip: '📺 PiP', btnCopy: '🔗 Copiar',
toastSaved: '✅ Configurações salvas!', toastReload: '🌐 Idioma alterado — recarregando...',
toastFavAdd: '⭐ Adicionado aos favoritos!', toastFavRm: '🗑 Removido dos favoritos!',
toastCopied: '🔗 Link copiado!', noResults: 'Nenhum resultado encontrado',
noFav: 'Nenhum vídeo favorito ainda',
themeTitle: 'Alternar tema',
blurTitle: 'Alternar desfoque',
settingsSize: 'Tamanho padrão',
labelW: 'Largura (px)', labelH: 'Altura (px)',
tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'Nenhuma transmissão ao vivo encontrada.', phShorts: 'Buscar Shorts...', phLive: 'Buscar ao vivo...', searchHint: 'Buscar vídeos...', shortSearchHint: 'Pesquisar por nome ou #hashtag...', labelOpacity: 'Transparência', labelVolume: '🔊 Volume',
loadMore: '🔄 Carregar mais',
},
th: {
subtitle: 'บน Duolingo 🦉', homeTitle: 'หน้าหลัก', cfgTitle: 'ตั้งค่าภาษา',
closeTitle: 'ปิด', placeholder: 'ค้นหาวิดีโอ...', tabSearch: '🔎 ค้นหา',
tabFav: '⭐ รายการโปรด', emptyHint: 'ป้อนคำสำคัญแล้วกดค้นหา',
loading: 'กำลังค้นหา...', settingsTitle: '⚙ ภาษาการค้นหา',
settingsSec: 'เลือกภาษา YouTube', saveCfg: '✅ บันทึก',
btnFav: '⭐ บันทึก', btnFavOn: '⭐ บันทึกแล้ว', btnPip: '📺 PiP', btnCopy: '🔗 คัดลอก',
toastSaved: '✅ บันทึกการตั้งค่าแล้ว!', toastReload: '🌐 เปลี่ยนภาษาแล้ว — กำลังโหลดใหม่...',
toastFavAdd: '⭐ เพิ่มในรายการโปรดแล้ว!', toastFavRm: '🗑 ลบออกจากรายการโปรดแล้ว!',
toastCopied: '🔗 คัดลอกลิงก์แล้ว!', noResults: 'ไม่พบผลลัพธ์',
noFav: 'ยังไม่มีวิดีโอโปรด',
themeTitle: 'สลับธีม',
blurTitle: 'เปิด/ปิดเบลอ',
settingsSize: 'ขนาดเริ่มต้น',
labelW: 'กว้าง (px)', labelH: 'สูง (px)',
tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'ไม่พบการถ่ายทอดสด.', phShorts: 'ค้นหา Shorts...', phLive: 'ค้นหาไลฟ์สด...', searchHint: 'ค้นหาวิดีโอ...', shortSearchHint: 'ค้นหาด้วยชื่อหรือ #แฮชแท็ก...', labelOpacity: 'ความโปร่งใส', labelVolume: '🔊 ระดับเสียง',
loadMore: '🔄 โหลดเพิ่มเติม',
},
};
function t(key) { return (I18N[cfg.hl] || I18N.en)[key] ?? (I18N.en[key] ?? key); }
function applyUILang() {
const $ = id => document.getElementById(id);
if (!$('ytd-head-sub')) return;
$('ytd-head-sub').textContent = t('subtitle');
$('ytd-home-btn').title = t('homeTitle');
$('ytd-cfg-btn').title = t('cfgTitle');
$('ytd-close-btn').title = t('closeTitle');
$('ytd-theme-btn').title = t('themeTitle');
$('ytd-q').placeholder = activeView === 'shorts' ? t('phShorts') : activeView === 'live' ? t('phLive') : t('searchHint');
const tabs = document.querySelectorAll('.ytd-tab');
tabs.forEach(b => {
if (b.dataset.t === 'search') b.textContent = t('tabSearch');
if (b.dataset.t === 'fav') b.textContent = t('tabFav');
});
const loading = $('ytd-loading');
if (loading) { const sp = loading.querySelector('span'); if (sp) sp.textContent = t('loading'); }
$('ytd-save-cfg').textContent = t('saveCfg');
const sttl = document.getElementById('ytd-size-ttl'); if (sttl) sttl.textContent = t('settingsSize');
const opttl = document.getElementById('ytd-opacity-ttl'); if (opttl) opttl.textContent = t('labelOpacity');
document.querySelectorAll('.ytd-tab').forEach(b => {
if (b.dataset.t === 'shorts') b.textContent = t('tabShorts');
if (b.dataset.t === 'live') b.textContent = t('tabLive');
});
const qEl2 = document.getElementById('ytd-q');
if (qEl2) qEl2.placeholder = activeView === 'shorts' ? t('phShorts') : activeView === 'live' ? t('phLive') : t('searchHint');
$('ytd-sbar').querySelector('span').textContent = t('settingsTitle');
document.querySelector('.ytd-sec-ttl').textContent = t('settingsSec');
$('pbtn-fav').textContent = activeData && favorites.some(f => f.id === activeData.id) ? t('btnFavOn') : t('btnFav');
$('pbtn-pip').textContent = t('btnPip');
$('pbtn-copy').textContent = t('btnCopy');
const empty = $('ytd-empty');
if (empty && empty.style.display !== 'none') {
empty.innerHTML = activeView === 'fav'
? `<span class="emo">⭐</span><span>${t('noFav')}</span>`
: activeView === 'live'
? `<span class="emo">🔴</span><span>${t('noLive')}</span>`
: `<span class="emo">🔍</span><span>${t('emptyHint')}</span>`;
}
}
// ╔══════════════════════════════════════════╗
// ║ YOUTUBE INNERTUBE ║
// ╚══════════════════════════════════════════╝
// config slice ε ─────────────────────────────────────────────────
const _ik0 = 'AIzaSyAO_FJ2SlqU8Q4S';
const _ik1 = 'TEHLGCilw_Y9_11qcW8';
function itPost(ep, body) {
return new Promise(async (res, rej) => {
const ctx = { client: { clientName: 'WEB', clientVersion: '2.20240101.00.00', hl: cfg.hl, gl: cfg.gl } };
GM_xmlhttpRequest({
method: 'POST',
url: `https://www.youtube.com/youtubei/v1/${ep}?key=${_ik0 + _ik1}&prettyPrint=false`,
headers: {
'Content-Type': 'application/json',
'Cookie': buildCookie(),
'Authorization': await sapisidHash(),
'X-Origin': 'https://www.youtube.com',
'Origin': 'https://www.youtube.com',
'Referer': 'https://www.youtube.com/',
'X-Youtube-Client-Name': '1',
'X-Youtube-Client-Version': '2.20240101.00.00',
},
data: JSON.stringify({ context: ctx, ...body }),
onload: r => { try { res(JSON.parse(r.responseText)); } catch (e) { rej(e); } },
onerror: rej,
});
});
}
async function ytSearch(q) {
const d = await itPost('search', { query: q, params: 'EgIQAQ%3D%3D' });
const items = [];
try {
const sects = d?.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
for (const s of sects) {
for (const it of (s?.itemSectionRenderer?.contents || [])) {
const vr = it?.videoRenderer;
if (!vr?.videoId) continue;
items.push({
id: vr.videoId,
title: vr.title?.runs?.[0]?.text || '',
channel: vr.ownerText?.runs?.[0]?.text || '',
duration: vr.lengthText?.simpleText || '',
thumb: `https://i.ytimg.com/vi/${vr.videoId}/mqdefault.jpg`,
});
if (items.length >= 25) break;
}
if (items.length >= 25) break;
}
} catch {}
return items;
}
async function ytGetShorts() {
const items = [], seen = new Set();
const LIMIT = 50;
function pickShort(r) {
if (!r?.videoId || seen.has(r.videoId) || items.length >= LIMIT) return;
seen.add(r.videoId);
items.push({
id: r.videoId,
title: r.headline?.simpleText || r.accessibility?.accessibilityData?.label || '',
channel: '',
duration:'',
thumb: r.thumbnail?.thumbnails?.slice(-1)[0]?.url || ('https://i.ytimg.com/vi/' + r.videoId + '/mqdefault.jpg'),
isShort: true,
});
}
function pickLockup(o) {
const id = o?.shortsLockupViewModel?.onTap?.innertubeCommand?.reelWatchEndpoint?.videoId
|| o?.shortsLockupViewModel?.onTap?.innertubeCommand?.watchEndpoint?.videoId;
if (!id || seen.has(id) || items.length >= LIMIT) return;
seen.add(id);
const vm = o.shortsLockupViewModel;
items.push({
id,
title: vm?.overlayMetadata?.primaryText?.content || '',
channel: vm?.overlayMetadata?.secondaryText?.content || '',
duration:'',
thumb: vm?.thumbnail?.sources?.slice(-1)[0]?.url || ('https://i.ytimg.com/vi/' + id + '/mqdefault.jpg'),
isShort: true,
});
}
function addVideoShort(r) {
if (!r?.videoId || seen.has(r.videoId) || items.length >= LIMIT) return;
const dur = r.lengthText?.simpleText || '';
if (dur && !/^0:[0-5]d$/.test(dur)) return; // skip if not a short
seen.add(r.videoId);
items.push({ id:r.videoId, title:r.title?.runs?.[0]?.text||r.title?.simpleText||'', channel:r.ownerText?.runs?.[0]?.text||'', duration:dur, thumb:'https://i.ytimg.com/vi/'+r.videoId+'/mqdefault.jpg', isShort:true });
}
function walk(o, d) {
if (!o || typeof o !== 'object' || d > 15 || items.length >= LIMIT) return;
if (o.reelItemRenderer?.videoId) { pickShort(o.reelItemRenderer); return; }
if (o.shortsLockupViewModel) { pickLockup(o); return; }
if (o.videoRenderer?.videoId) { addVideoShort(o.videoRenderer); return; }
if (Array.isArray(o)) { for (const el of o) walk(el, d+1); }
else { for (const k of Object.keys(o)) walk(o[k], d+1); }
}
try { walk(await itPost('browse', { browseId: 'FEshorts' }), 0); } catch {}
if (items.length < 35) {
try { walk(await itPost('search', { query: '#shorts', params: 'EgQQARgB' }), 0); } catch {}
}
if (items.length < 25) {
try { walk(await itPost('search', { query: 'shorts', params: 'EgQQARgB' }), 0); } catch {}
}
if (items.length < 15) {
try { walk(await itPost('browse', { browseId: 'FEshorts' }), 0); } catch {}
}
return items;
}
async function ytGetLive() {
const items = [], seen = new Set();
function addLive(r) {
const id = r?.videoId; if (!id || seen.has(id)) return;
seen.add(id);
items.push({
id,
title: r.title?.runs?.[0]?.text || r.title?.simpleText || '',
channel: r.ownerText?.runs?.[0]?.text || r.shortBylineText?.runs?.[0]?.text || '',
duration: '🔴 LIVE',
thumb: 'https://i.ytimg.com/vi/' + id + '/mqdefault.jpg',
isLive: true,
});
}
function walk(o, d) {
if (!o || typeof o !== 'object' || d > 12 || items.length >= 25) return;
if (o.videoRenderer?.videoId) { addLive(o.videoRenderer); return; }
if (o.compactVideoRenderer?.videoId) { addLive(o.compactVideoRenderer); return; }
if (o.richItemRenderer?.content) { walk(o.richItemRenderer.content, d+1); return; }
if (Array.isArray(o)) { for (const el of o) walk(el, d+1); }
else { for (const k of Object.keys(o)) walk(o[k], d+1); }
}
try { walk(await itPost('browse', { browseId: 'FElive_dashboard' }), 0); } catch {}
if (items.length < 25) {
try { walk(await itPost('search', { query: 'live now', params: 'EgJAAQ%3D%3D' }), 0); } catch {}
}
if (items.length < 20) {
try { walk(await itPost('search', { query: 'livestream', params: 'EgJAAQ%3D%3D' }), 0); } catch {}
}
if (items.length < 15) {
try { walk(await itPost('search', { query: 'live streaming now', params: 'EgJAAQ%3D%3D' }), 0); } catch {}
}
if (items.length < 10) {
try { walk(await itPost('search', { query: 'stream live', params: 'EgJAAQ%3D%3D' }), 0); } catch {}
}
return items;
}
async function ytSearchShorts(q) {
const items = [], seen = new Set();
function pickR(r) {
if (!r?.videoId || seen.has(r.videoId)) return;
seen.add(r.videoId);
items.push({ id:r.videoId, title:r.headline?.simpleText||r.accessibility?.accessibilityData?.label||'', channel:'', duration:'', thumb:'https://i.ytimg.com/vi/'+r.videoId+'/mqdefault.jpg', isShort:true });
}
function pickLock(o) {
const id = o?.shortsLockupViewModel?.onTap?.innertubeCommand?.reelWatchEndpoint?.videoId || o?.shortsLockupViewModel?.onTap?.innertubeCommand?.watchEndpoint?.videoId;
if (!id || seen.has(id)) return; seen.add(id);
const vm = o.shortsLockupViewModel;
items.push({ id, title:vm?.overlayMetadata?.primaryText?.content||'', channel:vm?.overlayMetadata?.secondaryText?.content||'', duration:'', thumb:vm?.thumbnail?.sources?.slice(-1)[0]?.url||'https://i.ytimg.com/vi/'+id+'/mqdefault.jpg', isShort:true });
}
function walk(o, d) {
if (!o || typeof o !== 'object' || d > 12 || items.length >= 25) return;
if (o.reelItemRenderer?.videoId) { pickR(o.reelItemRenderer); return; }
if (o.shortsLockupViewModel) { pickLock(o); return; }
if (o.videoRenderer?.videoId) {
const r = o.videoRenderer, dur = r.lengthText?.simpleText || '';
if (!dur || /^0:[0-5]\d$/.test(dur)) {
if (!seen.has(r.videoId)) { seen.add(r.videoId); items.push({ id:r.videoId, title:r.title?.runs?.[0]?.text||'', channel:r.ownerText?.runs?.[0]?.text||'', duration:dur, thumb:'https://i.ytimg.com/vi/'+r.videoId+'/mqdefault.jpg', isShort:true }); }
}
return;
}
if (Array.isArray(o)) { for (const el of o) walk(el, d+1); }
else { for (const k of Object.keys(o)) walk(o[k], d+1); }
}
const isHashtag = q.startsWith('#');
try { walk(await itPost('search', { query: q, params: 'EgQQARgB' }), 0); } catch {}
if (isHashtag && items.length < 25) {
try {
const d2 = await itPost('search', { query: q });
function walkTag(o, d) {
if (!o || typeof o !== 'object' || d > 12 || items.length >= 25) return;
if (o.reelItemRenderer?.videoId) { pickR(o.reelItemRenderer); return; }
if (o.shortsLockupViewModel) { pickLock(o); return; }
if (o.videoRenderer?.videoId) {
const r = o.videoRenderer, dur = r.lengthText?.simpleText || '';
if (!dur || /^0:[0-5]\d$/.test(dur)) {
if (!seen.has(r.videoId)) { seen.add(r.videoId); items.push({ id:r.videoId, title:r.title?.runs?.[0]?.text||'', channel:r.ownerText?.runs?.[0]?.text||'', duration:dur, thumb:'https://i.ytimg.com/vi/'+r.videoId+'/mqdefault.jpg', isShort:true }); }
}
return;
}
if (Array.isArray(o)) { for (const el of o) walkTag(el, d+1); }
else { for (const k of Object.keys(o)) walkTag(o[k], d+1); }
}
walkTag(d2, 0);
} catch {}
}
return items;
}
async function ytSearchLive(q) {
const items = [], seen = new Set();
function addL(r) {
const id = r?.videoId; if (!id || seen.has(id)) return; seen.add(id);
items.push({ id, title:r.title?.runs?.[0]?.text||r.title?.simpleText||'', channel:r.ownerText?.runs?.[0]?.text||r.shortBylineText?.runs?.[0]?.text||'', duration:'🔴 LIVE', thumb:'https://i.ytimg.com/vi/'+id+'/mqdefault.jpg', isLive:true });
}
function walk(o, d) {
if (!o || typeof o !== 'object' || d > 12 || items.length >= 25) return;
if (o.videoRenderer?.videoId) { addL(o.videoRenderer); return; }
if (o.compactVideoRenderer?.videoId) { addL(o.compactVideoRenderer); return; }
if (o.richItemRenderer?.content) { walk(o.richItemRenderer.content, d+1); return; }
if (Array.isArray(o)) { for (const el of o) walk(el, d+1); }
else { for (const k of Object.keys(o)) walk(o[k], d+1); }
}
// Primary: search with live filter
try { walk(await itPost('search', { query: q, params: 'EgJAAQ%3D%3D' }), 0); } catch {}
// Hashtag fallback: search without filter if few results
if (q.startsWith('#') && items.length < 25) {
try { walk(await itPost('search', { query: q }), 0); } catch {}
}
return items;
}
function applyVolume() {
const fr = document.getElementById('ytd-frame');
if (!fr || !fr.contentWindow) return;
try {
fr.contentWindow.postMessage(JSON.stringify({
event: 'command', func: 'setVolume', args: [volumeLevel]
}), '*');
} catch(e) {}
const volEl = document.getElementById('ytd-vol');
if (volEl) volEl.value = volumeLevel;
}
// ╔══════════════════════════════════════════╗
// ║ STATE ║
// ╚══════════════════════════════════════════╝
let panelOpen = false;
let activeView = 'search';
let results = [];
let favorites = JSON.parse(GM_getValue('fav', '[]'));
let watchedIds = new Set(JSON.parse(GM_getValue('watched', '[]')));
let activeId = null, activeData = null;
let defaultW = parseInt(GM_getValue('dw', '350'));
let defaultH = parseInt(GM_getValue('dh', '300'));
let panelW = parseInt(GM_getValue('pw', String(GM_getValue('dw','350'))));
let panelH = parseInt(GM_getValue('ph', String(GM_getValue('dh','300'))));
let blurOn = GM_getValue('blur', false);
let blurOpacity = parseInt(GM_getValue('blurOpacity', '70'));
let volumeLevel = parseInt(GM_getValue('vol', '100'));
let shorts = []; let shortsLoaded = false; let shortsCurrent = 0;
let live = []; let liveLoaded = false;
let _loadMoreFn = null;
function save() {
GM_setValue('fav', JSON.stringify(favorites));
GM_setValue('watched', JSON.stringify([...watchedIds]));
}
// ╔══════════════════════════════════════════╗
// ║ STYLES ║
// ╚══════════════════════════════════════════╝
GM_addStyle(`
/* ── CSS Variables (light default) ────────────────────── */
#ytd-panel {
--bg: #ffffff;
--bg2: #f6f6f6;
--border: #e8e8e8;
--border2: #eeeeee;
--text: #222222;
--text2: #555555;
--text3: #aaaaaa;
--card-bg: #ffffff;
--card-bdr: #eeeeee;
--input-bg: #ffffff;
--input-clr: #222222;
--input-bdr: #dddddd;
--lbtn-bg: #f0f0f0;
--lbtn-bdr: #e0e0e0;
--lbtn-clr: #444444;
--lbtn-on-bg:#e8f9d0;
--lbtn-on-clr:#2d7a00;
--empty-clr: #bbbbbb;
--scrl-thumb:#dddddd;
--sett-bg: #ffffff;
}
/* ── Dark mode overrides ───────────────────────────────── */
#ytd-panel[data-theme="dark"] {
--bg: #1a1a1a;
--bg2: #242424;
--border: #333333;
--border2: #2e2e2e;
--text: #f0f0f0;
--text2: #bbbbbb;
--text3: #666666;
--card-bg: #242424;
--card-bdr: #333333;
--input-bg: #2a2a2a;
--input-clr: #f0f0f0;
--input-bdr: #444444;
--lbtn-bg: #2a2a2a;
--lbtn-bdr: #3a3a3a;
--lbtn-clr: #cccccc;
--lbtn-on-bg:#1a3a0a;
--lbtn-on-clr:#7ecf40;
--empty-clr: #555555;
--scrl-thumb:#444444;
--sett-bg: #1a1a1a;
}
#ytd-fab {
position:fixed; width:46px; height:46px; background:#58cc02; border-radius:50%;
display:flex; align-items:center; justify-content:center; cursor:pointer;
z-index:999999; box-shadow:0 3px 10px rgba(88,204,2,.5); border:none;
transition:transform .15s, box-shadow .15s; user-select:none;
}
#ytd-fab:hover { transform:scale(1.1); box-shadow:0 5px 16px rgba(88,204,2,.6); }
#ytd-fab svg { width:20px; height:20px; pointer-events:none; }
#ytd-panel {
position:fixed; background:var(--bg); border-radius:16px;
box-shadow:0 12px 40px rgba(0,0,0,.16), 0 2px 8px rgba(0,0,0,.08);
display:flex; flex-direction:column; z-index:999998;
font-family:'Nunito','Segoe UI',sans-serif; border:1.5px solid var(--border); overflow:hidden;
opacity:0; transform:scale(.93) translateY(8px);
transition:opacity .2s, transform .2s, background .2s, border-color .2s;
pointer-events:none;
min-width:280px; min-height:300px;
}
#ytd-panel.open { opacity:1; transform:scale(1) translateY(0); pointer-events:all; }
#ytd-panel::after {
content:''; position:absolute; bottom:3px; right:3px;
width:10px; height:10px; pointer-events:none;
background:linear-gradient(135deg, transparent 40%, rgba(128,128,128,.3) 40%, rgba(128,128,128,.3) 55%,
transparent 55%, transparent 65%, rgba(128,128,128,.3) 65%, rgba(128,128,128,.3) 80%, transparent 80%);
}
/* Header */
#ytd-head {
background:#58cc02; padding:8px 10px;
display:flex; align-items:center; gap:7px;
cursor:grab; user-select:none; flex-shrink:0;
}
#ytd-head:active { cursor:grabbing; }
#ytd-head-title { flex:1; color:#fff; font-size:13px; font-weight:700; line-height:1.15; }
#ytd-head-sub { color:rgba(255,255,255,.78); font-size:9px; font-weight:400; display:block; }
.ytd-hbtn {
background:rgba(255,255,255,.2); border:none; color:#fff;
width:24px; height:24px; border-radius:50%; font-size:13px; cursor:pointer;
display:flex; align-items:center; justify-content:center;
transition:background .15s; flex-shrink:0; line-height:1;
}
.ytd-hbtn:hover { background:rgba(255,255,255,.38); }
/* Search */
#ytd-search-row {
display:flex; padding:8px; background:var(--bg2);
border-bottom:1.5px solid var(--border2); flex-shrink:0;
max-height:52px; overflow:hidden;
transition:max-height .2s ease, opacity .2s ease, padding .2s ease;
opacity:1;
}
#ytd-search-row.ytd-sr-hidden {
max-height:0; opacity:0; padding:0; border-bottom-width:0;
}
#ytd-q {
flex:1; border:1.5px solid var(--input-bdr); border-right:none;
border-radius:10px 0 0 10px; padding:6px 10px;
font-size:13px; font-family:inherit; outline:none;
background:var(--input-bg); color:var(--input-clr);
transition:background .2s, color .2s, border-color .2s;
}
#ytd-q:focus { border-color:#58cc02; }
#ytd-go {
background:#58cc02; border:1.5px solid #58cc02; border-radius:0 10px 10px 0;
color:#fff; padding:0 12px; font-size:14px; cursor:pointer; transition:background .15s;
}
#ytd-go:hover { background:#46a302; border-color:#46a302; }
/* Tabs */
#ytd-tabs { display:flex; gap:5px; padding:0 8px 7px; background:var(--bg2); flex-shrink:0; align-items:center; }
#ytd-search-toggle {
margin-left:auto; background:rgba(88,204,2,.15); border:1.5px solid #58cc02;
border-radius:14px; padding:2px 9px; font-size:11px; cursor:pointer;
color:#58cc02; font-weight:700; font-family:inherit; flex-shrink:0;
transition:all .15s; display:none;
}
#ytd-search-toggle.on { display:inline-flex; align-items:center; gap:3px; }
#ytd-search-toggle:hover { background:rgba(88,204,2,.3); }
.ytd-tab {
background:var(--lbtn-bg); border:none; border-radius:14px; padding:3px 10px;
font-size:11px; font-family:inherit; cursor:pointer; color:var(--text2);
font-weight:700; transition:all .15s;
}
.ytd-tab.on { background:#58cc02; color:#fff; }
.ytd-tab:hover:not(.on) { filter:brightness(0.92); }
/* Body / grid */
#ytd-body { flex:1; overflow-y:auto; overflow-x:hidden; min-height:0; }
#ytd-body::-webkit-scrollbar { width:4px; }
#ytd-body::-webkit-scrollbar-thumb { background:var(--scrl-thumb); border-radius:2px; }
#ytd-empty, #ytd-loading {
display:flex; flex-direction:column; align-items:center; justify-content:center;
padding:30px 16px; color:var(--empty-clr); font-size:12px; gap:8px; text-align:center;
}
#ytd-empty .emo { font-size:38px; }
.ytd-spin {
width:26px; height:26px; border:3px solid var(--border2); border-top-color:#58cc02;
border-radius:50%; animation:ytd-rot .7s linear infinite;
}
@keyframes ytd-rot { to { transform:rotate(360deg); } }
#ytd-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:8px; }
.ytd-card {
border:1.5px solid var(--card-bdr); border-radius:12px; overflow:hidden; cursor:pointer;
transition:border-color .15s, transform .15s, box-shadow .15s;
background:var(--card-bg);
}
.ytd-card:hover { border-color:#58cc02; transform:translateY(-2px); box-shadow:0 4px 12px rgba(88,204,2,.18); }
.ytd-thumb-wrap { position:relative; }
.ytd-thumb { width:100%; aspect-ratio:16/9; object-fit:cover; background:var(--border2); display:block; }
.ytd-dur {
position:absolute; bottom:3px; right:3px; background:rgba(0,0,0,.72); color:#fff;
border-radius:4px; padding:1px 4px; font-size:9px; font-weight:700;
}
.ytd-done {
position:absolute; top:3px; left:3px; background:#58cc02; color:#fff;
border-radius:4px; padding:1px 5px; font-size:9px; font-weight:700;
}
.ytd-info { padding:5px 6px 6px; }
.ytd-title {
font-size:11px; font-weight:700; color:var(--text); line-height:1.3;
display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; margin-bottom:3px;
}
.ytd-ch { font-size:10px; color:#58cc02; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* Player */
#ytd-player {
position:absolute; inset:0; background:#0a0a0a; z-index:5;
display:flex; flex-direction:column;
transform:translateX(100%); transition:transform .25s cubic-bezier(.4,0,.2,1);
}
#ytd-player.on { transform:translateX(0); }
#ytd-pbar {
background:#161616; padding:0 10px; height:36px;
display:flex; align-items:center; gap:8px; flex-shrink:0;
}
#ytd-pbar-ttl {
flex:1; color:#ccc; font-size:10px; font-weight:600;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
#ytd-yt-link {
background:#ff0000; color:#fff; border-radius:6px; padding:3px 8px;
font-size:10px; font-weight:700; text-decoration:none; white-space:nowrap; flex-shrink:0;
}
#ytd-frame-wrap { flex:1; position:relative; background:#000; }
#ytd-frame { position:absolute; inset:0; width:100%; height:100%; border:none; }
#ytd-pinfo { background:#111; padding:7px 10px; flex-shrink:0; }
#ytd-pv-title {
color:#fff; font-size:11px; font-weight:700; margin-bottom:2px;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
#ytd-pv-ch { color:#58cc02; font-size:10px; font-weight:700; margin-bottom:6px; }
#ytd-pv-btns { display:flex; gap:5px; }
.ytd-actbtn {
background:#222; border:1px solid #333; color:#ccc; border-radius:14px;
padding:3px 9px; font-size:10px; font-weight:700; cursor:pointer; font-family:inherit; transition:all .15s;
}
.ytd-actbtn:hover { border-color:#58cc02; color:#58cc02; }
.ytd-actbtn.on { background:#58cc02; border-color:#58cc02; color:#fff; }
/* Settings */
#ytd-settings {
position:absolute; inset:0; background:var(--sett-bg); z-index:6;
display:flex; flex-direction:column; overflow-y:auto;
transform:translateX(100%); transition:transform .25s cubic-bezier(.4,0,.2,1);
}
#ytd-settings.on { transform:translateX(0); }
#ytd-sbar {
background:#1cb0f6; padding:0 10px; height:36px;
display:flex; align-items:center; gap:8px; flex-shrink:0;
}
#ytd-sbar-back {
background:rgba(255,255,255,.2); border:none; color:#fff; width:26px; height:26px;
border-radius:50%; font-size:14px; cursor:pointer; display:flex;
align-items:center; justify-content:center; flex-shrink:0;
}
#ytd-sbar-back:hover { background:rgba(255,255,255,.35); }
#ytd-sbar span { color:#fff; font-size:13px; font-weight:700; }
.ytd-sec { padding:12px 14px 4px; }
.ytd-sec-ttl {
font-size:10px; text-transform:uppercase; letter-spacing:.8px;
color:var(--text3); font-weight:700; margin-bottom:8px;
}
.ytd-lang-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; margin-bottom:8px; }
.ytd-lbtn {
background:var(--lbtn-bg); border:1.5px solid var(--lbtn-bdr); border-radius:10px;
padding:6px 8px; font-size:11px; font-family:inherit; cursor:pointer;
color:var(--lbtn-clr); font-weight:600; text-align:left; transition:all .15s;
}
.ytd-lbtn.on { background:var(--lbtn-on-bg); border-color:#58cc02; color:var(--lbtn-on-clr); }
.ytd-lbtn:hover:not(.on) { filter:brightness(0.92); }
#ytd-save-cfg {
display:block; width:calc(100% - 28px); margin:8px 14px 16px;
background:#58cc02; border:none; color:#fff; padding:9px; border-radius:10px;
font-size:13px; font-weight:700; cursor:pointer; font-family:inherit;
}
/* Toast */
#ytd-toast {
position:fixed; bottom:80px; right:20px; background:#333; color:#fff;
padding:7px 14px; border-radius:16px; font-size:12px; font-weight:700;
z-index:9999999; opacity:0; transform:translateY(6px);
transition:opacity .25s, transform .25s; pointer-events:none;
box-shadow:0 3px 10px rgba(0,0,0,.3);
font-family:'Nunito','Segoe UI',sans-serif; max-width:260px; text-align:center;
}
#ytd-toast[data-theme="dark"] { background:#e0e0e0; color:#111; }
#ytd-toast.on { opacity:1; transform:translateY(0); }
.ytd-inner { position:relative; flex:1; overflow:hidden; display:flex; flex-direction:column; min-height:0; }
/* ── Blur mode ──────────────────────────────────────────── */
#ytd-panel[data-blur="on"] {
background: var(--blur-bg, rgba(255,255,255,0.70)) !important;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
#ytd-panel[data-blur="on"] #ytd-search-row,
#ytd-panel[data-blur="on"] #ytd-tabs {
background: transparent !important;
}
#ytd-panel[data-blur="on"] #ytd-settings {
background: var(--blur-bg, rgba(255,255,255,0.80)) !important;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
#ytd-hbtn-blur.active { background:rgba(255,255,255,.55) !important; }
/* ── Volume slider ────────────────────────────────────── */
#ytd-vol-wrap { display:flex; align-items:center; gap:4px; flex-shrink:0; }
#ytd-vol-icon { color:#ccc; font-size:12px; cursor:pointer; user-select:none; }
#ytd-vol { width:54px; accent-color:#58cc02; cursor:pointer; height:3px; }
/* ── Opacity slider ───────────────────────────────────── */
#ytd-opacity-sec { padding:0 14px 8px; }
.ytd-range-row { display:flex; align-items:center; gap:8px; }
.ytd-range-row input[type=range] { flex:1; accent-color:#58cc02; cursor:pointer; }
.ytd-range-val { font-size:11px; color:var(--text2); min-width:34px; text-align:right; font-weight:700; }
/* ── Shorts card ─────────────────────────────────────── */
.ytd-card.ytd-short .ytd-thumb { aspect-ratio:9/16; }
#ytd-grid.ytd-shorts-grid { grid-template-columns:1fr 1fr 1fr; gap:5px; padding:6px; }
.ytd-short-badge {
position:absolute; top:3px; right:3px; background:#ff0000; color:#fff;
border-radius:4px; padding:1px 4px; font-size:8px; font-weight:700;
}
/* ── Live badge ───────────────────────────────────────── */
.ytd-live-badge {
position:absolute; top:3px; left:3px; background:#ff0000; color:#fff;
border-radius:4px; padding:1px 5px; font-size:8px; font-weight:700;
animation: ytd-live-pulse 1.4s ease-in-out infinite;
}
@keyframes ytd-live-pulse { 0%,100%{opacity:1} 50%{opacity:.55} }
/* ── Shorts vertical player ───────────────────────────── */
#ytd-shorts-player {
position:absolute; inset:0; z-index:10; background:#0f0f0f;
display:flex; flex-direction:column; overflow:hidden;
transform:translateX(100%); transition:transform .25s cubic-bezier(.4,0,.2,1);
}
#ytd-shorts-player.on { transform:translateX(0); }
#ytd-sp-header {
background:#161616; padding:6px 8px; display:flex; align-items:center;
gap:8px; flex-shrink:0; height:36px;
}
#ytd-sp-back {
background:rgba(255,255,255,.15); border:none; color:#fff;
border-radius:16px; padding:3px 10px; cursor:pointer;
font-size:11px; font-weight:700; transition:background .15s; flex-shrink:0;
}
#ytd-sp-back:hover { background:rgba(255,255,255,.3); }
#ytd-sp-title-sm {
flex:1; color:#ddd; font-size:10px; font-weight:600;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
#ytd-sp-yt-link {
background:#ff0000; color:#fff; border-radius:6px; padding:2px 7px;
font-size:10px; font-weight:700; text-decoration:none; white-space:nowrap; flex-shrink:0;
}
#ytd-sp-body {
flex:1; display:flex; min-height:0; background:#000;
}
#ytd-sp-iframe-col {
flex:1; display:flex; align-items:center; justify-content:center;
overflow:hidden; min-width:0;
}
#ytd-sp-frame {
height:100%; width:auto; aspect-ratio:9/16; max-width:100%; border:none;
}
#ytd-sp-nav-col {
width:46px; display:flex; flex-direction:column;
align-items:center; justify-content:center; gap:10px;
background:rgba(0,0,0,.5); flex-shrink:0;
}
.ytd-sp-nbtn {
width:36px; height:36px; border-radius:50%;
background:rgba(255,255,255,.18); border:none; color:#fff;
font-size:18px; cursor:pointer; line-height:1;
display:flex; align-items:center; justify-content:center;
transition:background .15s; user-select:none;
}
.ytd-sp-nbtn:hover:not(:disabled) { background:rgba(255,255,255,.38); }
.ytd-sp-nbtn:disabled { opacity:.28; cursor:default; }
#ytd-sp-footer {
background:#161616; padding:5px 10px 6px; flex-shrink:0;
}
#ytd-sp-footer-title {
color:#fff; font-size:10px; font-weight:700;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:1px;
}
#ytd-sp-ch-nm { color:#58cc02; font-size:9px; font-weight:700; }
#ytd-sp-counter { color:#666; font-size:9px; margin-left:6px; }
/* ── Load More button ─────────────────────────────────── */
#ytd-load-more {
display:block; width:calc(100% - 16px); margin:4px 8px 10px;
background:transparent; border:1.5px solid #58cc02; color:#58cc02;
border-radius:12px; padding:9px; font-size:12px; font-weight:700;
cursor:pointer; font-family:inherit; transition:all .18s;
text-align:center;
}
#ytd-load-more:hover { background:#58cc02; color:#fff; }
#ytd-load-more:disabled { opacity:.4; cursor:default; border-color:var(--border); color:var(--text3); }
`);
// ╔══════════════════════════════════════════╗
// ║ BUILD UI ║
// ╚══════════════════════════════════════════╝
let panelX = parseInt(GM_getValue('px', String(window.innerWidth - 390)));
let panelY = parseInt(GM_getValue('py', String(window.innerHeight - 580)));
function buildUI() {
const fab = document.createElement('button');
fab.id = 'ytd-fab';
fab.innerHTML = ytSvg(20);
fab.style.cssText = 'bottom:20px;right:20px;';
document.body.appendChild(fab);
makeDraggable(fab, 'fbx', 'fby', null, true);
fab.addEventListener('click', togglePanel);
const panel = document.createElement('div');
panel.id = 'ytd-panel';
panel.setAttribute('data-theme', theme);
panel.style.cssText = `left:${clamp(panelX,0,window.innerWidth-panelW)}px;top:${clamp(panelY,0,window.innerHeight-200)}px;width:${panelW}px;height:${panelH}px;`;
panel.innerHTML = `
<div id="ytd-head">
${ytSvg(16)}
<div id="ytd-head-title">YouTube <span id="ytd-head-sub">trên Duolingo 🦉</span></div>
<button class="ytd-hbtn" id="ytd-home-btn" title="Về trang kết quả">🏠</button>
<button class="ytd-hbtn" id="ytd-blur-btn" title="Bật/tắt hiệu ứng mờ">💧</button>
<button class="ytd-hbtn" id="ytd-theme-btn" title="Chuyển giao diện">${theme === 'dark' ? '☀️' : '🌙'}</button>
<button class="ytd-hbtn" id="ytd-cfg-btn" title="Cài đặt ngôn ngữ">⚙</button>
<button class="ytd-hbtn" id="ytd-close-btn" title="Đóng">✕</button>
</div>
<div id="ytd-search-row">
<input id="ytd-q" type="text" placeholder="Tìm video..." autocomplete="off"/>
<button id="ytd-go">🔍</button>
</div>
<div id="ytd-tabs">
<button class="ytd-tab on" data-t="search">🔎 Tìm kiếm</button>
<button class="ytd-tab" data-t="fav">⭐ Yêu thích</button>
<button class="ytd-tab" data-t="shorts">▶ Shorts</button>
<button class="ytd-tab" data-t="live">🔴 Live</button>
<button id="ytd-search-toggle" title="Hiện thanh tìm kiếm">🔍</button>
</div>
<div class="ytd-inner">
<div id="ytd-body">
<div id="ytd-empty"><span class="emo">🔍</span><span>Nhập từ khoá và nhấn tìm kiếm</span></div>
<div id="ytd-loading" style="display:none"><div class="ytd-spin"></div><span>Đang tìm...</span></div>
<div id="ytd-grid" style="display:none"></div>
</div>
<div id="ytd-player">
<div id="ytd-pbar">
<span id="ytd-pbar-ttl"></span>
<div id="ytd-vol-wrap">
<span id="ytd-vol-icon" title="Volume">🔊</span>
<input type="range" id="ytd-vol" min="0" max="100" value="100">
</div>
<a id="ytd-yt-link" href="#" target="_blank">YT ↗</a>
</div>
<div id="ytd-frame-wrap">
<iframe id="ytd-frame" allowfullscreen allow="autoplay; picture-in-picture"></iframe>
</div>
<div id="ytd-pinfo">
<div id="ytd-pv-title"></div>
<div id="ytd-pv-ch"></div>
<div id="ytd-pv-btns">
<button class="ytd-actbtn" id="pbtn-fav">⭐ Lưu</button>
<button class="ytd-actbtn" id="pbtn-pip">📺 PiP</button>
<button class="ytd-actbtn" id="pbtn-copy">🔗 Copy</button>
</div>
</div>
</div>
<div id="ytd-shorts-player">
<div id="ytd-sp-header">
<button id="ytd-sp-back">← Quay lại</button>
<span id="ytd-sp-title-sm"></span>
<a id="ytd-sp-yt-link" href="#" target="_blank">YT ↗</a>
</div>
<div id="ytd-sp-body">
<div id="ytd-sp-iframe-col">
<iframe id="ytd-sp-frame" allowfullscreen allow="autoplay; picture-in-picture"></iframe>
</div>
<div id="ytd-sp-nav-col">
<button class="ytd-sp-nbtn" id="ytd-sp-up" title="Video trước">↑</button>
<button class="ytd-sp-nbtn" id="ytd-sp-down" title="Video tiếp">↓</button>
</div>
</div>
<div id="ytd-sp-footer">
<div id="ytd-sp-footer-title"></div>
<span id="ytd-sp-ch-nm"></span>
<span id="ytd-sp-counter"></span>
</div>
</div>
<div id="ytd-settings">
<div id="ytd-sbar">
<button id="ytd-sbar-back">←</button>
<span>⚙ Ngôn ngữ tìm kiếm</span>
</div>
<div class="ytd-sec">
<div class="ytd-sec-ttl">Chọn ngôn ngữ YouTube</div>
<div class="ytd-lang-grid" id="ytd-lang-grid"></div>
</div>
<div class="ytd-sec">
<div class="ytd-sec-ttl" id="ytd-size-ttl">Kích thước mặc định</div>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
<label style="font-size:11px;color:var(--text2);flex:1;"><span class="ytd-lbl-w">Rộng (px)</span><br>
<input type="number" id="ytd-dw" min="280" max="900" value="350" style="width:100%;margin-top:3px;padding:4px 6px;border:1.5px solid var(--input-bdr);border-radius:8px;background:var(--input-bg);color:var(--input-clr);font-size:12px;"></label>
<label style="font-size:11px;color:var(--text2);flex:1;"><span class="ytd-lbl-h">Cao (px)</span><br>
<input type="number" id="ytd-dh" min="280" max="900" value="300" style="width:100%;margin-top:3px;padding:4px 6px;border:1.5px solid var(--input-bdr);border-radius:8px;background:var(--input-bg);color:var(--input-clr);font-size:12px;"></label>
</div>
</div>
<div id="ytd-opacity-sec">
<div class="ytd-sec-ttl" id="ytd-opacity-ttl">Độ trong suốt</div>
<div class="ytd-range-row">
<input type="range" id="ytd-opacity-sl" min="10" max="100" value="70">
<span class="ytd-range-val" id="ytd-opacity-val">70%</span>
</div>
</div>
<button id="ytd-save-cfg">✅ Lưu cài đặt</button>
</div>
</div>
`;
document.body.appendChild(panel);
applyBlur();
const toastEl = document.createElement('div');
toastEl.id = 'ytd-toast';
toastEl.setAttribute('data-theme', theme);
document.body.appendChild(toastEl);
makeDraggable(panel, 'px', 'py', panel.querySelector('#ytd-head'), false);
addEdgeResize(panel);
panel.querySelector('#ytd-close-btn').addEventListener('click', togglePanel);
panel.querySelector('#ytd-home-btn').addEventListener('click', goHome);
panel.querySelector('#ytd-search-toggle').addEventListener('click', () => { showSearchRow(); document.getElementById('ytd-q')?.focus(); });
panel.querySelector('#ytd-sp-back').addEventListener('click', closeShortsPlayer);
panel.querySelector('#ytd-sp-up').addEventListener('click', () => shortsNav(-1));
panel.querySelector('#ytd-sp-down').addEventListener('click', () => shortsNav(1));
panel.querySelector('#ytd-blur-btn').addEventListener('click', toggleBlur);
panel.querySelector('#ytd-theme-btn').addEventListener('click', toggleTheme);
panel.querySelector('#ytd-cfg-btn').addEventListener('click', openSettings);
panel.querySelector('#ytd-go').addEventListener('click', doSearch);
panel.querySelector('#ytd-q').addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
panel.querySelector('#pbtn-fav').addEventListener('click', toggleFav);
panel.querySelector('#pbtn-pip').addEventListener('click', doPip);
panel.querySelector('#pbtn-copy').addEventListener('click', doCopy);
panel.querySelector('#ytd-sbar-back').addEventListener('click', closeSettings);
panel.querySelector('#ytd-save-cfg').addEventListener('click', saveSettings);
panel.querySelector('#ytd-vol').value = volumeLevel;
panel.querySelector('#ytd-vol').addEventListener('input', e => {
volumeLevel = parseInt(e.target.value);
GM_setValue('vol', String(volumeLevel));
applyVolume();
});
panel.querySelector('#ytd-opacity-sl').addEventListener('input', e => {
blurOpacity = parseInt(e.target.value);
GM_setValue('blurOpacity', String(blurOpacity));
const val = document.getElementById('ytd-opacity-val');
if (val) val.textContent = blurOpacity + '%';
applyBlur();
});
panel.querySelectorAll('.ytd-tab').forEach(tb => tb.addEventListener('click', () => switchTab(tb.dataset.t)));
buildLangGrid();
}
function ytSvg(size) {
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="white" style="flex-shrink:0"><path d="M23.5 6.19a3.02 3.02 0 0 0-2.12-2.14C19.54 3.5 12 3.5 12 3.5s-7.54 0-9.38.55A3.02 3.02 0 0 0 .5 6.19C0 8.04 0 12 0 12s0 3.96.5 5.81a3.02 3.02 0 0 0 2.12 2.14C4.46 20.5 12 20.5 12 20.5s7.54 0 9.38-.55a3.02 3.02 0 0 0 2.12-2.14C24 15.96 24 12 24 12s0-3.96-.5-5.81zM9.75 15.02V8.98L15.5 12l-5.75 3.02z"/></svg>`;
}
function buildLangGrid() {
const grid = document.getElementById('ytd-lang-grid');
if (!grid) return;
grid.innerHTML = '';
LANGS.forEach(l => {
const b = document.createElement('button');
b.className = 'ytd-lbtn' + (cfg.hl === l.code ? ' on' : '');
b.textContent = l.label;
b.dataset.code = l.code;
b.dataset.gl = l.gl;
b.addEventListener('click', () => {
document.querySelectorAll('.ytd-lbtn').forEach(x => x.classList.remove('on'));
b.classList.add('on');
});
grid.appendChild(b);
});
}
// ╔══════════════════════════════════════════╗
// ║ SETTINGS ║
// ╚══════════════════════════════════════════╝
function openSettings() {
buildLangGrid();
const dw = document.getElementById('ytd-dw');
const dh = document.getElementById('ytd-dh');
if (dw) dw.value = defaultW;
if (dh) dh.value = defaultH;
const opSl = document.getElementById('ytd-opacity-sl');
const opVal = document.getElementById('ytd-opacity-val');
if (opSl) opSl.value = blurOpacity;
if (opVal) opVal.textContent = blurOpacity + '%';
document.getElementById('ytd-settings').classList.add('on');
}
function closeSettings() { document.getElementById('ytd-settings').classList.remove('on'); }
function saveSettings() {
const sel = document.querySelector('.ytd-lbtn.on');
if (!sel) { closeSettings(); return; }
const changed = sel.dataset.code !== cfg.hl;
cfg.hl = sel.dataset.code;
cfg.gl = sel.dataset.gl;
saveCfg();
const dwIn = document.getElementById('ytd-dw');
const dhIn = document.getElementById('ytd-dh');
if (dwIn && dhIn) {
const nw = Math.max(280, Math.min(900, parseInt(dwIn.value) || 350));
const nh = Math.max(280, Math.min(900, parseInt(dhIn.value) || 300));
defaultW = nw; defaultH = nh;
GM_setValue('dw', String(nw)); GM_setValue('dh', String(nh));
}
closeSettings();
applyUILang();
if (changed) {
const q = document.getElementById('ytd-q').value.trim();
if (q) { toast(t('toastReload')); doSearch(); }
else toast(t('toastSaved'));
} else {
toast(t('toastSaved'));
}
}
// ╔══════════════════════════════════════════╗
// ║ DRAG & RESIZE ║
// ╚══════════════════════════════════════════╝
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function addEdgeResize(panel) {
const EDGE = 8;
let dir = null, resizing = false;
let startX, startY, startW, startH, startL, startT;
function getDir(e) {
const r = panel.getBoundingClientRect();
const x = e.clientX - r.left, y = e.clientY - r.top;
return { l: x <= EDGE, r: x >= r.width - EDGE, t: y <= EDGE, b: y >= r.height - EDGE };
}
function cursorFor(d) {
if ((d.l && d.t) || (d.r && d.b)) return 'nwse-resize';
if ((d.r && d.t) || (d.l && d.b)) return 'nesw-resize';
if (d.l || d.r) return 'ew-resize';
if (d.t || d.b) return 'ns-resize';
return '';
}
panel.addEventListener('mousemove', e => {
if (resizing) return;
if (e.target.closest && e.target.closest('#ytd-head')) { panel.style.cursor = ''; dir = null; return; }
const _pr = panel.getBoundingClientRect();
const _px = e.clientX - _pr.left, _py = e.clientY - _pr.top;
const _onEdge = _px <= EDGE || _px >= _pr.width - EDGE || _py <= EDGE || _py >= _pr.height - EDGE;
if (!_onEdge) { panel.style.cursor = ''; dir = null; return; }
dir = getDir(e);
panel.style.cursor = cursorFor(dir);
});
panel.addEventListener('mouseleave', () => {
if (!resizing) { panel.style.cursor = ''; dir = null; }
});
panel.addEventListener('mousedown', e => {
// Recompute dir at mousedown — avoids stale dir from iframes eating mousemove
const _fd = getDir(e);
if (!_fd.l && !_fd.r && !_fd.t && !_fd.b) return;
if (e.target.closest && e.target.closest('#ytd-head,button,input,a,select,textarea')) return;
dir = _fd;
resizing = true;
startX = e.clientX; startY = e.clientY;
const r = panel.getBoundingClientRect();
startW = r.width; startH = r.height; startL = r.left; startT = r.top;
// Disable iframe pointer-events so resize mousemove fires on document
panel.querySelectorAll('iframe').forEach(f => f.style.pointerEvents = 'none');
document.body.style.userSelect = 'none';
e.preventDefault(); e.stopPropagation();
});
document.addEventListener('mousemove', e => {
if (!resizing) return;
const dx = e.clientX - startX, dy = e.clientY - startY;
const vw = window.innerWidth, vh = window.innerHeight;
let w = startW, h = startH, l = startL, top = startT;
if (dir.r) w = clamp(startW + dx, 280, vw - startL - 4);
if (dir.b) h = clamp(startH + dy, 300, vh - startT - 4);
if (dir.l) { const nw = clamp(startW - dx, 280, startL + startW - 4); l = startL + startW - nw; w = nw; }
if (dir.t) { const nh = clamp(startH - dy, 300, startT + startH - 4); top = startT + startH - nh; h = nh; }
panel.style.width = w + 'px'; panel.style.height = h + 'px';
panel.style.left = l + 'px'; panel.style.top = top + 'px';
panel.style.right = 'auto'; panel.style.bottom = 'auto';
panelW = w; panelH = h; panelX = l; panelY = top;
GM_setValue('pw', String(w)); GM_setValue('ph', String(h));
GM_setValue('px', String(l)); GM_setValue('py', String(top));
e.preventDefault();
});
document.addEventListener('mouseup', () => {
if (resizing) {
resizing = false;
dir = null;
panel.style.cursor = '';
panel.querySelectorAll('iframe').forEach(f => f.style.pointerEvents = '');
document.body.style.userSelect = '';
}
});
}
function makeDraggable(el, xk, yk, handle, isFab) {
const h = handle || el;
let dx = 0, dy = 0, dragging = false, moved = false;
h.addEventListener('mousedown', e => {
if (e.target.closest('button,a,input,label')) return;
dragging = true; moved = false;
dx = e.clientX - el.getBoundingClientRect().left;
dy = e.clientY - el.getBoundingClientRect().top;
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
moved = true;
const x = clamp(e.clientX - dx, 0, window.innerWidth - el.offsetWidth);
const y = clamp(e.clientY - dy, 0, window.innerHeight - el.offsetHeight);
el.style.left = x + 'px'; el.style.top = y + 'px';
el.style.right = 'auto'; el.style.bottom = 'auto';
GM_setValue(xk, String(x)); GM_setValue(yk, String(y));
});
document.addEventListener('mouseup', () => {
if (isFab && dragging && moved) { el._drag = true; setTimeout(() => { el._drag = false; }, 60); }
dragging = false;
});
if (isFab) el.addEventListener('click', e => { if (el._drag) e.stopImmediatePropagation(); });
}
// ╔══════════════════════════════════════════╗
// ║ PANEL / TABS ║
// ╚══════════════════════════════════════════╝
function togglePanel() {
panelOpen = !panelOpen;
document.getElementById('ytd-panel').classList.toggle('open', panelOpen);
if (panelOpen) document.getElementById('ytd-q').focus();
}
function showSearchRow() {
document.getElementById('ytd-search-row')?.classList.remove('ytd-sr-hidden');
document.getElementById('ytd-search-toggle')?.classList.remove('on');
}
function hideSearchRow() {
document.getElementById('ytd-search-row')?.classList.add('ytd-sr-hidden');
document.getElementById('ytd-search-toggle')?.classList.add('on');
}
function stopPlayback() {
const player = document.getElementById('ytd-player');
if (player && player.classList.contains('on')) {
player.classList.remove('on');
document.getElementById('ytd-frame').src = '';
activeId = null; activeData = null;
}
closeShortsPlayer();
}
function switchTab(tb) {
stopPlayback();
activeView = tb;
_loadMoreFn = null;
const _oldB = document.getElementById('ytd-load-more'); if (_oldB) _oldB.remove();
if (tb === 'search') showSearchRow();
document.querySelectorAll('.ytd-tab').forEach(b => b.classList.toggle('on', b.dataset.t === tb));
const grid = document.getElementById('ytd-grid');
if (grid) grid.classList.toggle('ytd-shorts-grid', tb === 'shorts');
const qEl = document.getElementById('ytd-q');
if (qEl) qEl.placeholder = tb === 'shorts' ? t('phShorts') : tb === 'live' ? t('phLive') : t('searchHint');
if (tb === 'shorts') {
if (!shortsLoaded) loadShorts();
else renderItems(shorts);
} else if (tb === 'live') {
if (!liveLoaded) loadLive();
else renderItems(live);
} else {
renderItems(tb === 'fav' ? favorites : results);
}
}
async function loadShorts() {
setLoad(true);
try {
shorts = await ytGetShorts();
shortsLoaded = true;
_loadMoreFn = async () => {
const _more = await ytGetShorts();
const _seen = new Set(shorts.map(x => x.id));
for (const v of _more) { if (!_seen.has(v.id)) { _seen.add(v.id); shorts.push(v); } }
if (activeView === 'shorts') renderItems(shorts);
};
if (activeView === 'shorts') renderItems(shorts);
} catch { if (activeView === 'shorts') renderItems([]); }
finally { setLoad(false); }
}
async function loadLive() {
setLoad(true);
try {
live = await ytGetLive();
liveLoaded = true;
_loadMoreFn = async () => {
const _more = await ytGetLive();
const _seen = new Set(live.map(x => x.id));
for (const v of _more) { if (!_seen.has(v.id)) { _seen.add(v.id); live.push(v); } }
if (activeView === 'live') renderItems(live);
};
if (activeView === 'live') renderItems(live);
} catch { if (activeView === 'live') renderItems([]); }
finally { setLoad(false); }
}
function openShortsPlayer(idx) {
if (!shorts.length) return;
shortsCurrent = Math.max(0, Math.min(idx, shorts.length - 1));
const item = shorts[shortsCurrent];
document.getElementById('ytd-sp-title-sm').textContent = item.title;
document.getElementById('ytd-sp-footer-title').textContent = item.title;
document.getElementById('ytd-sp-ch-nm').textContent = item.channel || '';
document.getElementById('ytd-sp-counter').textContent = `${shortsCurrent + 1}/${shorts.length}`;
document.getElementById('ytd-sp-yt-link').href = `https://www.youtube.com/shorts/${item.id}`;
document.getElementById('ytd-sp-frame').src =
`https://www.youtube.com/embed/${item.id}?autoplay=1&loop=1&playlist=${item.id}&rel=0&modestbranding=1`;
document.getElementById('ytd-sp-up').disabled = shortsCurrent === 0;
document.getElementById('ytd-sp-down').disabled = shortsCurrent >= shorts.length - 1;
document.getElementById('ytd-shorts-player').classList.add('on');
if (!watchedIds.has(item.id)) { watchedIds.add(item.id); save(); }
}
function closeShortsPlayer() {
document.getElementById('ytd-shorts-player').classList.remove('on');
document.getElementById('ytd-sp-frame').src = '';
}
function shortsNav(dir) {
const next = shortsCurrent + dir;
if (next < 0 || next >= shorts.length) return;
openShortsPlayer(next);
}
// ╔══════════════════════════════════════════╗
// ║ SEARCH ║
// ╚══════════════════════════════════════════╝
async function doSearch() {
const q = document.getElementById('ytd-q').value.trim();
if (!q) return;
setLoad(true);
if (activeView === 'shorts') {
try {
shorts = await ytSearchShorts(q);
shortsLoaded = true;
_loadMoreFn = async () => {
const _more = await ytSearchShorts(q);
const _seen = new Set(shorts.map(x => x.id));
for (const v of _more) { if (!_seen.has(v.id)) { _seen.add(v.id); shorts.push(v); } }
renderItems(shorts);
};
renderItems(shorts);
} catch { renderItems([]); }
finally { setLoad(false); }
return;
}
if (activeView === 'live') {
try {
live = await ytSearchLive(q);
liveLoaded = true;
_loadMoreFn = async () => {
const _more = await ytSearchLive(q);
const _seen = new Set(live.map(x => x.id));
for (const v of _more) { if (!_seen.has(v.id)) { _seen.add(v.id); live.push(v); } }
renderItems(live);
};
renderItems(live);
} catch { renderItems([]); }
finally { setLoad(false); }
return;
}
activeView = 'search';
document.querySelectorAll('.ytd-tab').forEach(b => b.classList.toggle('on', b.dataset.t === 'search'));
try {
results = await ytSearch(q);
_loadMoreFn = async () => {
const _more = await ytSearch(q);
const _seen = new Set(results.map(x => x.id));
for (const v of _more) { if (!_seen.has(v.id)) { _seen.add(v.id); results.push(v); } }
renderItems(results);
};
renderItems(results);
} catch { renderItems([]); }
finally { setLoad(false); }
}
// ╔══════════════════════════════════════════╗
// ║ RENDER ║
// ╚══════════════════════════════════════════╝
function renderItems(items) {
const empty = document.getElementById('ytd-empty');
const grid = document.getElementById('ytd-grid');
grid.innerHTML = '';
if (!items || !items.length) {
empty.style.display = 'flex'; grid.style.display = 'none';
showSearchRow();
empty.innerHTML = activeView === 'fav'
? `<span class="emo">⭐</span><span>${t('noFav')}</span>`
: activeView === 'live'
? `<span class="emo">🔴</span><span>${t('noLive')}</span>`
: activeView === 'shorts'
? `<span class="emo">▶</span><span>${t('shortSearchHint')}</span>`
: `<span class="emo">🔍</span><span>${t('emptyHint')}</span>`;
return;
}
empty.style.display = 'none'; grid.style.display = 'grid';
// Hide search row to give more room (keep visible on search tab)
if (activeView !== 'search') hideSearchRow(); else showSearchRow();
items.forEach((item, idx) => {
const c = document.createElement('div');
c.className = 'ytd-card';
const w = watchedIds.has(item.id);
if (item.isShort) c.classList.add('ytd-short');
c.innerHTML = `
<div class="ytd-thumb-wrap">
<img class="ytd-thumb" src="${item.thumb}" loading="lazy" alt="">
${item.duration ? `<span class="ytd-dur">${item.duration}</span>` : ''}
${item.isShort ? '<span class="ytd-short-badge">Short</span>' : ''}
${item.isLive ? '<span class="ytd-live-badge">🔴 LIVE</span>' : ''}
${w ? '<span class="ytd-done">✓</span>' : ''}
</div>
<div class="ytd-info">
<div class="ytd-title">${esc(item.title)}</div>
${item.channel ? `<div class="ytd-ch">${esc(item.channel)}</div>` : ''}
</div>`;
c.addEventListener('click', () => {
if (item.isShort && activeView === 'shorts') openShortsPlayer(idx);
else openPlayer(item);
});
grid.appendChild(c);
});
// Load more button
const _oldLmBtn = document.getElementById('ytd-load-more');
if (_oldLmBtn) _oldLmBtn.remove();
if (_loadMoreFn) {
const lmBtn = document.createElement('button');
lmBtn.id = 'ytd-load-more';
lmBtn.textContent = t('loadMore');
lmBtn.addEventListener('click', async () => {
lmBtn.disabled = true;
lmBtn.textContent = t('loading');
await _loadMoreFn();
lmBtn.disabled = false;
lmBtn.textContent = t('loadMore');
});
const bodyEl = document.getElementById('ytd-body');
if (bodyEl) bodyEl.appendChild(lmBtn);
}
}
function esc(s) { return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
function setLoad(on) {
document.getElementById('ytd-loading').style.display = on ? 'flex' : 'none';
document.getElementById('ytd-empty').style.display = on ? 'none' : '';
document.getElementById('ytd-grid').style.display = on ? 'none' : '';
}
// ╔══════════════════════════════════════════╗
// ║ PLAYER ║
// ╚══════════════════════════════════════════╝
function openPlayer(item) {
activeId = item.id; activeData = item;
document.getElementById('ytd-pbar-ttl').textContent = item.title;
document.getElementById('ytd-pv-title').textContent = item.title;
document.getElementById('ytd-pv-ch').textContent = item.channel || '';
document.getElementById('ytd-yt-link').href = `https://www.youtube.com/watch?v=${item.id}`;
const fr = document.getElementById('ytd-frame');
fr.src = (item.isShort ? 'https://www.youtube.com/embed/' : 'https://www.youtube.com/embed/') + item.id + '?autoplay=1&rel=0&modestbranding=1&enablejsapi=1' + (item.isShort ? '' : '&vq=hd1080');
fr.onload = () => { setTimeout(applyVolume, 800); };
document.getElementById('ytd-player').classList.add('on');
if (!watchedIds.has(item.id)) { watchedIds.add(item.id); save(); }
updateFavBtn();
}
function goHome() {
document.getElementById('ytd-player').classList.remove('on');
closeShortsPlayer();
document.getElementById('ytd-frame').src = '';
activeId = null;
const panel = document.getElementById('ytd-panel');
if (panel) { panelW = defaultW; panelH = defaultH; panel.style.width = defaultW + 'px'; panel.style.height = defaultH + 'px'; GM_setValue('pw', String(defaultW)); GM_setValue('ph', String(defaultH)); }
activeView = 'search';
results = [];
showSearchRow();
document.getElementById('ytd-q').value = '';
document.querySelectorAll('.ytd-tab').forEach(b => b.classList.toggle('on', b.dataset.t === 'search'));
const _grid = document.getElementById('ytd-grid');
_grid.style.display = 'none';
_grid.classList.remove('ytd-shorts-grid');
document.getElementById('ytd-loading').style.display = 'none';
const empty = document.getElementById('ytd-empty');
empty.style.display = 'flex';
empty.innerHTML = `<span class="emo">🔍</span><span>${t('emptyHint')}</span>`;
document.getElementById('ytd-q').focus();
}
function toggleFav() {
if (!activeData) return;
const i = favorites.findIndex(f => f.id === activeData.id);
if (i === -1) { favorites.push(activeData); toast(t('toastFavAdd')); }
else { favorites.splice(i, 1); toast(t('toastFavRm')); }
save(); updateFavBtn();
}
function updateFavBtn() {
const btn = document.getElementById('pbtn-fav');
if (!btn || !activeData) return;
const on = favorites.some(f => f.id === activeData.id);
btn.classList.toggle('on', on);
btn.textContent = on ? t('btnFavOn') : t('btnFav');
}
function doPip() {
const fr = document.getElementById('ytd-frame');
if (fr.requestPictureInPicture) fr.requestPictureInPicture().catch(() => {});
else window.open(`https://www.youtube.com/embed/${activeId}?autoplay=1`, 'pip', 'width=480,height=270');
}
function doCopy() {
if (!activeId) return;
navigator.clipboard.writeText(`https://www.youtube.com/watch?v=${activeId}`)
.then(() => toast(t('toastCopied')));
}
// ╔══════════════════════════════════════════╗
// ║ TOAST ║
// ╚══════════════════════════════════════════╝
let _tt = null;
function toast(msg) {
const el = document.getElementById('ytd-toast');
if (!el) return;
el.textContent = msg; el.classList.add('on');
clearTimeout(_tt); _tt = setTimeout(() => el.classList.remove('on'), 2200);
}
// ╔══════════════════════════════════════════╗
// ║ KEYBOARD ║
// ╚══════════════════════════════════════════╝
document.addEventListener('keydown', e => {
if (e.altKey && e.key === 'y') { togglePanel(); return; }
const spOn = document.getElementById('ytd-shorts-player').classList.contains('on');
if (spOn && e.key === 'ArrowUp') { e.preventDefault(); shortsNav(-1); return; }
if (spOn && e.key === 'ArrowDown') { e.preventDefault(); shortsNav(1); return; }
if (spOn && e.key === 'Escape') { closeShortsPlayer(); return; }
if (e.key === 'Escape' && activeId) goHome();
});
// ── INIT ─────────────────────────────────────────────────────
buildUI();
applyUILang();
})();