Compare commits

..

1 commit

7 changed files with 97 additions and 75 deletions

View file

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

View file

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

View file

@ -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.18", "version": "0.0.19",
"identifier": "com.has.tauri-app", "identifier": "com.has.tauri-app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View file

@ -27,8 +27,6 @@ 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") ||
@ -55,6 +53,17 @@ 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;
}; };
@ -144,21 +153,6 @@ 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;
@ -525,6 +519,7 @@ export default function App() {
dbInfo={dbInfo} dbInfo={dbInfo}
dbStatus={dbStatus} dbStatus={dbStatus}
onLogout={handleLogout} onLogout={handleLogout}
isTvEnvironment={isTvEnvironment}
/> />
</> </>
); );
@ -542,6 +537,7 @@ 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");
}} }}
/> />
</> </>
@ -812,6 +808,7 @@ 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={{

View file

@ -22,8 +22,6 @@ 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") ||
@ -50,6 +48,17 @@ 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;
}; };
@ -88,7 +97,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.18"); const [currentAppVersion, setCurrentAppVersion] = useState<string>("0.0.19");
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);

View file

@ -493,8 +493,6 @@ 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") ||
@ -521,6 +519,17 @@ 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;
}; };
@ -540,10 +549,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(true); setIsTvMode(isTvEnvironment);
} }
}, [isTvEnvironment, isTvMode]); }, [isTvEnvironment]);
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);
@ -563,6 +572,7 @@ 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;
@ -2250,40 +2260,18 @@ 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;
// Android TV / Remote key mappings // Remote Menu key (ONLY in TV Mode, and NEVER for the 'R' key on PC keyboard!)
const isBack = const isMenuKey =
e.key === "Escape" || !isInput &&
e.key === "GoBack" || (e.key === "Menu" ||
e.key === "BrowserBack" || e.key === "ContextMenu" ||
e.keyCode === 4 || (isTvMode && e.keyCode === 82 && e.key !== "r" && e.key !== "R" && e.code !== "KeyR"));
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;
// Remote Menu key (Android TV remote Menu button / ContextMenu) if (isTvMode && isMenuKey) {
if (e.key === "Menu" || e.keyCode === 82 || e.key === "ContextMenu") {
e.preventDefault(); e.preventDefault();
setIsTvNavOpen((prev) => { setIsTvNavOpen((prev) => {
const next = !prev; const next = !prev;
@ -2310,6 +2298,29 @@ 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();

View file

@ -55,6 +55,7 @@ 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";
@ -62,8 +63,6 @@ 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") ||
@ -90,6 +89,17 @@ 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;
}; };
@ -98,13 +108,23 @@ 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>(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 [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);
@ -113,21 +133,6 @@ 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);