mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-02 12:08:37 +01:00
Compare commits
3 Commits
fix/downlo
...
fix/chapte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
076573e673 | ||
|
|
bf061403fe | ||
|
|
88163eb531 |
@@ -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<FlatList<ChapterEntry>>(null);
|
||||
|
||||
const entries = useMemo(() => sortedChapters(chapters), [chapters]);
|
||||
@@ -79,7 +81,17 @@ function ChapterListComponent({
|
||||
supportedOrientations={["portrait", "landscape"]}
|
||||
>
|
||||
<Pressable onPress={onClose} style={styles.backdrop}>
|
||||
<Pressable onPress={(e) => e.stopPropagation()} style={styles.sheet}>
|
||||
<Pressable
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
style={[
|
||||
styles.sheet,
|
||||
{
|
||||
marginLeft: safeArea.left,
|
||||
marginRight: safeArea.right,
|
||||
paddingBottom: safeArea.bottom,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>{t("chapters.title")}</Text>
|
||||
<Pressable
|
||||
@@ -160,14 +172,12 @@ const styles = StyleSheet.create({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
justifyContent: "flex-end",
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
},
|
||||
sheet: {
|
||||
backgroundColor: Colors.background,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
maxHeight: "70%",
|
||||
paddingBottom: 24,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
|
||||
@@ -8,10 +8,10 @@ import { useTranslation } from "react-i18next";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
import { type SharedValue } from "react-native-reanimated";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { ChapterList } from "@/components/chapters/ChapterList";
|
||||
import { ChapterTicks } from "@/components/chapters/ChapterTicks";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { chapterMarkers, chapterNameAt } from "@/utils/chapters";
|
||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||
@@ -75,9 +75,6 @@ interface BottomControlsProps {
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
// Chapter props
|
||||
chapterPositions?: number[];
|
||||
}
|
||||
|
||||
export const BottomControls: FC<BottomControlsProps> = ({
|
||||
@@ -111,11 +108,10 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
||||
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<BottomControlsProps> = ({
|
||||
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<BottomControlsProps> = ({
|
||||
) : null}
|
||||
</View>
|
||||
<View className='flex flex-row items-center space-x-2 shrink-0'>
|
||||
{hasChapters && (
|
||||
<Pressable
|
||||
onPress={() => setChapterListVisible(true)}
|
||||
hitSlop={10}
|
||||
className='justify-center mr-4'
|
||||
accessibilityRole='button'
|
||||
accessibilityLabel={t("chapters.open")}
|
||||
>
|
||||
<Ionicons name='bookmarks' size={24} color='white' />
|
||||
</Pressable>
|
||||
)}
|
||||
<SkipButton
|
||||
showButton={showSkipButton}
|
||||
onPress={skipIntro}
|
||||
@@ -230,6 +211,17 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
||||
onPress={handleNextEpisodeManual}
|
||||
/>
|
||||
)}
|
||||
{hasChapters && (
|
||||
<Pressable
|
||||
onPress={() => setChapterListVisible(true)}
|
||||
hitSlop={10}
|
||||
className='justify-center ml-4'
|
||||
accessibilityRole='button'
|
||||
accessibilityLabel={t("chapters.open")}
|
||||
>
|
||||
<Ionicons name='bookmarks' size={24} color='white' />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type { FC } from "react";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import AudioSlider from "./AudioSlider";
|
||||
import BrightnessSlider from "./BrightnessSlider";
|
||||
@@ -42,15 +42,15 @@ export const CenterControls: FC<CenterControlsProps> = ({
|
||||
goToNextChapter,
|
||||
}) => {
|
||||
const { settings } = useSettings();
|
||||
const insets = useSafeAreaInsets();
|
||||
const insets = useControlsSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
|
||||
right: (settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
|
||||
left: insets.left,
|
||||
right: insets.right,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
|
||||
@@ -219,7 +219,6 @@ export const Controls: FC<Props> = ({
|
||||
hasNextChapter,
|
||||
goToPreviousChapter,
|
||||
goToNextChapter,
|
||||
chapterPositions,
|
||||
} = useChapterNavigation({
|
||||
chapters: item.Chapters,
|
||||
progress,
|
||||
@@ -585,7 +584,6 @@ export const Controls: FC<Props> = ({
|
||||
trickPlayUrl={trickPlayUrl}
|
||||
trickplayInfo={trickplayInfo}
|
||||
time={isSliding || showRemoteBubble ? time : remoteTime}
|
||||
chapterPositions={chapterPositions}
|
||||
/>
|
||||
</Animated.View>
|
||||
</>
|
||||
|
||||
@@ -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<Props> = ({ 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<Props> = ({ 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,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -5,12 +5,11 @@ import type {
|
||||
} 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 { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||
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";
|
||||
@@ -58,9 +57,8 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
|
||||
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<HeaderControlsProps> = ({
|
||||
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,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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<TechnicalInfoOverlayProps> = memo(
|
||||
currentAudioIndex,
|
||||
}) => {
|
||||
const typography = useScaledTVTypography();
|
||||
const { settings } = useSettings();
|
||||
const insets = useSafeAreaInsets();
|
||||
const safeInsets = useControlsSafeAreaInsets();
|
||||
const [info, setInfo] = useState<TechnicalInfo | null>(null);
|
||||
|
||||
const opacity = useSharedValue(0);
|
||||
@@ -268,14 +268,8 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = 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
|
||||
|
||||
18
hooks/useControlsSafeAreaInsets.ts
Normal file
18
hooks/useControlsSafeAreaInsets.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -81,7 +81,6 @@ class MpvPlayerView: ExpoView {
|
||||
private func setupView() {
|
||||
clipsToBounds = true
|
||||
backgroundColor = .black
|
||||
configureAudioSession()
|
||||
|
||||
videoContainer = UIView()
|
||||
videoContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -141,21 +140,26 @@ class MpvPlayerView: ExpoView {
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
// MARK: - Audio Session & Notifications
|
||||
|
||||
private func configureAudioSession() {
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
try audioSession.setCategory(
|
||||
.playback,
|
||||
mode: .moviePlayback,
|
||||
policy: .longFormAudio,
|
||||
options: []
|
||||
)
|
||||
try audioSession.setActive(true)
|
||||
try session.setCategory(.playback, mode: .moviePlayback, policy: .longFormAudio, options: [])
|
||||
try session.setActive(true)
|
||||
} catch {
|
||||
print("Failed to configure audio session: \(error)")
|
||||
}
|
||||
}
|
||||
// MARK: - Audio Session & Notifications
|
||||
|
||||
/// Deactivate the session AND reset the category — `setActive(false)` alone
|
||||
/// leaves `.playback`/`.longFormAudio` on the shared singleton, so any later
|
||||
/// reactivation (foreground, route change, other modules) re-steals audio.
|
||||
private func tearDownAudioSession() {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
try? session.setActive(false, options: .notifyOthersOnDeactivation)
|
||||
try? session.setCategory(.ambient, mode: .default, options: [.mixWithOthers])
|
||||
}
|
||||
|
||||
private func setupNotifications() {
|
||||
// Handle audio session interruptions (e.g., incoming calls, other apps playing audio)
|
||||
@@ -270,6 +274,7 @@ class MpvPlayerView: ExpoView {
|
||||
|
||||
func play() {
|
||||
intendedPlayState = true
|
||||
configureAudioSession()
|
||||
setupRemoteCommands()
|
||||
renderer?.play()
|
||||
pipController?.setPlaybackRate(1.0)
|
||||
@@ -440,6 +445,7 @@ class MpvPlayerView: ExpoView {
|
||||
renderer?.stop()
|
||||
displayLayer.removeFromSuperlayer()
|
||||
clearNowPlayingInfo()
|
||||
tearDownAudioSession()
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
}
|
||||
@@ -519,9 +525,7 @@ extension MpvPlayerView: MPVLayerRendererDelegate {
|
||||
}
|
||||
|
||||
func renderer(_: MPVLayerRenderer, didSelectAudioOutput audioOutput: String) {
|
||||
// Audio output is now active - this is the right time to activate audio session and set Now Playing
|
||||
print("[MPV] Audio output ready (\(audioOutput)), activating audio session and syncing Now Playing")
|
||||
nowPlayingManager.activateAudioSession()
|
||||
print("[MPV] Audio output ready (\(audioOutput)), syncing Now Playing")
|
||||
syncNowPlaying(isPlaying: !isPaused())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user