diff --git a/components/chapters/ChapterList.tsx b/components/chapters/ChapterList.tsx index dd6ef1bd9..e44332095 100644 --- a/components/chapters/ChapterList.tsx +++ b/components/chapters/ChapterList.tsx @@ -11,6 +11,7 @@ import { useTranslation } from "react-i18next"; import { FlatList, Modal, Pressable, StyleSheet, View } from "react-native"; import { Text } from "@/components/common/Text"; import { Colors } from "@/constants/Colors"; +import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets"; import { type ChapterEntry, chapterStartsMs, @@ -38,6 +39,7 @@ function ChapterListComponent({ onClose, }: ChapterListProps) { const { t } = useTranslation(); + const safeArea = useControlsSafeAreaInsets(); const listRef = useRef>(null); const entries = useMemo(() => sortedChapters(chapters), [chapters]); @@ -79,7 +81,17 @@ function ChapterListComponent({ supportedOrientations={["portrait", "landscape"]} > - e.stopPropagation()} style={styles.sheet}> + e.stopPropagation()} + style={[ + styles.sheet, + { + marginLeft: safeArea.left, + marginRight: safeArea.right, + paddingBottom: safeArea.bottom, + }, + ]} + > {t("chapters.title")} = ({ @@ -111,11 +108,10 @@ export const BottomControls: FC = ({ trickPlayUrl, trickplayInfo, time, - chapterPositions = [], }) => { const { settings } = useSettings(); const { t } = useTranslation(); - const insets = useSafeAreaInsets(); + const insets = useControlsSafeAreaInsets(); const [chapterListVisible, setChapterListVisible] = useState(false); // Only expose chapter UI when there are at least two real markers. @@ -146,13 +142,9 @@ export const BottomControls: FC = ({ style={[ { position: "absolute", - right: - (settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0, - left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0, - bottom: - (settings?.safeAreaInControlsEnabled ?? true) - ? Math.max(insets.bottom - 17, 0) - : 0, + right: insets.right, + left: insets.left, + bottom: Math.max(insets.bottom - 17, 0), }, ]} className={"flex flex-col px-2"} @@ -188,17 +180,6 @@ export const BottomControls: FC = ({ ) : null} - {hasChapters && ( - setChapterListVisible(true)} - hitSlop={10} - className='justify-center mr-4' - accessibilityRole='button' - accessibilityLabel={t("chapters.open")} - > - - - )} = ({ onPress={handleNextEpisodeManual} /> )} + {hasChapters && ( + setChapterListVisible(true)} + hitSlop={10} + className='justify-center ml-4' + accessibilityRole='button' + accessibilityLabel={t("chapters.open")} + > + + + )} = ({ goToNextChapter, }) => { const { settings } = useSettings(); - const insets = useSafeAreaInsets(); + const insets = useControlsSafeAreaInsets(); return ( = ({ hasNextChapter, goToPreviousChapter, goToNextChapter, - chapterPositions, } = useChapterNavigation({ chapters: item.Chapters, progress, @@ -585,7 +584,6 @@ export const Controls: FC = ({ trickPlayUrl={trickPlayUrl} trickplayInfo={trickplayInfo} time={isSliding || showRemoteBubble ? time : remoteTime} - chapterPositions={chapterPositions} /> diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index 35a235353..b87215b3a 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -5,7 +5,6 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { atom, useAtom } from "jotai"; import { useEffect, useMemo, useRef } from "react"; import { TouchableOpacity, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; import { HorizontalScroll, @@ -17,10 +16,10 @@ import { SeasonDropdown, type SeasonIndexState, } from "@/components/series/SeasonDropdown"; +import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets"; import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider"; -import { useSettings } from "@/utils/atoms/settings"; import { getDownloadedEpisodesForSeason, getDownloadedSeasonNumbers, @@ -46,8 +45,7 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { scrollViewRef.current?.scrollToIndex(index, 100); }; const isOffline = useOfflineMode(); - const { settings } = useSettings(); - const insets = useSafeAreaInsets(); + const insets = useControlsSafeAreaInsets(); // Set the initial season index useEffect(() => { @@ -182,12 +180,9 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { backgroundColor: "black", height: "100%", width: "100%", - paddingTop: - (settings?.safeAreaInControlsEnabled ?? true) ? insets.top : 0, - paddingLeft: - (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0, - paddingRight: - (settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0, + paddingTop: insets.top, + paddingLeft: insets.left, + paddingRight: insets.right, }} > = ({ showTechnicalInfo = false, onToggleTechnicalInfo, }) => { - const { settings } = useSettings(); const router = useRouter(); - const insets = useSafeAreaInsets(); + const insets = useControlsSafeAreaInsets(); const lightHapticFeedback = useHaptic("light"); const { orientation, lockOrientation } = useOrientation(); const [isTogglingOrientation, setIsTogglingOrientation] = useState(false); @@ -99,10 +97,9 @@ export const HeaderControls: FC = ({ style={[ { position: "absolute", - top: (settings?.safeAreaInControlsEnabled ?? true) ? insets.top : 0, - left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0, - right: - (settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0, + top: insets.top, + left: insets.left, + right: insets.right, padding: HEADER_LAYOUT.CONTAINER_PADDING, }, ]} diff --git a/components/video-player/controls/TechnicalInfoOverlay.tsx b/components/video-player/controls/TechnicalInfoOverlay.tsx index 20ec1fc47..fe98a16a9 100644 --- a/components/video-player/controls/TechnicalInfoOverlay.tsx +++ b/components/video-player/controls/TechnicalInfoOverlay.tsx @@ -16,8 +16,8 @@ import Animated, { } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useScaledTVTypography } from "@/constants/TVTypography"; +import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets"; import type { TechnicalInfo } from "@/modules/mpv-player"; -import { useSettings } from "@/utils/atoms/settings"; import { HEADER_LAYOUT } from "./constants"; type PlayMethod = "DirectPlay" | "DirectStream" | "Transcode"; @@ -184,8 +184,8 @@ export const TechnicalInfoOverlay: FC = memo( currentAudioIndex, }) => { const typography = useScaledTVTypography(); - const { settings } = useSettings(); const insets = useSafeAreaInsets(); + const safeInsets = useControlsSafeAreaInsets(); const [info, setInfo] = useState(null); const opacity = useSharedValue(0); @@ -268,14 +268,8 @@ export const TechnicalInfoOverlay: FC = memo( left: Math.max(insets.left, 48) + 20, } : { - top: - (settings?.safeAreaInControlsEnabled ?? true) - ? insets.top + HEADER_LAYOUT.CONTAINER_PADDING + 4 - : HEADER_LAYOUT.CONTAINER_PADDING + 4, - left: - (settings?.safeAreaInControlsEnabled ?? true) - ? insets.left + HEADER_LAYOUT.CONTAINER_PADDING + 20 - : HEADER_LAYOUT.CONTAINER_PADDING + 20, + top: safeInsets.top + HEADER_LAYOUT.CONTAINER_PADDING + 4, + left: safeInsets.left + HEADER_LAYOUT.CONTAINER_PADDING + 20, }; const textStyle = Platform.isTV diff --git a/hooks/useControlsSafeAreaInsets.ts b/hooks/useControlsSafeAreaInsets.ts new file mode 100644 index 000000000..4fa4968e6 --- /dev/null +++ b/hooks/useControlsSafeAreaInsets.ts @@ -0,0 +1,18 @@ +import { + type EdgeInsets, + useSafeAreaInsets, +} from "react-native-safe-area-context"; +import { useSettings } from "@/utils/atoms/settings"; + +const ZERO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 }; + +/** + * Returns safe-area insets to apply to in-player controls, honoring the + * `safeAreaInControlsEnabled` user setting. When the setting is disabled, + * returns zero insets so controls can sit flush against the screen edges. + */ +export const useControlsSafeAreaInsets = (): EdgeInsets => { + const { settings } = useSettings(); + const insets = useSafeAreaInsets(); + return settings.safeAreaInControlsEnabled ? insets : ZERO_INSETS; +};