Compare commits

..

6 commits

Author SHA1 Message Date
e616eec246 feat(release): v0.0.19 - fix PC keyboard R key triggering TV navigation drawer, disable TV mode auto-activation on desktop 2026-11-02 14:54:23 +05:00
205678b3d7 feat(release): v0.0.18 - fix autoplay, 100% volume unmute, prioritize Russian balancers and complete TV remote navigation 2026-10-29 20:29:50 +05:00
f41430984c chore(release): bump version to 0.0.17 2026-10-29 17:00:33 +05:00
7da9ab00b1 Улучшить управление видеоплеером и громкостью
- Автоматическая установка громкости на 100% при запуске плеера
- Постоянный мониторинг громкости каждые 2 секунды во время воспроизведения
- Усиление надежности команд Play/Pause/Seek (тройная отправка с задержками)
- Улучшенная перемотка ±10 секунд для всех типов плееров
- Поддержка громкости для нативного HTML5 видео (AniLibria)
- Агрессивная установка громкости в первые 15 секунд после загрузки iframe

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-29 15:48:39 +05:00
2ab0457568 feat(release): v0.0.16 - deep recursive iframe window tree broadcast, fix remote D-pad settings drawer navigation, auto-switch Kodik for movies 2026-10-28 23:12:57 +05:00
9e9694ed19 feat(release): v0.0.15 - 100% player volume boost script, Android TV auto-update APK downloader & install modal 2026-10-28 22:17:50 +05:00
9 changed files with 1032 additions and 491 deletions

View file

