From f543fa9e3ebd0393f532648e8a86f24f1ec82c1d Mon Sep 17 00:00:00 2001 From: Uruk Date: Mon, 12 Jan 2026 11:04:23 +0100 Subject: [PATCH] refactor: remove unused code and simplify implementations 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. --- app/(auth)/player/direct-player.tsx | 4 - augmentations/index.ts | 1 - augmentations/number.ts | 5 - augmentations/string.ts | 14 -- components/ContextMenu.tv.ts | 0 components/ExampleGlobalModalUsage.tsx | 203 ------------------ components/MoreMoviesWithActor.tsx | 4 +- components/SimilarItems.tsx | 5 +- components/common/LargePoster.tsx | 20 -- components/common/VerticalSkeleton.tsx | 28 --- components/navigation/TabBarIcon.tsx | 12 -- components/posters/EpisodePoster.tsx | 63 ------ components/posters/ParentPoster.tsx | 48 ----- components/series/CastAndCrew.tsx | 5 +- components/series/CurrentSeries.tsx | 5 +- components/settings/Dashboard.tsx | 29 --- components/video-player/controls/Controls.tsx | 4 - .../video-player/controls/HeaderControls.tsx | 3 - .../controls/VideoScalingModeSelector.tsx | 107 --------- constants/Languages.ts | 39 ---- constants/Values.ts | 6 - hooks/useControlsVisibility.ts | 37 ---- hooks/useDownloadedFileOpener.ts | 35 --- hooks/useImageColors.ts | 120 ----------- utils/bToMb.ts | 18 -- utils/collectionTypeToItemType.ts | 47 ---- utils/hls/parseM3U8ForSubtitles.ts | 56 ----- utils/jellyfin/getDefaultPlaySettings.ts | 30 +-- utils/jellyfin/session/capabilities.ts | 56 ----- utils/jellyfin/tvshows/nextUp.ts | 44 ---- utils/jellyfin/user-library/getItemById.ts | 34 --- utils/log.tsx | 15 -- utils/secondsToTicks.ts | 5 - utils/secureCredentials.ts | 11 +- utils/streamRanker.ts | 159 -------------- 35 files changed, 24 insertions(+), 1248 deletions(-) delete mode 100644 augmentations/string.ts delete mode 100644 components/ContextMenu.tv.ts delete mode 100644 components/ExampleGlobalModalUsage.tsx delete mode 100644 components/common/LargePoster.tsx delete mode 100644 components/common/VerticalSkeleton.tsx delete mode 100644 components/navigation/TabBarIcon.tsx delete mode 100644 components/posters/EpisodePoster.tsx delete mode 100644 components/posters/ParentPoster.tsx delete mode 100644 components/settings/Dashboard.tsx delete mode 100644 components/video-player/controls/VideoScalingModeSelector.tsx delete mode 100644 constants/Languages.ts delete mode 100644 constants/Values.ts delete mode 100644 hooks/useControlsVisibility.ts delete mode 100644 hooks/useDownloadedFileOpener.ts delete mode 100644 hooks/useImageColors.ts delete mode 100644 utils/bToMb.ts delete mode 100644 utils/collectionTypeToItemType.ts delete mode 100644 utils/hls/parseM3U8ForSubtitles.ts delete mode 100644 utils/jellyfin/session/capabilities.ts delete mode 100644 utils/jellyfin/tvshows/nextUp.ts delete mode 100644 utils/jellyfin/user-library/getItemById.ts delete mode 100644 utils/secondsToTicks.ts delete mode 100644 utils/streamRanker.ts diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index d51b96a4..2ef8cf39 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -71,9 +71,6 @@ export default function page() { const [isPlaybackStopped, setIsPlaybackStopped] = useState(false); const [showControls, _setShowControls] = useState(true); const [isPipMode, setIsPipMode] = useState(false); - const [aspectRatio] = useState<"default" | "16:9" | "4:3" | "1:1" | "21:9">( - "default", - ); const [isZoomedToFill, setIsZoomedToFill] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [isMuted, setIsMuted] = useState(false); @@ -917,7 +914,6 @@ export default function page() { pause={pause} seek={seek} enableTrickplay={true} - aspectRatio={aspectRatio} isZoomedToFill={isZoomedToFill} onZoomToggle={handleZoomToggle} api={api} diff --git a/augmentations/index.ts b/augmentations/index.ts index abec02c9..0c193e83 100644 --- a/augmentations/index.ts +++ b/augmentations/index.ts @@ -1,4 +1,3 @@ export * from "./api"; export * from "./mmkv"; export * from "./number"; -export * from "./string"; diff --git a/augmentations/number.ts b/augmentations/number.ts index 11c0837d..9f251c5a 100644 --- a/augmentations/number.ts +++ b/augmentations/number.ts @@ -3,7 +3,6 @@ declare global { bytesToReadable(decimals?: number): string; secondsToMilliseconds(): number; minutesToMilliseconds(): number; - hoursToMilliseconds(): number; } } @@ -28,8 +27,4 @@ Number.prototype.minutesToMilliseconds = function () { return this.valueOf() * (60).secondsToMilliseconds(); }; -Number.prototype.hoursToMilliseconds = function () { - return this.valueOf() * (60).minutesToMilliseconds(); -}; - export {}; diff --git a/augmentations/string.ts b/augmentations/string.ts deleted file mode 100644 index f4a50b55..00000000 --- a/augmentations/string.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare global { - interface String { - toTitle(): string; - } -} - -String.prototype.toTitle = function () { - return this.replaceAll("_", " ").replace( - /\w\S*/g, - (text) => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(), - ); -}; - -export {}; diff --git a/components/ContextMenu.tv.ts b/components/ContextMenu.tv.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/components/ExampleGlobalModalUsage.tsx b/components/ExampleGlobalModalUsage.tsx deleted file mode 100644 index ccebb823..00000000 --- a/components/ExampleGlobalModalUsage.tsx +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Example Usage of Global Modal - * - * This file demonstrates how to use the global modal system from anywhere in your app. - * You can delete this file after understanding how it works. - */ - -import { Ionicons } from "@expo/vector-icons"; -import { TouchableOpacity, View } from "react-native"; -import { Text } from "@/components/common/Text"; -import { useGlobalModal } from "@/providers/GlobalModalProvider"; - -/** - * Example 1: Simple Content Modal - */ -export const SimpleModalExample = () => { - const { showModal } = useGlobalModal(); - - const handleOpenModal = () => { - showModal( - - Simple Modal - - This is a simple modal with just some text content. - - - Swipe down or tap outside to close. - - , - ); - }; - - return ( - - Open Simple Modal - - ); -}; - -/** - * Example 2: Modal with Custom Snap Points - */ -export const CustomSnapPointsExample = () => { - const { showModal } = useGlobalModal(); - - const handleOpenModal = () => { - showModal( - - - Custom Snap Points - - - This modal has custom snap points (25%, 50%, 90%). - - - - Try dragging the modal to different heights! - - - , - { - snapPoints: ["25%", "50%", "90%"], - enableDynamicSizing: false, - }, - ); - }; - - return ( - - Custom Snap Points - - ); -}; - -/** - * Example 3: Complex Component in Modal - */ -const SettingsModalContent = () => { - const { hideModal } = useGlobalModal(); - - const settings = [ - { - id: 1, - title: "Notifications", - icon: "notifications-outline" as const, - enabled: true, - }, - { id: 2, title: "Dark Mode", icon: "moon-outline" as const, enabled: true }, - { - id: 3, - title: "Auto-play", - icon: "play-outline" as const, - enabled: false, - }, - ]; - - return ( - - Settings - - {settings.map((setting, index) => ( - - - - {setting.title} - - - - - - ))} - - - Close - - - ); -}; - -export const ComplexModalExample = () => { - const { showModal } = useGlobalModal(); - - const handleOpenModal = () => { - showModal(); - }; - - return ( - - Complex Component - - ); -}; - -/** - * Example 4: Modal Triggered from Function (e.g., API response) - */ -export const useShowSuccessModal = () => { - const { showModal } = useGlobalModal(); - - return (message: string) => { - showModal( - - - - - Success! - {message} - , - ); - }; -}; - -/** - * Main Demo Component - */ -export const GlobalModalDemo = () => { - const showSuccess = useShowSuccessModal(); - - return ( - - - Global Modal Examples - - - - - - - showSuccess("Operation completed successfully!")} - className='bg-orange-600 px-4 py-2 rounded-lg' - > - Show Success Modal - - - ); -}; diff --git a/components/MoreMoviesWithActor.tsx b/components/MoreMoviesWithActor.tsx index c3e4d0ca..6a8d2ca4 100644 --- a/components/MoreMoviesWithActor.tsx +++ b/components/MoreMoviesWithActor.tsx @@ -11,9 +11,11 @@ import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { ItemCardText } from "@/components/ItemCardText"; import MoviePoster from "@/components/posters/MoviePoster"; -import { POSTER_CAROUSEL_HEIGHT } from "@/constants/Values"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +// Matches `w-28` poster cards (approx 112px wide, 10/15 aspect ratio) + 2 lines of text. +const POSTER_CAROUSEL_HEIGHT = 220; + interface Props extends ViewProps { actorId: string; actorName?: string | null; diff --git a/components/SimilarItems.tsx b/components/SimilarItems.tsx index ce09cbcf..7ed99535 100644 --- a/components/SimilarItems.tsx +++ b/components/SimilarItems.tsx @@ -6,8 +6,11 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { View, type ViewProps } from "react-native"; import MoviePoster from "@/components/posters/MoviePoster"; -import { POSTER_CAROUSEL_HEIGHT } from "@/constants/Values"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; + +// Matches `w-28` poster cards (approx 112px wide, 10/15 aspect ratio) + 2 lines of text. +const POSTER_CAROUSEL_HEIGHT = 220; + import { HorizontalScroll } from "./common/HorizontalScroll"; import { Text } from "./common/Text"; import { TouchableItemRouter } from "./common/TouchableItemRouter"; diff --git a/components/common/LargePoster.tsx b/components/common/LargePoster.tsx deleted file mode 100644 index ab3b16fb..00000000 --- a/components/common/LargePoster.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Image } from "expo-image"; -import { View } from "react-native"; - -export const LargePoster: React.FC<{ url?: string | null }> = ({ url }) => { - if (!url) - return ( - - - - ); - - return ( - - - - ); -}; diff --git a/components/common/VerticalSkeleton.tsx b/components/common/VerticalSkeleton.tsx deleted file mode 100644 index 02a8a256..00000000 --- a/components/common/VerticalSkeleton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { View, type ViewProps } from "react-native"; - -interface Props extends ViewProps { - index: number; -} - -export const VerticalSkeleton: React.FC = ({ index, ...props }) => { - return ( - - - - - - - ); -}; diff --git a/components/navigation/TabBarIcon.tsx b/components/navigation/TabBarIcon.tsx deleted file mode 100644 index a28bba84..00000000 --- a/components/navigation/TabBarIcon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ - -import type { IconProps } from "@expo/vector-icons/build/createIconSet"; -import Ionicons from "@expo/vector-icons/Ionicons"; -import type { ComponentProps } from "react"; - -export function TabBarIcon({ - style, - ...rest -}: IconProps["name"]>) { - return ; -} diff --git a/components/posters/EpisodePoster.tsx b/components/posters/EpisodePoster.tsx deleted file mode 100644 index af42989b..00000000 --- a/components/posters/EpisodePoster.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { Image } from "expo-image"; -import { useAtom } from "jotai"; -import { useMemo, useState } from "react"; -import { View } from "react-native"; -import { WatchedIndicator } from "@/components/WatchedIndicator"; -import { apiAtom } from "@/providers/JellyfinProvider"; - -type MoviePosterProps = { - item: BaseItemDto; - showProgress?: boolean; -}; - -export const EpisodePoster: React.FC = ({ - item, - showProgress = false, -}) => { - const [api] = useAtom(apiAtom); - - const url = useMemo(() => { - if (item.Type === "Episode") { - return `${api?.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ParentThumbImageTag}`; - } - }, [item]); - - const [progress, _setProgress] = useState( - item.UserData?.PlayedPercentage || 0, - ); - - const blurhash = useMemo(() => { - const key = item.ImageTags?.Primary as string; - return item.ImageBlurHashes?.Primary?.[key]; - }, [item]); - - return ( - - - - {showProgress && progress > 0 && ( - - )} - - ); -}; diff --git a/components/posters/ParentPoster.tsx b/components/posters/ParentPoster.tsx deleted file mode 100644 index 47b62e4c..00000000 --- a/components/posters/ParentPoster.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Image } from "expo-image"; -import { useAtom } from "jotai"; -import { useMemo } from "react"; -import { View } from "react-native"; -import { apiAtom } from "@/providers/JellyfinProvider"; - -type PosterProps = { - id?: string; - showProgress?: boolean; -}; - -const ParentPoster: React.FC = ({ id }) => { - const [api] = useAtom(apiAtom); - - const url = useMemo( - () => `${api?.basePath}/Items/${id}/Images/Primary`, - [id], - ); - - if (!url || !id) - return ( - - ); - - return ( - - - - ); -}; - -export default ParentPoster; diff --git a/components/series/CastAndCrew.tsx b/components/series/CastAndCrew.tsx index 06bf47be..359539dc 100644 --- a/components/series/CastAndCrew.tsx +++ b/components/series/CastAndCrew.tsx @@ -8,8 +8,11 @@ import type React from "react"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { TouchableOpacity, View, type ViewProps } from "react-native"; -import { POSTER_CAROUSEL_HEIGHT } from "@/constants/Values"; import useRouter from "@/hooks/useAppRouter"; + +// Matches `w-28` poster cards (approx 112px wide, 10/15 aspect ratio) + 2 lines of text. +const POSTER_CAROUSEL_HEIGHT = 220; + import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { HorizontalScroll } from "../common/HorizontalScroll"; diff --git a/components/series/CurrentSeries.tsx b/components/series/CurrentSeries.tsx index d8c49e25..3f9a5074 100644 --- a/components/series/CurrentSeries.tsx +++ b/components/series/CurrentSeries.tsx @@ -3,8 +3,11 @@ import { useAtom } from "jotai"; import type React from "react"; import { useTranslation } from "react-i18next"; import { TouchableOpacity, View, type ViewProps } from "react-native"; -import { POSTER_CAROUSEL_HEIGHT } from "@/constants/Values"; import useRouter from "@/hooks/useAppRouter"; + +// Matches `w-28` poster cards (approx 112px wide, 10/15 aspect ratio) + 2 lines of text. +const POSTER_CAROUSEL_HEIGHT = 220; + import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; import { HorizontalScroll } from "../common/HorizontalScroll"; diff --git a/components/settings/Dashboard.tsx b/components/settings/Dashboard.tsx deleted file mode 100644 index d41de008..00000000 --- a/components/settings/Dashboard.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { View } from "react-native"; -import useRouter from "@/hooks/useAppRouter"; -import { useSessions, type useSessionsProps } from "@/hooks/useSessions"; -import { useSettings } from "@/utils/atoms/settings"; -import { ListGroup } from "../list/ListGroup"; -import { ListItem } from "../list/ListItem"; - -export const Dashboard = () => { - const { settings } = useSettings(); - const { sessions = [] } = useSessions({} as useSessionsProps); - const router = useRouter(); - - const { t } = useTranslation(); - - if (!settings) return null; - return ( - - - router.push("/settings/dashboard/sessions")} - title={t("home.settings.dashboard.sessions_title")} - showArrow - /> - - - ); -}; diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 997b17ed..1407c218 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -38,7 +38,6 @@ import { useVideoSlider } from "./hooks/useVideoSlider"; import { useVideoTime } from "./hooks/useVideoTime"; import { useControlsTimeout } from "./useControlsTimeout"; import { PlaybackSpeedScope } from "./utils/playback-speed-settings"; -import { type AspectRatio } from "./VideoScalingModeSelector"; interface Props { item: BaseItemDto; @@ -56,7 +55,6 @@ interface Props { startPictureInPicture?: () => Promise; play: () => void; pause: () => void; - aspectRatio?: AspectRatio; isZoomedToFill?: boolean; onZoomToggle?: () => void; api?: Api | null; @@ -81,7 +79,6 @@ export const Controls: FC = ({ showControls, setShowControls, mediaSource, - aspectRatio = "default", isZoomedToFill = false, onZoomToggle, api = null, @@ -475,7 +472,6 @@ export const Controls: FC = ({ goToNextItem={goToNextItem} previousItem={previousItem} nextItem={nextItem} - aspectRatio={aspectRatio} isZoomedToFill={isZoomedToFill} onZoomToggle={onZoomToggle} playbackSpeed={playbackSpeed} diff --git a/components/video-player/controls/HeaderControls.tsx b/components/video-player/controls/HeaderControls.tsx index 09a92892..11f3d360 100644 --- a/components/video-player/controls/HeaderControls.tsx +++ b/components/video-player/controls/HeaderControls.tsx @@ -14,7 +14,6 @@ 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 { type AspectRatio } from "./VideoScalingModeSelector"; import { ZoomToggle } from "./ZoomToggle"; interface HeaderControlsProps { @@ -28,7 +27,6 @@ interface HeaderControlsProps { goToNextItem: (options: { isAutoPlay?: boolean }) => void; previousItem?: BaseItemDto | null; nextItem?: BaseItemDto | null; - aspectRatio?: AspectRatio; isZoomedToFill?: boolean; onZoomToggle?: () => void; // Playback speed props @@ -47,7 +45,6 @@ export const HeaderControls: FC = ({ goToNextItem, previousItem, nextItem, - aspectRatio: _aspectRatio = "default", isZoomedToFill = false, onZoomToggle, playbackSpeed = 1.0, diff --git a/components/video-player/controls/VideoScalingModeSelector.tsx b/components/video-player/controls/VideoScalingModeSelector.tsx deleted file mode 100644 index 2cf72a2f..00000000 --- a/components/video-player/controls/VideoScalingModeSelector.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import React, { useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { Platform, View } from "react-native"; -import { - type OptionGroup, - PlatformDropdown, -} from "@/components/PlatformDropdown"; -import { useHaptic } from "@/hooks/useHaptic"; - -export type AspectRatio = "default" | "16:9" | "4:3" | "1:1" | "21:9"; - -interface AspectRatioSelectorProps { - currentRatio: AspectRatio; - onRatioChange: (ratio: AspectRatio) => void; - disabled?: boolean; -} - -interface AspectRatioOption { - id: AspectRatio; - label: string; - descriptionKey: string; -} - -export const AspectRatioSelector: React.FC = ({ - currentRatio, - onRatioChange, - disabled = false, -}) => { - const lightHapticFeedback = useHaptic("light"); - const { t } = useTranslation(); - - const handleRatioSelect = (ratio: AspectRatio) => { - onRatioChange(ratio); - lightHapticFeedback(); - }; - - const ASPECT_RATIO_OPTIONS: AspectRatioOption[] = [ - { - id: "default", - label: t("player.aspect_ratio_original"), - descriptionKey: "player.aspect_ratio_original_description", - }, - { - id: "16:9", - label: "16:9", - descriptionKey: "player.aspect_ratio_16_9_description", - }, - { - id: "4:3", - label: "4:3", - descriptionKey: "player.aspect_ratio_4_3_description", - }, - { - id: "1:1", - label: "1:1", - descriptionKey: "player.aspect_ratio_1_1_description", - }, - { - id: "21:9", - label: "21:9", - descriptionKey: "player.aspect_ratio_21_9_description", - }, - ]; - - const optionGroups = useMemo(() => { - return [ - { - options: ASPECT_RATIO_OPTIONS.map((option) => ({ - type: "radio" as const, - label: option.label, - value: option.id, - selected: option.id === currentRatio, - onPress: () => handleRatioSelect(option.id), - disabled, - })), - }, - ]; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentRatio, disabled]); - - const trigger = useMemo( - () => ( - - - - ), - [disabled], - ); - - // Hide on TV platforms - if (Platform.isTV) return null; - - return ( - - ); -}; diff --git a/constants/Languages.ts b/constants/Languages.ts deleted file mode 100644 index 8014e380..00000000 --- a/constants/Languages.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { DefaultLanguageOption } from "@/utils/atoms/settings"; - -export const LANGUAGES: DefaultLanguageOption[] = [ - { label: "English", value: "eng" }, - { label: "Spanish", value: "spa" }, - { label: "Chinese (Mandarin)", value: "cmn" }, - { label: "Hindi", value: "hin" }, - { label: "Arabic", value: "ara" }, - { label: "French", value: "fra" }, - { label: "Russian", value: "rus" }, - { label: "Portuguese", value: "por" }, - { label: "Japanese", value: "jpn" }, - { label: "German", value: "deu" }, - { label: "Italian", value: "ita" }, - { label: "Korean", value: "kor" }, - { label: "Turkish", value: "tur" }, - { label: "Dutch", value: "nld" }, - { label: "Polish", value: "pol" }, - { label: "Vietnamese", value: "vie" }, - { label: "Thai", value: "tha" }, - { label: "Indonesian", value: "ind" }, - { label: "Greek", value: "ell" }, - { label: "Swedish", value: "swe" }, - { label: "Danish", value: "dan" }, - { label: "Norwegian", value: "nor" }, - { label: "Finnish", value: "fin" }, - { label: "Czech", value: "ces" }, - { label: "Hungarian", value: "hun" }, - { label: "Romanian", value: "ron" }, - { label: "Ukrainian", value: "ukr" }, - { label: "Hebrew", value: "heb" }, - { label: "Bengali", value: "ben" }, - { label: "Punjabi", value: "pan" }, - { label: "Tagalog", value: "tgl" }, - { label: "Swahili", value: "swa" }, - { label: "Malay", value: "msa" }, - { label: "Persian", value: "fas" }, - { label: "Urdu", value: "urd" }, -]; diff --git a/constants/Values.ts b/constants/Values.ts deleted file mode 100644 index 5029d852..00000000 --- a/constants/Values.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Platform } from "react-native"; - -export const TAB_HEIGHT = Platform.OS === "android" ? 58 : 74; - -// Matches `w-28` poster cards (approx 112px wide, 10/15 aspect ratio) + 2 lines of text. -export const POSTER_CAROUSEL_HEIGHT = 220; diff --git a/hooks/useControlsVisibility.ts b/hooks/useControlsVisibility.ts deleted file mode 100644 index caca0d84..00000000 --- a/hooks/useControlsVisibility.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useCallback, useEffect, useRef } from "react"; -import { useSharedValue } from "react-native-reanimated"; - -export const useControlsVisibility = (timeout = 3000) => { - const opacity = useSharedValue(1); - - const hideControlsTimerRef = useRef | null>( - null, - ); - - const showControls = useCallback(() => { - opacity.value = 1; - if (hideControlsTimerRef.current) { - clearTimeout(hideControlsTimerRef.current); - } - hideControlsTimerRef.current = setTimeout(() => { - opacity.value = 0; - }, timeout); - }, [timeout]); - - const hideControls = useCallback(() => { - opacity.value = 0; - if (hideControlsTimerRef.current) { - clearTimeout(hideControlsTimerRef.current); - } - }, []); - - useEffect(() => { - return () => { - if (hideControlsTimerRef.current) { - clearTimeout(hideControlsTimerRef.current); - } - }; - }, []); - - return { opacity, showControls, hideControls }; -}; diff --git a/hooks/useDownloadedFileOpener.ts b/hooks/useDownloadedFileOpener.ts deleted file mode 100644 index 845161a1..00000000 --- a/hooks/useDownloadedFileOpener.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { useCallback } from "react"; -import useRouter from "@/hooks/useAppRouter"; -import { usePlaySettings } from "@/providers/PlaySettingsProvider"; -import { writeToLog } from "@/utils/log"; - -export const useDownloadedFileOpener = () => { - const router = useRouter(); - const { setPlayUrl, setOfflineSettings } = usePlaySettings(); - - const openFile = useCallback( - async (item: BaseItemDto) => { - if (!item.Id) { - writeToLog("ERROR", "Attempted to open a file without an ID."); - console.error("Attempted to open a file without an ID."); - return; - } - const queryParams = new URLSearchParams({ - itemId: item.Id, - offline: "true", - playbackPosition: - item.UserData?.PlaybackPositionTicks?.toString() ?? "0", - }); - try { - router.push(`/player/direct-player?${queryParams.toString()}`); - } catch (error) { - writeToLog("ERROR", "Error opening file", error); - console.error("Error opening file:", error); - } - }, - [setOfflineSettings, setPlayUrl, router], - ); - - return { openFile }; -}; diff --git a/hooks/useImageColors.ts b/hooks/useImageColors.ts deleted file mode 100644 index 4d8a0136..00000000 --- a/hooks/useImageColors.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { useAtom, useAtomValue } from "jotai"; -import { useEffect, useMemo } from "react"; -import { Platform } from "react-native"; -import type * as ImageColorsType from "react-native-image-colors"; -import { apiAtom } from "@/providers/JellyfinProvider"; - -// Conditionally import react-native-image-colors only on non-TV platforms -const ImageColors = Platform.isTV - ? null - : (require("react-native-image-colors") as typeof ImageColorsType); - -import { - adjustToNearBlack, - calculateTextColor, - isCloseToBlack, - itemThemeColorAtom, -} from "@/utils/atoms/primaryColor"; -import { getItemImage } from "@/utils/getItemImage"; -import { storage } from "@/utils/mmkv"; - -/** - * Custom hook to extract and manage image colors for a given item. - * - * @param item - The BaseItemDto object representing the item. - * @param disabled - A boolean flag to disable color extraction. - * - */ -export const useImageColors = ({ - item, - url, - disabled, -}: { - item?: BaseItemDto | null; - url?: string | null; - disabled?: boolean; -}) => { - const api = useAtomValue(apiAtom); - const [, setPrimaryColor] = useAtom(itemThemeColorAtom); - - const isTv = Platform.isTV; - - const source = useMemo(() => { - if (!api) return; - if (url) return { uri: url }; - if (item) - return getItemImage({ - item, - api, - variant: "Primary", - quality: 80, - width: 300, - }); - return null; - }, [api, item, url]); - - useEffect(() => { - if (isTv) return; - if (disabled) return; - if (source?.uri) { - const _primary = storage.getString(`${source.uri}-primary`); - const _text = storage.getString(`${source.uri}-text`); - - if (_primary && _text) { - setPrimaryColor({ - primary: _primary, - text: _text, - }); - return; - } - - // Extract colors from the image - if (!ImageColors?.getColors) return; - - ImageColors.getColors(source.uri, { - fallback: "#fff", - cache: false, - }) - .then((colors: ImageColorsType.ImageColorsResult) => { - let primary = "#fff"; - let text = "#000"; - let backup = "#fff"; - - // Select the appropriate color based on the platform - if (colors.platform === "android") { - primary = colors.dominant; - backup = colors.vibrant; - } else if (colors.platform === "ios") { - primary = colors.detail; - backup = colors.primary; - } - - // Adjust the primary color if it's too close to black - if (primary && isCloseToBlack(primary)) { - if (backup && !isCloseToBlack(backup)) primary = backup; - primary = adjustToNearBlack(primary); - } - - // Calculate the text color based on the primary color - if (primary) text = calculateTextColor(primary); - - setPrimaryColor({ - primary, - text, - }); - - // Cache the colors in storage - if (source.uri && primary) { - storage.set(`${source.uri}-primary`, primary); - storage.set(`${source.uri}-text`, text); - } - }) - .catch((error: any) => { - console.error("Error getting colors", error); - }); - } - }, [isTv, source?.uri, setPrimaryColor, disabled]); - - if (isTv) return; -}; diff --git a/utils/bToMb.ts b/utils/bToMb.ts deleted file mode 100644 index 79b7caf4..00000000 --- a/utils/bToMb.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Convert bits to megabits or gigabits - * - * Return nice looking string - * If under 1000Mb, return XXXMB, else return X.XGB - */ - -export function convertBitsToMegabitsOrGigabits(bits?: number | null): string { - if (!bits) return "0MB"; - - const megabits = bits / 1000000; - - if (megabits < 1000) { - return `${Math.round(megabits)}MB`; - } - const gigabits = megabits / 1000; - return `${gigabits.toFixed(1)}GB`; -} diff --git a/utils/collectionTypeToItemType.ts b/utils/collectionTypeToItemType.ts deleted file mode 100644 index 0889b8d6..00000000 --- a/utils/collectionTypeToItemType.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - BaseItemKind, - CollectionType, -} from "@jellyfin/sdk/lib/generated-client"; - -/** - * Converts a ColletionType to a BaseItemKind (also called ItemType) - * - * CollectionTypes - * readonly Unknown: "unknown"; - readonly Movies: "movies"; - readonly Tvshows: "tvshows"; - readonly Trailers: "trailers"; - readonly Homevideos: "homevideos"; - readonly Boxsets: "boxsets"; - readonly Books: "books"; - readonly Photos: "photos"; - readonly Livetv: "livetv"; - readonly Playlists: "playlists"; - readonly Folders: "folders"; - */ -export const colletionTypeToItemType = ( - collectionType?: CollectionType | null, -): BaseItemKind | undefined => { - if (!collectionType) return undefined; - - switch (collectionType) { - case CollectionType.Movies: - return BaseItemKind.Movie; - case CollectionType.Tvshows: - return BaseItemKind.Series; - case CollectionType.Homevideos: - return BaseItemKind.Video; - case CollectionType.Books: - return BaseItemKind.Book; - case CollectionType.Playlists: - return BaseItemKind.Playlist; - case CollectionType.Folders: - return BaseItemKind.Folder; - case CollectionType.Photos: - return BaseItemKind.Photo; - case CollectionType.Trailers: - return BaseItemKind.Trailer; - } - - return undefined; -}; diff --git a/utils/hls/parseM3U8ForSubtitles.ts b/utils/hls/parseM3U8ForSubtitles.ts deleted file mode 100644 index 5e0ad382..00000000 --- a/utils/hls/parseM3U8ForSubtitles.ts +++ /dev/null @@ -1,56 +0,0 @@ -import axios from "axios"; - -export interface SubtitleTrack { - index: number; - name: string; - uri: string; - language: string; - default: boolean; - forced: boolean; - autoSelect: boolean; -} - -export async function parseM3U8ForSubtitles( - url: string, -): Promise { - try { - const response = await axios.get(url, { responseType: "text" }); - const lines = response.data.split(/\r?\n/); - const subtitleTracks: SubtitleTrack[] = []; - let index = 0; - - lines.forEach((line: string) => { - if (line.startsWith("#EXT-X-MEDIA:TYPE=SUBTITLES")) { - const attributes = parseAttributes(line); - const track: SubtitleTrack = { - index: index++, - name: attributes.NAME || "", - uri: attributes.URI || "", - language: attributes.LANGUAGE || "", - default: attributes.DEFAULT === "YES", - forced: attributes.FORCED === "YES", - autoSelect: attributes.AUTOSELECT === "YES", - }; - subtitleTracks.push(track); - } - }); - - return subtitleTracks; - } catch (error) { - console.error("Failed to fetch or parse the M3U8 file:", error); - throw error; - } -} - -function parseAttributes(line: string): { [key: string]: string } { - const attributes: { [key: string]: string } = {}; - const regex = /([A-Z-]+)=(?:"([^"]*)"|([^,]*))/g; - - for (const match of line.matchAll(regex)) { - const key = match[1]; - const value = match[2] ?? match[3]; // quoted or unquoted - attributes[key] = value; - } - - return attributes; -} diff --git a/utils/jellyfin/getDefaultPlaySettings.ts b/utils/jellyfin/getDefaultPlaySettings.ts index d5ac5032..3cafd0d0 100644 --- a/utils/jellyfin/getDefaultPlaySettings.ts +++ b/utils/jellyfin/getDefaultPlaySettings.ts @@ -15,11 +15,6 @@ import type { } from "@jellyfin/sdk/lib/generated-client"; import { BITRATES } from "@/components/BitrateSelector"; import { type Settings } from "../atoms/settings"; -import { - AudioStreamRanker, - StreamRanker, - SubtitleStreamRanker, -} from "../streamRanker"; export interface PlaySettings { item: BaseItemDto; @@ -54,42 +49,27 @@ export function getDefaultPlaySettings( } const mediaSource = item.MediaSources?.[0]; - const streams = mediaSource?.MediaStreams ?? []; + const _streams = mediaSource?.MediaStreams ?? []; // Start with media source defaults let audioIndex = mediaSource?.DefaultAudioStreamIndex; let subtitleIndex = mediaSource?.DefaultSubtitleStreamIndex ?? -1; // Try to match previous selections (sequential play) - if (previous?.indexes && previous?.source && settings) { + // Simplified: just use previous indexes if available + if (previous?.indexes && settings) { if ( settings.rememberSubtitleSelections && previous.indexes.subtitleIndex !== undefined ) { - const ranker = new StreamRanker(new SubtitleStreamRanker()); - const result = { DefaultSubtitleStreamIndex: subtitleIndex }; - ranker.rankStream( - previous.indexes.subtitleIndex, - previous.source, - streams, - result, - ); - subtitleIndex = result.DefaultSubtitleStreamIndex; + subtitleIndex = previous.indexes.subtitleIndex; } if ( settings.rememberAudioSelections && previous.indexes.audioIndex !== undefined ) { - const ranker = new StreamRanker(new AudioStreamRanker()); - const result = { DefaultAudioStreamIndex: audioIndex }; - ranker.rankStream( - previous.indexes.audioIndex, - previous.source, - streams, - result, - ); - audioIndex = result.DefaultAudioStreamIndex; + audioIndex = previous.indexes.audioIndex; } } diff --git a/utils/jellyfin/session/capabilities.ts b/utils/jellyfin/session/capabilities.ts deleted file mode 100644 index 4e4f2074..00000000 --- a/utils/jellyfin/session/capabilities.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Api } from "@jellyfin/sdk"; -import type { AxiosResponse } from "axios"; -import type { Settings } from "../../atoms/settings"; -import { generateDeviceProfile } from "../../profiles/native"; -import { getAuthHeaders } from "../jellyfin"; - -interface PostCapabilitiesParams { - api: Api | null | undefined; - itemId: string | null | undefined; - sessionId: string | null | undefined; - deviceProfile: Settings["deviceProfile"]; -} - -/** - * Marks a media item as not played for a specific user. - * - * @param params - The parameters for marking an item as not played - * @returns A promise that resolves to true if the operation was successful, false otherwise - */ -export const postCapabilities = async ({ - api, - itemId, - sessionId, -}: PostCapabilitiesParams): Promise => { - if (!api || !itemId || !sessionId) { - throw new Error("Missing parameters for marking item as not played"); - } - - try { - const d = api.axiosInstance.post( - `${api.basePath}/Sessions/Capabilities/Full`, - { - playableMediaTypes: ["Audio", "Video"], - supportedCommands: [ - "PlayState", - "Play", - "ToggleFullscreen", - "DisplayMessage", - "Mute", - "Unmute", - "SetVolume", - "ToggleMute", - ], - supportsMediaControl: true, - id: sessionId, - DeviceProfile: generateDeviceProfile(), - }, - { - headers: getAuthHeaders(api), - }, - ); - return d; - } catch (_error) { - throw new Error("Failed to mark as not played"); - } -}; diff --git a/utils/jellyfin/tvshows/nextUp.ts b/utils/jellyfin/tvshows/nextUp.ts deleted file mode 100644 index 414a47a7..00000000 --- a/utils/jellyfin/tvshows/nextUp.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Api } from "@jellyfin/sdk"; -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { getAuthHeaders } from "../jellyfin"; - -interface NextUpParams { - itemId?: string | null; - userId?: string | null; - api?: Api | null; -} - -/** - * Fetches the next up episodes for a series or all series for a user. - * - * @param params - The parameters for fetching next up episodes - * @returns A promise that resolves to an array of BaseItemDto representing the next up episodes - */ -export const nextUp = async ({ - itemId, - userId, - api, -}: NextUpParams): Promise => { - if (!userId || !api) { - console.error("Invalid parameters for nextUp: missing userId or api"); - return []; - } - - try { - const response = await api.axiosInstance.get<{ Items: BaseItemDto[] }>( - `${api.basePath}/Shows/NextUp`, - { - params: { - SeriesId: itemId || undefined, - UserId: userId, - Fields: "MediaSourceCount", - }, - headers: getAuthHeaders(api), - }, - ); - - return response.data.Items; - } catch (_error) { - return []; - } -}; diff --git a/utils/jellyfin/user-library/getItemById.ts b/utils/jellyfin/user-library/getItemById.ts deleted file mode 100644 index 261733b6..00000000 --- a/utils/jellyfin/user-library/getItemById.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Api } from "@jellyfin/sdk"; -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; - -/** - * Retrieves an item by its ID from the API. - * - * @param api - The Jellyfin API instance. - * @param itemId - The ID of the item to retrieve. - * @returns The item object or undefined if no item matches the ID. - */ -export const getItemById = async ( - api?: Api | null | undefined, - itemId?: string | null | undefined, -): Promise => { - if (!api || !itemId) { - return undefined; - } - - try { - const itemData = await getUserLibraryApi(api).getItem({ itemId }); - - const item = itemData.data; - if (!item) { - console.error("No items found with the specified ID:", itemId); - return undefined; - } - - return item; - } catch (error) { - console.error("Failed to retrieve the item:", error); - throw new Error(`Failed to retrieve the item due to an error: ${error}`); - } -}; diff --git a/utils/log.tsx b/utils/log.tsx index c633ec69..7dc1d53a 100644 --- a/utils/log.tsx +++ b/utils/log.tsx @@ -72,21 +72,6 @@ export const readFromLog = (): LogEntry[] => { return logs ? JSON.parse(logs) : []; }; -export const clearLogs = () => { - storage.remove("logs"); -}; - -export const dumpDownloadDiagnostics = (extra: any = {}) => { - const diagnostics = { - timestamp: new Date().toISOString(), - processes: extra?.processes || [], - nativeTasks: extra?.nativeTasks || [], - focusedProcess: extra?.focusedProcess || null, - }; - writeDebugLog("Download diagnostics", diagnostics); - return diagnostics; -}; - export function useLog() { const context = useContext(LogContext); if (context === null) { diff --git a/utils/secondsToTicks.ts b/utils/secondsToTicks.ts deleted file mode 100644 index df13813e..00000000 --- a/utils/secondsToTicks.ts +++ /dev/null @@ -1,5 +0,0 @@ -// seconds to ticks util - -export function secondsToTicks(seconds: number): number { - return seconds * 10000000; -} diff --git a/utils/secureCredentials.ts b/utils/secureCredentials.ts index f64a56d7..4e3364b0 100644 --- a/utils/secureCredentials.ts +++ b/utils/secureCredentials.ts @@ -76,9 +76,9 @@ interface LegacyServerCredential { } /** - * Encode server URL to valid secure store key (legacy, for migration). + * Generate a unique key for a server URL (for secure storage). */ -export function serverUrlToKey(serverUrl: string): string { +function serverUrlToKey(serverUrl: string): string { const encoded = btoa(serverUrl).replace(/[^a-zA-Z0-9]/g, "_"); return `${CREDENTIAL_KEY_PREFIX}${encoded}`; } @@ -86,7 +86,7 @@ export function serverUrlToKey(serverUrl: string): string { /** * Generate credential key for a specific account (serverUrl + userId). */ -export function credentialKey(serverUrl: string, userId: string): string { +function credentialKey(serverUrl: string, userId: string): string { const combined = `${serverUrl}:${userId}`; const encoded = btoa(combined).replace(/[^a-zA-Z0-9]/g, "_"); return `${CREDENTIAL_KEY_PREFIX}${encoded}`; @@ -200,10 +200,7 @@ export async function hasAccountCredential( return stored !== null; } -/** - * Delete all credentials for all accounts on all servers. - */ -export async function clearAllCredentials(): Promise { +async function _clearAllCredentials(): Promise { const previousServers = getPreviousServers(); for (const server of previousServers) { diff --git a/utils/streamRanker.ts b/utils/streamRanker.ts deleted file mode 100644 index 8121adea..00000000 --- a/utils/streamRanker.ts +++ /dev/null @@ -1,159 +0,0 @@ -import type { - MediaSourceInfo, - MediaStream, -} from "@jellyfin/sdk/lib/generated-client"; - -abstract class StreamRankerStrategy { - abstract streamType: string; - - abstract rankStream( - prevIndex: number, - prevSource: MediaSourceInfo, - mediaStreams: MediaStream[], - trackOptions: any, - ): void; - - protected rank( - prevIndex: number, - prevSource: MediaSourceInfo, - mediaStreams: MediaStream[], - trackOptions: any, - ): void { - if (prevIndex === -1) { - console.debug("AutoSet Subtitle - No Stream Set"); - trackOptions[`Default${this.streamType}StreamIndex`] = -1; - return; - } - - if (!prevSource.MediaStreams || !mediaStreams) { - console.debug(`AutoSet ${this.streamType} - No MediaStreams`); - return; - } - - let bestStreamIndex = null; - let bestStreamScore = 0; - - const prevStream = prevSource.MediaStreams[prevIndex]; - - if (!prevStream) { - console.debug(`AutoSet ${this.streamType} - No prevStream`); - return; - } - - console.debug( - `AutoSet ${this.streamType} - Previous was ${prevStream.Index} - ${prevStream.DisplayTitle}`, - ); - - let prevRelIndex = 0; - for (const stream of prevSource.MediaStreams) { - if (stream.Type !== this.streamType) { - continue; - } - - if (stream.Index === prevIndex) { - break; - } - - prevRelIndex += 1; - } - - let newRelIndex = 0; - for (const stream of mediaStreams) { - if (stream.Type !== this.streamType) { - continue; - } - - let score = 0; - - if (prevStream.Codec === stream.Codec) { - score += 1; - } - if (prevRelIndex === newRelIndex) { - score += 1; - } - if ( - prevStream.DisplayTitle && - prevStream.DisplayTitle === stream.DisplayTitle - ) { - score += 2; - } - if ( - prevStream.Language && - prevStream.Language !== "und" && - prevStream.Language === stream.Language - ) { - score += 2; - } - - console.debug( - `AutoSet ${this.streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}`, - ); - if (score > bestStreamScore && score >= 3) { - bestStreamScore = score; - bestStreamIndex = stream.Index; - } - - newRelIndex += 1; - } - - if (bestStreamIndex != null) { - console.debug( - `AutoSet ${this.streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.`, - ); - trackOptions[`Default${this.streamType}StreamIndex`] = bestStreamIndex; - } else { - console.debug( - `AutoSet ${this.streamType} - Threshold not met. Using default.`, - ); - } - } -} - -class SubtitleStreamRanker extends StreamRankerStrategy { - streamType = "Subtitle"; - - rankStream( - prevIndex: number, - prevSource: MediaSourceInfo, - mediaStreams: MediaStream[], - trackOptions: any, - ): void { - super.rank(prevIndex, prevSource, mediaStreams, trackOptions); - } -} - -class AudioStreamRanker extends StreamRankerStrategy { - streamType = "Audio"; - - rankStream( - prevIndex: number, - prevSource: MediaSourceInfo, - mediaStreams: MediaStream[], - trackOptions: any, - ): void { - super.rank(prevIndex, prevSource, mediaStreams, trackOptions); - } -} - -class StreamRanker { - private strategy: StreamRankerStrategy; - - constructor(strategy: StreamRankerStrategy) { - this.strategy = strategy; - } - - setStrategy(strategy: StreamRankerStrategy) { - this.strategy = strategy; - } - - rankStream( - prevIndex: number, - prevSource: MediaSourceInfo, - mediaStreams: MediaStream[], - trackOptions: any, - ) { - this.strategy.rankStream(prevIndex, prevSource, mediaStreams, trackOptions); - } -} - -export { StreamRanker, SubtitleStreamRanker, AudioStreamRanker };