From f380fc2add0983a0332e2686efec57488dcc3fb8 Mon Sep 17 00:00:00 2001 From: Autli Date: Tue, 27 Oct 2026 12:07:20 +0500 Subject: [PATCH] feat(tv): zona-like leanback navigation, hidden header in tv mode, left-edge drawer summon without collapse toggle to 0.0.5 --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/pages/cinema/Cinema.css | 163 +++++++++++++++++++----------- src/pages/cinema/Cinema.tsx | 192 ++++++++++++++++++++++++------------ 5 files changed, 237 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index c4dca4e..f4779f8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "has", "private": true, - "version": "0.0.4", + "version": "0.0.5", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 851b682..ef89e1b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "has" -version = "0.0.4" +version = "0.0.5" description = "has" authors = ["autli"] edition = "2024" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0b79f81..ff8d8e0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "has", - "version": "0.0.4", + "version": "0.0.5", "identifier": "com.has.tauri-app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/pages/cinema/Cinema.css b/src/pages/cinema/Cinema.css index a12e84a..d2fa75b 100644 --- a/src/pages/cinema/Cinema.css +++ b/src/pages/cinema/Cinema.css @@ -2017,21 +2017,22 @@ } /* Android TV Leanback Navigation Drawer (Slide-in Overlay) */ +/* Android TV Leanback Navigation Drawer (Zona / Android TV 10-Foot UI) */ .has-cinema-tv-nav-drawer { position: fixed; top: 0; left: 0; bottom: 0; - width: 270px; - background: rgba(8, 12, 24, 0.97); + width: 280px; + background: rgba(8, 12, 24, 0.98); backdrop-filter: blur(28px); border-right: 1px solid rgba(129, 140, 248, 0.35); - box-shadow: 15px 0 50px rgba(0, 0, 0, 0.9); + box-shadow: 20px 0 60px rgba(0, 0, 0, 0.95); display: flex; flex-direction: column; z-index: 2000; transform: translateX(-100%); - transition: transform 0.26s cubic-bezier(0.16, 1, 0.3, 1); + transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); pointer-events: none; visibility: hidden; } @@ -2045,63 +2046,40 @@ .has-cinema-tv-nav-header { display: flex; align-items: center; - justify-content: space-between; - padding: 18px 16px 14px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); + justify-content: flex-start; + padding: 24px 20px 18px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); } .has-cinema-tv-nav-logo { display: flex; align-items: center; - gap: 10px; - overflow: hidden; + gap: 12px; } .has-cinema-tv-nav-logo-badge { background: linear-gradient(135deg, #6366f1, #a855f7); color: #fff; - font-weight: 800; - font-size: 13px; - padding: 4px 8px; + font-weight: 900; + font-size: 14px; + padding: 5px 10px; border-radius: 8px; - letter-spacing: 1px; + letter-spacing: 1.5px; + box-shadow: 0 0 16px rgba(99, 102, 241, 0.5); } .has-cinema-tv-nav-logo-text { - font-size: 16px; - font-weight: 700; + font-size: 18px; + font-weight: 800; 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; + letter-spacing: 0.5px; } .has-cinema-tv-nav-list { display: flex; flex-direction: column; gap: 8px; - padding: 12px 10px; + padding: 16px 12px; flex: 1; overflow-y: auto; } @@ -2111,12 +2089,12 @@ display: flex; align-items: center; gap: 14px; - padding: 12px 14px; + padding: 12px 16px; background: transparent; border: 1px solid transparent; border-radius: 12px; color: #94a3b8; - font-size: 15px; + font-size: 15.5px; font-weight: 600; cursor: pointer; transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); @@ -2125,18 +2103,19 @@ .has-cinema-tv-nav-item:hover, .has-cinema-tv-nav-item:focus { - background: rgba(99, 102, 241, 0.25) !important; + background: rgba(99, 102, 241, 0.3) !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; + box-shadow: 0 0 24px rgba(129, 140, 248, 0.85) !important; + transform: scale(1.03) !important; } .has-cinema-tv-nav-item.active { - background: linear-gradient(135deg, rgba(99, 102, 241, 0.35), rgba(139, 92, 246, 0.35)); + background: linear-gradient(135deg, rgba(99, 102, 241, 0.4), rgba(139, 92, 246, 0.4)); border-color: rgba(129, 140, 248, 0.6); color: #ffffff; + font-weight: 700; } .has-cinema-tv-nav-item.logout { @@ -2168,36 +2147,100 @@ } .has-cinema-tv-nav-footer { - padding: 12px; - font-size: 11.5px; + padding: 14px 16px; + font-size: 12px; color: #64748b; text-align: center; border-top: 1px solid rgba(255, 255, 255, 0.08); } -/* TV Mode Root Overrides (Safe Overscan Area & Compact Proportions) */ +/* TV Mode Root Overrides (Zona TV & Android TV Experience) */ .has-tv-mode { font-size: 14.5px; } +/* Navigation is completely invisible on cinema screen (summoned by pressing Left from edge) */ .has-tv-mode .has-cinema-header { - padding: 16px 54px !important; -} - -.has-tv-mode .has-cinema-nav-links { - gap: 20px; -} - -.has-tv-mode .has-cinema-nav-item { - font-size: 15px; - padding: 6px 10px; + display: none !important; } .has-tv-mode .has-cinema-hero { - padding: 85px 54px 30px !important; + padding: 40px 54px 30px !important; min-height: 360px; } +/* Dedicated TV Search Section when activeTab === "search" */ +.has-cinema-tv-search-page { + padding: 40px 54px 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.has-cinema-tv-search-box-wrap { + display: flex; + align-items: center; + gap: 14px; + background: rgba(15, 23, 42, 0.85); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 16px; + padding: 14px 22px; + backdrop-filter: blur(16px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.has-cinema-tv-search-box-wrap:focus-within { + border-color: #818cf8; + box-shadow: 0 0 25px rgba(129, 140, 248, 0.6); +} + +.has-cinema-tv-search-lens { + font-size: 22px; +} + +.has-cinema-tv-search-input-field { + flex: 1; + background: transparent; + border: none; + outline: none; + color: #ffffff; + font-size: 18px; + font-weight: 600; +} + +.has-cinema-tv-search-input-field::placeholder { + color: #64748b; +} + +.has-cinema-tv-search-clear-btn { + background: rgba(255, 255, 255, 0.1); + border: none; + color: #94a3b8; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.has-cinema-tv-search-clear-btn:hover, +.has-cinema-tv-search-clear-btn:focus { + background: #ef4444; + color: #ffffff; + outline: 2px solid #ffffff; +} + +.has-cinema-tv-search-hint { + font-size: 13.5px; + color: #94a3b8; + padding-left: 6px; +} + .has-tv-mode .has-cinema-hero-title { font-size: 30px; margin-bottom: 8px; diff --git a/src/pages/cinema/Cinema.tsx b/src/pages/cinema/Cinema.tsx index 3087657..c8025c0 100644 --- a/src/pages/cinema/Cinema.tsx +++ b/src/pages/cinema/Cinema.tsx @@ -594,7 +594,7 @@ export default function Cinema({ const [, setConfig] = useState({ tmdb_api_key: null }); // Navigation Filter - const [activeTab, setActiveTab] = useState<"home" | "movies" | "series" | "anime" | "bookmarks">("home"); + const [activeTab, setActiveTab] = useState<"home" | "movies" | "series" | "anime" | "bookmarks" | "search">("home"); const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); @@ -1764,7 +1764,19 @@ export default function Cinema({ if (isTvNavOpen) { e.preventDefault(); setIsTvNavOpen(false); - focusFirstContentCard(); + setTimeout(() => { + if (lastFocusedCardRef.current && document.contains(lastFocusedCardRef.current)) { + lastFocusedCardRef.current.focus(); + lastFocusedCardRef.current.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center", + }); + playTvBeep(640, 0.03); + } else { + focusFirstContentCard(); + } + }, 40); return; } if (showTrailerModal) { @@ -1807,6 +1819,22 @@ export default function Cinema({ focusFirstContentCard(); return; } + // At home root on TV, Back button summons the navigation drawer + if (isTvMode && !isTvNavOpen) { + e.preventDefault(); + lastFocusedCardRef.current = document.activeElement as HTMLElement | null; + 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); + return; + } } // 3. D-Pad Directional Arrows Navigation @@ -1853,14 +1881,26 @@ export default function Cinema({ if (direction === "right") { e.preventDefault(); setIsTvNavOpen(false); - focusFirstContentCard(); + setTimeout(() => { + if (lastFocusedCardRef.current && document.contains(lastFocusedCardRef.current)) { + lastFocusedCardRef.current.focus(); + lastFocusedCardRef.current.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center", + }); + playTvBeep(640, 0.03); + } else { + focusFirstContentCard(); + } + }, 40); return; } } let focusables = Array.from( document.querySelectorAll( - '[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' + '[data-tv-focusable="true"], .has-cinema-nav-item, .has-cinema-tv-nav-item, .has-cinema-btn-play, .has-cinema-btn-bookmark, .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(); @@ -1873,6 +1913,22 @@ export default function Cinema({ ); }); + // TV navigation drawer focus isolation + if (isTvNavOpen) { + const drawerEl = document.querySelector(".has-cinema-tv-nav-drawer"); + if (drawerEl) { + focusables = focusables.filter((el) => drawerEl.contains(el)); + } + } else { + // When drawer is closed, NEVER include drawer items in focusable pool + focusables = focusables.filter((el) => !el.closest(".has-cinema-tv-nav-drawer")); + } + + // In TV mode, top header is hidden, so never include header elements + if (isTvMode) { + focusables = focusables.filter((el) => !el.closest(".has-cinema-header")); + } + // Trap focus inside modal or drawer if open if (settingsSheet) { focusables = focusables.filter((el) => settingsSheet.contains(el)); @@ -1952,10 +2008,11 @@ export default function Cinema({ 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) + // When moving LEFT from the leftmost card, summon the TV Navigation Drawer! if (!detailsEl && !playerEl && !trailerEl && !settingsSheet && !episodesSheet) { - if (direction === "left" || direction === "right") { + if (direction === "left") { e.preventDefault(); + lastFocusedCardRef.current = current; setIsTvNavOpen(true); setTimeout(() => { const targetNav = @@ -2276,46 +2333,38 @@ export default function Cinema({ )} {/* ======================================================== - ANDROID TV LEANBACK NAVIGATION DRAWER - (Opens when pressing Left from cards, remote Menu, or Menu pill) + ANDROID TV LEANBACK NAVIGATION DRAWER (Zona TV Experience) + (Summoned only by moving Left from the leftmost card) ======================================================== */} {isTvMode && ( )} {/* ======================================================== - 1. TOP NAVIGATION BAR (All categories & search) + 1. TOP NAVIGATION BAR (For Desktop / Mobile only; hidden in TV Mode) ======================================================== */}
- {isTvMode && ( - - )} {/* Universal Search Box */}
@@ -2560,6 +2594,42 @@ export default function Cinema({
+ {/* ======================================================== + TV SEARCH VIEW (When activeTab === "search" in TV Mode) + ======================================================== */} + {isTvMode && activeTab === "search" && ( +
+
+ 🔍 + setSearchQuery(e.target.value)} + data-tv-focusable="true" + autoFocus + /> + {searchQuery && ( + + )} +
+ {searchQuery.trim().length === 0 && ( +
+ 💡 Введите название с клавиатуры или пульта. Нажмите Влево с края для вызова меню. +
+ )} +
+ )} + {/* ======================================================== SEARCH RESULTS FULL GRID VIEW (When Searching) ======================================================== */}