From 040b21d2a785c4064dbb59485854ef018df36bf9 Mon Sep 17 00:00:00 2001 From: Autli Date: Mon, 26 Oct 2026 21:53:53 +0500 Subject: [PATCH] feat(tv): adapt app for Android TV remote D-Pad, dedicated navigation drawer, rename catalog & server tabs, add dedicated TV APK --- .github/workflows/release.yml | 5 + src/pages/cinema/Cinema.css | 191 +++++++++++++++-- src/pages/cinema/Cinema.tsx | 343 +++++++++++++++++++++++++++--- src/pages/dashboard/Dashboard.css | 22 ++ src/pages/dashboard/Dashboard.tsx | 97 ++++++++- 5 files changed, 613 insertions(+), 45 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea7a563..7af5376 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -163,6 +163,7 @@ jobs: cp "$apk" "dist-apk/Home.Autli.Server_${RELEASE_TAG}_${filename}" if [[ "$filename" == *"universal"* ]] || [[ "$filename" == *"release"* ]]; then cp "$apk" "dist-apk/Home.Autli.Server_${RELEASE_TAG}.apk" + cp "$apk" "dist-apk/Home.Autli.Server_TV_${RELEASE_TAG}.apk" fi done @@ -170,8 +171,12 @@ jobs: first_apk=$(find dist-apk -type f -name "*.apk" | head -n 1) if [ -n "$first_apk" ]; then cp "$first_apk" "dist-apk/Home.Autli.Server_${RELEASE_TAG}.apk" + cp "$first_apk" "dist-apk/Home.Autli.Server_TV_${RELEASE_TAG}.apk" fi fi + if [ ! -f "dist-apk/Home.Autli.Server_TV_${RELEASE_TAG}.apk" ] && [ -f "dist-apk/Home.Autli.Server_${RELEASE_TAG}.apk" ]; then + cp "dist-apk/Home.Autli.Server_${RELEASE_TAG}.apk" "dist-apk/Home.Autli.Server_TV_${RELEASE_TAG}.apk" + fi echo "Uploading APK assets to GitHub release $RELEASE_TAG..." if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then diff --git a/src/pages/cinema/Cinema.css b/src/pages/cinema/Cinema.css index 2f1fa6a..ebe7c83 100644 --- a/src/pages/cinema/Cinema.css +++ b/src/pages/cinema/Cinema.css @@ -1814,33 +1814,194 @@ 6. ANDROID TV & REMOTE CONTROL (10-FOOT UI) STYLES ======================================================== */ -/* Header TV Mode Switcher Button */ -.has-cinema-tv-mode-btn { +/* Header TV Mode Pill Button & Navigation Drawer */ +.has-cinema-tv-menu-pill { display: inline-flex; align-items: center; gap: 6px; - padding: 6px 14px; - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.18); + padding: 7px 16px; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.25), rgba(139, 92, 246, 0.25)); + border: 1px solid rgba(129, 140, 248, 0.5); border-radius: 9999px; - color: #cbd5e1; - font-size: 13px; + color: #e0e7ff; + font-size: 13.5px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; } -.has-cinema-tv-mode-btn:hover { - background: rgba(255, 255, 255, 0.16); - color: #ffffff; - border-color: rgba(255, 255, 255, 0.35); -} - -.has-cinema-tv-mode-btn.active { +.has-cinema-tv-menu-pill:hover, +.has-cinema-tv-menu-pill:focus { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); border-color: #818cf8; color: #ffffff; - box-shadow: 0 0 16px rgba(99, 102, 241, 0.6); + outline: 3px solid #818cf8 !important; + box-shadow: 0 0 20px rgba(129, 140, 248, 0.8) !important; + transform: scale(1.05); +} + +/* Android TV Leanback Navigation Rail & Drawer */ +.has-cinema-tv-nav-drawer { + position: fixed; + top: 0; + left: 0; + bottom: 0; + display: flex; + flex-direction: column; + z-index: 1000; + transition: width 0.28s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.28s ease, background 0.28s ease; +} + +.has-cinema-tv-nav-drawer.collapsed { + width: 66px; + background: rgba(10, 15, 28, 0.9); + backdrop-filter: blur(20px); + border-right: 1px solid rgba(255, 255, 255, 0.08); +} + +.has-cinema-tv-nav-drawer.open { + width: 270px; + background: rgba(8, 12, 24, 0.97); + backdrop-filter: blur(24px); + border-right: 1px solid rgba(129, 140, 248, 0.4); + box-shadow: 15px 0 50px rgba(0, 0, 0, 0.9); +} + +.has-cinema-tv-nav-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 12px 14px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.has-cinema-tv-nav-logo { + display: flex; + align-items: center; + gap: 10px; + overflow: hidden; +} + +.has-cinema-tv-nav-logo-badge { + background: linear-gradient(135deg, #6366f1, #a855f7); + color: #fff; + font-weight: 800; + font-size: 13px; + padding: 4px 8px; + border-radius: 8px; + letter-spacing: 1px; +} + +.has-cinema-tv-nav-logo-text { + font-size: 16px; + font-weight: 700; + color: #f8fafc; + white-space: nowrap; +} + +.has-cinema-tv-nav-toggle-btn { + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 8px; + color: #cbd5e1; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + font-size: 12px; +} + +.has-cinema-tv-nav-toggle-btn:focus, +.has-cinema-tv-nav-toggle-btn:hover { + background: #6366f1; + color: #ffffff; + outline: 3px solid #818cf8 !important; + box-shadow: 0 0 16px rgba(129, 140, 248, 0.7) !important; +} + +.has-cinema-tv-nav-list { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px 8px; + flex: 1; + overflow-y: auto; +} + +.has-cinema-tv-nav-item { + width: 100%; + display: flex; + align-items: center; + gap: 14px; + padding: 12px 14px; + background: transparent; + border: 1px solid transparent; + border-radius: 12px; + color: #94a3b8; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); + text-align: left; +} + +.has-cinema-tv-nav-item:hover, +.has-cinema-tv-nav-item:focus { + background: rgba(99, 102, 241, 0.25) !important; + border-color: #818cf8 !important; + color: #ffffff !important; + outline: 3px solid #818cf8 !important; + box-shadow: 0 0 24px rgba(129, 140, 248, 0.8) !important; + transform: scale(1.04) !important; +} + +.has-cinema-tv-nav-item.active { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.35), rgba(139, 92, 246, 0.35)); + border-color: rgba(129, 140, 248, 0.6); + color: #ffffff; +} + +.has-cinema-tv-nav-item.logout { + margin-top: auto; + color: #f87171; +} + +.has-cinema-tv-nav-item.logout:hover, +.has-cinema-tv-nav-item.logout:focus { + background: rgba(239, 68, 68, 0.25) !important; + border-color: #f87171 !important; + outline-color: #f87171 !important; + box-shadow: 0 0 24px rgba(239, 68, 68, 0.8) !important; +} + +.has-cinema-tv-nav-icon { + font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + width: 26px; + flex-shrink: 0; +} + +.has-cinema-tv-nav-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.has-cinema-tv-nav-footer { + padding: 12px; + font-size: 11.5px; + color: #64748b; + text-align: center; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.has-tv-mode { + padding-left: 74px !important; } /* Universal Glowing Focus Indicators for D-Pad / Keyboard Navigation */ diff --git a/src/pages/cinema/Cinema.tsx b/src/pages/cinema/Cinema.tsx index fd5a423..d9b0d1c 100644 --- a/src/pages/cinema/Cinema.tsx +++ b/src/pages/cinema/Cinema.tsx @@ -16,6 +16,9 @@ import "./Cinema.css"; interface CinemaProps { currentUser: UserDto; + onNavigateTab?: (tab: string) => void; + onLogout?: () => void; + isTvEnvironment?: boolean; } const buildAnimeSeasons = (seasonConfigs: { season_number: number; count: number }[], thumb: string): CinemaSeason[] => { @@ -485,7 +488,7 @@ const detectTvEnvironment = (): boolean => { const saved = localStorage.getItem("has_tv_mode"); if (saved !== null) return saved === "true"; const ua = (navigator.userAgent || "").toLowerCase(); - return ( + const isTvUa = ua.includes("android tv") || ua.includes("smarttv") || ua.includes("googletv") || @@ -495,15 +498,44 @@ const detectTvEnvironment = (): boolean => { ua.includes("crkey") || ua.includes("bravia") || ua.includes("aftt") || - ua.includes("aftm") - ); + ua.includes("aftm") || + ua.includes("mibox") || + ua.includes("mi box") || + ua.includes("shield") || + ua.includes("dtv") || + ua.includes("smart-tv") || + ua.includes("hbbtv") || + ua.includes("netcast") || + ua.includes("viera") || + ua.includes("roku") || + ua.includes("firetv"); + if (isTvUa) return true; + const isAndroid = ua.includes("android"); + const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0; + if (isAndroid && noTouch) return true; + return false; }; -export default function Cinema({ currentUser }: CinemaProps) { +export default function Cinema({ + currentUser, + onNavigateTab, + onLogout, + isTvEnvironment = false, +}: CinemaProps) { const isTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; // Android TV / Remote Control (10-Foot UI) State - const [isTvMode, setIsTvMode] = useState(detectTvEnvironment); + const [isTvMode, setIsTvMode] = useState(() => { + return isTvEnvironment || detectTvEnvironment(); + }); + const [isTvNavOpen, setIsTvNavOpen] = useState(false); + const lastFocusedCardRef = useRef(null); + + useEffect(() => { + if (isTvEnvironment && !isTvMode) { + setIsTvMode(true); + } + }, [isTvEnvironment, isTvMode]); const [tvToast, setTvToast] = useState<{ text: string; icon?: string } | null>(null); const tvToastTimerRef = useRef(null); const audioCtxRef = useRef(null); @@ -1307,6 +1339,7 @@ export default function Cinema({ currentUser }: CinemaProps) { // Card Click Action: Opens rich media details modal (DOES NOT start player immediately) const handleMediaCardClick = (item: CinemaMediaItem) => { + lastFocusedCardRef.current = document.activeElement as HTMLElement | null; setSelectedMedia(item); setDetailsMedia(item); setSelectedSeason(1); @@ -1318,6 +1351,20 @@ export default function Cinema({ currentUser }: CinemaProps) { playTvBeep(520, 0.035); }; + const focusFirstContentCard = useCallback(() => { + setTimeout(() => { + const target = + document.querySelector(".has-cinema-btn-play") || + document.querySelector(".has-cinema-media-card") || + document.querySelector(".has-cinema-grid-card"); + if (target) { + target.focus(); + target.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" }); + playTvBeep(640, 0.03); + } + }, 50); + }, [playTvBeep]); + // Play button from Details Modal const handlePlayFromDetails = (seasonNum: number = 1, episode?: CinemaEpisode) => { if (!detailsMedia) return; @@ -1537,6 +1584,15 @@ export default function Cinema({ currentUser }: CinemaProps) { // D-Pad Spatial Navigation & TV Remote Key Handler const handleRemoteKeyDown = useCallback( (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 = document.activeElement instanceof HTMLInputElement || document.activeElement instanceof HTMLTextAreaElement; @@ -1553,7 +1609,28 @@ export default function Cinema({ currentUser }: CinemaProps) { const isFastForward = e.key === "MediaFastForward" || e.keyCode === 90; const isRewind = e.key === "MediaRewind" || e.keyCode === 89; - // F10 key toggles TV mode + // Remote Menu key (Android TV remote Menu button / ContextMenu) + if (e.key === "Menu" || e.keyCode === 82 || e.key === "ContextMenu") { + e.preventDefault(); + setIsTvNavOpen((prev) => { + const next = !prev; + if (next) { + setTimeout(() => { + const navBtn = + document.querySelector(".has-cinema-tv-nav-item.active") || + document.querySelector(".has-cinema-tv-nav-item"); + if (navBtn) { + navBtn.focus(); + playTvBeep(640, 0.03); + } + }, 30); + } + return next; + }); + return; + } + + // F10 key toggles TV mode fallback if (e.key === "F10") { e.preventDefault(); toggleTvMode(); @@ -1585,7 +1662,6 @@ export default function Cinema({ currentUser }: CinemaProps) { e.key === "ArrowUp" || e.keyCode === 19 || e.keyCode === 38 || - e.key === "Menu" || e.key === "Info" ) { e.preventDefault(); @@ -1667,6 +1743,12 @@ export default function Cinema({ currentUser }: CinemaProps) { // 2. Global Back button outside player if (isBack) { + if (isTvNavOpen) { + e.preventDefault(); + setIsTvNavOpen(false); + focusFirstContentCard(); + return; + } if (showTrailerModal) { e.preventDefault(); setShowTrailerModal(false); @@ -1679,6 +1761,16 @@ export default function Cinema({ currentUser }: CinemaProps) { setDetailsMedia(null); showTvToast("Возврат к списку", "↩", 1000); playTvBeep(450, 0.04); + setTimeout(() => { + if (lastFocusedCardRef.current && document.contains(lastFocusedCardRef.current)) { + lastFocusedCardRef.current.focus(); + lastFocusedCardRef.current.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center", + }); + } + }, 40); return; } if (searchQuery.trim().length > 0) { @@ -1686,13 +1778,15 @@ export default function Cinema({ currentUser }: CinemaProps) { setSearchQuery(""); showTvToast("Поиск очищен", "🔍", 1200); playTvBeep(450, 0.04); + focusFirstContentCard(); return; } if (activeTab !== "home") { e.preventDefault(); setActiveTab("home"); - showTvToast("Главный экран", "🏠", 1200); + showTvToast("Каталог", "🏠", 1200); playTvBeep(450, 0.04); + focusFirstContentCard(); return; } } @@ -1735,9 +1829,20 @@ export default function Cinema({ currentUser }: CinemaProps) { const settingsSheet = document.querySelector(".has-cinema-player-settings-sheet"); const episodesSheet = document.querySelector(".has-cinema-player-episodes-sheet"); + // If focus is currently inside the TV Navigation drawer + const currentActive = document.activeElement as HTMLElement | null; + if (currentActive && currentActive.closest(".has-cinema-tv-nav-drawer")) { + if (direction === "right") { + e.preventDefault(); + setIsTvNavOpen(false); + focusFirstContentCard(); + return; + } + } + let focusables = Array.from( document.querySelectorAll( - '[data-tv-focusable="true"], .has-cinema-nav-item, .has-cinema-btn-play, .has-cinema-btn-bookmark, .has-cinema-search-input, .has-cinema-media-card, .has-cinema-grid-card, .has-cinema-episode-card, .has-cinema-player-hud-btn, .has-cinema-player-btn-back, .has-cinema-drawer-option-btn, .has-cinema-drawer-chip, .has-cinema-drawer-ep-btn, .has-cinema-drawer-close-btn, .has-cinema-drawer-btn-primary, .has-cinema-drawer-btn-secondary, .has-cinema-player-pill-nav, .has-cinema-season-tab, .has-cinema-tv-mode-btn, .has-cinema-details-btn-play, .has-cinema-details-close-btn, .has-cinema-details-btn-bookmark, .has-cinema-details-btn-trailer, .has-cinema-actor-card, .has-cinema-btn-info-wide' + '[data-tv-focusable="true"], .has-cinema-nav-item, .has-cinema-tv-nav-item, .has-cinema-tv-menu-pill, .has-cinema-btn-play, .has-cinema-btn-bookmark, .has-cinema-search-input, .has-cinema-media-card, .has-cinema-grid-card, .has-cinema-episode-card, .has-cinema-player-hud-btn, .has-cinema-player-btn-back, .has-cinema-drawer-option-btn, .has-cinema-drawer-chip, .has-cinema-drawer-ep-btn, .has-cinema-drawer-close-btn, .has-cinema-drawer-btn-primary, .has-cinema-drawer-btn-secondary, .has-cinema-player-pill-nav, .has-cinema-season-tab, .has-cinema-details-btn-play, .has-cinema-details-close-btn, .has-cinema-details-btn-bookmark, .has-cinema-details-btn-trailer, .has-cinema-actor-card, .has-cinema-btn-info-wide' ) ).filter((el) => { const r = el.getBoundingClientRect(); @@ -1823,14 +1928,34 @@ export default function Cinema({ currentUser }: CinemaProps) { bestEl.focus(); bestEl.scrollIntoView({ behavior: "smooth", - block: "nearest", - inline: "nearest", + block: "center", + inline: "center", }); playTvBeep(640, 0.025); + } else { + // If no element found in this direction outside modals: + // Trigger the TV Navigation Drawer on Left (from leftmost card) or Right (from rightmost edge) + if (!detailsEl && !playerEl && !trailerEl && !settingsSheet && !episodesSheet) { + if (direction === "left" || direction === "right") { + e.preventDefault(); + setIsTvNavOpen(true); + setTimeout(() => { + const targetNav = + document.querySelector(".has-cinema-tv-nav-item.active") || + document.querySelector(".has-cinema-tv-nav-item"); + if (targetNav) { + targetNav.focus(); + playTvBeep(640, 0.03); + } + }, 40); + } + } } } }, [ + isTvMode, + isTvNavOpen, playingEpisode, detailsMedia, showTrailerModal, @@ -1843,6 +1968,7 @@ export default function Cinema({ currentUser }: CinemaProps) { playTvBeep, toggleTvMode, resetHudTimer, + focusFirstContentCard, ] ); @@ -2125,6 +2251,174 @@ export default function Cinema({ currentUser }: CinemaProps) { return (
+ {/* ======================================================== + ANDROID TV LEANBACK NAVIGATION DRAWER + (Opens when pressing Left from cards, remote Menu, or Menu pill) + ======================================================== */} + {isTvMode && ( + + )} + {/* ======================================================== 1. TOP NAVIGATION BAR (All categories & search) ======================================================== */} @@ -2139,7 +2433,7 @@ export default function Cinema({ currentUser }: CinemaProps) { }} data-tv-focusable="true" > - Главная + Каталог + {isTvMode && ( + + )} {/* Universal Search Box */}
@@ -3672,8 +3967,8 @@ export default function Cinema({ currentUser }: CinemaProps) { Назад
- F10 - ТВ-режим + ◀ Меню + Разделы
)} diff --git a/src/pages/dashboard/Dashboard.css b/src/pages/dashboard/Dashboard.css index 6109e8a..c1d6e6a 100644 --- a/src/pages/dashboard/Dashboard.css +++ b/src/pages/dashboard/Dashboard.css @@ -704,3 +704,25 @@ } } +/* Smart TV / Android TV Environment Rules */ +.has-dashboard-wrapper.has-tv-mode-root .has-sidebar { + display: none !important; +} + +.has-dashboard-wrapper.has-tv-mode-root .has-mobile-top-bar { + display: none !important; +} + +.has-dashboard-wrapper.has-tv-mode-root .has-main-content { + margin-left: 0 !important; + width: 100vw !important; + max-width: 100vw !important; + padding: 0 !important; +} + +.has-dashboard-wrapper.has-tv-mode-root .has-tv-back-to-cinema-btn:focus { + outline: 4px solid #818cf8 !important; + box-shadow: 0 0 25px rgba(129, 140, 248, 0.9) !important; + transform: scale(1.05); +} + diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx index 07e4af8..5863577 100644 --- a/src/pages/dashboard/Dashboard.tsx +++ b/src/pages/dashboard/Dashboard.tsx @@ -32,6 +32,40 @@ interface DashboardProps { type NavTab = "home" | "storage" | "cinema" | "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") || + ua.includes("smarttv") || + ua.includes("googletv") || + ua.includes("tizen") || + ua.includes("webos") || + ua.includes("appletv") || + ua.includes("crkey") || + ua.includes("bravia") || + ua.includes("aftt") || + ua.includes("aftm") || + ua.includes("mibox") || + ua.includes("mi box") || + ua.includes("shield") || + ua.includes("dtv") || + ua.includes("smart-tv") || + ua.includes("hbbtv") || + ua.includes("netcast") || + ua.includes("viera") || + ua.includes("roku") || + ua.includes("firetv"); + if (isTvUa) return true; + const isAndroid = ua.includes("android"); + const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0; + if (isAndroid && noTouch) return true; + return false; +}; + export default function Dashboard({ user, dbInfo, @@ -43,7 +77,10 @@ export default function Dashboard({ setCurrentUser(user); }, [user]); - const [activeTab, setActiveTab] = useState("home"); + const [isTvEnvironment, setIsTvEnvironment] = useState(detectTvEnvironment); + const [activeTab, setActiveTab] = useState(() => { + return detectTvEnvironment() ? "cinema" : "home"; + }); const [serverUsers, setServerUsers] = useState([]); const [loadingUsers, setLoadingUsers] = useState(false); const [isCollapsed, setIsCollapsed] = useState(() => { @@ -51,6 +88,21 @@ 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); @@ -151,7 +203,7 @@ export default function Dashboard({ }, [activeTab, loadServerUsers]); return ( -
+
{/* Mobile Top Header (Visible on Mobile / Small screens) */}
handleNavClick("home")} - title={isCollapsed ? (isUserAdmin ? "Главная" : "Кинотеатр") : undefined} + title={isCollapsed ? (isUserAdmin ? "Панель сервера" : "Кинотеатр") : undefined} >
{isUserAdmin ? ( @@ -241,7 +293,7 @@ export default function Dashboard({ ) : ( )} - {!isCollapsed && {isUserAdmin ? "Главная" : "Кинотеатр"}} + {!isCollapsed && {isUserAdmin ? "Панель сервера" : "Кинотеатр"}}
@@ -389,6 +441,27 @@ export default function Dashboard({ {dbStatus?.connected ? "Система активна" : "Проверка связи..."}
+ + {isTvEnvironment && ( + + )}
)} @@ -404,13 +477,25 @@ export default function Dashboard({ usersCount={serverUsers.length} /> ) : ( - + handleNavClick(tab as NavTab)} + onLogout={onLogout} + isTvEnvironment={isTvEnvironment} + /> ) )} {activeTab === "storage" && } - {activeTab === "cinema" && isUserAdmin && } + {activeTab === "cinema" && isUserAdmin && ( + handleNavClick(tab as NavTab)} + onLogout={onLogout} + isTvEnvironment={isTvEnvironment} + /> + )} {activeTab === "users" && isUserAdmin && (