diff --git a/app/(auth)/(tabs)/(favorites)/index.tsx b/app/(auth)/(tabs)/(favorites)/index.tsx
index 198695a8..a3c83c04 100644
--- a/app/(auth)/(tabs)/(favorites)/index.tsx
+++ b/app/(auth)/(tabs)/(favorites)/index.tsx
@@ -2,6 +2,7 @@ import { useCallback, useState } from "react";
import { Platform, RefreshControl, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Favorites } from "@/components/home/Favorites";
+import { Favorites as TVFavorites } from "@/components/home/Favorites.tv";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
export default function favorites() {
@@ -15,6 +16,10 @@ export default function favorites() {
}, []);
const insets = useSafeAreaInsets();
+ if (Platform.isTV) {
+ return ;
+ }
+
return (
({
onSelect: (value: T) => void;
onClose: () => void;
}) => {
+ const [isReady, setIsReady] = useState(false);
+ const firstCardRef = useRef(null);
+
const initialSelectedIndex = useMemo(() => {
const idx = options.findIndex((o) => o.selected);
return idx >= 0 ? idx : 0;
}, [options]);
+ // Delay rendering to work around hasTVPreferredFocus timing issue
+ useEffect(() => {
+ if (visible) {
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => clearTimeout(timer);
+ }
+ setIsReady(false);
+ }, [visible]);
+
+ // Programmatic focus fallback
+ useEffect(() => {
+ if (isReady && firstCardRef.current) {
+ const timer = setTimeout(() => {
+ (firstCardRef.current as any)?.requestTVFocus?.();
+ }, 50);
+ return () => clearTimeout(timer);
+ }
+ }, [isReady]);
+
if (!visible) return null;
return (
@@ -337,7 +362,12 @@ const TVOptionSelector = ({
overflow: "hidden",
}}
>
- ({
{/* Horizontal options */}
-
- {options.map((option, index) => (
- {
- onSelect(option.value);
- onClose();
- }}
- />
- ))}
-
-
+ {isReady && (
+
+ {options.map((option, index) => (
+ {
+ onSelect(option.value);
+ onClose();
+ }}
+ />
+ ))}
+
+ )}
+
);
};
-// Option card for horizontal selector (Apple TV style)
-const TVOptionCard: React.FC<{
- label: string;
- selected: boolean;
- hasTVPreferredFocus?: boolean;
- onPress: () => void;
-}> = ({ label, selected, hasTVPreferredFocus, onPress }) => {
+// Option card for horizontal selector (Apple TV style) - with forwardRef for programmatic focus
+const TVOptionCard = React.forwardRef<
+ View,
+ {
+ label: string;
+ selected: boolean;
+ hasTVPreferredFocus?: boolean;
+ onPress: () => void;
+ }
+>(({ label, selected, hasTVPreferredFocus, onPress }, ref) => {
const [focused, setFocused] = useState(false);
const scale = useRef(new Animated.Value(1)).current;
@@ -409,6 +447,7 @@ const TVOptionCard: React.FC<{
return (
{
setFocused(true);
@@ -465,7 +504,7 @@ const TVOptionCard: React.FC<{
);
-};
+});
// Button to open option selector
const TVOptionButton: React.FC<{
@@ -606,6 +645,21 @@ export const ItemContentTV: React.FC = React.memo(
// Modal state for option selectors
type ModalType = "audio" | "subtitle" | "mediaSource" | "quality" | null;
const [openModal, setOpenModal] = useState(null);
+ const isModalOpen = openModal !== null;
+
+ // Android TV BackHandler for closing modals
+ useEffect(() => {
+ if (Platform.OS === "android" && isModalOpen) {
+ const backHandler = BackHandler.addEventListener(
+ "hardwareBackPress",
+ () => {
+ setOpenModal(null);
+ return true;
+ },
+ );
+ return () => backHandler.remove();
+ }
+ }, [isModalOpen]);
// Get available audio tracks
const audioTracks = useMemo(() => {
diff --git a/components/home/Favorites.tv.tsx b/components/home/Favorites.tv.tsx
new file mode 100644
index 00000000..6da3befa
--- /dev/null
+++ b/components/home/Favorites.tv.tsx
@@ -0,0 +1,231 @@
+import type { Api } from "@jellyfin/sdk";
+import type { BaseItemKind } from "@jellyfin/sdk/lib/generated-client";
+import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
+import { Image } from "expo-image";
+import { useAtom } from "jotai";
+import { useCallback, useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { ScrollView, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import heart from "@/assets/icons/heart.fill.png";
+import { Text } from "@/components/common/Text";
+import { InfiniteScrollingCollectionList } from "@/components/home/InfiniteScrollingCollectionList.tv";
+import { Colors } from "@/constants/Colors";
+import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
+
+const HORIZONTAL_PADDING = 60;
+const TOP_PADDING = 100;
+const SECTION_GAP = 10;
+
+type FavoriteTypes =
+ | "Series"
+ | "Movie"
+ | "Episode"
+ | "Video"
+ | "BoxSet"
+ | "Playlist";
+type EmptyState = Record;
+
+export const Favorites = () => {
+ const { t } = useTranslation();
+ const insets = useSafeAreaInsets();
+ const [api] = useAtom(apiAtom);
+ const [user] = useAtom(userAtom);
+ const pageSize = 20;
+ const [emptyState, setEmptyState] = useState({
+ Series: false,
+ Movie: false,
+ Episode: false,
+ Video: false,
+ BoxSet: false,
+ Playlist: false,
+ });
+
+ const fetchFavoritesByType = useCallback(
+ async (
+ itemType: BaseItemKind,
+ startIndex: number = 0,
+ limit: number = 20,
+ ) => {
+ const response = await getItemsApi(api as Api).getItems({
+ userId: user?.Id,
+ sortBy: ["SeriesSortName", "SortName"],
+ sortOrder: ["Ascending"],
+ filters: ["IsFavorite"],
+ recursive: true,
+ fields: ["PrimaryImageAspectRatio"],
+ collapseBoxSetItems: false,
+ excludeLocationTypes: ["Virtual"],
+ enableTotalRecordCount: false,
+ startIndex: startIndex,
+ limit: limit,
+ includeItemTypes: [itemType],
+ });
+ const items = response.data.Items || [];
+
+ if (startIndex === 0) {
+ setEmptyState((prev) => ({
+ ...prev,
+ [itemType as FavoriteTypes]: items.length === 0,
+ }));
+ }
+
+ return items;
+ },
+ [api, user],
+ );
+
+ useEffect(() => {
+ setEmptyState({
+ Series: false,
+ Movie: false,
+ Episode: false,
+ Video: false,
+ BoxSet: false,
+ Playlist: false,
+ });
+ }, [api, user]);
+
+ const areAllEmpty = () => {
+ const loadedCategories = Object.values(emptyState);
+ return (
+ loadedCategories.length > 0 &&
+ loadedCategories.every((isEmpty) => isEmpty)
+ );
+ };
+
+ const fetchFavoriteSeries = useCallback(
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("Series", pageParam, pageSize),
+ [fetchFavoritesByType, pageSize],
+ );
+ const fetchFavoriteMovies = useCallback(
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("Movie", pageParam, pageSize),
+ [fetchFavoritesByType, pageSize],
+ );
+ const fetchFavoriteEpisodes = useCallback(
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("Episode", pageParam, pageSize),
+ [fetchFavoritesByType, pageSize],
+ );
+ const fetchFavoriteVideos = useCallback(
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("Video", pageParam, pageSize),
+ [fetchFavoritesByType, pageSize],
+ );
+ const fetchFavoriteBoxsets = useCallback(
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("BoxSet", pageParam, pageSize),
+ [fetchFavoritesByType, pageSize],
+ );
+ const fetchFavoritePlaylists = useCallback(
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("Playlist", pageParam, pageSize),
+ [fetchFavoritesByType, pageSize],
+ );
+
+ if (areAllEmpty()) {
+ return (
+
+
+
+ {t("favorites.noDataTitle")}
+
+
+ {t("favorites.noData")}
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/components/library/TVLibraries.tsx b/components/library/TVLibraries.tsx
index 55e96db4..b98ffa09 100644
--- a/components/library/TVLibraries.tsx
+++ b/components/library/TVLibraries.tsx
@@ -1,27 +1,235 @@
-import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
-import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
+import { Ionicons } from "@expo/vector-icons";
+import type {
+ BaseItemDto,
+ CollectionType,
+} from "@jellyfin/sdk/lib/generated-client/models";
+import { getItemsApi, getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
+import { BlurView } from "expo-blur";
+import { Image } from "expo-image";
+import { LinearGradient } from "expo-linear-gradient";
import { useAtom } from "jotai";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
-import { FlatList, View } from "react-native";
+import { Animated, Easing, FlatList, Pressable, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
-import { getItemNavigation } from "@/components/common/TouchableItemRouter";
import { Loader } from "@/components/Loader";
-import {
- TV_LIBRARY_CARD_WIDTH,
- TVLibraryCard,
-} from "@/components/library/TVLibraryCard";
-import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
import useRouter from "@/hooks/useAppRouter";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
+import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
-const HORIZONTAL_PADDING = 60;
-const ITEM_GAP = 24;
+const HORIZONTAL_PADDING = 80;
+const CARD_HEIGHT = 220;
+const CARD_GAP = 24;
const SCALE_PADDING = 20;
+type IconName = React.ComponentProps["name"];
+
+const icons: Record = {
+ movies: "film",
+ tvshows: "tv",
+ music: "musical-notes",
+ books: "book",
+ homevideos: "videocam",
+ boxsets: "albums",
+ playlists: "list",
+ folders: "folder",
+ livetv: "tv",
+ musicvideos: "musical-notes",
+ photos: "images",
+ trailers: "videocam",
+ unknown: "help-circle",
+} as const;
+
+interface LibraryWithPreview extends BaseItemDto {
+ previewItems?: BaseItemDto[];
+ itemCount?: number;
+}
+
+const TVLibraryRow: React.FC<{
+ library: LibraryWithPreview;
+ isFirst: boolean;
+ onPress: () => void;
+}> = ({ library, isFirst, onPress }) => {
+ const [api] = useAtom(apiAtom);
+ const { t } = useTranslation();
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new Animated.Value(1)).current;
+ const opacity = useRef(new Animated.Value(0.7)).current;
+
+ const animateTo = (toScale: number, toOpacity: number) => {
+ Animated.parallel([
+ Animated.timing(scale, {
+ toValue: toScale,
+ duration: 200,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(opacity, {
+ toValue: toOpacity,
+ duration: 200,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ ]).start();
+ };
+
+ const backdropUrl = useMemo(() => {
+ // Try to get backdrop from library or first preview item
+ if (library.previewItems?.[0]) {
+ return getBackdropUrl({
+ api,
+ item: library.previewItems[0],
+ width: 1920,
+ });
+ }
+ return getBackdropUrl({
+ api,
+ item: library,
+ width: 1920,
+ });
+ }, [api, library]);
+
+ const iconName = icons[library.CollectionType!] || "folder";
+
+ const itemTypeName = useMemo(() => {
+ if (library.CollectionType === "movies")
+ return t("library.item_types.movies");
+ if (library.CollectionType === "tvshows")
+ return t("library.item_types.series");
+ if (library.CollectionType === "boxsets")
+ return t("library.item_types.boxsets");
+ if (library.CollectionType === "music")
+ return t("library.item_types.items");
+ return t("library.item_types.items");
+ }, [library.CollectionType, t]);
+
+ return (
+ {
+ setFocused(true);
+ animateTo(1.02, 1);
+ }}
+ onBlur={() => {
+ setFocused(false);
+ animateTo(1, 0.7);
+ }}
+ hasTVPreferredFocus={isFirst}
+ >
+
+ {/* Background Image */}
+ {backdropUrl && (
+
+ )}
+
+ {/* Gradient Overlay */}
+
+
+ {/* Content */}
+
+ {/* Icon Container */}
+
+
+
+
+ {/* Text Content */}
+
+
+ {library.Name}
+
+ {library.itemCount !== undefined && (
+
+ {library.itemCount} {itemTypeName}
+
+ )}
+
+
+ {/* Arrow Indicator */}
+
+
+
+
+
+
+ );
+};
+
export const TVLibraries: React.FC = () => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
@@ -29,85 +237,105 @@ export const TVLibraries: React.FC = () => {
const insets = useSafeAreaInsets();
const router = useRouter();
const { t } = useTranslation();
- const flatListRef = useRef>(null);
- const [focusedCount, setFocusedCount] = useState(0);
- const prevFocusedCount = useRef(0);
- const { data, isLoading } = useQuery({
+ const { data: userViews, isLoading: viewsLoading } = useQuery({
queryKey: ["user-views", user?.Id],
queryFn: async () => {
const response = await getUserViewsApi(api!).getUserViews({
userId: user?.Id,
});
-
- return response.data.Items || null;
+ return response.data.Items || [];
},
- staleTime: 60,
+ staleTime: 60 * 1000,
enabled: !!api && !!user?.Id,
});
const libraries = useMemo(
() =>
- data
+ userViews
?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!))
.filter((l) => l.CollectionType !== "books") || [],
- [data, settings?.hiddenLibraries],
+ [userViews, settings?.hiddenLibraries],
);
- // Scroll back to start when section loses focus
- useEffect(() => {
- if (prevFocusedCount.current > 0 && focusedCount === 0) {
- flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
- }
- prevFocusedCount.current = focusedCount;
- }, [focusedCount]);
+ // Fetch item counts and preview items for each library
+ const { data: librariesWithData, isLoading: dataLoading } = useQuery({
+ queryKey: ["library-data", libraries.map((l) => l.Id).join(",")],
+ queryFn: async () => {
+ const results: LibraryWithPreview[] = await Promise.all(
+ libraries.map(async (library) => {
+ let itemType: string | undefined;
+ if (library.CollectionType === "movies") itemType = "Movie";
+ else if (library.CollectionType === "tvshows") itemType = "Series";
+ else if (library.CollectionType === "boxsets") itemType = "BoxSet";
- const handleItemFocus = useCallback(() => {
- setFocusedCount((c) => c + 1);
- }, []);
+ // Fetch count
+ const countResponse = await getItemsApi(api!).getItems({
+ userId: user?.Id,
+ parentId: library.Id,
+ recursive: true,
+ limit: 0,
+ includeItemTypes: itemType ? [itemType as any] : undefined,
+ });
- const handleItemBlur = useCallback(() => {
- setFocusedCount((c) => Math.max(0, c - 1));
- }, []);
+ // Fetch preview items with backdrops
+ const previewResponse = await getItemsApi(api!).getItems({
+ userId: user?.Id,
+ parentId: library.Id,
+ recursive: true,
+ limit: 1,
+ sortBy: ["Random"],
+ includeItemTypes: itemType ? [itemType as any] : undefined,
+ imageTypes: ["Backdrop"],
+ });
- const handleItemPress = useCallback(
- (item: BaseItemDto) => {
- const navigation = getItemNavigation(item, "(libraries)");
- router.push(navigation as any);
+ return {
+ ...library,
+ itemCount: countResponse.data.TotalRecordCount,
+ previewItems: previewResponse.data.Items || [],
+ };
+ }),
+ );
+ return results;
+ },
+ enabled: !!api && !!user?.Id && libraries.length > 0,
+ staleTime: 60 * 1000,
+ });
+
+ const handleLibraryPress = useCallback(
+ (library: BaseItemDto) => {
+ if (library.CollectionType === "music") {
+ router.push({
+ pathname: `/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions`,
+ params: { libraryId: library.Id! },
+ });
+ } else {
+ router.push({
+ pathname: "/(auth)/(tabs)/(libraries)/[libraryId]",
+ params: { libraryId: library.Id! },
+ });
+ }
},
[router],
);
- const getItemLayout = useCallback(
- (_data: ArrayLike | null | undefined, index: number) => ({
- length: TV_LIBRARY_CARD_WIDTH + ITEM_GAP,
- offset: (TV_LIBRARY_CARD_WIDTH + ITEM_GAP) * index,
- index,
- }),
- [],
- );
-
const renderItem = useCallback(
- ({ item, index }: { item: BaseItemDto; index: number }) => {
- const isFirstItem = index === 0;
-
- return (
-
- handleItemPress(item)}
- hasTVPreferredFocus={isFirstItem}
- onFocus={handleItemFocus}
- onBlur={handleItemBlur}
- >
-
-
-
- );
- },
- [handleItemPress, handleItemFocus, handleItemBlur],
+ ({ item, index }: { item: LibraryWithPreview; index: number }) => (
+
+ handleLibraryPress(item)}
+ />
+
+ ),
+ [handleLibraryPress],
);
- if (isLoading) {
+ const isLoading = viewsLoading || dataLoading;
+ const displayLibraries = librariesWithData || libraries;
+
+ if (isLoading && libraries.length === 0) {
return (
{
);
}
- if (!libraries || libraries.length === 0) {
+ if (!displayLibraries || displayLibraries.length === 0) {
return (
{
item.Id || ""}
renderItem={renderItem}
- showsHorizontalScrollIndicator={false}
- getItemLayout={getItemLayout}
- style={{ overflow: "visible", flexGrow: 0 }}
+ showsVerticalScrollIndicator={false}
+ removeClippedSubviews={false}
contentContainerStyle={{
+ paddingBottom: 40,
+ paddingHorizontal: insets.left + HORIZONTAL_PADDING,
paddingVertical: SCALE_PADDING,
- paddingHorizontal: SCALE_PADDING,
}}
/>
diff --git a/components/video-player/controls/Controls.tv.tsx b/components/video-player/controls/Controls.tv.tsx
index 1c720b09..433d8a52 100644
--- a/components/video-player/controls/Controls.tv.tsx
+++ b/components/video-player/controls/Controls.tv.tsx
@@ -7,7 +7,7 @@ import type {
import { BlurView } from "expo-blur";
import { useLocalSearchParams } from "expo-router";
import { useAtomValue } from "jotai";
-import {
+import React, {
type FC,
useCallback,
useEffect,
@@ -17,12 +17,15 @@ import {
} from "react";
import { useTranslation } from "react-i18next";
import {
+ BackHandler,
Image,
+ Platform,
Pressable,
Animated as RNAnimated,
Easing as RNEasing,
ScrollView,
StyleSheet,
+ TVFocusGuideView,
View,
} from "react-native";
import Animated, {
@@ -99,50 +102,87 @@ const TVOptionSelector = ({
onSelect: (value: T) => void;
onClose: () => void;
}) => {
+ const [isReady, setIsReady] = useState(false);
+ const firstCardRef = useRef(null);
+
const initialSelectedIndex = useMemo(() => {
const idx = options.findIndex((o) => o.selected);
return idx >= 0 ? idx : 0;
}, [options]);
+ // Delay rendering to work around hasTVPreferredFocus timing issue
+ useEffect(() => {
+ if (visible) {
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => clearTimeout(timer);
+ }
+ setIsReady(false);
+ }, [visible]);
+
+ // Programmatic focus fallback
+ useEffect(() => {
+ if (isReady && firstCardRef.current) {
+ const timer = setTimeout(() => {
+ (firstCardRef.current as any)?.requestTVFocus?.();
+ }, 50);
+ return () => clearTimeout(timer);
+ }
+ }, [isReady]);
+
if (!visible) return null;
return (
-
+
{title}
-
- {options.map((option, index) => (
- {
- onSelect(option.value);
- onClose();
- }}
- />
- ))}
-
-
+ {isReady && (
+
+ {options.map((option, index) => (
+ {
+ onSelect(option.value);
+ onClose();
+ }}
+ />
+ ))}
+
+ )}
+
);
};
-// Option card for horizontal selector
-const TVOptionCard: FC<{
- label: string;
- selected: boolean;
- hasTVPreferredFocus?: boolean;
- onPress: () => void;
-}> = ({ label, selected, hasTVPreferredFocus, onPress }) => {
+// Option card for horizontal selector (with forwardRef for programmatic focus)
+const TVOptionCard = React.forwardRef<
+ View,
+ {
+ label: string;
+ selected: boolean;
+ hasTVPreferredFocus?: boolean;
+ onPress: () => void;
+ }
+>(({ label, selected, hasTVPreferredFocus, onPress }, ref) => {
const [focused, setFocused] = useState(false);
const scale = useRef(new RNAnimated.Value(1)).current;
@@ -156,6 +196,7 @@ const TVOptionCard: FC<{
return (
{
setFocused(true);
@@ -202,7 +243,7 @@ const TVOptionCard: FC<{
);
-};
+});
// Settings panel with tabs for Audio and Subtitles
const _TVSettingsPanel: FC<{
@@ -782,6 +823,20 @@ export const Controls: FC = ({
// Track which button last opened a modal (for returning focus)
const [lastOpenedModal, setLastOpenedModal] = useState(null);
+ // Android TV BackHandler for closing modals
+ useEffect(() => {
+ if (Platform.OS === "android" && isModalOpen) {
+ const backHandler = BackHandler.addEventListener(
+ "hardwareBackPress",
+ () => {
+ setOpenModal(null);
+ return true;
+ },
+ );
+ return () => backHandler.remove();
+ }
+ }, [isModalOpen]);
+
// Get available audio tracks
const audioTracks = useMemo(() => {
return mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") ?? [];
@@ -951,6 +1006,7 @@ export const Controls: FC = ({
const { isSliding: isRemoteSliding } = useRemoteControl({
showControls,
toggleControls,
+ togglePlay,
onBack: handleBack,
});
diff --git a/components/video-player/controls/hooks/useRemoteControl.ts b/components/video-player/controls/hooks/useRemoteControl.ts
index c279f649..406a99c3 100644
--- a/components/video-player/controls/hooks/useRemoteControl.ts
+++ b/components/video-player/controls/hooks/useRemoteControl.ts
@@ -42,10 +42,12 @@ interface UseRemoteControlProps {
* Simplified version - D-pad navigation is handled by native focus system.
* This hook handles:
* - Showing controls on any button press
+ * - Play/pause button on TV remote
*/
export function useRemoteControl({
showControls,
toggleControls,
+ togglePlay,
onBack,
}: UseRemoteControlProps) {
// Keep these for backward compatibility with the component
@@ -67,6 +69,14 @@ export function useRemoteControl({
return;
}
+ // Handle play/pause button press on TV remote
+ if (evt.eventType === "playPause") {
+ if (togglePlay) {
+ togglePlay();
+ }
+ return;
+ }
+
// Show controls on any D-pad press
if (!showControls) {
toggleControls();