mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 14:43:25 +00:00
feat: source selection & ugly source selector
This commit is contained in:
@@ -38,6 +38,7 @@ export default function VideoPlayerWrapper() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoPlayerData {
|
export interface VideoPlayerData {
|
||||||
|
sourceId?: string;
|
||||||
item: ItemData;
|
item: ItemData;
|
||||||
stream: Stream;
|
stream: Stream;
|
||||||
media: ScrapeMedia;
|
media: ScrapeMedia;
|
||||||
@@ -75,6 +76,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ data }) => {
|
|||||||
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 setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
||||||
|
const _setSourceId = usePlayerStore((state) => state.setSourceId);
|
||||||
|
const setData = usePlayerStore((state) => state.setData);
|
||||||
const presentFullscreenPlayer = usePlayerStore(
|
const presentFullscreenPlayer = usePlayerStore(
|
||||||
(state) => state.presentFullscreenPlayer,
|
(state) => state.presentFullscreenPlayer,
|
||||||
);
|
);
|
||||||
@@ -157,7 +160,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ data }) => {
|
|||||||
if (Platform.OS === "android") {
|
if (Platform.OS === "android") {
|
||||||
await NavigationBar.setVisibilityAsync("hidden");
|
await NavigationBar.setVisibilityAsync("hidden");
|
||||||
}
|
}
|
||||||
|
setData(data.item);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const { item, stream, media } = data;
|
const { item, stream, media } = data;
|
||||||
@@ -215,6 +218,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ data }) => {
|
|||||||
dismissFullscreenPlayer,
|
dismissFullscreenPlayer,
|
||||||
presentFullscreenPlayer,
|
presentFullscreenPlayer,
|
||||||
router,
|
router,
|
||||||
|
setData,
|
||||||
setStream,
|
setStream,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@@ -21,13 +21,20 @@ interface Event {
|
|||||||
|
|
||||||
export default function LoadingScreenWrapper() {
|
export default function LoadingScreenWrapper() {
|
||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
|
const sourceId = params.sourceID?.[0];
|
||||||
const data = params.data
|
const data = params.data
|
||||||
? (JSON.parse(params.data as string) as ItemData)
|
? (JSON.parse(params.data as string) as ItemData)
|
||||||
: null;
|
: null;
|
||||||
return <LoadingScreen data={data} />;
|
return <LoadingScreen sourceId={sourceId} data={data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoadingScreen({ data }: { data: ItemData | null }) {
|
function LoadingScreen({
|
||||||
|
sourceId,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
sourceId: string | undefined;
|
||||||
|
data: ItemData | null;
|
||||||
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [eventLog, setEventLog] = useState<Event[]>([]);
|
const [eventLog, setEventLog] = useState<Event[]>([]);
|
||||||
|
|
||||||
@@ -65,6 +72,7 @@ function LoadingScreen({ data }: { data: ItemData | null }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const stream = await getVideoStream({
|
const stream = await getVideoStream({
|
||||||
|
sourceId,
|
||||||
media: scrapeMedia,
|
media: scrapeMedia,
|
||||||
onEvent: handleEvent,
|
onEvent: handleEvent,
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
@@ -92,7 +100,7 @@ function LoadingScreen({ data }: { data: ItemData | null }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void initialize();
|
void initialize();
|
||||||
}, [data, router]);
|
}, [data, router, sourceId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenLayout
|
<ScreenLayout
|
||||||
|
@@ -6,6 +6,7 @@ import { Text } from "../ui/Text";
|
|||||||
import { CaptionsSelector } from "./CaptionsSelector";
|
import { CaptionsSelector } from "./CaptionsSelector";
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
import { ProgressBar } from "./ProgressBar";
|
import { ProgressBar } from "./ProgressBar";
|
||||||
|
import { SourceSelector } from "./SourceSelector";
|
||||||
import { mapMillisecondsToTime } from "./utils";
|
import { mapMillisecondsToTime } from "./utils";
|
||||||
|
|
||||||
export const BottomControls = () => {
|
export const BottomControls = () => {
|
||||||
@@ -53,6 +54,7 @@ export const BottomControls = () => {
|
|||||||
<ProgressBar />
|
<ProgressBar />
|
||||||
</View>
|
</View>
|
||||||
<View className="flex w-full flex-row items-center justify-between">
|
<View className="flex w-full flex-row items-center justify-between">
|
||||||
|
<SourceSelector />
|
||||||
<CaptionsSelector />
|
<CaptionsSelector />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
59
apps/expo/src/components/player/SourceSelector.tsx
Normal file
59
apps/expo/src/components/player/SourceSelector.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { ScrollView, View } from "react-native";
|
||||||
|
import Modal from "react-native-modal";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
|
||||||
|
import { getBuiltinSources } from "@movie-web/provider-utils";
|
||||||
|
import colors from "@movie-web/tailwind-config/colors";
|
||||||
|
|
||||||
|
import { useBoolean } from "~/hooks/useBoolean";
|
||||||
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
|
import { Button } from "../ui/Button";
|
||||||
|
import { Text } from "../ui/Text";
|
||||||
|
|
||||||
|
export const SourceSelector = () => {
|
||||||
|
const data = usePlayerStore((state) => state.interface.data);
|
||||||
|
const { isTrue, on, off } = useBoolean();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="max-w-36 flex-1">
|
||||||
|
<Button
|
||||||
|
title="Source"
|
||||||
|
variant="outline"
|
||||||
|
onPress={on}
|
||||||
|
iconLeft={
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="video"
|
||||||
|
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 source</Text>
|
||||||
|
{getBuiltinSources().map((source) => (
|
||||||
|
<Button
|
||||||
|
key={source.id}
|
||||||
|
title={source.name}
|
||||||
|
onPress={() => {
|
||||||
|
off();
|
||||||
|
router.replace({
|
||||||
|
pathname: "/videoPlayer/loading",
|
||||||
|
params: { sourceID: source.id, data: JSON.stringify(data) },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="max-w-16"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
@@ -3,16 +3,21 @@ import * as ScreenOrientation from "expo-screen-orientation";
|
|||||||
import type { Stream } from "@movie-web/provider-utils";
|
import type { Stream } from "@movie-web/provider-utils";
|
||||||
|
|
||||||
import type { MakeSlice } from "./types";
|
import type { MakeSlice } from "./types";
|
||||||
|
import type { ItemData } from "~/components/item/item";
|
||||||
|
|
||||||
export interface InterfaceSlice {
|
export interface InterfaceSlice {
|
||||||
interface: {
|
interface: {
|
||||||
isIdle: boolean;
|
isIdle: boolean;
|
||||||
idleTimeout: NodeJS.Timeout | null;
|
idleTimeout: NodeJS.Timeout | null;
|
||||||
stream: Stream | null;
|
stream: Stream | null;
|
||||||
|
sourceId: string | null;
|
||||||
|
data: ItemData | null;
|
||||||
selectedCaption: Stream["captions"][0] | null;
|
selectedCaption: Stream["captions"][0] | null;
|
||||||
};
|
};
|
||||||
setIsIdle(state: boolean): void;
|
setIsIdle(state: boolean): void;
|
||||||
setStream(stream: Stream): void;
|
setStream(stream: Stream): void;
|
||||||
|
setSourceId(sourceId: string): void;
|
||||||
|
setData(data: ItemData): void;
|
||||||
lockOrientation: () => Promise<void>;
|
lockOrientation: () => Promise<void>;
|
||||||
unlockOrientation: () => Promise<void>;
|
unlockOrientation: () => Promise<void>;
|
||||||
presentFullscreenPlayer: () => Promise<void>;
|
presentFullscreenPlayer: () => Promise<void>;
|
||||||
@@ -24,6 +29,8 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
|||||||
isIdle: true,
|
isIdle: true,
|
||||||
idleTimeout: null,
|
idleTimeout: null,
|
||||||
stream: null,
|
stream: null,
|
||||||
|
sourceId: null,
|
||||||
|
data: null,
|
||||||
selectedCaption: null,
|
selectedCaption: null,
|
||||||
},
|
},
|
||||||
setIsIdle: (state) => {
|
setIsIdle: (state) => {
|
||||||
@@ -46,6 +53,16 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
|||||||
s.interface.stream = stream;
|
s.interface.stream = stream;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setSourceId: (sourceId: string) => {
|
||||||
|
set((s) => {
|
||||||
|
s.interface.sourceId = sourceId;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setData: (data: ItemData) => {
|
||||||
|
set((s) => {
|
||||||
|
s.interface.data = data;
|
||||||
|
});
|
||||||
|
},
|
||||||
lockOrientation: async () => {
|
lockOrientation: async () => {
|
||||||
await ScreenOrientation.lockAsync(
|
await ScreenOrientation.lockAsync(
|
||||||
ScreenOrientation.OrientationLock.LANDSCAPE,
|
ScreenOrientation.OrientationLock.LANDSCAPE,
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import type { ScrapeMedia, Stream } from "@movie-web/providers";
|
import type { ScrapeMedia, Stream } from "@movie-web/providers";
|
||||||
|
import { getBuiltinSources } from "@movie-web/providers";
|
||||||
|
|
||||||
export const name = "provider-utils";
|
export const name = "provider-utils";
|
||||||
export * from "./video";
|
export * from "./video";
|
||||||
export * from "./util";
|
export * from "./util";
|
||||||
|
|
||||||
export type { Stream, ScrapeMedia };
|
export type { Stream, ScrapeMedia };
|
||||||
|
export { getBuiltinSources };
|
||||||
|
@@ -74,6 +74,8 @@ export async function getVideoStream({
|
|||||||
let stream: Stream | null = null;
|
let stream: Stream | null = null;
|
||||||
|
|
||||||
if (sourceId) {
|
if (sourceId) {
|
||||||
|
onEvent && onEvent({ sourceIds: [sourceId] });
|
||||||
|
|
||||||
let embedOutput: EmbedOutput | undefined;
|
let embedOutput: EmbedOutput | undefined;
|
||||||
|
|
||||||
const sourceResult = await providers
|
const sourceResult = await providers
|
||||||
@@ -81,9 +83,15 @@ export async function getVideoStream({
|
|||||||
id: sourceId,
|
id: sourceId,
|
||||||
media,
|
media,
|
||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch((error: Error) => {
|
||||||
|
onEvent &&
|
||||||
|
onEvent({ id: sourceId, percentage: 0, status: "failure", error });
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
if (sourceResult) {
|
if (sourceResult) {
|
||||||
|
onEvent && onEvent({ id: sourceId, percentage: 50, status: "pending" });
|
||||||
|
|
||||||
for (const embed of sourceResult.embeds) {
|
for (const embed of sourceResult.embeds) {
|
||||||
const embedResult = await providers
|
const embedResult = await providers
|
||||||
.runEmbedScraper({
|
.runEmbedScraper({
|
||||||
@@ -94,6 +102,8 @@ export async function getVideoStream({
|
|||||||
|
|
||||||
if (embedResult) {
|
if (embedResult) {
|
||||||
embedOutput = embedResult;
|
embedOutput = embedResult;
|
||||||
|
onEvent &&
|
||||||
|
onEvent({ id: embed.embedId, percentage: 100, status: "success" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,6 +113,12 @@ export async function getVideoStream({
|
|||||||
} else if (sourceResult) {
|
} else if (sourceResult) {
|
||||||
stream = sourceResult.stream?.[0] ?? null;
|
stream = sourceResult.stream?.[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stream) {
|
||||||
|
onEvent && onEvent({ id: sourceId, percentage: 100, status: "success" });
|
||||||
|
} else {
|
||||||
|
onEvent && onEvent({ id: sourceId, percentage: 100, status: "notfound" });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
stream = await providers
|
stream = await providers
|
||||||
.runAll(options)
|
.runAll(options)
|
||||||
|
Reference in New Issue
Block a user