From 4bc0a2c541e386d7637c17cf30635c00110151f4 Mon Sep 17 00:00:00 2001 From: Autli Date: Wed, 28 Oct 2026 11:05:33 +0500 Subject: [PATCH] release: v0.0.12 - smart platform auto-updater, clean player header with gear settings, tv remote optimization, fixed media buffering --- index.html | 127 +---- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 149 +++++- src-tauri/tauri.conf.json | 2 +- src/App.css | 13 + src/components/UpdaterBanner.tsx | 385 ++++++++++++--- src/pages/cinema/Cinema.css | 206 ++++++-- src/pages/cinema/Cinema.tsx | 806 ++++++++++++++++++------------- 10 files changed, 1140 insertions(+), 554 deletions(-) diff --git a/index.html b/index.html index 0710bf1..ace7fa2 100644 --- a/index.html +++ b/index.html @@ -7,10 +7,10 @@ has - + diff --git a/package.json b/package.json index 26e46ce..dba52b4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "has", "private": true, - "version": "0.0.11", + "version": "0.0.12", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5637eee..5763109 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1581,7 +1581,7 @@ dependencies = [ [[package]] name = "has" -version = "0.0.11" +version = "0.0.12" dependencies = [ "base64 0.22.1", "bcrypt", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8c21c35..0300fb0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "has" -version = "0.0.11" +version = "0.0.12" description = "has" authors = ["autli"] edition = "2024" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cc61d43..bc8a2a4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,7 +6,7 @@ mod cinema; use admin::DeviceSecurityInfo; use auth::{AuthResponse, DbStatus, ResetRequestResponse}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use storage::{ServerUserDto, StorageItemDto, StorageStats}; #[derive(Debug, Serialize)] @@ -201,6 +201,150 @@ async fn admin_delete_user( admin::admin_delete_user(&admin_email, target_id, admin_pin).await } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppReleaseInfo { + pub version: String, + pub current_version: String, + pub body: String, + pub has_update: bool, + pub download_url: String, + pub platform: String, + pub windows_url: Option, + pub tv_apk_url: Option, + pub mobile_apk_url: Option, + pub linux_url: Option, + pub macos_url: Option, +} + +fn detect_current_platform() -> &'static str { + #[cfg(target_os = "android")] + { + "android" + } + #[cfg(target_os = "windows")] + { + "windows" + } + #[cfg(target_os = "macos")] + { + "macos" + } + #[cfg(target_os = "linux")] + { + "linux" + } + #[cfg(not(any(target_os = "android", target_os = "windows", target_os = "macos", target_os = "linux")))] + { + "unknown" + } +} + +fn is_newer_semver(remote: &str, current: &str) -> bool { + let parse = |v: &str| -> Vec { + v.trim_start_matches('v') + .split('.') + .map(|s| s.parse::().unwrap_or(0)) + .collect() + }; + let r = parse(remote); + let c = parse(current); + for i in 0..r.len().max(c.len()) { + let rv = *r.get(i).unwrap_or(&0); + let cv = *c.get(i).unwrap_or(&0); + if rv > cv { + return true; + } + if rv < cv { + return false; + } + } + false +} + +#[tauri::command] +async fn check_app_update(current_version: Option) -> Result { + let cur_ver = current_version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); + let client = reqwest::Client::builder() + .user_agent("HAS-App-Updater") + .timeout(std::time::Duration::from_secs(10)) + .build() + .map_err(|e| e.to_string())?; + + let res = client + .get("https://api.github.com/repos/autli5/has/releases/latest") + .header("Accept", "application/vnd.github.v3+json") + .send() + .await + .map_err(|e| format!("Failed to reach GitHub: {}", e))?; + + if !res.status().is_success() { + return Err(format!("GitHub API returned HTTP {}", res.status())); + } + + let json: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + let raw_tag = json["tag_name"].as_str().unwrap_or("0.0.0"); + let remote_version = raw_tag.trim_start_matches('v').to_string(); + let body = json["body"].as_str().unwrap_or("Новое обновление доступно.").to_string(); + let html_url = json["html_url"] + .as_str() + .unwrap_or("https://github.com/autli5/has/releases/latest") + .to_string(); + + let has_update = is_newer_semver(&remote_version, &cur_ver); + + let mut tv_apk_url = None; + let mut mobile_apk_url = None; + let mut windows_url = None; + let mut linux_url = None; + let mut macos_url = None; + + if let Some(assets) = json["assets"].as_array() { + for asset in assets { + if let (Some(name), Some(url)) = (asset["name"].as_str(), asset["browser_download_url"].as_str()) { + let lower = name.to_lowercase(); + if (lower.contains("_tv_") || lower.contains("-tv-") || lower.contains(".tv.")) && lower.ends_with(".apk") { + tv_apk_url = Some(url.to_string()); + } else if lower.ends_with(".apk") && mobile_apk_url.is_none() { + mobile_apk_url = Some(url.to_string()); + } else if lower.ends_with(".exe") || lower.ends_with(".msi") { + if windows_url.is_none() || lower.ends_with(".exe") { + windows_url = Some(url.to_string()); + } + } else if lower.ends_with(".deb") || lower.ends_with(".appimage") || lower.ends_with(".rpm") { + if linux_url.is_none() || lower.ends_with(".deb") { + linux_url = Some(url.to_string()); + } + } else if lower.ends_with(".dmg") { + macos_url = Some(url.to_string()); + } + } + } + } + + if tv_apk_url.is_none() && mobile_apk_url.is_some() { + tv_apk_url = mobile_apk_url.clone(); + } + if mobile_apk_url.is_none() && tv_apk_url.is_some() { + mobile_apk_url = tv_apk_url.clone(); + } + + let platform = detect_current_platform().to_string(); + + Ok(AppReleaseInfo { + version: remote_version, + current_version: cur_ver, + body, + has_update, + download_url: html_url, + platform, + windows_url, + tv_apk_url, + mobile_apk_url, + linux_url, + macos_url, + }) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let _ = rustls::crypto::ring::default_provider().install_default(); @@ -254,7 +398,8 @@ pub fn run() { cinema::toggle_cinema_bookmark, cinema::tmdb_query, cinema::get_russian_media_info, - cinema::filter_playable_media + cinema::filter_playable_media, + check_app_update ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e69f2fc..297510f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "has", - "version": "0.0.11", + "version": "0.0.12", "identifier": "com.has.tauri-app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.css b/src/App.css index 50c5cf1..b79a3ec 100644 --- a/src/App.css +++ b/src/App.css @@ -417,6 +417,13 @@ svg { color: #cbd5e1; } +/* Button 3: Выход из приложения (Exit) */ +.has-tv-updater-btn.btn-exit { + background: rgba(239, 68, 68, 0.15); + border: 2px solid rgba(239, 68, 68, 0.35); + color: #fca5a5; +} + /* D-Pad Remote Control Focused Glow */ .has-tv-updater-btn.focused { transform: scale(1.08); @@ -430,6 +437,12 @@ svg { box-shadow: 0 0 0 4px #ffffff, 0 0 30px rgba(255, 255, 255, 0.4) !important; } +.has-tv-updater-btn.btn-exit.focused { + background: rgba(220, 38, 38, 0.9); + box-shadow: 0 0 0 4px #ffffff, 0 0 35px rgba(239, 68, 68, 0.8) !important; + color: #ffffff; +} + /* TV Progress View */ .has-tv-updater-progress-container { width: 100%; diff --git a/src/components/UpdaterBanner.tsx b/src/components/UpdaterBanner.tsx index 6ad18e3..53f3b67 100644 --- a/src/components/UpdaterBanner.tsx +++ b/src/components/UpdaterBanner.tsx @@ -1,9 +1,18 @@ -import { useState, useEffect, useCallback } from "react"; +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 { @@ -44,8 +53,42 @@ const detectTvEnvironment = (): boolean => { 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.12"); const [updateAvailable, setUpdateAvailable] = useState(false); const [updateInfo, setUpdateInfo] = useState(null); const [downloading, setDownloading] = useState(false); @@ -53,8 +96,9 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { const [error, setError] = useState(null); const [dismissed, setDismissed] = useState(false); - // Strictly two buttons for TV remote navigation: "update" | "skip" - const [tvFocusedBtn, setTvFocusedBtn] = useState<"update" | "skip">("update"); + // TV remote navigation: "update" | "skip" | "exit" + const [tvFocusedBtn, setTvFocusedBtn] = useState<"update" | "skip" | "exit">("update"); + const updateBtnRef = useRef(null); useEffect(() => { if (typeof isTvEnvironment === "boolean") { @@ -62,12 +106,52 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { } }, [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 official Tauri updater plugin first (Desktop) + // 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"); @@ -81,46 +165,78 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { setDismissed(false); return; } - } catch (err) { - console.warn("[Updater] Plugin check fallback:", err); + } catch { + // Expected on mobile / TV webview } } - // 2. Fallback check for Android TV / browser via releases endpoint + // 3. GitHub API check fallback try { - const resp = await fetch("https://github.com/autli5/has/releases/latest/download/latest.json"); + 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(); - if (!cancelled && data?.version && data.version !== "0.0.11") { + 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: data.version, - body: data.notes || data.body || "Новое обновление с улучшенной стабильностью и функциями.", + 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 {} + } catch (err) { + console.warn("[Updater] Fallback fetch error:", err); + } }; - // 1. Initial check after startup - const timer = setTimeout(checkForUpdates, 1000); - - // 2. Periodic check every 60 seconds - const interval = setInterval(checkForUpdates, 60 * 1000); - - // 3. Check whenever window gains focus + const timer = setTimeout(checkForUpdates, 1200); + const interval = setInterval(checkForUpdates, 90 * 1000); const handleFocus = () => checkForUpdates(); window.addEventListener("focus", handleFocus); - - // 4. Custom event for manual checks from UI or simulation window.addEventListener("has-check-update", handleFocus); const handleSimulateUpdate = (e: any) => { setUpdateAvailable(true); setUpdateInfo({ version: e.detail?.version || "1.0.0", - body: e.detail?.body || "Вышло масштабное обновление программы: оптимизация для Android TV, автопереключение плееров и синхронизация.", + body: e.detail?.body || "Вышло масштабное обновление программы.", + downloadUrl: "https://github.com/autli5/has/releases/latest", }); setDismissed(false); }; @@ -134,15 +250,94 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { 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: `Доступно обновление для Android TV (v${version})`, + desc: "Специальная сборка для телевизоров и ТВ-приставок с поддержкой пульта ДУ (D-Pad), Leanback-интерфейса и плавного воспроизведения.", + btnLabel: "🚀 Обновить APK для ТВ", + icon: "📺", + }; + case "android_mobile": + return { + badge: "📱 Android", + title: `Доступно обновление для Android (v${version})`, + 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 = (): 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"; + } + }; const handleInstall = useCallback(async () => { setDownloading(true); setError(null); setProgress(0); - // Desktop Tauri plugin install - if (typeof window !== "undefined" && "__TAURI_INTERNALS__" in window) { + // 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"); @@ -157,66 +352,99 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { total = event.data?.contentLength ?? 0; } else if (event.event === "Progress") { downloaded += event.data?.chunkLength ?? 0; - if (total > 0) { - setProgress(Math.round((downloaded / total) * 100)); - } } else if (event.event === "Finished") { setProgress(100); } + if (total > 0) { + setProgress(Math.round((downloaded / total) * 100)); + } }); await relaunch(); return; } } catch (e: unknown) { - console.warn("[Updater] Desktop updater failed, falling back to download:", e); + console.warn("[Updater] Desktop plugin install skipped/fallback:", e); } } - // Fallback for Android TV / browser: animated progress and open download + // Direct download and open target platform installer/APK + const targetUrl = resolveTargetUrl(); + let p = 0; const progressTimer = setInterval(() => { - p += 15; + p += 20; if (p >= 100) { clearInterval(progressTimer); setProgress(100); setTimeout(async () => { try { const { openUrl } = await import("@tauri-apps/plugin-opener"); - await openUrl("https://github.com/autli5/has/releases/latest"); + await openUrl(targetUrl); } catch { - window.open("https://github.com/autli5/has/releases/latest", "_blank"); + window.open(targetUrl, "_blank"); } setDismissed(true); setDownloading(false); - }, 600); + }, 400); } else { setProgress(p); } - }, 250); + }, 150); + }, [isTv, clientPlatform, updateInfo]); + + // Clean Exit from TV application + const handleExitApp = useCallback(async () => { + try { + const { exit } = await import("@tauri-apps/plugin-process"); + await exit(0); + } catch { + window.close(); + } }, []); // 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) => { - // Toggle strictly between "update" and "skip" on Left / Right / Up / Down / Tab + // Cycle between "update" -> "skip" -> "exit" if ( - e.key === "ArrowLeft" || e.key === "ArrowRight" || - e.key === "ArrowUp" || e.key === "ArrowDown" || - e.keyCode === 21 || e.keyCode === 22 || - e.keyCode === 19 || e.keyCode === 20 || e.key === "Tab" ) { e.preventDefault(); - setTvFocusedBtn((prev) => (prev === "update" ? "skip" : "update")); + setTvFocusedBtn((prev) => { + if (prev === "update") return "skip"; + if (prev === "skip") return "exit"; + return "update"; + }); + playTvBeep(520, 0.025); + return; + } + + if ( + e.key === "ArrowLeft" || + e.key === "ArrowUp" || + e.keyCode === 21 || + e.keyCode === 19 + ) { + e.preventDefault(); + setTvFocusedBtn((prev) => { + if (prev === "exit") return "skip"; + if (prev === "skip") return "update"; + return "exit"; + }); playTvBeep(520, 0.025); return; } @@ -227,6 +455,9 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { if (tvFocusedBtn === "update") { playTvBeep(680, 0.05); handleInstall(); + } else if (tvFocusedBtn === "exit") { + playTvBeep(320, 0.06); + handleExitApp(); } else { playTvBeep(420, 0.04); setDismissed(true); @@ -244,26 +475,25 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { window.addEventListener("keydown", handleTvKeyDown); return () => window.removeEventListener("keydown", handleTvKeyDown); - }, [updateAvailable, dismissed, isTv, downloading, tvFocusedBtn, handleInstall]); + }, [updateAvailable, dismissed, isTv, downloading, tvFocusedBtn, handleInstall, handleExitApp]); if (!updateAvailable || dismissed) return null; // ========================================================================= - // 1. ANDROID TV: Full-screen Centered Modal with EXACTLY Two Buttons + // 1. ANDROID TV: Full-screen Centered Modal with D-Pad TV Navigation & Exit // ========================================================================= if (isTv) { return (
-
🚀
-

Доступно обновление

+
{platformInfo.icon}
+

{platformInfo.title}

- Вышла новая версия v{updateInfo?.version || "1.0.0"} + {platformInfo.badge}

- {updateInfo?.body || - "Вышло новое обновление программы с оптимизацией для Android TV, автопереключением плееров и синхронизацией истории."} + {updateInfo?.body || platformInfo.desc}

{downloading ? ( @@ -280,8 +510,9 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) {
) : (
- {/* Button 1: Обновить */} + {/* Button 1: Обновить APK для ТВ */} {/* Button 2: Пропустить */} @@ -308,16 +540,35 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { }} onMouseEnter={() => setTvFocusedBtn("skip")} tabIndex={0} + data-tv-focusable="true" > Пропустить + + {/* Button 3: Выход из приложения (Exit) */} +
)} {error &&
{error}
}
- ◄ ► Стрелки на пульте: переключение • OK: подтвердить выбор • Back: пропустить + ◄ ► Стрелки на пульте: выбор • OK: подтвердить • Back: пропустить
@@ -325,17 +576,31 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { } // ========================================================================= - // 2. DESKTOP: Classic Bottom-Right Popup Banner + // 2. DESKTOP (WINDOWS / MAC / LINUX) & MOBILE: Smart Platform Banner // ========================================================================= return (
-
🚀
+
{platformInfo.icon}
- Доступно обновление v{updateInfo?.version} - {updateInfo?.body && ( - {updateInfo.body} - )} +
+ {platformInfo.title} + + {platformInfo.badge} + +
+ + {updateInfo?.body || platformInfo.desc} +
{downloading ? ( @@ -355,7 +620,7 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { className="updater-btn-install" onClick={handleInstall} > - Обновить сейчас + {platformInfo.btnLabel} + HAS Кинотеатр +
+ + + + )} + {/* ======================================================== 1. TOP NAVIGATION BAR (For Desktop / Mobile only; hidden in TV Mode) ======================================================== */} @@ -4170,6 +4299,7 @@ export default function Cinema({ }`} onMouseMove={resetHudTimer} onClick={resetHudTimer} + onDoubleClick={handleToggleFullscreen} > {/* Top Edge Sensor: triggers HUD immediately when cursor moves to top 90px of screen */}
← Назад @@ -4228,176 +4359,22 @@ export default function Cinema({ Полный фильм )}
- - {(playingMedia || selectedMedia).media_type !== "movie" && ( -
- - -
- )}
- {/* Episodes button for Series only */} - {(playingMedia || selectedMedia).media_type !== "movie" && ( - - )} - - {/* Server selector buttons right in the HUD */} -
- - - - - -
- - {/* Quick mirror switcher */} - - - {/* AdBlock Guard toggle */} - - - {/* Quick Skip Ad / Intro Pill */} - - - {/* Single Settings button (Contains voiceover studios & mirrors) */} + {/* Single Gear Button: Contains ALL player functions */} - - {/* Fullscreen toggle */} - {/* Close */} @@ -4414,24 +4391,98 @@ export default function Cinema({ }} title="Закрыть плеер" data-tv-focusable="true" + data-tv-row="top" > ✕
- {/* Quick Fallback Switcher (Always accessible if video fails to load) */} -
- Не работает плеер? - + {/* Leanback Bottom Playback Controls Bar */} +
{ + setIsHudVisible(true); + resetHudTimer(); + }} + onMouseMove={resetHudTimer} + > +
+ {(playingMedia || selectedMedia).media_type !== "movie" && ( + + )} + + + + + + + + {(playingMedia || selectedMedia).media_type !== "movie" && ( + + )} +
{/* Clean 100% Video Canvas */} @@ -4527,15 +4578,10 @@ export default function Cinema({ ) ) : playerMode === "balancer" ? (