mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-02-23 12:32:26 +00:00
Enhances README with comprehensive feature categorization including Media Playback, Media Management, and Advanced Features sections Expands documentation for music library support, search providers (Marlin, Streamystats, Jellysearch), and plugin functionality Updates FAQ section with more detailed answers about library visibility, downloads, subtitles, TV platform support, and Seerr integration Corrects typos throughout the application: - Fixes "liraries" to "libraries" in hide libraries settings - Changes "centralised" to "centralized" for consistency - Updates "Jellyseerr" references to "Seerr" throughout codebase Adds missing translations for player settings including aspect ratio options, alignment controls, and MPV subtitle customization Improves consistency in capitalization and punctuation across translation strings
211 lines
6.4 KiB
TypeScript
211 lines
6.4 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import { useLocalSearchParams } from "expo-router";
|
|
import { useCallback, useMemo, useRef } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Platform, View } from "react-native";
|
|
import { BITRATES } from "@/components/BitrateSelector";
|
|
import {
|
|
type OptionGroup,
|
|
PlatformDropdown,
|
|
} from "@/components/PlatformDropdown";
|
|
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
|
|
import useRouter from "@/hooks/useAppRouter";
|
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
|
import { useSettings } from "@/utils/atoms/settings";
|
|
import { usePlayerContext } from "../contexts/PlayerContext";
|
|
import { useVideoContext } from "../contexts/VideoContext";
|
|
import { PlaybackSpeedScope } from "../utils/playback-speed-settings";
|
|
|
|
// Subtitle size presets (stored as scale * 100, so 1.0 = 100)
|
|
const SUBTITLE_SIZE_PRESETS = [
|
|
{ label: "0.5", value: 50 },
|
|
{ label: "0.6", value: 60 },
|
|
{ label: "0.7", value: 70 },
|
|
{ label: "0.8", value: 80 },
|
|
{ label: "0.9", value: 90 },
|
|
{ label: "1.0", value: 100 },
|
|
{ label: "1.1", value: 110 },
|
|
{ label: "1.2", value: 120 },
|
|
] as const;
|
|
|
|
interface DropdownViewProps {
|
|
playbackSpeed?: number;
|
|
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
|
|
}
|
|
|
|
const DropdownView = ({
|
|
playbackSpeed = 1.0,
|
|
setPlaybackSpeed,
|
|
}: DropdownViewProps) => {
|
|
const { subtitleTracks, audioTracks } = useVideoContext();
|
|
const { item, mediaSource } = usePlayerContext();
|
|
const { settings, updateSettings } = useSettings();
|
|
const router = useRouter();
|
|
const isOffline = useOfflineMode();
|
|
const { t } = useTranslation();
|
|
|
|
const { subtitleIndex, audioIndex, bitrateValue, playbackPosition } =
|
|
useLocalSearchParams<{
|
|
itemId: string;
|
|
audioIndex: string;
|
|
subtitleIndex: string;
|
|
mediaSourceId: string;
|
|
bitrateValue: string;
|
|
playbackPosition: string;
|
|
}>();
|
|
|
|
// Use ref to track playbackPosition without causing re-renders
|
|
const playbackPositionRef = useRef(playbackPosition);
|
|
playbackPositionRef.current = playbackPosition;
|
|
|
|
// Stabilize IDs to prevent unnecessary recalculations
|
|
const itemIdRef = useRef(item.Id);
|
|
const mediaSourceIdRef = useRef(mediaSource?.Id);
|
|
itemIdRef.current = item.Id;
|
|
mediaSourceIdRef.current = mediaSource?.Id;
|
|
|
|
const changeBitrate = useCallback(
|
|
(bitrate: string) => {
|
|
const queryParams = new URLSearchParams({
|
|
itemId: itemIdRef.current ?? "",
|
|
audioIndex: audioIndex?.toString() ?? "",
|
|
subtitleIndex: subtitleIndex?.toString() ?? "",
|
|
mediaSourceId: mediaSourceIdRef.current ?? "",
|
|
bitrateValue: bitrate.toString(),
|
|
playbackPosition: playbackPositionRef.current,
|
|
}).toString();
|
|
router.replace(`player/direct-player?${queryParams}` as any);
|
|
},
|
|
[audioIndex, subtitleIndex, router],
|
|
);
|
|
|
|
// Create stable identifiers for tracks
|
|
const subtitleTracksKey = useMemo(
|
|
() => subtitleTracks?.map((t) => `${t.index}-${t.name}`).join(",") ?? "",
|
|
[subtitleTracks],
|
|
);
|
|
|
|
const audioTracksKey = useMemo(
|
|
() => audioTracks?.map((t) => `${t.index}-${t.name}`).join(",") ?? "",
|
|
[audioTracks],
|
|
);
|
|
|
|
// Transform sections into OptionGroup format
|
|
const optionGroups = useMemo<OptionGroup[]>(() => {
|
|
const groups: OptionGroup[] = [];
|
|
|
|
// Quality Section
|
|
if (!isOffline) {
|
|
groups.push({
|
|
title: "Quality",
|
|
options:
|
|
BITRATES?.map((bitrate) => ({
|
|
type: "radio" as const,
|
|
label: bitrate.key,
|
|
value: bitrate.value?.toString() ?? "",
|
|
selected: bitrateValue === (bitrate.value?.toString() ?? ""),
|
|
onPress: () => changeBitrate(bitrate.value?.toString() ?? ""),
|
|
})) || [],
|
|
});
|
|
}
|
|
|
|
// Subtitle Section
|
|
if (subtitleTracks && subtitleTracks.length > 0) {
|
|
groups.push({
|
|
title: "Subtitles",
|
|
options: subtitleTracks.map((sub) => ({
|
|
type: "radio" as const,
|
|
label: sub.name,
|
|
value: sub.index.toString(),
|
|
selected: subtitleIndex === sub.index.toString(),
|
|
onPress: () => sub.setTrack(),
|
|
})),
|
|
});
|
|
|
|
// Subtitle Size Section
|
|
groups.push({
|
|
title: "Subtitle Size",
|
|
options: SUBTITLE_SIZE_PRESETS.map((preset) => ({
|
|
type: "radio" as const,
|
|
label: preset.label,
|
|
value: preset.value.toString(),
|
|
selected: settings.subtitleSize === preset.value,
|
|
onPress: () => updateSettings({ subtitleSize: preset.value }),
|
|
})),
|
|
});
|
|
}
|
|
|
|
// Audio Section
|
|
if (audioTracks && audioTracks.length > 0) {
|
|
groups.push({
|
|
title: "Audio",
|
|
options: audioTracks.map((track) => ({
|
|
type: "radio" as const,
|
|
label: track.name,
|
|
value: track.index.toString(),
|
|
selected: audioIndex === track.index.toString(),
|
|
onPress: () => track.setTrack(),
|
|
})),
|
|
});
|
|
}
|
|
|
|
// Speed Section
|
|
if (setPlaybackSpeed) {
|
|
groups.push({
|
|
title: "Speed",
|
|
options: PLAYBACK_SPEEDS.map((speed) => ({
|
|
type: "radio" as const,
|
|
label: speed.label,
|
|
value: speed.value.toString(),
|
|
selected: playbackSpeed === speed.value,
|
|
onPress: () => setPlaybackSpeed(speed.value, PlaybackSpeedScope.All),
|
|
})),
|
|
});
|
|
}
|
|
|
|
return groups;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [
|
|
isOffline,
|
|
bitrateValue,
|
|
changeBitrate,
|
|
subtitleTracksKey,
|
|
audioTracksKey,
|
|
subtitleIndex,
|
|
audioIndex,
|
|
settings.subtitleSize,
|
|
updateSettings,
|
|
playbackSpeed,
|
|
setPlaybackSpeed,
|
|
// Note: subtitleTracks and audioTracks are intentionally excluded
|
|
// because we use subtitleTracksKey and audioTracksKey for stability
|
|
]);
|
|
|
|
// Memoize the trigger to prevent re-renders
|
|
const trigger = useMemo(
|
|
() => (
|
|
<View className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'>
|
|
<Ionicons name='ellipsis-horizontal' size={24} color={"white"} />
|
|
</View>
|
|
),
|
|
[],
|
|
);
|
|
|
|
// Hide on TV platforms
|
|
if (Platform.isTV) return null;
|
|
|
|
return (
|
|
<PlatformDropdown
|
|
title={t("player.playback_options_title")}
|
|
groups={optionGroups}
|
|
trigger={trigger}
|
|
expoUIConfig={{}}
|
|
bottomSheetConfig={{
|
|
enablePanDownToClose: true,
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default DropdownView;
|