From 9e9694ed19bf4be94784db909a709a25dae57cee Mon Sep 17 00:00:00 2001 From: Autli Date: Wed, 28 Oct 2026 22:17:50 +0500 Subject: [PATCH] feat(release): v0.0.15 - 100% player volume boost script, Android TV auto-update APK downloader & install modal --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- .../android/app/src/main/AndroidManifest.xml | 1 + src-tauri/tauri.conf.json | 2 +- src/App.css | 46 +++ src/components/UpdaterBanner.tsx | 358 +++++++++++++----- src/pages/cinema/Cinema.tsx | 101 ++++- 7 files changed, 407 insertions(+), 105 deletions(-) diff --git a/package.json b/package.json index cef3cee..f2dfce6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "has", "private": true, - "version": "0.0.14", + "version": "0.0.15", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7ff0d86..69ff9c6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "has" -version = "0.0.14" +version = "0.0.15" description = "has" authors = ["autli"] edition = "2024" diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml index e9a9dd3..e2ccca7 100644 --- a/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6af6b65..6b2a4cf 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.14", + "version": "0.0.15", "identifier": "com.has.tauri-app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.css b/src/App.css index b79a3ec..0163993 100644 --- a/src/App.css +++ b/src/App.css @@ -443,6 +443,52 @@ svg { color: #ffffff; } +/* TV Auto-update Toggle Chip & Countdown */ +.has-tv-updater-autotoggle { + margin-top: 20px; + background: rgba(255, 255, 255, 0.06); + border: 1px dashed rgba(255, 255, 255, 0.25); + border-radius: 12px; + padding: 10px 20px; + font-size: 13px; + color: #94a3b8; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.2s ease; + outline: none; +} + +.has-tv-updater-autotoggle.active { + background: rgba(16, 185, 129, 0.15); + border-color: rgba(16, 185, 129, 0.5); + color: #6ee7b7; +} + +.has-tv-updater-autotoggle.focused { + transform: scale(1.05); + border-color: #ffffff !important; + box-shadow: 0 0 0 3px #ffffff, 0 0 25px rgba(99, 102, 241, 0.7) !important; + color: #ffffff !important; + background: rgba(99, 102, 241, 0.35); +} + +.has-tv-updater-countdown { + margin-bottom: 14px; + font-size: 14px; + color: #38bdf8; + font-weight: 700; + display: flex; + align-items: center; + gap: 8px; + padding: 6px 14px; + border-radius: 8px; + background: rgba(56, 189, 248, 0.12); + border: 1px solid rgba(56, 189, 248, 0.3); +} + /* TV Progress View */ .has-tv-updater-progress-container { width: 100%; diff --git a/src/components/UpdaterBanner.tsx b/src/components/UpdaterBanner.tsx index 8cc994e..81dd05d 100644 --- a/src/components/UpdaterBanner.tsx +++ b/src/components/UpdaterBanner.tsx @@ -88,16 +88,24 @@ const isNewerSemver = (remote: string, current: string): boolean => { export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { const [isTv, setIsTv] = useState(() => isTvEnvironment ?? detectTvEnvironment()); - const [currentAppVersion, setCurrentAppVersion] = useState("0.0.14"); + 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); - // TV remote navigation: "update" | "skip" | "exit" - const [tvFocusedBtn, setTvFocusedBtn] = useState<"update" | "skip" | "exit">("update"); + // 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(() => { @@ -270,16 +278,16 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { case "android_tv": return { badge: "📺 Android TV", - title: `Доступно обновление для Android TV (v${version})`, - desc: "Специальная сборка для телевизоров и ТВ-приставок с поддержкой пульта ДУ (D-Pad), Leanback-интерфейса и плавного воспроизведения.", - btnLabel: "🚀 Обновить APK для ТВ", + title: "Хотите ли вы обновить приложение HAS?", + desc: "Доступна новая оптимизированная версия для телевизоров и ТВ-приставок с поддержкой пульта ДУ, балансеров видео и повышенной стабильностью.", + btnLabel: "🚀 Да, обновить сейчас", icon: "📺", }; case "android_mobile": return { badge: "📱 Android", - title: `Доступно обновление для Android (v${version})`, - desc: "Новая версия приложения для мобильных телефонов и планшетов на Android.", + title: "Хотите ли вы обновить приложение HAS?", + desc: "Новая версия приложения для мобильных устройств на Android.", btnLabel: "🚀 Скачать APK для Android", icon: "📱", }; @@ -313,7 +321,7 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { const platformInfo = getPlatformDetails(); // Smart download target selector - const resolveTargetUrl = (): string => { + const resolveTargetUrl = useCallback((): string => { if (!updateInfo) return "https://github.com/autli5/has/releases/latest"; switch (clientPlatform) { case "windows": @@ -329,12 +337,29 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { 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) { @@ -346,6 +371,7 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { if (update?.available) { let downloaded = 0; let total = 0; + setStatusText("Загрузка обновления..."); await update.downloadAndInstall((event: any) => { if (event.event === "Started") { @@ -354,9 +380,12 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { downloaded += event.data?.chunkLength ?? 0; } else if (event.event === "Finished") { setProgress(100); + setStatusText("Перезапуск программы..."); } if (total > 0) { - setProgress(Math.round((downloaded / total) * 100)); + const p = Math.round((downloaded / total) * 100); + setProgress(p); + setStatusText(`Загрузка: ${p}%`); } }); @@ -368,30 +397,82 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { } } - // Direct download and open target platform installer/APK + // Android TV / Mobile / Direct APK Download & Installation const targetUrl = resolveTargetUrl(); + setStatusText("Подключение к серверу загрузки APK..."); - let p = 0; - const progressTimer = setInterval(() => { - p += 20; - if (p >= 100) { - clearInterval(progressTimer); - setProgress(100); - setTimeout(async () => { - try { - const { openUrl } = await import("@tauri-apps/plugin-opener"); - await openUrl(targetUrl); - } catch { - window.open(targetUrl, "_blank"); + 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)} МБ`); + } } - setDismissed(true); - setDownloading(false); - }, 400); - } else { - setProgress(p); + } + + 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); } - }, 150); - }, [isTv, clientPlatform, updateInfo]); + } 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 () => { @@ -403,6 +484,30 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { } }, []); + // 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; @@ -415,37 +520,49 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { }, 50); const handleTvKeyDown = (e: KeyboardEvent) => { - // Cycle between "update" -> "skip" -> "exit" - if ( - e.key === "ArrowRight" || - e.key === "ArrowDown" || - e.keyCode === 22 || - e.keyCode === 20 || - e.key === "Tab" - ) { + cancelAutoCountdown(); + + // Down: switch to auto-update toggle + if (e.key === "ArrowDown" || e.keyCode === 20) { e.preventDefault(); - setTvFocusedBtn((prev) => { - if (prev === "update") return "skip"; - if (prev === "skip") return "exit"; - return "update"; - }); playTvBeep(520, 0.025); + if (tvFocusedBtn !== "autotoggle") { + setTvFocusedBtn("autotoggle"); + } return; } - if ( - e.key === "ArrowLeft" || - e.key === "ArrowUp" || - e.keyCode === 21 || - e.keyCode === 19 - ) { + // 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; } @@ -458,6 +575,9 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { } 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); @@ -475,7 +595,7 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { window.addEventListener("keydown", handleTvKeyDown); return () => window.removeEventListener("keydown", handleTvKeyDown); - }, [updateAvailable, dismissed, isTv, downloading, tvFocusedBtn, handleInstall, handleExitApp]); + }, [updateAvailable, dismissed, isTv, downloading, tvFocusedBtn, handleInstall, handleExitApp, cancelAutoCountdown, toggleAutoUpdate]); if (!updateAvailable || dismissed) return null; @@ -487,15 +607,21 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) {
{platformInfo.icon}
-

{platformInfo.title}

+

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

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

{updateInfo?.body || platformInfo.desc}

+ {autoCountdown !== null && !downloading && ( +
+ ⚡ Автообновление активно: начнется через {autoCountdown} сек... (нажмите любую кнопку для отмены) +
+ )} + {downloading ? (
@@ -505,70 +631,104 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { />
- Загрузка и установка обновления: {progress}% + {statusText}
) : ( -
- {/* Button 1: Обновить APK для ТВ */} - + <> +
+ {/* Button 1: Да, обновить сейчас */} + - {/* Button 2: Пропустить */} - + {/* Button 2: Напомнить позже */} + - {/* Button 3: Выход из приложения (Exit) */} + {/* Button 3: Выход из приложения (Exit) */} + +
+ + {/* Auto-update Toggle Chip for Android TV */} -
+ )} {error &&
{error}
}
- ◄ ► Стрелки на пульте: выбор • OK: подтвердить • Back: пропустить + ◄ ► Стрелки на пульте: выбор • ▲ ▼ Вверх/Вниз: переключение опций • OK: подтвердить • Back: позже
diff --git a/src/pages/cinema/Cinema.tsx b/src/pages/cinema/Cinema.tsx index 0371c5c..d37d971 100644 --- a/src/pages/cinema/Cinema.tsx +++ b/src/pages/cinema/Cinema.tsx @@ -704,15 +704,82 @@ export default function Cinema({ post({ command: isPlay ? "play" : "pause" }); }, []); + const setPlayerVolumeMax = useCallback((showToast: boolean = false) => { + // 1. Native HTML5 Video Element + if (videoRef.current) { + try { + videoRef.current.volume = 1.0; + videoRef.current.muted = false; + } catch {} + } + + // 2. Iframe Players (Kodik, Alloha, Playerjs, VidLink, YouTube/AutoEmbed, etc.) + const iframe = document.querySelector(".has-cinema-iframe-elem"); + if (iframe && iframe.contentWindow) { + const win = iframe.contentWindow; + const post = (payload: any) => { + try { win.postMessage(payload, "*"); } catch {} + try { win.postMessage(JSON.stringify(payload), "*"); } catch {} + }; + + // 1. Kodik API + post({ key: "kodik_player_api", value: { method: "volume", volume: 1 } }); + post({ key: "kodik_player_api", value: { method: "volume", volume: 100 } }); + post({ key: "kodik_player_api", value: { method: "unmute" } }); + + // 2. Playerjs API (Alloha, Kinoplay, Collaps, VideoCDN, Ashdi, HDRezka) + post({ api: "volume", volume: 1 }); + post({ api: "volume", volume: 100 }); + post({ api: "volume", value: 100 }); + post({ api: "unmute" }); + + // 3. Alloha API + post({ action: "volume", volume: 1 }); + post({ action: "volume", value: 100 }); + post({ action: "unmute" }); + post({ method: "volume", value: 1 }); + post({ method: "setVolume", value: 100 }); + post({ method: "unmute" }); + + // 4. VidLink Pro / Video.js API + post({ type: "VOLUME", volume: 1 }); + post({ type: "SET_VOLUME", data: { volume: 1 } }); + post({ type: "SET_VOLUME", data: { volume: 100 } }); + post({ type: "unmute" }); + post({ action: "volume", value: 1 }); + post({ action: "unmute" }); + + // 5. YouTube / VidSrc / AutoEmbed HTML5 API + post({ event: "command", func: "setVolume", args: [100] }); + post({ event: "command", func: "unMute", args: "" }); + + // 6. Generic HTML5 postMessage + post({ method: "setVolume", volume: 100 }); + post({ method: "setVolume", value: 100 }); + post({ method: "unmute" }); + post({ command: "setVolume", volume: 100 }); + post({ command: "unmute" }); + } + + if (showToast) { + showTvToast("Громкость плеера: 100%", "🔊", 1500); + } + }, [showTvToast]); + const triggerPlayerPlay = useCallback(() => { if (videoRef.current) { + try { + videoRef.current.muted = false; + videoRef.current.volume = 1.0; + } catch {} videoRef.current.play().catch(() => {}); } else { sendIframePlaybackCommand("play"); } + setPlayerVolumeMax(false); setIsPlayingState(true); showTvToast("Воспроизведение", "▶", 1200); - }, [sendIframePlaybackCommand, showTvToast]); + }, [sendIframePlaybackCommand, setPlayerVolumeMax, showTvToast]); const triggerPlayerPause = useCallback(() => { if (videoRef.current) { @@ -1467,6 +1534,10 @@ export default function Cinema({ hls.loadSource(anilibriaStreamUrl); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { + try { + video.volume = 1.0; + video.muted = false; + } catch {} video.play().catch(() => {}); }); hls.on(Hls.Events.ERROR, (_event, data) => { @@ -1570,14 +1641,19 @@ export default function Cinema({ mirrorWatchdogTimerRef.current = null; } setIsMirrorConnecting(false); - // Auto-trigger play once iframe scripts initialize + // Auto-trigger play and max volume once iframe scripts initialize setTimeout(() => { sendIframePlaybackCommand("play"); + setPlayerVolumeMax(false); }, 500); setTimeout(() => { sendIframePlaybackCommand("play"); + setPlayerVolumeMax(false); }, 1500); - }, [sendIframePlaybackCommand]); + setTimeout(() => { + setPlayerVolumeMax(false); + }, 2800); + }, [sendIframePlaybackCommand, setPlayerVolumeMax]); // Start playback for a media item (movie or specific series episode) const startPlayback = (media: CinemaMediaItem, seasonNum: number = 1, episode?: CinemaEpisode) => { @@ -1592,6 +1668,13 @@ export default function Cinema({ setShowPlayerEpisodes(false); resetHudTimer(); + if (videoRef.current) { + try { + videoRef.current.volume = 1.0; + videoRef.current.muted = false; + } catch {} + } + const ep = episode || media.seasons.find((s) => s.season_number === seasonNum)?.episodes[0] || @@ -4888,6 +4971,18 @@ export default function Cinema({ > ⏩ Пропуск опенинга (+85с) +