最好的YouTube下载器!通过本地服务器下载视频(全高清/4K/8K)和音频(MP3)。无限制,无需等待,最高速度。功能:类型图标,自动清理。
当前为
// ==UserScript==
// @name YouTube Downloader - Local Server Interface - PRO
// @name:pt-BR YouTube Downloader - Local Server Interface - PRO
// @name:es YouTube Downloader - Local Server Interface - PRO
// @name:fr YouTube Downloader - Local Server Interface - PRO
// @name:de YouTube Downloader - Local Server Interface - PRO
// @name:it YouTube Downloader - Local Server Interface - PRO
// @name:ru YouTube Downloader - Local Server Interface - PRO
// @name:zh-CN YouTube Downloader - Local Server Interface - PRO
// @name:ja YouTube Downloader - Local Server Interface - PRO
// @name:ko YouTube Downloader - Local Server Interface - PRO
// @name:hi YouTube Downloader - Local Server Interface - PRO
// @name:id YouTube Downloader - Local Server Interface - PRO
// @namespace http://tampermonkey.net/
// @version 3.11.1
// @description The Best YouTube Downloader! Download Video (Full HD/4K/8K) & Audio (MP3) via Local Server. No limits, no waiting, max speed. Features: Type icons (Video/Audio), auto-clean, fixed UI.
// @description:pt-BR A melhor ferramenta para baixar YouTube! Baixe Vídeos (Full HD/4K/8K) e Áudio (MP3) via Servidor Local. Sem limites, sem espera, velocidade máxima. Recursos: Ícones de tipo, limpeza automática, UI fixa.
// @description:es ¡El mejor descargador de YouTube! Descarga video (Full HD/4K/8K) y audio (MP3) a través del servidor local. Sin límites, sin esperas, máxima velocidad. Características: Iconos de tipo, limpieza automática.
// @description:zh-CN 最好的YouTube下载器!通过本地服务器下载视频(全高清/4K/8K)和音频(MP3)。无限制,无需等待,最高速度。功能:类型图标,自动清理。
// @description:ru Лучший загрузчик YouTube! Скачивайте видео (Full HD/4K/8K) и аудио (MP3) через локальный сервер. Без ограничений, без ожиданий, максимальная скорость.
// @description:fr Le meilleur téléchargeur YouTube ! Téléchargez Vidéo (Full HD/4K/8K) et Audio (MP3) via serveur local. Sans limites, sans attente, vitesse maximale.
// @description:de Der beste YouTube-Downloader! Video (Full HD/4K/8K) & Audio (MP3) über lokalen Server herunterladen. Keine Limits, keine Wartezeit, maximale Geschwindigkeit.
// @description:ja 最高のYouTubeダウンローダー!ローカルサーバー経由でビデオ(フルHD / 4K / 8K)とオーディオ(MP3)をダウンロードします。制限なし、待機なし、最高速度。
// @description:it Il miglior downloader di YouTube! Scarica video (Full HD/4K/8K) e audio (MP3) tramite server locale. Nessun limite, nessuna attesa, massima velocità.
// @description:hi सर्वश्रेष्ठ यूट्यूब डाउनलोडर! स्थानीय सर्वर के माध्यम से वीडियो (पूर्ण एचडी/4K/8K) और ऑडियो (MP3) डाउनलोड करें। कोई सीमा नहीं, कोई प्रतीक्षा नहीं, अधिकतम गति।
// @description:id Pengunduh YouTube Terbaik! Unduh Video (Full HD/4K/8K) & Audio (MP3) melalui Server Lokal. Tanpa batas, tanpa menunggu, kecepatan maksimal.
// @description:ko 최고의 YouTube 다운로더! 로컬 서버를 통해 비디오(Full HD/4K/8K) 및 오디오(MP3)를 다운로드하십시오. 제한 없음, 대기 없음, 최대 속도.
// @description:ar أفضل تنزيل يوتيوب! قم بتنزيل الفيديو (Full HD/4K/8K) والصوت (MP3) عبر الخادم المحلي. لا حدود ، لا انتظار ، أقصى سرعة.
// @copyright 2025, Tauã B. Kloch Leite - All Rights Reserved.
// @author Tauã B. Kloch Leite
// @icon https://img.icons8.com/?size=100&id=9F8aDI7mYs6V&format=png&color=000000
// @match https://www.youtube.com/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_openInTab
// @grant GM_setClipboard
// ==/UserScript==
(function () {
'use strict';
// --- POLÍTICA DE SEGURANÇA (TrustedTypes) ---
let policy = null;
if (window.trustedTypes && window.trustedTypes.createPolicy) {
try { policy = window.trustedTypes.createPolicy('yt-dl-policy', { createHTML: (s) => s }); } catch (e) {}
}
const safeHTML = (html) => policy ? policy.createHTML(html) : html;
const SERVER_URL = "http://127.0.0.1:5000";
const DRIVE_LINK = "https://drive.google.com/file/d/13TOlyMFAzz7eEOTqAaf6Ex7eBR6rx1ex/view?usp=drive_link";
const POLLING_INTERVAL = 1500;
// --- ÍCONES ---
const ICONS = {
pix: "https://upload.wikimedia.org/wikipedia/commons/a/a2/Logo%E2%80%94pix_powered_by_Banco_Central_%28Brazil%2C_2020%29.svg",
paypal: "https://www.paypalobjects.com/webstatic/icon/pp258.png",
btc: "https://cryptologos.cc/logos/bitcoin-btc-logo.svg?v=025",
eth: "https://cryptologos.cc/logos/ethereum-eth-logo.svg?v=025",
sol: "https://cryptologos.cc/logos/solana-sol-logo.svg?v=025",
bnb: "https://cryptologos.cc/logos/bnb-bnb-logo.svg?v=025",
matic: "https://cryptologos.cc/logos/polygon-matic-logo.svg?v=025",
usdt: "https://cryptologos.cc/logos/tether-usdt-logo.svg?v=025",
bubble: "https://img.icons8.com/?size=100&id=9F8aDI7mYs6V&format=png&color=ffffff",
// Ícone de aviso estável (Wikimedia)
warn: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Antu_dialog-warning.svg/200px-Antu_dialog-warning.svg.png"
};
// --- TRADUÇÕES ---
const STRINGS = {
en: { title: "Local Downloader PRO", tab_dl: "Downloads", tab_sup: "Donate", vid: "🎬 Video", aud: "🎵 Audio", queue: "Queue", done: "Done", err: "Error", refresh: "🔄 Refresh", clear: "🗑️ Clear", conn_err: "Server Offline?", open: "Open", folder: "Folder", sup_title: "SUPPORT THE CODE", sup_desc: "Help keep updates coming!", lbl_pix: "PIX KEY (BR)", btn_copy: "COPY", auto_dl: "⬇️ Saved: ", wallet_title: "CRYPTO WALLETS", login_err: "⚠️ LOGIN NEEDED (Click to fix)", retry: "Retry", cancel: "Cancel", open_panel: "🚀 Open Server Panel", toggle: "👁️ Show/Hide UI", help_btn: "❓ Help / Install", help_title: "SETUP REQUIRED", help_s1: "1. Download YT_Downloader.exe", help_s2: "2. Open the App", help_s3: "3. Click 'Start Server'", help_btn_dl: "DOWNLOAD SERVER", help_warn: "Script requires this app!", back: "Back to Panel" },
pt: { title: "Downloader Local PRO", tab_dl: "Downloads", tab_sup: "Doação", vid: "🎬 Vídeo", aud: "🎵 Áudio", queue: "Fila", done: "Prontos", err: "Erros", refresh: "🔄 Atualizar", clear: "🗑️ Limpar", conn_err: "Servidor Offline?", open: "Abrir", folder: "Pasta", sup_title: "APOIE O PROJETO", sup_desc: "Mantenha as atualizações vivas!", lbl_pix: "CHAVE PIX", btn_copy: "COPIAR", auto_dl: "⬇️ Salvo: ", wallet_title: "CARTEIRAS CRIPTO", login_err: "⚠️ LOGIN NECESSÁRIO (Clique para arrumar)", retry: "🔄 Reiniciar", cancel: "❌ Cancelar", open_panel: "🚀 Abrir Painel Server", toggle: "👁️ Mostrar/Ocultar UI", help_btn: "❓ Ajuda / Instalação", help_title: "INSTALAÇÃO NECESSÁRIA", help_s1: "1. Baixe o YT_Downloader.exe", help_s2: "2. Abra o Aplicativo", help_s3: "3. Clique em 'Start Server'", help_btn_dl: "BAIXAR SERVIDOR", help_warn: "O script precisa disso!", back: "Voltar para o Painel" },
es: { title: "Descargador Local PRO", tab_dl: "Descargas", tab_sup: "Donación", vid: "🎬 Video", aud: "🎵 Audio", queue: "Cola", done: "Hecho", err: "Error", refresh: "🔄 Actualizar", clear: "🗑️ Limpiar", conn_err: "¿Servidor Offline?", open: "Abrir", folder: "Carpeta", sup_title: "APOYA EL CÓDIGO", sup_desc: "¡Ayuda a mantener las actualizaciones!", lbl_pix: "CLAVE PIX", btn_copy: "COPIAR", auto_dl: "⬇️ Guardado: ", wallet_title: "BILLETERAS CRIPTO", login_err: "⚠️ LOGIN REQUERIDO", retry: "Reintentar", cancel: "Cancelar", open_panel: "🚀 Abrir Panel", toggle: "👁️ Mostrar/Ocultar", help_btn: "❓ Ayuda / Instalación", help_title: "INSTALACIÓN REQUERIDA", help_s1: "1. Descarga YT_Downloader.exe", help_s2: "2. Abre la Aplicación", help_s3: "3. Haz clic en 'Start Server'", help_btn_dl: "DESCARGAR SERVIDOR", help_warn: "¡Se requiere la app!", back: "Volver al Panel" },
ru: { title: "Локальный Загрузчик", tab_dl: "Скачать", tab_sup: "Донат", vid: "🎬 Видео", aud: "🎵 Аудио", queue: "Очередь", done: "Готово", err: "Ошибка", refresh: "🔄 Обновить", clear: "🗑️ Очистить", conn_err: "Сервер офлайн?", open: "Открыть", folder: "Папка", sup_title: "ПОДДЕРЖАТЬ КОД", sup_desc: "Помогите выпускать обновления!", lbl_pix: "PIX KEY", btn_copy: "КОПИРОВАТЬ", auto_dl: "⬇️ Сохранено: ", wallet_title: "КРИПТО КОШЕЛЬКИ", login_err: "⚠️ НУЖЕН ВХОД", retry: "Повтор", cancel: "Отмена", open_panel: "🚀 Открыть панель", toggle: "👁️ Скрыть/Показать", help_btn: "❓ Помощь / Установка", help_title: "ТРЕБУЕТСЯ УСТАНОВКА", help_s1: "1. Скачать YT_Downloader.exe", help_s2: "2. Открыть приложение", help_s3: "3. Нажать 'Start Server'", help_btn_dl: "СКАЧАТЬ СЕРВЕР", help_warn: "Скрипт требует это!", back: "Назад в панель" },
fr: { title: "Téléchargeur Local", tab_dl: "Téléch.", tab_sup: "Don", vid: "🎬 Vidéo", aud: "🎵 Audio", queue: "File", done: "Fait", err: "Erreur", refresh: "🔄 Rafraîchir", clear: "🗑️ Vider", conn_err: "Serveur Hors ligne?", open: "Ouvrir", folder: "Dossier", sup_title: "SOUTENIR LE CODE", sup_desc: "Aidez à garder les mises à jour!", lbl_pix: "CLÉ PIX", btn_copy: "COPIER", auto_dl: "⬇️ Enregistré: ", wallet_title: "PORTEFEUILLES CRYPTO", login_err: "⚠️ CONNEXION REQUISE", retry: "Réessayer", cancel: "Annuler", open_panel: "🚀 Ouvrir Panneau", toggle: "👁️ Afficher/Masquer", help_btn: "❓ Aide / Installation", help_title: "INSTALLATION REQUISE", help_s1: "1. Télécharger YT_Downloader.exe", help_s2: "2. Ouvrir l'application", help_s3: "3. Cliquez sur 'Start Server'", help_btn_dl: "TÉLÉCHARGER SERVEUR", help_warn: "Le script a besoin de ça!", back: "Retour au panneau" },
de: { title: "Lokaler Downloader", tab_dl: "Downloads", tab_sup: "Spenden", vid: "🎬 Video", aud: "🎵 Audio", queue: "Warteschlange", done: "Fertig", err: "Fehler", refresh: "🔄 Aktualisieren", clear: "🗑️ Löschen", conn_err: "Server Offline?", open: "Öffnen", folder: "Ordner", sup_title: "CODE UNTERSTÜTZEN", sup_desc: "Helfen Sie mit Updates!", lbl_pix: "PIX KEY", btn_copy: "KOPIEREN", auto_dl: "⬇️ Gespeichert: ", wallet_title: "KRYPTO-WALLETS", login_err: "⚠️ LOGIN ERFORDERLICH", retry: "Wiederholen", cancel: "Abbrechen", open_panel: "🚀 Panel öffnen", toggle: "👁️ Ein/Ausblenden", help_btn: "❓ Hilfe / Installation", help_title: "INSTALLATION ERFORDERLICH", help_s1: "1. YT_Downloader.exe herunterladen", help_s2: "2. App öffnen", help_s3: "3. 'Start Server' klicken", help_btn_dl: "SERVER HERUNTERLADEN", help_warn: "Skript benötigt dies!", back: "Zurück zum Panel" },
it: { title: "Downloader Locale", tab_dl: "Scarica", tab_sup: "Donazione", vid: "🎬 Video", aud: "🎵 Audio", queue: "Coda", done: "Fatto", err: "Errore", refresh: "🔄 Aggiorna", clear: "🗑️ Pulisci", conn_err: "Server Offline?", open: "Apri", folder: "Cartella", sup_title: "SUPPORTA IL CODICE", sup_desc: "Aiuta gli aggiornamenti!", lbl_pix: "CHIAVE PIX", btn_copy: "COPIA", auto_dl: "⬇️ Salvato: ", wallet_title: "PORTAFOGLI CRYPTO", login_err: "⚠️ LOGIN RICHIESTO", retry: "Riprova", cancel: "Annulla", open_panel: "🚀 Apri Pannello", toggle: "👁️ Mostra/Nascondi", help_btn: "❓ Aiuto / Installazione", help_title: "INSTALLAZIONE RICHIESTA", help_s1: "1. Scarica YT_Downloader.exe", help_s2: "2. Apri l'App", help_s3: "3. Clicca 'Start Server'", help_btn_dl: "SCARICA SERVER", help_warn: "Lo script richiede questo!", back: "Torna al Pannello" },
zh: { title: "本地下载器 PRO", tab_dl: "下载", tab_sup: "捐赠", vid: "🎬 视频", aud: "🎵 音频", queue: "队列", done: "完成", err: "错误", refresh: "🔄 刷新", clear: "🗑️ 清除", conn_err: "服务器离线?", open: "打开", folder: "文件夹", sup_title: "支持代码", sup_desc: "帮助保持更新!", lbl_pix: "PIX 密钥", btn_copy: "复制", auto_dl: "⬇️ 已保存: ", wallet_title: "加密钱包", login_err: "⚠️ 需要登录", retry: "重试", cancel: "取消", open_panel: "🚀 打开面板", toggle: "👁️ 显示/隐藏", help_btn: "❓ 帮助 / 安装", help_title: "需要安装", help_s1: "1. 下载 YT_Downloader.exe", help_s2: "2. 打开应用程序", help_s3: "3. 点击 'Start Server'", help_btn_dl: "下载服务器", help_warn: "脚本需要此应用!", back: "返回面板" },
ja: { title: "ローカルダウンローダー", tab_dl: "ダウンロード", tab_sup: "寄付", vid: "🎬 ビデオ", aud: "🎵 オーディオ", queue: "キュー", done: "完了", err: "エラー", refresh: "🔄 更新", clear: "🗑️ クリア", conn_err: "サーバーオフライン?", open: "開く", folder: "フォルダ", sup_title: "コードをサポート", sup_desc: "更新を続けるのを手伝ってください!", lbl_pix: "PIXキー", btn_copy: "コピー", auto_dl: "⬇️ 保存済み: ", wallet_title: "暗号ウォレット", login_err: "⚠️ ログインが必要", retry: "再試行", cancel: "キャンセル", open_panel: "🚀 パネルを開く", toggle: "👁️ 表示/非表示", help_btn: "❓ ヘルプ / インストール", help_title: "インストールが必要", help_s1: "1. YT_Downloader.exe をダウンロード", help_s2: "2. アプリを開く", help_s3: "3. 'Start Server' をクリック", help_btn_dl: "サーバーをダウンロード", help_warn: "スクリプトにはこれが必要です!", back: "パネルに戻る" }
};
const getLang = () => {
const l = navigator.language || "en";
if (l.startsWith("pt")) return STRINGS.pt;
if (l.startsWith("es")) return STRINGS.es;
if (l.startsWith("ru")) return STRINGS.ru;
if (l.startsWith("fr")) return STRINGS.fr;
if (l.startsWith("de")) return STRINGS.de;
if (l.startsWith("it")) return STRINGS.it;
if (l.startsWith("zh")) return STRINGS.zh;
if (l.startsWith("ja")) return STRINGS.ja;
return STRINGS.en;
};
const T = getLang();
// ESTADO DA UI
const state = { uiMode: GM_getValue("yt_dl_uiMode", 1), stats: {}, items: [], activeTab: 'dl' };
const setUIMode = (m) => { state.uiMode = m; GM_setValue("yt_dl_uiMode", m); renderUI(); };
const getHistory = () => GM_getValue('yt_dl_history_local', []);
const addToHistory = (f) => { let h=getHistory(); if(!h.includes(f)){ h.push(f); if(h.length>50)h.shift(); GM_setValue('yt_dl_history_local', h); }};
// --- API ---
const clearList = async () => { try { await fetch(`${SERVER_URL}/clear`, { method: 'POST' }); } catch(e){} GM_setValue('yt_dl_history_local', []); state.items = []; state.stats = { total:0, in_progress:0, finished:0, errors:0 }; updateListContent(); };
const openLocalFile = async (filename) => { try { await fetch(`${SERVER_URL}/open_file`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({filename: filename}) }); } catch(e) { toast(T.conn_err, false); } };
const openFolder = async (type) => { try { await fetch(`${SERVER_URL}/open_folder`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({type: type}) }); } catch(e) { toast(T.conn_err, false); } };
const copyToClipboard = (text) => { GM_setClipboard(text); toast(T.btn_copy + " OK!"); };
const cancelDownload = async (id) => { try { await fetch(`${SERVER_URL}/cancel`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}) }); toast(T.cancel + " OK"); refreshData(); } catch(e) {} };
const refreshData = async () => {
try {
const [sRes, fRes] = await Promise.all([ fetch(`${SERVER_URL}/stats`), fetch(`${SERVER_URL}/files`) ]);
state.stats = await sRes.json();
const files = await fRes.json();
state.items = files.items || [];
state.items.forEach(i => {
if(i.status === 'finished' && i.filename && !getHistory().includes(i.filename)) {
addToHistory(i.filename);
toast(T.auto_dl + i.title.substring(0,20)+"...");
}
});
if(state.uiMode === 2) updateListContent(); // Agora atualiza com segurança
} catch (e) {}
};
const send = async (type, retryUrl = null, retryThumb = null) => {
try {
let finalUrl = retryUrl || location.href;
let thumbUrl = retryThumb;
if (!thumbUrl) {
try {
const urlObj = new URL(finalUrl);
const vidId = urlObj.searchParams.get("v");
if (vidId) thumbUrl = `https://i.ytimg.com/vi/${vidId}/maxresdefault.jpg`;
else { const meta = document.querySelector('meta[property="og:image"]'); if(meta) thumbUrl = meta.content; }
} catch(e) {}
}
await fetch(`${SERVER_URL}/${type === 'video' ? 'download' : 'download_audio'}`, {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ videoUrl: finalUrl, thumb: thumbUrl })
});
refreshData();
toast("OK 🚀");
if(state.uiMode === 1) setUIMode(2);
} catch(e) { toast(T.conn_err, false); }
};
// --- CSS ---
const css = `
.yt-dl-container { font-family: 'Roboto', sans-serif; z-index: 2147483647; position: fixed; bottom: 20px; left: 20px; }
.yt-dl-bubble { width: 50px; height: 50px; background: #d63384; border-radius: 50%; box-shadow: 0 4px 15px rgba(0,0,0,0.5); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform 0.2s; border: 2px solid #fff; }
.yt-dl-bubble:hover { transform: scale(1.1); }
.yt-dl-bubble img { width: 30px; height: 30px; }
.yt-dl-panel { width: 350px; background: #0f0f0f; color: #fff; border-radius: 12px; border: 1px solid #333; font-size: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.9); display: flex; flex-direction: column; overflow: hidden; animation: slideUp 0.3s ease-out; }
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
.yt-dl-head { background: #1a1a1a; padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #333; }
.yt-dl-min-btn { cursor: pointer; font-size: 18px; color: #aaa; padding: 0 5px; }
.yt-dl-min-btn:hover { color: #fff; }
.yt-dl-tabs { display: flex; background: #111; }
/* ATUALIZADO: Cor da fonte mudou de #666 para #aaa para maior visibilidade */
.yt-dl-tab { flex: 1; text-align: center; padding: 10px 0; cursor: pointer; color: #aaa; border-bottom: 2px solid transparent; font-weight: 700; text-transform: uppercase; font-size: 11px; }
.yt-dl-tab.active { color: #fff; border-bottom: 2px solid #d63384; background: #222; }
.yt-dl-body { max-height: 400px; overflow-y: auto; padding: 15px; }
.yt-dl-btn-group { display: flex; gap: 8px; margin-bottom: 15px; }
.yt-dl-btn { flex: 1; border: none; padding: 10px; border-radius: 6px; cursor: pointer; color: #fff; font-weight: 700; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 5px; transition: 0.2s; }
.yt-dl-btn:hover { filter: brightness(1.1); }
.btn-blue { background: #3ea6ff; color: #000; } .btn-purple { background: #d63384; } .btn-gray { background: #333; border: 1px solid #444; } .btn-red { background: #d32f2f; }
.yt-dl-item { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #222; }
.yt-dl-thumb { width: 50px; height: 50px; background: #000; border-radius: 6px; object-fit: cover; }
.yt-dl-info { flex: 1; overflow: hidden; }
.yt-dl-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; font-size: 12px; margin-bottom: 4px; }
.yt-dl-status { font-size: 10px; display: flex; align-items: center; gap: 6px; }
.tag-type { padding: 2px 6px; border-radius: 4px; font-weight: bold; font-size: 9px; text-transform: uppercase; }
.tag-vid { background: #0f3d5c; color: #3ea6ff; border: 1px solid #1e5985; }
.tag-aud { background: #3c1f30; color: #ff66b2; border: 1px solid #7d2a58; }
.ctrl-btn { background: #333; border: 1px solid #444; color: #ccc; cursor: pointer; font-size: 10px; border-radius: 4px; padding: 3px 8px; margin-left: 5px; }
.ctrl-btn:hover { background: #555; color: #fff; }
.btn-retry { color: #4caf50; border-color: #2e7d32; } .btn-cancel { color: #f44336; border-color: #c62828; }
.sup-row { display: flex; align-items: center; gap: 8px; background: #1a1a1a; padding: 8px; border-radius: 6px; border: 1px solid #333; margin-bottom: 8px; }
.sup-icon { width: 20px; height: 20px; object-fit: contain; }
.sup-val { flex: 1; background: none; border: none; color: #eee; font-size: 11px; font-family: monospace; outline: none; }
.sup-copy { background: #d63384; border: none; color: #fff; border-radius: 4px; cursor: pointer; font-size: 10px; padding: 4px 8px; }
.auth-fix-btn { cursor: pointer; text-decoration: underline; }
.auth-fix-btn:hover { color: #fff !important; }
.yt-dl-toast { position: fixed; top: 20px; right: 20px; background: #28a745; color: white; padding: 10px 20px; border-radius: 4px; z-index: 2147483648; font-weight: bold; animation: fadein 0.5s; }
@keyframes fadein { from { opacity:0; transform:translateY(-10px); } to { opacity:1; transform:translateY(0); } }
`;
const injectCSS = () => { if(!document.getElementById("yt-dl-style")) { const s=document.createElement("style"); s.id="yt-dl-style"; s.textContent=css; document.head.appendChild(s); }};
const toast = (msg, success=true) => { const el=document.createElement("div"); el.className="yt-dl-toast"; el.textContent=msg; if(!success) el.style.background="#f44336"; document.body.appendChild(el); setTimeout(()=>el.remove(), 3000); };
let container;
// --- HTML GENERATOR ---
const generateListHTML = () => {
if(state.items.length === 0) return `<div style="text-align:center;color:#444;padding:20px;">Empty list</div>`;
return state.items.slice().reverse().slice(0,5).map(i => {
const isAud = i.type === 'audio';
const tagClass = isAud ? 'tag-aud' : 'tag-vid';
const tagText = isAud ? 'MP3' : 'MP4';
let statusHtml = `<span style="color:${i.status==='finished'?'#4caf50':(i.status==='error'?'#f44336':'#aaa')}">${i.status}</span>`;
// CORREÇÃO DO AVISO DE LOGIN
if(i.status==='auth_error') {
statusHtml = `<span class="auth-fix-btn" style="color:#ff9800;font-weight:bold" title="Click to Fix">${T.login_err}</span>`;
}
if(i.status==='cancelled') statusHtml = `<span style="color:#f44336;font-size:10px">${T.cancel}</span>`;
let actions = '';
if(i.status === 'downloading' || i.status === 'queued') {
actions = `<button class="ctrl-btn btn-cancel" data-act="cancel" data-id="${i.id}">${T.cancel}</button>`;
} else if(i.status === 'finished') {
actions = `<button class="ctrl-btn" data-act="open" data-file="${encodeURIComponent(i.filename)}">▶️</button>
<button class="ctrl-btn" data-act="folder" data-type="${i.type}">📂</button>`;
} else if(i.status === 'error' || i.status === 'cancelled' || i.status === 'auth_error') {
actions = `<button class="ctrl-btn btn-retry" data-act="retry" data-url="${i.url}" data-type="${i.type}" data-thumb="${i.thumb}">${T.retry}</button>`;
}
return `
<div class="yt-dl-item">
<img class="yt-dl-thumb" src="${i.thumb||''}">
<div class="yt-dl-info">
<div class="yt-dl-name" title="${i.title}">${isAud?'🎵':'🎬'} ${i.title||'...'}</div>
<div class="yt-dl-status"><span class="tag-type ${tagClass}">${tagText}</span>${statusHtml}</div>
</div>
<div style="display:flex; flex-direction:column; gap:2px;">${actions}</div>
</div>`;
}).join('');
};
// --- ATUALIZA APENAS O CONTEÚDO (COM PROTEÇÃO SAFEHTML) ---
const updateListContent = () => {
if(!container || state.uiMode !== 2) return;
const listEl = document.getElementById('yt-dl-list');
const statsEl = document.getElementById('yt-dl-stats-bar');
if(!listEl) return;
// Atualiza estatísticas com segurança
if(statsEl) {
const statsHtml = `
<span>${T.queue}: <b style="color:#ffeb3b">${state.stats.in_progress||0}</b></span>
<span>${T.done}: <b style="color:#4caf50">${state.stats.finished||0}</b></span>
<span>${T.err}: <b style="color:#f44336">${state.stats.errors||0}</b></span>
`;
statsEl.innerHTML = safeHTML(statsHtml);
}
// Atualiza lista com segurança
listEl.innerHTML = safeHTML(generateListHTML());
bindListButtons();
};
const bindListButtons = () => {
if(!container) return;
container.querySelectorAll('.ctrl-btn').forEach(b => {
b.onclick = (e) => {
const d = e.target.dataset;
if(d.act === 'open') openLocalFile(decodeURIComponent(d.file));
if(d.act === 'folder') openFolder(d.type);
if(d.act === 'cancel') cancelDownload(d.id);
if(d.act === 'retry') send(d.type, d.url, d.thumb);
};
});
container.querySelectorAll('.auth-fix-btn').forEach(b => {
b.onclick = (e) => {
e.preventDefault();
GM_openInTab(`${SERVER_URL}/panel?tab=cook`, {active: true});
};
});
};
// --- RENDERIZA A ESTRUTURA (SOMENTE QUANDO MUDA O MODO) ---
const renderUI = () => {
injectCSS();
if(!container) { container=document.createElement('div'); container.className='yt-dl-container'; document.body.appendChild(container); }
if(state.uiMode === 0) { container.style.display = 'none'; return; }
container.style.display = 'block';
if(state.uiMode === 1) {
container.innerHTML = safeHTML(`<div class="yt-dl-bubble" id="yt-dl-bubble-btn" title="${T.open}"><img src="${ICONS.bubble}"></div>`);
document.getElementById('yt-dl-bubble-btn').onclick = () => setUIMode(2);
return;
}
const dlContent = `
<div class="yt-dl-btn-group">
<button class="yt-dl-btn btn-blue" id="btn-vid">${T.vid}</button>
<button class="yt-dl-btn btn-purple" id="btn-aud">${T.aud}</button>
</div>
<div id="yt-dl-stats-bar" style="font-size:10px; color:#aaa; display:flex; justify-content:space-between; margin-bottom:10px; background:#1a1a1a; padding:8px; border-radius:6px;">
<span>${T.queue}: <b style="color:#ffeb3b">...</b></span>
</div>
<div id="yt-dl-list">${generateListHTML()}</div>
<div style="margin-top:15px; display:flex; gap:10px;">
<button class="yt-dl-btn btn-gray" id="btn-refresh" style="font-size:11px; padding:6px; flex:2;">${T.refresh}</button>
<button class="yt-dl-btn btn-red" id="btn-clear" style="font-size:11px; padding:6px; flex:1;">${T.clear}</button>
</div>`;
const cryptoList = [
{img: ICONS.btc, name: "BTC", val: "bc1q6gz3dtj9qvlxyyh3grz35x8xc7hkuj07knlemn"},
{img: ICONS.eth, name: "ETH", val: "0xd8724d0b19d355e9817d2a468f49e8ce067e70a6"},
{img: ICONS.sol, name: "SOL", val: "7ztAogE7SsyBw7mwVHhUr5ZcjUXQr99JoJ6oAgP99aCn"},
{img: ICONS.usdt, name: "USDT", val: "0xd8724d0b19d355e9817d2a468f49e8ce067e70a6"},
{img: ICONS.bnb, name: "BNB", val: "0xd8724d0b19d355e9817d2a468f49e8ce067e70a6"},
{img: ICONS.matic, name: "MATIC", val: "0xd8724d0b19d355e9817d2a468f49e8ce067e70a6"}
].map(c => `<div class="sup-row"><img src="${c.img}" class="sup-icon"><span style="font-size:9px;color:#888;width:30px">${c.name}</span><input type="text" class="sup-val" readonly value="${c.val}"><button class="sup-copy" data-val="${c.val}">${T.btn_copy}</button></div>`).join('');
const supContent = `<div style="padding:15px;text-align:center"><div style="color:#d63384;font-weight:bold;margin-bottom:5px">${T.sup_title}</div><div style="color:#aaa;font-size:11px;margin-bottom:15px">${T.sup_desc}</div><div style="text-align:left;color:#d63384;font-weight:bold;font-size:10px;margin-bottom:5px">${T.lbl_pix}</div><div class="sup-row"><img src="${ICONS.pix}" class="sup-icon"><input type="text" class="sup-val" readonly value="69993230419"><button class="sup-copy" data-val="69993230419">${T.btn_copy}</button></div><div style="text-align:left;color:#d63384;font-weight:bold;font-size:10px;margin:15px 0 5px">${T.wallet_title}</div>${cryptoList}<a href="https://www.paypal.com/donate/?business=4J4UK7ACU3DS6" target="_blank" style="display:inline-flex;align-items:center;gap:8px;background:#003087;color:white;padding:8px 20px;border-radius:20px;text-decoration:none;font-weight:bold;margin-top:20px;font-size:12px"><img src="${ICONS.paypal}" style="height:20px"> PayPal</a></div>`;
// CONTEÚDO DA TELA DE AJUDA
const helpContent = `
<div style="padding:20px; text-align:center;">
<img src="${ICONS.warn}" style="width:50px;margin-bottom:10px;" onerror="this.src='https://img.icons8.com/?size=100&id=42452&format=png&color=ff9800'">
<h3 style="color:#fff;margin:0 0 10px 0;font-size:16px;">${T.help_title}</h3>
<div style="background:#1a1a1a; border-radius:8px; padding:15px; text-align:left; font-size:12px; line-height:1.6; color:#ccc; border:1px solid #333;">
<div style="margin-bottom:5px"><b>${T.help_s1}</b></div>
<div style="margin-bottom:5px"><b>${T.help_s2}</b></div>
<div style="margin-bottom:5px"><b>${T.help_s3}</b></div>
</div>
<p style="color:#f44336; font-size:11px; font-weight:bold; margin:10px 0 15px;">${T.help_warn}</p>
<button id="btn-do-download" style="background:#4caf50; color:white; border:none; padding:10px 20px; border-radius:6px; font-weight:bold; cursor:pointer; width:100%; font-size:13px;">${T.help_btn_dl}</button>
<div id="btn-back-dl" style="margin-top:15px; font-size:11px; color:#aaa; cursor:pointer; text-decoration:underline;">${T.back}</div>
</div>
`;
let activeContent = dlContent;
if (state.activeTab === 'sup') activeContent = supContent;
if (state.activeTab === 'help') activeContent = helpContent;
const panelHtml = `
<div class="yt-dl-panel">
<div class="yt-dl-head">
<span style="font-weight:700;color:#fff;font-size:13px;">${T.title}</span>
<div style="display:flex;gap:10px;align-items:center">
<span id="yt-dl-help-btn" style="cursor:pointer;font-size:12px;color:${state.activeTab==='help'?'#fff':'#4caf50'};font-weight:bold" title="${T.help_btn}">[?]</span>
<span class="yt-dl-min-btn" id="yt-dl-min" title="Minimize">▼</span>
</div>
</div>
<div class="yt-dl-tabs">
<div class="yt-dl-tab ${state.activeTab==='dl'?'active':''}" id="tab-btn-dl">${T.tab_dl}</div>
<div class="yt-dl-tab ${state.activeTab==='sup'?'active':''}" id="tab-btn-sup">${T.tab_sup}</div>
</div>
<div class="yt-dl-body">${activeContent}</div>
</div>`;
container.innerHTML = safeHTML(panelHtml);
// Eventos Principais
document.getElementById('yt-dl-min').onclick = () => setUIMode(1);
document.getElementById('yt-dl-help-btn').onclick = () => { state.activeTab='help'; renderUI(); };
document.getElementById('tab-btn-dl').onclick = () => { state.activeTab='dl'; renderUI(); };
document.getElementById('tab-btn-sup').onclick = () => { state.activeTab='sup'; renderUI(); };
if(state.activeTab === 'dl') {
document.getElementById('btn-vid').onclick = () => send('video');
document.getElementById('btn-aud').onclick = () => send('audio');
document.getElementById('btn-refresh').onclick = refreshData;
document.getElementById('btn-clear').onclick = clearList;
bindListButtons(); // Conecta os botões da lista inicial
} else if (state.activeTab === 'help') {
// Ação do botão de download na tela de ajuda
document.getElementById('btn-do-download').onclick = () => GM_openInTab(DRIVE_LINK, {active:true});
// Ação do botão voltar
document.getElementById('btn-back-dl').onclick = () => { state.activeTab='dl'; renderUI(); };
} else {
container.querySelectorAll('.sup-copy').forEach(btn => { btn.onclick = (e) => copyToClipboard(e.target.dataset.val); });
}
updateListContent();
};
const addInlineButtons = () => {
const container = document.querySelector('[id^="top-level-buttons"]');
if (!container || container.querySelector("#yt-dl-inline-vid")) return;
// ESTILO COMUM PARA FICAR GRANDE E PARECIDO COM NATIVO (Altura 36px, Padding maior)
const style = "height:36px; padding:0 16px; border-radius:18px; margin-left:8px; cursor:pointer; font-weight:500; font-size:14px; border:none; display:inline-flex; align-items:center; justify-content:center;";
// BOTÃO VIDEO (Maior, Azul)
const btnV = document.createElement("button");
btnV.id = "yt-dl-inline-vid";
btnV.textContent = T.vid;
btnV.style.cssText = style + "background:#3ea6ff; color:#0f0f0f;";
btnV.onclick = (e) => { e.preventDefault(); send('video'); };
// BOTÃO AUDIO (NOVO - Mesmo tamanho, Roxo)
const btnA = document.createElement("button");
btnA.id = "yt-dl-inline-aud";
btnA.textContent = T.aud;
btnA.style.cssText = style + "background:#d63384; color:#fff;";
btnA.onclick = (e) => { e.preventDefault(); send('audio'); };
container.appendChild(btnV);
container.appendChild(btnA);
};
const observer = new MutationObserver(addInlineButtons);
observer.observe(document.body, { childList: true, subtree: true });
setInterval(refreshData, POLLING_INTERVAL);
// ATALHOS E MENU TAMPERMONKEY
window.addEventListener("keydown", (e) => {
if (e.altKey && e.shiftKey && (e.key === "Y" || e.key === "y")) {
setUIMode(state.uiMode === 0 ? 1 : 0);
e.preventDefault();
}
});
GM_registerMenuCommand(T.toggle + " (Alt+Shift+Y)", () => setUIMode(state.uiMode === 0 ? 1 : 0));
GM_registerMenuCommand(T.open_panel, () => GM_openInTab(SERVER_URL + '/panel', {active:true}));
GM_registerMenuCommand(T.help_btn, () => { state.activeTab='help'; setUIMode(2); });
// INICIALIZAÇÃO
setTimeout(() => renderUI(), 1000);
refreshData();
})();