Mini YouTube on Duolingo + Translate
// ==UserScript==
// @name Youlingo
// @namespace https://github.com/yt-duolingo
// @version 1.3.0
// @description Mini YouTube on Duolingo + Translate
// @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
// @connect translate.googleapis.com
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// ── config slice α ──────────────────────────────────────────────
const _kp0 = 'yt-';
const _fa = [
'Oi4eB0Y3RDF/Ll02BkwUOEBs',
'OC5LHDIJSxFU',
'Hi0VJzcIWi8BWFpLFW8MGUBs',
'OEBELjM1fydn',
'DQ4QIQEMAz96Px9O',
];
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}`;
}
// ── config slice β ─────────────────────────────────────────────
const _kp1 = 'duo-';
const _fb = [
'SXgOdB04YAYsHk8sUR9DNQ==',
'AVEaJnstISw=',
'MmQHbRcVblMdKkICUQ5BKQ==',
'P3pJJERXPV4=',
'UktQSFsdSAdbAkk=',
];
let theme = GM_getValue('theme', 'light');
let firstRun = GM_getValue('firstRun', true);
// Hàm hiển thị tutorial (có thể gọi lại từ cài đặt)
function showTutorial() {
// Xóa overlay cũ nếu có
const oldOverlay = document.getElementById('ytd-tutorial-overlay');
if (oldOverlay) oldOverlay.remove();
const tutorialHTML = `
<div id="ytd-tutorial-overlay">
<div class="ytd-tutorial-card">
<div class="ytd-tutorial-header">
<span>${t('tutorial_welcome')}</span>
<button id="ytd-tutorial-close">✕</button>
</div>
<div class="ytd-tutorial-body">
<div class="ytd-tutorial-step">
<div class="ytd-tutorial-icon">🔍</div>
<div class="ytd-tutorial-text">
<strong>${t('tutorial_step1_title')}</strong>
<p>${t('tutorial_step1_desc')}</p>
</div>
</div>
<div class="ytd-tutorial-step">
<div class="ytd-tutorial-icon">⭐</div>
<div class="ytd-tutorial-text">
<strong>${t('tutorial_step2_title')}</strong>
<p>${t('tutorial_step2_desc')}</p>
</div>
</div>
<div class="ytd-tutorial-step">
<div class="ytd-tutorial-icon">🌐</div>
<div class="ytd-tutorial-text">
<strong>${t('tutorial_step3_title')}</strong>
<p>${t('tutorial_step3_desc')}</p>
</div>
</div>
<div class="ytd-tutorial-step">
<div class="ytd-tutorial-icon">⌨️</div>
<div class="ytd-tutorial-text">
<strong>${t('tutorial_step4_title')}</strong>
<p>${t('tutorial_step4_desc')}</p>
</div>
</div>
<div class="ytd-tutorial-step">
<div class="ytd-tutorial-icon">🎨</div>
<div class="ytd-tutorial-text">
<strong>${t('tutorial_step5_title')}</strong>
<p>${t('tutorial_step5_desc')}</p>
</div>
</div>
<div class="ytd-tutorial-step">
<div class="ytd-tutorial-icon">🖱️</div>
<div class="ytd-tutorial-text">
<strong>${t('tutorial_step6_title')}</strong>
<p>${t('tutorial_step6_desc')}</p>
</div>
</div>
</div>
<div class="ytd-tutorial-footer">
<label class="ytd-tutorial-checkbox">
<input type="checkbox" id="ytd-tutorial-dont-show"> ${t('tutorial_dont_show')}
</label>
<button id="ytd-tutorial-start">${t('tutorial_start_btn')}</button>
</div>
</div>
</div>
`;
const style = document.createElement('style');
style.textContent = `
#ytd-tutorial-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.75);
z-index: 99999999;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Nunito', 'Segoe UI', sans-serif;
animation: ytd-fadein 0.2s ease;
}
@keyframes ytd-fadein {
from { opacity: 0; }
to { opacity: 1; }
}
.ytd-tutorial-card {
background: var(--tg-bg, #ffffff);
border-radius: 20px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
animation: ytd-slideup 0.3s ease;
}
@keyframes ytd-slideup {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.ytd-tutorial-card[data-theme="dark"] {
--tg-bg: #1a1a1a;
--tg-text: #f0f0f0;
--tg-text2: #bbbbbb;
--tg-border: #333333;
}
.ytd-tutorial-card {
--tg-bg: #ffffff;
--tg-text: #222222;
--tg-text2: #555555;
--tg-border: #e8e8e8;
}
.ytd-tutorial-header {
background: #58cc02;
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
color: white;
font-weight: bold;
font-size: 18px;
}
#ytd-tutorial-close {
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s;
}
#ytd-tutorial-close:hover {
background: rgba(255,255,255,0.4);
}
.ytd-tutorial-body {
padding: 20px;
max-height: 55vh;
overflow-y: auto;
color: var(--tg-text);
}
.ytd-tutorial-body::-webkit-scrollbar {
width: 5px;
}
.ytd-tutorial-body::-webkit-scrollbar-thumb {
background: #58cc02;
border-radius: 3px;
}
.ytd-tutorial-step {
display: flex;
gap: 15px;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--tg-border);
}
.ytd-tutorial-step:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.ytd-tutorial-icon {
font-size: 32px;
flex-shrink: 0;
}
.ytd-tutorial-text {
flex: 1;
}
.ytd-tutorial-text strong {
display: block;
margin-bottom: 6px;
color: #58cc02;
font-size: 14px;
}
.ytd-tutorial-text p {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: var(--tg-text2);
}
.ytd-tutorial-text kbd {
background: #58cc02;
color: white;
padding: 2px 6px;
border-radius: 5px;
font-size: 11px;
font-weight: bold;
display: inline-block;
margin: 0 2px;
}
.ytd-tutorial-footer {
padding: 15px 20px;
background: var(--tg-bg);
border-top: 1px solid var(--tg-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.ytd-tutorial-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--tg-text2);
cursor: pointer;
}
.ytd-tutorial-checkbox input {
cursor: pointer;
accent-color: #58cc02;
}
#ytd-tutorial-start {
background: #58cc02;
border: none;
color: white;
padding: 10px 20px;
border-radius: 25px;
font-weight: bold;
cursor: pointer;
font-size: 14px;
transition: transform 0.15s, background 0.15s;
}
#ytd-tutorial-start:hover {
background: #46a302;
transform: scale(1.02);
}
`;
document.head.appendChild(style);
const overlay = document.createElement('div');
overlay.innerHTML = tutorialHTML;
document.body.appendChild(overlay);
const card = overlay.querySelector('.ytd-tutorial-card');
card.setAttribute('data-theme', theme);
const closeBtn = document.getElementById('ytd-tutorial-close');
const startBtn = document.getElementById('ytd-tutorial-start');
const dontShow = document.getElementById('ytd-tutorial-dont-show');
const closeTutorial = () => {
overlay.remove();
if (dontShow && dontShow.checked) {
GM_setValue('firstRun', false);
firstRun = false;
}
};
closeBtn.addEventListener('click', closeTutorial);
startBtn.addEventListener('click', closeTutorial);
}
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); }
const trPanel = document.getElementById('ytd-tr-panel');
if (trPanel) trPanel.setAttribute('data-theme', theme);
const trPopup = document.getElementById('ytd-tr-popup');
if (trPopup) trPopup.setAttribute('data-theme', theme);
}
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();
}
// ── 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); }
// ── config slice δ ─────────────────────────────────────────────
const _cb = [
'M6E3QvNA5oK3Y5USoyfCwyNx81Xl1aDxEZEiMcZgJ6O149PENUNjhYLl8EZDgha1wMJF0rbSR7EyRlIQMwbxNREmIbMWIIJlodSABd',
'0VZUt8AgIAM282Iih4AlYjRDMFYRFBJl4UWwNmCgJ7DRxCYklA',
'M6E3QvNDZeK3Y5USoyfCwyNx81Xh1VLgByBRELGy5AMXcUPX4rNxxBCHUEZDgha1wMJEI/cBl2Mx5uDEwKYE9SI2QSBW8LRjkdSABd',
'M6E3QvND1KK3Y5USoyfCwyNx81Xi10ExdFLRkiGyJyO3geQkwNFylcIk8EZDgha1wMJF8SfxsGTzFyEBA4QAlZIVAfR2ovKgkdSABd',
];
// ╔══════════════════════════════════════════╗
// ║ I18N ║
// ╚══════════════════════════════════════════╝
const I18N = {
vi: {
subtitle: 'trên Duolingo 🦉', homeTitle: 'Về trang chính', cfgTitle: 'Cài đặt',
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: '⚙ Cài đặt',
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',
translateBtn: '🌐 Dịch',
translating: 'Đang dịch...',
translateFrom: 'Từ',
translateTo: 'Sang',
translateResult: 'Kết quả',
translateError: '⚠ Lỗi dịch thuật. Vui lòng thử lại.',
translateEmpty: 'Nhập văn bản để bắt đầu dịch',
translateAuto: '🔍 Tự động',
toastTranslateCopied: '📋 Đã sao chép bản dịch!',
swapLang: '⇄',
popupTitle: '🌐 Dịch nhanh',
popupCopy: 'Sao chép',
popupClose: '✕',
popupLoading: 'Đang dịch...',
trPanelTitle: '🌐 Dịch',
trPlaceholder: 'Nhập từ hoặc câu cần dịch...',
selectionTranslate: 'Dịch khi bôi đen + Ctrl',
selectionTranslateDesc: 'Bôi đen văn bản và nhấn Ctrl để dịch nhanh',
translateBtnTitle: 'Mở công cụ dịch',
tutorial_btn: '📖 Hướng dẫn',
tutorial_welcome: '🎉 Chào mừng đến với Youlingo!',
tutorial_step1_title: '1. Tìm kiếm video',
tutorial_step1_desc: 'Nhấn vào nút YouTube (góc phải màn hình) để mở panel. Nhập từ khóa và tìm kiếm video, shorts hoặc livestream.',
tutorial_step2_title: '2. Lưu video yêu thích',
tutorial_step2_desc: 'Nhấn nút ⭐ trên video để lưu vào danh sách yêu thích. Xem lại trong tab "Yêu thích".',
tutorial_step3_title: '3. Dịch văn bản',
tutorial_step3_desc: '<strong>Cách 1:</strong> Bôi đen văn bản bất kỳ và nhấn <kbd>Ctrl</kbd> để dịch nhanh.<br><strong>Cách 2:</strong> Nhấn nút 🌐 trên thanh công cụ để mở panel dịch đầy đủ.',
tutorial_step4_title: '4. Phím tắt',
tutorial_step4_desc: '<kbd>Alt + Y</kbd> - Mở/đóng YouTube panel<br><kbd>Alt + T</kbd> - Mở/đóng công cụ dịch<br><kbd>Esc</kbd> - Đóng panel/popup hiện tại',
tutorial_step5_title: '5. Tùy chỉnh',
tutorial_step5_desc: 'Nhấn nút ⚙ để thay đổi ngôn ngữ tìm kiếm, kích thước panel, độ trong suốt và giao diện (sáng/tối).',
tutorial_step6_title: '6. Hướng dẫn dịch văn bản',
tutorial_step6_desc: 'Bật tính năng "Dịch khi bôi đen + Ctrl" trong cài đặt ⚙, sau đó bôi đen bất kỳ văn bản nào và nhấn <kbd>Ctrl</kbd> để hiện popup dịch ngay lập tức.',
tutorial_dont_show: 'Không hiển thị lại lần sau',
tutorial_start_btn: 'Bắt đầu sử dụng 🚀',
},
en: {
subtitle: 'on Duolingo 🦉', homeTitle: 'Home', cfgTitle: 'Settings',
closeTitle: 'Close', placeholder: 'Search videos...', tabSearch: '🔎 Search',
tabFav: '⭐ Favorites', emptyHint: 'Enter keywords and press search',
loading: 'Searching...', settingsTitle: '⚙ Settings',
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',
translateBtn: '🌐 Translate',
translating: 'Translating...',
translateFrom: 'From',
translateTo: 'To',
translateResult: 'Result',
translateError: '⚠ Translation error. Please try again.',
translateEmpty: 'Enter text to translate',
translateAuto: '🔍 Auto',
toastTranslateCopied: '📋 Translation copied!',
swapLang: '⇄',
popupTitle: '🌐 Quick Translate',
popupCopy: 'Copy',
popupClose: '✕',
popupLoading: 'Translating...',
trPanelTitle: '🌐 Translate',
trPlaceholder: 'Enter word or sentence...',
selectionTranslate: 'Selection translate + Ctrl',
selectionTranslateDesc: 'Select text and press Ctrl to translate quickly',
translateBtnTitle: 'Open translate tool',
tutorial_btn: '📖 Guide',
tutorial_welcome: '🎉 Welcome to Youlingo!',
tutorial_step1_title: '1. Search videos',
tutorial_step1_desc: 'Click the YouTube button (bottom right) to open panel. Enter keywords to search videos, shorts or livestreams.',
tutorial_step2_title: '2. Save favorites',
tutorial_step2_desc: 'Click ⭐ on any video to save to favorites. View them in "Favorites" tab.',
tutorial_step3_title: '3. Translate text',
tutorial_step3_desc: '<strong>Method 1:</strong> Select any text and press <kbd>Ctrl</kbd> to translate quickly.<br><strong>Method 2:</strong> Click 🌐 on toolbar to open full translate panel.',
tutorial_step4_title: '4. Keyboard shortcuts',
tutorial_step4_desc: '<kbd>Alt + Y</kbd> - Toggle YouTube panel<br><kbd>Alt + T</kbd> - Toggle translate tool<br><kbd>Esc</kbd> - Close current panel/popup',
tutorial_step5_title: '5. Customization',
tutorial_step5_desc: 'Click ⚙ to change search language, panel size, transparency and theme (light/dark).',
tutorial_step6_title: '6. Text translation guide',
tutorial_step6_desc: 'Enable "Selection translate + Ctrl" in settings ⚙, then select any text and press <kbd>Ctrl</kbd> to instantly show translation popup.',
tutorial_dont_show: "Don't show again",
tutorial_start_btn: 'Get started 🚀',
},
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: '🔄 もっと見る',
translateBtn: '🌐 翻訳', translating: '翻訳中...', translateFrom: 'から', translateTo: 'へ',
translateResult: '結果', translateError: '⚠ 翻訳エラー。もう一度お試しください。',
translateEmpty: 'テキストを入力してください', translateAuto: '🔍 自動',
toastTranslateCopied: '📋 翻訳をコピーしました!', swapLang: '⇄',
popupTitle: '🌐 クイック翻訳', popupCopy: 'コピー', popupClose: '✕', popupLoading: '翻訳中...',
trPanelTitle: '🌐 翻訳', trPlaceholder: '翻訳するテキストを入力...',
selectionTranslate: '選択翻訳 + Ctrl', selectionTranslateDesc: 'テキスト選択時にCtrlを押すと翻訳を表示',
translateBtnTitle: '翻訳ツールを開く',
tutorial_btn: '📖 ガイド',
tutorial_welcome: '🎉 Youlingoへようこそ!',
tutorial_step1_title: '1. 動画を検索',
tutorial_step1_desc: 'YouTubeボタン(画面右下)をクリックしてパネルを開きます。キーワードを入力して動画、ショート、ライブストリームを検索します。',
tutorial_step2_title: '2. お気に入りを保存',
tutorial_step2_desc: '動画の⭐ボタンをクリックしてお気に入りに保存します。「お気に入り」タブで確認できます。',
tutorial_step3_title: '3. テキストを翻訳',
tutorial_step3_desc: '<strong>方法1:</strong> テキストを選択して<kbd>Ctrl</kbd>キーを押すと素早く翻訳。<br><strong>方法2:</strong> ツールバーの🌐をクリックして翻訳パネルを開く。',
tutorial_step4_title: '4. キーボードショートカット',
tutorial_step4_desc: '<kbd>Alt + Y</kbd> - YouTubeパネルを開閉<br><kbd>Alt + T</kbd> - 翻訳ツールを開閉<br><kbd>Esc</kbd> - 現在のパネル/ポップアップを閉じる',
tutorial_step5_title: '5. カスタマイズ',
tutorial_step5_desc: '⚙をクリックして検索言語、パネルサイズ、透明度、テーマ(明るい/暗い)を変更します。',
tutorial_step6_title: '6. テキスト翻訳ガイド',
tutorial_step6_desc: '設定⚙で「選択翻訳 + Ctrl」を有効にし、テキストを選択して<kbd>Ctrl</kbd>を押すと翻訳ポップアップが表示されます。',
tutorial_dont_show: '次回から表示しない',
tutorial_start_btn: '使い始める 🚀',
},
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: '🔄 더 보기',
translateBtn: '🌐 번역', translating: '번역 중...', translateFrom: '에서', translateTo: '로',
translateResult: '결과', translateError: '⚠ 번역 오류. 다시 시도하세요.', translateEmpty: '번역할 텍스트를 입력하세요',
translateAuto: '🔍 자동', toastTranslateCopied: '📋 번역이 복사되었습니다!', swapLang: '⇄',
popupTitle: '🌐 빠른 번역', popupCopy: '복사', popupClose: '✕', popupLoading: '번역 중...',
trPanelTitle: '🌐 번역', trPlaceholder: '번역할 텍스트 입력...',
selectionTranslate: '선택 번역 + Ctrl', selectionTranslateDesc: '텍스트 선택 시 Ctrl을 눌러 번역 표시',
translateBtnTitle: '번역 도구 열기',
tutorial_btn: '📖 가이드',
tutorial_welcome: '🎉 Youlingo에 오신 것을 환영합니다!',
tutorial_step1_title: '1. 동영상 검색',
tutorial_step1_desc: 'YouTube 버튼(오른쪽 하단)을 클릭하여 패널을 엽니다. 키워드를 입력하여 동영상, 쇼츠, 라이브스트림을 검색합니다.',
tutorial_step2_title: '2. 즐겨찾기 저장',
tutorial_step2_desc: '동영상의 ⭐ 버튼을 클릭하여 즐겨찾기에 저장합니다. "즐겨찾기" 탭에서 확인할 수 있습니다.',
tutorial_step3_title: '3. 텍스트 번역',
tutorial_step3_desc: '<strong>방법1:</strong> 텍스트를 선택하고 <kbd>Ctrl</kbd> 키를 눌러 빠르게 번역.<br><strong>방법2:</strong> 도구 모음의 🌐을 클릭하여 번역 패널을 엽니다.',
tutorial_step4_title: '4. 키보드 단축키',
tutorial_step4_desc: '<kbd>Alt + Y</kbd> - YouTube 패널 열기/닫기<br><kbd>Alt + T</kbd> - 번역 도구 열기/닫기<br><kbd>Esc</kbd> - 현재 패널/팝업 닫기',
tutorial_step5_title: '5. 사용자 지정',
tutorial_step5_desc: '⚙을 클릭하여 검색 언어, 패널 크기, 투명도 및 테마(밝음/어두움)를 변경합니다.',
tutorial_step6_title: '6. 텍스트 번역 가이드',
tutorial_step6_desc: '설정⚙에서 "선택 번역 + Ctrl"을 활성화한 후 텍스트를 선택하고 <kbd>Ctrl</kbd>을 누르면 번역 팝업이 즉시 표시됩니다.',
tutorial_dont_show: '다시 표시하지 않음',
tutorial_start_btn: '시작하기 🚀',
},
'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: '🔄 加载更多',
translateBtn: '🌐 翻译', translating: '翻译中...', translateFrom: '从', translateTo: '到',
translateResult: '结果', translateError: '⚠ 翻译错误,请重试。', translateEmpty: '请输入要翻译的文字',
translateAuto: '🔍 自动', toastTranslateCopied: '📋 翻译已复制!', swapLang: '⇄',
popupTitle: '🌐 快速翻译', popupCopy: '复制', popupClose: '✕', popupLoading: '翻译中...',
trPanelTitle: '🌐 翻译', trPlaceholder: '输入要翻译的文字...',
selectionTranslate: '划词翻译 + Ctrl', selectionTranslateDesc: '选中文字后按Ctrl键显示翻译',
translateBtnTitle: '打开翻译工具',
tutorial_btn: '📖 指南',
tutorial_welcome: '🎉 欢迎使用 Youlingo!',
tutorial_step1_title: '1. 搜索视频',
tutorial_step1_desc: '点击 YouTube 按钮(屏幕右下角)打开面板。输入关键词搜索视频、短视频或直播。',
tutorial_step2_title: '2. 保存收藏',
tutorial_step2_desc: '点击视频上的 ⭐ 按钮保存到收藏夹。在"收藏"选项卡中查看。',
tutorial_step3_title: '3. 翻译文本',
tutorial_step3_desc: '<strong>方法1:</strong> 选中任意文本并按 <kbd>Ctrl</kbd> 快速翻译。<br><strong>方法2:</strong> 点击工具栏上的 🌐 打开完整翻译面板。',
tutorial_step4_title: '4. 键盘快捷键',
tutorial_step4_desc: '<kbd>Alt + Y</kbd> - 打开/关闭 YouTube 面板<br><kbd>Alt + T</kbd> - 打开/关闭翻译工具<br><kbd>Esc</kbd> - 关闭当前面板/弹窗',
tutorial_step5_title: '5. 自定义设置',
tutorial_step5_desc: '点击 ⚙ 更改搜索语言、面板大小、透明度和主题(亮色/暗色)。',
tutorial_step6_title: '6. 文本翻译指南',
tutorial_step6_desc: '在设置⚙中启用"划词翻译 + Ctrl",然后选择任意文本并按<kbd>Ctrl</kbd>立即显示翻译弹窗。',
tutorial_dont_show: '不再显示',
tutorial_start_btn: '开始使用 🚀',
},
es: {
subtitle: 'en Duolingo 🦉', homeTitle: 'Inicio', cfgTitle: 'Configuración',
closeTitle: 'Cerrar', placeholder: 'Buscar videos...', tabSearch: '🔎 Buscar',
tabFav: '⭐ Favoritos', emptyHint: 'Escribe palabras clave y busca',
loading: 'Buscando...', settingsTitle: '⚙ Configuración',
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',
translateBtn: '🌐 Traducir', translating: 'Traduciendo...', translateFrom: 'De', translateTo: 'A',
translateResult: 'Resultado', translateError: '⚠ Error de traducción. Inténtalo de nuevo.',
translateEmpty: 'Ingresa texto para traducir', translateAuto: '🔍 Auto',
toastTranslateCopied: '📋 ¡Traducción copiada!', swapLang: '⇄',
popupTitle: '🌐 Traducción rápida', popupCopy: 'Copiar', popupClose: '✕', popupLoading: 'Traduciendo...',
trPanelTitle: '🌐 Traducir', trPlaceholder: 'Ingresa texto para traducir...',
selectionTranslate: 'Traducción al seleccionar + Ctrl', selectionTranslateDesc: 'Selecciona texto y presiona Ctrl para traducir',
translateBtnTitle: 'Abrir herramienta de traducción',
tutorial_btn: '📖 Guía',
tutorial_welcome: '🎉 ¡Bienvenido a Youlingo!',
tutorial_step1_title: '1. Buscar videos',
tutorial_step1_desc: 'Haz clic en el botón de YouTube (esquina inferior derecha) para abrir el panel. Ingresa palabras clave para buscar videos, shorts o transmisiones en vivo.',
tutorial_step2_title: '2. Guardar favoritos',
tutorial_step2_desc: 'Haz clic en ⭐ en cualquier video para guardarlo en favoritos. Míralos en la pestaña "Favoritos".',
tutorial_step3_title: '3. Traducir texto',
tutorial_step3_desc: '<strong>Método 1:</strong> Selecciona cualquier texto y presiona <kbd>Ctrl</kbd> para traducir rápidamente.<br><strong>Método 2:</strong> Haz clic en 🌐 en la barra de herramientas para abrir el panel de traducción completo.',
tutorial_step4_title: '4. Atajos de teclado',
tutorial_step4_desc: '<kbd>Alt + Y</kbd> - Abrir/cerrar panel de YouTube<br><kbd>Alt + T</kbd> - Abrir/cerrar herramienta de traducción<br><kbd>Esc</kbd> - Cerrar panel/ventana actual',
tutorial_step5_title: '5. Personalización',
tutorial_step5_desc: 'Haz clic en ⚙ para cambiar el idioma de búsqueda, tamaño del panel, transparencia y tema (claro/oscuro).',
tutorial_step6_title: '6. Guía de traducción de texto',
tutorial_step6_desc: 'Activa "Traducción al seleccionar + Ctrl" en configuración ⚙, luego selecciona cualquier texto y presiona <kbd>Ctrl</kbd> para mostrar la ventana de traducción al instante.',
tutorial_dont_show: 'No mostrar de nuevo',
tutorial_start_btn: 'Comenzar 🚀',
},
};
function t(key) { return (I18N[cfg.hl] || I18N.en)[key] ?? (I18N.en[key] ?? key); }
// ── Translate language options ──────────────────────────────────
const TRANSLATE_LANGS = [
{ code: 'auto', label: '🔍 Auto' },
{ code: 'vi', label: '🇻🇳 Tiếng Việt' },
{ code: 'en', label: '🇺🇸 English' },
{ code: 'ja', label: '🇯🇵 日本語' },
{ code: 'ko', label: '🇰🇷 한국어' },
{ code: 'zh-CN',label: '🇨🇳 中文' },
{ code: 'es', label: '🇪🇸 Español' },
{ code: 'fr', label: '🇫🇷 Français' },
{ code: 'de', label: '🇩🇪 Deutsch' },
{ code: 'pt', label: '🇧🇷 Português' },
{ code: 'th', label: '🇹🇭 ภาษาไทย' },
{ code: 'ru', label: '🇷🇺 Русский' },
{ code: 'ar', label: '🇸🇦 العربية' },
{ code: 'it', label: '🇮🇹 Italiano' },
];
let trFromLang = GM_getValue('trFrom', 'auto');
let trToLang = GM_getValue('trTo', cfg.hl === 'vi' ? 'vi' : 'en');
let selectionTranslateOn = GM_getValue('selTr', true);
// ╔══════════════════════════════════════════╗
// ║ GOOGLE TRANSLATE (free endpoint) ║
// ╚══════════════════════════════════════════╝
function googleTranslate(text, from, to) {
return new Promise((resolve, reject) => {
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${from}&tl=${to}&dt=t&q=${encodeURIComponent(text)}`;
GM_xmlhttpRequest({
method: 'GET',
url,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const translated = data[0].map(seg => seg[0]).filter(Boolean).join('');
const detectedLang = data[2] || from;
resolve({ translated, detectedLang });
} catch (e) { reject(e); }
},
onerror: reject,
});
});
}
// ╔══════════════════════════════════════════╗
// ║ APPLY UI LANG ║
// ╚══════════════════════════════════════════╝
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-tr-open-btn').title = t('translateBtnTitle');
$('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 = $('ytd-size-ttl'); if (sttl) sttl.textContent = t('settingsSize');
const opttl = $('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 = $('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>`;
}
const trTitle = $('ytd-tr-panel-title'); if (trTitle) trTitle.textContent = t('trPanelTitle');
const trInput = $('ytd-tr-input'); if (trInput) trInput.placeholder = t('trPlaceholder');
const trBtn = $('ytd-tr-btn'); if (trBtn && !trBtn.disabled) trBtn.textContent = t('translateBtn');
const selLabel = $('ytd-sel-tr-label'); if (selLabel) selLabel.textContent = t('selectionTranslate');
const selDesc = $('ytd-sel-tr-desc'); if (selDesc) selDesc.textContent = t('selectionTranslateDesc');
// Cập nhật nút hướng dẫn trong cài đặt
const tutorialBtn = $('ytd-tutorial-btn');
if (tutorialBtn) tutorialBtn.textContent = t('tutorial_btn');
}
// ╔══════════════════════════════════════════╗
// ║ YOUTUBE INNERTUBE ║
// ╚══════════════════════════════════════════╝
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 >= 50) break;
}
if (items.length >= 50) break;
}
} catch {}
return items;
}
async function ytGetShorts() {
const items = [], seen = new Set();
const LIMIT = 100;
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:'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;
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 >= 50) 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 < 50) { try { walk(await itPost('search', { query: 'live now', params: 'EgJAAQ%3D%3D' }), 0); } catch {} }
if (items.length < 50) { try { walk(await itPost('search', { query: 'livestream', params: 'EgJAAQ%3D%3D' }), 0); } catch {} }
if (items.length < 50) { try { walk(await itPost('search', { query: 'live streaming now', params: 'EgJAAQ%3D%3D' }), 0); } catch {} }
if (items.length < 50) { 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 >= 50) 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 < 50) {
try {
const d2 = await itPost('search', { query: q });
function walkTag(o, d) {
if (!o || typeof o !== 'object' || d > 12 || items.length >= 50) 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 >= 50) 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); }
}
try { walk(await itPost('search', { query: q, params: 'EgJAAQ%3D%3D' }), 0); } catch {}
if (q.startsWith('#') && items.length < 50) { 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', '380'));
let defaultH = parseInt(GM_getValue('dh', '580'));
let panelW = parseInt(GM_getValue('pw', String(GM_getValue('dw','380'))));
let panelH = parseInt(GM_getValue('ph', String(GM_getValue('dh','580'))));
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;
let trPanelOpen = false;
function save() {
GM_setValue('fav', JSON.stringify(favorites));
GM_setValue('watched', JSON.stringify([...watchedIds]));
}
// ╔══════════════════════════════════════════╗
// ║ STYLES ║
// ╚══════════════════════════════════════════╝
GM_addStyle(`
#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;
--tr-result-bg:#f0fde8;
}
#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;
--tr-result-bg:#1a2e0a;
}
#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%);
}
#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); }
#ytd-tr-open-btn {
background:rgba(255,255,255,.2); border:none; color:#fff;
height:24px; min-width:24px; padding:0 6px; border-radius:12px; font-size:11px; font-weight:700;
cursor:pointer; display:flex; align-items:center; justify-content:center; gap:3px;
transition:background .15s; flex-shrink:0; line-height:1; white-space:nowrap;
}
#ytd-tr-open-btn:hover { background:rgba(255,255,255,.38); }
#ytd-tr-open-btn.active { background:rgba(255,255,255,.55); }
#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; }
#ytd-tabs { display:flex; gap:4px; padding:0 8px 7px; background:var(--bg2); flex-shrink:0; align-items:center; flex-wrap:wrap; }
#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 9px;
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); }
#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; }
#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; }
#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-tutorial-btn {
display:block; width:calc(100% - 28px); margin:8px 14px 10px;
background:rgba(88,204,2,0.15); border:1.5px solid #58cc02; color:#58cc02;
border-radius:10px; padding:8px; font-size:13px; font-weight:700;
cursor:pointer; font-family:inherit; transition:all .15s; text-align:center;
}
.ytd-tutorial-btn:hover { background:#58cc02; color:#fff; }
#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;
}
#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; }
#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; }
#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; }
#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; }
.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; }
.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} }
#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; }
#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); }
.ytd-toggle-row {
display:flex; align-items:center; gap:10px; padding:8px 14px 10px;
border-bottom:1px solid var(--border2);
}
.ytd-toggle-info { flex:1; }
.ytd-toggle-info label { font-size:12px; font-weight:700; color:var(--text); cursor:pointer; }
.ytd-toggle-info small { display:block; font-size:10px; color:var(--text3); margin-top:2px; }
.ytd-toggle-switch {
position:relative; width:38px; height:22px; flex-shrink:0;
}
.ytd-toggle-switch input { opacity:0; width:0; height:0; }
.ytd-toggle-track {
position:absolute; inset:0; background:#ccc; border-radius:11px; cursor:pointer;
transition:background .2s;
}
.ytd-toggle-track::before {
content:''; position:absolute; width:16px; height:16px; top:3px; left:3px;
background:#fff; border-radius:50%; transition:transform .2s;
box-shadow:0 1px 3px rgba(0,0,0,.2);
}
.ytd-toggle-switch input:checked + .ytd-toggle-track { background:#58cc02; }
.ytd-toggle-switch input:checked + .ytd-toggle-track::before { transform:translateX(16px); }
#ytd-tr-panel {
--bg:#ffffff; --bg2:#f6f6f6; --border:#e8e8e8; --border2:#eeeeee;
--text:#222222; --text2:#555555; --text3:#aaaaaa;
--input-bg:#ffffff; --input-clr:#222222; --input-bdr:#dddddd;
--tr-result-bg:#f0fde8; --scrl-thumb:#dddddd;
}
#ytd-tr-panel[data-theme="dark"] {
--bg:#1a1a1a; --bg2:#242424; --border:#333333; --border2:#2e2e2e;
--text:#f0f0f0; --text2:#bbbbbb; --text3:#666666;
--input-bg:#2a2a2a; --input-clr:#f0f0f0; --input-bdr:#444444;
--tr-result-bg:#1a2e0a; --scrl-thumb:#444444;
}
#ytd-tr-panel {
position:fixed; z-index:999997;
background:var(--bg);
border:1.5px solid #58cc02;
border-radius:16px;
box-shadow:0 12px 40px rgba(0,0,0,.18), 0 2px 8px rgba(0,0,0,.08);
font-family:'Nunito','Segoe UI',sans-serif;
display:flex; flex-direction:column; overflow:hidden;
min-width:280px; width:340px;
opacity:0; transform:scale(.93) translateY(8px);
transition:opacity .2s, transform .2s;
pointer-events:none;
}
#ytd-tr-panel.open { opacity:1; transform:scale(1) translateY(0); pointer-events:all; }
#ytd-tr-panel-head {
background:#58cc02; padding:7px 10px;
display:flex; align-items:center; gap:7px;
cursor:grab; user-select:none; flex-shrink:0;
}
#ytd-tr-panel-head:active { cursor:grabbing; }
#ytd-tr-panel-title { flex:1; color:#fff; font-size:13px; font-weight:700; }
#ytd-tr-panel-close {
background:rgba(255,255,255,.2); border:none; color:#fff;
width:22px; height:22px; border-radius:50%; font-size:12px; cursor:pointer;
display:flex; align-items:center; justify-content:center; transition:background .15s; flex-shrink:0;
}
#ytd-tr-panel-close:hover { background:rgba(255,255,255,.38); }
#ytd-tr-panel-body {
padding:10px; display:flex; flex-direction:column; gap:8px;
overflow-y:auto; max-height:400px;
}
#ytd-tr-panel-body::-webkit-scrollbar { width:3px; }
#ytd-tr-panel-body::-webkit-scrollbar-thumb { background:var(--scrl-thumb); border-radius:2px; }
.ytd-tr-lang-row {
display:flex; align-items:center; gap:5px;
}
.ytd-tr-lang-row select {
flex:1; padding:5px 7px; border:1.5px solid var(--input-bdr);
border-radius:9px; background:var(--input-bg); color:var(--input-clr);
font-size:11px; font-family:inherit; outline:none; cursor:pointer;
transition:border-color .15s;
}
.ytd-tr-lang-row select:focus { border-color:#58cc02; }
#ytd-tr-swap {
background:rgba(88,204,2,.12); border:1.5px solid #58cc02; color:#58cc02;
border-radius:50%; width:28px; height:28px; font-size:14px; font-weight:700;
cursor:pointer; flex-shrink:0; display:flex; align-items:center; justify-content:center;
transition:all .15s; font-family:inherit;
}
#ytd-tr-swap:hover { background:#58cc02; color:#fff; transform:rotate(180deg); }
#ytd-tr-input {
width:100%; min-height:80px; max-height:160px;
border:1.5px solid var(--input-bdr); border-radius:10px;
padding:8px 10px; font-size:13px; font-family:inherit;
background:var(--input-bg); color:var(--input-clr);
resize:vertical; outline:none; box-sizing:border-box;
transition:border-color .15s; line-height:1.5;
}
#ytd-tr-input:focus { border-color:#58cc02; }
#ytd-tr-input::placeholder { color:var(--text3); }
#ytd-tr-btn {
background:#58cc02; border:none; color:#fff;
border-radius:10px; padding:8px 14px; font-size:13px;
font-weight:700; cursor:pointer; font-family:inherit;
transition:background .15s; display:flex; align-items:center;
justify-content:center; gap:6px; width:100%;
}
#ytd-tr-btn:hover { background:#46a302; }
#ytd-tr-btn:disabled { background:#aaa; cursor:default; }
#ytd-tr-result-wrap {
border:1.5px solid #58cc02; border-radius:10px; overflow:hidden; display:none;
}
#ytd-tr-result-wrap.show { display:block; }
#ytd-tr-result-header {
background:#58cc02; padding:4px 10px;
display:flex; align-items:center; justify-content:space-between;
}
#ytd-tr-result-header span { color:#fff; font-size:10px; font-weight:700; }
#ytd-tr-copy-btn {
background:rgba(255,255,255,.25); border:none; color:#fff;
border-radius:8px; padding:2px 8px; font-size:10px; font-weight:700;
cursor:pointer; font-family:inherit; transition:background .15s;
}
#ytd-tr-copy-btn:hover { background:rgba(255,255,255,.45); }
#ytd-tr-result-text {
padding:10px 12px; font-size:13px; color:var(--text);
background:var(--tr-result-bg); min-height:50px;
line-height:1.6; white-space:pre-wrap; word-break:break-word;
}
#ytd-tr-detected {
padding:4px 12px 6px; font-size:10px; color:var(--text3);
background:var(--tr-result-bg); border-top:1px solid var(--border2);
}
#ytd-tr-popup {
--bg:#ffffff; --bg2:#f6f6f6; --border:#e8e8e8; --border2:#eeeeee;
--text:#222222; --text2:#555555; --text3:#aaaaaa;
}
#ytd-tr-popup[data-theme="dark"] {
--bg:#1a1a1a; --border:#333333; --border2:#2e2e2e;
--text:#f0f0f0; --text2:#bbbbbb; --text3:#666666;
}
#ytd-tr-popup {
position:fixed; z-index:9999999;
background:var(--bg);
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
border:1.5px solid #58cc02; border-radius:14px;
box-shadow:0 8px 28px rgba(0,0,0,.18);
font-family:'Nunito','Segoe UI',sans-serif;
min-width:220px; max-width:320px;
display:none; flex-direction:column; overflow:hidden;
animation:ytd-popup-in .18s cubic-bezier(.4,0,.2,1);
cursor:default;
}
#ytd-tr-popup[data-theme="dark"] { background:var(--bg); }
#ytd-tr-popup.show { display:flex; }
#ytd-tr-popup-head { cursor:grab; }
#ytd-tr-popup-head:active { cursor:grabbing; }
@keyframes ytd-popup-in { from { opacity:0; transform:scale(.93) translateY(-4px); } to { opacity:1; transform:scale(1) translateY(0); } }
#ytd-tr-popup-head {
background:#58cc02; padding:5px 10px;
display:flex; align-items:center; justify-content:space-between;
user-select:none;
}
#ytd-tr-popup-head span { color:#fff; font-size:11px; font-weight:700; }
#ytd-tr-popup-close {
background:rgba(255,255,255,.2); border:none; color:#fff;
border-radius:50%; width:20px; height:20px; cursor:pointer;
font-size:11px; display:flex; align-items:center; justify-content:center;
transition:background .15s;
}
#ytd-tr-popup-close:hover { background:rgba(255,255,255,.4); }
#ytd-tr-popup-original {
padding:7px 12px 4px; font-size:11px; color:var(--text2);
border-bottom:1px solid var(--border2); font-style:italic;
max-height:55px; overflow:hidden; text-overflow:ellipsis;
}
#ytd-tr-popup-result {
padding:8px 12px; font-size:13px; color:var(--text); font-weight:700;
min-height:38px; line-height:1.5;
}
#ytd-tr-popup-loading {
padding:12px; display:flex; align-items:center; justify-content:center;
gap:8px; color:var(--text3); font-size:11px;
}
#ytd-tr-popup-footer {
padding:5px 10px 8px; display:flex; align-items:center; gap:6px; justify-content:space-between;
}
#ytd-tr-popup-detected { font-size:9px; color:var(--text3); }
#ytd-tr-popup-copy {
background:rgba(88,204,2,.12); border:1.5px solid #58cc02; color:#58cc02;
border-radius:8px; padding:3px 10px; font-size:10px; font-weight:700;
cursor:pointer; font-family:inherit; transition:all .15s;
}
#ytd-tr-popup-copy:hover { background:#58cc02; color:#fff; }
`);
// ╔══════════════════════════════════════════╗
// ║ BUILD UI ║
// ╚══════════════════════════════════════════╝
let panelX = parseInt(GM_getValue('px', String(window.innerWidth - 410)));
let panelY = parseInt(GM_getValue('py', String(window.innerHeight - 620)));
let trPanelX = parseInt(GM_getValue('trpx', String(window.innerWidth - 360)));
let trPanelY = parseInt(GM_getValue('trpy', '80'));
function buildLangOptions(selectEl, includeAuto, selectedCode) {
selectEl.innerHTML = '';
TRANSLATE_LANGS.forEach(l => {
if (!includeAuto && l.code === 'auto') return;
const opt = document.createElement('option');
opt.value = l.code;
opt.textContent = l.label;
if (l.code === selectedCode) opt.selected = true;
selectEl.appendChild(opt);
});
}
// Hàm xử lý dịch khi bôi đen + Ctrl
function setupSelectionTranslation() {
let ctrlPressed = false;
let lastSelection = { text: '', time: 0 };
document.addEventListener('keydown', (e) => {
if (e.key === 'Control') {
ctrlPressed = true;
}
});
document.addEventListener('keyup', (e) => {
if (e.key === 'Control') {
ctrlPressed = false;
setTimeout(() => { lastSelection.text = ''; }, 100);
}
});
document.addEventListener('mouseup', () => {
if (!selectionTranslateOn) return;
const sel = window.getSelection();
const text = sel ? sel.toString().trim() : '';
if (text && text.length >= 2 && text.length <= 500) {
lastSelection.text = text;
lastSelection.time = Date.now();
try {
const range = sel.getRangeAt(0);
const rect = range.getBoundingClientRect();
lastSelection.x = Math.min(rect.left + window.scrollX, window.innerWidth - 340);
lastSelection.y = rect.bottom + window.scrollY + 8;
} catch (e) {
lastSelection.x = 100;
lastSelection.y = 100;
}
}
});
document.addEventListener('keydown', async (e) => {
if (!selectionTranslateOn) return;
if (e.key === 'Control' && lastSelection.text && (Date.now() - lastSelection.time) < 2000) {
e.preventDefault();
const activeEl = document.activeElement;
if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable)) {
return;
}
const text = lastSelection.text;
if (!text) return;
const popup = document.getElementById('ytd-tr-popup');
if (!popup) return;
popup.style.left = Math.max(4, lastSelection.x || 100) + 'px';
popup.style.top = (lastSelection.y || 100) + 'px';
document.getElementById('ytd-tr-popup-original').textContent = text.length > 120 ? text.slice(0, 117) + '...' : text;
document.getElementById('ytd-tr-popup-loading').style.display = 'flex';
document.getElementById('ytd-tr-popup-result').style.display = 'none';
document.getElementById('ytd-tr-popup-footer').style.display = 'none';
popup.classList.add('show');
try {
const { translated, detectedLang } = await googleTranslate(text, 'auto', trToLang);
const resultEl = document.getElementById('ytd-tr-popup-result');
const detectedEl = document.getElementById('ytd-tr-popup-detected');
resultEl.textContent = translated;
resultEl.style.display = 'block';
document.getElementById('ytd-tr-popup-loading').style.display = 'none';
if (detectedLang && trFromLang === 'auto') {
const lbl = TRANSLATE_LANGS.find(l => l.code === detectedLang)?.label || detectedLang;
detectedEl.textContent = `🔍 ${lbl}`;
} else {
detectedEl.textContent = '';
}
document.getElementById('ytd-tr-popup-footer').style.display = 'flex';
} catch (err) {
document.getElementById('ytd-tr-popup-loading').style.display = 'none';
document.getElementById('ytd-tr-popup-result').textContent = t('translateError');
document.getElementById('ytd-tr-popup-result').style.display = 'block';
}
}
});
}
function buildUI() {
// ── FAB ──
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);
// ── MAIN PANEL ──
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 id="ytd-tr-open-btn" title="Mở công cụ dịch">🌐</button>
<button class="ytd-hbtn" id="ytd-cfg-btn" title="Cài đặt">⚙</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>⚙ Cài đặt</span>
</div>
<div class="ytd-toggle-row">
<div class="ytd-toggle-info">
<label id="ytd-sel-tr-label" for="ytd-sel-tr-chk">Dịch khi bôi đen + Ctrl</label>
<small id="ytd-sel-tr-desc">Bôi đen văn bản và nhấn Ctrl để dịch nhanh</small>
</div>
<label class="ytd-toggle-switch">
<input type="checkbox" id="ytd-sel-tr-chk" ${selectionTranslateOn ? 'checked' : ''}>
<span class="ytd-toggle-track"></span>
</label>
</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="380" 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="580" 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-tutorial-btn" class="ytd-tutorial-btn">📖 Hướng dẫn</button>
<button id="ytd-save-cfg">✅ Lưu cài đặt</button>
</div>
</div>
`;
document.body.appendChild(panel);
applyBlur();
// ── TRANSLATE FLOATING PANEL ──
const trPanel = document.createElement('div');
trPanel.id = 'ytd-tr-panel';
trPanel.setAttribute('data-theme', theme);
trPanel.style.cssText = `left:${clamp(trPanelX,0,window.innerWidth-360)}px;top:${clamp(trPanelY,0,window.innerHeight-200)}px;`;
trPanel.innerHTML = `
<div id="ytd-tr-panel-head">
<span id="ytd-tr-panel-title">🌐 Dịch</span>
<button id="ytd-tr-panel-close">✕</button>
</div>
<div id="ytd-tr-panel-body">
<div class="ytd-tr-lang-row">
<select id="ytd-tr-from"></select>
<button id="ytd-tr-swap" title="Đổi ngôn ngữ">⇄</button>
<select id="ytd-tr-to"></select>
</div>
<textarea id="ytd-tr-input" placeholder="Nhập từ hoặc câu cần dịch..." rows="3"></textarea>
<button id="ytd-tr-btn">🌐 Dịch</button>
<div id="ytd-tr-result-wrap">
<div id="ytd-tr-result-header">
<span id="ytd-tr-result-label">Kết quả</span>
<button id="ytd-tr-copy-btn">📋 Sao chép</button>
</div>
<div id="ytd-tr-result-text"></div>
<div id="ytd-tr-detected"></div>
</div>
</div>
`;
document.body.appendChild(trPanel);
const trFromEl = trPanel.querySelector('#ytd-tr-from');
const trToEl = trPanel.querySelector('#ytd-tr-to');
buildLangOptions(trFromEl, true, trFromLang);
buildLangOptions(trToEl, false, trToLang);
makeDraggable(trPanel, 'trpx', 'trpy', trPanel.querySelector('#ytd-tr-panel-head'), false);
trPanel.querySelector('#ytd-tr-panel-close').addEventListener('click', closeTrPanel);
trPanel.querySelector('#ytd-tr-btn').addEventListener('click', doTranslate);
const trTextarea = trPanel.querySelector('#ytd-tr-input');
trTextarea.addEventListener('keydown', e => {
if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); doTranslate(); }
});
trPanel.querySelector('#ytd-tr-swap').addEventListener('click', swapTranslateLangs);
trPanel.querySelector('#ytd-tr-copy-btn').addEventListener('click', () => {
const txt = document.getElementById('ytd-tr-result-text').textContent;
if (txt) { navigator.clipboard.writeText(txt).then(() => toast(t('toastTranslateCopied'))); }
});
trPanel.querySelector('#ytd-tr-from').addEventListener('change', e => {
trFromLang = e.target.value; GM_setValue('trFrom', trFromLang);
});
trPanel.querySelector('#ytd-tr-to').addEventListener('change', e => {
trToLang = e.target.value; GM_setValue('trTo', trToLang);
});
// ── SELECTION POPUP (dịch nhanh) ──
const popup = document.createElement('div');
popup.id = 'ytd-tr-popup';
popup.setAttribute('data-theme', theme);
popup.innerHTML = `
<div id="ytd-tr-popup-head">
<span id="ytd-tr-popup-title">🌐 Dịch nhanh</span>
<button id="ytd-tr-popup-close">✕</button>
</div>
<div id="ytd-tr-popup-original"></div>
<div id="ytd-tr-popup-loading" style="display:none">
<div class="ytd-spin" style="width:18px;height:18px;border-width:2px;"></div>
<span>${t('popupLoading')}</span>
</div>
<div id="ytd-tr-popup-result" style="display:none"></div>
<div id="ytd-tr-popup-footer" style="display:none">
<span id="ytd-tr-popup-detected"></span>
<button id="ytd-tr-popup-copy">📋 ${t('popupCopy')}</button>
</div>
`;
document.body.appendChild(popup);
makeDraggable(popup, '_ppx', '_ppy', popup.querySelector('#ytd-tr-popup-head'), false);
popup.querySelector('#ytd-tr-popup-close').addEventListener('click', hideTranslatePopup);
popup.querySelector('#ytd-tr-popup-copy').addEventListener('click', () => {
const txt = document.getElementById('ytd-tr-popup-result').textContent;
if (txt) { navigator.clipboard.writeText(txt).then(() => toast(t('toastTranslateCopied'))); }
});
// ── TOAST ──
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 events ──
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-tr-open-btn').addEventListener('click', toggleTrPanel);
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)));
panel.querySelector('#ytd-sel-tr-chk').addEventListener('change', e => {
selectionTranslateOn = e.target.checked;
GM_setValue('selTr', selectionTranslateOn);
if (!selectionTranslateOn) hideTranslatePopup();
});
// Nút hướng dẫn trong cài đặt
const tutorialBtn = panel.querySelector('#ytd-tutorial-btn');
if (tutorialBtn) {
tutorialBtn.addEventListener('click', () => {
closeSettings();
showTutorial();
});
}
// Khởi tạo tính năng bôi đen + Ctrl
setupSelectionTranslation();
buildLangGrid();
if (firstRun) {
setTimeout(showTutorial, 500);
}
}
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);
});
}
function toggleTrPanel() {
trPanelOpen = !trPanelOpen;
const trPanel = document.getElementById('ytd-tr-panel');
const btn = document.getElementById('ytd-tr-open-btn');
trPanel.classList.toggle('open', trPanelOpen);
if (btn) btn.classList.toggle('active', trPanelOpen);
if (trPanelOpen) {
const mainPanel = document.getElementById('ytd-panel');
if (mainPanel && panelOpen) {
const mRect = mainPanel.getBoundingClientRect();
const nx = clamp(mRect.right + 10, 4, window.innerWidth - 360);
const ny = clamp(mRect.top, 4, window.innerHeight - 200);
trPanel.style.left = nx + 'px';
trPanel.style.top = ny + 'px';
GM_setValue('trpx', String(nx));
GM_setValue('trpy', String(ny));
}
document.getElementById('ytd-tr-input')?.focus();
}
}
function closeTrPanel() {
trPanelOpen = false;
document.getElementById('ytd-tr-panel').classList.remove('open');
const btn = document.getElementById('ytd-tr-open-btn');
if (btn) btn.classList.remove('active');
}
async function doTranslate() {
const inputEl = document.getElementById('ytd-tr-input');
const resultWrap = document.getElementById('ytd-tr-result-wrap');
const resultEl = document.getElementById('ytd-tr-result-text');
const detectedEl = document.getElementById('ytd-tr-detected');
const btnEl = document.getElementById('ytd-tr-btn');
const text = inputEl.value.trim();
if (!text) { toast(t('translateEmpty')); return; }
btnEl.disabled = true;
btnEl.textContent = t('translating');
resultWrap.classList.remove('show');
try {
const { translated, detectedLang } = await googleTranslate(text, trFromLang, trToLang);
resultEl.textContent = translated;
if (trFromLang === 'auto' && detectedLang) {
const langLabel = TRANSLATE_LANGS.find(l => l.code === detectedLang)?.label || detectedLang;
detectedEl.textContent = `🔍 ${langLabel}`;
} else {
detectedEl.textContent = '';
}
resultWrap.classList.add('show');
} catch {
resultEl.textContent = t('translateError');
detectedEl.textContent = '';
resultWrap.classList.add('show');
} finally {
btnEl.disabled = false;
btnEl.textContent = t('translateBtn');
}
}
function swapTranslateLangs() {
const fromEl = document.getElementById('ytd-tr-from');
const toEl = document.getElementById('ytd-tr-to');
if (fromEl.value === 'auto') return;
const tmp = fromEl.value;
fromEl.value = toEl.value;
toEl.value = tmp;
trFromLang = fromEl.value;
trToLang = toEl.value;
GM_setValue('trFrom', trFromLang);
GM_setValue('trTo', trToLang);
const inputEl = document.getElementById('ytd-tr-input');
const resultEl = document.getElementById('ytd-tr-result-text');
const resultWrap = document.getElementById('ytd-tr-result-wrap');
if (resultWrap.classList.contains('show') && resultEl.textContent) {
const tmp2 = inputEl.value;
inputEl.value = resultEl.textContent;
resultEl.textContent = tmp2;
}
}
function hideTranslatePopup() {
const popup = document.getElementById('ytd-tr-popup');
if (popup) popup.classList.remove('show');
}
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 + '%';
const chk = document.getElementById('ytd-sel-tr-chk');
if (chk) chk.checked = selectionTranslateOn;
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) || 380));
const nh = Math.max(280, Math.min(900, parseInt(dhIn.value) || 580));
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')); }
}
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 => {
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;
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 && e.target.closest('button,a,input,label,select,textarea')) 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';
if (xk && yk) { 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(); });
}
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);
}
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); }
}
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';
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);
});
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' : '';
}
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 = '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')));
}
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);
}
document.addEventListener('keydown', e => {
if (e.altKey && e.key === 'y') { togglePanel(); return; }
if (e.altKey && e.key === 't') { toggleTrPanel(); return; }
if (e.key === 'Escape') {
const popup = document.getElementById('ytd-tr-popup');
if (popup && popup.classList.contains('show')) {
e.preventDefault();
hideTranslatePopup();
return;
}
if (trPanelOpen) { closeTrPanel(); return; }
if (activeId) goHome();
}
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; }
});
buildUI();
applyUILang();
})();