mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-16 22:36:25 +00:00
wip
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
Animated,
|
||||
@@ -39,6 +41,49 @@ export function Input(props: InputProps) {
|
||||
};
|
||||
|
||||
if (Platform.isTV) {
|
||||
const containerStyle = {
|
||||
height: 48,
|
||||
borderRadius: 50,
|
||||
borderWidth: isFocused ? 1.5 : 1,
|
||||
borderColor: isFocused
|
||||
? "rgba(255, 255, 255, 0.3)"
|
||||
: "rgba(255, 255, 255, 0.1)",
|
||||
overflow: "hidden" as const,
|
||||
flexDirection: "row" as const,
|
||||
alignItems: "center" as const,
|
||||
paddingLeft: 16,
|
||||
};
|
||||
|
||||
const inputElement = (
|
||||
<>
|
||||
<Ionicons
|
||||
name='search'
|
||||
size={20}
|
||||
color={isFocused ? "#999" : "#666"}
|
||||
style={{ marginRight: 12 }}
|
||||
/>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
allowFontScaling={false}
|
||||
placeholderTextColor='#666'
|
||||
style={[
|
||||
{
|
||||
flex: 1,
|
||||
height: 48,
|
||||
fontSize: 18,
|
||||
fontWeight: "400",
|
||||
color: "#FFFFFF",
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
style,
|
||||
]}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
{...otherProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => inputRef.current?.focus()}
|
||||
@@ -50,66 +95,28 @@ export function Input(props: InputProps) {
|
||||
transform: [{ scale }],
|
||||
}}
|
||||
>
|
||||
{/* Outer glow when focused */}
|
||||
{isFocused && (
|
||||
{Platform.OS === "ios" ? (
|
||||
<BlurView
|
||||
intensity={isFocused ? 90 : 80}
|
||||
tint='dark'
|
||||
style={containerStyle}
|
||||
>
|
||||
{inputElement}
|
||||
</BlurView>
|
||||
) : (
|
||||
<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={[
|
||||
containerStyle,
|
||||
{
|
||||
height: 60,
|
||||
fontSize: 22,
|
||||
fontWeight: "500",
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: isFocused ? 4 : 0,
|
||||
color: "#FFFFFF",
|
||||
backgroundColor: "transparent",
|
||||
backgroundColor: isFocused
|
||||
? "rgba(255, 255, 255, 0.12)"
|
||||
: "rgba(255, 255, 255, 0.08)",
|
||||
},
|
||||
style,
|
||||
]}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
{...otherProps}
|
||||
/>
|
||||
</View>
|
||||
>
|
||||
{inputElement}
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Animated, Easing, Pressable, type ViewStyle } from "react-native";
|
||||
|
||||
interface TVFocusablePosterProps {
|
||||
export interface TVFocusablePosterProps {
|
||||
children: React.ReactNode;
|
||||
onPress: () => void;
|
||||
hasTVPreferredFocus?: boolean;
|
||||
@@ -10,6 +10,7 @@ interface TVFocusablePosterProps {
|
||||
style?: ViewStyle;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const TVFocusablePoster: React.FC<TVFocusablePosterProps> = ({
|
||||
@@ -21,6 +22,7 @@ export const TVFocusablePoster: React.FC<TVFocusablePosterProps> = ({
|
||||
style,
|
||||
onFocus: onFocusProp,
|
||||
onBlur: onBlurProp,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const scale = useRef(new Animated.Value(1)).current;
|
||||
@@ -48,7 +50,9 @@ export const TVFocusablePoster: React.FC<TVFocusablePosterProps> = ({
|
||||
animateTo(1);
|
||||
onBlurProp?.();
|
||||
}}
|
||||
hasTVPreferredFocus={hasTVPreferredFocus}
|
||||
hasTVPreferredFocus={hasTVPreferredFocus && !disabled}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
|
||||
@@ -225,20 +225,20 @@ const TVSettingsPanel: FC<{
|
||||
<View style={selectorStyles.overlay}>
|
||||
<BlurView intensity={80} tint='dark' style={selectorStyles.blurContainer}>
|
||||
<View style={selectorStyles.content}>
|
||||
{/* Tab buttons - no preferred focus, navigate here via up from options */}
|
||||
{/* Tab buttons - switch automatically on focus */}
|
||||
<View style={selectorStyles.tabRow}>
|
||||
{audioOptions.length > 0 && (
|
||||
<TVSettingsTab
|
||||
label={t("item_card.audio")}
|
||||
active={activeTab === "audio"}
|
||||
onPress={() => setActiveTab("audio")}
|
||||
onSelect={() => setActiveTab("audio")}
|
||||
/>
|
||||
)}
|
||||
{subtitleOptions.length > 0 && (
|
||||
<TVSettingsTab
|
||||
label={t("item_card.subtitles")}
|
||||
active={activeTab === "subtitle"}
|
||||
onPress={() => setActiveTab("subtitle")}
|
||||
onSelect={() => setActiveTab("subtitle")}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
@@ -269,13 +269,13 @@ const TVSettingsPanel: FC<{
|
||||
);
|
||||
};
|
||||
|
||||
// Tab button for settings panel
|
||||
// Tab button for settings panel - switches on focus, no click needed
|
||||
const TVSettingsTab: FC<{
|
||||
label: string;
|
||||
active: boolean;
|
||||
onPress: () => void;
|
||||
onSelect: () => void;
|
||||
hasTVPreferredFocus?: boolean;
|
||||
}> = ({ label, active, onPress, hasTVPreferredFocus }) => {
|
||||
}> = ({ label, active, onSelect, hasTVPreferredFocus }) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const scale = useRef(new RNAnimated.Value(1)).current;
|
||||
|
||||
@@ -289,10 +289,11 @@ const TVSettingsTab: FC<{
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
animateTo(1.05);
|
||||
// Switch tab automatically on focus
|
||||
onSelect();
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocused(false);
|
||||
@@ -506,8 +507,8 @@ export const Controls: FC<Props> = ({
|
||||
const [openModal, setOpenModal] = useState<ModalType>(null);
|
||||
const isModalOpen = openModal !== null;
|
||||
|
||||
// Handle swipe up to open settings panel
|
||||
const handleSwipeUp = useCallback(() => {
|
||||
// Handle swipe down to open settings panel
|
||||
const handleSwipeDown = useCallback(() => {
|
||||
if (!isModalOpen) {
|
||||
setOpenModal("settings");
|
||||
}
|
||||
@@ -629,6 +630,16 @@ export const Controls: FC<Props> = ({
|
||||
isSeeking,
|
||||
});
|
||||
|
||||
const getFinishTime = () => {
|
||||
const now = new Date();
|
||||
const finishTime = new Date(now.getTime() + remainingTime);
|
||||
return finishTime.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
};
|
||||
|
||||
const toggleControls = useCallback(() => {
|
||||
setShowControls(!showControls);
|
||||
}, [showControls, setShowControls]);
|
||||
@@ -672,7 +683,7 @@ export const Controls: FC<Props> = ({
|
||||
handleSeekForward,
|
||||
handleSeekBackward,
|
||||
disableSeeking: isModalOpen,
|
||||
onSwipeUp: handleSwipeUp,
|
||||
onSwipeDown: handleSwipeDown,
|
||||
});
|
||||
|
||||
// Slider hook
|
||||
@@ -748,9 +759,16 @@ export const Controls: FC<Props> = ({
|
||||
{/* Center Play Button - shown when paused */}
|
||||
{!isPlaying && showControls && (
|
||||
<View style={styles.centerContainer}>
|
||||
<View style={styles.playButtonContainer}>
|
||||
<Ionicons name='play' size={80} color='white' />
|
||||
</View>
|
||||
<BlurView intensity={40} tint='dark' style={styles.playButtonBlur}>
|
||||
<View style={styles.playButtonInner}>
|
||||
<Ionicons
|
||||
name='play'
|
||||
size={44}
|
||||
color='white'
|
||||
style={styles.playIcon}
|
||||
/>
|
||||
</View>
|
||||
</BlurView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -771,14 +789,14 @@ export const Controls: FC<Props> = ({
|
||||
]}
|
||||
>
|
||||
<View style={styles.settingsHint}>
|
||||
<Text style={styles.settingsHintText}>
|
||||
{t("player.swipe_down_settings")}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name='chevron-up'
|
||||
name='chevron-down'
|
||||
size={16}
|
||||
color='rgba(255,255,255,0.5)'
|
||||
/>
|
||||
<Text style={styles.settingsHintText}>
|
||||
{t("player.swipe_up_settings")}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
@@ -855,9 +873,14 @@ export const Controls: FC<Props> = ({
|
||||
<Text style={styles.timeText}>
|
||||
{formatTimeString(currentTime, "ms")}
|
||||
</Text>
|
||||
<Text style={styles.timeText}>
|
||||
-{formatTimeString(remainingTime, "ms")}
|
||||
</Text>
|
||||
<View style={styles.timeRight}>
|
||||
<Text style={styles.timeText}>
|
||||
-{formatTimeString(remainingTime, "ms")}
|
||||
</Text>
|
||||
<Text style={styles.endsAtText}>
|
||||
{t("player.ends_at")} {getFinishTime()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
@@ -910,14 +933,23 @@ const styles = StyleSheet.create({
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
playButtonContainer: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 60,
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
playButtonBlur: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
overflow: "hidden",
|
||||
},
|
||||
playButtonInner: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingLeft: 8,
|
||||
backgroundColor: "rgba(255,255,255,0.1)",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.2)",
|
||||
borderRadius: 40,
|
||||
},
|
||||
playIcon: {
|
||||
marginLeft: 4,
|
||||
},
|
||||
topContainer: {
|
||||
position: "absolute",
|
||||
@@ -928,7 +960,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
topInner: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
justifyContent: "center",
|
||||
},
|
||||
bottomContainer: {
|
||||
position: "absolute",
|
||||
@@ -970,18 +1002,23 @@ const styles = StyleSheet.create({
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
fontSize: 22,
|
||||
},
|
||||
timeRight: {
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
endsAtText: {
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
fontSize: 16,
|
||||
marginTop: 2,
|
||||
},
|
||||
settingsRow: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
},
|
||||
settingsHint: {
|
||||
flexDirection: "row",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 20,
|
||||
gap: 4,
|
||||
},
|
||||
settingsHintText: {
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
|
||||
@@ -33,8 +33,8 @@ interface UseRemoteControlProps {
|
||||
handleSeekBackward: (seconds: number) => void;
|
||||
/** When true, disables left/right seeking (e.g., when settings modal is open) */
|
||||
disableSeeking?: boolean;
|
||||
/** Callback when swipe up is detected - used to open settings */
|
||||
onSwipeUp?: () => void;
|
||||
/** Callback when swipe down is detected - used to open settings */
|
||||
onSwipeDown?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ export function useRemoteControl({
|
||||
handleSeekForward,
|
||||
handleSeekBackward,
|
||||
disableSeeking = false,
|
||||
onSwipeUp,
|
||||
onSwipeDown,
|
||||
}: UseRemoteControlProps) {
|
||||
const remoteScrubProgress = useSharedValue<number | null>(null);
|
||||
const isRemoteScrubbing = useSharedValue(false);
|
||||
@@ -74,9 +74,9 @@ export function useRemoteControl({
|
||||
const disableSeekingRef = useRef(disableSeeking);
|
||||
disableSeekingRef.current = disableSeeking;
|
||||
|
||||
// Use ref for onSwipeUp callback
|
||||
const onSwipeUpRef = useRef(onSwipeUp);
|
||||
onSwipeUpRef.current = onSwipeUp;
|
||||
// Use ref for onSwipeDown callback
|
||||
const onSwipeDownRef = useRef(onSwipeDown);
|
||||
onSwipeDownRef.current = onSwipeDown;
|
||||
|
||||
// MPV uses ms
|
||||
const SCRUB_INTERVAL = CONTROLS_CONSTANTS.SCRUB_INTERVAL_MS;
|
||||
@@ -130,6 +130,10 @@ export function useRemoteControl({
|
||||
}
|
||||
case "playPause":
|
||||
case "select": {
|
||||
// Skip play/pause when modal is open (let native focus handle selection)
|
||||
if (disableSeekingRef.current) {
|
||||
break;
|
||||
}
|
||||
if (isRemoteScrubbing.value && remoteScrubProgress.value != null) {
|
||||
progress.value = remoteScrubProgress.value;
|
||||
|
||||
@@ -148,17 +152,17 @@ export function useRemoteControl({
|
||||
break;
|
||||
}
|
||||
case "down":
|
||||
// cancel scrubbing on down
|
||||
// cancel scrubbing and trigger swipe down callback (for settings)
|
||||
isRemoteScrubbing.value = false;
|
||||
remoteScrubProgress.value = null;
|
||||
setShowRemoteBubble(false);
|
||||
onSwipeDownRef.current?.();
|
||||
break;
|
||||
case "up":
|
||||
// cancel scrubbing and trigger swipe up callback (for settings)
|
||||
// cancel scrubbing on up
|
||||
isRemoteScrubbing.value = false;
|
||||
remoteScrubProgress.value = null;
|
||||
setShowRemoteBubble(false);
|
||||
onSwipeUpRef.current?.();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user