From 205678b3d75aa3830adba6403e468b7caef147f3 Mon Sep 17 00:00:00 2001 From: Autli Date: Thu, 29 Oct 2026 20:29:50 +0500 Subject: [PATCH] feat(release): v0.0.18 - fix autoplay, 100% volume unmute, prioritize Russian balancers and complete TV remote navigation --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/components/UpdaterBanner.tsx | 2 +- src/pages/cinema/Cinema.tsx | 238 +++++++++++++++++++------------ 5 files changed, 152 insertions(+), 94 deletions(-) diff --git a/package.json b/package.json index ae32200..f397368 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "has", "private": true, - "version": "0.0.17", + "version": "0.0.18", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f94dfbf..0006c16 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "has" -version = "0.0.17" +version = "0.0.18" description = "has" authors = ["autli"] edition = "2024" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 22eb39c..c663303 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.17", + "version": "0.0.18", "identifier": "com.has.tauri-app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/UpdaterBanner.tsx b/src/components/UpdaterBanner.tsx index 94c5a55..c56339d 100644 --- a/src/components/UpdaterBanner.tsx +++ b/src/components/UpdaterBanner.tsx @@ -88,7 +88,7 @@ const isNewerSemver = (remote: string, current: string): boolean => { export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) { const [isTv, setIsTv] = useState(() => isTvEnvironment ?? detectTvEnvironment()); - const [currentAppVersion, setCurrentAppVersion] = useState("0.0.17"); + const [currentAppVersion, setCurrentAppVersion] = useState("0.0.18"); const [updateAvailable, setUpdateAvailable] = useState(false); const [updateInfo, setUpdateInfo] = useState(null); const [downloading, setDownloading] = useState(false); diff --git a/src/pages/cinema/Cinema.tsx b/src/pages/cinema/Cinema.tsx index a4d9436..756ac00 100644 --- a/src/pages/cinema/Cinema.tsx +++ b/src/pages/cinema/Cinema.tsx @@ -760,12 +760,21 @@ export default function Cinema({ broadcastToWindowTree({ action: "unmute" }); broadcastToWindowTree({ method: "volume", value: 1 }); broadcastToWindowTree({ method: "setVolume", value: 100 }); + broadcastToWindowTree({ method: "setVolume", volume: 100 }); broadcastToWindowTree({ method: "unmute" }); + broadcastToWindowTree({ method: "unMute" }); - // 5. VidLink Pro / Video.js API - broadcastToWindowTree({ type: "VOLUME", volume: 1 }); - broadcastToWindowTree({ type: "SET_VOLUME", data: { volume: 1 } }); + // 5. JW Player / VidLink Pro / Video.js API + broadcastToWindowTree({ method: "setMute", value: false }); + broadcastToWindowTree({ method: "setMute", mute: false }); + broadcastToWindowTree({ method: "setVolume", value: 100 }); + broadcastToWindowTree({ method: "setVolume", volume: 100 }); + broadcastToWindowTree({ action: "setMute", value: false }); + broadcastToWindowTree({ action: "setVolume", value: 100 }); + broadcastToWindowTree({ command: "setMute", value: false }); + broadcastToWindowTree({ type: "SET_VOLUME", volume: 100 }); broadcastToWindowTree({ type: "SET_VOLUME", data: { volume: 100 } }); + broadcastToWindowTree({ type: "UNMUTE" }); broadcastToWindowTree({ type: "unmute" }); broadcastToWindowTree({ action: "volume", value: 1 }); broadcastToWindowTree({ action: "unmute" }); @@ -778,6 +787,7 @@ export default function Cinema({ broadcastToWindowTree({ method: "setVolume", volume: 100 }); broadcastToWindowTree({ method: "setVolume", value: 100 }); broadcastToWindowTree({ method: "unmute" }); + broadcastToWindowTree({ method: "unMute" }); broadcastToWindowTree({ command: "setVolume", volume: 100 }); broadcastToWindowTree({ command: "unmute" }); @@ -795,22 +805,17 @@ export default function Cinema({ videoRef.current.play().catch(() => {}); } else { sendIframePlaybackCommand("play"); - // Awaken iframe user activation without permanently trapping focus const iframe = document.querySelector(".has-cinema-iframe-elem"); if (iframe) { try { iframe.focus(); } catch {} - setTimeout(() => { - const playBtn = document.querySelector(".has-cinema-player-ctrl-btn.primary"); - if (playBtn) playBtn.focus(); - }, 120); } } // Установка громкости 100% каждый раз при воспроизведении setPlayerVolumeMax(false); - setTimeout(() => setPlayerVolumeMax(false), 500); - setTimeout(() => setPlayerVolumeMax(false), 1500); + setTimeout(() => setPlayerVolumeMax(false), 400); + setTimeout(() => setPlayerVolumeMax(false), 1200); setIsPlayingState(true); showTvToast("Воспроизведение", "▶", 1200); }, [sendIframePlaybackCommand, setPlayerVolumeMax, showTvToast]); @@ -1126,16 +1131,21 @@ export default function Cinema({ } } - // Also query Russian media info for Kinopoisk ID / rating + // Also query Russian media info for Kinopoisk ID / rating & pre-resolve stream if (isTauri && item.tmdb_id) { invoke("get_russian_media_info", { tmdbId: item.tmdb_id, isTv: item.media_type === "tv", title: item.title || item.original_title, }).then((info) => { - if (info?.kp_id) { - setDetailsMedia((prev) => (prev && prev.id === item.id ? { ...prev, kinopoisk_rating: info.kp_id ? "КП" : undefined } : prev)); - setSelectedMedia((prev) => (prev && prev.id === item.id ? { ...prev, kinopoisk_rating: info.kp_id ? "КП" : undefined } : prev)); + if (info) { + if (info.kp_id) setCurrentKpId(Number(info.kp_id)); + if (info.imdb_id) setCurrentImdbId(info.imdb_id); + if (info.iframe_url) setCurrentAllohaUrl(info.iframe_url); + if (info.kp_id) { + setDetailsMedia((prev) => (prev && prev.id === item.id ? { ...prev, kinopoisk_rating: info.kp_id ? "КП" : undefined } : prev)); + setSelectedMedia((prev) => (prev && prev.id === item.id ? { ...prev, kinopoisk_rating: info.kp_id ? "КП" : undefined } : prev)); + } } }).catch(() => {}); } @@ -1762,11 +1772,7 @@ export default function Cinema({ const knownKp = media.tmdb_id ? KNOWN_KP_MAP[media.tmdb_id] : null; if (knownKp) { setCurrentKpId(knownKp); - } else { - setCurrentKpId(null); } - setCurrentImdbId(null); - setCurrentAllohaUrl(null); // Reset mirror tracking and watchdog for fresh playback session triedMirrorsRef.current.clear(); @@ -1780,20 +1786,27 @@ export default function Cinema({ handleAutoSwitchNextMirror("Сервер не ответил за 8 секунд (автопереключение)"); }, 8000); - // Default to verified reliable high-speed 1080p stream: + // Prioritize reliable Russian servers with full remote control and working audio: if (isAnime) { if (anilibriaStreamUrl) { setSelectedMirror("anilibria"); } else if (kodikResults.length > 0) { setSelectedMirror("kodik_ru"); } else { - setSelectedMirror("vidlink_pro"); + setSelectedMirror("kodik_ru"); } showTvToast("🌸 Сервер: Kodik (Аниме / Озвучка)", "🎙️", 1800); } else { - // Default to VidLink Pro (ultra reliable, resolves all TMDB movies/series in 1080p) - setSelectedMirror("vidlink_pro"); - showTvToast("⚡ Запущен Сервер 1 (Full HD 1080p)", "🎬", 1800); + if (currentAllohaUrl || knownKp || currentKpId) { + setSelectedMirror("alloha_ru"); + showTvToast("🔊 Сервер: Alloha (Русский дубляж)", "🎬", 1800); + } else if (kodikResults.length > 0) { + setSelectedMirror("kodik_ru"); + showTvToast("🎙️ Сервер: Kodik (Студии дубляжа)", "🎬", 1800); + } else { + setSelectedMirror("alloha_ru"); + showTvToast("⚡ Запуск плеера Full HD", "🎬", 1800); + } } // Resolve Kinopoisk ID & Russian media data asynchronously via Rust backend or web fetch @@ -1815,6 +1828,13 @@ export default function Cinema({ clearTimeout(mirrorWatchdogTimerRef.current); mirrorWatchdogTimerRef.current = null; } + } else if (info.kp_id) { + setSelectedMirror("alloha_ru"); + setIsMirrorConnecting(false); + if (mirrorWatchdogTimerRef.current) { + clearTimeout(mirrorWatchdogTimerRef.current); + mirrorWatchdogTimerRef.current = null; + } } if (!isAnime) { setRussianDubNotice( @@ -2840,28 +2860,38 @@ export default function Cinema({ }; }, [handleRemoteKeyDown]); - // Guard TV remote focus: Never let iframe capture or trap remote control keys + // Auto-focus first interactive element when player settings drawer opens useEffect(() => { - if (playingEpisode !== null) { - const handleWindowBlur = () => { - setTimeout(() => { - if (document.activeElement?.tagName === "IFRAME") { - (document.activeElement as HTMLElement).blur(); - } - window.focus(); - const target = - document.querySelector(".has-cinema-player-ctrl-btn.primary") || - document.querySelector(".has-cinema-player-btn-back"); - if (target) { - target.focus(); - } - }, 30); - }; - - window.addEventListener("blur", handleWindowBlur); - return () => window.removeEventListener("blur", handleWindowBlur); + if (showPlayerSettings) { + const timer = setTimeout(() => { + const firstBtn = + document.querySelector(".has-cinema-player-settings-sheet .has-cinema-drawer-chip.primary") || + document.querySelector(".has-cinema-player-settings-sheet .has-cinema-drawer-chip") || + document.querySelector(".has-cinema-player-settings-sheet [data-tv-focusable='true']"); + if (firstBtn) { + firstBtn.focus(); + } + }, 50); + return () => clearTimeout(timer); } - }, [playingEpisode]); + }, [showPlayerSettings]); + + // Auto-focus current episode when episodes drawer opens + useEffect(() => { + if (showPlayerEpisodes) { + const timer = setTimeout(() => { + const activeEp = + document.querySelector(".has-cinema-player-episodes-sheet .has-cinema-drawer-ep-btn.active") || + document.querySelector(".has-cinema-player-episodes-sheet .has-cinema-drawer-ep-btn") || + document.querySelector(".has-cinema-player-episodes-sheet [data-tv-focusable='true']"); + if (activeEp) { + activeEp.focus(); + activeEp.scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" }); + } + }, 50); + return () => clearTimeout(timer); + } + }, [showPlayerEpisodes]); // Handle Android TV hardware back & browser popstate to guarantee return to movie details card useEffect(() => { @@ -3088,24 +3118,16 @@ export default function Cinema({ setKodikResults(finalDubbings); setSelectedKodikLink(finalDubbings[0].link); if (autoSwitch) { - setSelectedMirror("kodik_ru"); + setSelectedMirror((prev) => (prev !== "alloha_ru" && prev !== "anilibria" ? "kodik_ru" : prev)); setRussianDubNotice(`🇷🇺 Найдено ${finalDubbings.length} студий русской озвучки для «${targetTitle}».`); } } else { setKodikResults([]); setSelectedKodikLink(null); - if (autoSwitch) { - setSelectedMirror("vidlink_pro"); - setRussianDubNotice("🇷🇺 Для данного фильма русская аудиодорожка встроена в плеер VidLink Pro (нажмите на шестеренку ⚙️ или значок аудио в правом нижнем углу плеера)."); - } } } catch { setKodikResults([]); setSelectedKodikLink(null); - if (autoSwitch) { - setSelectedMirror("vidlink_pro"); - setRussianDubNotice("🌍 Переключено на основной плеер VidLink Pro."); - } } finally { setIsSearchingKodik(false); } @@ -3137,8 +3159,8 @@ export default function Cinema({ switch (selectedMirror) { case "vidlink_pro": return isTv - ? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=true&primaryColor=6366f1` - : `https://vidlink.pro/movie/${tmdb || 693134}?autoplay=true&primaryColor=6366f1`; + ? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=1&muted=0&volume=100&primaryColor=6366f1` + : `https://vidlink.pro/movie/${tmdb || 693134}?autoplay=1&muted=0&volume=100&primaryColor=6366f1`; case "kodik_ru": if (selectedKodikLink) { @@ -3149,9 +3171,16 @@ export default function Cinema({ const l = kodikResults[0].link.startsWith("//") ? `https:${kodikResults[0].link}` : kodikResults[0].link; return appendAutoplay(l); } + if (currentAllohaUrl) { + const l = currentAllohaUrl.startsWith("//") ? `https:${currentAllohaUrl}` : currentAllohaUrl; + return appendAutoplay(l); + } + if (kpId) { + return `https://theatre.stravers.live/?kp=${kpId}&autoplay=1`; + } return isTv - ? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=true&primaryColor=6366f1` - : `https://vidlink.pro/movie/${tmdb || 693134}?autoplay=true&primaryColor=6366f1`; + ? `https://player.autoembed.co/embed/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=1` + : `https://player.autoembed.co/embed/movie/${tmdb || 693134}?autoplay=1`; case "autoembed": return isTv @@ -3184,14 +3213,24 @@ export default function Cinema({ if (imdbId) { return `https://theatre.stravers.live/?imdb=${imdbId}&autoplay=1`; } + if (tmdb) { + return `https://theatre.stravers.live/?tmdb=${tmdb}&autoplay=1`; + } return isTv - ? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=true&primaryColor=6366f1` - : `https://vidlink.pro/movie/${tmdb || 693134}?autoplay=true&primaryColor=6366f1`; + ? `https://player.autoembed.co/embed/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=1` + : `https://player.autoembed.co/embed/movie/${tmdb || 693134}?autoplay=1`; default: + if (currentAllohaUrl) { + const l = currentAllohaUrl.startsWith("//") ? `https:${currentAllohaUrl}` : currentAllohaUrl; + return appendAutoplay(l); + } + if (kpId) { + return `https://theatre.stravers.live/?kp=${kpId}&autoplay=1`; + } return isTv - ? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=true&primaryColor=6366f1` - : `https://vidlink.pro/movie/${tmdb || 693134}?autoplay=true&primaryColor=6366f1`; + ? `https://player.autoembed.co/embed/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=1` + : `https://player.autoembed.co/embed/movie/${tmdb || 693134}?autoplay=1`; } }; @@ -4884,6 +4923,33 @@ export default function Cinema({ ⏩ +10с + + + + {(playingMedia || selectedMedia).media_type !== "movie" && ( - {currentAllohaUrl && ( - - )} + {/* Anime-specific AniLibria HLS stream */} {((playingMedia || selectedMedia).genre.toLowerCase().includes("аниме") ||