mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
Removes extensive dead code including unused components, utilities, and augmentations that were no longer referenced in the codebase. Simplifies play settings logic by removing complex stream ranking algorithm in favor of direct previous index matching for audio and subtitle selections. Removes aspectRatio prop from video player as it was set to a constant "default" value and never changed. Inlines POSTER_CAROUSEL_HEIGHT constant directly where used instead of importing from centralized constants file. Eliminates unused features including image color extraction for TV platforms, M3U8 subtitle parsing, and various Jellyfin API helpers that were no longer needed. Cleans up credential management by making internal helper functions private that should not be exposed to external consumers.
192 lines
6.1 KiB
TypeScript
192 lines
6.1 KiB
TypeScript
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
|
import type {
|
|
BaseItemDto,
|
|
MediaSourceInfo,
|
|
} from "@jellyfin/sdk/lib/generated-client";
|
|
import { type FC, useCallback, useState } from "react";
|
|
import { Platform, TouchableOpacity, View } from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import useRouter from "@/hooks/useAppRouter";
|
|
import { useHaptic } from "@/hooks/useHaptic";
|
|
import { useOrientation } from "@/hooks/useOrientation";
|
|
import { OrientationLock } from "@/packages/expo-screen-orientation";
|
|
import { useSettings } from "@/utils/atoms/settings";
|
|
import { HEADER_LAYOUT, ICON_SIZES } from "./constants";
|
|
import DropdownView from "./dropdown/DropdownView";
|
|
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
|
import { ZoomToggle } from "./ZoomToggle";
|
|
|
|
interface HeaderControlsProps {
|
|
item: BaseItemDto;
|
|
showControls: boolean;
|
|
offline: boolean;
|
|
mediaSource?: MediaSourceInfo | null;
|
|
startPictureInPicture?: () => Promise<void>;
|
|
switchOnEpisodeMode: () => void;
|
|
goToPreviousItem: () => void;
|
|
goToNextItem: (options: { isAutoPlay?: boolean }) => void;
|
|
previousItem?: BaseItemDto | null;
|
|
nextItem?: BaseItemDto | null;
|
|
isZoomedToFill?: boolean;
|
|
onZoomToggle?: () => void;
|
|
// Playback speed props
|
|
playbackSpeed?: number;
|
|
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
|
|
}
|
|
|
|
export const HeaderControls: FC<HeaderControlsProps> = ({
|
|
item,
|
|
showControls,
|
|
offline,
|
|
mediaSource,
|
|
startPictureInPicture,
|
|
switchOnEpisodeMode,
|
|
goToPreviousItem,
|
|
goToNextItem,
|
|
previousItem,
|
|
nextItem,
|
|
isZoomedToFill = false,
|
|
onZoomToggle,
|
|
playbackSpeed = 1.0,
|
|
setPlaybackSpeed,
|
|
}) => {
|
|
const { settings } = useSettings();
|
|
const router = useRouter();
|
|
const insets = useSafeAreaInsets();
|
|
const lightHapticFeedback = useHaptic("light");
|
|
const { orientation, lockOrientation } = useOrientation();
|
|
const [isTogglingOrientation, setIsTogglingOrientation] = useState(false);
|
|
|
|
const onClose = async () => {
|
|
lightHapticFeedback();
|
|
router.back();
|
|
};
|
|
|
|
const toggleOrientation = useCallback(async () => {
|
|
if (isTogglingOrientation) return;
|
|
|
|
setIsTogglingOrientation(true);
|
|
lightHapticFeedback();
|
|
|
|
try {
|
|
const isPortrait =
|
|
orientation === OrientationLock.PORTRAIT_UP ||
|
|
orientation === OrientationLock.PORTRAIT_DOWN;
|
|
|
|
await lockOrientation(
|
|
isPortrait ? OrientationLock.LANDSCAPE : OrientationLock.PORTRAIT_UP,
|
|
);
|
|
} finally {
|
|
setIsTogglingOrientation(false);
|
|
}
|
|
}, [
|
|
orientation,
|
|
lockOrientation,
|
|
isTogglingOrientation,
|
|
lightHapticFeedback,
|
|
]);
|
|
|
|
return (
|
|
<View
|
|
style={[
|
|
{
|
|
position: "absolute",
|
|
top: (settings?.safeAreaInControlsEnabled ?? true) ? insets.top : 0,
|
|
left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
|
|
right:
|
|
(settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
|
|
padding: HEADER_LAYOUT.CONTAINER_PADDING,
|
|
},
|
|
]}
|
|
pointerEvents={showControls ? "auto" : "none"}
|
|
className='flex flex-row justify-between'
|
|
>
|
|
<View className='mr-auto' pointerEvents='box-none'>
|
|
{!Platform.isTV && (!offline || !mediaSource?.TranscodingUrl) && (
|
|
<View pointerEvents='auto'>
|
|
<DropdownView
|
|
playbackSpeed={playbackSpeed}
|
|
setPlaybackSpeed={setPlaybackSpeed}
|
|
/>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
<View className='flex flex-row items-center space-x-2'>
|
|
{!Platform.isTV && (
|
|
<TouchableOpacity
|
|
onPress={toggleOrientation}
|
|
disabled={isTogglingOrientation}
|
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
|
accessibilityLabel='Toggle screen orientation'
|
|
accessibilityHint='Toggles the screen orientation between portrait and landscape'
|
|
>
|
|
<MaterialIcons
|
|
name='screen-rotation'
|
|
size={ICON_SIZES.HEADER}
|
|
color='white'
|
|
style={{ opacity: isTogglingOrientation ? 0.5 : 1 }}
|
|
/>
|
|
</TouchableOpacity>
|
|
)}
|
|
{!Platform.isTV && startPictureInPicture && (
|
|
<TouchableOpacity
|
|
onPress={startPictureInPicture}
|
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
|
>
|
|
<MaterialIcons
|
|
name='picture-in-picture'
|
|
size={ICON_SIZES.HEADER}
|
|
color='white'
|
|
/>
|
|
</TouchableOpacity>
|
|
)}
|
|
{item?.Type === "Episode" && (
|
|
<TouchableOpacity
|
|
onPress={switchOnEpisodeMode}
|
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
|
>
|
|
<Ionicons name='list' size={ICON_SIZES.HEADER} color='white' />
|
|
</TouchableOpacity>
|
|
)}
|
|
{previousItem && (
|
|
<TouchableOpacity
|
|
onPress={goToPreviousItem}
|
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
|
>
|
|
<Ionicons
|
|
name='play-skip-back'
|
|
size={ICON_SIZES.HEADER}
|
|
color='white'
|
|
/>
|
|
</TouchableOpacity>
|
|
)}
|
|
{nextItem && (
|
|
<TouchableOpacity
|
|
onPress={() => goToNextItem({ isAutoPlay: false })}
|
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
|
>
|
|
<Ionicons
|
|
name='play-skip-forward'
|
|
size={ICON_SIZES.HEADER}
|
|
color='white'
|
|
/>
|
|
</TouchableOpacity>
|
|
)}
|
|
{/* MPV Zoom Toggle */}
|
|
<ZoomToggle
|
|
isZoomedToFill={isZoomedToFill}
|
|
onToggle={onZoomToggle ?? (() => {})}
|
|
disabled={!onZoomToggle}
|
|
/>
|
|
<TouchableOpacity
|
|
onPress={onClose}
|
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
|
>
|
|
<Ionicons name='close' size={ICON_SIZES.HEADER} color='white' />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|