mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-04 04:58:30 +01:00
wip
This commit is contained in:
@@ -36,6 +36,8 @@ import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
|
|||||||
import { MediaSourceSelector } from "./MediaSourceSelector";
|
import { MediaSourceSelector } from "./MediaSourceSelector";
|
||||||
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
||||||
import { AddToFavorites } from "./AddToFavorites";
|
import { AddToFavorites } from "./AddToFavorites";
|
||||||
|
import { NativeDownloadButton } from "./NativeDownloadButton";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
|
||||||
export type SelectedOptions = {
|
export type SelectedOptions = {
|
||||||
bitrate: Bitrate;
|
bitrate: Bitrate;
|
||||||
@@ -95,7 +97,13 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
/>
|
/>
|
||||||
{item.Type !== "Program" && (
|
{item.Type !== "Program" && (
|
||||||
<View className="flex flex-row items-center space-x-2">
|
<View className="flex flex-row items-center space-x-2">
|
||||||
<DownloadSingleItem item={item} size="large" />
|
{/* <DownloadSingleItem item={item} size="large" /> */}
|
||||||
|
<NativeDownloadButton
|
||||||
|
size={"large"}
|
||||||
|
title={"Download"}
|
||||||
|
subtitle={item.Name!}
|
||||||
|
item={item}
|
||||||
|
/>
|
||||||
<PlayedStatus items={[item]} size="large" />
|
<PlayedStatus items={[item]} size="large" />
|
||||||
<AddToFavorites item={item} type="item" />
|
<AddToFavorites item={item} type="item" />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
354
components/NativeDownloadButton.tsx
Normal file
354
components/NativeDownloadButton.tsx
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||||
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
|
import download from "@/utils/profiles/download";
|
||||||
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
|
import {
|
||||||
|
BottomSheetBackdrop,
|
||||||
|
BottomSheetBackdropProps,
|
||||||
|
BottomSheetModal,
|
||||||
|
BottomSheetView,
|
||||||
|
} from "@gorhom/bottom-sheet";
|
||||||
|
import {
|
||||||
|
BaseItemDto,
|
||||||
|
MediaSourceInfo,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import RNBackgroundDownloader, {
|
||||||
|
DownloadTaskState,
|
||||||
|
} from "@kesha-antonov/react-native-background-downloader";
|
||||||
|
import { useFocusEffect } from "expo-router";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { View, ViewProps } from "react-native";
|
||||||
|
import { toast } from "sonner-native";
|
||||||
|
import { AudioTrackSelector } from "./AudioTrackSelector";
|
||||||
|
import { Bitrate, BitrateSelector } from "./BitrateSelector";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { Text } from "./common/Text";
|
||||||
|
import { MediaSourceSelector } from "./MediaSourceSelector";
|
||||||
|
import { RoundButton } from "./RoundButton";
|
||||||
|
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
|
||||||
|
|
||||||
|
import * as FileSystem from "expo-file-system";
|
||||||
|
import ProgressCircle from "./ProgressCircle";
|
||||||
|
|
||||||
|
import {
|
||||||
|
downloadHLSAsset,
|
||||||
|
useDownloadProgress,
|
||||||
|
useDownloadError,
|
||||||
|
useDownloadComplete,
|
||||||
|
} from "@/modules/expo-hls-downloader";
|
||||||
|
|
||||||
|
interface NativeDownloadButton extends ViewProps {
|
||||||
|
item: BaseItemDto;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
size?: "default" | "large";
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadState = {
|
||||||
|
id: string;
|
||||||
|
bytesDownloaded: number;
|
||||||
|
bytesTotal: number;
|
||||||
|
state: DownloadTaskState;
|
||||||
|
metadata?: {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NativeDownloadButton: React.FC<NativeDownloadButton> = ({
|
||||||
|
item,
|
||||||
|
title = "Download",
|
||||||
|
subtitle = "",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [api] = useAtom(apiAtom);
|
||||||
|
const [user] = useAtom(userAtom);
|
||||||
|
const [settings] = useSettings();
|
||||||
|
|
||||||
|
const [activeDownload, setActiveDownload] = useState<
|
||||||
|
DownloadState | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const [selectedMediaSource, setSelectedMediaSource] = useState<
|
||||||
|
MediaSourceInfo | undefined | null
|
||||||
|
>(undefined);
|
||||||
|
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
|
||||||
|
const [selectedSubtitleStream, setSelectedSubtitleStream] =
|
||||||
|
useState<number>(0);
|
||||||
|
const [maxBitrate, setMaxBitrate] = useState<Bitrate>(
|
||||||
|
settings?.defaultBitrate ?? {
|
||||||
|
key: "Max",
|
||||||
|
value: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const userCanDownload = useMemo(
|
||||||
|
() => user?.Policy?.EnableContentDownloading,
|
||||||
|
[user]
|
||||||
|
);
|
||||||
|
const usingOptimizedServer = useMemo(
|
||||||
|
() => settings?.downloadMethod === DownloadMethod.Optimized,
|
||||||
|
[settings]
|
||||||
|
);
|
||||||
|
|
||||||
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||||
|
|
||||||
|
const handlePresentModalPress = useCallback(() => {
|
||||||
|
bottomSheetModalRef.current?.present();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSheetChanges = useCallback((index: number) => {}, []);
|
||||||
|
|
||||||
|
const closeModal = useCallback(() => {
|
||||||
|
bottomSheetModalRef.current?.dismiss();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const progress = useDownloadProgress();
|
||||||
|
const complete = useDownloadComplete("download");
|
||||||
|
const downloadError = useDownloadError();
|
||||||
|
|
||||||
|
const acceptDownloadOptions = useCallback(async () => {
|
||||||
|
if (userCanDownload === true) {
|
||||||
|
closeModal();
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
selectedAudioStream,
|
||||||
|
selectedMediaSource,
|
||||||
|
selectedSubtitleStream,
|
||||||
|
maxBitrate,
|
||||||
|
item,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await getStreamUrl({
|
||||||
|
api,
|
||||||
|
item,
|
||||||
|
startTimeTicks: 0,
|
||||||
|
userId: user?.Id,
|
||||||
|
audioStreamIndex: selectedAudioStream,
|
||||||
|
maxStreamingBitrate: maxBitrate.value,
|
||||||
|
mediaSourceId: selectedMediaSource?.Id,
|
||||||
|
subtitleStreamIndex: selectedSubtitleStream,
|
||||||
|
deviceProfile: download,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("acceptDownloadOptions ~", res);
|
||||||
|
|
||||||
|
if (!res?.url) throw new Error("No url found");
|
||||||
|
|
||||||
|
if (res.url.includes("master.m3u8")) {
|
||||||
|
// TODO: Download with custom native module
|
||||||
|
downloadHLSAsset(
|
||||||
|
res.url,
|
||||||
|
`${FileSystem.documentDirectory}${item.Name}.mkv`
|
||||||
|
);
|
||||||
|
console.log("TODO: Download with custom native module");
|
||||||
|
} else {
|
||||||
|
// Download with reac-native-background-downloader
|
||||||
|
const destination = `${FileSystem.documentDirectory}${item.Name}.mkv`;
|
||||||
|
const jobId = item.Id!;
|
||||||
|
|
||||||
|
try {
|
||||||
|
RNBackgroundDownloader.download({
|
||||||
|
id: jobId,
|
||||||
|
url: res.url,
|
||||||
|
destination,
|
||||||
|
})
|
||||||
|
.begin(({ expectedBytes, headers }) => {
|
||||||
|
console.log(`Starting download of ${expectedBytes} bytes`);
|
||||||
|
toast.success("Download started");
|
||||||
|
setActiveDownload({
|
||||||
|
id: jobId,
|
||||||
|
bytesDownloaded: 0,
|
||||||
|
bytesTotal: expectedBytes,
|
||||||
|
state: "DOWNLOADING",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.progress(({ bytesDownloaded, bytesTotal }) =>
|
||||||
|
console.log(`Downloaded: ${bytesDownloaded} of ${bytesTotal}`)
|
||||||
|
)
|
||||||
|
.done(({ bytesDownloaded, bytesTotal }) => {
|
||||||
|
console.log("Download completed:", bytesDownloaded, bytesTotal);
|
||||||
|
|
||||||
|
RNBackgroundDownloader.completeHandler(jobId);
|
||||||
|
})
|
||||||
|
.error(({ error, errorCode }) =>
|
||||||
|
console.error("Download error:", error)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("error ~", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
t("home.downloads.toasts.you_are_not_allowed_to_download_files")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
}, [
|
||||||
|
userCanDownload,
|
||||||
|
maxBitrate,
|
||||||
|
selectedMediaSource,
|
||||||
|
selectedAudioStream,
|
||||||
|
selectedSubtitleStream,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(progress);
|
||||||
|
}, [progress]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
RNBackgroundDownloader.checkForExistingDownloads().then((downloads) => {
|
||||||
|
console.log("checkForExistingDownloads ~", downloads);
|
||||||
|
const e = downloads?.[0];
|
||||||
|
setActiveDownload({
|
||||||
|
id: e?.id,
|
||||||
|
bytesDownloaded: e?.bytesDownloaded,
|
||||||
|
bytesTotal: e?.bytesTotal,
|
||||||
|
state: e?.state,
|
||||||
|
});
|
||||||
|
|
||||||
|
e.progress(({ bytesDownloaded, bytesTotal }) => {
|
||||||
|
console.log(`Downloaded: ${bytesDownloaded} of ${bytesTotal}`);
|
||||||
|
setActiveDownload({
|
||||||
|
id: e?.id,
|
||||||
|
bytesDownloaded,
|
||||||
|
bytesTotal,
|
||||||
|
state: e?.state,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
e.done(({ bytesDownloaded, bytesTotal }) => {
|
||||||
|
console.log("Download completed:", bytesDownloaded, bytesTotal);
|
||||||
|
setActiveDownload(undefined);
|
||||||
|
});
|
||||||
|
e.error(({ error, errorCode }) => {
|
||||||
|
console.error("Download error:", error);
|
||||||
|
setActiveDownload(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
if (!settings) return;
|
||||||
|
const { bitrate, mediaSource, audioIndex, subtitleIndex } =
|
||||||
|
getDefaultPlaySettings(item, settings);
|
||||||
|
|
||||||
|
setSelectedMediaSource(mediaSource ?? undefined);
|
||||||
|
setSelectedAudioStream(audioIndex ?? 0);
|
||||||
|
setSelectedSubtitleStream(subtitleIndex ?? -1);
|
||||||
|
setMaxBitrate(bitrate);
|
||||||
|
}, [item, settings])
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderBackdrop = useCallback(
|
||||||
|
(props: BottomSheetBackdropProps) => (
|
||||||
|
<BottomSheetBackdrop
|
||||||
|
{...props}
|
||||||
|
disappearsOnIndex={-1}
|
||||||
|
appearsOnIndex={0}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onButtonPress = () => {
|
||||||
|
handlePresentModalPress();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View {...props}>
|
||||||
|
<RoundButton
|
||||||
|
disabled={userCanDownload === false || activeDownload?.id !== undefined}
|
||||||
|
size={size}
|
||||||
|
onPress={onButtonPress}
|
||||||
|
>
|
||||||
|
{activeDownload &&
|
||||||
|
activeDownload?.bytesTotal > 0 &&
|
||||||
|
activeDownload?.bytesDownloaded > 0 ? (
|
||||||
|
<ProgressCircle
|
||||||
|
size={24}
|
||||||
|
fill={
|
||||||
|
(activeDownload.bytesDownloaded / activeDownload.bytesTotal) * 100
|
||||||
|
}
|
||||||
|
width={4}
|
||||||
|
tintColor="#9334E9"
|
||||||
|
backgroundColor="#bdc3c7"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Ionicons name="cloud-download-outline" size={24} color="white" />
|
||||||
|
)}
|
||||||
|
</RoundButton>
|
||||||
|
<BottomSheetModal
|
||||||
|
ref={bottomSheetModalRef}
|
||||||
|
enableDynamicSizing
|
||||||
|
handleIndicatorStyle={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
}}
|
||||||
|
backgroundStyle={{
|
||||||
|
backgroundColor: "#171717",
|
||||||
|
}}
|
||||||
|
onChange={handleSheetChanges}
|
||||||
|
backdropComponent={renderBackdrop}
|
||||||
|
>
|
||||||
|
<BottomSheetView>
|
||||||
|
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
||||||
|
<View>
|
||||||
|
<Text className="font-bold text-2xl text-neutral-100">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View className="flex flex-col space-y-2 w-full items-start">
|
||||||
|
<BitrateSelector
|
||||||
|
inverted
|
||||||
|
onChange={setMaxBitrate}
|
||||||
|
selected={maxBitrate}
|
||||||
|
/>
|
||||||
|
<MediaSourceSelector
|
||||||
|
item={item}
|
||||||
|
onChange={setSelectedMediaSource}
|
||||||
|
selected={selectedMediaSource}
|
||||||
|
/>
|
||||||
|
{selectedMediaSource && (
|
||||||
|
<View className="flex flex-col space-y-2">
|
||||||
|
<AudioTrackSelector
|
||||||
|
source={selectedMediaSource}
|
||||||
|
onChange={setSelectedAudioStream}
|
||||||
|
selected={selectedAudioStream}
|
||||||
|
/>
|
||||||
|
<SubtitleTrackSelector
|
||||||
|
source={selectedMediaSource}
|
||||||
|
onChange={setSelectedSubtitleStream}
|
||||||
|
selected={selectedSubtitleStream}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Button
|
||||||
|
className="mt-auto"
|
||||||
|
onPress={acceptDownloadOptions}
|
||||||
|
color="purple"
|
||||||
|
>
|
||||||
|
{t("item_card.download.download_button")}
|
||||||
|
</Button>
|
||||||
|
<View className="opacity-70 text-center w-full flex items-center">
|
||||||
|
<Text className="text-xs">
|
||||||
|
{usingOptimizedServer
|
||||||
|
? t("item_card.download.using_optimized_server")
|
||||||
|
: t("item_card.download.using_default_method")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</BottomSheetView>
|
||||||
|
</BottomSheetModal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -24,7 +24,7 @@ const ProgressCircle: React.FC<ProgressCircleProps> = ({
|
|||||||
fill={fill}
|
fill={fill}
|
||||||
tintColor={tintColor}
|
tintColor={tintColor}
|
||||||
backgroundColor={backgroundColor}
|
backgroundColor={backgroundColor}
|
||||||
rotation={45}
|
rotation={0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
useDownloadComplete,
|
useDownloadComplete,
|
||||||
useDownloadError,
|
useDownloadError,
|
||||||
useDownloadProgress,
|
useDownloadProgress,
|
||||||
} from "../src/ExpoHlsDownloader";
|
} from "..";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses boot.xml in the root download directory to extract the stream folder name.
|
* Parses boot.xml in the root download directory to extract the stream folder name.
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ import type {
|
|||||||
OnCompleteEventPayload,
|
OnCompleteEventPayload,
|
||||||
OnErrorEventPayload,
|
OnErrorEventPayload,
|
||||||
OnProgressEventPayload,
|
OnProgressEventPayload,
|
||||||
} from "./ExpoHlsDownloader.types";
|
} from "./src/ExpoHlsDownloader.types";
|
||||||
import ExpoHlsDownloaderModule from "./ExpoHlsDownloaderModule";
|
import ExpoHlsDownloaderModule from "./src/ExpoHlsDownloaderModule";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiates an HLS download.
|
* Initiates an HLS download.
|
||||||
* @param url - The HLS stream URL.
|
* @param url - The HLS stream URL.
|
||||||
* @param assetTitle - A title for the asset.
|
* @param assetTitle - A title for the asset.
|
||||||
*/
|
*/
|
||||||
export function downloadHLSAsset(url: string, assetTitle: string): void {
|
function downloadHLSAsset(url: string, assetTitle: string): void {
|
||||||
ExpoHlsDownloaderModule.downloadHLSAsset(url, assetTitle);
|
ExpoHlsDownloaderModule.downloadHLSAsset(url, assetTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export function downloadHLSAsset(url: string, assetTitle: string): void {
|
|||||||
* @param listener A callback invoked with progress updates.
|
* @param listener A callback invoked with progress updates.
|
||||||
* @returns A subscription that can be removed.
|
* @returns A subscription that can be removed.
|
||||||
*/
|
*/
|
||||||
export function addProgressListener(
|
function addProgressListener(
|
||||||
listener: (event: OnProgressEventPayload) => void
|
listener: (event: OnProgressEventPayload) => void
|
||||||
): EventSubscription {
|
): EventSubscription {
|
||||||
return ExpoHlsDownloaderModule.addListener("onProgress", listener);
|
return ExpoHlsDownloaderModule.addListener("onProgress", listener);
|
||||||
@@ -34,7 +34,7 @@ export function addProgressListener(
|
|||||||
* @param listener A callback invoked with error details.
|
* @param listener A callback invoked with error details.
|
||||||
* @returns A subscription that can be removed.
|
* @returns A subscription that can be removed.
|
||||||
*/
|
*/
|
||||||
export function addErrorListener(
|
function addErrorListener(
|
||||||
listener: (event: OnErrorEventPayload) => void
|
listener: (event: OnErrorEventPayload) => void
|
||||||
): EventSubscription {
|
): EventSubscription {
|
||||||
return ExpoHlsDownloaderModule.addListener("onError", listener);
|
return ExpoHlsDownloaderModule.addListener("onError", listener);
|
||||||
@@ -45,7 +45,7 @@ export function addErrorListener(
|
|||||||
* @param listener A callback invoked when the download completes.
|
* @param listener A callback invoked when the download completes.
|
||||||
* @returns A subscription that can be removed.
|
* @returns A subscription that can be removed.
|
||||||
*/
|
*/
|
||||||
export function addCompleteListener(
|
function addCompleteListener(
|
||||||
listener: (event: OnCompleteEventPayload) => void
|
listener: (event: OnCompleteEventPayload) => void
|
||||||
): EventSubscription {
|
): EventSubscription {
|
||||||
return ExpoHlsDownloaderModule.addListener("onComplete", listener);
|
return ExpoHlsDownloaderModule.addListener("onComplete", listener);
|
||||||
@@ -54,7 +54,7 @@ export function addCompleteListener(
|
|||||||
/**
|
/**
|
||||||
* React hook that returns the current download progress (0–1).
|
* React hook that returns the current download progress (0–1).
|
||||||
*/
|
*/
|
||||||
export function useDownloadProgress(): number {
|
function useDownloadProgress(): number {
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = addProgressListener((event) => {
|
const subscription = addProgressListener((event) => {
|
||||||
@@ -68,7 +68,7 @@ export function useDownloadProgress(): number {
|
|||||||
/**
|
/**
|
||||||
* React hook that returns the latest download error (or null if none).
|
* React hook that returns the latest download error (or null if none).
|
||||||
*/
|
*/
|
||||||
export function useDownloadError(): string | null {
|
function useDownloadError(): string | null {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = addErrorListener((event) => {
|
const subscription = addErrorListener((event) => {
|
||||||
@@ -111,9 +111,7 @@ async function persistDownloadedFile(
|
|||||||
* @param destinationFileName Optional filename (with extension) to persist the file.
|
* @param destinationFileName Optional filename (with extension) to persist the file.
|
||||||
* @returns The final file URI or null if not completed.
|
* @returns The final file URI or null if not completed.
|
||||||
*/
|
*/
|
||||||
export function useDownloadComplete(
|
function useDownloadComplete(destinationFileName?: string): string | null {
|
||||||
destinationFileName?: string
|
|
||||||
): string | null {
|
|
||||||
const [location, setLocation] = useState<string | null>(null);
|
const [location, setLocation] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -161,3 +159,10 @@ export function useDownloadComplete(
|
|||||||
|
|
||||||
return location;
|
return location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
downloadHLSAsset,
|
||||||
|
useDownloadComplete,
|
||||||
|
useDownloadError,
|
||||||
|
useDownloadProgress,
|
||||||
|
};
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import ExpoModulesCore
|
|
||||||
import WebKit
|
|
||||||
|
|
||||||
// This view will be used as a native component. Make sure to inherit from `ExpoView`
|
|
||||||
// to apply the proper styling (e.g. border radius and shadows).
|
|
||||||
class ExpoHlsDownloaderView: ExpoView {
|
|
||||||
let webView = WKWebView()
|
|
||||||
let onLoad = EventDispatcher()
|
|
||||||
var delegate: WebViewDelegate?
|
|
||||||
|
|
||||||
required init(appContext: AppContext? = nil) {
|
|
||||||
super.init(appContext: appContext)
|
|
||||||
clipsToBounds = true
|
|
||||||
delegate = WebViewDelegate { url in
|
|
||||||
self.onLoad(["url": url])
|
|
||||||
}
|
|
||||||
webView.navigationDelegate = delegate
|
|
||||||
addSubview(webView)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
webView.frame = bounds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebViewDelegate: NSObject, WKNavigationDelegate {
|
|
||||||
let onUrlChange: (String) -> Void
|
|
||||||
|
|
||||||
init(onUrlChange: @escaping (String) -> Void) {
|
|
||||||
self.onUrlChange = onUrlChange
|
|
||||||
}
|
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation) {
|
|
||||||
if let url = webView.url {
|
|
||||||
onUrlChange(url.absoluteString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
import { NativeModule, requireNativeModule } from "expo";
|
import { requireNativeModule } from "expo";
|
||||||
import { ExpoHlsDownloaderModuleEvents } from "./ExpoHlsDownloader.types";
|
|
||||||
|
|
||||||
declare class ExpoHlsDownloaderModule extends NativeModule<ExpoHlsDownloaderModuleEvents> {
|
export default requireNativeModule("ExpoHlsDownloader");
|
||||||
downloadHLSAsset(url: string, assetTitle: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default requireNativeModule<ExpoHlsDownloaderModule>(
|
|
||||||
"ExpoHlsDownloader"
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { requireNativeView } from 'expo';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { ExpoHlsDownloaderViewProps } from './ExpoHlsDownloader.types';
|
|
||||||
|
|
||||||
const NativeView: React.ComponentType<ExpoHlsDownloaderViewProps> =
|
|
||||||
requireNativeView('ExpoHlsDownloader');
|
|
||||||
|
|
||||||
export default function ExpoHlsDownloaderView(props: ExpoHlsDownloaderViewProps) {
|
|
||||||
return <NativeView {...props} />;
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,6 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./build"
|
"outDir": "./build"
|
||||||
},
|
},
|
||||||
"include": ["./src"],
|
"include": ["./src", "index.ts"],
|
||||||
"exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
|
"exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user