import { useState, useEffect, useCallback, useRef } from "react"; import { playTvBeep } from "../utils/tvAudio"; export type DetectedPlatform = "windows" | "android_tv" | "android_mobile" | "linux" | "macos" | "web"; interface UpdateInfo { version: string; body: string | null; downloadUrl?: string; platform?: string; windowsUrl?: string | null; tvApkUrl?: string | null; mobileApkUrl?: string | null; linuxUrl?: string | null; macosUrl?: string | null; } interface UpdaterBannerProps { isTvEnvironment?: boolean; } // Helper for Smart TV / Android TV detection const detectTvEnvironment = (): boolean => { if (typeof window === "undefined") return false; const saved = localStorage.getItem("has_tv_mode"); if (saved !== null) return saved === "true"; const ua = (navigator.userAgent || "").toLowerCase(); const isTvUa = ua.includes("android tv") || ua.includes("smarttv") || ua.includes("googletv") || ua.includes("tizen") || ua.includes("webos") || ua.includes("appletv") || ua.includes("crkey") || ua.includes("bravia") || ua.includes("aftt") || ua.includes("aftm") || ua.includes("mibox") || ua.includes("mi box") || ua.includes("shield") || ua.includes("dtv") || ua.includes("smart-tv") || ua.includes("hbbtv") || ua.includes("netcast") || ua.includes("viera") || ua.includes("roku") || ua.includes("firetv"); if (isTvUa) return true; const isAndroid = ua.includes("android"); const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0; if (isAndroid && noTouch) return true; return false; }; // Detect client platform const detectClientPlatform = (isTv: boolean, backendPlatform?: string): DetectedPlatform => { if (isTv) return "android_tv"; if (backendPlatform === "android") return "android_mobile"; if (backendPlatform === "windows") return "windows"; if (backendPlatform === "macos") return "macos"; if (backendPlatform === "linux") return "linux"; const ua = (typeof navigator !== "undefined" ? navigator.userAgent : "").toLowerCase(); const plat = (typeof navigator !== "undefined" ? navigator.platform : "").toLowerCase(); if (ua.includes("android")) return isTv ? "android_tv" : "android_mobile"; if (plat.includes("win") || ua.includes("windows")) return "windows"; if (plat.includes("mac") || ua.includes("macintosh")) return "macos"; if (plat.includes("linux") || ua.includes("linux")) return "linux"; return "web"; }; // Check if a remote version is strictly newer than the current version const isNewerSemver = (remote: string, current: string): boolean => { const cleanRemote = remote.replace(/^v/i, "").trim(); const cleanCurrent = current.replace(/^v/i, "").trim(); if (cleanRemote === cleanCurrent) return false; const rParts = cleanRemote.split(".").map((n) => parseInt(n, 10) || 0); const cParts = cleanCurrent.split(".").map((n) => parseInt(n, 10) || 0); for (let i = 0; i < Math.max(rParts.length, cParts.length); i++) { const r = rParts[i] ?? 0; const c = cParts[i] ?? 0; if (r > c) return true; if (r < c) return false; } return false; }; export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { const [isTv, setIsTv] = useState(() => isTvEnvironment ?? detectTvEnvironment()); const [currentAppVersion, setCurrentAppVersion] = useState("0.0.15"); const [updateAvailable, setUpdateAvailable] = useState(false); const [updateInfo, setUpdateInfo] = useState(null); const [downloading, setDownloading] = useState(false); const [progress, setProgress] = useState(0); const [statusText, setStatusText] = useState("Загрузка обновления..."); const [error, setError] = useState(null); const [dismissed, setDismissed] = useState(false); // Auto-update preferences for Android TV const [autoUpdateTv, setAutoUpdateTv] = useState(() => { if (typeof window === "undefined") return false; return localStorage.getItem("has_auto_update_tv") === "true"; }); const [autoCountdown, setAutoCountdown] = useState(null); // TV remote navigation: "update" | "skip" | "exit" | "autotoggle" const [tvFocusedBtn, setTvFocusedBtn] = useState<"update" | "skip" | "exit" | "autotoggle">("update"); const updateBtnRef = useRef(null); useEffect(() => { if (typeof isTvEnvironment === "boolean") { setIsTv(isTvEnvironment); } }, [isTvEnvironment]); // Read current running version from Tauri on startup useEffect(() => { if (typeof window !== "undefined" && "__TAURI_INTERNALS__" in window) { import("@tauri-apps/api/app") .then(({ getVersion }) => getVersion()) .then((ver) => { if (ver) setCurrentAppVersion(ver); }) .catch(() => {}); } }, []); // Check for updates useEffect(() => { let cancelled = false; const checkForUpdates = async () => { // 1. Try Rust backend command first (Works on Android TV, Mobile & Desktop without CORS limits) if (typeof window !== "undefined" && "__TAURI_INTERNALS__" in window) { try { const { invoke } = await import("@tauri-apps/api/core"); const res = await invoke("check_app_update", { currentVersion: currentAppVersion, }); if (!cancelled && res && res.has_update) { setUpdateAvailable(true); setUpdateInfo({ version: res.version, body: res.body || "Новое обновление с улучшенной стабильностью и функциями.", downloadUrl: res.download_url, platform: res.platform, windowsUrl: res.windows_url, tvApkUrl: res.tv_apk_url, mobileApkUrl: res.mobile_apk_url, linuxUrl: res.linux_url, macosUrl: res.macos_url, }); setDismissed(false); return; } } catch (err) { console.warn("[Updater] Rust backend check failed, trying fallback:", err); } } // 2. Desktop Tauri updater plugin (macOS / Windows / Linux) if (typeof window !== "undefined" && "__TAURI_INTERNALS__" in window) { try { const { check } = await import("@tauri-apps/plugin-updater"); const update = await check(); if (!cancelled && update?.available) { setUpdateAvailable(true); setUpdateInfo({ version: update.version, body: update.body ?? null, }); setDismissed(false); return; } } catch { // Expected on mobile / TV webview } } // 3. GitHub API check fallback try { const resp = await fetch("https://api.github.com/repos/autli5/has/releases/latest", { headers: { Accept: "application/vnd.github.v3+json" }, }); if (resp.ok) { const data = await resp.json(); const remoteTag = (data?.tag_name || "").replace(/^v/i, "").trim(); if (!cancelled && remoteTag && isNewerSemver(remoteTag, currentAppVersion)) { let tvApk: string | null = null; let mobileApk: string | null = null; let winUrl: string | null = null; let linUrl: string | null = null; let macUrl: string | null = null; if (Array.isArray(data?.assets)) { for (const asset of data.assets) { const name: string = (asset.name || "").toLowerCase(); const url: string = asset.browser_download_url || ""; if ((name.includes("_tv_") || name.includes("-tv-") || name.includes(".tv.")) && name.endsWith(".apk")) { tvApk = url; } else if (name.endsWith(".apk") && !mobileApk) { mobileApk = url; } else if (name.endsWith(".exe") || name.endsWith(".msi")) { if (!winUrl || name.endsWith(".exe")) winUrl = url; } else if (name.endsWith(".deb") || name.endsWith(".appimage") || name.endsWith(".rpm")) { if (!linUrl || name.endsWith(".deb")) linUrl = url; } else if (name.endsWith(".dmg")) { macUrl = url; } } } if (!tvApk && mobileApk) tvApk = mobileApk; if (!mobileApk && tvApk) mobileApk = tvApk; setUpdateAvailable(true); setUpdateInfo({ version: remoteTag, body: data.body || "Новое обновление с улучшенной стабильностью и функциями.", downloadUrl: data.html_url || "https://github.com/autli5/has/releases/latest", windowsUrl: winUrl, tvApkUrl: tvApk, mobileApkUrl: mobileApk, linuxUrl: linUrl, macosUrl: macUrl, }); setDismissed(false); } } } catch (err) { console.warn("[Updater] Fallback fetch error:", err); } }; const timer = setTimeout(checkForUpdates, 1200); const interval = setInterval(checkForUpdates, 90 * 1000); const handleFocus = () => checkForUpdates(); window.addEventListener("focus", handleFocus); window.addEventListener("has-check-update", handleFocus); const handleSimulateUpdate = (e: any) => { setUpdateAvailable(true); setUpdateInfo({ version: e.detail?.version || "1.0.0", body: e.detail?.body || "Вышло масштабное обновление программы.", downloadUrl: "https://github.com/autli5/has/releases/latest", }); setDismissed(false); }; window.addEventListener("has-simulate-update", handleSimulateUpdate); return () => { cancelled = true; clearTimeout(timer); clearInterval(interval); window.removeEventListener("focus", handleFocus); window.removeEventListener("has-check-update", handleFocus); window.removeEventListener("has-simulate-update", handleSimulateUpdate); }; }, [currentAppVersion]); // Determine current active platform const clientPlatform = detectClientPlatform(isTv, updateInfo?.platform); // Platform-tailored texts and assets const getPlatformDetails = () => { const version = updateInfo?.version || "1.0.0"; switch (clientPlatform) { case "windows": return { badge: "🪟 Windows", title: `Доступно обновление для Windows (v${version})`, desc: "Вышла новая версия приложения для Windows. Нажмите «Обновить», чтобы установить последнюю версию программы.", btnLabel: "🚀 Обновить для Windows (.exe)", icon: "🪟", }; case "android_tv": return { badge: "📺 Android TV", title: "Хотите ли вы обновить приложение HAS?", desc: "Доступна новая оптимизированная версия для телевизоров и ТВ-приставок с поддержкой пульта ДУ, балансеров видео и повышенной стабильностью.", btnLabel: "🚀 Да, обновить сейчас", icon: "📺", }; case "android_mobile": return { badge: "📱 Android", title: "Хотите ли вы обновить приложение HAS?", desc: "Новая версия приложения для мобильных устройств на Android.", btnLabel: "🚀 Скачать APK для Android", icon: "📱", }; case "linux": return { badge: "🐧 Linux", title: `Доступно обновление для Linux (v${version})`, desc: "Пакет обновления для дистрибутивов Linux (.deb / .AppImage / .rpm).", btnLabel: "🚀 Скачать для Linux (.deb)", icon: "🐧", }; case "macos": return { badge: "🍏 macOS", title: `Доступно обновление для macOS (v${version})`, desc: "Новая версия приложения для macOS (.dmg).", btnLabel: "🚀 Скачать образ (.dmg)", icon: "🍏", }; default: return { badge: "🚀 Обновление", title: `Доступно обновление v${version}`, desc: "Вышла новая версия программы с улучшенной стабильностью.", btnLabel: "🚀 Обновить сейчас", icon: "🚀", }; } }; const platformInfo = getPlatformDetails(); // Smart download target selector const resolveTargetUrl = useCallback((): string => { if (!updateInfo) return "https://github.com/autli5/has/releases/latest"; switch (clientPlatform) { case "windows": return updateInfo.windowsUrl || updateInfo.downloadUrl || "https://github.com/autli5/has/releases/latest"; case "android_tv": return updateInfo.tvApkUrl || updateInfo.mobileApkUrl || updateInfo.downloadUrl || "https://github.com/autli5/has/releases/latest"; case "android_mobile": return updateInfo.mobileApkUrl || updateInfo.tvApkUrl || updateInfo.downloadUrl || "https://github.com/autli5/has/releases/latest"; case "linux": return updateInfo.linuxUrl || updateInfo.downloadUrl || "https://github.com/autli5/has/releases/latest"; case "macos": return updateInfo.macosUrl || updateInfo.downloadUrl || "https://github.com/autli5/has/releases/latest"; default: return updateInfo.downloadUrl || "https://github.com/autli5/has/releases/latest"; } }, [clientPlatform, updateInfo]); const cancelAutoCountdown = useCallback(() => { setAutoCountdown(null); }, []); const toggleAutoUpdate = useCallback(() => { setAutoUpdateTv((prev) => { const next = !prev; try { localStorage.setItem("has_auto_update_tv", next ? "true" : "false"); } catch {} return next; }); cancelAutoCountdown(); }, [cancelAutoCountdown]); const handleInstall = useCallback(async () => { cancelAutoCountdown(); setDownloading(true); setError(null); setProgress(0); setStatusText("Подготовка к установке обновления..."); // Desktop Windows / macOS / Linux Tauri updater plugin if (typeof window !== "undefined" && "__TAURI_INTERNALS__" in window && !isTv) { try { const { check } = await import("@tauri-apps/plugin-updater"); const { relaunch } = await import("@tauri-apps/plugin-process"); const update = await check(); if (update?.available) { let downloaded = 0; let total = 0; setStatusText("Загрузка обновления..."); await update.downloadAndInstall((event: any) => { if (event.event === "Started") { total = event.data?.contentLength ?? 0; } else if (event.event === "Progress") { downloaded += event.data?.chunkLength ?? 0; } else if (event.event === "Finished") { setProgress(100); setStatusText("Перезапуск программы..."); } if (total > 0) { const p = Math.round((downloaded / total) * 100); setProgress(p); setStatusText(`Загрузка: ${p}%`); } }); await relaunch(); return; } } catch (e: unknown) { console.warn("[Updater] Desktop plugin install skipped/fallback:", e); } } // Android TV / Mobile / Direct APK Download & Installation const targetUrl = resolveTargetUrl(); setStatusText("Подключение к серверу загрузки APK..."); let streamSucceeded = false; try { const response = await fetch(targetUrl, { mode: "cors" }); if (response.ok && response.body) { const contentLength = +(response.headers.get("Content-Length") || 0); const reader = response.body.getReader(); let received = 0; const chunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; if (value) { chunks.push(value); received += value.length; if (contentLength > 0) { const pct = Math.min(99, Math.round((received / contentLength) * 100)); setProgress(pct); setStatusText(`Скачивание APK: ${pct}% (${(received / 1024 / 1024).toFixed(1)} МБ)`); } else { setStatusText(`Скачивание APK: ${(received / 1024 / 1024).toFixed(1)} МБ`); } } } setProgress(100); setStatusText("APK успешно скачан! Запуск установки..."); streamSucceeded = true; const blob = new Blob(chunks as BlobPart[], { type: "application/vnd.android.package-archive" }); const blobUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = blobUrl; a.download = `has-tv-v${updateInfo?.version || "update"}.apk`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } } catch { // Fetch streaming can be restricted by CORS on certain hosts; fallback below } if (!streamSucceeded) { let p = 0; await new Promise((resolve) => { const progressTimer = setInterval(() => { p += 20; if (p >= 100) { clearInterval(progressTimer); setProgress(100); setStatusText("Запуск установщика приложения..."); resolve(); } else { setProgress(p); setStatusText(`Загрузка: ${p}%`); } }, 120); }); } try { const { openUrl } = await import("@tauri-apps/plugin-opener"); await openUrl(targetUrl); } catch { window.open(targetUrl, "_blank"); } setTimeout(() => { setDismissed(true); setDownloading(false); }, 1800); }, [cancelAutoCountdown, isTv, updateInfo, resolveTargetUrl]); // Clean Exit from TV application const handleExitApp = useCallback(async () => { try { const { exit } = await import("@tauri-apps/plugin-process"); await exit(0); } catch { window.close(); } }, []); // Auto-update countdown for Android TV useEffect(() => { if (!updateAvailable || dismissed || !isTv || downloading) return; if (!autoUpdateTv) { setAutoCountdown(null); return; } setAutoCountdown(5); const interval = setInterval(() => { setAutoCountdown((prev) => { if (prev === null) return null; if (prev <= 1) { clearInterval(interval); handleInstall(); return null; } return prev - 1; }); }, 1000); return () => clearInterval(interval); }, [updateAvailable, dismissed, isTv, downloading, autoUpdateTv, handleInstall]); // Strict Remote Control D-Pad Navigation for TV Update Modal useEffect(() => { if (!updateAvailable || dismissed || !isTv || downloading) return; // Auto-focus the "Update" button when TV modal appears setTimeout(() => { if (updateBtnRef.current) { updateBtnRef.current.focus(); } }, 50); const handleTvKeyDown = (e: KeyboardEvent) => { cancelAutoCountdown(); // Down: switch to auto-update toggle if (e.key === "ArrowDown" || e.keyCode === 20) { e.preventDefault(); playTvBeep(520, 0.025); if (tvFocusedBtn !== "autotoggle") { setTvFocusedBtn("autotoggle"); } return; } // Up: switch back from auto-update toggle to update button if (e.key === "ArrowUp" || e.keyCode === 19) { e.preventDefault(); playTvBeep(520, 0.025); if (tvFocusedBtn === "autotoggle") { setTvFocusedBtn("update"); } return; } // Left: cycle within top button row if (e.key === "ArrowLeft" || e.keyCode === 21) { e.preventDefault(); playTvBeep(520, 0.025); setTvFocusedBtn((prev) => { if (prev === "exit") return "skip"; if (prev === "skip") return "update"; return "exit"; }); return; } // Right / Tab: cycle within top button row if (e.key === "ArrowRight" || e.keyCode === 22 || e.key === "Tab") { e.preventDefault(); playTvBeep(520, 0.025); setTvFocusedBtn((prev) => { if (prev === "update") return "skip"; if (prev === "skip") return "exit"; return "update"; }); return; } // Enter / OK (KeyCode 13, 23, 66) if (e.key === "Enter" || e.key === " " || e.keyCode === 13 || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); if (tvFocusedBtn === "update") { playTvBeep(680, 0.05); handleInstall(); } else if (tvFocusedBtn === "exit") { playTvBeep(320, 0.06); handleExitApp(); } else if (tvFocusedBtn === "autotoggle") { playTvBeep(560, 0.04); toggleAutoUpdate(); } else { playTvBeep(420, 0.04); setDismissed(true); } return; } // Back / Escape key triggers "skip" if (e.key === "Escape" || e.keyCode === 27 || e.keyCode === 4) { e.preventDefault(); playTvBeep(420, 0.04); setDismissed(true); } }; window.addEventListener("keydown", handleTvKeyDown); return () => window.removeEventListener("keydown", handleTvKeyDown); }, [updateAvailable, dismissed, isTv, downloading, tvFocusedBtn, handleInstall, handleExitApp, cancelAutoCountdown, toggleAutoUpdate]); if (!updateAvailable || dismissed) return null; // ========================================================================= // 1. ANDROID TV: Full-screen Centered Modal with D-Pad TV Navigation & Exit // ========================================================================= if (isTv) { return (
{platformInfo.icon}

Хотите ли вы обновить приложение HAS?

📺 Android TV • v{currentAppVersion} ➔ v{updateInfo?.version || "новейшая"}

{updateInfo?.body || platformInfo.desc}

{autoCountdown !== null && !downloading && (
⚡ Автообновление активно: начнется через {autoCountdown} сек... (нажмите любую кнопку для отмены)
)} {downloading ? (
{statusText}
) : ( <>
{/* Button 1: Да, обновить сейчас */} {/* Button 2: Напомнить позже */} {/* Button 3: Выход из приложения (Exit) */}
{/* Auto-update Toggle Chip for Android TV */} )} {error &&
{error}
}
◄ ► Стрелки на пульте: выбор • ▲ ▼ Вверх/Вниз: переключение опций • OK: подтвердить • Back: позже
); } // ========================================================================= // 2. DESKTOP (WINDOWS / MAC / LINUX) & MOBILE: Smart Platform Banner // ========================================================================= return (
{platformInfo.icon}
{platformInfo.title} {platformInfo.badge}
{updateInfo?.body || platformInfo.desc}
{downloading ? (
{progress}%
) : ( <> )}
{error &&
{error}
}
); }