// First-run guidance uses the same native connection and preferences as downloads. export function createOnboarding({ call, connectHelper, getHelper, applyPreferences, onClose }) { const $ = id => document.getElementById(id); const dialog = $("setup"); let step = 0, working = false, saved = {}; let videoFormat = "mp4", audioFormat = "m4a"; function error(message = "") { $("setup-error").textContent = message; $("setup-error").hidden = !message; } function formats(remember = true) { if (remember && $("setup-format").value) { if ($("setup-format").dataset.mode === "audio") audioFormat = $("setup-format").value; else videoFormat = $("setup-format").value; } const audio = $("setup-mode").value === "audio"; const items = audio ? [["m4a", "M4A · AAC"], ["mp3", "MP3"], ["opus", "Opus"], ["flac", "FLAC"], ["wav", "WAV"]] : [["mp4", "MP4"], ["mkv", "MKV"], ["webm", "WebM"]]; $("setup-format").replaceChildren(...items.map(([value, label]) => new Option(label, value))); $("setup-format").dataset.mode = audio ? "audio" : "video"; $("setup-format").value = audio ? audioFormat : videoFormat; $("setup-quality-field").hidden = audio; $("setup-format-hint").textContent = audio ? "Сохранится только звуковая дорожка. Формат не повышает исходное качество звука." : $("setup-mode").value === "video_only" ? "Сохранится видео без звука. Если выбранного качества нет, используем ближайшее доступное." : "Отдельный звук автоматически добавится к видео. Если выбранного качества нет, используем ближайшее доступное."; } function ready() { const helper = getHelper(); return helper.status === "running" && helper.hardware?.ready === true; } function render() { const helper = getHelper(), hardware = helper.hardware; const checking = helper.status === "checking"; $("setup-helper").dataset.state = ready() ? "running" : helper.status; $("setup-helper-status").textContent = ready() ? "Помощник и компоненты готовы" : checking ? "Проверяем помощник…" : helper.status === "incompatible" ? "Нужно обновить версии" : helper.status === "running" ? "Комплект нужно восстановить" : helper.status === "missing" ? "Нужно установить помощник" : "Подключите помощник"; const dependencies = hardware?.dependencies || []; $("setup-helper-message").textContent = ready() ? `${dependencies.filter(item => item.status === "installed").length}/${dependencies.length} компонентов готовы. Дополнительные программы устанавливать не нужно.` : helper.message || "Скачайте установщик, установите комплект и проверьте подключение."; const helperVersion = hardware?.version || hardware?.bundle?.version; $("setup-versions").textContent = `Расширение ${chrome.runtime.getManifest().version} · Помощник ${helperVersion || "не проверен"}${ready() ? " · Версии совместимы" : ""}`; $("setup-versions").dataset.state = helper.status; $("setup-install").hidden = ready(); $("setup-install-steps").hidden = ready() || helper.compatibility?.update === "extension"; $("setup-install").textContent = helper.compatibility?.update === "extension" ? "Обновить расширение" : helper.compatibility?.update === "helper" ? "Обновить помощник" : helper.status === "running" && !ready() ? "Скачать для восстановления" : "Скачать в релизах"; $("setup-check").disabled = working || checking; $("setup-pick-folder").disabled = working || !ready(); $("setup-pick-work-folder").disabled = working || !ready(); $("setup-check").textContent = checking ? "Проверяем…" : "Проверить подключение"; $("setup-gpu").hidden = !ready(); if (ready()) { const engines = [hardware.nvenc?.length && "NVIDIA NVENC", hardware.amf?.length && "AMD AMF"].filter(Boolean); $("setup-gpu").textContent = `${hardware.gpu || "Видеокарта не определена"}. ${engines.length ? `Проверено ускорение: ${engines.join(", ")}.` : "Ускорение недоступно, конвертация будет выполняться на CPU."}`; } const updates = ready() && hardware.supportsComponentUpdates === true; $("setup-updates-row").hidden = $("setup-updates-note").hidden = !updates; $("setup-updates").disabled = working; if (!working) $("setup-updates").checked = hardware?.updateSettings?.enabled === true; $("setup-back").hidden = step === 0; $("setup-back").disabled = working; $("setup-next").disabled = working || ([1, 2].includes(step) && !ready()); $("setup-next").textContent = ["Начать настройку", "Продолжить", "Сохранить и продолжить", "Готово"][step]; $("setup-skip").hidden = step === 3; $("setup-close").disabled = $("setup-skip").disabled = working; // A typed or selected destination is never replaced by a health refresh. if (!$("setup-folder").value && ready()) $("setup-folder").value = saved.folder || hardware.defaultFolder || ""; } function showStep(next) { step = next; error(); const titles = ["Сохраняйте ролики в один клик", "Подключим локальный помощник", "Выберите настройки скачивания", "Всё готово к скачиванию"]; $("setup-title").textContent = titles[step]; for (const section of dialog.querySelectorAll("[data-setup-step]")) section.hidden = Number(section.dataset.setupStep) !== step; for (const [index, item] of [...dialog.querySelectorAll(".setup-progress li")].entries()) { item.dataset.state = index < step ? "complete" : index === step ? "current" : "pending"; if (index === step) item.setAttribute("aria-current", "step"); else item.removeAttribute("aria-current"); } $("setup-saved").textContent = `Файлы будут сохраняться в ${saved.folder || "выбранную папку"}.${saved.workFolder ? ` Загрузка и обработка — в ${saved.workFolder}.` : ""}`; $("setup-title").focus(); $("setup-title").parentElement.scrollTop = 0; render(); } async function close(state) { if (working) return; try { await chrome.storage.local.set({ onboarding: { version: 1, state, finishedAt: Date.now() } }); dialog.close(); onClose(); } catch (failure) { error(`Не удалось сохранить настройку: ${failure.message}`); } } async function run(task) { if (working) return; working = true; error(); render(); try { await task(); } catch (failure) { error(failure.message); } finally { working = false; render(); } } $("setup-check").addEventListener("click", () => run(connectHelper)); $("setup-install").addEventListener("click", () => { $("setup-helper-message").textContent = "Скачайте YouTubeDL-Helper-Setup.exe на открывшейся странице релизов проекта, запустите его и вернитесь сюда для проверки подключения."; }); $("setup-updates").addEventListener("change", () => { const enabled = $("setup-updates").checked; run(async () => { const settings = await call("update_settings", { enabled }); if (getHelper().hardware) getHelper().hardware.updateSettings = settings; }); }); $("setup-pick-folder").addEventListener("click", () => run(async () => { const result = await call("pick_folder", { current: $("setup-folder").value }); if (result.folder) $("setup-folder").value = result.folder; })); $("setup-pick-work-folder").addEventListener("click", () => run(async () => { const result = await call("pick_folder", { current: $("setup-work-folder").value || $("setup-folder").value }); if (result.folder) $("setup-work-folder").value = result.folder; })); $("setup-mode").addEventListener("change", () => formats()); $("setup-back").addEventListener("click", () => showStep(step - 1)); $("setup-next").addEventListener("click", async () => { if (working || ([1, 2].includes(step) && !ready())) return; if (step === 3) { await close("complete"); return; } if (step === 2) { if (!$("setup-folder").value.trim()) { error("Выберите папку для сохранения файлов."); $("setup-folder").focus(); return; } await run(async () => { formats(); const quality = $("setup-quality").value; const preferences = { ...saved, folder: $("setup-folder").value.trim(), workFolder: $("setup-work-folder").value.trim(), mode: $("setup-mode").value, videoContainer: videoFormat, audioContainer: audioFormat, codec: "auto", engine: "auto", preset: "balanced", bitrate: saved.bitrate || 256, videoId: quality === "best" ? "best" : "setup-quality", audioId: "best", quality: quality === "best" ? null : { height: Number(quality), fps: 30, codec: "" } }; await chrome.storage.local.set({ preferences }); saved = preferences; applyPreferences(preferences); showStep(3); }); } else showStep(step + 1); }); for (const id of ["setup-close", "setup-skip"]) $(id).addEventListener("click", () => close("skipped")); dialog.addEventListener("cancel", event => { event.preventDefault(); close("skipped"); }); return { get isOpen() { return dialog.open; }, render, async open() { if (dialog.open) return; const stored = await chrome.storage.local.get("preferences"); saved = stored.preferences || {}; videoFormat = saved.videoContainer || "mp4"; audioFormat = saved.audioContainer || "m4a"; $("setup-folder").value = saved.folder || getHelper().hardware?.defaultFolder || ""; $("setup-work-folder").value = saved.workFolder || ""; $("setup-mode").value = ["video", "video_only", "audio"].includes(saved.mode) ? saved.mode : "video"; $("setup-quality").value = saved.videoId !== "best" && saved.quality?.height ? String(saved.quality.height) : "best"; if (!$("setup-quality").value) $("setup-quality").value = "best"; formats(false); dialog.showModal(); showStep(0); }, }; }