fix: correct control buttions top right for each player
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🌐 Translation Sync / sync-translations (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled

This commit is contained in:
Fredrik Burmester
2026-01-03 18:22:38 +01:00
parent 60b0040681
commit 1d8d92175a
4 changed files with 223 additions and 28 deletions

View File

@@ -75,9 +75,12 @@ 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 [aspectRatio, setAspectRatio] = useState<
"default" | "16:9" | "4:3" | "1:1" | "21:9"
>("default");
const [scaleFactor, setScaleFactor] = useState<
0 | 0.25 | 0.5 | 0.75 | 1.0 | 1.25 | 1.5 | 2.0
>(0);
const [isZoomedToFill, setIsZoomedToFill] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
const [isMuted, setIsMuted] = useState(false);
@@ -892,6 +895,37 @@ export default function page() {
);
}, [isZoomedToFill, useVlcPlayer]);
// VLC-specific handlers for aspect ratio and scale factor
const handleSetVideoAspectRatio = useCallback(
async (newAspectRatio: string | null) => {
if (!useVlcPlayer) return;
const ratio = (newAspectRatio ?? "default") as
| "default"
| "16:9"
| "4:3"
| "1:1"
| "21:9";
setAspectRatio(ratio);
await (videoRef.current as VlcPlayerViewRef)?.setVideoAspectRatio?.(
newAspectRatio,
);
},
[useVlcPlayer],
);
const handleSetVideoScaleFactor = useCallback(
async (newScaleFactor: number) => {
if (!useVlcPlayer) return;
setScaleFactor(
newScaleFactor as 0 | 0.25 | 0.5 | 0.75 | 1.0 | 1.25 | 1.5 | 2.0,
);
await (videoRef.current as VlcPlayerViewRef)?.setVideoScaleFactor?.(
newScaleFactor,
);
},
[useVlcPlayer],
);
// Apply KSPlayer global settings before video loads (only when using KSPlayer)
useEffect(() => {
if (Platform.OS === "ios" && !useVlcPlayer) {
@@ -1058,7 +1092,11 @@ export default function page() {
seek={seek}
enableTrickplay={true}
offline={offline}
useVlcPlayer={useVlcPlayer}
aspectRatio={aspectRatio}
setVideoAspectRatio={handleSetVideoAspectRatio}
scaleFactor={scaleFactor}
setVideoScaleFactor={handleSetVideoScaleFactor}
isZoomedToFill={isZoomedToFill}
onZoomToggle={handleZoomToggle}
api={api}

View File

@@ -36,6 +36,7 @@ import { useVideoSlider } from "./hooks/useVideoSlider";
import { useVideoTime } from "./hooks/useVideoTime";
import { useControlsTimeout } from "./useControlsTimeout";
import { type AspectRatio } from "./VideoScalingModeSelector";
import { type ScaleFactor } from "./VlcZoomControl";
interface Props {
item: BaseItemDto;
@@ -54,8 +55,13 @@ interface Props {
startPictureInPicture?: () => Promise<void>;
play: () => void;
pause: () => void;
useVlcPlayer?: boolean;
// VLC-specific props
setVideoAspectRatio?: (aspectRatio: string | null) => Promise<void>;
aspectRatio?: AspectRatio;
scaleFactor?: ScaleFactor;
setVideoScaleFactor?: (scaleFactor: number) => Promise<void>;
// KSPlayer-specific props
isZoomedToFill?: boolean;
onZoomToggle?: () => void;
api?: Api | null;
@@ -77,8 +83,11 @@ export const Controls: FC<Props> = ({
showControls,
setShowControls,
mediaSource,
useVlcPlayer = false,
setVideoAspectRatio,
aspectRatio = "default",
scaleFactor = 0,
setVideoScaleFactor,
isZoomedToFill = false,
onZoomToggle,
offline = false,
@@ -468,8 +477,11 @@ export const Controls: FC<Props> = ({
goToNextItem={goToNextItem}
previousItem={previousItem}
nextItem={nextItem}
useVlcPlayer={useVlcPlayer}
aspectRatio={aspectRatio}
setVideoAspectRatio={setVideoAspectRatio}
scaleFactor={scaleFactor}
setVideoScaleFactor={setVideoScaleFactor}
isZoomedToFill={isZoomedToFill}
onZoomToggle={onZoomToggle}
/>

View File

@@ -5,12 +5,7 @@ import type {
} from "@jellyfin/sdk/lib/generated-client";
import { useRouter } from "expo-router";
import { type FC, useCallback, useState } from "react";
import {
Platform,
TouchableOpacity,
useWindowDimensions,
View,
} from "react-native";
import { Platform, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useHaptic } from "@/hooks/useHaptic";
import { useOrientation } from "@/hooks/useOrientation";
@@ -18,7 +13,11 @@ import { OrientationLock } from "@/packages/expo-screen-orientation";
import { useSettings, VideoPlayerIOS } from "@/utils/atoms/settings";
import { ICON_SIZES } from "./constants";
import DropdownView from "./dropdown/DropdownView";
import { type AspectRatio } from "./VideoScalingModeSelector";
import {
type AspectRatio,
AspectRatioSelector,
} from "./VideoScalingModeSelector";
import { type ScaleFactor, VlcZoomControl } from "./VlcZoomControl";
import { ZoomToggle } from "./ZoomToggle";
interface HeaderControlsProps {
@@ -32,8 +31,13 @@ interface HeaderControlsProps {
goToNextItem: (options: { isAutoPlay?: boolean }) => void;
previousItem?: BaseItemDto | null;
nextItem?: BaseItemDto | null;
useVlcPlayer?: boolean;
// VLC-specific props
aspectRatio?: AspectRatio;
setVideoAspectRatio?: (aspectRatio: string | null) => Promise<void>;
scaleFactor?: ScaleFactor;
setVideoScaleFactor?: (scaleFactor: number) => Promise<void>;
// KSPlayer-specific props
isZoomedToFill?: boolean;
onZoomToggle?: () => void;
}
@@ -49,15 +53,17 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
goToNextItem,
previousItem,
nextItem,
aspectRatio: _aspectRatio = "default",
setVideoAspectRatio: _setVideoAspectRatio,
useVlcPlayer = false,
aspectRatio = "default",
setVideoAspectRatio,
scaleFactor = 0,
setVideoScaleFactor,
isZoomedToFill = false,
onZoomToggle,
}) => {
const { settings } = useSettings();
const router = useRouter();
const insets = useSafeAreaInsets();
const { width: _screenWidth } = useWindowDimensions();
const lightHapticFeedback = useHaptic("light");
const { orientation, lockOrientation } = useOrientation();
const [isTogglingOrientation, setIsTogglingOrientation] = useState(false);
@@ -175,21 +181,39 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
/>
</TouchableOpacity>
)}
{/*<AspectRatioSelector
currentRatio={aspectRatio}
onRatioChange={async (newRatio) => {
if (setVideoAspectRatio) {
const aspectRatioString = newRatio === "default" ? null : newRatio;
await setVideoAspectRatio(aspectRatioString);
}
}}
disabled={!setVideoAspectRatio}
/>*/}
<ZoomToggle
isZoomedToFill={isZoomedToFill}
onToggle={onZoomToggle ?? (() => {})}
disabled={!onZoomToggle}
/>
{/* VLC-specific controls: Aspect Ratio and Scale/Zoom */}
{useVlcPlayer && (
<AspectRatioSelector
currentRatio={aspectRatio}
onRatioChange={async (newRatio) => {
if (setVideoAspectRatio) {
const aspectRatioString =
newRatio === "default" ? null : newRatio;
await setVideoAspectRatio(aspectRatioString);
}
}}
disabled={!setVideoAspectRatio}
/>
)}
{useVlcPlayer && (
<VlcZoomControl
currentScale={scaleFactor}
onScaleChange={async (newScale) => {
if (setVideoScaleFactor) {
await setVideoScaleFactor(newScale);
}
}}
disabled={!setVideoScaleFactor}
/>
)}
{/* KSPlayer-specific control: Zoom to Fill */}
{!useVlcPlayer && (
<ZoomToggle
isZoomedToFill={isZoomedToFill}
onToggle={onZoomToggle ?? (() => {})}
disabled={!onZoomToggle}
/>
)}
<TouchableOpacity
onPress={onClose}
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'

View File

@@ -0,0 +1,121 @@
import { Ionicons } from "@expo/vector-icons";
import React, { useMemo } from "react";
import { Platform, View } from "react-native";
import {
type OptionGroup,
PlatformDropdown,
} from "@/components/PlatformDropdown";
import { useHaptic } from "@/hooks/useHaptic";
import { ICON_SIZES } from "./constants";
export type ScaleFactor = 0 | 0.25 | 0.5 | 0.75 | 1.0 | 1.25 | 1.5 | 2.0;
interface VlcZoomControlProps {
currentScale: ScaleFactor;
onScaleChange: (scale: ScaleFactor) => void;
disabled?: boolean;
}
interface ScaleOption {
id: ScaleFactor;
label: string;
description: string;
}
const SCALE_OPTIONS: ScaleOption[] = [
{
id: 0,
label: "Fit",
description: "Fit video to screen",
},
{
id: 0.25,
label: "25%",
description: "Quarter size",
},
{
id: 0.5,
label: "50%",
description: "Half size",
},
{
id: 0.75,
label: "75%",
description: "Three quarters",
},
{
id: 1.0,
label: "100%",
description: "Original video size",
},
{
id: 1.25,
label: "125%",
description: "Slight zoom",
},
{
id: 1.5,
label: "150%",
description: "Medium zoom",
},
{
id: 2.0,
label: "200%",
description: "Maximum zoom",
},
];
export const VlcZoomControl: React.FC<VlcZoomControlProps> = ({
currentScale,
onScaleChange,
disabled = false,
}) => {
const lightHapticFeedback = useHaptic("light");
const handleScaleSelect = (scale: ScaleFactor) => {
onScaleChange(scale);
lightHapticFeedback();
};
const optionGroups = useMemo<OptionGroup[]>(() => {
return [
{
options: SCALE_OPTIONS.map((option) => ({
type: "radio" as const,
label: option.label,
value: option.id,
selected: option.id === currentScale,
onPress: () => handleScaleSelect(option.id),
disabled,
})),
},
];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentScale, disabled]);
const trigger = useMemo(
() => (
<View
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
style={{ opacity: disabled ? 0.5 : 1 }}
>
<Ionicons name='scan-outline' size={ICON_SIZES.HEADER} color='white' />
</View>
),
[disabled],
);
// Hide on TV platforms
if (Platform.isTV) return null;
return (
<PlatformDropdown
title='Zoom'
groups={optionGroups}
trigger={trigger}
bottomSheetConfig={{
enablePanDownToClose: true,
}}
/>
);
};