fix: conditionals for tv to build / run

This commit is contained in:
Fredrik Burmester
2026-01-16 08:04:09 +01:00
parent 36304ad58e
commit 4ad103acb6
11 changed files with 249 additions and 48 deletions

View File

@@ -23,7 +23,7 @@ export const Tag: React.FC<
textStyle?: StyleProp<TextStyle>;
} & ViewProps
> = ({ text, textClass, textStyle, ...props }) => {
if (Platform.OS === "ios") {
if (Platform.OS === "ios" && !Platform.isTV) {
return (
<View>
<GlassEffectView style={styles.glass}>

View File

@@ -1,50 +1,125 @@
import React, { useState } from "react";
import { useRef, useState } from "react";
import {
Animated,
Easing,
Platform,
Pressable,
TextInput,
type TextInputProps,
TouchableOpacity,
View,
} from "react-native";
interface InputProps extends TextInputProps {
extraClassName?: string; // new prop for additional classes
extraClassName?: string;
}
export function Input(props: InputProps) {
const { style, extraClassName = "", ...otherProps } = props;
const inputRef = React.useRef<TextInput>(null);
const inputRef = useRef<TextInput>(null);
const [isFocused, setIsFocused] = useState(false);
const scale = useRef(new Animated.Value(1)).current;
return Platform.isTV ? (
<TouchableOpacity
onPress={() => inputRef?.current?.focus?.()}
activeOpacity={1}
>
<TextInput
ref={inputRef}
className={`
w-full text-lg px-5 py-4 rounded-2xl
${isFocused ? "bg-neutral-700 border-2 border-white" : "bg-neutral-900 border-2 border-transparent"}
text-white ${extraClassName}
`}
allowFontScaling={false}
style={[
style,
{
backgroundColor: isFocused ? "#ffffff88" : "#8f8d8d88",
},
]}
placeholderTextColor={"#ffffffff"}
clearButtonMode='while-editing'
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
{...otherProps}
/>
</TouchableOpacity>
) : (
const animateFocus = (focused: boolean) => {
Animated.timing(scale, {
toValue: focused ? 1.02 : 1,
duration: 150,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}).start();
};
const handleFocus = () => {
setIsFocused(true);
animateFocus(true);
};
const handleBlur = () => {
setIsFocused(false);
animateFocus(false);
};
if (Platform.isTV) {
return (
<Pressable
onPress={() => inputRef.current?.focus()}
onFocus={handleFocus}
onBlur={handleBlur}
>
<Animated.View
style={{
transform: [{ scale }],
}}
>
{/* Outer glow when focused */}
{isFocused && (
<View
style={{
position: "absolute",
top: -4,
left: -4,
right: -4,
bottom: -4,
backgroundColor: "#9334E9",
borderRadius: 18,
opacity: 0.5,
}}
/>
)}
<View
style={{
backgroundColor: isFocused ? "#2a2a2a" : "#1a1a1a",
borderWidth: 3,
borderColor: isFocused ? "#FFFFFF" : "#333333",
borderRadius: 14,
overflow: "hidden",
}}
>
{/* Purple accent bar at top when focused */}
{isFocused && (
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 3,
backgroundColor: "#9334E9",
}}
/>
)}
<TextInput
ref={inputRef}
allowFontScaling={false}
placeholderTextColor={isFocused ? "#AAAAAA" : "#666666"}
style={[
{
height: 60,
fontSize: 22,
fontWeight: "500",
paddingHorizontal: 20,
paddingTop: isFocused ? 4 : 0,
color: "#FFFFFF",
backgroundColor: "transparent",
},
style,
]}
onFocus={handleFocus}
onBlur={handleBlur}
{...otherProps}
/>
</View>
</Animated.View>
</Pressable>
);
}
// Mobile version unchanged
return (
<TextInput
ref={inputRef}
className='p-4 rounded-xl bg-neutral-900'
className={`p-4 rounded-xl bg-neutral-900 ${extraClassName}`}
allowFontScaling={false}
style={[{ color: "white" }, style]}
placeholderTextColor={"#9CA3AF"}

View File

@@ -243,7 +243,7 @@ export const MiniPlayerBar: React.FC = () => {
]}
>
<Animated.View style={[styles.touchable, animatedBarStyle]}>
{Platform.OS === "ios" ? (
{Platform.OS === "ios" && !Platform.isTV ? (
<GlassEffectView style={styles.blurContainer}>
<View
style={{

View File

@@ -1,5 +1,5 @@
import * as Location from "expo-location";
import { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native";
import { getSSID } from "@/modules/wifi-ssid";
export type PermissionStatus =
@@ -15,13 +15,28 @@ export interface UseWifiSSIDReturn {
isLoading: boolean;
}
function mapLocationStatus(
status: Location.PermissionStatus,
): PermissionStatus {
// WiFi SSID is not available on tvOS
if (Platform.isTV) {
// Export a stub hook for tvOS
module.exports = {
useWifiSSID: (): UseWifiSSIDReturn => ({
ssid: null,
permissionStatus: "unavailable" as PermissionStatus,
requestPermission: async () => false,
isLoading: false,
}),
};
}
// Only import Location on non-TV platforms
const Location = Platform.isTV ? null : require("expo-location");
function mapLocationStatus(status: number | undefined): PermissionStatus {
if (!Location) return "unavailable";
switch (status) {
case Location.PermissionStatus.GRANTED:
case Location.PermissionStatus?.GRANTED:
return "granted";
case Location.PermissionStatus.DENIED:
case Location.PermissionStatus?.DENIED:
return "denied";
default:
return "undetermined";
@@ -30,17 +45,24 @@ function mapLocationStatus(
export function useWifiSSID(): UseWifiSSIDReturn {
const [ssid, setSSID] = useState<string | null>(null);
const [permissionStatus, setPermissionStatus] =
useState<PermissionStatus>("undetermined");
const [isLoading, setIsLoading] = useState(true);
const [permissionStatus, setPermissionStatus] = useState<PermissionStatus>(
Platform.isTV ? "unavailable" : "undetermined",
);
const [isLoading, setIsLoading] = useState(!Platform.isTV);
const fetchSSID = useCallback(async () => {
if (Platform.isTV) return;
const result = await getSSID();
console.log("[WiFi Debug] Native module SSID:", result);
setSSID(result);
}, []);
const requestPermission = useCallback(async (): Promise<boolean> => {
if (Platform.isTV || !Location) {
setPermissionStatus("unavailable");
return false;
}
try {
const { status } = await Location.requestForegroundPermissionsAsync();
const newStatus = mapLocationStatus(status);
@@ -58,6 +80,11 @@ export function useWifiSSID(): UseWifiSSIDReturn {
}, [fetchSSID]);
useEffect(() => {
if (Platform.isTV || !Location) {
setIsLoading(false);
return;
}
async function initialize() {
setIsLoading(true);
try {
@@ -79,6 +106,8 @@ export function useWifiSSID(): UseWifiSSIDReturn {
// Refresh SSID when permission status changes to granted
useEffect(() => {
if (Platform.isTV) return;
if (permissionStatus === "granted") {
fetchSSID();

View File

@@ -1,6 +1,10 @@
import "react-native-url-polyfill/auto";
import TrackPlayer from "react-native-track-player";
import { PlaybackService } from "./services/PlaybackService";
import { Platform } from "react-native";
import "expo-router/entry";
TrackPlayer.registerPlaybackService(() => PlaybackService);
// TrackPlayer is not supported on tvOS
if (!Platform.isTV) {
const TrackPlayer = require("react-native-track-player").default;
const { PlaybackService } = require("./services/PlaybackService");
TrackPlayer.registerPlaybackService(() => PlaybackService);
}

View File

@@ -219,7 +219,7 @@ final class MPVLayerRenderer {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
if #available(iOS 18.0, *) {
if #available(iOS 18.0, tvOS 17.0, *) {
self.displayLayer.sampleBufferRenderer.flush(removingDisplayedImage: true, completionHandler: nil)
} else {
self.displayLayer.flushAndRemoveImage()

View File

@@ -72,9 +72,11 @@ class MpvPlayerView: ExpoView {
displayLayer.frame = bounds
displayLayer.videoGravity = .resizeAspect
#if !os(tvOS)
if #available(iOS 17.0, *) {
displayLayer.wantsExtendedDynamicRangeContent = true
}
#endif
displayLayer.backgroundColor = UIColor.black.cgColor
videoContainer.layer.addSublayer(displayLayer)

View File

@@ -1,13 +1,19 @@
import ExpoModulesCore
#if !os(tvOS)
import NetworkExtension
import SystemConfiguration.CaptiveNetwork
#endif
public class WifiSsidModule: Module {
public func definition() -> ModuleDefinition {
Name("WifiSsid")
// Get current WiFi SSID using NEHotspotNetwork (iOS 14+)
// Not available on tvOS
AsyncFunction("getSSID") { () -> String? in
#if os(tvOS)
return nil
#else
return await withCheckedContinuation { continuation in
NEHotspotNetwork.fetchCurrent { network in
if let ssid = network?.ssid {
@@ -21,14 +27,21 @@ public class WifiSsidModule: Module {
}
}
}
#endif
}
// Synchronous version using only CNCopyCurrentNetworkInfo
// Not available on tvOS
Function("getSSIDSync") { () -> String? in
#if os(tvOS)
return nil
#else
return self.getSSIDViaCNCopy()
#endif
}
}
#if !os(tvOS)
private func getSSIDViaCNCopy() -> String? {
guard let interfaces = CNCopySupportedInterfaces() as? [String] else {
print("[WifiSsid] CNCopySupportedInterfaces returned nil")
@@ -49,4 +62,5 @@ public class WifiSsidModule: Module {
print("[WifiSsid] No SSID found via CNCopyCurrentNetworkInfo")
return nil
}
#endif
}

View File

@@ -78,7 +78,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-i18next": "16.5.3",
"react-native": "0.81.5",
"react-native": "npm:react-native-tvos@0.81.5-2",
"react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "1.1.0",
"react-native-circular-progress": "^1.4.1",

View File

@@ -0,0 +1,72 @@
diff --git a/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift b/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift
--- a/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift
+++ b/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift
@@ -8,7 +8,7 @@
self.delegate = delegate
}
- #if !os(macOS)
+ #if !os(macOS) && !os(tvOS)
@available(iOS 26.0, *)
public func emitPlacementChanged(_ placement: TabViewBottomAccessoryPlacement?) {
var placementValue = "none"
diff --git a/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift
--- a/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift
+++ b/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift
@@ -67,11 +67,11 @@
}
func body(content: Content) -> some View {
- #if os(macOS)
- // tabViewBottomAccessory is not available on macOS
+ #if os(macOS) || os(tvOS)
+ // tabViewBottomAccessory is not available on macOS or tvOS
content
#else
- if #available(iOS 26.0, tvOS 26.0, visionOS 3.0, *), bottomAccessoryView != nil {
+ if #available(iOS 26.0, visionOS 3.0, *), bottomAccessoryView != nil {
content
.tabViewBottomAccessory {
renderBottomAccessoryView()
@@ -84,7 +84,7 @@
@ViewBuilder
private func renderBottomAccessoryView() -> some View {
- #if !os(macOS)
+ #if !os(macOS) && !os(tvOS)
if let bottomAccessoryView {
if #available(iOS 26.0, *) {
BottomAccessoryRepresentableView(view: bottomAccessoryView)
@@ -94,7 +94,7 @@
}
}
-#if !os(macOS)
+#if !os(macOS) && !os(tvOS)
@available(iOS 26.0, *)
struct BottomAccessoryRepresentableView: PlatformViewRepresentable {
@Environment(\.tabViewBottomAccessoryPlacement) var tabViewBottomAccessoryPlacement
diff --git a/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift b/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift
--- a/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift
+++ b/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift
@@ -281,7 +281,7 @@
@ViewBuilder
func tabBarMinimizeBehavior(_ behavior: MinimizeBehavior?) -> some View {
- #if compiler(>=6.2)
+ #if compiler(>=6.2) && !os(tvOS)
if #available(iOS 26.0, macOS 26.0, *) {
if let behavior {
self.tabBarMinimizeBehavior(behavior.convert())
diff --git a/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift b/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift
--- a/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift
+++ b/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift
@@ -6,7 +6,7 @@
case onScrollUp
case onScrollDown
-#if compiler(>=6.2)
+#if compiler(>=6.2) && !os(tvOS)
@available(iOS 26.0, macOS 26.0, *)
func convert() -> TabBarMinimizeBehavior {
#if os(macOS)

View File

@@ -37,6 +37,11 @@ const dependencies = {
),
"react-native-ios-utilities": disableForTV("react-native-ios-utilities"),
"react-native-pager-view": disableForTV("react-native-pager-view"),
"react-native-track-player": disableForTV("react-native-track-player"),
"expo-location": disableForTV("expo-location"),
"react-native-glass-effect-view": disableForTV(
"react-native-glass-effect-view",
),
};
// Filter out undefined values