forked from Ninjalama/streamyfin_mirror
Compare commits
7 Commits
feature/se
...
feat/switc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f07260177 | ||
|
|
09e9462ac0 | ||
|
|
dd65505f7f | ||
|
|
951158bcd3 | ||
|
|
9b1dd0923a | ||
|
|
bd908516b5 | ||
|
|
8cb10d1062 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ npm-debug.*
|
||||
*.orig.*
|
||||
web-build/
|
||||
modules/vlc-player/android/build
|
||||
modules/vlc-player/android/.gradle
|
||||
bun.lockb
|
||||
|
||||
# macOS
|
||||
|
||||
@@ -78,12 +78,6 @@ 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={{
|
||||
|
||||
@@ -42,25 +42,28 @@ 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, ...result } =
|
||||
const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } =
|
||||
params as unknown as {
|
||||
mediaTitle: string;
|
||||
releaseYear: number;
|
||||
canRequest: string;
|
||||
posterSrc: string;
|
||||
} & Partial<MovieResult | TvResult>;
|
||||
mediaType: MediaType;
|
||||
} & Partial<MovieResult | TvResult | MovieDetails | TvDetails>;
|
||||
|
||||
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);
|
||||
|
||||
@@ -71,7 +74,7 @@ const Page: React.FC = () => {
|
||||
refetch,
|
||||
} = useQuery({
|
||||
enabled: !!jellyseerrApi && !!result && !!result.id,
|
||||
queryKey: ["jellyseerr", "detail", result.mediaType, result.id],
|
||||
queryKey: ["jellyseerr", "detail", mediaType, result.id],
|
||||
staleTime: 0,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true,
|
||||
@@ -79,7 +82,7 @@ const Page: React.FC = () => {
|
||||
retryOnMount: true,
|
||||
refetchInterval: 0,
|
||||
queryFn: async () => {
|
||||
return result.mediaType === MediaType.MOVIE
|
||||
return mediaType === MediaType.MOVIE
|
||||
? jellyseerrApi?.movieDetails(result.id!!)
|
||||
: jellyseerrApi?.tvDetails(result.id!!);
|
||||
},
|
||||
@@ -111,10 +114,15 @@ 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: result.mediaType!!,
|
||||
mediaType: mediaType!!,
|
||||
tvdbId: details?.externalIds?.tvdbId,
|
||||
seasons: (details as TvDetails)?.seasons
|
||||
?.filter?.((s) => s.seasonNumber !== 0)
|
||||
@@ -122,7 +130,7 @@ const Page: React.FC = () => {
|
||||
};
|
||||
|
||||
if (hasAdvancedRequestPermission) {
|
||||
advancedReqModalRef?.current?.present?.(body);
|
||||
setRequestBody(body)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,7 +140,7 @@ const Page: React.FC = () => {
|
||||
const isAnime = useMemo(
|
||||
() =>
|
||||
(details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) &&
|
||||
result.mediaType === MediaType.TV,
|
||||
mediaType === MediaType.TV,
|
||||
[details]
|
||||
);
|
||||
|
||||
@@ -200,7 +208,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} />
|
||||
<JellyserrRatings result={result as MovieResult | TvResult | MovieDetails | TvDetails} />
|
||||
<Text
|
||||
uiTextView
|
||||
selectable
|
||||
@@ -247,15 +255,14 @@ const Page: React.FC = () => {
|
||||
<OverviewText text={result.overview} className="mt-4" />
|
||||
</View>
|
||||
|
||||
{result.mediaType === MediaType.TV && (
|
||||
{mediaType === MediaType.TV && (
|
||||
<JellyseerrSeasons
|
||||
isLoading={isLoading || isFetching}
|
||||
result={result as TvResult}
|
||||
details={details as TvDetails}
|
||||
refetch={refetch}
|
||||
hasAdvancedRequest={hasAdvancedRequestPermission}
|
||||
onAdvancedRequest={(data) =>
|
||||
advancedReqModalRef?.current?.present(data)
|
||||
setRequestBody(data)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -269,14 +276,17 @@ const Page: React.FC = () => {
|
||||
</ParallaxScrollView>
|
||||
<RequestModal
|
||||
ref={advancedReqModalRef}
|
||||
requestBody={requestBody}
|
||||
title={mediaTitle}
|
||||
id={result.id!!}
|
||||
type={result.mediaType as MediaType}
|
||||
type={mediaType}
|
||||
isAnime={isAnime}
|
||||
onRequested={() => {
|
||||
_setRequestBody(undefined)
|
||||
advancedReqModalRef?.current?.close();
|
||||
refetch();
|
||||
}}
|
||||
onDismiss={() => _setRequestBody(undefined)}
|
||||
/>
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
|
||||
@@ -5,13 +5,13 @@ 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/vlc-player";
|
||||
import { VlcPlayerView } from "@/modules";
|
||||
import {
|
||||
PipStartedPayload,
|
||||
PlaybackStatePayload,
|
||||
ProgressUpdatePayload,
|
||||
VlcPlayerViewRef,
|
||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||
} from "@/modules/VlcPlayer.types";
|
||||
const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null;
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
|
||||
@@ -21,14 +21,19 @@ export const Tag: React.FC<{ text: string, textClass?: ViewProps["className"], t
|
||||
);
|
||||
};
|
||||
|
||||
export const Tags: React.FC<TagProps & ViewProps> = ({ tags, textClass = "text-xs", ...props }) => {
|
||||
export const Tags: React.FC<TagProps & {tagProps?: ViewProps} & ViewProps> = ({
|
||||
tags,
|
||||
textClass = "text-xs",
|
||||
tagProps,
|
||||
...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}/>
|
||||
<Tag key={idx} textClass={textClass} text={tag} {...tagProps}/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -7,6 +7,9 @@ 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;
|
||||
@@ -49,14 +52,17 @@ export const Ratings: React.FC<Props> = ({ item, ...props }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult }> = ({
|
||||
export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult | TvDetails | MovieDetails }> = ({
|
||||
result,
|
||||
}) => {
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
const { jellyseerrApi, getMediaType } = useJellyseerr();
|
||||
|
||||
const mediaType = useMemo(() => getMediaType(result), [result]);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["jellyseerr", result.id, result.mediaType, "ratings"],
|
||||
queryKey: ["jellyseerr", result.id, mediaType, "ratings"],
|
||||
queryFn: async () => {
|
||||
return result.mediaType === MediaType.MOVIE
|
||||
return 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;
|
||||
multi?: boolean;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
const Dropdown = <T extends unknown>({
|
||||
@@ -30,7 +30,7 @@ const Dropdown = <T extends unknown>({
|
||||
title,
|
||||
label,
|
||||
onSelected,
|
||||
multi = false,
|
||||
multiple = 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) =>
|
||||
multi ? (
|
||||
multiple ? (
|
||||
<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, previous) =>
|
||||
onValueChange={(next: "on" | "off", previous: "on" | "off") => {
|
||||
setSelected((p) => {
|
||||
const prev = p || [];
|
||||
if (next == "on") {
|
||||
@@ -92,7 +92,7 @@ const Dropdown = <T extends unknown>({
|
||||
),
|
||||
];
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemTitle>
|
||||
{titleExtractor(item)}
|
||||
|
||||
@@ -9,13 +9,16 @@ 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;
|
||||
result: MovieResult | TvResult | MovieDetails | TvDetails;
|
||||
mediaTitle: string;
|
||||
releaseYear: number;
|
||||
canRequest: boolean;
|
||||
posterSrc: string;
|
||||
mediaType: MediaType;
|
||||
}
|
||||
|
||||
export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
@@ -24,6 +27,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
releaseYear,
|
||||
canRequest,
|
||||
posterSrc,
|
||||
mediaType,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
@@ -46,7 +50,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
() =>
|
||||
requestMedia(mediaTitle, {
|
||||
mediaId: result.id,
|
||||
mediaType: result.mediaType,
|
||||
mediaType,
|
||||
}),
|
||||
[jellyseerrApi, result]
|
||||
);
|
||||
@@ -67,6 +71,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
releaseYear,
|
||||
canRequest,
|
||||
posterSrc,
|
||||
mediaType
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -83,7 +88,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
key={"content"}
|
||||
>
|
||||
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
|
||||
{canRequest && result.mediaType === MediaType.MOVIE && (
|
||||
{canRequest && mediaType === MediaType.MOVIE && (
|
||||
<ContextMenu.Item
|
||||
key="item-1"
|
||||
onSelect={() => {
|
||||
|
||||
@@ -21,6 +21,7 @@ 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;
|
||||
@@ -77,25 +78,28 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
||||
|
||||
const jellyseerrMovieResults = useMemo(
|
||||
() =>
|
||||
jellyseerrResults?.filter(
|
||||
(r) => r.mediaType === MediaType.MOVIE
|
||||
) as MovieResult[],
|
||||
uniqBy(
|
||||
jellyseerrResults?.filter((r) => r.mediaType === MediaType.MOVIE) as MovieResult[],
|
||||
"id"
|
||||
),
|
||||
[jellyseerrResults]
|
||||
);
|
||||
|
||||
const jellyseerrTvResults = useMemo(
|
||||
() =>
|
||||
jellyseerrResults?.filter(
|
||||
(r) => r.mediaType === MediaType.TV
|
||||
) as TvResult[],
|
||||
uniqBy(
|
||||
jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[],
|
||||
"id"
|
||||
),
|
||||
[jellyseerrResults]
|
||||
);
|
||||
|
||||
const jellyseerrPersonResults = useMemo(
|
||||
() =>
|
||||
jellyseerrResults?.filter(
|
||||
(r) => r.mediaType === "person"
|
||||
) as PersonResult[],
|
||||
uniqBy(
|
||||
jellyseerrResults?.filter((r) => r.mediaType === "person") as PersonResult[],
|
||||
"id"
|
||||
),
|
||||
[jellyseerrResults]
|
||||
);
|
||||
|
||||
|
||||
@@ -15,18 +15,22 @@ 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();
|
||||
@@ -39,8 +43,6 @@ 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'),
|
||||
@@ -98,16 +100,19 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
||||
: defaultServiceDetails?.server.activeTags
|
||||
)?.includes(t.id)
|
||||
) ?? []
|
||||
|
||||
console.log(tags)
|
||||
return tags
|
||||
},
|
||||
[defaultServiceDetails]
|
||||
);
|
||||
|
||||
const seasonTitle = useMemo(
|
||||
() => modalRequestProps?.seasons?.length ? t("jellyseerr.season_x", {seasons: modalRequestProps?.seasons}) : undefined,
|
||||
[modalRequestProps?.seasons]
|
||||
() => {
|
||||
if (requestBody?.seasons && requestBody?.seasons?.length > 1) {
|
||||
return t("jellyseerr.season_all")
|
||||
}
|
||||
return t("jellyseerr.season_number", {season_number: requestBody?.seasons})
|
||||
},
|
||||
[requestBody?.seasons]
|
||||
);
|
||||
|
||||
const request = useCallback(() => {requestMedia(
|
||||
@@ -117,12 +122,12 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
||||
profileId: defaultProfile.id,
|
||||
rootFolder: defaultFolder.path,
|
||||
tags: defaultTags.map(t => t.id),
|
||||
...modalRequestProps,
|
||||
...requestBody,
|
||||
...requestOverrides
|
||||
},
|
||||
onRequested
|
||||
)
|
||||
}, [modalRequestProps, requestOverrides, defaultProfile, defaultFolder, defaultTags]);
|
||||
}, [requestBody, requestOverrides, defaultProfile, defaultFolder, defaultTags]);
|
||||
|
||||
const pathTitleExtractor = (item: RootFolder) => `${item.path} (${item.freeSpace.bytesToReadable()})`;
|
||||
|
||||
@@ -131,7 +136,7 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
||||
ref={ref}
|
||||
enableDynamicSizing
|
||||
enableDismissOnClose
|
||||
onDismiss={() => setModalRequestProps(undefined)}
|
||||
onDismiss={onDismiss}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
@@ -146,89 +151,86 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(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>
|
||||
<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>
|
||||
</BottomSheetView>
|
||||
}}
|
||||
<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>
|
||||
</BottomSheetModal>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ 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[];
|
||||
@@ -25,6 +26,8 @@ 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,6 +10,7 @@ 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();
|
||||
@@ -57,7 +58,11 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) =>
|
||||
});
|
||||
|
||||
const flatData = useMemo(
|
||||
() => data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results),
|
||||
() =>
|
||||
uniqBy(
|
||||
data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results),
|
||||
"id"
|
||||
),
|
||||
[data]
|
||||
);
|
||||
|
||||
@@ -74,7 +79,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) =>
|
||||
fetchNextPage()
|
||||
}}
|
||||
renderItem={(item) =>
|
||||
<JellyseerrPoster item={item as MovieResult | TvResult} />
|
||||
<JellyseerrPoster item={item as MovieResult | TvResult} key={item?.id}/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
69
components/jellyseerr/discover/RecentRequestsSlide.tsx
Normal file
69
components/jellyseerr/discover/RecentRequestsSlide.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
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,28 +1,42 @@
|
||||
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 { 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";
|
||||
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";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
item: MovieResult | TvResult;
|
||||
item: MovieResult | TvResult | MovieDetails | TvDetails;
|
||||
horizontal?: boolean;
|
||||
showDownloadInfo?: boolean;
|
||||
mediaRequest?: MediaRequest;
|
||||
}
|
||||
|
||||
const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
const JellyseerrPoster: React.FC<Props> = ({
|
||||
item,
|
||||
horizontal,
|
||||
showDownloadInfo,
|
||||
mediaRequest,
|
||||
...props
|
||||
}) => {
|
||||
const { jellyseerrApi, getTitle, getYear, getMediaType, isJellyseerrResult } = useJellyseerr();
|
||||
const loadingOpacity = useSharedValue(1);
|
||||
const imageOpacity = useSharedValue(0);
|
||||
const {t} = useTranslation();
|
||||
|
||||
const loadingAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: loadingOpacity.value,
|
||||
@@ -38,27 +52,64 @@ const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
|
||||
};
|
||||
|
||||
const imageSrc = useMemo(
|
||||
() => jellyseerrApi?.imageProxy(item.posterPath, "w300_and_h450_face"),
|
||||
[item, jellyseerrApi]
|
||||
() => jellyseerrApi?.imageProxy(
|
||||
horizontal ? item.backdropPath : item.posterPath,
|
||||
horizontal ? "w1920_and_h800_multi_faces" : "w300_and_h450_face"
|
||||
),
|
||||
[item, jellyseerrApi, horizontal]
|
||||
);
|
||||
|
||||
const title = useMemo(
|
||||
() => (item.mediaType === MediaType.MOVIE ? item.title : item.name),
|
||||
[item]
|
||||
);
|
||||
const title = useMemo(() => getTitle(item), [item]);
|
||||
const releaseYear = useMemo(() => getYear(item), [item]);
|
||||
const mediaType = useMemo(() => getMediaType(item), [item]);
|
||||
|
||||
const releaseYear = useMemo(
|
||||
() =>
|
||||
new Date(
|
||||
item.mediaType === MediaType.MOVIE
|
||||
? item.releaseDate
|
||||
: item.firstAirDate
|
||||
).getFullYear(),
|
||||
[item]
|
||||
);
|
||||
const size = useMemo(() => horizontal ? 'h-28' : 'w-28', [horizontal])
|
||||
const ratio = useMemo(() => horizontal ? '15/10' : '10/15', [horizontal])
|
||||
|
||||
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}
|
||||
@@ -66,9 +117,10 @@ const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
|
||||
releaseYear={releaseYear}
|
||||
canRequest={canRequest}
|
||||
posterSrc={imageSrc!!}
|
||||
mediaType={mediaType}
|
||||
>
|
||||
<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]">
|
||||
<View className={`flex flex-col mr-2 h-auto`}>
|
||||
<View className={`relative rounded-lg overflow-hidden border border-neutral-900 ${size} aspect-[${ratio}]`}>
|
||||
<Animated.View style={imageAnimatedStyle}>
|
||||
<Image
|
||||
key={item.id}
|
||||
@@ -77,26 +129,65 @@ const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
|
||||
cachePolicy={"memory-disk"}
|
||||
contentFit="cover"
|
||||
style={{
|
||||
aspectRatio: "10/15",
|
||||
width: "100%",
|
||||
aspectRatio: ratio,
|
||||
[horizontal ? 'height' : '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={item?.mediaInfo?.status}
|
||||
mediaStatus={mediaRequest?.media?.status || item?.mediaInfo?.status}
|
||||
/>
|
||||
<JellyseerrMediaIcon
|
||||
className="absolute top-1 left-1"
|
||||
mediaType={item?.mediaType}
|
||||
mediaType={mediaType}
|
||||
/>
|
||||
</View>
|
||||
<View className="mt-2 flex flex-col">
|
||||
<Text numberOfLines={2}>{title}</Text>
|
||||
<Text className="text-xs opacity-50 align-bottom">{releaseYear}</Text>
|
||||
</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>
|
||||
</TouchableJellyseerrRouter>
|
||||
);
|
||||
|
||||
@@ -128,14 +128,12 @@ 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,
|
||||
@@ -195,7 +193,7 @@ const JellyseerrSeasons: React.FC<{
|
||||
return onAdvancedRequest?.(body)
|
||||
}
|
||||
|
||||
requestMedia(result?.name!!, body, refetch);
|
||||
requestMedia(details.name, body, refetch);
|
||||
}
|
||||
}, [jellyseerrApi, seasons, details, hasAdvancedRequest, onAdvancedRequest]);
|
||||
|
||||
@@ -227,7 +225,7 @@ const JellyseerrSeasons: React.FC<{
|
||||
return onAdvancedRequest?.(body)
|
||||
}
|
||||
|
||||
requestMedia(`${result?.name!!}, Season ${seasonNumber}`, body, refetch);
|
||||
requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch);
|
||||
}
|
||||
}, [requestMedia, hasAdvancedRequest, onAdvancedRequest]);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform } from "react-native";
|
||||
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
||||
import { ScreenOrientationEnum, useSettings, VideoPlayer } from "@/utils/atoms/settings";
|
||||
import { BitrateSelector, BITRATES } from "@/components/BitrateSelector";
|
||||
import {
|
||||
BACKGROUND_FETCH_TASK,
|
||||
@@ -7,9 +7,7 @@ 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";
|
||||
@@ -22,6 +20,7 @@ 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();
|
||||
@@ -84,10 +83,7 @@ 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}
|
||||
@@ -97,17 +93,11 @@ 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={
|
||||
@@ -115,17 +105,11 @@ 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>
|
||||
|
||||
@@ -136,27 +120,49 @@ 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
|
||||
@@ -164,10 +170,7 @@ 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}
|
||||
@@ -176,14 +179,8 @@ 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")}
|
||||
@@ -197,9 +194,7 @@ export const OtherSettings: React.FC = () => {
|
||||
<Switch
|
||||
value={settings.disableHapticFeedback}
|
||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||
onValueChange={(disableHapticFeedback) =>
|
||||
updateSettings({ disableHapticFeedback })
|
||||
}
|
||||
onValueChange={(disableHapticFeedback) => updateSettings({ disableHapticFeedback })}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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();
|
||||
@@ -61,7 +62,7 @@ export const StorageSettings = () => {
|
||||
<View
|
||||
style={{
|
||||
width: `${(size.app / size.total) * 100}%`,
|
||||
backgroundColor: "rgb(147 51 234)",
|
||||
backgroundColor: Colors.primaryRGB,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
@@ -70,7 +71,7 @@ export const StorageSettings = () => {
|
||||
((size.total - size.remaining - size.app) / size.total) *
|
||||
100
|
||||
}%`,
|
||||
backgroundColor: "rgb(192 132 252)",
|
||||
backgroundColor: Colors.primaryLightRGB,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,65 +1,40 @@
|
||||
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 {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 * 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;
|
||||
@@ -494,7 +469,7 @@ export const Controls: React.FC<Props> = ({
|
||||
)}
|
||||
|
||||
<View className="flex flex-row items-center space-x-2 ">
|
||||
{!Platform.isTV && (
|
||||
{!Platform.isTV && settings.defaultPlayer == VideoPlayer.VLC_4 && (
|
||||
<TouchableOpacity
|
||||
onPress={startPictureInPicture}
|
||||
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TrackInfo } from "@/modules/vlc-player";
|
||||
import {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TrackInfo } from "@/modules/vlc-player";
|
||||
import { TrackInfo } from "@/modules/VlcPlayer.types";
|
||||
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/vlc-player/src/VlcPlayer.types";
|
||||
} from "@/modules/VlcPlayer.types";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 { Results } from "@/utils/jellyseerr/server/models/Search";
|
||||
import {MovieResult, Results, TvResult} 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 } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
||||
import {MediaRequestBody, RequestResultsResponse} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
||||
import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
||||
import {
|
||||
SeasonWithEpisodes,
|
||||
@@ -227,6 +227,23 @@ 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}`)
|
||||
@@ -439,14 +456,34 @@ export const useJellyseerr = () => {
|
||||
);
|
||||
|
||||
const isJellyseerrResult = (
|
||||
items: any[] | null | undefined
|
||||
): items is Results[] => {
|
||||
items: any | null | undefined
|
||||
): items is Results => {
|
||||
return (
|
||||
!items ||
|
||||
(items.length >= 0 &&
|
||||
Object.hasOwn(items[0], "mediaType") &&
|
||||
Object.values(MediaType).includes(items[0]["mediaType"]))
|
||||
);
|
||||
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
|
||||
};
|
||||
|
||||
const jellyseerrRegion = useMemo(
|
||||
@@ -464,6 +501,9 @@ export const useJellyseerr = () => {
|
||||
setJellyseerrUser,
|
||||
clearAllJellyseerData,
|
||||
isJellyseerrResult,
|
||||
getTitle,
|
||||
getYear,
|
||||
getMediaType,
|
||||
jellyseerrRegion,
|
||||
jellyseerrLocale,
|
||||
requestMedia,
|
||||
|
||||
@@ -6,16 +6,30 @@ 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 NativeViewManager = requireNativeViewManager("VlcPlayer");
|
||||
const VLCViewManager = requireNativeViewManager("VlcPlayer");
|
||||
const VLC3ViewManager = requireNativeViewManager("VlcPlayer3");
|
||||
|
||||
// Create a forwarded ref version of the native view
|
||||
const NativeView = React.forwardRef<NativeViewRef, VlcPlayerViewProps>(
|
||||
(props, ref) => <NativeViewManager {...props} ref={ref} />
|
||||
(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}/>
|
||||
}
|
||||
);
|
||||
|
||||
const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
27
modules/index.ts
Normal file
27
modules/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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,
|
||||
};
|
||||
6
modules/vlc-player-3/expo-module.config.json
Normal file
6
modules/vlc-player-3/expo-module.config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"platforms": ["ios", "tvos"],
|
||||
"ios": {
|
||||
"modules": ["VlcPlayer3Module"]
|
||||
}
|
||||
}
|
||||
23
modules/vlc-player-3/ios/VlcPlayer3.podspec
Normal file
23
modules/vlc-player-3/ios/VlcPlayer3.podspec
Normal file
@@ -0,0 +1,23 @@
|
||||
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
|
||||
71
modules/vlc-player-3/ios/VlcPlayer3Module.swift
Normal file
71
modules/vlc-player-3/ios/VlcPlayer3Module.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
388
modules/vlc-player-3/ios/VlcPlayer3View.swift
Normal file
388
modules/vlc-player-3/ios/VlcPlayer3View.swift
Normal file
@@ -0,0 +1,388 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
modules/vlc-player-3/src/VlcPlayer3Module.ts
Normal file
5
modules/vlc-player-3/src/VlcPlayer3Module.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
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');
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
#Sun Nov 17 18:25:45 AEDT 2024
|
||||
gradle.version=8.9
|
||||
@@ -1,68 +0,0 @@
|
||||
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,6 +19,5 @@ 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
|
||||
|
||||
@@ -3,7 +3,6 @@ import UIKit
|
||||
import VLCKit
|
||||
import os
|
||||
|
||||
|
||||
public class VLCPlayerView: UIView {
|
||||
func setupView(parent: UIView) {
|
||||
self.backgroundColor = .black
|
||||
|
||||
@@ -128,7 +128,12 @@
|
||||
"OTHER": "Andere",
|
||||
"UNKNOWN": "Unbekannt"
|
||||
},
|
||||
"safe_area_in_controls": "Sicherer Bereich in den Steuerungen",
|
||||
"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)"
|
||||
},
|
||||
"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.",
|
||||
@@ -168,7 +173,8 @@
|
||||
"tv_quota_limit": "TV-Anfragelimit",
|
||||
"tv_quota_days": "TV-Anfragetage",
|
||||
"reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück",
|
||||
"unlimited": "Unlimitiert"
|
||||
"unlimited": "Unlimitiert",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Aktiviere Marlin Search",
|
||||
@@ -433,7 +439,7 @@
|
||||
"tags": "Tags",
|
||||
"quality_profile": "Qualitätsprofil",
|
||||
"root_folder": "Root-Ordner",
|
||||
"season_x": "Staffel {{seasons}}",
|
||||
"season_all": "Season (all)",
|
||||
"season_number": "Staffel {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Episodes",
|
||||
"born": "Geboren",
|
||||
|
||||
@@ -129,11 +129,16 @@
|
||||
"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",
|
||||
@@ -168,7 +173,8 @@
|
||||
"tv_quota_limit": "TV quota limit",
|
||||
"tv_quota_days": "TV quota days",
|
||||
"reset_jellyseerr_config_button": "Reset Jellyseerr config",
|
||||
"unlimited": "Unlimited"
|
||||
"unlimited": "Unlimited",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Enable Marlin Search ",
|
||||
@@ -437,7 +443,7 @@
|
||||
"tags": "Tags",
|
||||
"quality_profile": "Quality Profile",
|
||||
"root_folder": "Root Folder",
|
||||
"season_x": "Season {{seasons}}",
|
||||
"season_all": "Season (all)",
|
||||
"season_number": "Season {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Episodes",
|
||||
"born": "Born",
|
||||
|
||||
@@ -128,7 +128,12 @@
|
||||
"OTHER": "Otra",
|
||||
"UNKNOWN": "Desconocida"
|
||||
},
|
||||
"safe_area_in_controls": "Área segura en controles",
|
||||
"safe_area_in_controls": "Área segura en controles",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"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.",
|
||||
@@ -168,7 +173,8 @@
|
||||
"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"
|
||||
"unlimited": "Ilimitado",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Habilitar búsqueda de Marlin",
|
||||
@@ -433,7 +439,7 @@
|
||||
"tags": "Etiquetas",
|
||||
"quality_profile": "Perfil de calidad",
|
||||
"root_folder": "Carpeta raíz",
|
||||
"season_x": "Temporada {{seasons}}",
|
||||
"season_all": "Season (all)",
|
||||
"season_number": "Temporada {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} episodios",
|
||||
"born": "Nacido",
|
||||
|
||||
@@ -128,7 +128,12 @@
|
||||
"OTHER": "Autre",
|
||||
"UNKNOWN": "Inconnu"
|
||||
},
|
||||
"safe_area_in_controls": "Zone de sécurité dans les contrôles",
|
||||
"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)"
|
||||
},
|
||||
"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.",
|
||||
@@ -169,7 +174,8 @@
|
||||
"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é"
|
||||
"unlimited": "Illimité",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Activer Marlin Search ",
|
||||
@@ -434,7 +440,7 @@
|
||||
"tags": "Tags",
|
||||
"quality_profile": "Profil de qualité",
|
||||
"root_folder": "Dossier racine",
|
||||
"season_x": "Saison {{seasons}}",
|
||||
"season_all": "Season (all)",
|
||||
"season_number": "Saison {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} épisodes",
|
||||
"born": "Né(e) le",
|
||||
|
||||
@@ -128,7 +128,12 @@
|
||||
"OTHER": "Altro",
|
||||
"UNKNOWN": "Sconosciuto"
|
||||
},
|
||||
"safe_area_in_controls": "Area sicura per i controlli",
|
||||
"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)"
|
||||
},
|
||||
"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.",
|
||||
@@ -168,7 +173,8 @@
|
||||
"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"
|
||||
"unlimited": "Illimitato",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Abilita la ricerca Marlin ",
|
||||
@@ -433,7 +439,7 @@
|
||||
"tags": "Tag",
|
||||
"quality_profile": "Profilo qualità",
|
||||
"root_folder": "Cartella radice",
|
||||
"season_x": "Stagione {{seasons}}",
|
||||
"season_all": "Season (all)",
|
||||
"season_number": "Stagione {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Episodio",
|
||||
"born": "Nato",
|
||||
|
||||
@@ -128,7 +128,12 @@
|
||||
"OTHER": "その他",
|
||||
"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": "カスタムメニューのリンクを表示",
|
||||
"hide_libraries": "ライブラリを非表示",
|
||||
"select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。",
|
||||
@@ -167,7 +172,8 @@
|
||||
"tv_quota_limit": "テレビのクオータ制限",
|
||||
"tv_quota_days": "テレビのクオータ日数",
|
||||
"reset_jellyseerr_config_button": "Jellyseerrの設定をリセット",
|
||||
"unlimited": "無制限"
|
||||
"unlimited": "無制限",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "マーリン検索を有効にする ",
|
||||
@@ -432,7 +438,7 @@
|
||||
"tags": "タグ",
|
||||
"quality_profile": "画質プロファイル",
|
||||
"root_folder": "ルートフォルダ",
|
||||
"season_x": "シーズン{{seasons}}",
|
||||
"season_all": "Season (all)",
|
||||
"season_number": "シーズン{{season_number}}",
|
||||
"number_episodes": "エピソード{{episode_number}}",
|
||||
"born": "生まれ",
|
||||
|
||||
@@ -128,7 +128,12 @@
|
||||
"OTHER": "Andere",
|
||||
"UNKNOWN": "Onbekend"
|
||||
},
|
||||
"safe_area_in_controls": "Veilig gebied in bedieningen",
|
||||
"safe_area_in_controls": "Veilig gebied in bedieningen",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"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.",
|
||||
@@ -168,7 +173,8 @@
|
||||
"tv_quota_limit": "Limiet serie quota",
|
||||
"tv_quota_days": "Serie Quota dagen",
|
||||
"reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen",
|
||||
"unlimited": "Ongelimiteerd"
|
||||
"unlimited": "Ongelimiteerd",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Marlin Search inschakelen ",
|
||||
@@ -433,7 +439,7 @@
|
||||
"tags": "Labels",
|
||||
"quality_profile": "Kwaliteitsprofiel",
|
||||
"root_folder": "Hoofdmap",
|
||||
"season_x": "Seizoen {{seasons}}",
|
||||
"season_all": "Season (all)",
|
||||
"season_number": "Seizoen {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Afleveringen",
|
||||
"born": "Geboren",
|
||||
|
||||
@@ -128,7 +128,12 @@
|
||||
"OTHER": "Diğer",
|
||||
"UNKNOWN": "Bilinmeyen"
|
||||
},
|
||||
"safe_area_in_controls": "Kontrollerde Güvenli Alan",
|
||||
"safe_area_in_controls": "Kontrollerde Güvenli Alan",
|
||||
"video_player": "Video player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"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.",
|
||||
@@ -167,7 +172,8 @@
|
||||
"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"
|
||||
"unlimited": "Sınırsız",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Marlin Aramasını Etkinleştir ",
|
||||
@@ -432,7 +438,7 @@
|
||||
"tags": "Etiketler",
|
||||
"quality_profile": "Kalite Profili",
|
||||
"root_folder": "Kök Klasör",
|
||||
"season_x": "Sezon {{seasons}}",
|
||||
"season_all": "Season (all)",
|
||||
"season_number": "Sezon {{season_number}}",
|
||||
"number_episodes": "Bölüm {{episode_number}}",
|
||||
"born": "Doğum",
|
||||
|
||||
@@ -128,7 +128,12 @@
|
||||
"OTHER": "其他",
|
||||
"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": "显示自定义菜单链接",
|
||||
"hide_libraries": "隐藏媒体库",
|
||||
"select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。",
|
||||
@@ -167,7 +172,8 @@
|
||||
"tv_quota_limit": "剧集配额限制",
|
||||
"tv_quota_days": "剧集配额天数",
|
||||
"reset_jellyseerr_config_button": "重置 Jellyseerr 设置",
|
||||
"unlimited": "无限制"
|
||||
"unlimited": "无限制",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "启用 Marlin 搜索",
|
||||
@@ -432,7 +438,7 @@
|
||||
"tags": "标签",
|
||||
"quality_profile": "质量配置文件",
|
||||
"root_folder": "根文件夹",
|
||||
"season_x": "第 {{seasons}} 季",
|
||||
"season_all": "Season (all)",
|
||||
"season_number": "第 {{season_number}} 季",
|
||||
"number_episodes": "{{episode_number}} 集",
|
||||
"born": "出生",
|
||||
|
||||
@@ -128,7 +128,12 @@
|
||||
"OTHER": "其他",
|
||||
"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": "顯示自定義菜單鏈接",
|
||||
"hide_libraries": "隱藏媒體庫",
|
||||
"select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
|
||||
@@ -167,7 +172,8 @@
|
||||
"tv_quota_limit": "電視配額限制",
|
||||
"tv_quota_days": "電視配額天數",
|
||||
"reset_jellyseerr_config_button": "重置 Jellyseerr 配置",
|
||||
"unlimited": "無限制"
|
||||
"unlimited": "無限制",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "啟用 Marlin 搜索",
|
||||
@@ -432,7 +438,7 @@
|
||||
"tags": "標籤",
|
||||
"quality_profile": "質量配置文件",
|
||||
"root_folder": "根文件夾",
|
||||
"season_x": "第 {{seasons}} 季",
|
||||
"season_all": "Season (all)",
|
||||
"season_number": "第 {{season_number}} 季",
|
||||
"number_episodes": "{{episode_number}} 集",
|
||||
"born": "出生",
|
||||
|
||||
@@ -14,6 +14,7 @@ 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";
|
||||
@@ -112,6 +113,12 @@ 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;
|
||||
@@ -146,6 +153,7 @@ export type Settings = {
|
||||
jellyseerrServerUrl?: string;
|
||||
hiddenLibraries?: string[];
|
||||
enableH265ForChromecast: boolean;
|
||||
defaultPlayer: VideoPlayer;
|
||||
};
|
||||
|
||||
export interface Lockable<T> {
|
||||
@@ -200,6 +208,7 @@ 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