(() => { let toastHost, toastRoot, tabId = null; const dismissed = new Set(); const toastTimers = new Map(); const knownJobs = new Map(); const stages = { preparing: "Подготовка", downloading: "Скачивание", merging: "Объединение дорожек", remuxing: "Подготовка файла", finalizing: "Финализация файла", encoding: "Перекодирование", audio: "Обработка аудио", saving: "Сохранение файла", installing: "Установка", updating: "Обновление компонентов", complete: "Файл сохранён", error: "Ошибка", cancelled: "Отменено", cancelling: "Останавливаю обработку" }; const isDark = () => document.documentElement.hasAttribute("dark") || document.documentElement.dataset.theme === "dark"; function updateText(node, value) { if (node.textContent === value) return; if (node.childNodes.length === 1 && node.firstChild.nodeType === 3) node.firstChild.data = value; else node.textContent = value; } function downloadSizeText(job) { const megabytes = value => `${(value / 1048576).toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} МБ`; if (job.status === "complete" && Number.isFinite(job.result?.size) && job.result.size >= 0) return `Размер файла: ${megabytes(job.result.size)}`; const value = job.stage === "saving" ? job.copied : job.downloaded; if (!["downloading", "saving"].includes(job.stage) || !Number.isFinite(value) || value < 0) return ""; const downloaded = `${job.stage === "saving" ? "Сохранено" : "Скачано"} ${megabytes(value)}`; return Number.isFinite(job.total) && job.total > 0 ? `${downloaded} из ${megabytes(job.total)}` : downloaded; } function processingText(job) { if (!["merging", "remuxing", "finalizing", "encoding", "audio"].includes(job.stage)) return ""; const duration = value => { const seconds = Math.floor(value); return seconds >= 3600 ? `${Math.floor(seconds / 3600)}:${String(Math.floor(seconds / 60) % 60).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}` : `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`; }; const parts = []; if (Number.isFinite(job.processedSeconds) && job.processedSeconds >= 0) parts.push(`Обработано ${duration(job.processedSeconds)}${job.durationSeconds > 0 ? ` из ${duration(job.durationSeconds)}` : ""}`); if (Number.isFinite(job.written) && job.written >= 0) parts.push(`Записано ${(job.written / 1048576).toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} МБ`); return parts.join(" · "); } const commonStyle = ` :host{font-family:Roboto,"Segoe UI",sans-serif;color-scheme:light;--bg:#fff;--fg:#152334;--muted:#6a7787;--line:#e1e7ee;--track:#edf2f7;--accent:#0788bc}:host([dark]){color-scheme:dark;--bg:#1b2029;--fg:#eef2fa;--muted:#a1adbd;--line:#333e4e;--track:#2b3543;--accent:#59d0f4}*{box-sizing:border-box}button{font:inherit;cursor:pointer}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important}}`; function applyTheme() { toastHost?.toggleAttribute("dark", isDark()); } function ensureToasts() { if (toastHost?.isConnected) return; toastHost = document.createElement("div"); toastHost.id = "ytdl-notifications"; toastHost.style.cssText = "position:fixed;right:20px;bottom:20px;width:min(365px,calc(100vw - 32px));z-index:2147483647;pointer-events:none"; toastRoot = toastHost.attachShadow({ mode: "open" }); toastRoot.innerHTML = ``; document.documentElement.append(toastHost); applyTheme(); } function dismiss(id) { dismissed.add(id); clearTimeout(toastTimers.get(id)); const toast = toastRoot?.querySelector(`[data-id="${CSS.escape(id)}"]`); toast?.classList.add("exit"); setTimeout(() => toast?.remove(), 180); } function showJob(job) { knownJobs.set(job.id, job); if (dismissed.has(job.id)) return; ensureToasts(); let toast = toastRoot.querySelector(`[data-id="${CSS.escape(job.id)}"]`); if (!toast) { toast = document.createElement("article"); toast.className = "toast"; toast.dataset.id = job.id; toast.setAttribute("role", "status"); toast.setAttribute("aria-live", "polite"); toast.innerHTML = '
↓
'; toast.querySelector(".dismiss").addEventListener("click", () => dismiss(job.id)); toast.querySelector(".action").addEventListener("click", () => { const current = knownJobs.get(job.id); chrome.runtime.sendMessage({ type: current.status === "complete" ? "REVEAL_JOB" : "CANCEL_JOB", jobId: job.id }).catch(() => {}); }); toastRoot.append(toast); } const className = `toast ${job.stage}`; if (toast.className !== className) toast.className = className; if (toast.dataset.status !== job.status) toast.dataset.status = job.status; updateText(toast.querySelector(".title"), job.title || "YouTubeDL"); const suffix = job.stage === "downloading" ? ` · ${job.track === "audio" ? "аудио" : "видео"}` : ["encoding", "audio"].includes(job.stage) && job.engine ? ` · ${{ nvenc: "NVIDIA NVENC", amf: "AMD AMF", cpu: "CPU", copy: "без перекодирования" }[job.engine] || job.engine}` : ["merging", "remuxing"].includes(job.stage) && !job.message ? " · без перекодирования" : ""; updateText(toast.querySelector(".stage"), (job.stage === "complete" && job.kind === "update" ? job.result?.title || "Обновление завершено" : ["updating", "merging", "remuxing", "finalizing", "encoding", "audio"].includes(job.stage) && job.message ? job.message : job.stage === "complete" && job.kind === "dependency" ? "Установка завершена" : stages[job.stage] || "Обработка") + suffix); updateText(toast.querySelector(".icon"), { complete: "✓", error: "!", cancelled: "×", encoding: "✦", merging: "⇄", remuxing: "⇄", audio: "♫" }[job.stage] || "↓"); const fill = toast.querySelector(".fill"); const width = `${Number.isFinite(job.percent) ? Math.max(0, Math.min(100, job.percent)) : 0}%`; if (fill.style.width !== width) fill.style.width = width; fill.classList.toggle("indeterminate", job.status === "running" && !Number.isFinite(job.percent)); toast.querySelector(".track").hidden = ["error", "cancelled"].includes(job.status); const transfer = toast.querySelector(".transfer"); updateText(transfer, downloadSizeText(job) || processingText(job)); transfer.hidden = !transfer.textContent; let label = Number.isFinite(job.percent) ? `${Math.round(job.percent)}%` : ""; if (job.stage === "downloading" && Number.isFinite(job.speed) && job.speed >= 0) label += `${label ? " · " : ""}${(job.speed / 1048576).toFixed(1)} МБ/с`; if (["merging", "remuxing", "encoding", "audio"].includes(job.stage) && job.speed) label += `${label ? " · " : ""}${job.speed}`; if (["merging", "remuxing", "encoding", "audio"].includes(job.stage) && Number.isFinite(job.eta) && job.eta > 0) label += `${label ? " · " : ""}осталось ≈ ${Math.ceil(job.eta)} с`; if (job.status === "complete") label = job.result?.filename || job.result?.title || "Готово"; if (job.status === "error") label = job.error || "Ошибка"; const readout = toast.querySelector(".label"); updateText(readout, label); const tooltip = job.error || job.result?.path || label; if (readout.title !== tooltip) readout.title = tooltip; const action = toast.querySelector(".action"); updateText(action, job.status === "complete" ? "Открыть папку ↗" : "Отменить"); action.hidden = !["running", "complete"].includes(job.status) || job.canCancel === false || (["dependency", "update"].includes(job.kind) && job.status === "complete"); if (job.status !== "running" && !toastTimers.has(job.id)) toastTimers.set(job.id, setTimeout(() => dismiss(job.id), job.status === "error" ? 60000 : 20000)); } chrome.runtime.onMessage.addListener(message => { if (message.type === "JOB_PROGRESS") showJob(message.job); if (message.type === "REMOVE_JOB") dismiss(message.jobId); }); new MutationObserver(applyTheme).observe(document.documentElement, { attributes: true, attributeFilter: ["dark", "data-theme"] }); chrome.runtime.sendMessage({ type: "GET_CONTEXT" }).then(context => { tabId = context?.tabId; for (const job of context?.jobs || []) if (job.status === "running") showJob(job); }).catch(() => {}); })();