mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 18:13:25 +00:00
first version of a really buggy and ugly caption selector and renderer
This commit is contained in:
@@ -45,12 +45,14 @@
|
|||||||
"react-native-context-menu-view": "^1.14.1",
|
"react-native-context-menu-view": "^1.14.1",
|
||||||
"react-native-css-interop": "~0.0.22",
|
"react-native-css-interop": "~0.0.22",
|
||||||
"react-native-gesture-handler": "~2.14.1",
|
"react-native-gesture-handler": "~2.14.1",
|
||||||
|
"react-native-modal": "^13.0.1",
|
||||||
"react-native-quick-base64": "^2.0.8",
|
"react-native-quick-base64": "^2.0.8",
|
||||||
"react-native-quick-crypto": "^0.6.1",
|
"react-native-quick-crypto": "^0.6.1",
|
||||||
"react-native-reanimated": "~3.6.2",
|
"react-native-reanimated": "~3.6.2",
|
||||||
"react-native-safe-area-context": "~4.8.2",
|
"react-native-safe-area-context": "~4.8.2",
|
||||||
"react-native-screens": "~3.29.0",
|
"react-native-screens": "~3.29.0",
|
||||||
"react-native-web": "^0.19.10",
|
"react-native-web": "^0.19.10",
|
||||||
|
"subsrt-ts": "^2.1.2",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
|
@@ -22,6 +22,7 @@ import {
|
|||||||
|
|
||||||
import type { ItemData } from "~/components/item/item";
|
import type { ItemData } from "~/components/item/item";
|
||||||
import type { HeaderData } from "~/components/player/Header";
|
import type { HeaderData } from "~/components/player/Header";
|
||||||
|
import { CaptionRenderer } from "~/components/player/CaptionRenderer";
|
||||||
import { ControlsOverlay } from "~/components/player/ControlsOverlay";
|
import { ControlsOverlay } from "~/components/player/ControlsOverlay";
|
||||||
import { Text } from "~/components/ui/Text";
|
import { Text } from "~/components/ui/Text";
|
||||||
import { useBrightness } from "~/hooks/player/useBrightness";
|
import { useBrightness } from "~/hooks/player/useBrightness";
|
||||||
@@ -69,9 +70,10 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ data }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const scale = useSharedValue(1);
|
const scale = useSharedValue(1);
|
||||||
|
|
||||||
|
const isIdle = usePlayerStore((state) => state.interface.isIdle);
|
||||||
|
const setStream = usePlayerStore((state) => state.setStream);
|
||||||
const setVideoRef = usePlayerStore((state) => state.setVideoRef);
|
const setVideoRef = usePlayerStore((state) => state.setVideoRef);
|
||||||
const setStatus = usePlayerStore((state) => state.setStatus);
|
const setStatus = usePlayerStore((state) => state.setStatus);
|
||||||
const isIdle = usePlayerStore((state) => state.interface.isIdle);
|
|
||||||
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
||||||
const presentFullscreenPlayer = usePlayerStore(
|
const presentFullscreenPlayer = usePlayerStore(
|
||||||
(state) => state.presentFullscreenPlayer,
|
(state) => state.presentFullscreenPlayer,
|
||||||
@@ -160,6 +162,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ data }) => {
|
|||||||
|
|
||||||
const { item, stream, media } = data;
|
const { item, stream, media } = data;
|
||||||
|
|
||||||
|
setStream(stream);
|
||||||
|
|
||||||
setHeaderData({
|
setHeaderData({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
year: item.year,
|
year: item.year,
|
||||||
@@ -252,6 +256,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ data }) => {
|
|||||||
<Text className="font-bold">Brightness: {debouncedBrightness}</Text>
|
<Text className="font-bold">Brightness: {debouncedBrightness}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
<CaptionRenderer />
|
||||||
</View>
|
</View>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
);
|
);
|
||||||
|
@@ -3,15 +3,18 @@ import { TouchableOpacity, View } from "react-native";
|
|||||||
|
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
import { Text } from "../ui/Text";
|
import { Text } from "../ui/Text";
|
||||||
|
import { CaptionsSelector } from "./CaptionsSelector";
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
import { ProgressBar } from "./ProgressBar";
|
import { ProgressBar } from "./ProgressBar";
|
||||||
import { mapMillisecondsToTime } from "./utils";
|
import { mapMillisecondsToTime } from "./utils";
|
||||||
|
|
||||||
export const BottomControls = () => {
|
export const BottomControls = () => {
|
||||||
const status = usePlayerStore((state) => state.status);
|
const status = usePlayerStore((state) => state.status);
|
||||||
|
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
||||||
const [showRemaining, setShowRemaining] = useState(false);
|
const [showRemaining, setShowRemaining] = useState(false);
|
||||||
|
|
||||||
const toggleTimeDisplay = () => {
|
const toggleTimeDisplay = () => {
|
||||||
|
setIsIdle(false);
|
||||||
setShowRemaining(!showRemaining);
|
setShowRemaining(!showRemaining);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,9 +35,9 @@ export const BottomControls = () => {
|
|||||||
if (status?.isLoaded) {
|
if (status?.isLoaded) {
|
||||||
return (
|
return (
|
||||||
<Controls>
|
<Controls>
|
||||||
<View className="flex h-16 w-full flex-col items-center justify-center">
|
<View className="flex h-40 w-full flex-col items-center justify-center p-6">
|
||||||
<View className="w-full px-4">
|
<View className="w-full">
|
||||||
<View className="ml-10 flex flex-row items-center">
|
<View className="flex flex-row items-center">
|
||||||
<Text className="font-bold">{getCurrentTime()}</Text>
|
<Text className="font-bold">{getCurrentTime()}</Text>
|
||||||
<Text className="mx-1 font-bold">/</Text>
|
<Text className="mx-1 font-bold">/</Text>
|
||||||
<TouchableOpacity onPress={toggleTimeDisplay}>
|
<TouchableOpacity onPress={toggleTimeDisplay}>
|
||||||
@@ -46,9 +49,12 @@ export const BottomControls = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="py-2">
|
<View>
|
||||||
<ProgressBar />
|
<ProgressBar />
|
||||||
</View>
|
</View>
|
||||||
|
<View className="flex w-full flex-row items-center justify-between">
|
||||||
|
<CaptionsSelector />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Controls>
|
</Controls>
|
||||||
|
101
apps/expo/src/components/player/CaptionRenderer.tsx
Normal file
101
apps/expo/src/components/player/CaptionRenderer.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import Animated, {
|
||||||
|
useAnimatedReaction,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useDerivedValue,
|
||||||
|
useSharedValue,
|
||||||
|
withSpring,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
|
||||||
|
import { Text } from "~/components/ui/Text";
|
||||||
|
import { convertMilliSecondsToSeconds } from "~/lib/number";
|
||||||
|
import { useCaptionsStore } from "~/stores/captions";
|
||||||
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
|
|
||||||
|
export const captionIsVisible = (
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
delay: number,
|
||||||
|
currentTime: number,
|
||||||
|
) => {
|
||||||
|
const delayedStart = start / 1000 + delay;
|
||||||
|
const delayedEnd = end / 1000 + delay;
|
||||||
|
return (
|
||||||
|
Math.max(0, delayedStart) <= currentTime &&
|
||||||
|
Math.max(0, delayedEnd) >= currentTime
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CaptionRenderer = () => {
|
||||||
|
const isIdle = usePlayerStore((state) => state.interface.isIdle);
|
||||||
|
const selectedCaption = useCaptionsStore((state) => state.selectedCaption);
|
||||||
|
const delay = useCaptionsStore((state) => state.delay);
|
||||||
|
const status = usePlayerStore((state) => state.status);
|
||||||
|
|
||||||
|
const translateY = useSharedValue(0);
|
||||||
|
|
||||||
|
const animatedStyles = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
transform: [{ translateY: translateY.value }],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const transitionValue = useDerivedValue(() => {
|
||||||
|
return isIdle ? 50 : 0;
|
||||||
|
}, [isIdle]);
|
||||||
|
|
||||||
|
useAnimatedReaction(
|
||||||
|
() => {
|
||||||
|
return transitionValue.value;
|
||||||
|
},
|
||||||
|
(newValue) => {
|
||||||
|
translateY.value = withSpring(newValue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleCaptions = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedCaption?.data.filter(({ start, end }) =>
|
||||||
|
captionIsVisible(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
delay,
|
||||||
|
status?.isLoaded
|
||||||
|
? convertMilliSecondsToSeconds(status.positionMillis)
|
||||||
|
: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[selectedCaption, delay, status],
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(visibleCaptions);
|
||||||
|
|
||||||
|
if (!status?.isLoaded || !selectedCaption || !visibleCaptions?.length)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
// https://github.com/marklawlor/nativewind/issues/790
|
||||||
|
<Animated.View
|
||||||
|
// className="rounded px-4 py-1 text-center leading-normal [text-shadow:0_2px_4px_rgba(0,0,0,0.5)]"
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
paddingLeft: 16,
|
||||||
|
paddingRight: 16,
|
||||||
|
paddingTop: 4,
|
||||||
|
paddingBottom: 4,
|
||||||
|
borderRadius: 10,
|
||||||
|
marginTop: 32,
|
||||||
|
},
|
||||||
|
animatedStyles,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{visibleCaptions?.map((caption) => (
|
||||||
|
<View key={caption.index}>
|
||||||
|
<Text>{caption.text}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
};
|
84
apps/expo/src/components/player/CaptionsSelector.tsx
Normal file
84
apps/expo/src/components/player/CaptionsSelector.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import type { ContentCaption } from "subsrt-ts/dist/types/handler";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { ScrollView, View } from "react-native";
|
||||||
|
import Modal from "react-native-modal";
|
||||||
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { parse } from "subsrt-ts";
|
||||||
|
|
||||||
|
import type { Stream } from "@movie-web/provider-utils";
|
||||||
|
import colors from "@movie-web/tailwind-config/colors";
|
||||||
|
|
||||||
|
import { useBoolean } from "~/hooks/useBoolean";
|
||||||
|
import { useCaptionsStore } from "~/stores/captions";
|
||||||
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
|
import { Button } from "../ui/Button";
|
||||||
|
import { Text } from "../ui/Text";
|
||||||
|
|
||||||
|
const parseCaption = async (
|
||||||
|
caption: Stream["captions"][0],
|
||||||
|
): Promise<ContentCaption[]> => {
|
||||||
|
const response = await fetch(caption.url);
|
||||||
|
const data = await response.text();
|
||||||
|
return parse(data).filter(
|
||||||
|
(cue) => cue.type === "caption",
|
||||||
|
) as ContentCaption[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CaptionsSelector = () => {
|
||||||
|
const captions = usePlayerStore((state) => state.interface.stream?.captions);
|
||||||
|
const setSelectedCaption = useCaptionsStore(
|
||||||
|
(state) => state.setSelectedCaption,
|
||||||
|
);
|
||||||
|
const { isTrue, on, off } = useBoolean();
|
||||||
|
|
||||||
|
const downloadAndSetCaption = useCallback(
|
||||||
|
(caption: Stream["captions"][0]) => {
|
||||||
|
parseCaption(caption)
|
||||||
|
.then((data) => {
|
||||||
|
setSelectedCaption({ ...caption, data });
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
},
|
||||||
|
[setSelectedCaption],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!captions?.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="max-w-36 flex-1">
|
||||||
|
<Button
|
||||||
|
title="Subtitles"
|
||||||
|
variant="outline"
|
||||||
|
onPress={on}
|
||||||
|
iconLeft={
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="subtitles"
|
||||||
|
size={24}
|
||||||
|
color={colors.primary[300]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isVisible={isTrue}
|
||||||
|
onBackdropPress={off}
|
||||||
|
supportedOrientations={["portrait", "landscape"]}
|
||||||
|
>
|
||||||
|
<ScrollView className="flex-1 bg-gray-900">
|
||||||
|
<Text className="text-center font-bold">Select subtitle</Text>
|
||||||
|
{captions?.map((caption) => (
|
||||||
|
<Button
|
||||||
|
key={caption.id}
|
||||||
|
title={caption.language}
|
||||||
|
onPress={() => {
|
||||||
|
downloadAndSetCaption(caption);
|
||||||
|
off();
|
||||||
|
}}
|
||||||
|
className="max-w-16"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
@@ -11,7 +11,7 @@ interface ControlsOverlayProps {
|
|||||||
|
|
||||||
export const ControlsOverlay = ({ headerData }: ControlsOverlayProps) => {
|
export const ControlsOverlay = ({ headerData }: ControlsOverlayProps) => {
|
||||||
return (
|
return (
|
||||||
<View className="absolute left-0 top-0 flex h-full w-full flex-1">
|
<View className="absolute left-0 top-0 flex h-full w-full flex-1 flex-col justify-between">
|
||||||
<Header data={headerData} />
|
<Header data={headerData} />
|
||||||
<MiddleControls />
|
<MiddleControls />
|
||||||
<BottomControls />
|
<BottomControls />
|
||||||
|
@@ -22,7 +22,7 @@ export const Header = ({ data }: HeaderProps) => {
|
|||||||
|
|
||||||
if (!isIdle) {
|
if (!isIdle) {
|
||||||
return (
|
return (
|
||||||
<View className="flex h-16 w-full flex-row items-center justify-between px-6 pt-6">
|
<View className="flex h-16 w-full flex-row justify-between px-6 pt-6">
|
||||||
<Controls>
|
<Controls>
|
||||||
<BackButton className="w-36" />
|
<BackButton className="w-36" />
|
||||||
</Controls>
|
</Controls>
|
||||||
@@ -31,7 +31,7 @@ export const Header = ({ data }: HeaderProps) => {
|
|||||||
? `${data.title} (${data.year}) S${data.season.toString().padStart(2, "0")}E${data.episode.toString().padStart(2, "0")}`
|
? `${data.title} (${data.year}) S${data.season.toString().padStart(2, "0")}E${data.episode.toString().padStart(2, "0")}`
|
||||||
: `${data.title} (${data.year})`}
|
: `${data.title} (${data.year})`}
|
||||||
</Text>
|
</Text>
|
||||||
<View className="flex w-36 flex-row items-center justify-center gap-2 space-x-2 rounded-full bg-secondary-300 px-4 py-2 opacity-80">
|
<View className="flex h-12 w-36 flex-row items-center justify-center gap-2 space-x-2 rounded-full bg-secondary-300 px-4 py-2 opacity-80">
|
||||||
<Image source={Icon} className="h-6 w-6" />
|
<Image source={Icon} className="h-6 w-6" />
|
||||||
<Text className="font-bold">movie-web</Text>
|
<Text className="font-bold">movie-web</Text>
|
||||||
</View>
|
</View>
|
||||||
|
@@ -32,6 +32,9 @@ export const MiddleControls = () => {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
position: "absolute",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
@@ -21,7 +21,7 @@ export const ProgressBar = () => {
|
|||||||
if (status?.isLoaded) {
|
if (status?.isLoaded) {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
className="flex h-10 flex-1 items-center justify-center p-8"
|
className="flex flex-1 items-center justify-center pb-12 pt-6"
|
||||||
onPress={() => setIsIdle(false)}
|
onPress={() => setIsIdle(false)}
|
||||||
>
|
>
|
||||||
<VideoSlider onSlidingComplete={updateProgress} />
|
<VideoSlider onSlidingComplete={updateProgress} />
|
||||||
|
@@ -36,7 +36,7 @@ const VideoSlider = ({ onSlidingComplete }: VideoSliderProps) => {
|
|||||||
const status = usePlayerStore((state) => state.status);
|
const status = usePlayerStore((state) => state.status);
|
||||||
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
||||||
|
|
||||||
const width = Dimensions.get("screen").width - 100;
|
const width = Dimensions.get("screen").width - 40;
|
||||||
const knobSize_ = 20;
|
const knobSize_ = 20;
|
||||||
const trackSize_ = 8;
|
const trackSize_ = 8;
|
||||||
const minimumValue = 0;
|
const minimumValue = 0;
|
||||||
|
60
apps/expo/src/components/ui/Button.tsx
Normal file
60
apps/expo/src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { VariantProps } from "class-variance-authority";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { PressableProps } from "react-native";
|
||||||
|
import { Pressable } from "react-native";
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { Text } from "./Text";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"flex flex-row items-center justify-center gap-4 rounded-md disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary-300",
|
||||||
|
outline: "border border-primary-400 bg-transparent",
|
||||||
|
secondary: "bg-secondary-300",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends PressableProps,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
iconLeft?: ReactNode;
|
||||||
|
iconRight?: ReactNode;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
onPress,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
className,
|
||||||
|
iconLeft,
|
||||||
|
iconRight,
|
||||||
|
title,
|
||||||
|
}: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={onPress}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
>
|
||||||
|
{iconLeft}
|
||||||
|
<Text className="font-bold">{title}</Text>
|
||||||
|
{iconRight}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
19
apps/expo/src/hooks/useBoolean.ts
Normal file
19
apps/expo/src/hooks/useBoolean.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
type InitialState = boolean | (() => boolean);
|
||||||
|
|
||||||
|
export const useBoolean = (initialState: InitialState = false) => {
|
||||||
|
const [value, setValue] = useState(initialState);
|
||||||
|
const callbacks = useMemo(
|
||||||
|
() => ({
|
||||||
|
on: () => setValue(true),
|
||||||
|
off: () => setValue(false),
|
||||||
|
toggle: () => setValue((prev) => !prev),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isTrue: value,
|
||||||
|
...callbacks,
|
||||||
|
};
|
||||||
|
};
|
3
apps/expo/src/lib/number.ts
Normal file
3
apps/expo/src/lib/number.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const convertMilliSecondsToSeconds = (milliSeconds: number) => {
|
||||||
|
return milliSeconds / 1000;
|
||||||
|
};
|
33
apps/expo/src/stores/captions/index.ts
Normal file
33
apps/expo/src/stores/captions/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { ContentCaption } from "subsrt-ts/dist/types/handler";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
|
import type { Stream } from "@movie-web/provider-utils";
|
||||||
|
|
||||||
|
type CaptionWithData = Stream["captions"][0] & {
|
||||||
|
data: ContentCaption[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CaptionsStore {
|
||||||
|
selectedCaption: CaptionWithData | null;
|
||||||
|
delay: number;
|
||||||
|
setSelectedCaption(caption: CaptionWithData | null): void;
|
||||||
|
setDelay(delay: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCaptionsStore = create(
|
||||||
|
immer<CaptionsStore>((set) => ({
|
||||||
|
selectedCaption: null,
|
||||||
|
delay: 0,
|
||||||
|
setSelectedCaption: (caption) => {
|
||||||
|
set((s) => {
|
||||||
|
s.selectedCaption = caption;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setDelay: (delay) => {
|
||||||
|
set((s) => {
|
||||||
|
s.delay = delay;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
@@ -1,13 +1,18 @@
|
|||||||
import * as ScreenOrientation from "expo-screen-orientation";
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
|
|
||||||
|
import type { Stream } from "@movie-web/provider-utils";
|
||||||
|
|
||||||
import type { MakeSlice } from "./types";
|
import type { MakeSlice } from "./types";
|
||||||
|
|
||||||
export interface InterfaceSlice {
|
export interface InterfaceSlice {
|
||||||
interface: {
|
interface: {
|
||||||
isIdle: boolean;
|
isIdle: boolean;
|
||||||
idleTimeout: NodeJS.Timeout | null;
|
idleTimeout: NodeJS.Timeout | null;
|
||||||
|
stream: Stream | null;
|
||||||
|
selectedCaption: Stream["captions"][0] | null;
|
||||||
};
|
};
|
||||||
setIsIdle(state: boolean): void;
|
setIsIdle(state: boolean): void;
|
||||||
|
setStream(stream: Stream): void;
|
||||||
lockOrientation: () => Promise<void>;
|
lockOrientation: () => Promise<void>;
|
||||||
unlockOrientation: () => Promise<void>;
|
unlockOrientation: () => Promise<void>;
|
||||||
presentFullscreenPlayer: () => Promise<void>;
|
presentFullscreenPlayer: () => Promise<void>;
|
||||||
@@ -18,6 +23,8 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
|||||||
interface: {
|
interface: {
|
||||||
isIdle: true,
|
isIdle: true,
|
||||||
idleTimeout: null,
|
idleTimeout: null,
|
||||||
|
stream: null,
|
||||||
|
selectedCaption: null,
|
||||||
},
|
},
|
||||||
setIsIdle: (state) => {
|
setIsIdle: (state) => {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
@@ -34,6 +41,11 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
|||||||
s.interface.isIdle = state;
|
s.interface.isIdle = state;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setStream: (stream) => {
|
||||||
|
set((s) => {
|
||||||
|
s.interface.stream = stream;
|
||||||
|
});
|
||||||
|
},
|
||||||
lockOrientation: async () => {
|
lockOrientation: async () => {
|
||||||
await ScreenOrientation.lockAsync(
|
await ScreenOrientation.lockAsync(
|
||||||
ScreenOrientation.OrientationLock.LANDSCAPE,
|
ScreenOrientation.OrientationLock.LANDSCAPE,
|
||||||
|
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -112,6 +112,9 @@ importers:
|
|||||||
react-native-gesture-handler:
|
react-native-gesture-handler:
|
||||||
specifier: ~2.14.1
|
specifier: ~2.14.1
|
||||||
version: 2.14.1(react-native@0.73.2)(react@18.2.0)
|
version: 2.14.1(react-native@0.73.2)(react@18.2.0)
|
||||||
|
react-native-modal:
|
||||||
|
specifier: ^13.0.1
|
||||||
|
version: 13.0.1(react-native@0.73.2)(react@18.2.0)
|
||||||
react-native-quick-base64:
|
react-native-quick-base64:
|
||||||
specifier: ^2.0.8
|
specifier: ^2.0.8
|
||||||
version: 2.0.8(react-native@0.73.2)(react@18.2.0)
|
version: 2.0.8(react-native@0.73.2)(react@18.2.0)
|
||||||
@@ -130,6 +133,9 @@ importers:
|
|||||||
react-native-web:
|
react-native-web:
|
||||||
specifier: ^0.19.10
|
specifier: ^0.19.10
|
||||||
version: 0.19.10(react-dom@18.2.0)(react@18.2.0)
|
version: 0.19.10(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
subsrt-ts:
|
||||||
|
specifier: ^2.1.2
|
||||||
|
version: 2.1.2
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
@@ -8952,6 +8958,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-native-animatable@1.3.3:
|
||||||
|
resolution: {integrity: sha512-2ckIxZQAsvWn25Ho+DK3d1mXIgj7tITkrS4pYDvx96WyOttSvzzFeQnM2od0+FUMzILbdHDsDEqZvnz1DYNQ1w==}
|
||||||
|
dependencies:
|
||||||
|
prop-types: 15.8.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-native-context-menu-view@1.14.1(react-native@0.73.2)(react@18.2.0):
|
/react-native-context-menu-view@1.14.1(react-native@0.73.2)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-rPtC6RCbEVismTQ6M7WSt1HisNvgbS9bWqWX4RQXNXHKOKsVvXpI+bWRypFAjeBN/P+winn6Dxn1+meLBMrjmQ==}
|
resolution: {integrity: sha512-rPtC6RCbEVismTQ6M7WSt1HisNvgbS9bWqWX4RQXNXHKOKsVvXpI+bWRypFAjeBN/P+winn6Dxn1+meLBMrjmQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -9008,6 +9020,18 @@ packages:
|
|||||||
react-native: 0.73.2(@babel/core@7.23.9)(@babel/preset-env@7.23.9)(react@18.2.0)
|
react-native: 0.73.2(@babel/core@7.23.9)(@babel/preset-env@7.23.9)(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-native-modal@13.0.1(react-native@0.73.2)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-UB+mjmUtf+miaG/sDhOikRfBOv0gJdBU2ZE1HtFWp6UixW9jCk/bhGdHUgmZljbPpp0RaO/6YiMmQSSK3kkMaw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '*'
|
||||||
|
react-native: '>=0.65.0'
|
||||||
|
dependencies:
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-native: 0.73.2(@babel/core@7.23.9)(@babel/preset-env@7.23.9)(react@18.2.0)
|
||||||
|
react-native-animatable: 1.3.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-native-quick-base64@2.0.8(react-native@0.73.2)(react@18.2.0):
|
/react-native-quick-base64@2.0.8(react-native@0.73.2)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-2kMlnLSy0qz4NA0KXMGugd3qNB5EAizxZ6ghEVNGIxAOlc9CGvC8miv35wgpFbSKeiaBRfcPfkdTM/5Erb/6SQ==}
|
resolution: {integrity: sha512-2kMlnLSy0qz4NA0KXMGugd3qNB5EAizxZ6ghEVNGIxAOlc9CGvC8miv35wgpFbSKeiaBRfcPfkdTM/5Erb/6SQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -9985,6 +10009,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==}
|
resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/subsrt-ts@2.1.2:
|
||||||
|
resolution: {integrity: sha512-45yNlK42Z0pz4lAaNYbR5P60M2jmHl+gfAaiJxDIXsXXqoE7TkDCzl/00HgWyZXKkdIU6s8FiNtRvrlOZb+5Qg==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/sucrase@3.34.0:
|
/sucrase@3.34.0:
|
||||||
resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==}
|
resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@@ -10,5 +10,8 @@ export default {
|
|||||||
300: "#32324F",
|
300: "#32324F",
|
||||||
700: "#131322",
|
700: "#131322",
|
||||||
},
|
},
|
||||||
|
playerSettings: {
|
||||||
|
captionBackground: "#161b23",
|
||||||
|
},
|
||||||
background: "#0a0a12",
|
background: "#0a0a12",
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user