mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
refactor: replace any types with proper TypeScript types
Improves type safety throughout the codebase by eliminating unsafe `any` type assertions and replacing them with proper type definitions. Adds explicit type parameters and constraints to MMKV augmentations, component props, and router navigation calls. Updates function signatures to use `unknown` instead of `any` where appropriate, and properly types Icon glyphs, router Href parameters, and component prop spreads. Enhances maintainability and catches potential type errors at compile time rather than runtime.
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
import { MMKV } from "react-native-mmkv";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
|
||||
declare module "react-native-mmkv" {
|
||||
interface MMKV {
|
||||
get<T>(key: string): T | undefined;
|
||||
setAny(key: string, value: any | undefined): void;
|
||||
setAny(key: string, value: unknown): void;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the augmentation methods directly to the MMKV prototype
|
||||
// This follows the recommended pattern while adding the helper methods your app uses
|
||||
(storage as any).get = function <T>(key: string): T | undefined {
|
||||
// Add the augmentation methods directly to the MMKV instance
|
||||
// We need to bind these methods to preserve the 'this' context
|
||||
storage.get = function <T>(this: MMKV, key: string): T | undefined {
|
||||
try {
|
||||
const serializedItem = this.getString(key);
|
||||
if (!serializedItem) return undefined;
|
||||
@@ -20,7 +21,7 @@ declare module "react-native-mmkv" {
|
||||
}
|
||||
};
|
||||
|
||||
(storage as any).setAny = function (key: string, value: any | undefined): void {
|
||||
storage.setAny = function (this: MMKV, key: string, value: unknown): void {
|
||||
try {
|
||||
if (value === undefined) {
|
||||
this.remove(key);
|
||||
|
||||
@@ -108,10 +108,10 @@ export const BitrateSheet: React.FC<Props> = ({
|
||||
values={selected ? [selected] : []}
|
||||
multiple={false}
|
||||
searchFilter={(item, query) => {
|
||||
const label = (item as any).key || "";
|
||||
const label = item.key || "";
|
||||
return label.toLowerCase().includes(query.toLowerCase());
|
||||
}}
|
||||
renderItemLabel={(item) => <Text>{(item as any).key || ""}</Text>}
|
||||
renderItemLabel={(item) => <Text>{item.key || ""}</Text>}
|
||||
set={(vals) => {
|
||||
const chosen = vals[0] as Bitrate | undefined;
|
||||
if (chosen) onChange(chosen);
|
||||
|
||||
@@ -4,7 +4,19 @@ import type { PropsWithChildren } from "react";
|
||||
import { Platform, TouchableOpacity, type ViewProps } from "react-native";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
interface Props
|
||||
extends Omit<
|
||||
ViewProps,
|
||||
| "children"
|
||||
| "onPressIn"
|
||||
| "onPressOut"
|
||||
| "onPress"
|
||||
| "nextFocusDown"
|
||||
| "nextFocusForward"
|
||||
| "nextFocusLeft"
|
||||
| "nextFocusRight"
|
||||
| "nextFocusUp"
|
||||
> {
|
||||
onPress?: () => void;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
background?: boolean;
|
||||
@@ -41,7 +53,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
@@ -60,7 +72,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
@@ -78,7 +90,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
@@ -98,7 +110,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
className={`rounded-full ${buttonSize} flex items-center justify-center ${
|
||||
fillColor ? fillColorClass : "bg-transparent"
|
||||
}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
@@ -112,11 +124,11 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={handlePress} {...(viewProps as any)}>
|
||||
<TouchableOpacity onPress={handlePress} {...viewProps}>
|
||||
<BlurView
|
||||
intensity={90}
|
||||
className={`rounded-full overflow-hidden ${buttonSize} flex items-center justify-center ${fillColorClass}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
|
||||
@@ -81,14 +81,12 @@ export const TrackSheet: React.FC<Props> = ({
|
||||
}
|
||||
multiple={false}
|
||||
searchFilter={(item, query) => {
|
||||
const label = (item as any).DisplayTitle || "";
|
||||
const label = item.DisplayTitle || "";
|
||||
return label.toLowerCase().includes(query.toLowerCase());
|
||||
}}
|
||||
renderItemLabel={(item) => (
|
||||
<Text>{(item as any).DisplayTitle || ""}</Text>
|
||||
)}
|
||||
renderItemLabel={(item) => <Text>{item.DisplayTitle || ""}</Text>}
|
||||
set={(vals) => {
|
||||
const chosen = vals[0] as any;
|
||||
const chosen = vals[0];
|
||||
if (chosen && chosen.Index !== null && chosen.Index !== undefined) {
|
||||
onChange(chosen.Index);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useRouter } from "expo-router";
|
||||
import { type Href, useRouter } from "expo-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
@@ -340,7 +340,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
const navigateToItem = useCallback(
|
||||
(item: BaseItemDto) => {
|
||||
const navigation = getItemNavigation(item, "(home)");
|
||||
router.push(navigation as any);
|
||||
router.push(navigation as Href);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useRouter, useSegments } from "expo-router";
|
||||
import { type Href, useRouter, useSegments } from "expo-router";
|
||||
import { type PropsWithChildren, useCallback } from "react";
|
||||
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
|
||||
import { useFavorite } from "@/hooks/useFavorite";
|
||||
@@ -146,12 +146,12 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
if (isOffline) {
|
||||
// For offline mode, we still need to use query params
|
||||
const url = `${itemRouter(item, from)}&offline=true`;
|
||||
router.push(url as any);
|
||||
router.push(url as Href);
|
||||
return;
|
||||
}
|
||||
|
||||
const navigation = getItemNavigation(item, from);
|
||||
router.push(navigation as any);
|
||||
router.push(navigation as Href);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { useRouter, useSegments } from "expo-router";
|
||||
import { type Href, useRouter, useSegments } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { Dimensions, View, type ViewProps } from "react-native";
|
||||
@@ -156,7 +156,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
if (!from) return;
|
||||
lightHapticFeedback();
|
||||
const navigation = getItemNavigation(item, from);
|
||||
router.push(navigation as any);
|
||||
router.push(navigation as Href);
|
||||
}, [item, from]);
|
||||
|
||||
const tap = Gesture.Tap()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { router, useSegments } from "expo-router";
|
||||
import { type Href, router, useSegments } from "expo-router";
|
||||
import type React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { TouchableOpacity, type ViewProps } from "react-native";
|
||||
@@ -21,10 +21,10 @@ const CompanySlide: React.FC<
|
||||
const navigate = useCallback(
|
||||
({ id, image, name }: Network | Studio) =>
|
||||
router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}` as any,
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`,
|
||||
params: { id, image, name, type: slide.type },
|
||||
}),
|
||||
[slide],
|
||||
} as Href),
|
||||
[slide, from],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { router, useSegments } from "expo-router";
|
||||
import { type Href, router, useSegments } from "expo-router";
|
||||
import type React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { TouchableOpacity, type ViewProps } from "react-native";
|
||||
@@ -18,10 +18,10 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||
const navigate = useCallback(
|
||||
(genre: GenreSliderItem) =>
|
||||
router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}` as any,
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`,
|
||||
params: { type: slide.type, name: genre.name },
|
||||
}),
|
||||
[slide],
|
||||
} as Href),
|
||||
[slide, from],
|
||||
);
|
||||
|
||||
const { data } = useQuery({
|
||||
|
||||
@@ -31,15 +31,16 @@ export const ListGroup: React.FC<PropsWithChildren<Props>> = ({
|
||||
className='flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900'
|
||||
>
|
||||
{Children.map(childrenArray, (child, index) => {
|
||||
if (isValidElement<{ style?: ViewStyle }>(child)) {
|
||||
return cloneElement(child as any, {
|
||||
style: StyleSheet.compose(
|
||||
child.props.style,
|
||||
index < childrenArray.length - 1
|
||||
? styles.borderBottom
|
||||
: undefined,
|
||||
),
|
||||
});
|
||||
if (isValidElement(child)) {
|
||||
const style = StyleSheet.compose(
|
||||
(child.props as { style?: ViewStyle }).style,
|
||||
index < childrenArray.length - 1
|
||||
? styles.borderBottom
|
||||
: undefined,
|
||||
);
|
||||
return cloneElement(child, { style } as Partial<
|
||||
typeof child.props
|
||||
>);
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
|
||||
@@ -3,7 +3,18 @@ import type { PropsWithChildren, ReactNode } from "react";
|
||||
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
||||
import { Text } from "../common/Text";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
interface Props
|
||||
extends Omit<
|
||||
ViewProps,
|
||||
| "children"
|
||||
| "onPressIn"
|
||||
| "onPressOut"
|
||||
| "nextFocusDown"
|
||||
| "nextFocusForward"
|
||||
| "nextFocusLeft"
|
||||
| "nextFocusRight"
|
||||
| "nextFocusUp"
|
||||
> {
|
||||
title?: string | null | undefined;
|
||||
subtitle?: string | null | undefined;
|
||||
value?: string | null | undefined;
|
||||
@@ -37,7 +48,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
||||
className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${
|
||||
disabled ? "opacity-50" : ""
|
||||
}`}
|
||||
{...(viewProps as any)}
|
||||
{...viewProps}
|
||||
>
|
||||
<ListItemContent
|
||||
title={title}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { type Href, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import {
|
||||
type Dispatch,
|
||||
type FC,
|
||||
@@ -379,7 +379,7 @@ export const Controls: FC<Props> = ({
|
||||
|
||||
console.log("queryParams", queryParams);
|
||||
|
||||
router.replace(`player/direct-player?${queryParams}` as any);
|
||||
router.replace(`player/direct-player?${queryParams}` as Href);
|
||||
},
|
||||
[settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router],
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
|
||||
interface FeedbackState {
|
||||
visible: boolean;
|
||||
icon: string;
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
text: string;
|
||||
side?: "left" | "right";
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export const GestureOverlay = ({
|
||||
|
||||
const [feedback, setFeedback] = useState<FeedbackState>({
|
||||
visible: false,
|
||||
icon: "",
|
||||
icon: "play",
|
||||
text: "",
|
||||
});
|
||||
const [fadeAnim] = useState(new Animated.Value(0));
|
||||
@@ -46,7 +46,7 @@ export const GestureOverlay = ({
|
||||
|
||||
const showFeedback = useCallback(
|
||||
(
|
||||
icon: string,
|
||||
icon: keyof typeof Ionicons.glyphMap,
|
||||
text: string,
|
||||
side?: "left" | "right",
|
||||
isDuringDrag = false,
|
||||
@@ -320,7 +320,7 @@ export const GestureOverlay = ({
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={feedback.icon as any}
|
||||
name={feedback.icon}
|
||||
size={24}
|
||||
color='white'
|
||||
style={{ marginRight: 8 }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SubtitleDeliveryMethod } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { type Href, router, useLocalSearchParams } from "expo-router";
|
||||
import type React from "react";
|
||||
import {
|
||||
createContext,
|
||||
@@ -95,7 +95,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
playbackPosition: playbackPosition,
|
||||
}).toString();
|
||||
|
||||
router.replace(`player/direct-player?${queryParams}` as any);
|
||||
router.replace(`player/direct-player?${queryParams}` as Href);
|
||||
};
|
||||
|
||||
const setTrackParams = (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { type Href, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { BITRATES } from "@/components/BitrateSelector";
|
||||
@@ -53,7 +53,7 @@ const DropdownView = () => {
|
||||
bitrateValue: bitrate.toString(),
|
||||
playbackPosition: playbackPositionRef.current,
|
||||
}).toString();
|
||||
router.replace(`player/direct-player?${queryParams}` as any);
|
||||
router.replace(`player/direct-player?${queryParams}` as Href);
|
||||
},
|
||||
[audioIndex, subtitleIndex, router],
|
||||
);
|
||||
|
||||
@@ -362,10 +362,11 @@ export const useSettings = () => {
|
||||
value !== undefined &&
|
||||
_settings?.[settingsKey] !== value
|
||||
) {
|
||||
(unlockedPluginDefaults as any)[settingsKey] = value;
|
||||
(unlockedPluginDefaults as Record<string, unknown>)[settingsKey] =
|
||||
value;
|
||||
}
|
||||
|
||||
(acc as any)[settingsKey] = locked
|
||||
(acc as Record<string, unknown>)[settingsKey] = locked
|
||||
? value
|
||||
: (_settings?.[settingsKey] ?? value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user