mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-20 15:54:42 +01:00
feat: scale factor and aspect ratio (#942)
This commit is contained in:
committed by
GitHub
parent
4fed25a3ab
commit
9410239c48
@@ -59,8 +59,13 @@ import { VideoProvider } from "./contexts/VideoContext";
|
||||
import DropdownView from "./dropdown/DropdownView";
|
||||
import { EpisodeList } from "./EpisodeList";
|
||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||
import { type ScaleFactor, ScaleFactorSelector } from "./ScaleFactorSelector";
|
||||
import SkipButton from "./SkipButton";
|
||||
import { useControlsTimeout } from "./useControlsTimeout";
|
||||
import {
|
||||
type AspectRatio,
|
||||
AspectRatioSelector,
|
||||
} from "./VideoScalingModeSelector";
|
||||
import { VideoTouchOverlay } from "./VideoTouchOverlay";
|
||||
|
||||
interface Props {
|
||||
@@ -72,8 +77,7 @@ interface Props {
|
||||
progress: SharedValue<number>;
|
||||
isBuffering: boolean;
|
||||
showControls: boolean;
|
||||
ignoreSafeAreas?: boolean;
|
||||
setIgnoreSafeAreas: Dispatch<SetStateAction<boolean>>;
|
||||
|
||||
enableTrickplay?: boolean;
|
||||
togglePlay: () => void;
|
||||
setShowControls: (shown: boolean) => void;
|
||||
@@ -89,6 +93,12 @@ interface Props {
|
||||
setSubtitleURL?: (url: string, customName: string) => void;
|
||||
setSubtitleTrack?: (index: number) => void;
|
||||
setAudioTrack?: (index: number) => void;
|
||||
setVideoAspectRatio?: (aspectRatio: string | null) => Promise<void>;
|
||||
setVideoScaleFactor?: (scaleFactor: number) => Promise<void>;
|
||||
aspectRatio?: AspectRatio;
|
||||
scaleFactor?: ScaleFactor;
|
||||
setAspectRatio?: Dispatch<SetStateAction<AspectRatio>>;
|
||||
setScaleFactor?: Dispatch<SetStateAction<ScaleFactor>>;
|
||||
isVlc?: boolean;
|
||||
}
|
||||
|
||||
@@ -108,8 +118,6 @@ export const Controls: FC<Props> = ({
|
||||
cacheProgress,
|
||||
showControls,
|
||||
setShowControls,
|
||||
ignoreSafeAreas,
|
||||
setIgnoreSafeAreas,
|
||||
mediaSource,
|
||||
isVideoLoaded,
|
||||
getAudioTracks,
|
||||
@@ -117,6 +125,12 @@ export const Controls: FC<Props> = ({
|
||||
setSubtitleURL,
|
||||
setSubtitleTrack,
|
||||
setAudioTrack,
|
||||
setVideoAspectRatio,
|
||||
setVideoScaleFactor,
|
||||
aspectRatio = "default",
|
||||
scaleFactor = 1.0,
|
||||
setAspectRatio,
|
||||
setScaleFactor,
|
||||
offline = false,
|
||||
isVlc = false,
|
||||
}) => {
|
||||
@@ -631,10 +645,26 @@ export const Controls: FC<Props> = ({
|
||||
}
|
||||
}, [settings, isPlaying, isVlc, play, seek]);
|
||||
|
||||
const toggleIgnoreSafeAreas = useCallback(() => {
|
||||
setIgnoreSafeAreas((prev) => !prev);
|
||||
lightHapticFeedback();
|
||||
}, []);
|
||||
const handleAspectRatioChange = useCallback(
|
||||
async (newRatio: AspectRatio) => {
|
||||
if (!setAspectRatio || !setVideoAspectRatio) return;
|
||||
|
||||
setAspectRatio(newRatio);
|
||||
const aspectRatioString = newRatio === "default" ? null : newRatio;
|
||||
await setVideoAspectRatio(aspectRatioString);
|
||||
},
|
||||
[setAspectRatio, setVideoAspectRatio],
|
||||
);
|
||||
|
||||
const handleScaleFactorChange = useCallback(
|
||||
async (newScale: ScaleFactor) => {
|
||||
if (!setScaleFactor || !setVideoScaleFactor) return;
|
||||
|
||||
setScaleFactor(newScale);
|
||||
await setVideoScaleFactor(newScale);
|
||||
},
|
||||
[setScaleFactor, setVideoScaleFactor],
|
||||
);
|
||||
|
||||
const switchOnEpisodeMode = useCallback(() => {
|
||||
setEpisodeView(true);
|
||||
@@ -801,17 +831,17 @@ export const Controls: FC<Props> = ({
|
||||
<Ionicons name='play-skip-forward' size={24} color='white' />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{/* {mediaSource?.TranscodingUrl && ( */}
|
||||
<TouchableOpacity
|
||||
onPress={toggleIgnoreSafeAreas}
|
||||
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
||||
>
|
||||
<Ionicons
|
||||
name={ignoreSafeAreas ? "contract-outline" : "expand"}
|
||||
size={24}
|
||||
color='white'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{/* Video Controls */}
|
||||
<AspectRatioSelector
|
||||
currentRatio={aspectRatio}
|
||||
onRatioChange={handleAspectRatioChange}
|
||||
disabled={!setVideoAspectRatio}
|
||||
/>
|
||||
<ScaleFactorSelector
|
||||
currentScale={scaleFactor}
|
||||
onScaleChange={handleScaleFactorChange}
|
||||
disabled={!setVideoScaleFactor}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
||||
|
||||
135
components/video-player/controls/ScaleFactorSelector.tsx
Normal file
135
components/video-player/controls/ScaleFactorSelector.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React from "react";
|
||||
import { Platform, TouchableOpacity } from "react-native";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
|
||||
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||
|
||||
export type ScaleFactor =
|
||||
| 1.0
|
||||
| 1.1
|
||||
| 1.2
|
||||
| 1.3
|
||||
| 1.4
|
||||
| 1.5
|
||||
| 1.6
|
||||
| 1.7
|
||||
| 1.8
|
||||
| 1.9
|
||||
| 2.0;
|
||||
|
||||
interface ScaleFactorSelectorProps {
|
||||
currentScale: ScaleFactor;
|
||||
onScaleChange: (scale: ScaleFactor) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface ScaleFactorOption {
|
||||
id: ScaleFactor;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const SCALE_FACTOR_OPTIONS: ScaleFactorOption[] = [
|
||||
{
|
||||
id: 1.0,
|
||||
label: "1.0x",
|
||||
description: "Original size",
|
||||
},
|
||||
{
|
||||
id: 1.1,
|
||||
label: "1.1x",
|
||||
description: "10% larger",
|
||||
},
|
||||
{
|
||||
id: 1.2,
|
||||
label: "1.2x",
|
||||
description: "20% larger",
|
||||
},
|
||||
{
|
||||
id: 1.3,
|
||||
label: "1.3x",
|
||||
description: "30% larger",
|
||||
},
|
||||
{
|
||||
id: 1.4,
|
||||
label: "1.4x",
|
||||
description: "40% larger",
|
||||
},
|
||||
{
|
||||
id: 1.5,
|
||||
label: "1.5x",
|
||||
description: "50% larger",
|
||||
},
|
||||
{
|
||||
id: 1.6,
|
||||
label: "1.6x",
|
||||
description: "60% larger",
|
||||
},
|
||||
{
|
||||
id: 1.7,
|
||||
label: "1.7x",
|
||||
description: "70% larger",
|
||||
},
|
||||
{
|
||||
id: 1.8,
|
||||
label: "1.8x",
|
||||
description: "80% larger",
|
||||
},
|
||||
{
|
||||
id: 1.9,
|
||||
label: "1.9x",
|
||||
description: "90% larger",
|
||||
},
|
||||
{
|
||||
id: 2.0,
|
||||
label: "2.0x",
|
||||
description: "Double size",
|
||||
},
|
||||
];
|
||||
|
||||
export const ScaleFactorSelector: React.FC<ScaleFactorSelectorProps> = ({
|
||||
currentScale,
|
||||
onScaleChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const lightHapticFeedback = useHaptic("light");
|
||||
|
||||
// Hide on TV platforms since zeego doesn't support TV
|
||||
if (Platform.isTV || !DropdownMenu) return null;
|
||||
|
||||
const handleScaleSelect = (scale: ScaleFactor) => {
|
||||
onScaleChange(scale);
|
||||
lightHapticFeedback();
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<TouchableOpacity
|
||||
disabled={disabled}
|
||||
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
||||
style={{ opacity: disabled ? 0.5 : 1 }}
|
||||
>
|
||||
<Ionicons name='search-outline' size={24} color='white' />
|
||||
</TouchableOpacity>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Label>Scale Factor</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
{SCALE_FACTOR_OPTIONS.map((option) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={option.id}
|
||||
value={currentScale === option.id ? "on" : "off"}
|
||||
onValueChange={() => handleScaleSelect(option.id)}
|
||||
>
|
||||
<DropdownMenu.ItemTitle>{option.label}</DropdownMenu.ItemTitle>
|
||||
<DropdownMenu.ItemIndicator />
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React from "react";
|
||||
import { Platform, TouchableOpacity } from "react-native";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
|
||||
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||
|
||||
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;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const ASPECT_RATIO_OPTIONS: AspectRatioOption[] = [
|
||||
{
|
||||
id: "default",
|
||||
label: "Original",
|
||||
description: "Use video's original aspect ratio",
|
||||
},
|
||||
{
|
||||
id: "16:9",
|
||||
label: "16:9",
|
||||
description: "Widescreen (most common)",
|
||||
},
|
||||
{
|
||||
id: "4:3",
|
||||
label: "4:3",
|
||||
description: "Traditional TV format",
|
||||
},
|
||||
{
|
||||
id: "1:1",
|
||||
label: "1:1",
|
||||
description: "Square format",
|
||||
},
|
||||
{
|
||||
id: "21:9",
|
||||
label: "21:9",
|
||||
description: "Ultra-wide cinematic",
|
||||
},
|
||||
];
|
||||
|
||||
export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
||||
currentRatio,
|
||||
onRatioChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const lightHapticFeedback = useHaptic("light");
|
||||
|
||||
// Hide on TV platforms since zeego doesn't support TV
|
||||
if (Platform.isTV || !DropdownMenu) return null;
|
||||
|
||||
const handleRatioSelect = (ratio: AspectRatio) => {
|
||||
onRatioChange(ratio);
|
||||
lightHapticFeedback();
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<TouchableOpacity
|
||||
disabled={disabled}
|
||||
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
||||
style={{ opacity: disabled ? 0.5 : 1 }}
|
||||
>
|
||||
<Ionicons name='crop-outline' size={24} color='white' />
|
||||
</TouchableOpacity>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Label>Aspect Ratio</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
{ASPECT_RATIO_OPTIONS.map((option) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={option.id}
|
||||
value={currentRatio === option.id ? "on" : "off"}
|
||||
onValueChange={() => handleRatioSelect(option.id)}
|
||||
>
|
||||
<DropdownMenu.ItemTitle>{option.label}</DropdownMenu.ItemTitle>
|
||||
<DropdownMenu.ItemSubtitle>
|
||||
{option.description}
|
||||
</DropdownMenu.ItemSubtitle>
|
||||
<DropdownMenu.ItemIndicator />
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user