@ -1,7 +1,7 @@
{
"name": "has",
"private": true,
"version": "0.0.14",
"version": "0.0.19",
"type": "module",
"scripts": {
"dev": "vite",

View file

@ -1,6 +1,6 @@
[package]
name = "has"
version = "0.0.14"
version = "0.0.19"
description = "has"
authors = ["autli"]
edition = "2024"

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" />

View file

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "has",
"version": "0.0.14",
"version": "0.0.19",
"identifier": "com.has.tauri-app",
"build": {
"beforeDevCommand": "npm run dev",

View file

@ -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%;

View file

@ -27,8 +27,6 @@ const isTauri = () => {
// Helper for Smart TV / Android TV detection
const detectTvEnvironment = (): boolean => {
if (typeof window === "undefined") return false;
const saved = localStorage.getItem("has_tv_mode");
if (saved !== null) return saved === "true";
const ua = (navigator.userAgent || "").toLowerCase();
const isTvUa =
ua.includes("android tv") ||
@ -55,6 +53,17 @@ const detectTvEnvironment = (): boolean => {
const isAndroid = ua.includes("android");
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
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;
};
@ -144,21 +153,6 @@ export default function App() {
checkDatabase();
}, [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
useEffect(() => {
if (resetStage !== 2) return;
@ -525,6 +519,7 @@ export default function App() {
dbInfo={dbInfo}
dbStatus={dbStatus}
onLogout={handleLogout}
isTvEnvironment={isTvEnvironment}
/>
</>
);
@ -542,6 +537,7 @@ export default function App() {
onSwitchToDesktop={() => {
setIsTvEnvironment(false);
localStorage.setItem("has_tv_mode", "false");
localStorage.setItem("has_tv_mode_manual", "false");
}}
/>
</>
@ -812,6 +808,7 @@ export default function App() {
onClick={() => {
setIsTvEnvironment(true);
localStorage.setItem("has_tv_mode", "true");
localStorage.setItem("has_tv_mode_manual", "true");
}}
title="Перейти в режим Android TV (выбор профилей)"
style={{

View file

@ -22,8 +22,6 @@ interface UpdaterBannerProps {
// Helper for Smart TV / Android TV detection
const detectTvEnvironment = (): boolean => {
if (typeof window === "undefined") return false;
const saved = localStorage.getItem("has_tv_mode");
if (saved !== null) return saved === "true";
const ua = (navigator.userAgent || "").toLowerCase();
const isTvUa =
ua.includes("android tv") ||
@ -50,6 +48,17 @@ const detectTvEnvironment = (): boolean => {
const isAndroid = ua.includes("android");
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
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;
};
@ -88,16 +97,24 @@ const isNewerSemver = (remote: string, current: string): boolean => {
export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) {
const [isTv, setIsTv] = useState<boolean>(() => isTvEnvironment ?? detectTvEnvironment());
const [currentAppVersion, setCurrentAppVersion] = useState<string>("0.0.14");
const [currentAppVersion, setCurrentAppVersion] = useState<string>("0.0.19");
const [updateAvailable, setUpdateAvailable] = useState(false);
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
const [statusText, setStatusText] = useState<string>("Загрузка обновления...");
const [error, setError] = useState<string | null>(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<boolean>(() => {
if (typeof window === "undefined") return false;
return localStorage.getItem("has_auto_update_tv") === "true";
});
const [autoCountdown, setAutoCountdown] = useState<number | null>(null);
// TV remote navigation: "update" | "skip" | "exit" | "autotoggle"
const [tvFocusedBtn, setTvFocusedBtn] = useState<"update" | "skip" | "exit" | "autotoggle">("update");
const updateBtnRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
@ -270,16 +287,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 +330,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 +346,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 +380,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 +389,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 +406,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<void>((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 +493,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 +529,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 +584,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 +604,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 +616,21 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) {
<div className="has-tv-updater-overlay" role="dialog" aria-modal="true">
<div className="has-tv-updater-card">
<div className="has-tv-updater-icon-ring">{platformInfo.icon}</div>
<h2 className="has-tv-updater-title">{platformInfo.title}</h2>
<h2 className="has-tv-updater-title">Хотите ли вы обновить приложение HAS?</h2>
<div className="has-tv-updater-version-tag">
<span>{platformInfo.badge}</span>
<span>📺 Android TV • v{currentAppVersion} ➔ v{updateInfo?.version || "новейшая"}</span>
</div>
<p className="has-tv-updater-desc">
{updateInfo?.body || platformInfo.desc}
</p>
{autoCountdown !== null && !downloading && (
<div className="has-tv-updater-countdown">
⚡ Автообновление активно: начнется через {autoCountdown} сек... (нажмите любую кнопку для отмены)
</div>
)}
{downloading ? (
<div className="has-tv-updater-progress-container">
<div className="has-tv-updater-progress-track">
@ -505,70 +640,104 @@ export function UpdaterBanner({ isTvEnvironment }: UpdaterBannerProps = {}) {
/>
</div>
<span className="has-tv-updater-progress-text">
Загрузка и установка обновления: {progress}%
{statusText}
</span>
</div>
) : (
<div className="has-tv-updater-buttons">
{/* Button 1: Обновить APK для ТВ */}
<button
ref={updateBtnRef}
type="button"
className={`has-tv-updater-btn btn-update ${
tvFocusedBtn === "update" ? "focused" : ""
}`}
onClick={() => {
playTvBeep(680, 0.05);
handleInstall();
}}
onMouseEnter={() => setTvFocusedBtn("update")}
tabIndex={0}
data-tv-focusable="true"
>
<span>{platformInfo.btnLabel}</span>
</button>
<>
<div className="has-tv-updater-buttons">
{/* Button 1: Да, обновить сейчас */}
<button
ref={updateBtnRef}
type="button"
className={`has-tv-updater-btn btn-update ${
tvFocusedBtn === "update" ? "focused" : ""
}`}
onClick={() => {
playTvBeep(680, 0.05);
cancelAutoCountdown();
handleInstall();
}}
onMouseEnter={() => {
cancelAutoCountdown();
setTvFocusedBtn("update");
}}
tabIndex={0}
data-tv-focusable="true"
>
<span>{platformInfo.btnLabel}</span>
</button>
{/* Button 2: Пропустить */}
<button
type="button"
className={`has-tv-updater-btn btn-skip ${
tvFocusedBtn === "skip" ? "focused" : ""
}`}
onClick={() => {
playTvBeep(420, 0.04);
setDismissed(true);
}}
onMouseEnter={() => setTvFocusedBtn("skip")}
tabIndex={0}
data-tv-focusable="true"
>
<span>Пропустить</span>
</button>
{/* Button 2: Напомнить позже */}
<button
type="button"
className={`has-tv-updater-btn btn-skip ${
tvFocusedBtn === "skip" ? "focused" : ""
}`}
onClick={() => {
playTvBeep(420, 0.04);
cancelAutoCountdown();
setDismissed(true);
}}
onMouseEnter={() => {
cancelAutoCountdown();
setTvFocusedBtn("skip");
}}
tabIndex={0}
data-tv-focusable="true"
>
<span>Напомнить позже</span>
</button>
{/* Button 3: Выход из приложения (Exit) */}
{/* Button 3: Выход из приложения (Exit) */}
<button
type="button"
className={`has-tv-updater-btn btn-exit ${
tvFocusedBtn === "exit" ? "focused" : ""
}`}
onClick={() => {
playTvBeep(320, 0.06);
cancelAutoCountdown();
handleExitApp();
}}
onMouseEnter={() => {
cancelAutoCountdown();
setTvFocusedBtn("exit");
}}
tabIndex={0}
data-tv-focusable="true"
title="Выйти из приложения"
>
<span>✕ Выход</span>
</button>
</div>
{/* Auto-update Toggle Chip for Android TV */}
<button
type="button"
className={`has-tv-updater-btn btn-exit ${
tvFocusedBtn === "exit" ? "focused" : ""
className={`has-tv-updater-autotoggle ${autoUpdateTv ? "active" : ""} ${
tvFocusedBtn === "autotoggle" ? "focused" : ""
}`}
onClick={() => {
playTvBeep(320, 0.06);
handleExitApp();
onClick={toggleAutoUpdate}
onMouseEnter={() => {
cancelAutoCountdown();
setTvFocusedBtn("autotoggle");
}}
onMouseEnter={() => setTvFocusedBtn("exit")}
tabIndex={0}
data-tv-focusable="true"
title="Выйти из приложения"
title="Переключить режим автоматического скачивания обновлений для ТВ"
>
<span>✕ Выход</span>
{autoUpdateTv
? "🟢 Автообновление для TV: ВКЛЮЧЕНО (скачивать без запроса)"
: "⚪ Автообновление для TV: ВЫКЛЮЧЕНО (нажмите OK чтобы включить)"}
</button>
</div>
</>
)}
{error && <div className="updater-error" style={{ marginTop: 16 }}>{error}</div>}
<div className="has-tv-updater-remote-hint">
◄ ► Стрелки на пульте: выбор • OK: подтвердить • Back: пропустить
◄ ► Стрелки на пульте: выбор • ▲ ▼ Вверх/Вниз: переключение опций • OK: подтвердить • Back: позже
</div>
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -55,6 +55,7 @@ interface DashboardProps {
dbInfo: SafeDbInfo | null;
dbStatus: DbStatus | null;
onLogout: () => void;
isTvEnvironment?: boolean;
}
type NavTab = "cinema" | "home" | "users" | "settings" | "profile" | "help";
@ -62,8 +63,6 @@ type NavTab = "cinema" | "home" | "users" | "settings" | "profile" | "help";
// Helper for Smart TV / Android TV detection
const detectTvEnvironment = (): boolean => {
if (typeof window === "undefined") return false;
const saved = localStorage.getItem("has_tv_mode");
if (saved !== null) return saved === "true";
const ua = (navigator.userAgent || "").toLowerCase();
const isTvUa =
ua.includes("android tv") ||
@ -90,6 +89,17 @@ const detectTvEnvironment = (): boolean => {
const isAndroid = ua.includes("android");
const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0;
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;
};
@ -98,13 +108,23 @@ export default function Dashboard({
dbInfo,
dbStatus,
onLogout,
isTvEnvironment: propIsTv,
}: DashboardProps) {
const [currentUser, setCurrentUser] = useState<UserDto>(user);
useEffect(() => {
setCurrentUser(user);
}, [user]);
const [isTvEnvironment, setIsTvEnvironment] = useState<boolean>(detectTvEnvironment);
const [isTvEnvironment, setIsTvEnvironment] = useState<boolean>(() => {
return propIsTv !== undefined ? propIsTv : detectTvEnvironment();
});
useEffect(() => {
if (propIsTv !== undefined && propIsTv !== isTvEnvironment) {
setIsTvEnvironment(propIsTv);
}
}, [propIsTv, isTvEnvironment]);
const [activeTab, setActiveTab] = useState<NavTab>("cinema");
const [serverUsers, setServerUsers] = useState<ServerUserDto[]>([]);
const [loadingUsers, setLoadingUsers] = useState(false);
@ -113,21 +133,6 @@ export default function Dashboard({
});
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) => {
setActiveTab(tab);
setMobileMenuOpen(false);