Compare commits
No commits in common. "main" and "v0.0.17" have entirely different histories.
7 changed files with 164 additions and 244 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "has",
|
"name": "has",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.19",
|
"version": "0.0.17",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "has"
|
name = "has"
|
||||||
version = "0.0.19"
|
version = "0.0.17"
|
||||||
description = "has"
|
description = "has"
|
||||||
authors = ["autli"]
|
authors = ["autli"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "has",
|
"productName": "has",
|
||||||
"version": "0.0.19",
|
"version": "0.0.17",
|
||||||
"identifier": "com.has.tauri-app",
|
"identifier": "com.has.tauri-app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|
|
||||||
31
src/App.tsx
31
src/App.tsx
|
|
@ -27,6 +27,8 @@ const isTauri = () => {
|
||||||
// Helper for Smart TV / Android TV detection
|
// Helper for Smart TV / Android TV detection
|
||||||
const detectTvEnvironment = (): boolean => {
|
const detectTvEnvironment = (): boolean => {
|
||||||
if (typeof window === "undefined") return false;
|
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 ua = (navigator.userAgent || "").toLowerCase();
|
||||||
const isTvUa =
|
const isTvUa =
|
||||||
ua.includes("android tv") ||
|
ua.includes("android tv") ||
|
||||||
|
|
@ -53,17 +55,6 @@ const detectTvEnvironment = (): boolean => {
|
||||||
const isAndroid = ua.includes("android");
|
const isAndroid = ua.includes("android");
|
||||||
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
|
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
|
||||||
if (isAndroid && noTouch) return true;
|
if (isAndroid && noTouch) return true;
|
||||||
|
|
||||||
// On pure desktop (macOS / Windows / Desktop Linux), default to desktop PC mode
|
|
||||||
const isDesktop =
|
|
||||||
(ua.includes("windows nt") || ua.includes("macintosh") || (ua.includes("linux") && !ua.includes("android"))) &&
|
|
||||||
!isTvUa;
|
|
||||||
if (isDesktop) {
|
|
||||||
return localStorage.getItem("has_tv_mode_manual") === "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
const saved = localStorage.getItem("has_tv_mode");
|
|
||||||
if (saved !== null) return saved === "true";
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -153,6 +144,21 @@ export default function App() {
|
||||||
checkDatabase();
|
checkDatabase();
|
||||||
}, [isTvEnvironment]);
|
}, [isTvEnvironment]);
|
||||||
|
|
||||||
|
// Activate TV mode immediately upon any remote control key press
|
||||||
|
useEffect(() => {
|
||||||
|
const handleRemoteKey = (e: KeyboardEvent) => {
|
||||||
|
const isRemoteKey =
|
||||||
|
[19, 20, 21, 22, 23, 4, 82, 85, 179].includes(e.keyCode) ||
|
||||||
|
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key);
|
||||||
|
if (isRemoteKey && !isTvEnvironment) {
|
||||||
|
setIsTvEnvironment(true);
|
||||||
|
localStorage.setItem("has_tv_mode", "true");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleRemoteKey);
|
||||||
|
return () => window.removeEventListener("keydown", handleRemoteKey);
|
||||||
|
}, [isTvEnvironment]);
|
||||||
|
|
||||||
// Countdown timer for OTP re-send in resetStage 2
|
// Countdown timer for OTP re-send in resetStage 2
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (resetStage !== 2) return;
|
if (resetStage !== 2) return;
|
||||||
|
|
@ -519,7 +525,6 @@ export default function App() {
|
||||||
dbInfo={dbInfo}
|
dbInfo={dbInfo}
|
||||||
dbStatus={dbStatus}
|
dbStatus={dbStatus}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
isTvEnvironment={isTvEnvironment}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -537,7 +542,6 @@ export default function App() {
|
||||||
onSwitchToDesktop={() => {
|
onSwitchToDesktop={() => {
|
||||||
setIsTvEnvironment(false);
|
setIsTvEnvironment(false);
|
||||||
localStorage.setItem("has_tv_mode", "false");
|
localStorage.setItem("has_tv_mode", "false");
|
||||||
localStorage.setItem("has_tv_mode_manual", "false");
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
@ -808,7 +812,6 @@ export default function App() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsTvEnvironment(true);
|
setIsTvEnvironment(true);
|
||||||
localStorage.setItem("has_tv_mode", "true");
|
localStorage.setItem("has_tv_mode", "true");
|
||||||
localStorage.setItem("has_tv_mode_manual", "true");
|
|
||||||
}}
|
}}
|
||||||
title="Перейти в режим Android TV (выбор профилей)"
|
title="Перейти в режим Android TV (выбор профилей)"
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ interface UpdaterBannerProps {
|
||||||
// Helper for Smart TV / Android TV detection
|
// Helper for Smart TV / Android TV detection
|
||||||
const detectTvEnvironment = (): boolean => {
|
const detectTvEnvironment = (): boolean => {
|
||||||
if (typeof window === "undefined") return false;
|
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 ua = (navigator.userAgent || "").toLowerCase();
|
||||||
const isTvUa =
|
const isTvUa =
|
||||||
ua.includes("android tv") ||
|
ua.includes("android tv") ||
|
||||||
|
|
@ -48,17 +50,6 @@ const detectTvEnvironment = (): boolean => {
|
||||||
const isAndroid = ua.includes("android");
|
const isAndroid = ua.includes("android");
|
||||||
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
|
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
|
||||||
if (isAndroid && noTouch) return true;
|
if (isAndroid && noTouch) return true;
|
||||||
|
|
||||||
// On pure desktop (macOS / Windows / Desktop Linux), default to desktop PC mode
|
|
||||||
const isDesktop =
|
|
||||||
(ua.includes("windows nt") || ua.includes("macintosh") || (ua.includes("linux") && !ua.includes("android"))) &&
|
|
||||||
!isTvUa;
|
|
||||||
if (isDesktop) {
|
|
||||||
return localStorage.getItem("has_tv_mode_manual") === "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
const saved = localStorage.getItem("has_tv_mode");
|
|
||||||
if (saved !== null) return saved === "true";
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -97,7 +88,7 @@ const isNewerSemver = (remote: string, current: string): boolean => {
|
||||||
|
|
||||||
export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) {
|
export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) {
|
||||||
const [isTv, setIsTv] = useState<boolean>(() => isTvEnvironment ?? detectTvEnvironment());
|
const [isTv, setIsTv] = useState<boolean>(() => isTvEnvironment ?? detectTvEnvironment());
|
||||||
const [currentAppVersion, setCurrentAppVersion] = useState<string>("0.0.19");
|
const [currentAppVersion, setCurrentAppVersion] = useState<string>("0.0.17");
|
||||||
const [updateAvailable, setUpdateAvailable] = useState(false);
|
const [updateAvailable, setUpdateAvailable] = useState(false);
|
||||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||||
const [downloading, setDownloading] = useState(false);
|
const [downloading, setDownloading] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -493,6 +493,8 @@ const formatMediaImg = (path: string | undefined, size: "w500" | "original" = "w
|
||||||
// Detect Android TV / Smart TV user agent or previous user preference
|
// Detect Android TV / Smart TV user agent or previous user preference
|
||||||
const detectTvEnvironment = (): boolean => {
|
const detectTvEnvironment = (): boolean => {
|
||||||
if (typeof window === "undefined") return false;
|
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 ua = (navigator.userAgent || "").toLowerCase();
|
||||||
const isTvUa =
|
const isTvUa =
|
||||||
ua.includes("android tv") ||
|
ua.includes("android tv") ||
|
||||||
|
|
@ -519,17 +521,6 @@ const detectTvEnvironment = (): boolean => {
|
||||||
const isAndroid = ua.includes("android");
|
const isAndroid = ua.includes("android");
|
||||||
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
|
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
|
||||||
if (isAndroid && noTouch) return true;
|
if (isAndroid && noTouch) return true;
|
||||||
|
|
||||||
// On pure desktop (macOS / Windows / Desktop Linux), default to desktop PC mode
|
|
||||||
const isDesktop =
|
|
||||||
(ua.includes("windows nt") || ua.includes("macintosh") || (ua.includes("linux") && !ua.includes("android"))) &&
|
|
||||||
!isTvUa;
|
|
||||||
if (isDesktop) {
|
|
||||||
return localStorage.getItem("has_tv_mode_manual") === "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
const saved = localStorage.getItem("has_tv_mode");
|
|
||||||
if (saved !== null) return saved === "true";
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -549,10 +540,10 @@ export default function Cinema({
|
||||||
const lastFocusedCardRef = useRef<HTMLElement | null>(null);
|
const lastFocusedCardRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTvEnvironment !== isTvMode) {
|
if (isTvEnvironment && !isTvMode) {
|
||||||
setIsTvMode(isTvEnvironment);
|
setIsTvMode(true);
|
||||||
}
|
}
|
||||||
}, [isTvEnvironment]);
|
}, [isTvEnvironment, isTvMode]);
|
||||||
const [tvToast, setTvToast] = useState<{ text: string; icon?: string } | null>(null);
|
const [tvToast, setTvToast] = useState<{ text: string; icon?: string } | null>(null);
|
||||||
const tvToastTimerRef = useRef<any>(null);
|
const tvToastTimerRef = useRef<any>(null);
|
||||||
|
|
||||||
|
|
@ -572,7 +563,6 @@ export default function Cinema({
|
||||||
setIsTvMode((prev) => {
|
setIsTvMode((prev) => {
|
||||||
const next = !prev;
|
const next = !prev;
|
||||||
localStorage.setItem("has_tv_mode", String(next));
|
localStorage.setItem("has_tv_mode", String(next));
|
||||||
localStorage.setItem("has_tv_mode_manual", String(next));
|
|
||||||
showTvToast(next ? "ТВ-Режим включен (10-Foot UI)" : "ПК-Режим включен", "📺", 2500);
|
showTvToast(next ? "ТВ-Режим включен (10-Foot UI)" : "ПК-Режим включен", "📺", 2500);
|
||||||
playTvBeep(next ? 750 : 400, 0.05);
|
playTvBeep(next ? 750 : 400, 0.05);
|
||||||
return next;
|
return next;
|
||||||
|
|
@ -770,21 +760,12 @@ export default function Cinema({
|
||||||
broadcastToWindowTree({ action: "unmute" });
|
broadcastToWindowTree({ action: "unmute" });
|
||||||
broadcastToWindowTree({ method: "volume", value: 1 });
|
broadcastToWindowTree({ method: "volume", value: 1 });
|
||||||
broadcastToWindowTree({ method: "setVolume", value: 100 });
|
broadcastToWindowTree({ method: "setVolume", value: 100 });
|
||||||
broadcastToWindowTree({ method: "setVolume", volume: 100 });
|
|
||||||
broadcastToWindowTree({ method: "unmute" });
|
broadcastToWindowTree({ method: "unmute" });
|
||||||
broadcastToWindowTree({ method: "unMute" });
|
|
||||||
|
|
||||||
// 5. JW Player / VidLink Pro / Video.js API
|
// 5. VidLink Pro / Video.js API
|
||||||
broadcastToWindowTree({ method: "setMute", value: false });
|
broadcastToWindowTree({ type: "VOLUME", volume: 1 });
|
||||||
broadcastToWindowTree({ method: "setMute", mute: false });
|
broadcastToWindowTree({ type: "SET_VOLUME", data: { volume: 1 } });
|
||||||
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: "SET_VOLUME", data: { volume: 100 } });
|
||||||
broadcastToWindowTree({ type: "UNMUTE" });
|
|
||||||
broadcastToWindowTree({ type: "unmute" });
|
broadcastToWindowTree({ type: "unmute" });
|
||||||
broadcastToWindowTree({ action: "volume", value: 1 });
|
broadcastToWindowTree({ action: "volume", value: 1 });
|
||||||
broadcastToWindowTree({ action: "unmute" });
|
broadcastToWindowTree({ action: "unmute" });
|
||||||
|
|
@ -797,7 +778,6 @@ export default function Cinema({
|
||||||
broadcastToWindowTree({ method: "setVolume", volume: 100 });
|
broadcastToWindowTree({ method: "setVolume", volume: 100 });
|
||||||
broadcastToWindowTree({ method: "setVolume", value: 100 });
|
broadcastToWindowTree({ method: "setVolume", value: 100 });
|
||||||
broadcastToWindowTree({ method: "unmute" });
|
broadcastToWindowTree({ method: "unmute" });
|
||||||
broadcastToWindowTree({ method: "unMute" });
|
|
||||||
broadcastToWindowTree({ command: "setVolume", volume: 100 });
|
broadcastToWindowTree({ command: "setVolume", volume: 100 });
|
||||||
broadcastToWindowTree({ command: "unmute" });
|
broadcastToWindowTree({ command: "unmute" });
|
||||||
|
|
||||||
|
|
@ -815,17 +795,22 @@ export default function Cinema({
|
||||||
videoRef.current.play().catch(() => {});
|
videoRef.current.play().catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
sendIframePlaybackCommand("play");
|
sendIframePlaybackCommand("play");
|
||||||
|
// Awaken iframe user activation without permanently trapping focus
|
||||||
const iframe = document.querySelector<HTMLIFrameElement>(".has-cinema-iframe-elem");
|
const iframe = document.querySelector<HTMLIFrameElement>(".has-cinema-iframe-elem");
|
||||||
if (iframe) {
|
if (iframe) {
|
||||||
try {
|
try {
|
||||||
iframe.focus();
|
iframe.focus();
|
||||||
} catch {}
|
} catch {}
|
||||||
|
setTimeout(() => {
|
||||||
|
const playBtn = document.querySelector<HTMLElement>(".has-cinema-player-ctrl-btn.primary");
|
||||||
|
if (playBtn) playBtn.focus();
|
||||||
|
}, 120);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Установка громкости 100% каждый раз при воспроизведении
|
// Установка громкости 100% каждый раз при воспроизведении
|
||||||
setPlayerVolumeMax(false);
|
setPlayerVolumeMax(false);
|
||||||
setTimeout(() => setPlayerVolumeMax(false), 400);
|
setTimeout(() => setPlayerVolumeMax(false), 500);
|
||||||
setTimeout(() => setPlayerVolumeMax(false), 1200);
|
setTimeout(() => setPlayerVolumeMax(false), 1500);
|
||||||
setIsPlayingState(true);
|
setIsPlayingState(true);
|
||||||
showTvToast("Воспроизведение", "▶", 1200);
|
showTvToast("Воспроизведение", "▶", 1200);
|
||||||
}, [sendIframePlaybackCommand, setPlayerVolumeMax, showTvToast]);
|
}, [sendIframePlaybackCommand, setPlayerVolumeMax, showTvToast]);
|
||||||
|
|
@ -1141,22 +1126,17 @@ export default function Cinema({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also query Russian media info for Kinopoisk ID / rating & pre-resolve stream
|
// Also query Russian media info for Kinopoisk ID / rating
|
||||||
if (isTauri && item.tmdb_id) {
|
if (isTauri && item.tmdb_id) {
|
||||||
invoke<RussianMediaInfo>("get_russian_media_info", {
|
invoke<RussianMediaInfo>("get_russian_media_info", {
|
||||||
tmdbId: item.tmdb_id,
|
tmdbId: item.tmdb_id,
|
||||||
isTv: item.media_type === "tv",
|
isTv: item.media_type === "tv",
|
||||||
title: item.title || item.original_title,
|
title: item.title || item.original_title,
|
||||||
}).then((info) => {
|
}).then((info) => {
|
||||||
if (info) {
|
if (info?.kp_id) {
|
||||||
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));
|
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));
|
setSelectedMedia((prev) => (prev && prev.id === item.id ? { ...prev, kinopoisk_rating: info.kp_id ? "КП" : undefined } : prev));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -1782,7 +1762,11 @@ export default function Cinema({
|
||||||
const knownKp = media.tmdb_id ? KNOWN_KP_MAP[media.tmdb_id] : null;
|
const knownKp = media.tmdb_id ? KNOWN_KP_MAP[media.tmdb_id] : null;
|
||||||
if (knownKp) {
|
if (knownKp) {
|
||||||
setCurrentKpId(knownKp);
|
setCurrentKpId(knownKp);
|
||||||
|
} else {
|
||||||
|
setCurrentKpId(null);
|
||||||
}
|
}
|
||||||
|
setCurrentImdbId(null);
|
||||||
|
setCurrentAllohaUrl(null);
|
||||||
|
|
||||||
// Reset mirror tracking and watchdog for fresh playback session
|
// Reset mirror tracking and watchdog for fresh playback session
|
||||||
triedMirrorsRef.current.clear();
|
triedMirrorsRef.current.clear();
|
||||||
|
|
@ -1796,27 +1780,20 @@ export default function Cinema({
|
||||||
handleAutoSwitchNextMirror("Сервер не ответил за 8 секунд (автопереключение)");
|
handleAutoSwitchNextMirror("Сервер не ответил за 8 секунд (автопереключение)");
|
||||||
}, 8000);
|
}, 8000);
|
||||||
|
|
||||||
// Prioritize reliable Russian servers with full remote control and working audio:
|
// Default to verified reliable high-speed 1080p stream:
|
||||||
if (isAnime) {
|
if (isAnime) {
|
||||||
if (anilibriaStreamUrl) {
|
if (anilibriaStreamUrl) {
|
||||||
setSelectedMirror("anilibria");
|
setSelectedMirror("anilibria");
|
||||||
} else if (kodikResults.length > 0) {
|
} else if (kodikResults.length > 0) {
|
||||||
setSelectedMirror("kodik_ru");
|
setSelectedMirror("kodik_ru");
|
||||||
} else {
|
} else {
|
||||||
setSelectedMirror("kodik_ru");
|
setSelectedMirror("vidlink_pro");
|
||||||
}
|
}
|
||||||
showTvToast("🌸 Сервер: Kodik (Аниме / Озвучка)", "🎙️", 1800);
|
showTvToast("🌸 Сервер: Kodik (Аниме / Озвучка)", "🎙️", 1800);
|
||||||
} else {
|
} else {
|
||||||
if (currentAllohaUrl || knownKp || currentKpId) {
|
// Default to VidLink Pro (ultra reliable, resolves all TMDB movies/series in 1080p)
|
||||||
setSelectedMirror("alloha_ru");
|
setSelectedMirror("vidlink_pro");
|
||||||
showTvToast("🔊 Сервер: Alloha (Русский дубляж)", "🎬", 1800);
|
showTvToast("⚡ Запущен Сервер 1 (Full HD 1080p)", "🎬", 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
|
// Resolve Kinopoisk ID & Russian media data asynchronously via Rust backend or web fetch
|
||||||
|
|
@ -1838,13 +1815,6 @@ export default function Cinema({
|
||||||
clearTimeout(mirrorWatchdogTimerRef.current);
|
clearTimeout(mirrorWatchdogTimerRef.current);
|
||||||
mirrorWatchdogTimerRef.current = null;
|
mirrorWatchdogTimerRef.current = null;
|
||||||
}
|
}
|
||||||
} else if (info.kp_id) {
|
|
||||||
setSelectedMirror("alloha_ru");
|
|
||||||
setIsMirrorConnecting(false);
|
|
||||||
if (mirrorWatchdogTimerRef.current) {
|
|
||||||
clearTimeout(mirrorWatchdogTimerRef.current);
|
|
||||||
mirrorWatchdogTimerRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!isAnime) {
|
if (!isAnime) {
|
||||||
setRussianDubNotice(
|
setRussianDubNotice(
|
||||||
|
|
@ -2260,18 +2230,40 @@ export default function Cinema({
|
||||||
// D-Pad Spatial Navigation & TV Remote Key Handler
|
// D-Pad Spatial Navigation & TV Remote Key Handler
|
||||||
const handleRemoteKeyDown = useCallback(
|
const handleRemoteKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
|
// Auto-activate TV mode immediately on ANY remote control button press
|
||||||
|
const isRemoteKey =
|
||||||
|
[19, 20, 21, 22, 23, 4, 82, 85, 179].includes(e.keyCode) ||
|
||||||
|
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key);
|
||||||
|
if (isRemoteKey && !isTvMode) {
|
||||||
|
setIsTvMode(true);
|
||||||
|
localStorage.setItem("has_tv_mode", "true");
|
||||||
|
}
|
||||||
|
|
||||||
const isInput =
|
const isInput =
|
||||||
document.activeElement instanceof HTMLInputElement ||
|
document.activeElement instanceof HTMLInputElement ||
|
||||||
document.activeElement instanceof HTMLTextAreaElement;
|
document.activeElement instanceof HTMLTextAreaElement;
|
||||||
|
|
||||||
// Remote Menu key (ONLY in TV Mode, and NEVER for the 'R' key on PC keyboard!)
|
// Android TV / Remote key mappings
|
||||||
const isMenuKey =
|
const isBack =
|
||||||
!isInput &&
|
e.key === "Escape" ||
|
||||||
(e.key === "Menu" ||
|
e.key === "GoBack" ||
|
||||||
e.key === "ContextMenu" ||
|
e.key === "BrowserBack" ||
|
||||||
(isTvMode && e.keyCode === 82 && e.key !== "r" && e.key !== "R" && e.code !== "KeyR"));
|
e.keyCode === 4 ||
|
||||||
|
e.keyCode === 27;
|
||||||
|
const isOk =
|
||||||
|
e.key === "Enter" ||
|
||||||
|
e.key === "Select" ||
|
||||||
|
e.key === "Accept" ||
|
||||||
|
e.keyCode === 13 ||
|
||||||
|
e.keyCode === 23 ||
|
||||||
|
e.keyCode === 66;
|
||||||
|
const isPlayPause =
|
||||||
|
e.key === "MediaPlayPause" || e.keyCode === 85 || e.keyCode === 179;
|
||||||
|
const isFastForward = e.key === "MediaFastForward" || e.keyCode === 90;
|
||||||
|
const isRewind = e.key === "MediaRewind" || e.keyCode === 89;
|
||||||
|
|
||||||
if (isTvMode && isMenuKey) {
|
// Remote Menu key (Android TV remote Menu button / ContextMenu)
|
||||||
|
if (e.key === "Menu" || e.keyCode === 82 || e.key === "ContextMenu") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsTvNavOpen((prev) => {
|
setIsTvNavOpen((prev) => {
|
||||||
const next = !prev;
|
const next = !prev;
|
||||||
|
|
@ -2298,29 +2290,6 @@ export default function Cinema({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Android TV / Remote key mappings
|
|
||||||
const isBack =
|
|
||||||
e.key === "Escape" ||
|
|
||||||
e.key === "GoBack" ||
|
|
||||||
e.key === "BrowserBack" ||
|
|
||||||
(isTvMode && e.keyCode === 4) ||
|
|
||||||
e.keyCode === 27;
|
|
||||||
const isOk =
|
|
||||||
e.key === "Enter" ||
|
|
||||||
e.key === "Select" ||
|
|
||||||
e.key === "Accept" ||
|
|
||||||
e.keyCode === 13 ||
|
|
||||||
(isTvMode && (e.keyCode === 23 || (e.keyCode === 66 && e.code !== "KeyB")));
|
|
||||||
const isPlayPause =
|
|
||||||
e.key === "MediaPlayPause" ||
|
|
||||||
(isTvMode && (e.keyCode === 85 || e.keyCode === 179) && e.code !== "KeyU");
|
|
||||||
const isFastForward =
|
|
||||||
e.key === "MediaFastForward" ||
|
|
||||||
(isTvMode && e.keyCode === 90 && e.code !== "KeyZ");
|
|
||||||
const isRewind =
|
|
||||||
e.key === "MediaRewind" ||
|
|
||||||
(isTvMode && e.keyCode === 89 && e.code !== "KeyY");
|
|
||||||
|
|
||||||
// 1. In-player remote control
|
// 1. In-player remote control
|
||||||
if (playingEpisode !== null) {
|
if (playingEpisode !== null) {
|
||||||
resetHudTimer();
|
resetHudTimer();
|
||||||
|
|
@ -2871,38 +2840,28 @@ export default function Cinema({
|
||||||
};
|
};
|
||||||
}, [handleRemoteKeyDown]);
|
}, [handleRemoteKeyDown]);
|
||||||
|
|
||||||
// Auto-focus first interactive element when player settings drawer opens
|
// Guard TV remote focus: Never let iframe capture or trap remote control keys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showPlayerSettings) {
|
if (playingEpisode !== null) {
|
||||||
const timer = setTimeout(() => {
|
const handleWindowBlur = () => {
|
||||||
const firstBtn =
|
setTimeout(() => {
|
||||||
document.querySelector<HTMLElement>(".has-cinema-player-settings-sheet .has-cinema-drawer-chip.primary") ||
|
if (document.activeElement?.tagName === "IFRAME") {
|
||||||
document.querySelector<HTMLElement>(".has-cinema-player-settings-sheet .has-cinema-drawer-chip") ||
|
(document.activeElement as HTMLElement).blur();
|
||||||
document.querySelector<HTMLElement>(".has-cinema-player-settings-sheet [data-tv-focusable='true']");
|
|
||||||
if (firstBtn) {
|
|
||||||
firstBtn.focus();
|
|
||||||
}
|
}
|
||||||
}, 50);
|
window.focus();
|
||||||
return () => clearTimeout(timer);
|
const target =
|
||||||
|
document.querySelector<HTMLElement>(".has-cinema-player-ctrl-btn.primary") ||
|
||||||
|
document.querySelector<HTMLElement>(".has-cinema-player-btn-back");
|
||||||
|
if (target) {
|
||||||
|
target.focus();
|
||||||
}
|
}
|
||||||
}, [showPlayerSettings]);
|
}, 30);
|
||||||
|
};
|
||||||
|
|
||||||
// Auto-focus current episode when episodes drawer opens
|
window.addEventListener("blur", handleWindowBlur);
|
||||||
useEffect(() => {
|
return () => window.removeEventListener("blur", handleWindowBlur);
|
||||||
if (showPlayerEpisodes) {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
const activeEp =
|
|
||||||
document.querySelector<HTMLElement>(".has-cinema-player-episodes-sheet .has-cinema-drawer-ep-btn.active") ||
|
|
||||||
document.querySelector<HTMLElement>(".has-cinema-player-episodes-sheet .has-cinema-drawer-ep-btn") ||
|
|
||||||
document.querySelector<HTMLElement>(".has-cinema-player-episodes-sheet [data-tv-focusable='true']");
|
|
||||||
if (activeEp) {
|
|
||||||
activeEp.focus();
|
|
||||||
activeEp.scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
|
|
||||||
}
|
}
|
||||||
}, 50);
|
}, [playingEpisode]);
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [showPlayerEpisodes]);
|
|
||||||
|
|
||||||
// Handle Android TV hardware back & browser popstate to guarantee return to movie details card
|
// Handle Android TV hardware back & browser popstate to guarantee return to movie details card
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -3129,16 +3088,24 @@ export default function Cinema({
|
||||||
setKodikResults(finalDubbings);
|
setKodikResults(finalDubbings);
|
||||||
setSelectedKodikLink(finalDubbings[0].link);
|
setSelectedKodikLink(finalDubbings[0].link);
|
||||||
if (autoSwitch) {
|
if (autoSwitch) {
|
||||||
setSelectedMirror((prev) => (prev !== "alloha_ru" && prev !== "anilibria" ? "kodik_ru" : prev));
|
setSelectedMirror("kodik_ru");
|
||||||
setRussianDubNotice(`🇷🇺 Найдено ${finalDubbings.length} студий русской озвучки для «${targetTitle}».`);
|
setRussianDubNotice(`🇷🇺 Найдено ${finalDubbings.length} студий русской озвучки для «${targetTitle}».`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setKodikResults([]);
|
setKodikResults([]);
|
||||||
setSelectedKodikLink(null);
|
setSelectedKodikLink(null);
|
||||||
|
if (autoSwitch) {
|
||||||
|
setSelectedMirror("vidlink_pro");
|
||||||
|
setRussianDubNotice("🇷🇺 Для данного фильма русская аудиодорожка встроена в плеер VidLink Pro (нажмите на шестеренку ⚙️ или значок аудио в правом нижнем углу плеера).");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setKodikResults([]);
|
setKodikResults([]);
|
||||||
setSelectedKodikLink(null);
|
setSelectedKodikLink(null);
|
||||||
|
if (autoSwitch) {
|
||||||
|
setSelectedMirror("vidlink_pro");
|
||||||
|
setRussianDubNotice("🌍 Переключено на основной плеер VidLink Pro.");
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSearchingKodik(false);
|
setIsSearchingKodik(false);
|
||||||
}
|
}
|
||||||
|
|
@ -3170,8 +3137,8 @@ export default function Cinema({
|
||||||
switch (selectedMirror) {
|
switch (selectedMirror) {
|
||||||
case "vidlink_pro":
|
case "vidlink_pro":
|
||||||
return isTv
|
return isTv
|
||||||
? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=1&muted=0&volume=100&primaryColor=6366f1`
|
? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=true&primaryColor=6366f1`
|
||||||
: `https://vidlink.pro/movie/${tmdb || 693134}?autoplay=1&muted=0&volume=100&primaryColor=6366f1`;
|
: `https://vidlink.pro/movie/${tmdb || 693134}?autoplay=true&primaryColor=6366f1`;
|
||||||
|
|
||||||
case "kodik_ru":
|
case "kodik_ru":
|
||||||
if (selectedKodikLink) {
|
if (selectedKodikLink) {
|
||||||
|
|
@ -3182,16 +3149,9 @@ export default function Cinema({
|
||||||
const l = kodikResults[0].link.startsWith("//") ? `https:${kodikResults[0].link}` : kodikResults[0].link;
|
const l = kodikResults[0].link.startsWith("//") ? `https:${kodikResults[0].link}` : kodikResults[0].link;
|
||||||
return appendAutoplay(l);
|
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
|
return isTv
|
||||||
? `https://player.autoembed.co/embed/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=1`
|
? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=true&primaryColor=6366f1`
|
||||||
: `https://player.autoembed.co/embed/movie/${tmdb || 693134}?autoplay=1`;
|
: `https://vidlink.pro/movie/${tmdb || 693134}?autoplay=true&primaryColor=6366f1`;
|
||||||
|
|
||||||
case "autoembed":
|
case "autoembed":
|
||||||
return isTv
|
return isTv
|
||||||
|
|
@ -3224,24 +3184,14 @@ export default function Cinema({
|
||||||
if (imdbId) {
|
if (imdbId) {
|
||||||
return `https://theatre.stravers.live/?imdb=${imdbId}&autoplay=1`;
|
return `https://theatre.stravers.live/?imdb=${imdbId}&autoplay=1`;
|
||||||
}
|
}
|
||||||
if (tmdb) {
|
|
||||||
return `https://theatre.stravers.live/?tmdb=${tmdb}&autoplay=1`;
|
|
||||||
}
|
|
||||||
return isTv
|
return isTv
|
||||||
? `https://player.autoembed.co/embed/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=1`
|
? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=true&primaryColor=6366f1`
|
||||||
: `https://player.autoembed.co/embed/movie/${tmdb || 693134}?autoplay=1`;
|
: `https://vidlink.pro/movie/${tmdb || 693134}?autoplay=true&primaryColor=6366f1`;
|
||||||
|
|
||||||
default:
|
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
|
return isTv
|
||||||
? `https://player.autoembed.co/embed/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=1`
|
? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?autoplay=true&primaryColor=6366f1`
|
||||||
: `https://player.autoembed.co/embed/movie/${tmdb || 693134}?autoplay=1`;
|
: `https://vidlink.pro/movie/${tmdb || 693134}?autoplay=true&primaryColor=6366f1`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -4934,33 +4884,6 @@ export default function Cinema({
|
||||||
⏩ +10с
|
⏩ +10с
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="has-cinema-player-ctrl-btn"
|
|
||||||
onClick={() => {
|
|
||||||
setPlayerVolumeMax(true);
|
|
||||||
resetHudTimer();
|
|
||||||
}}
|
|
||||||
title="Максимальная громкость 100%"
|
|
||||||
data-tv-focusable="true"
|
|
||||||
data-tv-row="bottom"
|
|
||||||
>
|
|
||||||
🔊 100%
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="has-cinema-player-ctrl-btn"
|
|
||||||
onClick={() => {
|
|
||||||
triggerIframeDirectFocus();
|
|
||||||
}}
|
|
||||||
title="Управление пультом напрямую внутри плеера"
|
|
||||||
data-tv-focusable="true"
|
|
||||||
data-tv-row="bottom"
|
|
||||||
>
|
|
||||||
🎮 Плеер
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{(playingMedia || selectedMedia).media_type !== "movie" && (
|
{(playingMedia || selectedMedia).media_type !== "movie" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -5037,7 +4960,7 @@ export default function Cinema({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triedMirrorsRef.current.clear();
|
triedMirrorsRef.current.clear();
|
||||||
setAllMirrorsFailed(false);
|
setAllMirrorsFailed(false);
|
||||||
handleSelectMirror("alloha_ru");
|
handleSelectMirror("vidlink_pro");
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
>
|
>
|
||||||
|
|
@ -5079,7 +5002,7 @@ export default function Cinema({
|
||||||
allow="autoplay *; fullscreen *; encrypted-media *; picture-in-picture *; clipboard-write *"
|
allow="autoplay *; fullscreen *; encrypted-media *; picture-in-picture *; clipboard-write *"
|
||||||
referrerPolicy="no-referrer-when-downgrade"
|
referrerPolicy="no-referrer-when-downgrade"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
tabIndex={0}
|
tabIndex={-1}
|
||||||
onLoad={handleIframeLoaded}
|
onLoad={handleIframeLoaded}
|
||||||
onError={() => handleAutoSwitchNextMirror("Сервер вернул ошибку")}
|
onError={() => handleAutoSwitchNextMirror("Сервер вернул ошибку")}
|
||||||
{...{ webkitallowfullscreen: "true", mozallowfullscreen: "true" }}
|
{...{ webkitallowfullscreen: "true", mozallowfullscreen: "true" }}
|
||||||
|
|
@ -5140,6 +5063,7 @@ export default function Cinema({
|
||||||
className="has-cinema-drawer-chip primary"
|
className="has-cinema-drawer-chip primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triggerPlayerPlayPause();
|
triggerPlayerPlayPause();
|
||||||
|
setShowPlayerSettings(false);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Воспроизведение / Пауза"
|
title="Воспроизведение / Пауза"
|
||||||
|
|
@ -5151,6 +5075,7 @@ export default function Cinema({
|
||||||
className="has-cinema-drawer-chip"
|
className="has-cinema-drawer-chip"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triggerPlayerSeek(-10);
|
triggerPlayerSeek(-10);
|
||||||
|
setShowPlayerSettings(false);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Перемотать на 10 секунд назад"
|
title="Перемотать на 10 секунд назад"
|
||||||
|
|
@ -5162,6 +5087,7 @@ export default function Cinema({
|
||||||
className="has-cinema-drawer-chip"
|
className="has-cinema-drawer-chip"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triggerPlayerSeek(10);
|
triggerPlayerSeek(10);
|
||||||
|
setShowPlayerSettings(false);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Перемотать на 10 секунд вперед"
|
title="Перемотать на 10 секунд вперед"
|
||||||
|
|
@ -5173,6 +5099,7 @@ export default function Cinema({
|
||||||
className="has-cinema-drawer-chip"
|
className="has-cinema-drawer-chip"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPlayerVolumeMax(true);
|
setPlayerVolumeMax(true);
|
||||||
|
setShowPlayerSettings(false);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Выставить громкость плеера на максимум (100%)"
|
title="Выставить громкость плеера на максимум (100%)"
|
||||||
|
|
@ -5184,6 +5111,7 @@ export default function Cinema({
|
||||||
className="has-cinema-drawer-chip"
|
className="has-cinema-drawer-chip"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triggerPlayerSeek(20);
|
triggerPlayerSeek(20);
|
||||||
|
setShowPlayerSettings(false);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Перемотать заставку на 20 секунд вперед"
|
title="Перемотать заставку на 20 секунд вперед"
|
||||||
|
|
@ -5195,6 +5123,7 @@ export default function Cinema({
|
||||||
className="has-cinema-drawer-chip"
|
className="has-cinema-drawer-chip"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triggerPlayerSeek(85);
|
triggerPlayerSeek(85);
|
||||||
|
setShowPlayerSettings(false);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Перемотать опенинг / интро на 85 секунд вперед"
|
title="Перемотать опенинг / интро на 85 секунд вперед"
|
||||||
|
|
@ -5278,16 +5207,16 @@ export default function Cinema({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`has-cinema-drawer-option-btn ${
|
className={`has-cinema-drawer-option-btn ${
|
||||||
selectedMirror === "alloha_ru" ? "active" : ""
|
selectedMirror === "vidlink_pro" ? "active" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelectMirror("alloha_ru");
|
handleSelectMirror("vidlink_pro");
|
||||||
showTvToast("Включен Сервер 1: Alloha (Русский дубляж)", "🔊", 1500);
|
showTvToast("Включен Сервер 1: Full HD (1080p)", "⚡", 1500);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
>
|
>
|
||||||
<span className="option-name">🔊 Сервер 1: Alloha (Русский дубляж)</span>
|
<span className="option-name">⚡ Сервер 1: VidLink Pro (1080p)</span>
|
||||||
<span className="option-desc">Полная поддержка пульта, автозапуск, русская озвучка</span>
|
<span className="option-desc">Высокая скорость, Full HD качество, стабильный поток</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -5297,7 +5226,7 @@ export default function Cinema({
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelectMirror("kodik_ru");
|
handleSelectMirror("kodik_ru");
|
||||||
showTvToast("Включен Сервер 2: Kodik (Студии дубляжа)", "🎙️", 1500);
|
showTvToast("Включен Сервер 2: Kodik (Дубляж)", "🎙️", 1500);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
>
|
>
|
||||||
|
|
@ -5305,20 +5234,22 @@ export default function Cinema({
|
||||||
<span className="option-desc">Профессиональный русский дубляж (LostFilm, Red Head Sound, HDRezka)</span>
|
<span className="option-desc">Профессиональный русский дубляж (LostFilm, Red Head Sound, HDRezka)</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{currentAllohaUrl && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`has-cinema-drawer-option-btn ${
|
className={`has-cinema-drawer-option-btn ${
|
||||||
selectedMirror === "vidlink_pro" ? "active" : ""
|
selectedMirror === "alloha_ru" ? "active" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelectMirror("vidlink_pro");
|
handleSelectMirror("alloha_ru");
|
||||||
showTvToast("Включен Сервер 3: VidLink Pro (1080p)", "⚡", 1500);
|
showTvToast("Включен Сервер 3: Alloha", "🔊", 1500);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
>
|
>
|
||||||
<span className="option-name">⚡ Сервер 3: VidLink Pro (1080p)</span>
|
<span className="option-name">🔊 Сервер 3: Alloha Player</span>
|
||||||
<span className="option-desc">Высокая скорость, Full HD качество, стабильный поток</span>
|
<span className="option-desc">Российский балансер кинолент</span>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Anime-specific AniLibria HLS stream */}
|
{/* Anime-specific AniLibria HLS stream */}
|
||||||
{((playingMedia || selectedMedia).genre.toLowerCase().includes("аниме") ||
|
{((playingMedia || selectedMedia).genre.toLowerCase().includes("аниме") ||
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ interface DashboardProps {
|
||||||
dbInfo: SafeDbInfo | null;
|
dbInfo: SafeDbInfo | null;
|
||||||
dbStatus: DbStatus | null;
|
dbStatus: DbStatus | null;
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
isTvEnvironment?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NavTab = "cinema" | "home" | "users" | "settings" | "profile" | "help";
|
type NavTab = "cinema" | "home" | "users" | "settings" | "profile" | "help";
|
||||||
|
|
@ -63,6 +62,8 @@ type NavTab = "cinema" | "home" | "users" | "settings" | "profile" | "help";
|
||||||
// Helper for Smart TV / Android TV detection
|
// Helper for Smart TV / Android TV detection
|
||||||
const detectTvEnvironment = (): boolean => {
|
const detectTvEnvironment = (): boolean => {
|
||||||
if (typeof window === "undefined") return false;
|
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 ua = (navigator.userAgent || "").toLowerCase();
|
||||||
const isTvUa =
|
const isTvUa =
|
||||||
ua.includes("android tv") ||
|
ua.includes("android tv") ||
|
||||||
|
|
@ -89,17 +90,6 @@ const detectTvEnvironment = (): boolean => {
|
||||||
const isAndroid = ua.includes("android");
|
const isAndroid = ua.includes("android");
|
||||||
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
|
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
|
||||||
if (isAndroid && noTouch) return true;
|
if (isAndroid && noTouch) return true;
|
||||||
|
|
||||||
// On pure desktop (macOS / Windows / Desktop Linux), default to desktop PC mode
|
|
||||||
const isDesktop =
|
|
||||||
(ua.includes("windows nt") || ua.includes("macintosh") || (ua.includes("linux") && !ua.includes("android"))) &&
|
|
||||||
!isTvUa;
|
|
||||||
if (isDesktop) {
|
|
||||||
return localStorage.getItem("has_tv_mode_manual") === "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
const saved = localStorage.getItem("has_tv_mode");
|
|
||||||
if (saved !== null) return saved === "true";
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -108,23 +98,13 @@ export default function Dashboard({
|
||||||
dbInfo,
|
dbInfo,
|
||||||
dbStatus,
|
dbStatus,
|
||||||
onLogout,
|
onLogout,
|
||||||
isTvEnvironment: propIsTv,
|
|
||||||
}: DashboardProps) {
|
}: DashboardProps) {
|
||||||
const [currentUser, setCurrentUser] = useState<UserDto>(user);
|
const [currentUser, setCurrentUser] = useState<UserDto>(user);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentUser(user);
|
setCurrentUser(user);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const [isTvEnvironment, setIsTvEnvironment] = useState<boolean>(() => {
|
const [isTvEnvironment, setIsTvEnvironment] = useState<boolean>(detectTvEnvironment);
|
||||||
return propIsTv !== undefined ? propIsTv : detectTvEnvironment();
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (propIsTv !== undefined && propIsTv !== isTvEnvironment) {
|
|
||||||
setIsTvEnvironment(propIsTv);
|
|
||||||
}
|
|
||||||
}, [propIsTv, isTvEnvironment]);
|
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<NavTab>("cinema");
|
const [activeTab, setActiveTab] = useState<NavTab>("cinema");
|
||||||
const [serverUsers, setServerUsers] = useState<ServerUserDto[]>([]);
|
const [serverUsers, setServerUsers] = useState<ServerUserDto[]>([]);
|
||||||
const [loadingUsers, setLoadingUsers] = useState(false);
|
const [loadingUsers, setLoadingUsers] = useState(false);
|
||||||
|
|
@ -133,6 +113,21 @@ export default function Dashboard({
|
||||||
});
|
});
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
// Dynamic D-Pad Remote Control Key Listener (Auto-detects TV when user presses remote buttons)
|
||||||
|
useEffect(() => {
|
||||||
|
const handleRemoteDpadKey = (e: KeyboardEvent) => {
|
||||||
|
const isRemoteKey =
|
||||||
|
[19, 20, 21, 22, 23, 4, 82, 85, 179].includes(e.keyCode) ||
|
||||||
|
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key);
|
||||||
|
if (isRemoteKey && !isTvEnvironment) {
|
||||||
|
setIsTvEnvironment(true);
|
||||||
|
localStorage.setItem("has_tv_mode", "true");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleRemoteDpadKey);
|
||||||
|
return () => window.removeEventListener("keydown", handleRemoteDpadKey);
|
||||||
|
}, [isTvEnvironment]);
|
||||||
|
|
||||||
const handleNavClick = (tab: NavTab) => {
|
const handleNavClick = (tab: NavTab) => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
setMobileMenuOpen(false);
|
setMobileMenuOpen(false);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue