forked from Ninjalama/streamyfin_mirror
Compare commits
1 Commits
feat/switc
...
feat/vlc-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ea2d81fb4 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,7 +10,6 @@ npm-debug.*
|
||||
*.orig.*
|
||||
web-build/
|
||||
modules/vlc-player/android/build
|
||||
modules/vlc-player/android/.gradle
|
||||
bun.lockb
|
||||
|
||||
# macOS
|
||||
|
||||
4
app.json
4
app.json
@@ -33,9 +33,7 @@
|
||||
"jsEngine": "hermes",
|
||||
"versionCode": 53,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive_icon.png",
|
||||
"backgroundColor": "#464646"
|
||||
|
||||
"foregroundImage": "./assets/images/adaptive_icon.png"
|
||||
},
|
||||
"package": "com.fredrikburmester.streamyfin",
|
||||
"permissions": [
|
||||
|
||||
@@ -78,6 +78,12 @@ export default function IndexLayout() {
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="settings/dashboard/sessions"
|
||||
options={{
|
||||
title: t("home.settings.dashboard.sessions_title"),
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="settings/jellyseerr/page"
|
||||
options={{
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FlashList } from "@shopify/flash-list";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { HardwareAccelerationType, SessionInfoDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { SessionInfoDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import Poster from "@/components/posters/Poster";
|
||||
@@ -186,7 +186,6 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => {
|
||||
resolution: <Ionicons name="film-outline" size={12} color="white" />,
|
||||
language: <Ionicons name="language-outline" size={12} color="white" />,
|
||||
audioChannels: <Ionicons name="mic-outline" size={12} color="white" />,
|
||||
hwType: <Ionicons name="hardware-chip-outline" size={12} color="white" />,
|
||||
} as const;
|
||||
|
||||
const icon = (val: string) => {
|
||||
@@ -201,8 +200,6 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => {
|
||||
switch (key) {
|
||||
case "bitrate":
|
||||
return formatBitrate(val);
|
||||
case "hwType":
|
||||
return val === HardwareAccelerationType.None ? "sw" : "hw";
|
||||
default:
|
||||
return val;
|
||||
}
|
||||
@@ -222,7 +219,6 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => {
|
||||
};
|
||||
|
||||
interface StreamProps {
|
||||
hwType?: HardwareAccelerationType | null | undefined;
|
||||
resolution?: string | null | undefined;
|
||||
language?: string | null | undefined;
|
||||
codec?: string | null | undefined;
|
||||
@@ -300,8 +296,8 @@ const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
}, [session.PlayState?.SubtitleStreamIndex]);
|
||||
|
||||
const isTranscoding = useMemo(() => {
|
||||
return session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo;
|
||||
}, [session.PlayState?.PlayMethod, session.TranscodingInfo]);
|
||||
return session.PlayState?.PlayMethod == "Transcode";
|
||||
}, [session.PlayState?.PlayMethod]);
|
||||
|
||||
const videoStreamTitle = () => {
|
||||
return videoStream?.DisplayTitle?.split(" ")[0];
|
||||
@@ -317,7 +313,6 @@ const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
codec: videoStream?.Codec,
|
||||
}}
|
||||
transcodeProperties={{
|
||||
hwType: session.TranscodingInfo?.HardwareAccelerationType,
|
||||
bitrate: session.TranscodingInfo?.Bitrate,
|
||||
codec: session.TranscodingInfo?.VideoCodec,
|
||||
}}
|
||||
@@ -337,6 +332,7 @@ const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
audioChannels: audioStream?.ChannelLayout,
|
||||
}}
|
||||
transcodeProperties={{
|
||||
bitrate: session.TranscodingInfo?.Bitrate,
|
||||
codec: session.TranscodingInfo?.AudioCodec,
|
||||
audioChannels: session.TranscodingInfo?.AudioChannels?.toString(),
|
||||
}}
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function page() {
|
||||
});
|
||||
|
||||
return await getStatistics({
|
||||
url: updatedUrl,
|
||||
url: settings?.optimizedVersionsServerUrl,
|
||||
authHeader: api?.accessToken,
|
||||
deviceId: getOrSetDeviceId(),
|
||||
});
|
||||
|
||||
@@ -42,28 +42,25 @@ const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||
import RequestModal from "@/components/jellyseerr/RequestModal";
|
||||
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
|
||||
import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
||||
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const params = useLocalSearchParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } =
|
||||
const { mediaTitle, releaseYear, posterSrc, ...result } =
|
||||
params as unknown as {
|
||||
mediaTitle: string;
|
||||
releaseYear: number;
|
||||
canRequest: string;
|
||||
posterSrc: string;
|
||||
mediaType: MediaType;
|
||||
} & Partial<MovieResult | TvResult | MovieDetails | TvDetails>;
|
||||
} & Partial<MovieResult | TvResult>;
|
||||
|
||||
const navigation = useNavigation();
|
||||
const { jellyseerrApi, requestMedia } = useJellyseerr();
|
||||
|
||||
const [issueType, setIssueType] = useState<IssueType>();
|
||||
const [issueMessage, setIssueMessage] = useState<string>();
|
||||
const [requestBody, _setRequestBody] = useState<MediaRequestBody>();
|
||||
const advancedReqModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
|
||||
@@ -74,7 +71,7 @@ const Page: React.FC = () => {
|
||||
refetch,
|
||||
} = useQuery({
|
||||
enabled: !!jellyseerrApi && !!result && !!result.id,
|
||||
queryKey: ["jellyseerr", "detail", mediaType, result.id],
|
||||
queryKey: ["jellyseerr", "detail", result.mediaType, result.id],
|
||||
staleTime: 0,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true,
|
||||
@@ -82,7 +79,7 @@ const Page: React.FC = () => {
|
||||
retryOnMount: true,
|
||||
refetchInterval: 0,
|
||||
queryFn: async () => {
|
||||
return mediaType === MediaType.MOVIE
|
||||
return result.mediaType === MediaType.MOVIE
|
||||
? jellyseerrApi?.movieDetails(result.id!!)
|
||||
: jellyseerrApi?.tvDetails(result.id!!);
|
||||
},
|
||||
@@ -114,15 +111,10 @@ const Page: React.FC = () => {
|
||||
}
|
||||
}, [jellyseerrApi, details, result, issueType, issueMessage]);
|
||||
|
||||
const setRequestBody = useCallback((body: MediaRequestBody) => {
|
||||
_setRequestBody(body)
|
||||
advancedReqModalRef?.current?.present?.();
|
||||
}, [requestBody, _setRequestBody, advancedReqModalRef])
|
||||
|
||||
const request = useCallback(async () => {
|
||||
const body: MediaRequestBody = {
|
||||
mediaId: Number(result.id!!),
|
||||
mediaType: mediaType!!,
|
||||
mediaType: result.mediaType!!,
|
||||
tvdbId: details?.externalIds?.tvdbId,
|
||||
seasons: (details as TvDetails)?.seasons
|
||||
?.filter?.((s) => s.seasonNumber !== 0)
|
||||
@@ -130,7 +122,7 @@ const Page: React.FC = () => {
|
||||
};
|
||||
|
||||
if (hasAdvancedRequestPermission) {
|
||||
setRequestBody(body)
|
||||
advancedReqModalRef?.current?.present?.(body);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -140,7 +132,7 @@ const Page: React.FC = () => {
|
||||
const isAnime = useMemo(
|
||||
() =>
|
||||
(details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) &&
|
||||
mediaType === MediaType.TV,
|
||||
result.mediaType === MediaType.TV,
|
||||
[details]
|
||||
);
|
||||
|
||||
@@ -208,7 +200,7 @@ const Page: React.FC = () => {
|
||||
<View className="px-4">
|
||||
<View className="flex flex-row justify-between w-full">
|
||||
<View className="flex flex-col w-56">
|
||||
<JellyserrRatings result={result as MovieResult | TvResult | MovieDetails | TvDetails} />
|
||||
<JellyserrRatings result={result as MovieResult | TvResult} />
|
||||
<Text
|
||||
uiTextView
|
||||
selectable
|
||||
@@ -255,14 +247,15 @@ const Page: React.FC = () => {
|
||||
<OverviewText text={result.overview} className="mt-4" />
|
||||
</View>
|
||||
|
||||
{mediaType === MediaType.TV && (
|
||||
{result.mediaType === MediaType.TV && (
|
||||
<JellyseerrSeasons
|
||||
isLoading={isLoading || isFetching}
|
||||
result={result as TvResult}
|
||||
details={details as TvDetails}
|
||||
refetch={refetch}
|
||||
hasAdvancedRequest={hasAdvancedRequestPermission}
|
||||
onAdvancedRequest={(data) =>
|
||||
setRequestBody(data)
|
||||
advancedReqModalRef?.current?.present(data)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -276,17 +269,14 @@ const Page: React.FC = () => {
|
||||
</ParallaxScrollView>
|
||||
<RequestModal
|
||||
ref={advancedReqModalRef}
|
||||
requestBody={requestBody}
|
||||
title={mediaTitle}
|
||||
id={result.id!!}
|
||||
type={mediaType}
|
||||
type={result.mediaType as MediaType}
|
||||
isAnime={isAnime}
|
||||
onRequested={() => {
|
||||
_setRequestBody(undefined)
|
||||
advancedReqModalRef?.current?.close();
|
||||
refetch();
|
||||
}}
|
||||
onDismiss={() => _setRequestBody(undefined)}
|
||||
/>
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
|
||||
@@ -5,13 +5,14 @@ import { Controls } from "@/components/video-player/controls/Controls";
|
||||
import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener";
|
||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
||||
import { VlcPlayerView } from "@/modules";
|
||||
import { VlcPlayerView } from "@/modules/vlc-player";
|
||||
import {
|
||||
OnDiscoveryStateChangedPayload,
|
||||
PipStartedPayload,
|
||||
PlaybackStatePayload,
|
||||
ProgressUpdatePayload,
|
||||
VlcPlayerViewRef,
|
||||
} from "@/modules/VlcPlayer.types";
|
||||
VlcPlayerViewRef, VLCRendererItem,
|
||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||
const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null;
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
@@ -29,14 +30,12 @@ import { useSharedValue } from "react-native-reanimated";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
PlaybackOrder,
|
||||
PlaybackProgressInfo,
|
||||
PlaybackStartInfo,
|
||||
RepeatMode,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import { BaseItemDto, MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||
import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView} from "@gorhom/bottom-sheet";
|
||||
import {ListGroup} from "@/components/list/ListGroup";
|
||||
import {ListItem} from "@/components/list/ListItem";
|
||||
import {storage} from "@/utils/mmkv";
|
||||
import {t} from "i18next";
|
||||
|
||||
export default function page() {
|
||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||
@@ -52,6 +51,8 @@ export default function page() {
|
||||
const [isBuffering, setIsBuffering] = useState(true);
|
||||
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
||||
const [isPipStarted, setIsPipStarted] = useState(false);
|
||||
const [rendererItems, setRendererItems] = useState<VLCRendererItem[]>([]);
|
||||
const discoveryModal = useRef<BottomSheetModal>(null);
|
||||
|
||||
const progress = useSharedValue(0);
|
||||
const isSeeking = useSharedValue(false);
|
||||
@@ -182,15 +183,16 @@ export default function page() {
|
||||
fetchStreamData();
|
||||
}, [itemId, mediaSourceId, bitrateValue, api, item, user?.Id]);
|
||||
|
||||
const togglePlay = async () => {
|
||||
const togglePlay = useCallback(async () => {
|
||||
if (!api) return;
|
||||
|
||||
lightHapticFeedback();
|
||||
setIsPlaying(!isPlaying);
|
||||
if (isPlaying) {
|
||||
await videoRef.current?.pause();
|
||||
} else {
|
||||
videoRef.current?.play();
|
||||
}
|
||||
};
|
||||
}, [isPlaying, api, item, stream, videoRef, audioIndex, subtitleIndex, mediaSourceId, offline, progress]);
|
||||
|
||||
const reportPlaybackStopped = useCallback(async () => {
|
||||
if (offline) return;
|
||||
@@ -218,23 +220,6 @@ export default function page() {
|
||||
};
|
||||
}, [navigation, stop]);
|
||||
|
||||
const currentPlayStateInfo = () => {
|
||||
return {
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: msToTicks(progress.get()),
|
||||
isPaused: !isPlaying,
|
||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream.sessionId,
|
||||
isMuted: false,
|
||||
canSeek: true,
|
||||
repeatMode: RepeatMode.RepeatNone,
|
||||
playbackOrder: PlaybackOrder.Default,
|
||||
};
|
||||
};
|
||||
|
||||
const onProgress = useCallback(
|
||||
async (data: ProgressUpdatePayload) => {
|
||||
if (isSeeking.get() || isPlaybackStopped) return;
|
||||
@@ -248,30 +233,67 @@ export default function page() {
|
||||
|
||||
if (offline) return;
|
||||
|
||||
const currentTimeInTicks = msToTicks(currentTime);
|
||||
|
||||
if (!item?.Id || !stream) return;
|
||||
|
||||
reportPlaybackProgress();
|
||||
await getPlaystateApi(api!).onPlaybackProgress({
|
||||
itemId: item.Id,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: Math.floor(currentTimeInTicks),
|
||||
isPaused: !isPlaying,
|
||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
},
|
||||
[item?.Id, audioIndex, subtitleIndex, mediaSourceId, isPlaying, stream, isSeeking, isPlaybackStopped, isBuffering]
|
||||
);
|
||||
|
||||
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
||||
const { pipStarted } = e.nativeEvent;
|
||||
setIsPipStarted(pipStarted);
|
||||
}, []);
|
||||
|
||||
const reportPlaybackProgress = useCallback(async () => {
|
||||
if (!api || offline || !stream) return;
|
||||
await getPlaystateApi(api).reportPlaybackProgress({
|
||||
playbackProgressInfo: currentPlayStateInfo() as PlaybackProgressInfo,
|
||||
});
|
||||
}, [api, isPlaying, offline, stream, item?.Id, audioIndex, subtitleIndex, mediaSourceId, progress]);
|
||||
const changePlaybackState = useCallback(
|
||||
async (isPlaying: boolean) => {
|
||||
if (!api || offline || !stream) return;
|
||||
await getPlaystateApi(api).onPlaybackProgress({
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: msToTicks(progress.get()),
|
||||
isPaused: !isPlaying,
|
||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
},
|
||||
[api, offline, stream, item?.Id, audioIndex, subtitleIndex, mediaSourceId, progress]
|
||||
);
|
||||
|
||||
const startPosition = useMemo(() => {
|
||||
if (offline) return 0;
|
||||
return item?.UserData?.PlaybackPositionTicks ? ticksToSeconds(item.UserData.PlaybackPositionTicks) : 0;
|
||||
}, [item]);
|
||||
|
||||
const reportPlaybackStart = useCallback(async () => {
|
||||
if (offline || !stream) return;
|
||||
await getPlaystateApi(api!).onPlaybackStart({
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
playMethod: stream.url?.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream?.sessionId ? stream?.sessionId : undefined,
|
||||
});
|
||||
hasReportedRef.current = true;
|
||||
}, [api, item, stream]);
|
||||
|
||||
const hasReportedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (stream && !hasReportedRef.current) {
|
||||
reportPlaybackStart();
|
||||
hasReportedRef.current = true; // Mark as reported
|
||||
}
|
||||
}, [stream]);
|
||||
|
||||
useWebSocket({
|
||||
isPlaying: isPlaying,
|
||||
togglePlay: togglePlay,
|
||||
@@ -279,19 +301,36 @@ export default function page() {
|
||||
offline,
|
||||
});
|
||||
|
||||
const onPlaybackStateChanged = useCallback(
|
||||
async (e: PlaybackStatePayload) => {
|
||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
||||
const { pipStarted } = e.nativeEvent;
|
||||
setIsPipStarted(pipStarted);
|
||||
}, []);
|
||||
|
||||
const onDiscoveryStateChanged = useCallback((e: OnDiscoveryStateChangedPayload) => {
|
||||
const {renderers} = e.nativeEvent;
|
||||
setRendererItems(renderers);
|
||||
}, []);
|
||||
|
||||
const startDiscovery = useCallback(async () => {
|
||||
videoRef?.current?.pause?.()
|
||||
videoRef?.current?.stopDiscovery?.()
|
||||
videoRef?.current?.startDiscovery?.()
|
||||
discoveryModal?.current?.present?.()
|
||||
}, [rendererItems, videoRef])
|
||||
|
||||
const onPlaybackStateChanged = useCallback(async (e: PlaybackStatePayload) => {
|
||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||
|
||||
if (state === "Playing") {
|
||||
setIsPlaying(true);
|
||||
reportPlaybackProgress();
|
||||
await changePlaybackState(true);
|
||||
if (!Platform.isTV) await activateKeepAwakeAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === "Paused") {
|
||||
setIsPlaying(false);
|
||||
reportPlaybackProgress();
|
||||
await changePlaybackState(false);
|
||||
if (!Platform.isTV) await deactivateKeepAwake();
|
||||
return;
|
||||
}
|
||||
@@ -303,7 +342,7 @@ export default function page() {
|
||||
setIsBuffering(true);
|
||||
}
|
||||
},
|
||||
[reportPlaybackProgress]
|
||||
[changePlaybackState]
|
||||
);
|
||||
|
||||
const allAudio = stream?.mediaSource.MediaStreams?.filter((audio) => audio.Type === "Audio") || [];
|
||||
@@ -389,6 +428,7 @@ export default function page() {
|
||||
progressUpdateInterval={1000}
|
||||
onVideoStateChange={onPlaybackStateChanged}
|
||||
onPipStarted={onPipStarted}
|
||||
onDiscoveryStateChanged={onDiscoveryStateChanged}
|
||||
onVideoLoadEnd={() => {
|
||||
setIsVideoLoaded(true);
|
||||
}}
|
||||
@@ -400,34 +440,76 @@ export default function page() {
|
||||
/>
|
||||
</View>
|
||||
{videoRef.current && !isPipStarted && isMounted === true ? (
|
||||
<Controls
|
||||
mediaSource={stream?.mediaSource}
|
||||
item={item}
|
||||
videoRef={videoRef}
|
||||
togglePlay={togglePlay}
|
||||
isPlaying={isPlaying}
|
||||
isSeeking={isSeeking}
|
||||
progress={progress}
|
||||
cacheProgress={cacheProgress}
|
||||
isBuffering={isBuffering}
|
||||
showControls={showControls}
|
||||
setShowControls={setShowControls}
|
||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
||||
ignoreSafeAreas={ignoreSafeAreas}
|
||||
isVideoLoaded={isVideoLoaded}
|
||||
startPictureInPicture={videoRef?.current?.startPictureInPicture}
|
||||
play={videoRef.current?.play}
|
||||
pause={videoRef.current?.pause}
|
||||
seek={videoRef.current?.seekTo}
|
||||
enableTrickplay={true}
|
||||
getAudioTracks={videoRef.current?.getAudioTracks}
|
||||
getSubtitleTracks={videoRef.current?.getSubtitleTracks}
|
||||
offline={offline}
|
||||
setSubtitleTrack={videoRef.current.setSubtitleTrack}
|
||||
setSubtitleURL={videoRef.current.setSubtitleURL}
|
||||
setAudioTrack={videoRef.current.setAudioTrack}
|
||||
isVlc
|
||||
/>
|
||||
<>
|
||||
<Controls
|
||||
mediaSource={stream?.mediaSource}
|
||||
item={item}
|
||||
videoRef={videoRef}
|
||||
togglePlay={togglePlay}
|
||||
isPlaying={isPlaying}
|
||||
isSeeking={isSeeking}
|
||||
progress={progress}
|
||||
cacheProgress={cacheProgress}
|
||||
isBuffering={isBuffering}
|
||||
showControls={showControls}
|
||||
setShowControls={setShowControls}
|
||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
||||
ignoreSafeAreas={ignoreSafeAreas}
|
||||
isVideoLoaded={isVideoLoaded}
|
||||
startPictureInPicture={videoRef?.current?.startPictureInPicture}
|
||||
play={videoRef.current?.play}
|
||||
pause={videoRef.current?.pause}
|
||||
seek={videoRef.current?.seekTo}
|
||||
enableTrickplay={true}
|
||||
getAudioTracks={videoRef.current?.getAudioTracks}
|
||||
getSubtitleTracks={videoRef.current?.getSubtitleTracks}
|
||||
offline={offline}
|
||||
setSubtitleTrack={videoRef.current.setSubtitleTrack}
|
||||
setSubtitleURL={videoRef.current.setSubtitleURL}
|
||||
setAudioTrack={videoRef.current.setAudioTrack}
|
||||
startDiscovery={startDiscovery}
|
||||
isVlc
|
||||
/>
|
||||
<BottomSheetModal
|
||||
ref={discoveryModal}
|
||||
enableDynamicSizing
|
||||
enableDismissOnClose
|
||||
snapPoints={["100%"]}
|
||||
onDismiss={() => {
|
||||
videoRef.current?.stopDiscovery?.()
|
||||
videoRef.current?.play?.()
|
||||
}}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
backgroundStyle={{
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
backdropComponent={(sheetProps: BottomSheetBackdropProps) =>
|
||||
<BottomSheetBackdrop
|
||||
{...sheetProps}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<BottomSheetView>
|
||||
<ListGroup title={t("player.device_discovery")} className="mt-4 h-1/3">
|
||||
{rendererItems.map((renderItem, index) => (
|
||||
<ListItem
|
||||
onPress={() => {
|
||||
// todo: set renderer item on player to change to device
|
||||
}}
|
||||
icon="cast"
|
||||
title={renderItem.name}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</ListGroup>
|
||||
|
||||
</BottomSheetView>
|
||||
</BottomSheetModal>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 91 KiB |
@@ -21,19 +21,14 @@ export const Tag: React.FC<{ text: string, textClass?: ViewProps["className"], t
|
||||
);
|
||||
};
|
||||
|
||||
export const Tags: React.FC<TagProps & {tagProps?: ViewProps} & ViewProps> = ({
|
||||
tags,
|
||||
textClass = "text-xs",
|
||||
tagProps,
|
||||
...props
|
||||
}) => {
|
||||
export const Tags: React.FC<TagProps & ViewProps> = ({ tags, textClass = "text-xs", ...props }) => {
|
||||
if (!tags || tags.length === 0) return null;
|
||||
|
||||
return (
|
||||
<View className={`flex flex-row flex-wrap gap-1 ${props.className}`} {...props}>
|
||||
{tags.map((tag, idx) => (
|
||||
<View key={idx}>
|
||||
<Tag key={idx} textClass={textClass} text={tag} {...tagProps}/>
|
||||
<Tag key={idx} textClass={textClass} text={tag}/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -7,9 +7,6 @@ import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
|
||||
import {TvDetails} from "@/utils/jellyseerr/server/models/Tv";
|
||||
import {useMemo} from "react";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
item?: BaseItemDto | null;
|
||||
@@ -52,17 +49,14 @@ export const Ratings: React.FC<Props> = ({ item, ...props }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult | TvDetails | MovieDetails }> = ({
|
||||
export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult }> = ({
|
||||
result,
|
||||
}) => {
|
||||
const { jellyseerrApi, getMediaType } = useJellyseerr();
|
||||
|
||||
const mediaType = useMemo(() => getMediaType(result), [result]);
|
||||
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["jellyseerr", result.id, mediaType, "ratings"],
|
||||
queryKey: ["jellyseerr", result.id, result.mediaType, "ratings"],
|
||||
queryFn: async () => {
|
||||
return mediaType === MediaType.MOVIE
|
||||
return result.mediaType === MediaType.MOVIE
|
||||
? jellyseerrApi?.movieRatings(result.id)
|
||||
: jellyseerrApi?.tvRatings(result.id);
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Props<T> {
|
||||
title: string | ReactNode;
|
||||
label: string;
|
||||
onSelected: (...item: T[]) => void;
|
||||
multiple?: boolean;
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
const Dropdown = <T extends unknown>({
|
||||
@@ -30,7 +30,7 @@ const Dropdown = <T extends unknown>({
|
||||
title,
|
||||
label,
|
||||
onSelected,
|
||||
multiple = false,
|
||||
multi = false,
|
||||
...props
|
||||
}: PropsWithChildren<Props<T> & ViewProps>) => {
|
||||
if (Platform.isTV) return null;
|
||||
@@ -72,7 +72,7 @@ const Dropdown = <T extends unknown>({
|
||||
>
|
||||
<DropdownMenu.Label>{label}</DropdownMenu.Label>
|
||||
{data.map((item, idx) =>
|
||||
multiple ? (
|
||||
multi ? (
|
||||
<DropdownMenu.CheckboxItem
|
||||
value={
|
||||
selected?.some((s) => keyExtractor(s) == keyExtractor(item))
|
||||
@@ -80,7 +80,7 @@ const Dropdown = <T extends unknown>({
|
||||
: "off"
|
||||
}
|
||||
key={keyExtractor(item)}
|
||||
onValueChange={(next: "on" | "off", previous: "on" | "off") => {
|
||||
onValueChange={(next, previous) =>
|
||||
setSelected((p) => {
|
||||
const prev = p || [];
|
||||
if (next == "on") {
|
||||
@@ -92,7 +92,7 @@ const Dropdown = <T extends unknown>({
|
||||
),
|
||||
];
|
||||
})
|
||||
}}
|
||||
}
|
||||
>
|
||||
<DropdownMenu.ItemTitle>
|
||||
{titleExtractor(item)}
|
||||
|
||||
@@ -9,16 +9,13 @@ import {
|
||||
Permission,
|
||||
} from "@/utils/jellyseerr/server/lib/permissions";
|
||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||
import {TvDetails} from "@/utils/jellyseerr/server/models/Tv";
|
||||
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
|
||||
|
||||
interface Props extends TouchableOpacityProps {
|
||||
result: MovieResult | TvResult | MovieDetails | TvDetails;
|
||||
result: MovieResult | TvResult;
|
||||
mediaTitle: string;
|
||||
releaseYear: number;
|
||||
canRequest: boolean;
|
||||
posterSrc: string;
|
||||
mediaType: MediaType;
|
||||
}
|
||||
|
||||
export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
@@ -27,7 +24,6 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
releaseYear,
|
||||
canRequest,
|
||||
posterSrc,
|
||||
mediaType,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
@@ -50,7 +46,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
() =>
|
||||
requestMedia(mediaTitle, {
|
||||
mediaId: result.id,
|
||||
mediaType,
|
||||
mediaType: result.mediaType,
|
||||
}),
|
||||
[jellyseerrApi, result]
|
||||
);
|
||||
@@ -71,7 +67,6 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
releaseYear,
|
||||
canRequest,
|
||||
posterSrc,
|
||||
mediaType
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -88,7 +83,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
key={"content"}
|
||||
>
|
||||
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
|
||||
{canRequest && mediaType === MediaType.MOVIE && (
|
||||
{canRequest && result.mediaType === MediaType.MOVIE && (
|
||||
<ContextMenu.Item
|
||||
key="item-1"
|
||||
onSelect={() => {
|
||||
|
||||
@@ -21,7 +21,6 @@ import { LoadingSkeleton } from "../search/LoadingSkeleton";
|
||||
import { SearchItemWrapper } from "../search/SearchItemWrapper";
|
||||
import PersonPoster from "./PersonPoster";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {uniqBy} from "lodash";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
searchQuery: string;
|
||||
@@ -78,28 +77,25 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
||||
|
||||
const jellyseerrMovieResults = useMemo(
|
||||
() =>
|
||||
uniqBy(
|
||||
jellyseerrResults?.filter((r) => r.mediaType === MediaType.MOVIE) as MovieResult[],
|
||||
"id"
|
||||
),
|
||||
jellyseerrResults?.filter(
|
||||
(r) => r.mediaType === MediaType.MOVIE
|
||||
) as MovieResult[],
|
||||
[jellyseerrResults]
|
||||
);
|
||||
|
||||
const jellyseerrTvResults = useMemo(
|
||||
() =>
|
||||
uniqBy(
|
||||
jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[],
|
||||
"id"
|
||||
),
|
||||
jellyseerrResults?.filter(
|
||||
(r) => r.mediaType === MediaType.TV
|
||||
) as TvResult[],
|
||||
[jellyseerrResults]
|
||||
);
|
||||
|
||||
const jellyseerrPersonResults = useMemo(
|
||||
() =>
|
||||
uniqBy(
|
||||
jellyseerrResults?.filter((r) => r.mediaType === "person") as PersonResult[],
|
||||
"id"
|
||||
),
|
||||
jellyseerrResults?.filter(
|
||||
(r) => r.mediaType === "person"
|
||||
) as PersonResult[],
|
||||
[jellyseerrResults]
|
||||
);
|
||||
|
||||
|
||||
@@ -15,22 +15,18 @@ import { useTranslation } from "react-i18next";
|
||||
interface Props {
|
||||
id: number;
|
||||
title: string,
|
||||
requestBody?: MediaRequestBody,
|
||||
type: MediaType;
|
||||
isAnime?: boolean;
|
||||
is4k?: boolean;
|
||||
onRequested?: () => void;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps, 'id'>>(({
|
||||
id,
|
||||
title,
|
||||
requestBody,
|
||||
type,
|
||||
isAnime = false,
|
||||
onRequested,
|
||||
onDismiss,
|
||||
...props
|
||||
}, ref) => {
|
||||
const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr();
|
||||
@@ -43,6 +39,8 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [modalRequestProps, setModalRequestProps] = useState<MediaRequestBody>();
|
||||
|
||||
const {data: serviceSettings} = useQuery({
|
||||
queryKey: ["jellyseerr", "request", type, 'service'],
|
||||
queryFn: async () => jellyseerrApi?.service(type == 'movie' ? 'radarr' : 'sonarr'),
|
||||
@@ -100,19 +98,16 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
||||
: defaultServiceDetails?.server.activeTags
|
||||
)?.includes(t.id)
|
||||
) ?? []
|
||||
|
||||
console.log(tags)
|
||||
return tags
|
||||
},
|
||||
[defaultServiceDetails]
|
||||
);
|
||||
|
||||
const seasonTitle = useMemo(
|
||||
() => {
|
||||
if (requestBody?.seasons && requestBody?.seasons?.length > 1) {
|
||||
return t("jellyseerr.season_all")
|
||||
}
|
||||
return t("jellyseerr.season_number", {season_number: requestBody?.seasons})
|
||||
},
|
||||
[requestBody?.seasons]
|
||||
() => modalRequestProps?.seasons?.length ? t("jellyseerr.season_x", {seasons: modalRequestProps?.seasons}) : undefined,
|
||||
[modalRequestProps?.seasons]
|
||||
);
|
||||
|
||||
const request = useCallback(() => {requestMedia(
|
||||
@@ -122,12 +117,12 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
||||
profileId: defaultProfile.id,
|
||||
rootFolder: defaultFolder.path,
|
||||
tags: defaultTags.map(t => t.id),
|
||||
...requestBody,
|
||||
...modalRequestProps,
|
||||
...requestOverrides
|
||||
},
|
||||
onRequested
|
||||
)
|
||||
}, [requestBody, requestOverrides, defaultProfile, defaultFolder, defaultTags]);
|
||||
}, [modalRequestProps, requestOverrides, defaultProfile, defaultFolder, defaultTags]);
|
||||
|
||||
const pathTitleExtractor = (item: RootFolder) => `${item.path} (${item.freeSpace.bytesToReadable()})`;
|
||||
|
||||
@@ -136,7 +131,7 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
||||
ref={ref}
|
||||
enableDynamicSizing
|
||||
enableDismissOnClose
|
||||
onDismiss={onDismiss}
|
||||
onDismiss={() => setModalRequestProps(undefined)}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
@@ -151,86 +146,89 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
||||
/>
|
||||
}
|
||||
>
|
||||
<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">{t("jellyseerr.advanced")}</Text>
|
||||
{seasonTitle &&
|
||||
<Text className="text-neutral-300">{seasonTitle}</Text>
|
||||
}
|
||||
{(data) => {
|
||||
setModalRequestProps(data?.data as MediaRequestBody)
|
||||
return <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">{t("jellyseerr.advanced")}</Text>
|
||||
{seasonTitle &&
|
||||
<Text className="text-neutral-300">{seasonTitle}</Text>
|
||||
}
|
||||
</View>
|
||||
<View className="flex flex-col space-y-2">
|
||||
{(defaultService && defaultServiceDetails && users) && (
|
||||
<>
|
||||
<Dropdown
|
||||
data={defaultServiceDetails.profiles}
|
||||
titleExtractor={(item) => item.name}
|
||||
placeholderText={defaultProfile.name}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
label={t("jellyseerr.quality_profile")}
|
||||
onSelected={(item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
profileId: item?.id
|
||||
}))
|
||||
}
|
||||
title={t("jellyseerr.quality_profile")}
|
||||
/>
|
||||
<Dropdown
|
||||
data={defaultServiceDetails.rootFolders}
|
||||
titleExtractor={pathTitleExtractor}
|
||||
placeholderText={defaultFolder ? pathTitleExtractor(defaultFolder) : ""}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
label={t("jellyseerr.root_folder")}
|
||||
onSelected={(item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
rootFolder: item.path
|
||||
}))}
|
||||
title={t("jellyseerr.root_folder")}
|
||||
/>
|
||||
<Dropdown
|
||||
multi={true}
|
||||
data={defaultServiceDetails.tags}
|
||||
titleExtractor={(item) => item.label}
|
||||
placeholderText={defaultTags.map(t => t.label).join(",")}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
label={t("jellyseerr.tags")}
|
||||
onSelected={(...item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
tags: item.map(i => i.id)
|
||||
}))
|
||||
}
|
||||
title={t("jellyseerr.tags")}
|
||||
/>
|
||||
<Dropdown
|
||||
data={users}
|
||||
titleExtractor={(item) => item.displayName}
|
||||
placeholderText={jellyseerrUser!!.displayName}
|
||||
keyExtractor={(item) => item.id.toString() || ""}
|
||||
label={t("jellyseerr.request_as")}
|
||||
onSelected={(item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
userId: item?.id
|
||||
}))
|
||||
}
|
||||
title={t("jellyseerr.request_as")}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
<Button
|
||||
className="mt-auto"
|
||||
onPress={request}
|
||||
color="purple"
|
||||
>
|
||||
{t("jellyseerr.request_button")}
|
||||
</Button>
|
||||
</View>
|
||||
<View className="flex flex-col space-y-2">
|
||||
{(defaultService && defaultServiceDetails && users) && (
|
||||
<>
|
||||
<Dropdown
|
||||
data={defaultServiceDetails.profiles}
|
||||
titleExtractor={(item) => item.name}
|
||||
placeholderText={requestOverrides.profileName || defaultProfile.name}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
label={t("jellyseerr.quality_profile")}
|
||||
onSelected={(item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
profileId: item?.id
|
||||
}))
|
||||
}
|
||||
title={t("jellyseerr.quality_profile")}
|
||||
/>
|
||||
<Dropdown
|
||||
data={defaultServiceDetails.rootFolders}
|
||||
titleExtractor={pathTitleExtractor}
|
||||
placeholderText={defaultFolder ? pathTitleExtractor(defaultFolder) : ""}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
label={t("jellyseerr.root_folder")}
|
||||
onSelected={(item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
rootFolder: item.path
|
||||
}))}
|
||||
title={t("jellyseerr.root_folder")}
|
||||
/>
|
||||
<Dropdown
|
||||
multiple
|
||||
data={defaultServiceDetails.tags}
|
||||
titleExtractor={(item) => item.label}
|
||||
placeholderText={defaultTags.map(t => t.label).join(",")}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
label={t("jellyseerr.tags")}
|
||||
onSelected={(...selected) =>
|
||||
setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
tags: selected.map(i => i.id)
|
||||
}))
|
||||
}
|
||||
title={t("jellyseerr.tags")}
|
||||
/>
|
||||
<Dropdown
|
||||
data={users}
|
||||
titleExtractor={(item) => item.displayName}
|
||||
placeholderText={jellyseerrUser!!.displayName}
|
||||
keyExtractor={(item) => item.id.toString() || ""}
|
||||
label={t("jellyseerr.request_as")}
|
||||
onSelected={(item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
userId: item?.id
|
||||
}))
|
||||
}
|
||||
title={t("jellyseerr.request_as")}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
<Button
|
||||
className="mt-auto"
|
||||
onPress={request}
|
||||
color="purple"
|
||||
>
|
||||
{t("jellyseerr.request_button")}
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheetView>
|
||||
</BottomSheetView>
|
||||
}}
|
||||
</BottomSheetModal>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import {View} from "react-native";
|
||||
import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
|
||||
import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
|
||||
import GenreSlide from "@/components/jellyseerr/discover/GenreSlide";
|
||||
import RecentRequestsSlide from "@/components/jellyseerr/discover/RecentRequestsSlide";
|
||||
|
||||
interface Props {
|
||||
sliders?: DiscoverSlider[];
|
||||
@@ -26,8 +25,6 @@ const Discover: React.FC<Props> = ({ sliders }) => {
|
||||
<View className="flex flex-col space-y-4 mb-8">
|
||||
{sortedSliders.map(slide => {
|
||||
switch (slide.type) {
|
||||
case DiscoverSliderType.RECENT_REQUESTS:
|
||||
return <RecentRequestsSlide key={slide.id} slide={slide} />
|
||||
case DiscoverSliderType.NETWORKS:
|
||||
return <CompanySlide key={slide.id} slide={slide} data={networks}/>
|
||||
case DiscoverSliderType.STUDIOS:
|
||||
|
||||
@@ -10,7 +10,6 @@ import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
||||
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
|
||||
import {ViewProps} from "react-native";
|
||||
import {uniqBy} from "lodash";
|
||||
|
||||
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
@@ -58,11 +57,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) =>
|
||||
});
|
||||
|
||||
const flatData = useMemo(
|
||||
() =>
|
||||
uniqBy(
|
||||
data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results),
|
||||
"id"
|
||||
),
|
||||
() => data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results),
|
||||
[data]
|
||||
);
|
||||
|
||||
@@ -79,7 +74,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) =>
|
||||
fetchNextPage()
|
||||
}}
|
||||
renderItem={(item) =>
|
||||
<JellyseerrPoster item={item as MovieResult | TvResult} key={item?.id}/>
|
||||
<JellyseerrPoster item={item as MovieResult | TvResult} />
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from "react";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {useJellyseerr} from "@/hooks/useJellyseerr";
|
||||
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
|
||||
import {ViewProps} from "react-native";
|
||||
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
||||
import {NonFunctionProperties} from "@/utils/jellyseerr/server/interfaces/api/common";
|
||||
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
|
||||
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||
|
||||
const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => {
|
||||
const {jellyseerrApi} = useJellyseerr();
|
||||
|
||||
const { data: details, isLoading, isError } = useQuery({
|
||||
queryKey: ["jellyseerr", "detail", request.media.mediaType, request.media.tmdbId],
|
||||
queryFn: async () => {
|
||||
|
||||
return request.media.mediaType == MediaType.MOVIE
|
||||
? jellyseerrApi?.movieDetails(request.media.tmdbId)
|
||||
: jellyseerrApi?.tvDetails(request.media.tmdbId);
|
||||
},
|
||||
enabled: !!jellyseerrApi,
|
||||
refetchOnMount: true,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const { data: refreshedRequest } = useQuery({
|
||||
queryKey: ["jellyseerr", "requests", request.media.mediaType, request.id],
|
||||
queryFn: async () => jellyseerrApi?.getRequest(request.id),
|
||||
enabled: !!jellyseerrApi,
|
||||
refetchOnMount: true,
|
||||
refetchInterval: 5000,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
details && <JellyseerrPoster horizontal showDownloadInfo item={details} mediaRequest={refreshedRequest} />
|
||||
)
|
||||
}
|
||||
|
||||
const RecentRequestsSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||
const {jellyseerrApi} = useJellyseerr();
|
||||
|
||||
const { data: requests, isLoading, isError } = useQuery({
|
||||
queryKey: ["jellyseerr", "recent_requests"],
|
||||
queryFn: async () => jellyseerrApi?.requests(),
|
||||
enabled: !!jellyseerrApi,
|
||||
refetchOnMount: true,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
requests &&
|
||||
requests.results.length > 0 &&
|
||||
!isError && (
|
||||
<Slide
|
||||
{...props}
|
||||
slide={slide}
|
||||
data={requests.results}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={(item: NonFunctionProperties<MediaRequest>) => (
|
||||
<RequestCard request={item}/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
export default RecentRequestsSlide;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {Ionicons, MaterialCommunityIcons} from "@expo/vector-icons";
|
||||
import { PropsWithChildren, ReactNode } from "react";
|
||||
import {
|
||||
TouchableOpacity,
|
||||
@@ -13,7 +13,7 @@ interface Props extends TouchableOpacityProps, ViewProps {
|
||||
value?: string | null | undefined;
|
||||
children?: ReactNode;
|
||||
iconAfter?: ReactNode;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
icon?: keyof typeof Ionicons.glyphMap | keyof typeof MaterialCommunityIcons.glyphMap;
|
||||
showArrow?: boolean;
|
||||
textColor?: "default" | "blue" | "red";
|
||||
onPress?: () => void;
|
||||
@@ -89,7 +89,19 @@ const ListItemContent = ({
|
||||
<View className="flex flex-row items-center w-full">
|
||||
{icon && (
|
||||
<View className="border border-neutral-800 rounded-md h-8 w-8 flex items-center justify-center mr-2">
|
||||
<Ionicons name="person-circle-outline" size={18} color="white" />
|
||||
{icon in Ionicons.glyphMap ?
|
||||
<Ionicons
|
||||
name={icon as keyof typeof Ionicons.glyphMap}
|
||||
size={18}
|
||||
color="white"
|
||||
/>
|
||||
:
|
||||
<MaterialCommunityIcons
|
||||
name={icon as keyof typeof MaterialCommunityIcons.glyphMap}
|
||||
size={18}
|
||||
color="white"
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
)}
|
||||
<Text
|
||||
|
||||
@@ -1,42 +1,28 @@
|
||||
import {TouchableJellyseerrRouter} from "@/components/common/JellyseerrItemRouter";
|
||||
import {Text} from "@/components/common/Text";
|
||||
import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
|
||||
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
|
||||
import {useJellyseerr} from "@/hooks/useJellyseerr";
|
||||
import {useJellyseerrCanRequest} from "@/utils/_jellyseerr/useJellyseerrCanRequest";
|
||||
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||
import {Image} from "expo-image";
|
||||
import {useMemo} from "react";
|
||||
import {View, ViewProps} from "react-native";
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming,} from "react-native-reanimated";
|
||||
import {TvDetails} from "@/utils/jellyseerr/server/models/Tv";
|
||||
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
|
||||
import type {DownloadingItem} from "@/utils/jellyseerr/server/lib/downloadtracker";
|
||||
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {MediaStatus} from "@/utils/jellyseerr/server/constants/media";
|
||||
import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard";
|
||||
import {Colors} from "@/constants/Colors";
|
||||
import {Tags} from "@/components/GenreTags";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
|
||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
||||
import { Image } from "expo-image";
|
||||
import { useMemo } from "react";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
item: MovieResult | TvResult | MovieDetails | TvDetails;
|
||||
horizontal?: boolean;
|
||||
showDownloadInfo?: boolean;
|
||||
mediaRequest?: MediaRequest;
|
||||
item: MovieResult | TvResult;
|
||||
}
|
||||
|
||||
const JellyseerrPoster: React.FC<Props> = ({
|
||||
item,
|
||||
horizontal,
|
||||
showDownloadInfo,
|
||||
mediaRequest,
|
||||
...props
|
||||
}) => {
|
||||
const { jellyseerrApi, getTitle, getYear, getMediaType, isJellyseerrResult } = useJellyseerr();
|
||||
const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
const loadingOpacity = useSharedValue(1);
|
||||
const imageOpacity = useSharedValue(0);
|
||||
const {t} = useTranslation();
|
||||
|
||||
const loadingAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: loadingOpacity.value,
|
||||
@@ -52,64 +38,27 @@ const JellyseerrPoster: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const imageSrc = useMemo(
|
||||
() => jellyseerrApi?.imageProxy(
|
||||
horizontal ? item.backdropPath : item.posterPath,
|
||||
horizontal ? "w1920_and_h800_multi_faces" : "w300_and_h450_face"
|
||||
),
|
||||
[item, jellyseerrApi, horizontal]
|
||||
() => jellyseerrApi?.imageProxy(item.posterPath, "w300_and_h450_face"),
|
||||
[item, jellyseerrApi]
|
||||
);
|
||||
|
||||
const title = useMemo(() => getTitle(item), [item]);
|
||||
const releaseYear = useMemo(() => getYear(item), [item]);
|
||||
const mediaType = useMemo(() => getMediaType(item), [item]);
|
||||
const title = useMemo(
|
||||
() => (item.mediaType === MediaType.MOVIE ? item.title : item.name),
|
||||
[item]
|
||||
);
|
||||
|
||||
const size = useMemo(() => horizontal ? 'h-28' : 'w-28', [horizontal])
|
||||
const ratio = useMemo(() => horizontal ? '15/10' : '10/15', [horizontal])
|
||||
const releaseYear = useMemo(
|
||||
() =>
|
||||
new Date(
|
||||
item.mediaType === MediaType.MOVIE
|
||||
? item.releaseDate
|
||||
: item.firstAirDate
|
||||
).getFullYear(),
|
||||
[item]
|
||||
);
|
||||
|
||||
const [canRequest] = useJellyseerrCanRequest(item);
|
||||
|
||||
const is4k = useMemo(
|
||||
() => mediaRequest?.is4k === true,
|
||||
[mediaRequest]
|
||||
);
|
||||
|
||||
const downloadItems = useMemo(
|
||||
() => (is4k ? mediaRequest?.media.downloadStatus4k : mediaRequest?.media.downloadStatus) || [],
|
||||
[mediaRequest, is4k]
|
||||
)
|
||||
|
||||
const progress = useMemo(() => {
|
||||
const [totalSize, sizeLeft] = downloadItems
|
||||
.reduce((sum: number[], next: DownloadingItem) =>
|
||||
[sum[0] + next.size, sum[1] + next.sizeLeft],
|
||||
[0, 0]
|
||||
);
|
||||
|
||||
return (((totalSize - sizeLeft) / totalSize) * 100);
|
||||
},
|
||||
[downloadItems]
|
||||
);
|
||||
|
||||
const requestedSeasons: string[] | undefined = useMemo(
|
||||
() => {
|
||||
const seasons = mediaRequest?.seasons?.flatMap(s => s.seasonNumber.toString()) || []
|
||||
if (seasons.length > 4) {
|
||||
const [first, second, third, fourth, ...rest] = seasons;
|
||||
return [first, second, third, fourth, t("home.settings.plugins.jellyseerr.plus_n_more", {n: rest.length })]
|
||||
}
|
||||
return seasons
|
||||
},
|
||||
[mediaRequest]
|
||||
);
|
||||
|
||||
const available = useMemo(
|
||||
() => {
|
||||
const status = mediaRequest?.media?.[is4k ? 'status4k' : 'status'];
|
||||
return status === MediaStatus.AVAILABLE
|
||||
},
|
||||
[mediaRequest, is4k]
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableJellyseerrRouter
|
||||
result={item}
|
||||
@@ -117,10 +66,9 @@ const JellyseerrPoster: React.FC<Props> = ({
|
||||
releaseYear={releaseYear}
|
||||
canRequest={canRequest}
|
||||
posterSrc={imageSrc!!}
|
||||
mediaType={mediaType}
|
||||
>
|
||||
<View className={`flex flex-col mr-2 h-auto`}>
|
||||
<View className={`relative rounded-lg overflow-hidden border border-neutral-900 ${size} aspect-[${ratio}]`}>
|
||||
<View className="flex flex-col w-28 mr-2">
|
||||
<View className="relative rounded-lg overflow-hidden border border-neutral-900 w-28 aspect-[10/15]">
|
||||
<Animated.View style={imageAnimatedStyle}>
|
||||
<Image
|
||||
key={item.id}
|
||||
@@ -129,65 +77,26 @@ const JellyseerrPoster: React.FC<Props> = ({
|
||||
cachePolicy={"memory-disk"}
|
||||
contentFit="cover"
|
||||
style={{
|
||||
aspectRatio: ratio,
|
||||
[horizontal ? 'height' : 'width']: "100%"
|
||||
aspectRatio: "10/15",
|
||||
width: "100%",
|
||||
}}
|
||||
onLoad={handleImageLoad}
|
||||
/>
|
||||
</Animated.View>
|
||||
{mediaRequest && showDownloadInfo && (
|
||||
<>
|
||||
<View className={`absolute w-full h-full bg-black ${!available ? 'opacity-70' : 'opacity-0'}`} />
|
||||
{!available && !Number.isNaN(progress) && (
|
||||
<>
|
||||
<View
|
||||
className="absolute left-0 h-full opacity-40"
|
||||
style={{
|
||||
width: `${progress || 0}%`,
|
||||
backgroundColor: Colors.primaryRGB,
|
||||
}}
|
||||
/>
|
||||
<View className="absolute w-full h-full justify-center items-center">
|
||||
<Text
|
||||
className="font-bold"
|
||||
style={textShadowStyle.shadow}
|
||||
>
|
||||
{progress?.toFixed(0)}%
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
<Text
|
||||
className="absolute right-1 top-1 text-right font-bold"
|
||||
style={textShadowStyle.shadow}
|
||||
>
|
||||
{mediaRequest?.requestedBy.displayName}
|
||||
</Text>
|
||||
{requestedSeasons.length > 0 && (
|
||||
<Tags
|
||||
className="absolute bottom-1 left-0.5 w-32"
|
||||
tagProps={{
|
||||
className: "bg-black rounded-full px-1"
|
||||
}}
|
||||
tags={requestedSeasons}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<JellyseerrStatusIcon
|
||||
className="absolute bottom-1 right-1"
|
||||
showRequestIcon={canRequest}
|
||||
mediaStatus={mediaRequest?.media?.status || item?.mediaInfo?.status}
|
||||
mediaStatus={item?.mediaInfo?.status}
|
||||
/>
|
||||
<JellyseerrMediaIcon
|
||||
className="absolute top-1 left-1"
|
||||
mediaType={mediaType}
|
||||
mediaType={item?.mediaType}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className={`mt-2 flex flex-col ${horizontal ? 'w-44' : 'w-28'}`}>
|
||||
<Text numberOfLines={2}>{title}</Text>
|
||||
<Text className="text-xs opacity-50 align-bottom">{releaseYear}</Text>
|
||||
<View className="mt-2 flex flex-col">
|
||||
<Text numberOfLines={2}>{title}</Text>
|
||||
<Text className="text-xs opacity-50 align-bottom">{releaseYear}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableJellyseerrRouter>
|
||||
);
|
||||
|
||||
@@ -128,12 +128,14 @@ const RenderItem = ({ item, index }: any) => {
|
||||
|
||||
const JellyseerrSeasons: React.FC<{
|
||||
isLoading: boolean;
|
||||
result?: TvResult;
|
||||
details?: TvDetails;
|
||||
hasAdvancedRequest?: boolean,
|
||||
onAdvancedRequest?: (data: MediaRequestBody) => void;
|
||||
refetch: (options?: (RefetchOptions | undefined)) => Promise<QueryObserverResult<TvDetails | MovieDetails | undefined, Error>>;
|
||||
}> = ({
|
||||
isLoading,
|
||||
result,
|
||||
details,
|
||||
refetch,
|
||||
hasAdvancedRequest,
|
||||
@@ -193,7 +195,7 @@ const JellyseerrSeasons: React.FC<{
|
||||
return onAdvancedRequest?.(body)
|
||||
}
|
||||
|
||||
requestMedia(details.name, body, refetch);
|
||||
requestMedia(result?.name!!, body, refetch);
|
||||
}
|
||||
}, [jellyseerrApi, seasons, details, hasAdvancedRequest, onAdvancedRequest]);
|
||||
|
||||
@@ -225,7 +227,7 @@ const JellyseerrSeasons: React.FC<{
|
||||
return onAdvancedRequest?.(body)
|
||||
}
|
||||
|
||||
requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch);
|
||||
requestMedia(`${result?.name!!}, Season ${seasonNumber}`, body, refetch);
|
||||
}
|
||||
}, [requestMedia, hasAdvancedRequest, onAdvancedRequest]);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform } from "react-native";
|
||||
import { ScreenOrientationEnum, useSettings, VideoPlayer } from "@/utils/atoms/settings";
|
||||
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
||||
import { BitrateSelector, BITRATES } from "@/components/BitrateSelector";
|
||||
import {
|
||||
BACKGROUND_FETCH_TASK,
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
unregisterBackgroundFetchAsync,
|
||||
} from "@/utils/background-tasks";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null;
|
||||
const BackgroundFetch = !Platform.isTV
|
||||
? require("expo-background-fetch")
|
||||
: null;
|
||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
|
||||
import { useRouter } from "expo-router";
|
||||
@@ -20,7 +22,6 @@ import { ListItem } from "../list/ListItem";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||
import Dropdown from "@/components/common/Dropdown";
|
||||
import { isNumber } from "lodash";
|
||||
|
||||
export const OtherSettings: React.FC = () => {
|
||||
const router = useRouter();
|
||||
@@ -83,7 +84,10 @@ export const OtherSettings: React.FC = () => {
|
||||
return (
|
||||
<DisabledSetting disabled={disabled}>
|
||||
<ListGroup title={t("home.settings.other.other_title")} className="">
|
||||
<ListItem title={t("home.settings.other.auto_rotate")} disabled={pluginSettings?.autoRotate?.locked}>
|
||||
<ListItem
|
||||
title={t("home.settings.other.auto_rotate")}
|
||||
disabled={pluginSettings?.autoRotate?.locked}
|
||||
>
|
||||
<Switch
|
||||
value={settings.autoRotate}
|
||||
disabled={pluginSettings?.autoRotate?.locked}
|
||||
@@ -93,11 +97,17 @@ export const OtherSettings: React.FC = () => {
|
||||
|
||||
<ListItem
|
||||
title={t("home.settings.other.video_orientation")}
|
||||
disabled={pluginSettings?.defaultVideoOrientation?.locked || settings.autoRotate}
|
||||
disabled={
|
||||
pluginSettings?.defaultVideoOrientation?.locked ||
|
||||
settings.autoRotate
|
||||
}
|
||||
>
|
||||
<Dropdown
|
||||
data={orientations}
|
||||
disabled={pluginSettings?.defaultVideoOrientation?.locked || settings.autoRotate}
|
||||
disabled={
|
||||
pluginSettings?.defaultVideoOrientation?.locked ||
|
||||
settings.autoRotate
|
||||
}
|
||||
keyExtractor={String}
|
||||
titleExtractor={(item) => ScreenOrientationEnum[item]}
|
||||
title={
|
||||
@@ -105,11 +115,17 @@ export const OtherSettings: React.FC = () => {
|
||||
<Text className="mr-1 text-[#8E8D91]">
|
||||
{t(ScreenOrientationEnum[settings.defaultVideoOrientation])}
|
||||
</Text>
|
||||
<Ionicons name="chevron-expand-sharp" size={18} color="#5A5960" />
|
||||
<Ionicons
|
||||
name="chevron-expand-sharp"
|
||||
size={18}
|
||||
color="#5A5960"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
label={t("home.settings.other.orientation")}
|
||||
onSelected={(defaultVideoOrientation) => updateSettings({ defaultVideoOrientation })}
|
||||
onSelected={(defaultVideoOrientation) =>
|
||||
updateSettings({ defaultVideoOrientation })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
@@ -120,49 +136,27 @@ export const OtherSettings: React.FC = () => {
|
||||
<Switch
|
||||
value={settings.safeAreaInControlsEnabled}
|
||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||
onValueChange={(value) => updateSettings({ safeAreaInControlsEnabled: value })}
|
||||
onValueChange={(value) =>
|
||||
updateSettings({ safeAreaInControlsEnabled: value })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
{/* {(Platform.OS === "ios" || Platform.isTVOS)&& (
|
||||
<ListItem
|
||||
title={t("home.settings.other.video_player")}
|
||||
disabled={pluginSettings?.defaultPlayer?.locked}
|
||||
>
|
||||
<Dropdown
|
||||
data={Object.values(VideoPlayer).filter(isNumber)}
|
||||
disabled={pluginSettings?.defaultPlayer?.locked}
|
||||
keyExtractor={String}
|
||||
titleExtractor={(item) => t(`home.settings.other.video_players.${VideoPlayer[item]}`)}
|
||||
title={
|
||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||
<Text className="mr-1 text-[#8E8D91]">
|
||||
{t(`home.settings.other.video_players.${VideoPlayer[settings.defaultPlayer]}`)}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="chevron-expand-sharp"
|
||||
size={18}
|
||||
color="#5A5960"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
label={t("home.settings.other.orientation")}
|
||||
onSelected={(defaultPlayer) =>
|
||||
updateSettings({ defaultPlayer })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
)} */}
|
||||
|
||||
<ListItem
|
||||
title={t("home.settings.other.show_custom_menu_links")}
|
||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||
onPress={() => Linking.openURL("https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links")}
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links"
|
||||
)
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
value={settings.showCustomMenuLinks}
|
||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||
onValueChange={(value) => updateSettings({ showCustomMenuLinks: value })}
|
||||
onValueChange={(value) =>
|
||||
updateSettings({ showCustomMenuLinks: value })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
@@ -170,7 +164,10 @@ export const OtherSettings: React.FC = () => {
|
||||
title={t("home.settings.other.hide_libraries")}
|
||||
showArrow
|
||||
/>
|
||||
<ListItem title={t("home.settings.other.default_quality")} disabled={pluginSettings?.defaultBitrate?.locked}>
|
||||
<ListItem
|
||||
title={t("home.settings.other.default_quality")}
|
||||
disabled={pluginSettings?.defaultBitrate?.locked}
|
||||
>
|
||||
<Dropdown
|
||||
data={BITRATES}
|
||||
disabled={pluginSettings?.defaultBitrate?.locked}
|
||||
@@ -179,8 +176,14 @@ export const OtherSettings: React.FC = () => {
|
||||
selected={settings.defaultBitrate}
|
||||
title={
|
||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||
<Text className="mr-1 text-[#8E8D91]">{settings.defaultBitrate?.key}</Text>
|
||||
<Ionicons name="chevron-expand-sharp" size={18} color="#5A5960" />
|
||||
<Text className="mr-1 text-[#8E8D91]">
|
||||
{settings.defaultBitrate?.key}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="chevron-expand-sharp"
|
||||
size={18}
|
||||
color="#5A5960"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
label={t("home.settings.other.default_quality")}
|
||||
@@ -194,7 +197,9 @@ export const OtherSettings: React.FC = () => {
|
||||
<Switch
|
||||
value={settings.disableHapticFeedback}
|
||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||
onValueChange={(disableHapticFeedback) => updateSettings({ disableHapticFeedback })}
|
||||
onValueChange={(disableHapticFeedback) =>
|
||||
updateSettings({ disableHapticFeedback })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { toast } from "sonner-native";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
import { ListItem } from "../list/ListItem";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {Colors} from "@/constants/Colors";
|
||||
|
||||
export const StorageSettings = () => {
|
||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||
@@ -62,7 +61,7 @@ export const StorageSettings = () => {
|
||||
<View
|
||||
style={{
|
||||
width: `${(size.app / size.total) * 100}%`,
|
||||
backgroundColor: Colors.primaryRGB,
|
||||
backgroundColor: "rgb(147 51 234)",
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
@@ -71,7 +70,7 @@ export const StorageSettings = () => {
|
||||
((size.total - size.remaining - size.app) / size.total) *
|
||||
100
|
||||
}%`,
|
||||
backgroundColor: Colors.primaryLightRGB,
|
||||
backgroundColor: "rgb(192 132 252)",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,40 +1,65 @@
|
||||
import {Text} from "@/components/common/Text";
|
||||
import {Loader} from "@/components/Loader";
|
||||
import {useAdjacentItems} from "@/hooks/useAdjacentEpisodes";
|
||||
import {useCreditSkipper} from "@/hooks/useCreditSkipper";
|
||||
import {useHaptic} from "@/hooks/useHaptic";
|
||||
import {useIntroSkipper} from "@/hooks/useIntroSkipper";
|
||||
import {useTrickplay} from "@/hooks/useTrickplay";
|
||||
import {TrackInfo, VlcPlayerViewRef,} from "@/modules/VlcPlayer.types";
|
||||
import {apiAtom} from "@/providers/JellyfinProvider";
|
||||
import {useSettings, VideoPlayer} from "@/utils/atoms/settings";
|
||||
import {getDefaultPlaySettings,} from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||
import {getItemById} from "@/utils/jellyfin/user-library/getItemById";
|
||||
import {writeToLog} from "@/utils/log";
|
||||
import {formatTimeString, msToTicks, secondsToMs, ticksToMs, ticksToSeconds,} from "@/utils/time";
|
||||
import {Ionicons, MaterialIcons} from "@expo/vector-icons";
|
||||
import {BaseItemDto, MediaSourceInfo,} from "@jellyfin/sdk/lib/generated-client";
|
||||
import {Image} from "expo-image";
|
||||
import {useLocalSearchParams, useRouter} from "expo-router";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes";
|
||||
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
|
||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
||||
import {
|
||||
TrackInfo,
|
||||
VlcPlayerViewRef,
|
||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import {
|
||||
getDefaultPlaySettings,
|
||||
previousIndexes,
|
||||
} from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||
import { getItemById } from "@/utils/jellyfin/user-library/getItemById";
|
||||
import { writeToLog } from "@/utils/log";
|
||||
import {
|
||||
formatTimeString,
|
||||
msToTicks,
|
||||
secondsToMs,
|
||||
ticksToMs,
|
||||
ticksToSeconds,
|
||||
} from "@/utils/time";
|
||||
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
||||
import {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import { Image } from "expo-image";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||
import {useAtom} from "jotai";
|
||||
import {debounce} from "lodash";
|
||||
import React, {useCallback, useEffect, useRef, useState} from "react";
|
||||
import {Platform, TouchableOpacity, useWindowDimensions, View,} from "react-native";
|
||||
import {Slider} from "react-native-awesome-slider";
|
||||
import {runOnJS, SharedValue, useAnimatedReaction, useSharedValue,} from "react-native-reanimated";
|
||||
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
||||
import {VideoRef} from "react-native-video";
|
||||
import { useAtom } from "jotai";
|
||||
import { debounce } from "lodash";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
import {
|
||||
runOnJS,
|
||||
SharedValue,
|
||||
useAnimatedReaction,
|
||||
useSharedValue,
|
||||
} from "react-native-reanimated";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { VideoRef } from "react-native-video";
|
||||
import AudioSlider from "./AudioSlider";
|
||||
import BrightnessSlider from "./BrightnessSlider";
|
||||
import {ControlProvider} from "./contexts/ControlContext";
|
||||
import {VideoProvider} from "./contexts/VideoContext";
|
||||
import { ControlProvider } from "./contexts/ControlContext";
|
||||
import { VideoProvider } from "./contexts/VideoContext";
|
||||
import DropdownView from "./dropdown/DropdownView";
|
||||
import {EpisodeList} from "./EpisodeList";
|
||||
import { EpisodeList } from "./EpisodeList";
|
||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||
import SkipButton from "./SkipButton";
|
||||
import {useControlsTimeout} from "./useControlsTimeout";
|
||||
import {VideoTouchOverlay} from "./VideoTouchOverlay";
|
||||
import { useControlsTimeout } from "./useControlsTimeout";
|
||||
import { VideoTouchOverlay } from "./VideoTouchOverlay";
|
||||
|
||||
interface Props {
|
||||
item: BaseItemDto;
|
||||
@@ -55,6 +80,7 @@ interface Props {
|
||||
mediaSource?: MediaSourceInfo | null;
|
||||
seek: (ticks: number) => void;
|
||||
startPictureInPicture: () => Promise<void>;
|
||||
startDiscovery: () => Promise<void>;
|
||||
play: (() => Promise<void>) | (() => void);
|
||||
pause: () => void;
|
||||
getAudioTracks?: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]);
|
||||
@@ -68,32 +94,33 @@ interface Props {
|
||||
const CONTROLS_TIMEOUT = 4000;
|
||||
|
||||
export const Controls: React.FC<Props> = ({
|
||||
item,
|
||||
seek,
|
||||
startPictureInPicture,
|
||||
play,
|
||||
pause,
|
||||
togglePlay,
|
||||
isPlaying,
|
||||
isSeeking,
|
||||
progress,
|
||||
isBuffering,
|
||||
cacheProgress,
|
||||
showControls,
|
||||
setShowControls,
|
||||
ignoreSafeAreas,
|
||||
setIgnoreSafeAreas,
|
||||
mediaSource,
|
||||
isVideoLoaded,
|
||||
getAudioTracks,
|
||||
getSubtitleTracks,
|
||||
setSubtitleURL,
|
||||
setSubtitleTrack,
|
||||
setAudioTrack,
|
||||
offline = false,
|
||||
enableTrickplay = true,
|
||||
isVlc = false,
|
||||
}) => {
|
||||
item,
|
||||
seek,
|
||||
startDiscovery,
|
||||
startPictureInPicture,
|
||||
play,
|
||||
pause,
|
||||
togglePlay,
|
||||
isPlaying,
|
||||
isSeeking,
|
||||
progress,
|
||||
isBuffering,
|
||||
cacheProgress,
|
||||
showControls,
|
||||
setShowControls,
|
||||
ignoreSafeAreas,
|
||||
setIgnoreSafeAreas,
|
||||
mediaSource,
|
||||
isVideoLoaded,
|
||||
getAudioTracks,
|
||||
getSubtitleTracks,
|
||||
setSubtitleURL,
|
||||
setSubtitleTrack,
|
||||
setAudioTrack,
|
||||
offline = false,
|
||||
enableTrickplay = true,
|
||||
isVlc = false,
|
||||
}) => {
|
||||
const [settings] = useSettings();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -469,7 +496,18 @@ export const Controls: React.FC<Props> = ({
|
||||
)}
|
||||
|
||||
<View className="flex flex-row items-center space-x-2 ">
|
||||
{!Platform.isTV && settings.defaultPlayer == VideoPlayer.VLC_4 && (
|
||||
<TouchableOpacity
|
||||
onPress={startDiscovery}
|
||||
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||
>
|
||||
<MaterialIcons
|
||||
name="cast"
|
||||
size={24}
|
||||
color="white"
|
||||
style={{ opacity: showControls ? 1 : 0 }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{!Platform.isTV && (
|
||||
<TouchableOpacity
|
||||
onPress={startPictureInPicture}
|
||||
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TrackInfo } from "@/modules/vlc-player";
|
||||
import {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TrackInfo } from "@/modules/VlcPlayer.types";
|
||||
import { TrackInfo } from "@/modules/vlc-player";
|
||||
import React, { createContext, useContext, useState, ReactNode, useEffect, useMemo } from "react";
|
||||
import { useControlContext } from "./ControlContext";
|
||||
import { Track } from "../types";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
TrackInfo,
|
||||
VlcPlayerViewRef,
|
||||
} from "@/modules/VlcPlayer.types";
|
||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export const Colors = {
|
||||
primary: "#9334E9",
|
||||
primaryRGB: "rgb(147 51 234)",
|
||||
primaryLightRGB: "rgb(192 132 252)",
|
||||
text: "#ECEDEE",
|
||||
background: "#151718",
|
||||
tint: "#fff",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||
import { Results } from "@/utils/jellyseerr/server/models/Search";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import { inRange } from "lodash";
|
||||
import { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User";
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
MediaType,
|
||||
} from "@/utils/jellyseerr/server/constants/media";
|
||||
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
||||
import {MediaRequestBody, RequestResultsResponse} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
||||
import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
||||
import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
||||
import {
|
||||
SeasonWithEpisodes,
|
||||
@@ -227,23 +227,6 @@ export class JellyseerrApi {
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
async getRequest(id: number): Promise<MediaRequest> {
|
||||
return this.axios
|
||||
?.get<MediaRequest>(Endpoints.API_V1 + Endpoints.REQUEST + `/${id}`)
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
async requests(params = {
|
||||
filter: "all",
|
||||
take: 10,
|
||||
sort: "modified",
|
||||
skip: 0
|
||||
}): Promise<RequestResultsResponse> {
|
||||
return this.axios
|
||||
?.get<RequestResultsResponse>(Endpoints.API_V1 + Endpoints.REQUEST, {params})
|
||||
.then(({data}) => data);
|
||||
}
|
||||
|
||||
async movieDetails(id: number) {
|
||||
return this.axios
|
||||
?.get<MovieDetails>(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`)
|
||||
@@ -456,34 +439,14 @@ export const useJellyseerr = () => {
|
||||
);
|
||||
|
||||
const isJellyseerrResult = (
|
||||
items: any | null | undefined
|
||||
): items is Results => {
|
||||
items: any[] | null | undefined
|
||||
): items is Results[] => {
|
||||
return (
|
||||
items &&
|
||||
Object.hasOwn(items, "mediaType") &&
|
||||
Object.values(MediaType).includes(items["mediaType"])
|
||||
)
|
||||
};
|
||||
|
||||
const getTitle = (item: TvResult | TvDetails | MovieResult | MovieDetails) => {
|
||||
return isJellyseerrResult(item)
|
||||
? (item.mediaType == MediaType.MOVIE ? item?.originalTitle : item?.name)
|
||||
: (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name)
|
||||
};
|
||||
|
||||
const getYear = (item: TvResult | TvDetails | MovieResult | MovieDetails) => {
|
||||
return new Date((
|
||||
isJellyseerrResult(item)
|
||||
? (item.mediaType == MediaType.MOVIE ? item?.releaseDate : item?.firstAirDate)
|
||||
: (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.releaseDate : (item as TvDetails)?.firstAirDate))
|
||||
|| ""
|
||||
)?.getFullYear?.()
|
||||
};
|
||||
|
||||
const getMediaType = (item: TvResult | TvDetails | MovieResult | MovieDetails): MediaType => {
|
||||
return isJellyseerrResult(item)
|
||||
? item.mediaType
|
||||
: item?.mediaInfo?.mediaType
|
||||
!items ||
|
||||
(items.length >= 0 &&
|
||||
Object.hasOwn(items[0], "mediaType") &&
|
||||
Object.values(MediaType).includes(items[0]["mediaType"]))
|
||||
);
|
||||
};
|
||||
|
||||
const jellyseerrRegion = useMemo(
|
||||
@@ -501,9 +464,6 @@ export const useJellyseerr = () => {
|
||||
setJellyseerrUser,
|
||||
clearAllJellyseerData,
|
||||
isJellyseerrResult,
|
||||
getTitle,
|
||||
getYear,
|
||||
getMediaType,
|
||||
jellyseerrRegion,
|
||||
jellyseerrLocale,
|
||||
requestMedia,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import VlcPlayerView from "./VlcPlayerView";
|
||||
import {
|
||||
PlaybackStatePayload,
|
||||
ProgressUpdatePayload,
|
||||
VideoLoadStartPayload,
|
||||
VideoStateChangePayload,
|
||||
VideoProgressPayload,
|
||||
VlcPlayerSource,
|
||||
TrackInfo,
|
||||
ChapterInfo,
|
||||
VlcPlayerViewProps,
|
||||
VlcPlayerViewRef,
|
||||
} from "./VlcPlayer.types";
|
||||
|
||||
export {
|
||||
VlcPlayerView,
|
||||
VlcPlayerViewProps,
|
||||
VlcPlayerViewRef,
|
||||
PlaybackStatePayload,
|
||||
ProgressUpdatePayload,
|
||||
VideoLoadStartPayload,
|
||||
VideoStateChangePayload,
|
||||
VideoProgressPayload,
|
||||
VlcPlayerSource,
|
||||
TrackInfo,
|
||||
ChapterInfo,
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"platforms": ["ios", "tvos"],
|
||||
"ios": {
|
||||
"modules": ["VlcPlayer3Module"]
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'VlcPlayer3'
|
||||
s.version = '3.6.1b1'
|
||||
s.summary = 'A sample project summary'
|
||||
s.description = 'A sample project description'
|
||||
s.author = ''
|
||||
s.homepage = 'https://docs.expo.dev/modules/'
|
||||
s.platforms = { :ios => '13.4', :tvos => '13.4' }
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
s.ios.dependency 'MobileVLCKit', s.version
|
||||
s.tvos.dependency 'TVVLCKit', s.version
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
|
||||
s.source_files = "*.{h,m,mm,swift,hpp,cpp}"
|
||||
end
|
||||
@@ -1,71 +0,0 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class VlcPlayer3Module: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("VlcPlayer3")
|
||||
View(VlcPlayer3View.self) {
|
||||
Prop("source") { (view: VlcPlayer3View, source: [String: Any]) in
|
||||
view.setSource(source)
|
||||
}
|
||||
|
||||
Prop("paused") { (view: VlcPlayer3View, paused: Bool) in
|
||||
if paused {
|
||||
view.pause()
|
||||
} else {
|
||||
view.play()
|
||||
}
|
||||
}
|
||||
|
||||
Events(
|
||||
"onPlaybackStateChanged",
|
||||
"onVideoStateChange",
|
||||
"onVideoLoadStart",
|
||||
"onVideoLoadEnd",
|
||||
"onVideoProgress",
|
||||
"onVideoError",
|
||||
"onPipStarted"
|
||||
)
|
||||
|
||||
AsyncFunction("startPictureInPicture") { (view: VlcPlayer3View) in
|
||||
view.startPictureInPicture()
|
||||
}
|
||||
|
||||
AsyncFunction("play") { (view: VlcPlayer3View) in
|
||||
view.play()
|
||||
}
|
||||
|
||||
AsyncFunction("pause") { (view: VlcPlayer3View) in
|
||||
view.pause()
|
||||
}
|
||||
|
||||
AsyncFunction("stop") { (view: VlcPlayer3View) in
|
||||
view.stop()
|
||||
}
|
||||
|
||||
AsyncFunction("seekTo") { (view: VlcPlayer3View, time: Int32) in
|
||||
view.seekTo(time)
|
||||
}
|
||||
|
||||
AsyncFunction("setAudioTrack") { (view: VlcPlayer3View, trackIndex: Int) in
|
||||
view.setAudioTrack(trackIndex)
|
||||
}
|
||||
|
||||
AsyncFunction("getAudioTracks") { (view: VlcPlayer3View) -> [[String: Any]]? in
|
||||
return view.getAudioTracks()
|
||||
}
|
||||
|
||||
AsyncFunction("setSubtitleTrack") { (view: VlcPlayer3View, trackIndex: Int) in
|
||||
view.setSubtitleTrack(trackIndex)
|
||||
}
|
||||
|
||||
AsyncFunction("getSubtitleTracks") { (view: VlcPlayer3View) -> [[String: Any]]? in
|
||||
return view.getSubtitleTracks()
|
||||
}
|
||||
|
||||
AsyncFunction("setSubtitleURL") {
|
||||
(view: VlcPlayer3View, url: String, name: String) in
|
||||
view.setSubtitleURL(url, name: name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
import ExpoModulesCore
|
||||
#if os(tvOS)
|
||||
import TVVLCKit
|
||||
#else
|
||||
import MobileVLCKit
|
||||
#endif
|
||||
import UIKit
|
||||
|
||||
class VlcPlayer3View: ExpoView {
|
||||
private var mediaPlayer: VLCMediaPlayer?
|
||||
private var videoView: UIView?
|
||||
private var progressUpdateInterval: TimeInterval = 1.0 // Update interval set to 1 second
|
||||
private var isPaused: Bool = false
|
||||
private var currentGeometryCString: [CChar]?
|
||||
private var lastReportedState: VLCMediaPlayerState?
|
||||
private var lastReportedIsPlaying: Bool?
|
||||
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
||||
private var startPosition: Int32 = 0
|
||||
private var isMediaReady: Bool = false
|
||||
private var externalTrack: [String: String]?
|
||||
private var progressTimer: DispatchSourceTimer?
|
||||
private var isStopping: Bool = false // Define isStopping here
|
||||
private var lastProgressCall = Date().timeIntervalSince1970
|
||||
var hasSource = false
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
setupView()
|
||||
setupNotifications()
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupView() {
|
||||
DispatchQueue.main.async {
|
||||
self.backgroundColor = .black
|
||||
self.videoView = UIView()
|
||||
self.videoView?.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
if let videoView = self.videoView {
|
||||
self.addSubview(videoView)
|
||||
NSLayoutConstraint.activate([
|
||||
videoView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
videoView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
videoView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
videoView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNotifications() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(applicationWillResignActive),
|
||||
name: UIApplication.willResignActiveNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(applicationDidBecomeActive),
|
||||
name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
func startPictureInPicture() { }
|
||||
|
||||
@objc func play() {
|
||||
self.mediaPlayer?.play()
|
||||
self.isPaused = false
|
||||
print("Play")
|
||||
}
|
||||
|
||||
@objc func pause() {
|
||||
self.mediaPlayer?.pause()
|
||||
self.isPaused = true
|
||||
}
|
||||
|
||||
@objc func seekTo(_ time: Int32) {
|
||||
guard let player = self.mediaPlayer else { return }
|
||||
|
||||
let wasPlaying = player.isPlaying
|
||||
if wasPlaying {
|
||||
self.pause()
|
||||
}
|
||||
|
||||
if let duration = player.media?.length.intValue {
|
||||
print("Seeking to time: \(time) Video Duration \(duration)")
|
||||
|
||||
// If the specified time is greater than the duration, seek to the end
|
||||
let seekTime = time > duration ? duration - 1000 : time
|
||||
player.time = VLCTime(int: seekTime)
|
||||
|
||||
if wasPlaying {
|
||||
self.play()
|
||||
}
|
||||
self.updatePlayerState()
|
||||
} else {
|
||||
print("Error: Unable to retrieve video duration")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setSource(_ source: [String: Any]) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.hasSource {
|
||||
return
|
||||
}
|
||||
|
||||
let mediaOptions = source["mediaOptions"] as? [String: Any] ?? [:]
|
||||
self.externalTrack = source["externalTrack"] as? [String: String]
|
||||
var initOptions = source["initOptions"] as? [Any] ?? []
|
||||
self.startPosition = source["startPosition"] as? Int32 ?? 0
|
||||
initOptions.append("--start-time=\(self.startPosition)")
|
||||
|
||||
guard let uri = source["uri"] as? String, !uri.isEmpty else {
|
||||
print("Error: Invalid or empty URI")
|
||||
self.onVideoError?(["error": "Invalid or empty URI"])
|
||||
return
|
||||
}
|
||||
|
||||
let autoplay = source["autoplay"] as? Bool ?? false
|
||||
let isNetwork = source["isNetwork"] as? Bool ?? false
|
||||
|
||||
self.onVideoLoadStart?(["target": self.reactTag ?? NSNull()])
|
||||
self.mediaPlayer = VLCMediaPlayer(options: initOptions)
|
||||
self.mediaPlayer?.delegate = self
|
||||
self.mediaPlayer?.drawable = self.videoView
|
||||
self.mediaPlayer?.scaleFactor = 0
|
||||
|
||||
let media: VLCMedia
|
||||
if isNetwork {
|
||||
print("Loading network file: \(uri)")
|
||||
media = VLCMedia(url: URL(string: uri)!)
|
||||
} else {
|
||||
print("Loading local file: \(uri)")
|
||||
if uri.starts(with: "file://"), let url = URL(string: uri) {
|
||||
media = VLCMedia(url: url)
|
||||
} else {
|
||||
media = VLCMedia(path: uri)
|
||||
}
|
||||
}
|
||||
|
||||
print("Debug: Media options: \(mediaOptions)")
|
||||
media.addOptions(mediaOptions)
|
||||
|
||||
self.mediaPlayer?.media = media
|
||||
self.hasSource = true
|
||||
|
||||
if autoplay {
|
||||
print("Playing...")
|
||||
self.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setAudioTrack(_ trackIndex: Int) {
|
||||
self.mediaPlayer?.currentAudioTrackIndex = Int32(trackIndex)
|
||||
}
|
||||
|
||||
@objc func getAudioTracks() -> [[String: Any]]? {
|
||||
guard let trackNames = mediaPlayer?.audioTrackNames,
|
||||
let trackIndexes = mediaPlayer?.audioTrackIndexes
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return zip(trackNames, trackIndexes).map { name, index in
|
||||
return ["name": name, "index": index]
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setSubtitleTrack(_ trackIndex: Int) {
|
||||
print("Debug: Attempting to set subtitle track to index: \(trackIndex)")
|
||||
self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex)
|
||||
print(
|
||||
"Debug: Current subtitle track index after setting: \(self.mediaPlayer?.currentVideoSubTitleIndex ?? -1)"
|
||||
)
|
||||
}
|
||||
|
||||
@objc func setSubtitleURL(_ subtitleURL: String, name: String) {
|
||||
guard let url = URL(string: subtitleURL) else {
|
||||
print("Error: Invalid subtitle URL")
|
||||
return
|
||||
}
|
||||
|
||||
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
||||
if let result = result {
|
||||
let internalName = "Track \(self.customSubtitles.count + 1)"
|
||||
print("Subtitle added with result: \(result) \(internalName)")
|
||||
self.customSubtitles.append((internalName: internalName, originalName: name))
|
||||
} else {
|
||||
print("Failed to add subtitle")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getSubtitleTracks() -> [[String: Any]]? {
|
||||
guard let mediaPlayer = self.mediaPlayer else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let count = mediaPlayer.numberOfSubtitlesTracks
|
||||
print("Debug: Number of subtitle tracks: \(count)")
|
||||
|
||||
guard count > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tracks: [[String: Any]] = []
|
||||
|
||||
if let names = mediaPlayer.videoSubTitlesNames as? [String],
|
||||
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber]
|
||||
{
|
||||
for (index, name) in zip(indexes, names) {
|
||||
if let customSubtitle = customSubtitles.first(where: { $0.internalName == name }) {
|
||||
tracks.append(["name": customSubtitle.originalName, "index": index.intValue])
|
||||
} else {
|
||||
tracks.append(["name": name, "index": index.intValue])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("Debug: Subtitle tracks: \(tracks)")
|
||||
return tracks
|
||||
}
|
||||
|
||||
@objc func stop(completion: (() -> Void)? = nil) {
|
||||
guard !isStopping else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
isStopping = true
|
||||
|
||||
// If we're not on the main thread, dispatch to main thread
|
||||
if !Thread.isMainThread {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.performStop(completion: completion)
|
||||
}
|
||||
} else {
|
||||
performStop(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
@objc private func applicationWillResignActive() {
|
||||
|
||||
}
|
||||
|
||||
@objc private func applicationDidBecomeActive() {
|
||||
|
||||
}
|
||||
|
||||
private func performStop(completion: (() -> Void)? = nil) {
|
||||
// Stop the media player
|
||||
mediaPlayer?.stop()
|
||||
|
||||
// Remove observer
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
// Clear the video view
|
||||
videoView?.removeFromSuperview()
|
||||
videoView = nil
|
||||
|
||||
// Release the media player
|
||||
mediaPlayer?.delegate = nil
|
||||
mediaPlayer = nil
|
||||
|
||||
isStopping = false
|
||||
completion?()
|
||||
}
|
||||
|
||||
private func updateVideoProgress() {
|
||||
guard let player = self.mediaPlayer else { return }
|
||||
|
||||
let currentTimeMs = player.time.intValue
|
||||
let durationMs = player.media?.length.intValue ?? 0
|
||||
|
||||
print("Debug: Current time: \(currentTimeMs)")
|
||||
if currentTimeMs >= 0 && currentTimeMs < durationMs {
|
||||
if player.isPlaying && !self.isMediaReady {
|
||||
self.isMediaReady = true
|
||||
// Set external track subtitle when starting.
|
||||
if let externalTrack = self.externalTrack {
|
||||
if let name = externalTrack["name"], !name.isEmpty {
|
||||
let deliveryUrl = externalTrack["DeliveryUrl"] ?? ""
|
||||
self.setSubtitleURL(deliveryUrl, name: name)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.onVideoProgress?([
|
||||
"currentTime": currentTimeMs,
|
||||
"duration": durationMs,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Expo Events
|
||||
|
||||
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||
@objc var onVideoStateChange: RCTDirectEventBlock?
|
||||
@objc var onVideoProgress: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadEnd: RCTDirectEventBlock?
|
||||
@objc var onVideoError: RCTDirectEventBlock?
|
||||
@objc var onPipStarted: RCTDirectEventBlock?
|
||||
|
||||
// MARK: - Deinitialization
|
||||
|
||||
deinit {
|
||||
performStop()
|
||||
}
|
||||
}
|
||||
|
||||
extension VlcPlayer3View: VLCMediaPlayerDelegate {
|
||||
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
||||
// self?.updateVideoProgress()
|
||||
let timeNow = Date().timeIntervalSince1970
|
||||
if timeNow - lastProgressCall >= 1 {
|
||||
lastProgressCall = timeNow
|
||||
updateVideoProgress()
|
||||
}
|
||||
}
|
||||
|
||||
func mediaPlayerStateChanged(_ aNotification: Notification) {
|
||||
self.updatePlayerState()
|
||||
}
|
||||
|
||||
private func updatePlayerState() {
|
||||
guard let player = self.mediaPlayer else { return }
|
||||
let currentState = player.state
|
||||
|
||||
var stateInfo: [String: Any] = [
|
||||
"target": self.reactTag ?? NSNull(),
|
||||
"currentTime": player.time.intValue,
|
||||
"duration": player.media?.length.intValue ?? 0,
|
||||
"error": false,
|
||||
]
|
||||
|
||||
if player.isPlaying {
|
||||
stateInfo["isPlaying"] = true
|
||||
stateInfo["isBuffering"] = false
|
||||
stateInfo["state"] = "Playing"
|
||||
} else {
|
||||
stateInfo["isPlaying"] = false
|
||||
stateInfo["state"] = "Paused"
|
||||
}
|
||||
|
||||
if player.state == VLCMediaPlayerState.buffering {
|
||||
stateInfo["isBuffering"] = true
|
||||
stateInfo["state"] = "Buffering"
|
||||
} else if player.state == VLCMediaPlayerState.error {
|
||||
print("player.state ~ error")
|
||||
stateInfo["state"] = "Error"
|
||||
self.onVideoLoadEnd?(stateInfo)
|
||||
} else if player.state == VLCMediaPlayerState.opening {
|
||||
print("player.state ~ opening")
|
||||
stateInfo["state"] = "Opening"
|
||||
}
|
||||
|
||||
if self.lastReportedState != currentState
|
||||
|| self.lastReportedIsPlaying != player.isPlaying
|
||||
{
|
||||
self.lastReportedState = currentState
|
||||
self.lastReportedIsPlaying = player.isPlaying
|
||||
self.onVideoStateChange?(stateInfo)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension VlcPlayer3View: VLCMediaDelegate {
|
||||
// Implement VLCMediaDelegate methods if needed
|
||||
}
|
||||
|
||||
extension VLCMediaPlayerState {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .opening: return "Opening"
|
||||
case .buffering: return "Buffering"
|
||||
case .playing: return "Playing"
|
||||
case .paused: return "Paused"
|
||||
case .stopped: return "Stopped"
|
||||
case .ended: return "Ended"
|
||||
case .error: return "Error"
|
||||
case .esAdded: return "ESAdded"
|
||||
@unknown default: return "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { requireNativeModule } from 'expo-modules-core';
|
||||
|
||||
// It loads the native module object from the JSI or falls back to
|
||||
// the bridge module (from NativeModulesProxy) if the remote debugger is on.
|
||||
export default requireNativeModule('VlcPlayer3');
|
||||
BIN
modules/vlc-player/android/.gradle/8.9/checksums/checksums.lock
Normal file
BIN
modules/vlc-player/android/.gradle/8.9/checksums/checksums.lock
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
#Sun Nov 17 18:25:45 AEDT 2024
|
||||
gradle.version=8.9
|
||||
68
modules/vlc-player/index.ts
Normal file
68
modules/vlc-player/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
EventEmitter,
|
||||
EventSubscription,
|
||||
} from "expo-modules-core";
|
||||
|
||||
import VlcPlayerModule from "./src/VlcPlayerModule";
|
||||
import VlcPlayerView from "./src/VlcPlayerView";
|
||||
import {
|
||||
PlaybackStatePayload,
|
||||
ProgressUpdatePayload,
|
||||
VideoLoadStartPayload,
|
||||
VideoStateChangePayload,
|
||||
VideoProgressPayload,
|
||||
VlcPlayerSource,
|
||||
TrackInfo,
|
||||
ChapterInfo,
|
||||
VlcPlayerViewProps,
|
||||
VlcPlayerViewRef,
|
||||
} from "./src/VlcPlayer.types";
|
||||
|
||||
const emitter = new EventEmitter(VlcPlayerModule);
|
||||
|
||||
export function addPlaybackStateListener(
|
||||
listener: (event: PlaybackStatePayload) => void
|
||||
): EventSubscription {
|
||||
return emitter.addListener<PlaybackStatePayload>(
|
||||
"onPlaybackStateChanged",
|
||||
listener
|
||||
);
|
||||
}
|
||||
|
||||
export function addVideoLoadStartListener(
|
||||
listener: (event: VideoLoadStartPayload) => void
|
||||
): EventSubscription {
|
||||
return emitter.addListener<VideoLoadStartPayload>(
|
||||
"onVideoLoadStart",
|
||||
listener
|
||||
);
|
||||
}
|
||||
|
||||
export function addVideoStateChangeListener(
|
||||
listener: (event: VideoStateChangePayload) => void
|
||||
): EventSubscription {
|
||||
return emitter.addListener<VideoStateChangePayload>(
|
||||
"onVideoStateChange",
|
||||
listener
|
||||
);
|
||||
}
|
||||
|
||||
export function addVideoProgressListener(
|
||||
listener: (event: VideoProgressPayload) => void
|
||||
): EventSubscription {
|
||||
return emitter.addListener<VideoProgressPayload>("onVideoProgress", listener);
|
||||
}
|
||||
|
||||
export {
|
||||
VlcPlayerView,
|
||||
VlcPlayerViewProps,
|
||||
VlcPlayerViewRef,
|
||||
PlaybackStatePayload,
|
||||
ProgressUpdatePayload,
|
||||
VideoLoadStartPayload,
|
||||
VideoStateChangePayload,
|
||||
VideoProgressPayload,
|
||||
VlcPlayerSource,
|
||||
TrackInfo,
|
||||
ChapterInfo,
|
||||
};
|
||||
@@ -19,5 +19,6 @@ Pod::Spec.new do |s|
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
s.source_files = "*.{h,m,mm,swift,hpp,cpp}"
|
||||
|
||||
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
||||
end
|
||||
|
||||
@@ -23,7 +23,8 @@ public class VlcPlayerModule: Module {
|
||||
"onVideoLoadEnd",
|
||||
"onVideoProgress",
|
||||
"onVideoError",
|
||||
"onPipStarted"
|
||||
"onPipStarted",
|
||||
"onDiscoveryStateChanged"
|
||||
)
|
||||
|
||||
AsyncFunction("startPictureInPicture") { (view: VlcPlayerView) in
|
||||
@@ -42,6 +43,14 @@ public class VlcPlayerModule: Module {
|
||||
view.stop()
|
||||
}
|
||||
|
||||
AsyncFunction("startDiscovery") { (view: VlcPlayerView) in
|
||||
view.startDiscovery()
|
||||
}
|
||||
|
||||
AsyncFunction("stopDiscovery") { (view: VlcPlayerView) in
|
||||
view.stopDiscovery()
|
||||
}
|
||||
|
||||
AsyncFunction("seekTo") { (view: VlcPlayerView, time: Int32) in
|
||||
view.seekTo(time)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import UIKit
|
||||
import VLCKit
|
||||
import os
|
||||
|
||||
|
||||
public class VLCPlayerView: UIView {
|
||||
func setupView(parent: UIView) {
|
||||
self.backgroundColor = .black
|
||||
@@ -25,11 +26,15 @@ public class VLCPlayerView: UIView {
|
||||
}
|
||||
|
||||
class VLCPlayerWrapper: NSObject {
|
||||
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VLCPlayerWrapper")
|
||||
|
||||
private var lastProgressCall = Date().timeIntervalSince1970
|
||||
public var player: VLCMediaPlayer = VLCMediaPlayer()
|
||||
private var updatePlayerState: (() -> Void)?
|
||||
private var updateVideoProgress: (() -> Void)?
|
||||
private var onDiscoveryStateChanged: ((_ renderers: [[String : Any]]) -> Void)?
|
||||
private var playerView: VLCPlayerView = VLCPlayerView()
|
||||
public var discoverer: VLCRendererDiscoverer?
|
||||
public weak var pipController: VLCPictureInPictureWindowControlling?
|
||||
|
||||
override public init() {
|
||||
@@ -37,15 +42,22 @@ class VLCPlayerWrapper: NSObject {
|
||||
player.delegate = self
|
||||
player.drawable = self
|
||||
player.scaleFactor = 0
|
||||
#if DEBUG
|
||||
let consoleLogger = VLCConsoleLogger()
|
||||
consoleLogger.level = VLCLogLevel.debug
|
||||
player.libraryInstance.loggers = [consoleLogger]
|
||||
#endif
|
||||
}
|
||||
|
||||
public func setup(
|
||||
parent: UIView,
|
||||
updatePlayerState: (() -> Void)?,
|
||||
updateVideoProgress: (() -> Void)?
|
||||
updateVideoProgress: (() -> Void)?,
|
||||
onDiscoveryStateChanged: ((_ renderers: [[String : Any]]) -> Void)?
|
||||
) {
|
||||
self.updatePlayerState = updatePlayerState
|
||||
self.updateVideoProgress = updateVideoProgress
|
||||
self.onDiscoveryStateChanged = onDiscoveryStateChanged
|
||||
|
||||
player.delegate = self
|
||||
parent.addSubview(playerView)
|
||||
@@ -55,6 +67,50 @@ class VLCPlayerWrapper: NSObject {
|
||||
public func getPlayerView() -> UIView {
|
||||
return playerView
|
||||
}
|
||||
|
||||
public func startDiscovery() {
|
||||
if self.discoverer != nil {
|
||||
self.discoverer!.stop()
|
||||
self.discoverer!.start()
|
||||
return
|
||||
}
|
||||
let _discoverer = VLCRendererDiscoverer(name: "bonjour renderer")
|
||||
_discoverer!.delegate = self
|
||||
|
||||
self.discoverer = _discoverer
|
||||
self.discoverer?.start()
|
||||
}
|
||||
|
||||
public func stopDiscovery() {
|
||||
guard let discoverer = self.discoverer else { return }
|
||||
discoverer.stop()
|
||||
}
|
||||
}
|
||||
|
||||
extension VLCPlayerWrapper: VLCRendererDiscovererDelegate {
|
||||
func rendererDiscovererItemAdded(_ rendererDiscoverer: VLCRendererDiscoverer?, item: VLCRendererItem?) {
|
||||
logger.debug("Renderer item added: \(item)")
|
||||
self.onDiscoveryStateChanged?(getRenderersMap(rendererDiscoverer: rendererDiscoverer))
|
||||
}
|
||||
|
||||
func rendererDiscovererItemDeleted(_ rendererDiscoverer: VLCRendererDiscoverer?, item: VLCRendererItem?) {
|
||||
logger.debug("Renderer item removed: \(item)")
|
||||
self.onDiscoveryStateChanged?(getRenderersMap(rendererDiscoverer: rendererDiscoverer))
|
||||
}
|
||||
|
||||
private func getRenderersMap(rendererDiscoverer: VLCRendererDiscoverer?) -> [[String : Any]] {
|
||||
let renderers = (rendererDiscoverer ?? discoverer)?.renderers.enumerated().map { (index, rendererItem) in
|
||||
return [
|
||||
"index": index,
|
||||
"name": rendererItem.name,
|
||||
"type": rendererItem.type,
|
||||
"iconURI": rendererItem.iconURI,
|
||||
"flags": rendererItem.flags
|
||||
]
|
||||
} ?? []
|
||||
logger.debug("Renderers mapped to: \(renderers)")
|
||||
return renderers
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - VLCPictureInPictureDrawable
|
||||
@@ -155,6 +211,16 @@ class VlcPlayerView: ExpoView {
|
||||
private var externalSubtitles: [[String: String]]?
|
||||
var hasSource = false
|
||||
|
||||
// MARK: - Expo Events
|
||||
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||
@objc var onVideoStateChange: RCTDirectEventBlock?
|
||||
@objc var onVideoProgress: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadEnd: RCTDirectEventBlock?
|
||||
@objc var onVideoError: RCTDirectEventBlock?
|
||||
@objc var onPipStarted: RCTDirectEventBlock?
|
||||
@objc var onDiscoveryStateChanged: RCTDirectEventBlock?
|
||||
|
||||
// MARK: - Initialization
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
@@ -168,10 +234,15 @@ class VlcPlayerView: ExpoView {
|
||||
vlc.setup(
|
||||
parent: self,
|
||||
updatePlayerState: updatePlayerState,
|
||||
updateVideoProgress: updateVideoProgress
|
||||
updateVideoProgress: updateVideoProgress,
|
||||
onDiscoveryStateChanged: updateDiscoveryState
|
||||
)
|
||||
}
|
||||
|
||||
private func updateDiscoveryState(renderers: [[String: Any]]) {
|
||||
self.onDiscoveryStateChanged?(["renderers": renderers])
|
||||
}
|
||||
|
||||
private func setupNotifications() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(applicationWillResignActive),
|
||||
@@ -189,6 +260,19 @@ class VlcPlayerView: ExpoView {
|
||||
self.vlc.pipController?.startPictureInPicture()
|
||||
}
|
||||
|
||||
func startDiscovery() {
|
||||
logger.debug("Starting Discovery")
|
||||
self.vlc.startDiscovery()
|
||||
if self.vlc.discoverer != nil {
|
||||
logger.debug("Discoverer description: \(self.vlc.discoverer!.description)")
|
||||
logger.debug("Discoverer renderer: \(self.vlc.discoverer!.renderers)")
|
||||
}
|
||||
}
|
||||
|
||||
func stopDiscovery() {
|
||||
self.vlc.stopDiscovery()
|
||||
}
|
||||
|
||||
@objc func play() {
|
||||
self.vlc.player.play()
|
||||
self.isPaused = false
|
||||
@@ -239,12 +323,6 @@ class VlcPlayerView: ExpoView {
|
||||
self.startPosition = source["startPosition"] as? Int32 ?? 0
|
||||
self.externalSubtitles = source["externalSubtitles"] as? [[String: String]]
|
||||
|
||||
for item in initOptions {
|
||||
let option = item.components(separatedBy: "=")
|
||||
mediaOptions.updateValue(
|
||||
option[1], forKey: option[0].replacingOccurrences(of: "--", with: ""))
|
||||
}
|
||||
|
||||
guard let uri = source["uri"] as? String, !uri.isEmpty else {
|
||||
logger.error("Invalid or empty URI")
|
||||
self.onVideoError?(["error": "Invalid or empty URI"])
|
||||
@@ -269,6 +347,20 @@ class VlcPlayerView: ExpoView {
|
||||
}
|
||||
}
|
||||
|
||||
for item in initOptions {
|
||||
let option = item.components(separatedBy: "=")
|
||||
var key = option[0].replacingOccurrences(of: "--", with: "")
|
||||
if option.count > 1 {
|
||||
mediaOptions.updateValue(
|
||||
option[1],
|
||||
forKey: key
|
||||
)
|
||||
}
|
||||
else {
|
||||
media.addOption(key)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Media options: \(mediaOptions)")
|
||||
media.addOptions(mediaOptions)
|
||||
|
||||
@@ -401,7 +493,7 @@ class VlcPlayerView: ExpoView {
|
||||
}
|
||||
|
||||
private func updateVideoProgress() {
|
||||
guard self.vlc.player.media != nil else { return }
|
||||
guard let media = self.vlc.player.media else { return }
|
||||
|
||||
let currentTimeMs = self.vlc.player.time.intValue
|
||||
let durationMs = self.vlc.player.media?.length.intValue ?? 0
|
||||
@@ -426,15 +518,6 @@ class VlcPlayerView: ExpoView {
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Expo Events
|
||||
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||
@objc var onVideoStateChange: RCTDirectEventBlock?
|
||||
@objc var onVideoProgress: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadEnd: RCTDirectEventBlock?
|
||||
@objc var onVideoError: RCTDirectEventBlock?
|
||||
@objc var onPipStarted: RCTDirectEventBlock?
|
||||
|
||||
// MARK: - Deinitialization
|
||||
|
||||
deinit {
|
||||
@@ -458,7 +541,7 @@ extension VlcPlayerView: SimpleAppLifecycleListener {
|
||||
}
|
||||
|
||||
// Current solution to fixing black screen when re-entering application
|
||||
if let videoTrack = self.vlc.player.videoTracks.first(where: { $0.isSelected == true }),
|
||||
if let videoTrack = self.vlc.player.videoTracks.first { $0.isSelected == true },
|
||||
!self.vlc.isMediaPlaying()
|
||||
{
|
||||
videoTrack.isSelected = false
|
||||
@@ -478,7 +561,6 @@ extension VLCMediaPlayerState {
|
||||
case .paused: return "Paused"
|
||||
case .stopped: return "Stopped"
|
||||
case .error: return "Error"
|
||||
case .stopping: return "Stopping"
|
||||
@unknown default: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,18 @@ export type PipStartedPayload = {
|
||||
};
|
||||
};
|
||||
|
||||
export type VLCRendererItem = {
|
||||
index: number,
|
||||
name: string,
|
||||
type: string,
|
||||
iconURI: string,
|
||||
flags: number
|
||||
}
|
||||
|
||||
export type OnDiscoveryStateChangedPayload = {
|
||||
nativeEvent: { renderers: VLCRendererItem[] }
|
||||
}
|
||||
|
||||
export type VideoStateChangePayload = PlaybackStatePayload;
|
||||
|
||||
export type VideoProgressPayload = ProgressUpdatePayload;
|
||||
@@ -71,9 +83,12 @@ export type VlcPlayerViewProps = {
|
||||
onVideoLoadEnd?: (event: VideoLoadStartPayload) => void;
|
||||
onVideoError?: (event: PlaybackStatePayload) => void;
|
||||
onPipStarted?: (event: PipStartedPayload) => void;
|
||||
onDiscoveryStateChanged?: (event: OnDiscoveryStateChangedPayload) => void;
|
||||
};
|
||||
|
||||
export interface VlcPlayerViewRef {
|
||||
startDiscovery: () => Promise<void>;
|
||||
stopDiscovery: () => Promise<void>;
|
||||
startPictureInPicture: () => Promise<void>;
|
||||
play: () => Promise<void>;
|
||||
pause: () => Promise<void>;
|
||||
@@ -6,30 +6,16 @@ import {
|
||||
VlcPlayerViewRef,
|
||||
VlcPlayerSource,
|
||||
} from "./VlcPlayer.types";
|
||||
import {useSettings, VideoPlayer} from "@/utils/atoms/settings";
|
||||
import {Platform} from "react-native";
|
||||
|
||||
interface NativeViewRef extends VlcPlayerViewRef {
|
||||
setNativeProps?: (props: Partial<VlcPlayerViewProps>) => void;
|
||||
}
|
||||
|
||||
const VLCViewManager = requireNativeViewManager("VlcPlayer");
|
||||
const VLC3ViewManager = requireNativeViewManager("VlcPlayer3");
|
||||
const NativeViewManager = requireNativeViewManager("VlcPlayer");
|
||||
|
||||
// Create a forwarded ref version of the native view
|
||||
const NativeView = React.forwardRef<NativeViewRef, VlcPlayerViewProps>(
|
||||
(props, ref) => {
|
||||
const [settings] = useSettings();
|
||||
|
||||
if (Platform.OS === "ios" || Platform.isTVOS) {
|
||||
if (settings.defaultPlayer == VideoPlayer.VLC_3) {
|
||||
console.log("[Apple] Using Vlc Player 3")
|
||||
return <VLC3ViewManager {...props} ref={ref}/>
|
||||
}
|
||||
}
|
||||
console.log("Using default Vlc Player")
|
||||
return <VLCViewManager {...props} ref={ref}/>
|
||||
}
|
||||
(props, ref) => <NativeViewManager {...props} ref={ref} />
|
||||
);
|
||||
|
||||
const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
@@ -37,6 +23,12 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
const nativeRef = React.useRef<NativeViewRef>(null);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
startDiscovery: async () => {
|
||||
await nativeRef.current?.startDiscovery()
|
||||
},
|
||||
stopDiscovery: async () => {
|
||||
await nativeRef.current?.stopDiscovery()
|
||||
},
|
||||
startPictureInPicture: async () => {
|
||||
await nativeRef.current?.startPictureInPicture()
|
||||
},
|
||||
@@ -114,6 +106,7 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
onVideoLoadEnd,
|
||||
onVideoError,
|
||||
onPipStarted,
|
||||
onDiscoveryStateChanged,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@@ -141,6 +134,7 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
onVideoProgress={onVideoProgress}
|
||||
onVideoError={onVideoError}
|
||||
onPipStarted={onPipStarted}
|
||||
onDiscoveryStateChanged={onDiscoveryStateChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
"android:tv": "EXPO_TV=1 expo run:android",
|
||||
"prebuild": "EXPO_TV=0 bun run clean",
|
||||
"prebuild:tv": "EXPO_TV=1 bun run clean",
|
||||
"prebuild:tv-new": "EXPO_TV=1 node ./scripts/symlink-native-dirs.js; bun run prebuild:tv",
|
||||
"test": "jest --watchAll",
|
||||
"lint": "expo lint",
|
||||
"postinstall": "patch-package"
|
||||
|
||||
@@ -128,12 +128,7 @@
|
||||
"OTHER": "Andere",
|
||||
"UNKNOWN": "Unbekannt"
|
||||
},
|
||||
"safe_area_in_controls": "Sicherer Bereich in den Steuerungen",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"safe_area_in_controls": "Sicherer Bereich in den Steuerungen",
|
||||
"show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen",
|
||||
"hide_libraries": "Bibliotheken ausblenden",
|
||||
"select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.",
|
||||
@@ -173,8 +168,7 @@
|
||||
"tv_quota_limit": "TV-Anfragelimit",
|
||||
"tv_quota_days": "TV-Anfragetage",
|
||||
"reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück",
|
||||
"unlimited": "Unlimitiert",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"unlimited": "Unlimitiert"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Aktiviere Marlin Search",
|
||||
@@ -358,7 +352,8 @@
|
||||
"audio_tracks": "Audiospuren:",
|
||||
"playback_state": "Wiedergabestatus:",
|
||||
"no_data_available": "Keine Daten verfügbar",
|
||||
"index": "Index:"
|
||||
"index": "Index:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Als Nächstes",
|
||||
@@ -439,7 +434,7 @@
|
||||
"tags": "Tags",
|
||||
"quality_profile": "Qualitätsprofil",
|
||||
"root_folder": "Root-Ordner",
|
||||
"season_all": "Season (all)",
|
||||
"season_x": "Staffel {{seasons}}",
|
||||
"season_number": "Staffel {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Episodes",
|
||||
"born": "Geboren",
|
||||
|
||||
@@ -129,16 +129,11 @@
|
||||
"UNKNOWN": "Unknown"
|
||||
},
|
||||
"safe_area_in_controls": "Safe area in controls",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"show_custom_menu_links": "Show Custom Menu Links",
|
||||
"hide_libraries": "Hide Libraries",
|
||||
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||
"disable_haptic_feedback": "Disable Haptic Feedback",
|
||||
"default_quality": "Default quality",
|
||||
"default_quality": "Default quality"
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
@@ -173,8 +168,7 @@
|
||||
"tv_quota_limit": "TV quota limit",
|
||||
"tv_quota_days": "TV quota days",
|
||||
"reset_jellyseerr_config_button": "Reset Jellyseerr config",
|
||||
"unlimited": "Unlimited",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"unlimited": "Unlimited"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Enable Marlin Search ",
|
||||
@@ -362,7 +356,8 @@
|
||||
"audio_tracks": "Audio Tracks:",
|
||||
"playback_state": "Playback State:",
|
||||
"no_data_available": "No data available",
|
||||
"index": "Index:"
|
||||
"index": "Index:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Next up",
|
||||
@@ -443,7 +438,7 @@
|
||||
"tags": "Tags",
|
||||
"quality_profile": "Quality Profile",
|
||||
"root_folder": "Root Folder",
|
||||
"season_all": "Season (all)",
|
||||
"season_x": "Season {{seasons}}",
|
||||
"season_number": "Season {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Episodes",
|
||||
"born": "Born",
|
||||
|
||||
@@ -128,12 +128,7 @@
|
||||
"OTHER": "Otra",
|
||||
"UNKNOWN": "Desconocida"
|
||||
},
|
||||
"safe_area_in_controls": "Área segura en controles",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"safe_area_in_controls": "Área segura en controles",
|
||||
"show_custom_menu_links": "Mostrar enlaces de menú personalizados",
|
||||
"hide_libraries": "Ocultar bibliotecas",
|
||||
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
|
||||
@@ -173,8 +168,7 @@
|
||||
"tv_quota_limit": "Límite de cuota de series",
|
||||
"tv_quota_days": "Días de cuota de series",
|
||||
"reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr",
|
||||
"unlimited": "Ilimitado",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"unlimited": "Ilimitado"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Habilitar búsqueda de Marlin",
|
||||
@@ -358,7 +352,8 @@
|
||||
"audio_tracks": "Pistas de audio:",
|
||||
"playback_state": "Estado de la reproducción:",
|
||||
"no_data_available": "No hay datos disponibles",
|
||||
"index": "Índice:"
|
||||
"index": "Índice:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "A continuación",
|
||||
@@ -439,7 +434,7 @@
|
||||
"tags": "Etiquetas",
|
||||
"quality_profile": "Perfil de calidad",
|
||||
"root_folder": "Carpeta raíz",
|
||||
"season_all": "Season (all)",
|
||||
"season_x": "Temporada {{seasons}}",
|
||||
"season_number": "Temporada {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} episodios",
|
||||
"born": "Nacido",
|
||||
|
||||
@@ -128,12 +128,7 @@
|
||||
"OTHER": "Autre",
|
||||
"UNKNOWN": "Inconnu"
|
||||
},
|
||||
"safe_area_in_controls": "Zone de sécurité dans les contrôles",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"safe_area_in_controls": "Zone de sécurité dans les contrôles",
|
||||
"show_custom_menu_links": "Afficher les liens personnalisés",
|
||||
"hide_libraries": "Cacher des bibliothèques",
|
||||
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l’onglet Bibliothèque et les sections de la page d’accueil.",
|
||||
@@ -174,8 +169,7 @@
|
||||
"tv_quota_limit": "Limite de quota TV",
|
||||
"tv_quota_days": "Jours de quota TV",
|
||||
"reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr",
|
||||
"unlimited": "Illimité",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"unlimited": "Illimité"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Activer Marlin Search ",
|
||||
@@ -359,7 +353,8 @@
|
||||
"audio_tracks": "Pistes audio:",
|
||||
"playback_state": "État de lecture:",
|
||||
"no_data_available": "Aucune donnée disponible",
|
||||
"index": "Index:"
|
||||
"index": "Index:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "À suivre",
|
||||
@@ -440,7 +435,7 @@
|
||||
"tags": "Tags",
|
||||
"quality_profile": "Profil de qualité",
|
||||
"root_folder": "Dossier racine",
|
||||
"season_all": "Season (all)",
|
||||
"season_x": "Saison {{seasons}}",
|
||||
"season_number": "Saison {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} épisodes",
|
||||
"born": "Né(e) le",
|
||||
|
||||
@@ -128,12 +128,7 @@
|
||||
"OTHER": "Altro",
|
||||
"UNKNOWN": "Sconosciuto"
|
||||
},
|
||||
"safe_area_in_controls": "Area sicura per i controlli",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"safe_area_in_controls": "Area sicura per i controlli",
|
||||
"show_custom_menu_links": "Mostra i link del menu personalizzato",
|
||||
"hide_libraries": "Nascondi Librerie",
|
||||
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
|
||||
@@ -173,8 +168,7 @@
|
||||
"tv_quota_limit": "Limite di quota per le serie TV",
|
||||
"tv_quota_days": "Giorni di quota per le serie TV",
|
||||
"reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr",
|
||||
"unlimited": "Illimitato",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"unlimited": "Illimitato"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Abilita la ricerca Marlin ",
|
||||
@@ -358,7 +352,8 @@
|
||||
"audio_tracks": "Tracce audio:",
|
||||
"playback_state": "Stato della riproduzione:",
|
||||
"no_data_available": "Nessun dato disponibile",
|
||||
"index": "Indice:"
|
||||
"index": "Indice:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Il prossimo",
|
||||
@@ -439,7 +434,7 @@
|
||||
"tags": "Tag",
|
||||
"quality_profile": "Profilo qualità",
|
||||
"root_folder": "Cartella radice",
|
||||
"season_all": "Season (all)",
|
||||
"season_x": "Stagione {{seasons}}",
|
||||
"season_number": "Stagione {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Episodio",
|
||||
"born": "Nato",
|
||||
|
||||
@@ -128,12 +128,7 @@
|
||||
"OTHER": "その他",
|
||||
"UNKNOWN": "不明"
|
||||
},
|
||||
"safe_area_in_controls": "コントロールの安全エリア",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"safe_area_in_controls": "コントロールの安全エリア",
|
||||
"show_custom_menu_links": "カスタムメニューのリンクを表示",
|
||||
"hide_libraries": "ライブラリを非表示",
|
||||
"select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。",
|
||||
@@ -172,8 +167,7 @@
|
||||
"tv_quota_limit": "テレビのクオータ制限",
|
||||
"tv_quota_days": "テレビのクオータ日数",
|
||||
"reset_jellyseerr_config_button": "Jellyseerrの設定をリセット",
|
||||
"unlimited": "無制限",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"unlimited": "無制限"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "マーリン検索を有効にする ",
|
||||
@@ -357,7 +351,8 @@
|
||||
"audio_tracks": "音声トラック:",
|
||||
"playback_state": "再生状態:",
|
||||
"no_data_available": "データなし",
|
||||
"index": "インデックス:"
|
||||
"index": "インデックス:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "次",
|
||||
@@ -438,7 +433,7 @@
|
||||
"tags": "タグ",
|
||||
"quality_profile": "画質プロファイル",
|
||||
"root_folder": "ルートフォルダ",
|
||||
"season_all": "Season (all)",
|
||||
"season_x": "シーズン{{seasons}}",
|
||||
"season_number": "シーズン{{season_number}}",
|
||||
"number_episodes": "エピソード{{episode_number}}",
|
||||
"born": "生まれ",
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
"login_button": "Aanmelden",
|
||||
"quick_connect": "Snel Verbinden",
|
||||
"enter_code_to_login": "Vul code {{code}} in om aan te melden",
|
||||
"failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten",
|
||||
"failed_to_initiate_quick_connect": "Gefaald om Snel Verbinden op te starten",
|
||||
"got_it": "Begrepen",
|
||||
"connection_failed": "Verbinding mislukt",
|
||||
"connection_failed": "Verbinding gefaald",
|
||||
"could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.",
|
||||
"an_unexpected_error_occured": "Er is een onverwachte fout opgetreden",
|
||||
"change_server": "Server wijzigen",
|
||||
"invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord",
|
||||
"change_server": "Verander server",
|
||||
"invalid_username_or_password": "Ongeldige gebruikersnaam of wachtwoord",
|
||||
"user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden",
|
||||
"server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw",
|
||||
"server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw",
|
||||
@@ -42,7 +42,7 @@
|
||||
"continue_watching": "Verder Kijken",
|
||||
"next_up": "Volgende",
|
||||
"recently_added_in": "Recent toegevoegd in {{libraryName}}",
|
||||
"suggested_movies": "Voorgestelde films",
|
||||
"suggested_movies": "Voorgestelde Films",
|
||||
"suggested_episodes": "Voorgestelde Afleveringen",
|
||||
"intro": {
|
||||
"welcome_to_streamyfin": "Welkom bij Streamyfin",
|
||||
@@ -56,7 +56,7 @@
|
||||
"centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen",
|
||||
"centralised_settings_plugin_description": "Configureer instellingen vanaf een centrale locatie op je Jellyfin server. Alle clientinstellingen voor alle gebruikers worden automatisch gesynchroniseerd.",
|
||||
"done_button": "Gedaan",
|
||||
"go_to_settings_button": "Ga naar instellingen",
|
||||
"go_to_settings_button": "Go naar instellingen",
|
||||
"read_more": "Lees meer"
|
||||
},
|
||||
"settings": {
|
||||
@@ -82,7 +82,7 @@
|
||||
"media_controls": {
|
||||
"media_controls_title": "Media Bedieningen",
|
||||
"forward_skip_length": "Duur voorwaarts overslaan",
|
||||
"rewind_length": "Duur terugspoelen",
|
||||
"rewind_length": "Duur terugspeolen",
|
||||
"seconds_unit": "s"
|
||||
},
|
||||
"audio": {
|
||||
@@ -96,7 +96,7 @@
|
||||
"subtitles": {
|
||||
"subtitle_title": "Ondertitels",
|
||||
"subtitle_language": "Ondertitel taal",
|
||||
"subtitle_mode": "Ondertitelmodus",
|
||||
"subtitle_mode": "Ondertitle Modus",
|
||||
"set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item",
|
||||
"subtitle_size": "Ondertitel Grootte",
|
||||
"subtitle_hint": "Stel ondertitel voorkeuren in.",
|
||||
@@ -108,7 +108,7 @@
|
||||
"Smart": "Slim",
|
||||
"Always": "Altijd",
|
||||
"None": "Geen",
|
||||
"OnlyForced": "Alleen Geforceerd"
|
||||
"OnlyForced": "Alleen Geforceeerd"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
@@ -128,26 +128,21 @@
|
||||
"OTHER": "Andere",
|
||||
"UNKNOWN": "Onbekend"
|
||||
},
|
||||
"safe_area_in_controls": "Veilig gebied in bedieningen",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"safe_area_in_controls": "Veilig gebied in bedieningen",
|
||||
"show_custom_menu_links": "Aangepaste menulinks tonen",
|
||||
"hide_libraries": "Verberg Bibliotheken",
|
||||
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
|
||||
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheek tab en hoofdpagina onderdelen.",
|
||||
"disable_haptic_feedback": "Haptische feedback uitschakelen",
|
||||
"default_quality": "Standaard kwaliteit"
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
"download_method": "Download methode",
|
||||
"remux_max_download": "Maximale Remux-download",
|
||||
"remux_max_download": "Remux max download",
|
||||
"auto_download": "Auto download",
|
||||
"optimized_versions_server": "Geoptimaliseerde server versies",
|
||||
"save_button": "Opslaan",
|
||||
"optimized_server": "Geoptimaliseerde Server",
|
||||
"optimized_server": "Geoptimailseerde Server",
|
||||
"optimized": "Geoptimaliseerd",
|
||||
"default": "Standaard",
|
||||
"optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.",
|
||||
@@ -166,18 +161,17 @@
|
||||
"password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}",
|
||||
"save_button": "Opslaan",
|
||||
"clear_button": "Wissen",
|
||||
"login_button": "Aanmelden",
|
||||
"login_button": "Aannmelden",
|
||||
"total_media_requests": "Totaal aantal mediaverzoeken",
|
||||
"movie_quota_limit": "Limiet filmquota",
|
||||
"movie_quota_days": "Filmquota dagen",
|
||||
"tv_quota_limit": "Limiet serie quota",
|
||||
"tv_quota_days": "Serie Quota dagen",
|
||||
"reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen",
|
||||
"unlimited": "Ongelimiteerd",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"unlimited": "Ongelimiteerd"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Marlin Search inschakelen ",
|
||||
"enable_marlin_search": "Marlin Search inschakeln ",
|
||||
"url": "URL",
|
||||
"server_url_placeholder": "http(s)://domein.org:poort",
|
||||
"marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.",
|
||||
@@ -211,7 +205,7 @@
|
||||
"system": "Systeem"
|
||||
},
|
||||
"toasts": {
|
||||
"error_deleting_files": "Fout bij het verwijderen van bestanden",
|
||||
"error_deleting_files": "Fout bij het verwijden van bestanden",
|
||||
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
|
||||
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld",
|
||||
"connected": "Verbonden",
|
||||
@@ -243,7 +237,7 @@
|
||||
"methods": "Methoden",
|
||||
"toasts": {
|
||||
"you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.",
|
||||
"deleted_all_movies_successfully": "Alle films succesvol verwijderd!",
|
||||
"deleted_all_movies_successfully": "Alle filns succesvol verwijderd!",
|
||||
"failed_to_delete_all_movies": "Alle films zijn niet verwijderd",
|
||||
"deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!",
|
||||
"failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd",
|
||||
@@ -286,18 +280,18 @@
|
||||
"recent_requests": "Recent Aangevraagd",
|
||||
"plex_watchlist": "Plex Kijklijst",
|
||||
"trending": "Trending",
|
||||
"popular_movies": "Populaire films",
|
||||
"popular_movies": "Populaire Films",
|
||||
"movie_genres": "Film Genres",
|
||||
"upcoming_movies": "Aankomende films",
|
||||
"upcoming_movies": "Aankomende Movies",
|
||||
"studios": "Studios",
|
||||
"popular_tv": "Populaire TV",
|
||||
"tv_genres": "TV Genres",
|
||||
"upcoming_tv": "Aankomende TV",
|
||||
"upcoming_tv": "Opkomend TV",
|
||||
"networks": "Netwerken",
|
||||
"tmdb_movie_keyword": "TMDB Film Trefwoord",
|
||||
"tmdb_movie_genre": "TMDB Filmgenres",
|
||||
"tmdb_movie_genre": "TMDB Film Genre",
|
||||
"tmdb_tv_keyword": "TMDB TV Trefwoord",
|
||||
"tmdb_tv_genre": "TMDB TV-Genres",
|
||||
"tmdb_tv_genre": "TMDB TV Genre",
|
||||
"tmdb_search": "TMDB Zoeken",
|
||||
"tmdb_studio": "TMDB Studio",
|
||||
"tmdb_network": "TMDB Netwerk",
|
||||
@@ -309,9 +303,9 @@
|
||||
"no_results": "Geen resultaten",
|
||||
"no_libraries_found": "Geen bibliotheken gevonden",
|
||||
"item_types": {
|
||||
"movies": "Films",
|
||||
"series": "Series",
|
||||
"boxsets": "Boxsets",
|
||||
"movies": "films",
|
||||
"series": "series",
|
||||
"boxsets": "box sets",
|
||||
"items": "items"
|
||||
},
|
||||
"options": {
|
||||
@@ -351,14 +345,15 @@
|
||||
"could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast",
|
||||
"message_from_server": "Bericht van de server",
|
||||
"video_has_finished_playing": "Video is gedaan met spelen!",
|
||||
"no_video_source": "Geen videobron...",
|
||||
"no_video_source": "Geen video bron...",
|
||||
"next_episode": "Volgende Aflevering",
|
||||
"refresh_tracks": "Tracks verversen",
|
||||
"subtitle_tracks": "Ondertitel Tracks:",
|
||||
"audio_tracks": "Audio Tracks:",
|
||||
"playback_state": "Afspeelstatus:",
|
||||
"no_data_available": "Geen data beschikbaar",
|
||||
"index": "Index:"
|
||||
"index": "Index:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Volgende",
|
||||
@@ -378,7 +373,7 @@
|
||||
"audio": "Audio",
|
||||
"subtitles": "Ondertitel",
|
||||
"show_more": "Toon meer",
|
||||
"show_less": "Toon minder",
|
||||
"show_less": "Toon minden",
|
||||
"appeared_in": "Verschenen in",
|
||||
"could_not_load_item": "Kon item niet laden",
|
||||
"none": "Geen",
|
||||
@@ -423,7 +418,7 @@
|
||||
"details": "Details",
|
||||
"status": "Status",
|
||||
"original_title": "Originele titel",
|
||||
"series_type": "Serietype",
|
||||
"series_type": "Serie Type",
|
||||
"release_dates": "Verschijningsdatums",
|
||||
"first_air_date": "Eerste uitzenddatum",
|
||||
"next_air_date": "Volgende uitzenddatum",
|
||||
@@ -439,19 +434,19 @@
|
||||
"tags": "Labels",
|
||||
"quality_profile": "Kwaliteitsprofiel",
|
||||
"root_folder": "Hoofdmap",
|
||||
"season_all": "Season (all)",
|
||||
"season_x": "Seizoen {{seasons}}",
|
||||
"season_number": "Seizoen {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Afleveringen",
|
||||
"born": "Geboren",
|
||||
"appearances": "Verschijningen",
|
||||
"toasts": {
|
||||
"jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0",
|
||||
"jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.",
|
||||
"jellyseerr_test_failed": "Jellyseerr test gefaald. Probeer opnieuw.",
|
||||
"failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url",
|
||||
"issue_submitted": "Probleem ingediend!",
|
||||
"requested_item": "{{item}} aangevraagd!",
|
||||
"you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!",
|
||||
"something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!"
|
||||
"something_went_wrong_requesting_media": "Er ging iets iets mis met het aavragen van media!"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
|
||||
@@ -128,12 +128,7 @@
|
||||
"OTHER": "Diğer",
|
||||
"UNKNOWN": "Bilinmeyen"
|
||||
},
|
||||
"safe_area_in_controls": "Kontrollerde Güvenli Alan",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"safe_area_in_controls": "Kontrollerde Güvenli Alan",
|
||||
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
|
||||
"hide_libraries": "Kütüphaneleri Gizle",
|
||||
"select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.",
|
||||
@@ -172,8 +167,7 @@
|
||||
"tv_quota_limit": "TV kota limiti",
|
||||
"tv_quota_days": "TV kota günleri",
|
||||
"reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla",
|
||||
"unlimited": "Sınırsız",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"unlimited": "Sınırsız"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Marlin Aramasını Etkinleştir ",
|
||||
@@ -357,7 +351,8 @@
|
||||
"audio_tracks": "Ses Parçaları:",
|
||||
"playback_state": "Oynatma Durumu:",
|
||||
"no_data_available": "Veri bulunamadı",
|
||||
"index": "İndeks:"
|
||||
"index": "İndeks:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Sıradaki",
|
||||
@@ -438,7 +433,7 @@
|
||||
"tags": "Etiketler",
|
||||
"quality_profile": "Kalite Profili",
|
||||
"root_folder": "Kök Klasör",
|
||||
"season_all": "Season (all)",
|
||||
"season_x": "Sezon {{seasons}}",
|
||||
"season_number": "Sezon {{season_number}}",
|
||||
"number_episodes": "Bölüm {{episode_number}}",
|
||||
"born": "Doğum",
|
||||
|
||||
@@ -128,12 +128,7 @@
|
||||
"OTHER": "其他",
|
||||
"UNKNOWN": "未知"
|
||||
},
|
||||
"safe_area_in_controls": "控制中的安全区域",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"safe_area_in_controls": "控制中的安全区域",
|
||||
"show_custom_menu_links": "显示自定义菜单链接",
|
||||
"hide_libraries": "隐藏媒体库",
|
||||
"select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。",
|
||||
@@ -172,8 +167,7 @@
|
||||
"tv_quota_limit": "剧集配额限制",
|
||||
"tv_quota_days": "剧集配额天数",
|
||||
"reset_jellyseerr_config_button": "重置 Jellyseerr 设置",
|
||||
"unlimited": "无限制",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"unlimited": "无限制"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "启用 Marlin 搜索",
|
||||
@@ -357,7 +351,8 @@
|
||||
"audio_tracks": "音频轨道:",
|
||||
"playback_state": "播放状态:",
|
||||
"no_data_available": "无可用数据",
|
||||
"index": "索引:"
|
||||
"index": "索引:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "下一个",
|
||||
@@ -438,7 +433,7 @@
|
||||
"tags": "标签",
|
||||
"quality_profile": "质量配置文件",
|
||||
"root_folder": "根文件夹",
|
||||
"season_all": "Season (all)",
|
||||
"season_x": "第 {{seasons}} 季",
|
||||
"season_number": "第 {{season_number}} 季",
|
||||
"number_episodes": "{{episode_number}} 集",
|
||||
"born": "出生",
|
||||
|
||||
@@ -128,12 +128,7 @@
|
||||
"OTHER": "其他",
|
||||
"UNKNOWN": "未知"
|
||||
},
|
||||
"safe_area_in_controls": "控制中的安全區域",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"safe_area_in_controls": "控制中的安全區域",
|
||||
"show_custom_menu_links": "顯示自定義菜單鏈接",
|
||||
"hide_libraries": "隱藏媒體庫",
|
||||
"select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
|
||||
@@ -172,8 +167,7 @@
|
||||
"tv_quota_limit": "電視配額限制",
|
||||
"tv_quota_days": "電視配額天數",
|
||||
"reset_jellyseerr_config_button": "重置 Jellyseerr 配置",
|
||||
"unlimited": "無限制",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"unlimited": "無限制"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "啟用 Marlin 搜索",
|
||||
@@ -357,7 +351,8 @@
|
||||
"audio_tracks": "音頻軌道:",
|
||||
"playback_state": "播放狀態:",
|
||||
"no_data_available": "無可用數據",
|
||||
"index": "索引:"
|
||||
"index": "索引:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "下一個",
|
||||
@@ -438,7 +433,7 @@
|
||||
"tags": "標籤",
|
||||
"quality_profile": "質量配置文件",
|
||||
"root_folder": "根文件夾",
|
||||
"season_all": "Season (all)",
|
||||
"season_x": "第 {{seasons}} 季",
|
||||
"season_number": "第 {{season_number}} 季",
|
||||
"number_episodes": "{{episode_number}} 集",
|
||||
"born": "出生",
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import { Bitrate, BITRATES } from "@/components/BitrateSelector";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { writeInfoLog } from "@/utils/log";
|
||||
import {Video} from "@/utils/jellyseerr/server/models/Movie";
|
||||
|
||||
const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004";
|
||||
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS";
|
||||
@@ -113,12 +112,6 @@ export type HomeSectionNextUpResolver = {
|
||||
enableRewatching?: boolean;
|
||||
};
|
||||
|
||||
export enum VideoPlayer {
|
||||
// NATIVE, //todo: changes will make this a lot more easier to implement if we want. delete if not wanted
|
||||
VLC_3,
|
||||
VLC_4
|
||||
}
|
||||
|
||||
export type Settings = {
|
||||
home?: Home | null;
|
||||
autoRotate?: boolean;
|
||||
@@ -153,7 +146,6 @@ export type Settings = {
|
||||
jellyseerrServerUrl?: string;
|
||||
hiddenLibraries?: string[];
|
||||
enableH265ForChromecast: boolean;
|
||||
defaultPlayer: VideoPlayer;
|
||||
};
|
||||
|
||||
export interface Lockable<T> {
|
||||
@@ -208,7 +200,6 @@ const defaultValues: Settings = {
|
||||
jellyseerrServerUrl: undefined,
|
||||
hiddenLibraries: [],
|
||||
enableH265ForChromecast: false,
|
||||
defaultPlayer: VideoPlayer.VLC_3, // ios only setting. does not matter what this is for android
|
||||
};
|
||||
|
||||
const loadSettings = (): Partial<Settings> => {
|
||||
|
||||
Reference in New Issue
Block a user