feat(tv): zona-like leanback navigation, hidden header in tv mode, left-edge drawer summon without collapse toggle to 0.0.5
This commit is contained in:
parent
6750d7d437
commit
f380fc2add
5 changed files with 237 additions and 124 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "has",
|
"name": "has",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "has"
|
name = "has"
|
||||||
version = "0.0.4"
|
version = "0.0.5"
|
||||||
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.4",
|
"version": "0.0.5",
|
||||||
"identifier": "com.has.tauri-app",
|
"identifier": "com.has.tauri-app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|
|
||||||
|
|
@ -2017,21 +2017,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Android TV Leanback Navigation Drawer (Slide-in Overlay) */
|
/* Android TV Leanback Navigation Drawer (Slide-in Overlay) */
|
||||||
|
/* Android TV Leanback Navigation Drawer (Zona / Android TV 10-Foot UI) */
|
||||||
.has-cinema-tv-nav-drawer {
|
.has-cinema-tv-nav-drawer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 270px;
|
width: 280px;
|
||||||
background: rgba(8, 12, 24, 0.97);
|
background: rgba(8, 12, 24, 0.98);
|
||||||
backdrop-filter: blur(28px);
|
backdrop-filter: blur(28px);
|
||||||
border-right: 1px solid rgba(129, 140, 248, 0.35);
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
transform: translateX(-100%);
|
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;
|
pointer-events: none;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -2045,63 +2046,40 @@
|
||||||
.has-cinema-tv-nav-header {
|
.has-cinema-tv-nav-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
padding: 18px 16px 14px;
|
padding: 24px 20px 18px;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-cinema-tv-nav-logo {
|
.has-cinema-tv-nav-logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-cinema-tv-nav-logo-badge {
|
.has-cinema-tv-nav-logo-badge {
|
||||||
background: linear-gradient(135deg, #6366f1, #a855f7);
|
background: linear-gradient(135deg, #6366f1, #a855f7);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 800;
|
font-weight: 900;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
padding: 4px 8px;
|
padding: 5px 10px;
|
||||||
border-radius: 8px;
|
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 {
|
.has-cinema-tv-nav-logo-text {
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
white-space: nowrap;
|
letter-spacing: 0.5px;
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
.has-cinema-tv-nav-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 10px;
|
padding: 16px 12px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
@ -2111,12 +2089,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 12px 14px;
|
padding: 12px 16px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-size: 15px;
|
font-size: 15.5px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
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:hover,
|
||||||
.has-cinema-tv-nav-item:focus {
|
.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;
|
border-color: #818cf8 !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
outline: 3px solid #818cf8 !important;
|
outline: 3px solid #818cf8 !important;
|
||||||
box-shadow: 0 0 24px rgba(129, 140, 248, 0.8) !important;
|
box-shadow: 0 0 24px rgba(129, 140, 248, 0.85) !important;
|
||||||
transform: scale(1.04) !important;
|
transform: scale(1.03) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-cinema-tv-nav-item.active {
|
.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);
|
border-color: rgba(129, 140, 248, 0.6);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-cinema-tv-nav-item.logout {
|
.has-cinema-tv-nav-item.logout {
|
||||||
|
|
@ -2168,36 +2147,100 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-cinema-tv-nav-footer {
|
.has-cinema-tv-nav-footer {
|
||||||
padding: 12px;
|
padding: 14px 16px;
|
||||||
font-size: 11.5px;
|
font-size: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
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 {
|
.has-tv-mode {
|
||||||
font-size: 14.5px;
|
font-size: 14.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Navigation is completely invisible on cinema screen (summoned by pressing Left from edge) */
|
||||||
.has-tv-mode .has-cinema-header {
|
.has-tv-mode .has-cinema-header {
|
||||||
padding: 16px 54px !important;
|
display: none !important;
|
||||||
}
|
|
||||||
|
|
||||||
.has-tv-mode .has-cinema-nav-links {
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-tv-mode .has-cinema-nav-item {
|
|
||||||
font-size: 15px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-tv-mode .has-cinema-hero {
|
.has-tv-mode .has-cinema-hero {
|
||||||
padding: 85px 54px 30px !important;
|
padding: 40px 54px 30px !important;
|
||||||
min-height: 360px;
|
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 {
|
.has-tv-mode .has-cinema-hero-title {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
|
||||||
|
|
@ -594,7 +594,7 @@ export default function Cinema({
|
||||||
const [, setConfig] = useState<CinemaConfig>({ tmdb_api_key: null });
|
const [, setConfig] = useState<CinemaConfig>({ tmdb_api_key: null });
|
||||||
|
|
||||||
// Navigation Filter
|
// 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 [searchQuery, setSearchQuery] = useState("");
|
||||||
const [searchResults, setSearchResults] = useState<CinemaMediaItem[]>([]);
|
const [searchResults, setSearchResults] = useState<CinemaMediaItem[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
|
@ -1764,7 +1764,19 @@ export default function Cinema({
|
||||||
if (isTvNavOpen) {
|
if (isTvNavOpen) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsTvNavOpen(false);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (showTrailerModal) {
|
if (showTrailerModal) {
|
||||||
|
|
@ -1807,6 +1819,22 @@ export default function Cinema({
|
||||||
focusFirstContentCard();
|
focusFirstContentCard();
|
||||||
return;
|
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<HTMLElement>(".has-cinema-tv-nav-item.active") ||
|
||||||
|
document.querySelector<HTMLElement>(".has-cinema-tv-nav-item");
|
||||||
|
if (targetNav) {
|
||||||
|
targetNav.focus();
|
||||||
|
playTvBeep(640, 0.03);
|
||||||
|
}
|
||||||
|
}, 40);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. D-Pad Directional Arrows Navigation
|
// 3. D-Pad Directional Arrows Navigation
|
||||||
|
|
@ -1853,14 +1881,26 @@ export default function Cinema({
|
||||||
if (direction === "right") {
|
if (direction === "right") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsTvNavOpen(false);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let focusables = Array.from(
|
let focusables = Array.from(
|
||||||
document.querySelectorAll<HTMLElement>(
|
document.querySelectorAll<HTMLElement>(
|
||||||
'[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) => {
|
).filter((el) => {
|
||||||
const r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
|
|
@ -1873,6 +1913,22 @@ export default function Cinema({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TV navigation drawer focus isolation
|
||||||
|
if (isTvNavOpen) {
|
||||||
|
const drawerEl = document.querySelector<HTMLElement>(".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
|
// Trap focus inside modal or drawer if open
|
||||||
if (settingsSheet) {
|
if (settingsSheet) {
|
||||||
focusables = focusables.filter((el) => settingsSheet.contains(el));
|
focusables = focusables.filter((el) => settingsSheet.contains(el));
|
||||||
|
|
@ -1952,10 +2008,11 @@ export default function Cinema({
|
||||||
playTvBeep(640, 0.025);
|
playTvBeep(640, 0.025);
|
||||||
} else {
|
} else {
|
||||||
// If no element found in this direction outside modals:
|
// 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 (!detailsEl && !playerEl && !trailerEl && !settingsSheet && !episodesSheet) {
|
||||||
if (direction === "left" || direction === "right") {
|
if (direction === "left") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
lastFocusedCardRef.current = current;
|
||||||
setIsTvNavOpen(true);
|
setIsTvNavOpen(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const targetNav =
|
const targetNav =
|
||||||
|
|
@ -2276,46 +2333,38 @@ export default function Cinema({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ========================================================
|
{/* ========================================================
|
||||||
ANDROID TV LEANBACK NAVIGATION DRAWER
|
ANDROID TV LEANBACK NAVIGATION DRAWER (Zona TV Experience)
|
||||||
(Opens when pressing Left from cards, remote Menu, or Menu pill)
|
(Summoned only by moving Left from the leftmost card)
|
||||||
======================================================== */}
|
======================================================== */}
|
||||||
{isTvMode && (
|
{isTvMode && (
|
||||||
<aside
|
<aside
|
||||||
className={`has-cinema-tv-nav-drawer ${isTvNavOpen ? "open" : "collapsed"}`}
|
className={`has-cinema-tv-nav-drawer ${isTvNavOpen ? "open" : ""}`}
|
||||||
aria-label="Навигация для ТВ"
|
aria-label="Навигация для ТВ"
|
||||||
>
|
>
|
||||||
<div className="has-cinema-tv-nav-header">
|
<div className="has-cinema-tv-nav-header">
|
||||||
<div className="has-cinema-tv-nav-logo">
|
<div className="has-cinema-tv-nav-logo">
|
||||||
<span className="has-cinema-tv-nav-logo-badge">HAS</span>
|
<span className="has-cinema-tv-nav-logo-badge">HAS</span>
|
||||||
{isTvNavOpen && <span className="has-cinema-tv-nav-logo-text">Кинотеатр</span>}
|
<span className="has-cinema-tv-nav-logo-text">Кинотеатр</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="has-cinema-tv-nav-toggle-btn"
|
|
||||||
onClick={() => setIsTvNavOpen((prev) => !prev)}
|
|
||||||
title={isTvNavOpen ? "Свернуть меню (Вправо)" : "Развернуть меню"}
|
|
||||||
data-tv-focusable="true"
|
|
||||||
>
|
|
||||||
{isTvNavOpen ? "◀" : "▶"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="has-cinema-tv-nav-list">
|
<div className="has-cinema-tv-nav-list">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`has-cinema-tv-nav-item ${searchQuery ? "active" : ""}`}
|
className={`has-cinema-tv-nav-item ${activeTab === "search" || searchQuery ? "active" : ""}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setActiveTab("search");
|
||||||
setIsTvNavOpen(false);
|
setIsTvNavOpen(false);
|
||||||
const searchInput = document.querySelector<HTMLInputElement>(".has-cinema-search-input");
|
setTimeout(() => {
|
||||||
if (searchInput) {
|
const s = document.querySelector<HTMLInputElement>(".has-cinema-tv-search-input-field") || document.querySelector<HTMLInputElement>(".has-cinema-search-input");
|
||||||
searchInput.focus();
|
if (s) s.focus();
|
||||||
}
|
}, 60);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Поиск"
|
title="Поиск"
|
||||||
>
|
>
|
||||||
<span className="has-cinema-tv-nav-icon">🔍</span>
|
<span className="has-cinema-tv-nav-icon">🔍</span>
|
||||||
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Поиск</span>}
|
<span className="has-cinema-tv-nav-label">Поиск</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -2325,13 +2374,13 @@ export default function Cinema({
|
||||||
setActiveTab("home");
|
setActiveTab("home");
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setIsTvNavOpen(false);
|
setIsTvNavOpen(false);
|
||||||
focusFirstContentCard();
|
setTimeout(() => focusFirstContentCard(), 60);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Каталог"
|
title="Каталог"
|
||||||
>
|
>
|
||||||
<span className="has-cinema-tv-nav-icon">🎬</span>
|
<span className="has-cinema-tv-nav-icon">🏠</span>
|
||||||
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Каталог</span>}
|
<span className="has-cinema-tv-nav-label">Каталог</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -2341,13 +2390,13 @@ export default function Cinema({
|
||||||
setActiveTab("movies");
|
setActiveTab("movies");
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setIsTvNavOpen(false);
|
setIsTvNavOpen(false);
|
||||||
focusFirstContentCard();
|
setTimeout(() => focusFirstContentCard(), 60);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Фильмы"
|
title="Фильмы"
|
||||||
>
|
>
|
||||||
<span className="has-cinema-tv-nav-icon">🍿</span>
|
<span className="has-cinema-tv-nav-icon">🎬</span>
|
||||||
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Фильмы</span>}
|
<span className="has-cinema-tv-nav-label">Фильмы</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -2357,13 +2406,13 @@ export default function Cinema({
|
||||||
setActiveTab("series");
|
setActiveTab("series");
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setIsTvNavOpen(false);
|
setIsTvNavOpen(false);
|
||||||
focusFirstContentCard();
|
setTimeout(() => focusFirstContentCard(), 60);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Сериалы"
|
title="Сериалы"
|
||||||
>
|
>
|
||||||
<span className="has-cinema-tv-nav-icon">📺</span>
|
<span className="has-cinema-tv-nav-icon">📺</span>
|
||||||
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Сериалы</span>}
|
<span className="has-cinema-tv-nav-label">Сериалы</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -2373,13 +2422,13 @@ export default function Cinema({
|
||||||
setActiveTab("anime");
|
setActiveTab("anime");
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setIsTvNavOpen(false);
|
setIsTvNavOpen(false);
|
||||||
focusFirstContentCard();
|
setTimeout(() => focusFirstContentCard(), 60);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Аниме"
|
title="Аниме"
|
||||||
>
|
>
|
||||||
<span className="has-cinema-tv-nav-icon">⛩️</span>
|
<span className="has-cinema-tv-nav-icon">🌸</span>
|
||||||
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Аниме</span>}
|
<span className="has-cinema-tv-nav-label">Аниме</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -2389,17 +2438,15 @@ export default function Cinema({
|
||||||
setActiveTab("bookmarks");
|
setActiveTab("bookmarks");
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setIsTvNavOpen(false);
|
setIsTvNavOpen(false);
|
||||||
focusFirstContentCard();
|
setTimeout(() => focusFirstContentCard(), 60);
|
||||||
}}
|
}}
|
||||||
data-tv-focusable="true"
|
data-tv-focusable="true"
|
||||||
title="Мой список"
|
title="Мой список"
|
||||||
>
|
>
|
||||||
<span className="has-cinema-tv-nav-icon">⭐</span>
|
<span className="has-cinema-tv-nav-icon">⭐</span>
|
||||||
{isTvNavOpen && (
|
<span className="has-cinema-tv-nav-label">
|
||||||
<span className="has-cinema-tv-nav-label">
|
Мой список {cinemaState.bookmarks.length > 0 && `(${cinemaState.bookmarks.length})`}
|
||||||
Мой список {cinemaState.bookmarks.length > 0 && `(${cinemaState.bookmarks.length})`}
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{onNavigateTab && (
|
{onNavigateTab && (
|
||||||
|
|
@ -2414,7 +2461,7 @@ export default function Cinema({
|
||||||
title="Панель сервера"
|
title="Панель сервера"
|
||||||
>
|
>
|
||||||
<span className="has-cinema-tv-nav-icon">🖥️</span>
|
<span className="has-cinema-tv-nav-icon">🖥️</span>
|
||||||
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Панель сервера</span>}
|
<span className="has-cinema-tv-nav-label">Панель сервера</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -2430,21 +2477,19 @@ export default function Cinema({
|
||||||
title="Выйти"
|
title="Выйти"
|
||||||
>
|
>
|
||||||
<span className="has-cinema-tv-nav-icon">🚪</span>
|
<span className="has-cinema-tv-nav-icon">🚪</span>
|
||||||
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Выйти</span>}
|
<span className="has-cinema-tv-nav-label">Выйти</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isTvNavOpen && (
|
<div className="has-cinema-tv-nav-footer">
|
||||||
<div className="has-cinema-tv-nav-footer">
|
<span>▶ Вправо или Назад — закрыть меню</span>
|
||||||
<span>▶ Нажмите Вправо для перехода к фильмам</span>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ========================================================
|
{/* ========================================================
|
||||||
1. TOP NAVIGATION BAR (All categories & search)
|
1. TOP NAVIGATION BAR (For Desktop / Mobile only; hidden in TV Mode)
|
||||||
======================================================== */}
|
======================================================== */}
|
||||||
<header className="has-cinema-header">
|
<header className="has-cinema-header">
|
||||||
<nav className="has-cinema-nav-links">
|
<nav className="has-cinema-nav-links">
|
||||||
|
|
@ -2506,17 +2551,6 @@ export default function Cinema({
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="has-cinema-header-right">
|
<div className="has-cinema-header-right">
|
||||||
{isTvMode && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`has-cinema-tv-menu-pill ${isTvNavOpen ? "active" : ""}`}
|
|
||||||
onClick={() => setIsTvNavOpen((prev) => !prev)}
|
|
||||||
title="Меню навигации (Пульт: ◀ или Влево)"
|
|
||||||
data-tv-focusable="true"
|
|
||||||
>
|
|
||||||
☰ Разделы
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Universal Search Box */}
|
{/* Universal Search Box */}
|
||||||
<div className="has-cinema-search-wrapper">
|
<div className="has-cinema-search-wrapper">
|
||||||
|
|
@ -2560,6 +2594,42 @@ export default function Cinema({
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{/* ========================================================
|
||||||
|
TV SEARCH VIEW (When activeTab === "search" in TV Mode)
|
||||||
|
======================================================== */}
|
||||||
|
{isTvMode && activeTab === "search" && (
|
||||||
|
<section className="has-cinema-tv-search-page">
|
||||||
|
<div className="has-cinema-tv-search-box-wrap">
|
||||||
|
<span className="has-cinema-tv-search-lens">🔍</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="has-cinema-tv-search-input-field"
|
||||||
|
placeholder="Введите название фильма, сериала или аниме..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
data-tv-focusable="true"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="has-cinema-tv-search-clear-btn"
|
||||||
|
onClick={() => setSearchQuery("")}
|
||||||
|
data-tv-focusable="true"
|
||||||
|
title="Очистить"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{searchQuery.trim().length === 0 && (
|
||||||
|
<div className="has-cinema-tv-search-hint">
|
||||||
|
<span>💡 Введите название с клавиатуры или пульта. Нажмите Влево с края для вызова меню.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ========================================================
|
{/* ========================================================
|
||||||
SEARCH RESULTS FULL GRID VIEW (When Searching)
|
SEARCH RESULTS FULL GRID VIEW (When Searching)
|
||||||
======================================================== */}
|
======================================================== */}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue