Compare commits

...

2 Commits

Author SHA1 Message Date
Uruk
379e93edf9 fix: update setAny method to use 'any' type for value parameter 2025-11-25 19:06:43 +01:00
Uruk
75d6948a81 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.
2025-11-25 19:06:43 +01:00
16 changed files with 84 additions and 56 deletions

View File

@@ -1,3 +1,4 @@
import { MMKV } from "react-native-mmkv";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
declare module "react-native-mmkv" { declare module "react-native-mmkv" {
@@ -7,9 +8,9 @@ declare module "react-native-mmkv" {
} }
} }
// Add the augmentation methods directly to the MMKV prototype // Add the augmentation methods directly to the MMKV instance
// This follows the recommended pattern while adding the helper methods your app uses // We need to bind these methods to preserve the 'this' context
(storage as any).get = function <T>(key: string): T | undefined { storage.get = function <T>(this: MMKV, key: string): T | undefined {
try { try {
const serializedItem = this.getString(key); const serializedItem = this.getString(key);
if (!serializedItem) return undefined; if (!serializedItem) return undefined;
@@ -20,7 +21,11 @@ declare module "react-native-mmkv" {
} }
}; };
(storage as any).setAny = function (key: string, value: any | undefined): void { storage.setAny = function (
this: MMKV,
key: string,
value: any | undefined,
): void {
try { try {
if (value === undefined) { if (value === undefined) {
this.remove(key); this.remove(key);

View File

@@ -108,10 +108,10 @@ export const BitrateSheet: React.FC<Props> = ({
values={selected ? [selected] : []} values={selected ? [selected] : []}
multiple={false} multiple={false}
searchFilter={(item, query) => { searchFilter={(item, query) => {
const label = (item as any).key || ""; const label = item.key || "";
return label.toLowerCase().includes(query.toLowerCase()); return label.toLowerCase().includes(query.toLowerCase());
}} }}
renderItemLabel={(item) => <Text>{(item as any).key || ""}</Text>} renderItemLabel={(item) => <Text>{item.key || ""}</Text>}
set={(vals) => { set={(vals) => {
const chosen = vals[0] as Bitrate | undefined; const chosen = vals[0] as Bitrate | undefined;
if (chosen) onChange(chosen); if (chosen) onChange(chosen);

View File

@@ -4,7 +4,19 @@ import type { PropsWithChildren } from "react";
import { Platform, TouchableOpacity, type ViewProps } from "react-native"; import { Platform, TouchableOpacity, type ViewProps } from "react-native";
import { useHaptic } from "@/hooks/useHaptic"; 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; onPress?: () => void;
icon?: keyof typeof Ionicons.glyphMap; icon?: keyof typeof Ionicons.glyphMap;
background?: boolean; background?: boolean;
@@ -41,7 +53,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity <TouchableOpacity
onPress={handlePress} onPress={handlePress}
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`} className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
{...(viewProps as any)} {...viewProps}
> >
{icon ? ( {icon ? (
<Ionicons <Ionicons
@@ -60,7 +72,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity <TouchableOpacity
onPress={handlePress} onPress={handlePress}
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`} className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
{...(viewProps as any)} {...viewProps}
> >
{icon ? ( {icon ? (
<Ionicons <Ionicons
@@ -78,7 +90,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity <TouchableOpacity
onPress={handlePress} onPress={handlePress}
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`} className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
{...(viewProps as any)} {...viewProps}
> >
{icon ? ( {icon ? (
<Ionicons <Ionicons
@@ -98,7 +110,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
className={`rounded-full ${buttonSize} flex items-center justify-center ${ className={`rounded-full ${buttonSize} flex items-center justify-center ${
fillColor ? fillColorClass : "bg-transparent" fillColor ? fillColorClass : "bg-transparent"
}`} }`}
{...(viewProps as any)} {...viewProps}
> >
{icon ? ( {icon ? (
<Ionicons <Ionicons
@@ -112,11 +124,11 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
); );
return ( return (
<TouchableOpacity onPress={handlePress} {...(viewProps as any)}> <TouchableOpacity onPress={handlePress} {...viewProps}>
<BlurView <BlurView
intensity={90} intensity={90}
className={`rounded-full overflow-hidden ${buttonSize} flex items-center justify-center ${fillColorClass}`} className={`rounded-full overflow-hidden ${buttonSize} flex items-center justify-center ${fillColorClass}`}
{...(viewProps as any)} {...viewProps}
> >
{icon ? ( {icon ? (
<Ionicons <Ionicons

View File

@@ -81,14 +81,12 @@ export const TrackSheet: React.FC<Props> = ({
} }
multiple={false} multiple={false}
searchFilter={(item, query) => { searchFilter={(item, query) => {
const label = (item as any).DisplayTitle || ""; const label = item.DisplayTitle || "";
return label.toLowerCase().includes(query.toLowerCase()); return label.toLowerCase().includes(query.toLowerCase());
}} }}
renderItemLabel={(item) => ( renderItemLabel={(item) => <Text>{item.DisplayTitle || ""}</Text>}
<Text>{(item as any).DisplayTitle || ""}</Text>
)}
set={(vals) => { set={(vals) => {
const chosen = vals[0] as any; const chosen = vals[0];
if (chosen && chosen.Index !== null && chosen.Index !== undefined) { if (chosen && chosen.Index !== null && chosen.Index !== undefined) {
onChange(chosen.Index); onChange(chosen.Index);
} }

View File

@@ -7,7 +7,7 @@ import {
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router"; import { type Href, useRouter } from "expo-router";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { import {
@@ -340,7 +340,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
const navigateToItem = useCallback( const navigateToItem = useCallback(
(item: BaseItemDto) => { (item: BaseItemDto) => {
const navigation = getItemNavigation(item, "(home)"); const navigation = getItemNavigation(item, "(home)");
router.push(navigation as any); router.push(navigation as Href);
}, },
[router], [router],
); );

View File

@@ -1,6 +1,6 @@
import { useActionSheet } from "@expo/react-native-action-sheet"; import { useActionSheet } from "@expo/react-native-action-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; 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 { type PropsWithChildren, useCallback } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import { useFavorite } from "@/hooks/useFavorite"; import { useFavorite } from "@/hooks/useFavorite";
@@ -146,12 +146,12 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
if (isOffline) { if (isOffline) {
// For offline mode, we still need to use query params // For offline mode, we still need to use query params
const url = `${itemRouter(item, from)}&offline=true`; const url = `${itemRouter(item, from)}&offline=true`;
router.push(url as any); router.push(url as Href);
return; return;
} }
const navigation = getItemNavigation(item, from); const navigation = getItemNavigation(item, from);
router.push(navigation as any); router.push(navigation as Href);
}} }}
{...props} {...props}
> >

View File

@@ -2,7 +2,7 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useRouter, useSegments } from "expo-router"; import { type Href, useRouter, useSegments } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useMemo } from "react";
import { Dimensions, View, type ViewProps } from "react-native"; import { Dimensions, View, type ViewProps } from "react-native";
@@ -156,7 +156,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
if (!from) return; if (!from) return;
lightHapticFeedback(); lightHapticFeedback();
const navigation = getItemNavigation(item, from); const navigation = getItemNavigation(item, from);
router.push(navigation as any); router.push(navigation as Href);
}, [item, from]); }, [item, from]);
const tap = Gesture.Tap() const tap = Gesture.Tap()

View File

@@ -1,4 +1,4 @@
import { router, useSegments } from "expo-router"; import { type Href, router, useSegments } from "expo-router";
import type React from "react"; import type React from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native"; import { TouchableOpacity, type ViewProps } from "react-native";
@@ -21,10 +21,10 @@ const CompanySlide: React.FC<
const navigate = useCallback( const navigate = useCallback(
({ id, image, name }: Network | Studio) => ({ id, image, name }: Network | Studio) =>
router.push({ 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 }, params: { id, image, name, type: slide.type },
}), } as Href),
[slide], [slide, from],
); );
return ( return (

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"; 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 type React from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native"; import { TouchableOpacity, type ViewProps } from "react-native";
@@ -18,10 +18,10 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
const navigate = useCallback( const navigate = useCallback(
(genre: GenreSliderItem) => (genre: GenreSliderItem) =>
router.push({ 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 }, params: { type: slide.type, name: genre.name },
}), } as Href),
[slide], [slide, from],
); );
const { data } = useQuery({ const { data } = useQuery({

View File

@@ -31,15 +31,16 @@ export const ListGroup: React.FC<PropsWithChildren<Props>> = ({
className='flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900' className='flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900'
> >
{Children.map(childrenArray, (child, index) => { {Children.map(childrenArray, (child, index) => {
if (isValidElement<{ style?: ViewStyle }>(child)) { if (isValidElement(child)) {
return cloneElement(child as any, { const style = StyleSheet.compose(
style: StyleSheet.compose( (child.props as { style?: ViewStyle }).style,
child.props.style, index < childrenArray.length - 1
index < childrenArray.length - 1 ? styles.borderBottom
? styles.borderBottom : undefined,
: undefined, );
), return cloneElement(child, { style } as Partial<
}); typeof child.props
>);
} }
return child; return child;
})} })}

View File

@@ -3,7 +3,18 @@ import type { PropsWithChildren, ReactNode } from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native"; import { TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "../common/Text"; 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; title?: string | null | undefined;
subtitle?: string | null | undefined; subtitle?: string | null | undefined;
value?: 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 ${ className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${
disabled ? "opacity-50" : "" disabled ? "opacity-50" : ""
}`} }`}
{...(viewProps as any)} {...viewProps}
> >
<ListItemContent <ListItemContent
title={title} title={title}

View File

@@ -3,7 +3,7 @@ import type {
BaseItemDto, BaseItemDto,
MediaSourceInfo, MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import { useLocalSearchParams, useRouter } from "expo-router"; import { type Href, useLocalSearchParams, useRouter } from "expo-router";
import { import {
type Dispatch, type Dispatch,
type FC, type FC,
@@ -379,7 +379,7 @@ export const Controls: FC<Props> = ({
console.log("queryParams", queryParams); 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], [settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router],
); );

View File

@@ -18,7 +18,7 @@ interface Props {
interface FeedbackState { interface FeedbackState {
visible: boolean; visible: boolean;
icon: string; icon: keyof typeof Ionicons.glyphMap;
text: string; text: string;
side?: "left" | "right"; side?: "left" | "right";
} }
@@ -36,7 +36,7 @@ export const GestureOverlay = ({
const [feedback, setFeedback] = useState<FeedbackState>({ const [feedback, setFeedback] = useState<FeedbackState>({
visible: false, visible: false,
icon: "", icon: "play",
text: "", text: "",
}); });
const [fadeAnim] = useState(new Animated.Value(0)); const [fadeAnim] = useState(new Animated.Value(0));
@@ -46,7 +46,7 @@ export const GestureOverlay = ({
const showFeedback = useCallback( const showFeedback = useCallback(
( (
icon: string, icon: keyof typeof Ionicons.glyphMap,
text: string, text: string,
side?: "left" | "right", side?: "left" | "right",
isDuringDrag = false, isDuringDrag = false,
@@ -320,7 +320,7 @@ export const GestureOverlay = ({
}} }}
> >
<Ionicons <Ionicons
name={feedback.icon as any} name={feedback.icon}
size={24} size={24}
color='white' color='white'
style={{ marginRight: 8 }} style={{ marginRight: 8 }}

View File

@@ -1,5 +1,5 @@
import { SubtitleDeliveryMethod } from "@jellyfin/sdk/lib/generated-client"; 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 type React from "react";
import { import {
createContext, createContext,
@@ -95,7 +95,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
playbackPosition: playbackPosition, playbackPosition: playbackPosition,
}).toString(); }).toString();
router.replace(`player/direct-player?${queryParams}` as any); router.replace(`player/direct-player?${queryParams}` as Href);
}; };
const setTrackParams = ( const setTrackParams = (

View File

@@ -1,5 +1,5 @@
import { Ionicons } from "@expo/vector-icons"; 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 { useCallback, useMemo, useRef } from "react";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { BITRATES } from "@/components/BitrateSelector"; import { BITRATES } from "@/components/BitrateSelector";
@@ -53,7 +53,7 @@ const DropdownView = () => {
bitrateValue: bitrate.toString(), bitrateValue: bitrate.toString(),
playbackPosition: playbackPositionRef.current, playbackPosition: playbackPositionRef.current,
}).toString(); }).toString();
router.replace(`player/direct-player?${queryParams}` as any); router.replace(`player/direct-player?${queryParams}` as Href);
}, },
[audioIndex, subtitleIndex, router], [audioIndex, subtitleIndex, router],
); );

View File

@@ -362,10 +362,11 @@ export const useSettings = () => {
value !== undefined && value !== undefined &&
_settings?.[settingsKey] !== value _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 ? value
: (_settings?.[settingsKey] ?? value); : (_settings?.[settingsKey] ?? value);
} }