mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-09 10:31:55 +01:00
Compare commits
43 Commits
v0.26.1
...
feat/switc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f07260177 | ||
|
|
09e9462ac0 | ||
|
|
dd65505f7f | ||
|
|
951158bcd3 | ||
|
|
9b1dd0923a | ||
|
|
bd908516b5 | ||
|
|
8cb10d1062 | ||
|
|
446439c2e0 | ||
|
|
a5463d783d | ||
|
|
640db35456 | ||
|
|
caa4b765c1 | ||
|
|
9c6aebe66a | ||
|
|
ef42510383 | ||
|
|
5273dfd22b | ||
|
|
00bc4232fb | ||
|
|
35c9258062 | ||
|
|
89bf51c3cc | ||
|
|
f64c5a02db | ||
|
|
cf284eb3d8 | ||
|
|
b581a077e1 | ||
|
|
e651b975b7 | ||
|
|
1c550b1b77 | ||
|
|
5bcae81538 | ||
|
|
c951725222 | ||
|
|
0b966d7c04 | ||
|
|
8e0e35afe3 | ||
|
|
daf7f35196 | ||
|
|
d5ac30b6d8 | ||
|
|
81b91bbb97 | ||
|
|
af2bd030e9 | ||
|
|
5590c2f784 | ||
|
|
6cc70dd123 | ||
|
|
fae588b0f0 | ||
|
|
bd2aeb2234 | ||
|
|
cca0bbf42c | ||
|
|
06e0eb5c4e | ||
|
|
b478fbb6bf | ||
|
|
b98a7b0634 | ||
|
|
ce38024a3f | ||
|
|
04dce9265b | ||
|
|
5b8418cd82 | ||
|
|
b0c5255bd7 | ||
|
|
73dd171987 |
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
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -43,6 +43,7 @@ body:
|
|||||||
label: Version
|
label: Version
|
||||||
description: What version of Streamyfin are you running?
|
description: What version of Streamyfin are you running?
|
||||||
options:
|
options:
|
||||||
|
- 0.27.0
|
||||||
- 0.26.1
|
- 0.26.1
|
||||||
- 0.26.0
|
- 0.26.0
|
||||||
- 0.25.0
|
- 0.25.0
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,6 +10,8 @@ npm-debug.*
|
|||||||
*.orig.*
|
*.orig.*
|
||||||
web-build/
|
web-build/
|
||||||
modules/vlc-player/android/build
|
modules/vlc-player/android/build
|
||||||
|
modules/vlc-player/android/.gradle
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -9,6 +9,7 @@
|
|||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true
|
||||||
},
|
},
|
||||||
|
"prettier.printWidth": 120,
|
||||||
"[swift]": {
|
"[swift]": {
|
||||||
"editor.defaultFormatter": "sswg.swift-lang"
|
"editor.defaultFormatter": "sswg.swift-lang"
|
||||||
}
|
}
|
||||||
|
|||||||
6
Makefile
Normal file
6
Makefile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
e2e:
|
||||||
|
maestro start-device --platform android
|
||||||
|
maestro test login.yaml
|
||||||
|
|
||||||
|
e2e-setup:
|
||||||
|
curl -fsSL "https://get.maestro.mobile.dev" | bash
|
||||||
6
app.json
6
app.json
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Streamyfin",
|
"name": "Streamyfin",
|
||||||
"slug": "streamyfin",
|
"slug": "streamyfin",
|
||||||
"version": "0.26.1",
|
"version": "0.27.0",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "streamyfin",
|
"scheme": "streamyfin",
|
||||||
@@ -33,7 +33,9 @@
|
|||||||
"jsEngine": "hermes",
|
"jsEngine": "hermes",
|
||||||
"versionCode": 53,
|
"versionCode": 53,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/images/adaptive_icon.png"
|
"foregroundImage": "./assets/images/adaptive_icon.png",
|
||||||
|
"backgroundColor": "#464646"
|
||||||
|
|
||||||
},
|
},
|
||||||
"package": "com.fredrikburmester.streamyfin",
|
"package": "com.fredrikburmester.streamyfin",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Ionicons, Feather } from "@expo/vector-icons";
|
||||||
import { Stack, useRouter } from "expo-router";
|
import { Stack, useRouter } from "expo-router";
|
||||||
import { Platform, TouchableOpacity, View } from "react-native";
|
import { Platform, TouchableOpacity, View } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
const Chromecast = !Platform.isTV ? require("@/components/Chromecast") : null;
|
const Chromecast = !Platform.isTV ? require("@/components/Chromecast") : null;
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { useSessions, useSessionsProps } from "@/hooks/useSessions";
|
||||||
|
|
||||||
export default function IndexLayout() {
|
export default function IndexLayout() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [user] = useAtom(userAtom);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -27,13 +32,10 @@ export default function IndexLayout() {
|
|||||||
{!Platform.isTV && (
|
{!Platform.isTV && (
|
||||||
<>
|
<>
|
||||||
<Chromecast.Chromecast />
|
<Chromecast.Chromecast />
|
||||||
<TouchableOpacity
|
{user && user.Policy?.IsAdministrator && (
|
||||||
onPress={() => {
|
<SessionsButton />
|
||||||
router.push("/(auth)/settings");
|
)}
|
||||||
}}
|
<SettingsButton />
|
||||||
>
|
|
||||||
<Feather name="settings" color={"white"} size={22} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -52,6 +54,12 @@ export default function IndexLayout() {
|
|||||||
title: t("home.downloads.tvseries"),
|
title: t("home.downloads.tvseries"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="sessions/index"
|
||||||
|
options={{
|
||||||
|
title: t("home.sessions.title"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="settings"
|
name="settings"
|
||||||
options={{
|
options={{
|
||||||
@@ -112,3 +120,38 @@ export default function IndexLayout() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SettingsButton = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/(auth)/settings");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Feather name="settings" color={"white"} size={22} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SessionsButton = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { sessions = [], _ } = useSessions({} as useSessionsProps);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/(auth)/sessions");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="mr-4">
|
||||||
|
<Ionicons
|
||||||
|
name="play-circle"
|
||||||
|
color={sessions.length === 0 ? "white" : "#9333ea"}
|
||||||
|
size={25}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SettingsIndex } from "@/components/settings/SettingsIndex";
|
import { HomeIndex } from "@/components/settings/HomeIndex";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
return <SettingsIndex />;
|
return <HomeIndex />;
|
||||||
}
|
}
|
||||||
|
|||||||
365
app/(auth)/(tabs)/(home)/sessions/index.tsx
Normal file
365
app/(auth)/(tabs)/(home)/sessions/index.tsx
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { useSessions, useSessionsProps } from "@/hooks/useSessions";
|
||||||
|
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 { useAtomValue } from "jotai";
|
||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import Poster from "@/components/posters/Poster";
|
||||||
|
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||||
|
import { useInterval } from "@/hooks/useInterval";
|
||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import { formatTimeString } from "@/utils/time";
|
||||||
|
import { formatBitrate } from "@/utils/bitrate";
|
||||||
|
import {
|
||||||
|
Ionicons,
|
||||||
|
Entypo,
|
||||||
|
AntDesign,
|
||||||
|
MaterialCommunityIcons,
|
||||||
|
} from "@expo/vector-icons";
|
||||||
|
import { Badge } from "@/components/Badge";
|
||||||
|
|
||||||
|
export default function page() {
|
||||||
|
const { sessions, isLoading } = useSessions({} as useSessionsProps);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<View className="justify-center items-center h-full">
|
||||||
|
<Loader />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sessions || sessions.length == 0)
|
||||||
|
return (
|
||||||
|
<View className="h-full w-full flex justify-center items-center">
|
||||||
|
<Text className="text-lg text-neutral-500">
|
||||||
|
{t("home.sessions.no_active_sessions")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlashList
|
||||||
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingTop: 17,
|
||||||
|
paddingHorizontal: 17,
|
||||||
|
paddingBottom: 150,
|
||||||
|
}}
|
||||||
|
data={sessions}
|
||||||
|
renderItem={({ item }) => <SessionCard session={item} />}
|
||||||
|
keyExtractor={(item) => item.Id || ""}
|
||||||
|
estimatedItemSize={200}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionCardProps {
|
||||||
|
session: SessionInfoDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SessionCard = ({ session }: SessionCardProps) => {
|
||||||
|
const api = useAtomValue(apiAtom);
|
||||||
|
const [remainingTicks, setRemainingTicks] = useState<number>(0);
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (session.PlayState?.IsPaused) return;
|
||||||
|
setRemainingTicks(remainingTicks - 10000000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgressPercentage = () => {
|
||||||
|
if (!session.NowPlayingItem || !session.NowPlayingItem.RunTimeTicks) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(
|
||||||
|
(100 / session.NowPlayingItem?.RunTimeTicks) *
|
||||||
|
(session.NowPlayingItem?.RunTimeTicks - remainingTicks)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentTime = session.PlayState?.PositionTicks;
|
||||||
|
const duration = session.NowPlayingItem?.RunTimeTicks;
|
||||||
|
if (
|
||||||
|
duration !== null &&
|
||||||
|
duration !== undefined &&
|
||||||
|
currentTime !== null &&
|
||||||
|
currentTime !== undefined
|
||||||
|
) {
|
||||||
|
const remainingTimeTicks = duration - currentTime;
|
||||||
|
setRemainingTicks(remainingTimeTicks);
|
||||||
|
}
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
useInterval(tick, 1000);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex flex-col shadow-md bg-neutral-900 rounded-2xl mb-4">
|
||||||
|
<View className="flex flex-row p-4">
|
||||||
|
<View className="w-20 pr-4">
|
||||||
|
<Poster
|
||||||
|
id={session.NowPlayingItem?.Id}
|
||||||
|
url={getPrimaryImageUrl({ api, item: session.NowPlayingItem })}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="w-full flex-1">
|
||||||
|
<View className="flex flex-row justify-between">
|
||||||
|
<View className="flex-1 pr-4">
|
||||||
|
{session.NowPlayingItem?.Type === "Episode" ? (
|
||||||
|
<>
|
||||||
|
<Text className="font-bold">
|
||||||
|
{session.NowPlayingItem?.Name}
|
||||||
|
</Text>
|
||||||
|
<Text numberOfLines={1} className="text-xs opacity-50">
|
||||||
|
{`S${session.NowPlayingItem.ParentIndexNumber?.toString()}:E${session.NowPlayingItem.IndexNumber?.toString()}`}
|
||||||
|
{" - "}
|
||||||
|
{session.NowPlayingItem.SeriesName}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text className="font-bold">
|
||||||
|
{session.NowPlayingItem?.Name}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs opacity-50">
|
||||||
|
{session.NowPlayingItem?.ProductionYear}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs opacity-50">
|
||||||
|
{session.NowPlayingItem?.SeriesName}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text className="text-xs opacity-50 align-right text-right">
|
||||||
|
{session.UserName}
|
||||||
|
{"\n"}
|
||||||
|
{session.Client}
|
||||||
|
{"\n"}
|
||||||
|
{session.DeviceName}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View className="flex-1" />
|
||||||
|
<View className="flex flex-col align-bottom">
|
||||||
|
<View className="flex flex-row justify-between align-bottom mb-1">
|
||||||
|
<Text className="-ml-0.5 text-xs opacity-50 align-left text-left">
|
||||||
|
{!session.PlayState?.IsPaused ? (
|
||||||
|
<Ionicons name="play" size={14} color="white" />
|
||||||
|
) : (
|
||||||
|
<Ionicons name="pause" size={14} color="white" />
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs opacity-50 align-right text-right">
|
||||||
|
{formatTimeString(remainingTicks, "tick")} left
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View className="align-bottom bg-gray-800 h-1">
|
||||||
|
<View
|
||||||
|
className={`bg-purple-600 h-full`}
|
||||||
|
style={{
|
||||||
|
width: `${getProgressPercentage()}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<TranscodingView session={session} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TranscodingBadgesProps {
|
||||||
|
properties: StreamProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => {
|
||||||
|
const iconMap = {
|
||||||
|
bitrate: <Ionicons name="speedometer-outline" size={12} color="white" />,
|
||||||
|
codec: <Ionicons name="layers-outline" size={12} color="white" />,
|
||||||
|
videoRange: (
|
||||||
|
<Ionicons name="color-palette-outline" size={12} color="white" />
|
||||||
|
),
|
||||||
|
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) => {
|
||||||
|
return (
|
||||||
|
iconMap[val as keyof typeof iconMap] ?? (
|
||||||
|
<Ionicons name="layers-outline" size={12} color="white" />
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatVal = (key: string, val: any) => {
|
||||||
|
switch (key) {
|
||||||
|
case "bitrate":
|
||||||
|
return formatBitrate(val);
|
||||||
|
case "hwType":
|
||||||
|
return val === HardwareAccelerationType.None ? "sw" : "hw";
|
||||||
|
default:
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.entries(properties)
|
||||||
|
.filter(([_, value]) => value !== undefined && value !== null)
|
||||||
|
.map(([key]) => (
|
||||||
|
<Badge
|
||||||
|
key={key}
|
||||||
|
variant="gray"
|
||||||
|
className="m-0 p-0 pt-0.5 mr-1"
|
||||||
|
text={formatVal(key, properties[key as keyof StreamProps])}
|
||||||
|
iconLeft={icon(key)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StreamProps {
|
||||||
|
hwType?: HardwareAccelerationType | null | undefined;
|
||||||
|
resolution?: string | null | undefined;
|
||||||
|
language?: string | null | undefined;
|
||||||
|
codec?: string | null | undefined;
|
||||||
|
bitrate?: number | null | undefined;
|
||||||
|
videoRange?: string | null | undefined;
|
||||||
|
audioChannels?: string | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TranscodingStreamViewProps {
|
||||||
|
title: string | undefined;
|
||||||
|
value?: string;
|
||||||
|
isTranscoding: Boolean;
|
||||||
|
transcodeValue?: string | undefined | null;
|
||||||
|
properties: StreamProps;
|
||||||
|
transcodeProperties?: StreamProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TranscodingStreamView = ({
|
||||||
|
title,
|
||||||
|
isTranscoding,
|
||||||
|
properties,
|
||||||
|
transcodeProperties,
|
||||||
|
value,
|
||||||
|
transcodeValue,
|
||||||
|
}: TranscodingStreamViewProps) => {
|
||||||
|
return (
|
||||||
|
<View className="flex flex-col pt-2 first:pt-0">
|
||||||
|
<View className="flex flex-row">
|
||||||
|
<Text className="text-xs opacity-50 w-20 font-bold text-right pr-4">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text className="flex-1">
|
||||||
|
<TranscodingBadges properties={properties} />
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{isTranscoding && transcodeProperties ? (
|
||||||
|
<>
|
||||||
|
<View className="flex flex-row">
|
||||||
|
<Text className="-mt-0 text-xs opacity-50 w-20 font-bold text-right pr-4">
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="arrow-right-bottom"
|
||||||
|
size={14}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
<Text className="flex-1 text-sm mt-1">
|
||||||
|
<TranscodingBadges properties={transcodeProperties} />
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TranscodingView = ({ session }: SessionCardProps) => {
|
||||||
|
const videoStream = useMemo(() => {
|
||||||
|
return session.NowPlayingItem?.MediaStreams?.filter(
|
||||||
|
(s) => s.Type == "Video"
|
||||||
|
)[0];
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
const audioStream = useMemo(() => {
|
||||||
|
const index = session.PlayState?.AudioStreamIndex;
|
||||||
|
return index !== null && index !== undefined
|
||||||
|
? session.NowPlayingItem?.MediaStreams?.[index]
|
||||||
|
: undefined;
|
||||||
|
}, [session.PlayState?.AudioStreamIndex]);
|
||||||
|
|
||||||
|
const subtitleStream = useMemo(() => {
|
||||||
|
const index = session.PlayState?.SubtitleStreamIndex;
|
||||||
|
return index !== null && index !== undefined
|
||||||
|
? session.NowPlayingItem?.MediaStreams?.[index]
|
||||||
|
: undefined;
|
||||||
|
}, [session.PlayState?.SubtitleStreamIndex]);
|
||||||
|
|
||||||
|
const isTranscoding = useMemo(() => {
|
||||||
|
return session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo;
|
||||||
|
}, [session.PlayState?.PlayMethod, session.TranscodingInfo]);
|
||||||
|
|
||||||
|
const videoStreamTitle = () => {
|
||||||
|
return videoStream?.DisplayTitle?.split(" ")[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex flex-col bg-neutral-800 rounded-b-2xl p-4 pt-2">
|
||||||
|
<TranscodingStreamView
|
||||||
|
title="Video"
|
||||||
|
properties={{
|
||||||
|
resolution: videoStreamTitle(),
|
||||||
|
bitrate: videoStream?.BitRate,
|
||||||
|
codec: videoStream?.Codec,
|
||||||
|
}}
|
||||||
|
transcodeProperties={{
|
||||||
|
hwType: session.TranscodingInfo?.HardwareAccelerationType,
|
||||||
|
bitrate: session.TranscodingInfo?.Bitrate,
|
||||||
|
codec: session.TranscodingInfo?.VideoCodec,
|
||||||
|
}}
|
||||||
|
isTranscoding={
|
||||||
|
isTranscoding && !session.TranscodingInfo?.IsVideoDirect
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TranscodingStreamView
|
||||||
|
title="Audio"
|
||||||
|
properties={{
|
||||||
|
language: audioStream?.Language,
|
||||||
|
bitrate: audioStream?.BitRate,
|
||||||
|
codec: audioStream?.Codec,
|
||||||
|
audioChannels: audioStream?.ChannelLayout,
|
||||||
|
}}
|
||||||
|
transcodeProperties={{
|
||||||
|
codec: session.TranscodingInfo?.AudioCodec,
|
||||||
|
audioChannels: session.TranscodingInfo?.AudioChannels?.toString(),
|
||||||
|
}}
|
||||||
|
isTranscoding={
|
||||||
|
isTranscoding && !session.TranscodingInfo?.IsVideoDirect
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{subtitleStream && (
|
||||||
|
<>
|
||||||
|
<TranscodingStreamView
|
||||||
|
title="Subtitle"
|
||||||
|
isTranscoding={false}
|
||||||
|
properties={{
|
||||||
|
language: subtitleStream?.Language,
|
||||||
|
codec: subtitleStream?.Codec,
|
||||||
|
}}
|
||||||
|
transcodeValue={null}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,12 +19,16 @@ import { storage } from "@/utils/mmkv";
|
|||||||
import { useNavigation, useRouter } from "expo-router";
|
import { useNavigation, useRouter } from "expo-router";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { ScrollView, TouchableOpacity, View } from "react-native";
|
import { ScrollView, Switch, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { ChromecastSettings } from "@/components/settings/ChromecastSettings";
|
||||||
|
|
||||||
export default function settings() {
|
export default function settings() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const [user] = useAtom(userAtom);
|
||||||
const { logout } = useJellyfin();
|
const { logout } = useJellyfin();
|
||||||
const successHapticFeedback = useHaptic("success");
|
const successHapticFeedback = useHaptic("success");
|
||||||
|
|
||||||
@@ -59,6 +63,7 @@ export default function settings() {
|
|||||||
>
|
>
|
||||||
<View className="p-4 flex flex-col gap-y-4">
|
<View className="p-4 flex flex-col gap-y-4">
|
||||||
<UserInfo />
|
<UserInfo />
|
||||||
|
|
||||||
<QuickConnect className="mb-4" />
|
<QuickConnect className="mb-4" />
|
||||||
|
|
||||||
<MediaProvider>
|
<MediaProvider>
|
||||||
@@ -75,6 +80,8 @@ export default function settings() {
|
|||||||
|
|
||||||
<AppLanguageSelector />
|
<AppLanguageSelector />
|
||||||
|
|
||||||
|
<ChromecastSettings />
|
||||||
|
|
||||||
<ListGroup title={"Intro"}>
|
<ListGroup title={"Intro"}>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function page() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return await getStatistics({
|
return await getStatistics({
|
||||||
url: settings?.optimizedVersionsServerUrl,
|
url: updatedUrl,
|
||||||
authHeader: api?.accessToken,
|
authHeader: api?.accessToken,
|
||||||
deviceId: getOrSetDeviceId(),
|
deviceId: getOrSetDeviceId(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,25 +42,28 @@ const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
|||||||
import RequestModal from "@/components/jellyseerr/RequestModal";
|
import RequestModal from "@/components/jellyseerr/RequestModal";
|
||||||
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
|
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
|
||||||
import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
||||||
|
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
|
||||||
|
|
||||||
const Page: React.FC = () => {
|
const Page: React.FC = () => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { mediaTitle, releaseYear, posterSrc, ...result } =
|
const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } =
|
||||||
params as unknown as {
|
params as unknown as {
|
||||||
mediaTitle: string;
|
mediaTitle: string;
|
||||||
releaseYear: number;
|
releaseYear: number;
|
||||||
canRequest: string;
|
canRequest: string;
|
||||||
posterSrc: string;
|
posterSrc: string;
|
||||||
} & Partial<MovieResult | TvResult>;
|
mediaType: MediaType;
|
||||||
|
} & Partial<MovieResult | TvResult | MovieDetails | TvDetails>;
|
||||||
|
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const { jellyseerrApi, requestMedia } = useJellyseerr();
|
const { jellyseerrApi, requestMedia } = useJellyseerr();
|
||||||
|
|
||||||
const [issueType, setIssueType] = useState<IssueType>();
|
const [issueType, setIssueType] = useState<IssueType>();
|
||||||
const [issueMessage, setIssueMessage] = useState<string>();
|
const [issueMessage, setIssueMessage] = useState<string>();
|
||||||
|
const [requestBody, _setRequestBody] = useState<MediaRequestBody>();
|
||||||
const advancedReqModalRef = useRef<BottomSheetModal>(null);
|
const advancedReqModalRef = useRef<BottomSheetModal>(null);
|
||||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||||
|
|
||||||
@@ -71,7 +74,7 @@ const Page: React.FC = () => {
|
|||||||
refetch,
|
refetch,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
enabled: !!jellyseerrApi && !!result && !!result.id,
|
enabled: !!jellyseerrApi && !!result && !!result.id,
|
||||||
queryKey: ["jellyseerr", "detail", result.mediaType, result.id],
|
queryKey: ["jellyseerr", "detail", mediaType, result.id],
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
refetchOnReconnect: true,
|
refetchOnReconnect: true,
|
||||||
@@ -79,7 +82,7 @@ const Page: React.FC = () => {
|
|||||||
retryOnMount: true,
|
retryOnMount: true,
|
||||||
refetchInterval: 0,
|
refetchInterval: 0,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return result.mediaType === MediaType.MOVIE
|
return mediaType === MediaType.MOVIE
|
||||||
? jellyseerrApi?.movieDetails(result.id!!)
|
? jellyseerrApi?.movieDetails(result.id!!)
|
||||||
: jellyseerrApi?.tvDetails(result.id!!);
|
: jellyseerrApi?.tvDetails(result.id!!);
|
||||||
},
|
},
|
||||||
@@ -111,10 +114,15 @@ const Page: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [jellyseerrApi, details, result, issueType, issueMessage]);
|
}, [jellyseerrApi, details, result, issueType, issueMessage]);
|
||||||
|
|
||||||
|
const setRequestBody = useCallback((body: MediaRequestBody) => {
|
||||||
|
_setRequestBody(body)
|
||||||
|
advancedReqModalRef?.current?.present?.();
|
||||||
|
}, [requestBody, _setRequestBody, advancedReqModalRef])
|
||||||
|
|
||||||
const request = useCallback(async () => {
|
const request = useCallback(async () => {
|
||||||
const body: MediaRequestBody = {
|
const body: MediaRequestBody = {
|
||||||
mediaId: Number(result.id!!),
|
mediaId: Number(result.id!!),
|
||||||
mediaType: result.mediaType!!,
|
mediaType: mediaType!!,
|
||||||
tvdbId: details?.externalIds?.tvdbId,
|
tvdbId: details?.externalIds?.tvdbId,
|
||||||
seasons: (details as TvDetails)?.seasons
|
seasons: (details as TvDetails)?.seasons
|
||||||
?.filter?.((s) => s.seasonNumber !== 0)
|
?.filter?.((s) => s.seasonNumber !== 0)
|
||||||
@@ -122,7 +130,7 @@ const Page: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (hasAdvancedRequestPermission) {
|
if (hasAdvancedRequestPermission) {
|
||||||
advancedReqModalRef?.current?.present?.(body);
|
setRequestBody(body)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +140,7 @@ const Page: React.FC = () => {
|
|||||||
const isAnime = useMemo(
|
const isAnime = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) &&
|
(details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) &&
|
||||||
result.mediaType === MediaType.TV,
|
mediaType === MediaType.TV,
|
||||||
[details]
|
[details]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -200,7 +208,7 @@ const Page: React.FC = () => {
|
|||||||
<View className="px-4">
|
<View className="px-4">
|
||||||
<View className="flex flex-row justify-between w-full">
|
<View className="flex flex-row justify-between w-full">
|
||||||
<View className="flex flex-col w-56">
|
<View className="flex flex-col w-56">
|
||||||
<JellyserrRatings result={result as MovieResult | TvResult} />
|
<JellyserrRatings result={result as MovieResult | TvResult | MovieDetails | TvDetails} />
|
||||||
<Text
|
<Text
|
||||||
uiTextView
|
uiTextView
|
||||||
selectable
|
selectable
|
||||||
@@ -247,15 +255,14 @@ const Page: React.FC = () => {
|
|||||||
<OverviewText text={result.overview} className="mt-4" />
|
<OverviewText text={result.overview} className="mt-4" />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{result.mediaType === MediaType.TV && (
|
{mediaType === MediaType.TV && (
|
||||||
<JellyseerrSeasons
|
<JellyseerrSeasons
|
||||||
isLoading={isLoading || isFetching}
|
isLoading={isLoading || isFetching}
|
||||||
result={result as TvResult}
|
|
||||||
details={details as TvDetails}
|
details={details as TvDetails}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
hasAdvancedRequest={hasAdvancedRequestPermission}
|
hasAdvancedRequest={hasAdvancedRequestPermission}
|
||||||
onAdvancedRequest={(data) =>
|
onAdvancedRequest={(data) =>
|
||||||
advancedReqModalRef?.current?.present(data)
|
setRequestBody(data)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -269,14 +276,17 @@ const Page: React.FC = () => {
|
|||||||
</ParallaxScrollView>
|
</ParallaxScrollView>
|
||||||
<RequestModal
|
<RequestModal
|
||||||
ref={advancedReqModalRef}
|
ref={advancedReqModalRef}
|
||||||
|
requestBody={requestBody}
|
||||||
title={mediaTitle}
|
title={mediaTitle}
|
||||||
id={result.id!!}
|
id={result.id!!}
|
||||||
type={result.mediaType as MediaType}
|
type={mediaType}
|
||||||
isAnime={isAnime}
|
isAnime={isAnime}
|
||||||
onRequested={() => {
|
onRequested={() => {
|
||||||
|
_setRequestBody(undefined)
|
||||||
advancedReqModalRef?.current?.close();
|
advancedReqModalRef?.current?.close();
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
|
onDismiss={() => _setRequestBody(undefined)}
|
||||||
/>
|
/>
|
||||||
<BottomSheetModal
|
<BottomSheetModal
|
||||||
ref={bottomSheetModalRef}
|
ref={bottomSheetModalRef}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { Image } from "expo-image";
|
|||||||
import { useLocalSearchParams, useNavigation } from "expo-router";
|
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useEffect, useMemo } from "react";
|
import React, { useEffect, useMemo } from "react";
|
||||||
import { View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const page: React.FC = () => {
|
const page: React.FC = () => {
|
||||||
@@ -84,22 +84,26 @@ const page: React.FC = () => {
|
|||||||
allEpisodes &&
|
allEpisodes &&
|
||||||
allEpisodes.length > 0 && (
|
allEpisodes.length > 0 && (
|
||||||
<View className="flex flex-row items-center space-x-2">
|
<View className="flex flex-row items-center space-x-2">
|
||||||
<AddToFavorites item={item} type="series" />
|
<AddToFavorites item={item} />
|
||||||
<DownloadItems
|
{!Platform.isTV && (
|
||||||
size="large"
|
<>
|
||||||
title={t("item_card.download.download_series")}
|
<DownloadItems
|
||||||
items={allEpisodes || []}
|
size="large"
|
||||||
MissingDownloadIconComponent={() => (
|
title={t("item_card.download.download_series")}
|
||||||
<Ionicons name="download" size={22} color="white" />
|
items={allEpisodes || []}
|
||||||
)}
|
MissingDownloadIconComponent={() => (
|
||||||
DownloadedIconComponent={() => (
|
<Ionicons name="download" size={22} color="white" />
|
||||||
<Ionicons
|
)}
|
||||||
name="checkmark-done-outline"
|
DownloadedIconComponent={() => (
|
||||||
size={24}
|
<Ionicons
|
||||||
color="#9333ea"
|
name="checkmark-done-outline"
|
||||||
|
size={24}
|
||||||
|
color="#9333ea"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
</>
|
||||||
/>
|
)}
|
||||||
</View>
|
</View>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
|
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { eventBus } from "@/utils/eventBus";
|
||||||
|
|
||||||
type SearchType = "Library" | "Discover";
|
type SearchType = "Library" | "Discover";
|
||||||
|
|
||||||
@@ -120,21 +122,44 @@ export default function search() {
|
|||||||
[api, searchEngine, settings]
|
[api, searchEngine, settings]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type HeaderSearchBarRef = {
|
||||||
|
focus: () => void;
|
||||||
|
blur: () => void;
|
||||||
|
setText: (text: string) => void;
|
||||||
|
clearText: () => void;
|
||||||
|
cancelSearch: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchBarRef = useRef<HeaderSearchBarRef>(null);
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerSearchBarOptions: {
|
headerSearchBarOptions: {
|
||||||
|
ref: searchBarRef,
|
||||||
placeholder: t("search.search"),
|
placeholder: t("search.search"),
|
||||||
onChangeText: (e: any) => {
|
onChangeText: (e: any) => {
|
||||||
router.setParams({ q: "" });
|
router.setParams({ q: "" });
|
||||||
setSearch(e.nativeEvent.text);
|
setSearch(e.nativeEvent.text);
|
||||||
},
|
},
|
||||||
hideWhenScrolling: false,
|
hideWhenScrolling: false,
|
||||||
autoFocus: true,
|
autoFocus: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = eventBus.on("searchTabPressed", () => {
|
||||||
|
// Screen not actuve
|
||||||
|
if (!searchBarRef.current) return;
|
||||||
|
// Screen is active, focus search bar
|
||||||
|
searchBarRef.current?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { data: movies, isFetching: l1 } = useQuery({
|
const { data: movies, isFetching: l1 } = useQuery({
|
||||||
queryKey: ["search", "movies", debouncedSearch],
|
queryKey: ["search", "movies", debouncedSearch],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "@bottom-tabs/react-navigation";
|
} from "@bottom-tabs/react-navigation";
|
||||||
|
|
||||||
const { Navigator } = createNativeBottomTabNavigator();
|
const { Navigator } = createNativeBottomTabNavigator();
|
||||||
|
|
||||||
import { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs";
|
import { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs";
|
||||||
|
|
||||||
import { Colors } from "@/constants/Colors";
|
import { Colors } from "@/constants/Colors";
|
||||||
@@ -21,6 +20,7 @@ import type {
|
|||||||
TabNavigationState,
|
TabNavigationState,
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { SystemBars } from "react-native-edge-to-edge";
|
import { SystemBars } from "react-native-edge-to-edge";
|
||||||
|
import { eventBus } from "@/utils/eventBus";
|
||||||
|
|
||||||
export const NativeTabs = withLayoutContext<
|
export const NativeTabs = withLayoutContext<
|
||||||
BottomTabNavigationOptions,
|
BottomTabNavigationOptions,
|
||||||
@@ -63,6 +63,11 @@ export default function TabLayout() {
|
|||||||
>
|
>
|
||||||
<NativeTabs.Screen redirect name="index" />
|
<NativeTabs.Screen redirect name="index" />
|
||||||
<NativeTabs.Screen
|
<NativeTabs.Screen
|
||||||
|
listeners={({ navigation }) => ({
|
||||||
|
tabPress: (e) => {
|
||||||
|
eventBus.emit("scrollToTop");
|
||||||
|
},
|
||||||
|
})}
|
||||||
name="(home)"
|
name="(home)"
|
||||||
options={{
|
options={{
|
||||||
title: t("tabs.home"),
|
title: t("tabs.home"),
|
||||||
@@ -77,6 +82,11 @@ export default function TabLayout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<NativeTabs.Screen
|
<NativeTabs.Screen
|
||||||
|
listeners={({ navigation }) => ({
|
||||||
|
tabPress: (e) => {
|
||||||
|
eventBus.emit("searchTabPressed");
|
||||||
|
},
|
||||||
|
})}
|
||||||
name="(search)"
|
name="(search)"
|
||||||
options={{
|
options={{
|
||||||
title: t("tabs.search"),
|
title: t("tabs.search"),
|
||||||
|
|||||||
@@ -3,16 +3,21 @@ import React, { useEffect } from "react";
|
|||||||
import { SystemBars } from "react-native-edge-to-edge";
|
import { SystemBars } from "react-native-edge-to-edge";
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (Platform.isTV) return;
|
||||||
|
|
||||||
if (settings.defaultVideoOrientation) {
|
if (settings.defaultVideoOrientation) {
|
||||||
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
|
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (Platform.isTV) return;
|
||||||
|
|
||||||
if (settings.autoRotate === true) {
|
if (settings.autoRotate === true) {
|
||||||
ScreenOrientation.unlockAsync();
|
ScreenOrientation.unlockAsync();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,47 +5,40 @@ import { Controls } from "@/components/video-player/controls/Controls";
|
|||||||
import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener";
|
import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener";
|
||||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
import { useWebSocket } from "@/hooks/useWebsockets";
|
||||||
import { VlcPlayerView } from "@/modules/vlc-player";
|
import { VlcPlayerView } from "@/modules";
|
||||||
import {
|
import {
|
||||||
PipStartedPayload,
|
PipStartedPayload,
|
||||||
PlaybackStatePayload,
|
PlaybackStatePayload,
|
||||||
ProgressUpdatePayload,
|
ProgressUpdatePayload,
|
||||||
VlcPlayerViewRef,
|
VlcPlayerViewRef,
|
||||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
} from "@/modules/VlcPlayer.types";
|
||||||
// import { useDownload } from "@/providers/DownloadProvider";
|
const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null;
|
||||||
const downloadProvider = !Platform.isTV
|
|
||||||
? require("@/providers/DownloadProvider")
|
|
||||||
: null;
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import native from "@/utils/profiles/native";
|
import native from "@/utils/profiles/native";
|
||||||
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
||||||
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
||||||
import {
|
import { getPlaystateApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
getPlaystateApi,
|
|
||||||
getUserLibraryApi,
|
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useGlobalSearchParams, useNavigation } from "expo-router";
|
import { useGlobalSearchParams, useNavigation } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, {
|
import React, { useCallback, useMemo, useRef, useState, useEffect } from "react";
|
||||||
useCallback,
|
import { Alert, View, Platform } from "react-native";
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
} from "react";
|
|
||||||
import { Alert, View, AppState, AppStateStatus, Platform } from "react-native";
|
|
||||||
import { useSharedValue } from "react-native-reanimated";
|
import { useSharedValue } from "react-native-reanimated";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client";
|
import {
|
||||||
|
BaseItemDto,
|
||||||
|
MediaSourceInfo,
|
||||||
|
PlaybackOrder,
|
||||||
|
PlaybackProgressInfo,
|
||||||
|
PlaybackStartInfo,
|
||||||
|
RepeatMode,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
console.log("Direct Player");
|
|
||||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
@@ -93,152 +86,115 @@ export default function page() {
|
|||||||
offline: string;
|
offline: string;
|
||||||
}>();
|
}>();
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
const offline = offlineStr === "true";
|
const offline = offlineStr === "true";
|
||||||
|
|
||||||
const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined;
|
const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined;
|
||||||
const subtitleIndex = subtitleIndexStr ? parseInt(subtitleIndexStr, 10) : -1;
|
const subtitleIndex = subtitleIndexStr ? parseInt(subtitleIndexStr, 10) : -1;
|
||||||
const bitrateValue = bitrateValueStr
|
const bitrateValue = bitrateValueStr ? parseInt(bitrateValueStr, 10) : BITRATES[0].value;
|
||||||
? parseInt(bitrateValueStr, 10)
|
|
||||||
: BITRATES[0].value;
|
|
||||||
|
|
||||||
const {
|
const [item, setItem] = useState<BaseItemDto | null>(null);
|
||||||
data: item,
|
const [itemStatus, setItemStatus] = useState({
|
||||||
isLoading: isLoadingItem,
|
isLoading: true,
|
||||||
isError: isErrorItem,
|
isError: false,
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["item", itemId],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (offline && !Platform.isTV) {
|
|
||||||
const item = await getDownloadedItem.getDownloadedItem(itemId);
|
|
||||||
if (item) return item.item;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await getUserLibraryApi(api!).getItem({
|
|
||||||
itemId,
|
|
||||||
userId: user?.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
enabled: !!itemId,
|
|
||||||
staleTime: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [stream, setStream] = useState<{
|
|
||||||
mediaSource: MediaSourceInfo;
|
|
||||||
url: string;
|
|
||||||
sessionId: string | undefined;
|
|
||||||
} | null>(null);
|
|
||||||
const [isLoadingStream, setIsLoadingStream] = useState(true);
|
|
||||||
const [isErrorStream, setIsErrorStream] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStream = async () => {
|
const fetchItemData = async () => {
|
||||||
setIsLoadingStream(true);
|
setItemStatus({ isLoading: true, isError: false });
|
||||||
setIsErrorStream(false);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let fetchedItem: BaseItemDto | null = null;
|
||||||
if (offline && !Platform.isTV) {
|
if (offline && !Platform.isTV) {
|
||||||
const data = await getDownloadedItem.getDownloadedItem(itemId);
|
const data = await getDownloadedItem.getDownloadedItem(itemId);
|
||||||
if (!data?.mediaSource) {
|
if (data) fetchedItem = data.item as BaseItemDto;
|
||||||
setStream(null);
|
} else {
|
||||||
return;
|
const res = await getUserLibraryApi(api!).getItem({
|
||||||
}
|
itemId,
|
||||||
|
userId: user?.Id,
|
||||||
const url = await getDownloadedFileUrl(data.item.Id!);
|
});
|
||||||
|
fetchedItem = res.data;
|
||||||
if (item) {
|
|
||||||
setStream({
|
|
||||||
mediaSource: data.mediaSource as MediaSourceInfo,
|
|
||||||
url,
|
|
||||||
sessionId: undefined,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setItem(fetchedItem);
|
||||||
const res = await getStreamUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
|
||||||
userId: user?.Id,
|
|
||||||
audioStreamIndex: audioIndex,
|
|
||||||
maxStreamingBitrate: bitrateValue,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
subtitleStreamIndex: subtitleIndex,
|
|
||||||
deviceProfile: native,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
setStream(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { mediaSource, sessionId, url } = res;
|
|
||||||
|
|
||||||
if (!sessionId || !mediaSource || !url) {
|
|
||||||
Alert.alert(t("player.error"), t("player.failed_to_get_stream_url"));
|
|
||||||
setStream(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStream({
|
|
||||||
mediaSource,
|
|
||||||
sessionId,
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching stream:", error);
|
console.error("Failed to fetch item:", error);
|
||||||
setIsErrorStream(true);
|
setItemStatus({ isLoading: false, isError: true });
|
||||||
setStream(null);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingStream(false);
|
setItemStatus({ isLoading: false, isError: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchStream();
|
if (itemId) {
|
||||||
}, [itemId, mediaSourceId]);
|
fetchItemData();
|
||||||
|
}
|
||||||
|
}, [itemId, offline, api, user?.Id]);
|
||||||
|
|
||||||
const togglePlay = useCallback(async () => {
|
interface Stream {
|
||||||
if (!api) return;
|
mediaSource: MediaSourceInfo;
|
||||||
|
sessionId: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [stream, setStream] = useState<Stream | null>(null);
|
||||||
|
const [streamStatus, setStreamStatus] = useState({
|
||||||
|
isLoading: true,
|
||||||
|
isError: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchStreamData = async () => {
|
||||||
|
try {
|
||||||
|
let result: Stream | null = null;
|
||||||
|
if (offline && !Platform.isTV) {
|
||||||
|
const data = await getDownloadedItem.getDownloadedItem(itemId);
|
||||||
|
if (!data?.mediaSource) return;
|
||||||
|
const url = await getDownloadedFileUrl(data.item.Id!);
|
||||||
|
if (item) {
|
||||||
|
result = { mediaSource: data.mediaSource, sessionId: "", url };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await getStreamUrl({
|
||||||
|
api,
|
||||||
|
item,
|
||||||
|
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
||||||
|
userId: user?.Id,
|
||||||
|
audioStreamIndex: audioIndex,
|
||||||
|
maxStreamingBitrate: bitrateValue,
|
||||||
|
mediaSourceId: mediaSourceId,
|
||||||
|
subtitleStreamIndex: subtitleIndex,
|
||||||
|
deviceProfile: native,
|
||||||
|
});
|
||||||
|
if (!res) return;
|
||||||
|
const { mediaSource, sessionId, url } = res;
|
||||||
|
if (!sessionId || !mediaSource || !url) {
|
||||||
|
Alert.alert(t("player.error"), t("player.failed_to_get_stream_url"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result = { mediaSource, sessionId, url };
|
||||||
|
}
|
||||||
|
setStream(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch stream:", error);
|
||||||
|
setStreamStatus({ isLoading: false, isError: true });
|
||||||
|
} finally {
|
||||||
|
setStreamStatus({ isLoading: false, isError: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchStreamData();
|
||||||
|
}, [itemId, mediaSourceId, bitrateValue, api, item, user?.Id]);
|
||||||
|
|
||||||
|
const togglePlay = async () => {
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
|
setIsPlaying(!isPlaying);
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
await videoRef.current?.pause();
|
await videoRef.current?.pause();
|
||||||
} else {
|
} else {
|
||||||
videoRef.current?.play();
|
videoRef.current?.play();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
if (!offline && stream) {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
isPlaying,
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
stream,
|
|
||||||
videoRef,
|
|
||||||
audioIndex,
|
|
||||||
subtitleIndex,
|
|
||||||
mediaSourceId,
|
|
||||||
offline,
|
|
||||||
progress,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const reportPlaybackStopped = useCallback(async () => {
|
const reportPlaybackStopped = useCallback(async () => {
|
||||||
if (offline) return;
|
if (offline) return;
|
||||||
|
|
||||||
const currentTimeInTicks = msToTicks(progress.get());
|
const currentTimeInTicks = msToTicks(progress.get());
|
||||||
|
|
||||||
await getPlaystateApi(api!).onPlaybackStopped({
|
await getPlaystateApi(api!).onPlaybackStopped({
|
||||||
itemId: item?.Id!,
|
itemId: item?.Id!,
|
||||||
mediaSourceId: mediaSourceId,
|
mediaSourceId: mediaSourceId,
|
||||||
@@ -255,12 +211,35 @@ export default function page() {
|
|||||||
videoRef.current?.stop();
|
videoRef.current?.stop();
|
||||||
}, [videoRef, reportPlaybackStopped]);
|
}, [videoRef, reportPlaybackStopped]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
||||||
|
return () => {
|
||||||
|
beforeRemoveListener();
|
||||||
|
};
|
||||||
|
}, [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(
|
const onProgress = useCallback(
|
||||||
async (data: ProgressUpdatePayload) => {
|
async (data: ProgressUpdatePayload) => {
|
||||||
if (isSeeking.get() || isPlaybackStopped) return;
|
if (isSeeking.get() || isPlaybackStopped) return;
|
||||||
|
|
||||||
const { currentTime } = data.nativeEvent;
|
const { currentTime } = data.nativeEvent;
|
||||||
|
|
||||||
if (isBuffering) {
|
if (isBuffering) {
|
||||||
setIsBuffering(false);
|
setIsBuffering(false);
|
||||||
}
|
}
|
||||||
@@ -269,24 +248,30 @@ export default function page() {
|
|||||||
|
|
||||||
if (offline) return;
|
if (offline) return;
|
||||||
|
|
||||||
const currentTimeInTicks = msToTicks(currentTime);
|
|
||||||
|
|
||||||
if (!item?.Id || !stream) return;
|
if (!item?.Id || !stream) return;
|
||||||
|
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
reportPlaybackProgress();
|
||||||
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, isSeeking, api, isPlaybackStopped, audioIndex, subtitleIndex]
|
[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 startPosition = useMemo(() => {
|
||||||
|
if (offline) return 0;
|
||||||
|
return item?.UserData?.PlaybackPositionTicks ? ticksToSeconds(item.UserData.PlaybackPositionTicks) : 0;
|
||||||
|
}, [item]);
|
||||||
|
|
||||||
useWebSocket({
|
useWebSocket({
|
||||||
isPlaying: isPlaying,
|
isPlaying: isPlaying,
|
||||||
togglePlay: togglePlay,
|
togglePlay: togglePlay,
|
||||||
@@ -294,75 +279,40 @@ export default function page() {
|
|||||||
offline,
|
offline,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
const onPlaybackStateChanged = useCallback(
|
||||||
const { pipStarted } = e.nativeEvent;
|
async (e: PlaybackStatePayload) => {
|
||||||
setIsPipStarted(pipStarted);
|
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||||
}, []);
|
if (state === "Playing") {
|
||||||
|
setIsPlaying(true);
|
||||||
|
reportPlaybackProgress();
|
||||||
|
if (!Platform.isTV) await activateKeepAwakeAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const onPlaybackStateChanged = useCallback(async (e: PlaybackStatePayload) => {
|
if (state === "Paused") {
|
||||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
setIsPlaying(false);
|
||||||
|
reportPlaybackProgress();
|
||||||
|
if (!Platform.isTV) await deactivateKeepAwake();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state === "Playing") {
|
if (isPlaying) {
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
if (!Platform.isTV) await activateKeepAwakeAsync()
|
setIsBuffering(false);
|
||||||
return;
|
} else if (isBuffering) {
|
||||||
}
|
setIsBuffering(true);
|
||||||
|
}
|
||||||
if (state === "Paused") {
|
},
|
||||||
setIsPlaying(false);
|
[reportPlaybackProgress]
|
||||||
if (!Platform.isTV) await deactivateKeepAwake();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaying) {
|
|
||||||
setIsPlaying(true);
|
|
||||||
setIsBuffering(false);
|
|
||||||
} else if (isBuffering) {
|
|
||||||
setIsBuffering(true);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const startPosition = useMemo(() => {
|
|
||||||
if (offline) return 0;
|
|
||||||
|
|
||||||
return item?.UserData?.PlaybackPositionTicks
|
|
||||||
? ticksToSeconds(item.UserData.PlaybackPositionTicks)
|
|
||||||
: 0;
|
|
||||||
}, [item]);
|
|
||||||
|
|
||||||
// Preselection of audio and subtitle tracks.
|
|
||||||
if (!settings) return null;
|
|
||||||
let initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
|
|
||||||
|
|
||||||
const allAudio =
|
|
||||||
stream?.mediaSource.MediaStreams?.filter(
|
|
||||||
(audio) => audio.Type === "Audio"
|
|
||||||
) || [];
|
|
||||||
const allSubs =
|
|
||||||
stream?.mediaSource.MediaStreams?.filter(
|
|
||||||
(sub) => sub.Type === "Subtitle"
|
|
||||||
) || [];
|
|
||||||
const textSubs = allSubs.filter((sub) => sub.IsTextSubtitleStream);
|
|
||||||
|
|
||||||
const chosenSubtitleTrack = allSubs.find(
|
|
||||||
(sub) => sub.Index === subtitleIndex
|
|
||||||
);
|
);
|
||||||
const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex);
|
|
||||||
|
|
||||||
const notTranscoding = !stream?.mediaSource.TranscodingUrl;
|
const allAudio = stream?.mediaSource.MediaStreams?.filter((audio) => audio.Type === "Audio") || [];
|
||||||
if (
|
|
||||||
chosenSubtitleTrack &&
|
|
||||||
(notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
|
|
||||||
) {
|
|
||||||
const finalIndex = notTranscoding
|
|
||||||
? allSubs.indexOf(chosenSubtitleTrack)
|
|
||||||
: textSubs.indexOf(chosenSubtitleTrack);
|
|
||||||
initOptions.push(`--sub-track=${finalIndex}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notTranscoding && chosenAudioTrack) {
|
// Move all the external subtitles last, because vlc places them last.
|
||||||
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
const allSubs =
|
||||||
}
|
stream?.mediaSource.MediaStreams?.filter((sub) => sub.Type === "Subtitle").sort(
|
||||||
|
(a, b) => Number(a.IsExternal) - Number(b.IsExternal)
|
||||||
|
) || [];
|
||||||
|
|
||||||
const externalSubtitles = allSubs
|
const externalSubtitles = allSubs
|
||||||
.filter((sub: any) => sub.DeliveryMethod === "External")
|
.filter((sub: any) => sub.DeliveryMethod === "External")
|
||||||
@@ -371,6 +321,22 @@ export default function page() {
|
|||||||
DeliveryUrl: api?.basePath + sub.DeliveryUrl,
|
DeliveryUrl: api?.basePath + sub.DeliveryUrl,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const textSubs = allSubs.filter((sub) => sub.IsTextSubtitleStream);
|
||||||
|
|
||||||
|
const chosenSubtitleTrack = allSubs.find((sub) => sub.Index === subtitleIndex);
|
||||||
|
const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex);
|
||||||
|
|
||||||
|
const notTranscoding = !stream?.mediaSource.TranscodingUrl;
|
||||||
|
let initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
|
||||||
|
if (chosenSubtitleTrack && (notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)) {
|
||||||
|
const finalIndex = notTranscoding ? allSubs.indexOf(chosenSubtitleTrack) : textSubs.indexOf(chosenSubtitleTrack);
|
||||||
|
initOptions.push(`--sub-track=${finalIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notTranscoding && chosenAudioTrack) {
|
||||||
|
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
||||||
|
}
|
||||||
|
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
|
|
||||||
// Add useEffect to handle mounting
|
// Add useEffect to handle mounting
|
||||||
@@ -379,22 +345,15 @@ export default function page() {
|
|||||||
return () => setIsMounted(false);
|
return () => setIsMounted(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
if (itemStatus.isLoading || streamStatus.isLoading) {
|
||||||
useEffect(() => {
|
|
||||||
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
|
||||||
return () => {
|
|
||||||
beforeRemoveListener();
|
|
||||||
};
|
|
||||||
}, [navigation]);
|
|
||||||
|
|
||||||
if (!item || isLoadingItem || !stream)
|
|
||||||
return (
|
return (
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
||||||
<Loader />
|
<Loader />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isErrorItem || isErrorStream)
|
if (!item || !stream || itemStatus.isError || streamStatus.isError)
|
||||||
return (
|
return (
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
||||||
<Text className="text-white">{t("player.error")}</Text>
|
<Text className="text-white">{t("player.error")}</Text>
|
||||||
@@ -435,10 +394,7 @@ export default function page() {
|
|||||||
}}
|
}}
|
||||||
onVideoError={(e) => {
|
onVideoError={(e) => {
|
||||||
console.error("Video Error:", e.nativeEvent);
|
console.error("Video Error:", e.nativeEvent);
|
||||||
Alert.alert(
|
Alert.alert(t("player.error"), t("player.an_error_occured_while_playing_the_video"));
|
||||||
t("player.error"),
|
|
||||||
t("player.an_error_occured_while_playing_the_video")
|
|
||||||
);
|
|
||||||
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -470,7 +426,6 @@ export default function page() {
|
|||||||
setSubtitleTrack={videoRef.current.setSubtitleTrack}
|
setSubtitleTrack={videoRef.current.setSubtitleTrack}
|
||||||
setSubtitleURL={videoRef.current.setSubtitleURL}
|
setSubtitleURL={videoRef.current.setSubtitleURL}
|
||||||
setAudioTrack={videoRef.current.setAudioTrack}
|
setAudioTrack={videoRef.current.setAudioTrack}
|
||||||
stop={stop}
|
|
||||||
isVlc
|
isVlc
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ function Layout() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If the user has auto rotate enabled, unlock the orientation
|
// If the user has auto rotate enabled, unlock the orientation
|
||||||
|
if (Platform.isTV) return;
|
||||||
if (settings.autoRotate === true) {
|
if (settings.autoRotate === true) {
|
||||||
ScreenOrientation.unlockAsync();
|
ScreenOrientation.unlockAsync();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 79 KiB |
70
bun.lock
70
bun.lock
@@ -60,7 +60,7 @@
|
|||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
"react-native-bottom-tabs": "0.8.7",
|
"react-native-bottom-tabs": "0.8.6",
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
"react-native-compressor": "^1.10.3",
|
"react-native-compressor": "^1.10.3",
|
||||||
"react-native-country-flag": "^2.0.2",
|
"react-native-country-flag": "^2.0.2",
|
||||||
@@ -386,7 +386,7 @@
|
|||||||
|
|
||||||
"@expo/bunyan": ["@expo/bunyan@4.0.1", "", { "dependencies": { "uuid": "^8.0.0" } }, "sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg=="],
|
"@expo/bunyan": ["@expo/bunyan@4.0.1", "", { "dependencies": { "uuid": "^8.0.0" } }, "sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg=="],
|
||||||
|
|
||||||
"@expo/cli": ["@expo/cli@0.22.16", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/devcert": "^1.1.2", "@expo/env": "~0.4.2", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@expo/metro-config": "~0.19.10", "@expo/osascript": "^2.1.6", "@expo/package-manager": "^1.7.2", "@expo/plist": "^0.2.2", "@expo/prebuild-config": "^8.0.27", "@expo/rudder-sdk-node": "^1.1.1", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.76.7", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.0.7", "bplist-parser": "^0.3.1", "cacache": "^18.0.2", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "fast-glob": "^3.3.2", "form-data": "^3.0.1", "freeport-async": "^2.0.0", "fs-extra": "~8.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "internal-ip": "^4.3.0", "is-docker": "^2.0.0", "is-wsl": "^2.1.1", "lodash.debounce": "^4.0.8", "minimatch": "^3.0.4", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^6.2.1", "temp-dir": "^2.0.0", "tempy": "^0.7.1", "terminal-link": "^2.1.1", "undici": "^6.18.2", "unique-string": "~2.0.0", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "bin": { "expo-internal": "build/bin/cli" } }, "sha512-a8Ulbnji9kFatnOtsWGCRs6nMUj9UNC0/WhE74HQdXGDGMn5Pl8eNe3cLMy9G54DdqAmEZmRZpgXmcudT78fEQ=="],
|
"@expo/cli": ["@expo/cli@0.22.18", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/devcert": "^1.1.2", "@expo/env": "~0.4.2", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@expo/metro-config": "~0.19.11", "@expo/osascript": "^2.1.6", "@expo/package-manager": "^1.7.2", "@expo/plist": "^0.2.2", "@expo/prebuild-config": "^8.0.28", "@expo/rudder-sdk-node": "^1.1.1", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.76.7", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.0.7", "bplist-parser": "^0.3.1", "cacache": "^18.0.2", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "fast-glob": "^3.3.2", "form-data": "^3.0.1", "freeport-async": "^2.0.0", "fs-extra": "~8.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "internal-ip": "^4.3.0", "is-docker": "^2.0.0", "is-wsl": "^2.1.1", "lodash.debounce": "^4.0.8", "minimatch": "^3.0.4", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^6.2.1", "temp-dir": "^2.0.0", "tempy": "^0.7.1", "terminal-link": "^2.1.1", "undici": "^6.18.2", "unique-string": "~2.0.0", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "bin": { "expo-internal": "build/bin/cli" } }, "sha512-TWGKHWTYU9xE7YETPk2zQzLPl+bldpzZCa0Cqg0QeENpu03ZEnMxUqrgHwrbWGTf7ONTYC1tODBkFCFw/qgPGA=="],
|
||||||
|
|
||||||
"@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.5", "", { "dependencies": { "node-forge": "^1.2.1", "nullthrows": "^1.1.1" } }, "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw=="],
|
"@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.5", "", { "dependencies": { "node-forge": "^1.2.1", "nullthrows": "^1.1.1" } }, "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw=="],
|
||||||
|
|
||||||
@@ -400,13 +400,13 @@
|
|||||||
|
|
||||||
"@expo/env": ["@expo/env@0.4.2", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^1.0.0" } }, "sha512-TgbCgvSk0Kq0e2fLoqHwEBL4M0ztFjnBEz0YCDm5boc1nvkV1VMuIMteVdeBwnTh8Z0oPJTwHCD49vhMEt1I6A=="],
|
"@expo/env": ["@expo/env@0.4.2", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^1.0.0" } }, "sha512-TgbCgvSk0Kq0e2fLoqHwEBL4M0ztFjnBEz0YCDm5boc1nvkV1VMuIMteVdeBwnTh8Z0oPJTwHCD49vhMEt1I6A=="],
|
||||||
|
|
||||||
"@expo/fingerprint": ["@expo/fingerprint@0.11.10", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "find-up": "^5.0.0", "getenv": "^1.0.0", "minimatch": "^3.0.4", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-34ZwPjbnnD7KHSyceaxcLQbClCkYHbEp6wBDe+aqimvQw25m2LnliN1cMCVQnpOHkBFRTcbKlowby0fIxAm2bQ=="],
|
"@expo/fingerprint": ["@expo/fingerprint@0.11.11", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "find-up": "^5.0.0", "getenv": "^1.0.0", "minimatch": "^3.0.4", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-gNyn1KnAOpEa8gSNsYqXMTcq0fSwqU/vit6fP5863vLSKxHm/dNt/gm/uZJxrRZxKq71KUJWF6I7d3z8qIfq5g=="],
|
||||||
|
|
||||||
"@expo/image-utils": ["@expo/image-utils@0.6.5", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "fs-extra": "9.0.0", "getenv": "^1.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "temp-dir": "~2.0.0", "unique-string": "~2.0.0" } }, "sha512-RsS/1CwJYzccvlprYktD42KjyfWZECH6PPIEowvoSmXfGLfdViwcUEI4RvBfKX5Jli6P67H+6YmHvPTbGOboew=="],
|
"@expo/image-utils": ["@expo/image-utils@0.6.5", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "fs-extra": "9.0.0", "getenv": "^1.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "temp-dir": "~2.0.0", "unique-string": "~2.0.0" } }, "sha512-RsS/1CwJYzccvlprYktD42KjyfWZECH6PPIEowvoSmXfGLfdViwcUEI4RvBfKX5Jli6P67H+6YmHvPTbGOboew=="],
|
||||||
|
|
||||||
"@expo/json-file": ["@expo/json-file@9.0.2", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3", "write-file-atomic": "^2.3.0" } }, "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw=="],
|
"@expo/json-file": ["@expo/json-file@9.0.2", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3", "write-file-atomic": "^2.3.0" } }, "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw=="],
|
||||||
|
|
||||||
"@expo/metro-config": ["@expo/metro-config@0.19.10", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "@expo/config": "~10.0.9", "@expo/env": "~0.4.1", "@expo/json-file": "~9.0.1", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "debug": "^4.3.2", "fs-extra": "^9.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "jsc-safe-url": "^0.2.4", "lightningcss": "~1.27.0", "minimatch": "^3.0.4", "postcss": "~8.4.32", "resolve-from": "^5.0.0" } }, "sha512-/CtsMLhELJRJjAllM4EUnlPUAixn8Q2YhorKBa4uXZ6FvTEZWHJjqsXnQD39gWSEuAIVwLfJ1qgJi8666+dW2w=="],
|
"@expo/metro-config": ["@expo/metro-config@0.19.11", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "@expo/config": "~10.0.10", "@expo/env": "~0.4.2", "@expo/json-file": "~9.0.2", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "debug": "^4.3.2", "fs-extra": "^9.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "jsc-safe-url": "^0.2.4", "lightningcss": "~1.27.0", "minimatch": "^3.0.4", "postcss": "~8.4.32", "resolve-from": "^5.0.0" } }, "sha512-XaobHTcsoHQdKEH7PI/DIpr2QiugkQmPYolbfzkpSJMplNWfSh+cTRjrm4//mS2Sb78qohtu0u2CGJnFqFUGag=="],
|
||||||
|
|
||||||
"@expo/metro-runtime": ["@expo/metro-runtime@4.0.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw=="],
|
"@expo/metro-runtime": ["@expo/metro-runtime@4.0.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw=="],
|
||||||
|
|
||||||
@@ -416,7 +416,7 @@
|
|||||||
|
|
||||||
"@expo/plist": ["@expo/plist@0.2.2", "", { "dependencies": { "@xmldom/xmldom": "~0.7.7", "base64-js": "^1.2.3", "xmlbuilder": "^14.0.0" } }, "sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g=="],
|
"@expo/plist": ["@expo/plist@0.2.2", "", { "dependencies": { "@xmldom/xmldom": "~0.7.7", "base64-js": "^1.2.3", "xmlbuilder": "^14.0.0" } }, "sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g=="],
|
||||||
|
|
||||||
"@expo/prebuild-config": ["@expo/prebuild-config@8.0.27", "", { "dependencies": { "@expo/config": "~10.0.9", "@expo/config-plugins": "~9.0.15", "@expo/config-types": "^52.0.4", "@expo/image-utils": "^0.6.4", "@expo/json-file": "^9.0.1", "@react-native/normalize-colors": "0.76.7", "debug": "^4.3.1", "fs-extra": "^9.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-UFGOx4TfiT2gOde8RylwmXctp/WvqBQ4TN7z1YL0WWXfG9TWfO7HdsUnqQhGMW+CDDc7FOJMEo8q1a6xiikfYA=="],
|
"@expo/prebuild-config": ["@expo/prebuild-config@8.0.28", "", { "dependencies": { "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/config-types": "^52.0.4", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@react-native/normalize-colors": "0.76.7", "debug": "^4.3.1", "fs-extra": "^9.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-SDDgCKKS1wFNNm3de2vBP8Q5bnxcabuPDE9Mnk9p7Gb4qBavhwMbAtrLcAyZB+WRb4QM+yan3z3K95vvCfI/+A=="],
|
||||||
|
|
||||||
"@expo/react-native-action-sheet": ["@expo/react-native-action-sheet@4.1.0", "", { "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "react": ">=18.0.0" } }, "sha512-RILoWhREgjMdr1NUSmZa/cHg8onV2YPDAMOy0iIP1c3H7nT9QQZf5dQNHK8ehcLM82sarVxriBJyYSSHAx7j6w=="],
|
"@expo/react-native-action-sheet": ["@expo/react-native-action-sheet@4.1.0", "", { "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "react": ">=18.0.0" } }, "sha512-RILoWhREgjMdr1NUSmZa/cHg8onV2YPDAMOy0iIP1c3H7nT9QQZf5dQNHK8ehcLM82sarVxriBJyYSSHAx7j6w=="],
|
||||||
|
|
||||||
@@ -430,7 +430,7 @@
|
|||||||
|
|
||||||
"@expo/vector-icons": ["@expo/vector-icons@14.0.4", "", { "dependencies": { "prop-types": "^15.8.1" } }, "sha512-+yKshcbpDfbV4zoXOgHxCwh7lkE9VVTT5T03OUlBsqfze1PLy6Hi4jp1vSb1GVbY6eskvMIivGVc9SKzIv0oEQ=="],
|
"@expo/vector-icons": ["@expo/vector-icons@14.0.4", "", { "dependencies": { "prop-types": "^15.8.1" } }, "sha512-+yKshcbpDfbV4zoXOgHxCwh7lkE9VVTT5T03OUlBsqfze1PLy6Hi4jp1vSb1GVbY6eskvMIivGVc9SKzIv0oEQ=="],
|
||||||
|
|
||||||
"@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.4", "", {}, "sha512-spXCVXxbeKOe8YZ9igd+MDfXZe6LeDvFAdILijeTSG+XcxGrZLmqMWWkFKR0nV8lTWZ+NugUT3CoiXmEuKKQ7w=="],
|
"@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.5", "", {}, "sha512-Ta9KzslHAIbw2ZoyZ7Ud7/QImucy+K4YvOqo9AhGfUfH76hQzaffQreOySzYusDfW8Y+EXh0ZNWE68dfCumFFw=="],
|
||||||
|
|
||||||
"@expo/xcpretty": ["@expo/xcpretty@4.3.2", "", { "dependencies": { "@babel/code-frame": "7.10.4", "chalk": "^4.1.0", "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw=="],
|
"@expo/xcpretty": ["@expo/xcpretty@4.3.2", "", { "dependencies": { "@babel/code-frame": "7.10.4", "chalk": "^4.1.0", "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw=="],
|
||||||
|
|
||||||
@@ -676,9 +676,9 @@
|
|||||||
|
|
||||||
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
|
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
|
||||||
|
|
||||||
"@tanstack/query-core": ["@tanstack/query-core@5.66.0", "", {}, "sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw=="],
|
"@tanstack/query-core": ["@tanstack/query-core@5.66.4", "", {}, "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA=="],
|
||||||
|
|
||||||
"@tanstack/react-query": ["@tanstack/react-query@5.66.0", "", { "dependencies": { "@tanstack/query-core": "5.66.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-z3sYixFQJe8hndFnXgWu7C79ctL+pI0KAelYyW+khaNJ1m22lWrhJU2QrsTcRKMuVPtoZvfBYrTStIdKo+x0Xw=="],
|
"@tanstack/react-query": ["@tanstack/react-query@5.66.9", "", { "dependencies": { "@tanstack/query-core": "5.66.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-NRI02PHJsP5y2gAuWKP+awamTIBFBSKMnO6UVzi03GTclmHHHInH5UzVgzi5tpu4+FmGfsdT7Umqegobtsp23A=="],
|
||||||
|
|
||||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||||
|
|
||||||
@@ -828,7 +828,7 @@
|
|||||||
|
|
||||||
"babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.1.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw=="],
|
"babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.1.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw=="],
|
||||||
|
|
||||||
"babel-preset-expo": ["babel-preset-expo@12.0.8", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.12.13", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.76.7", "babel-plugin-react-native-web": "~0.19.13", "react-refresh": "^0.14.2" }, "peerDependencies": { "babel-plugin-react-compiler": "^19.0.0-beta-9ee70a1-20241017", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020" }, "optionalPeers": ["babel-plugin-react-compiler", "react-compiler-runtime"] }, "sha512-bojAddWZJusLs3NVdF+jN3WweTYVEZXBKIeO0sOhqOg7UPh5w1bnMkx7SDua0FgQMGBxb13qM31Y46yeZnmXjw=="],
|
"babel-preset-expo": ["babel-preset-expo@12.0.9", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.12.13", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.76.7", "babel-plugin-react-native-web": "~0.19.13", "react-refresh": "^0.14.2" }, "peerDependencies": { "babel-plugin-react-compiler": "^19.0.0-beta-9ee70a1-20241017", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020" }, "optionalPeers": ["babel-plugin-react-compiler", "react-compiler-runtime"] }, "sha512-1c+ysrTavT49WgVAj0OX/TEzt1kU2mfPhDaDajstshNHXFKPenMPWSViA/DHrJKVIMwaqr+z3GbUOD9GtKgpdg=="],
|
||||||
|
|
||||||
"babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="],
|
"babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="],
|
||||||
|
|
||||||
@@ -896,7 +896,7 @@
|
|||||||
|
|
||||||
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
|
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001699", "", {}, "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001700", "", {}, "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ=="],
|
||||||
|
|
||||||
"centra": ["centra@2.7.0", "", { "dependencies": { "follow-redirects": "^1.15.6" } }, "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg=="],
|
"centra": ["centra@2.7.0", "", { "dependencies": { "follow-redirects": "^1.15.6" } }, "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg=="],
|
||||||
|
|
||||||
@@ -1056,7 +1056,7 @@
|
|||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.100", "", {}, "sha512-u1z9VuzDXV86X2r3vAns0/5ojfXBue9o0+JDUDBKYqGLjxLkSqsSUoPU/6kW0gx76V44frHaf6Zo+QF74TQCMg=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.103", "", {}, "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
@@ -1086,6 +1086,8 @@
|
|||||||
|
|
||||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
@@ -1110,11 +1112,11 @@
|
|||||||
|
|
||||||
"expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="],
|
"expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="],
|
||||||
|
|
||||||
"expo": ["expo@52.0.35", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.22.16", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/fingerprint": "0.11.10", "@expo/metro-config": "0.19.10", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~12.0.8", "expo-asset": "~11.0.3", "expo-constants": "~17.0.6", "expo-file-system": "~18.0.10", "expo-font": "~13.0.3", "expo-keep-awake": "~14.0.2", "expo-modules-autolinking": "2.0.7", "expo-modules-core": "2.2.2", "fbemitter": "^3.0.0", "web-streams-polyfill": "^3.3.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli" } }, "sha512-VagwS6MJbU0Eky18i4amkkSy7FTi0v31B0W+qoEcsU4x5OurA381rxw4qGsQE+8pmSD/Gf3DGb8ygJw+HoAsXw=="],
|
"expo": ["expo@52.0.37", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.22.18", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/fingerprint": "0.11.11", "@expo/metro-config": "0.19.11", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~12.0.9", "expo-asset": "~11.0.4", "expo-constants": "~17.0.7", "expo-file-system": "~18.0.11", "expo-font": "~13.0.4", "expo-keep-awake": "~14.0.3", "expo-modules-autolinking": "2.0.8", "expo-modules-core": "2.2.2", "fbemitter": "^3.0.0", "web-streams-polyfill": "^3.3.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli" } }, "sha512-fo37ClqjNLOVInerm7BU27H8lfPfeTC7Pmu72roPzq46DnJfs+KzTxTzE34GcJ0b6hMUx9FRSSGyTQqxzo2TVQ=="],
|
||||||
|
|
||||||
"expo-application": ["expo-application@6.0.2", "", { "peerDependencies": { "expo": "*" } }, "sha512-qcj6kGq3mc7x5yIb5KxESurFTJCoEKwNEL34RdPEvTB/xhl7SeVZlu05sZBqxB1V4Ryzq/LsCb7NHNfBbb3L7A=="],
|
"expo-application": ["expo-application@6.0.2", "", { "peerDependencies": { "expo": "*" } }, "sha512-qcj6kGq3mc7x5yIb5KxESurFTJCoEKwNEL34RdPEvTB/xhl7SeVZlu05sZBqxB1V4Ryzq/LsCb7NHNfBbb3L7A=="],
|
||||||
|
|
||||||
"expo-asset": ["expo-asset@11.0.3", "", { "dependencies": { "@expo/image-utils": "^0.6.4", "expo-constants": "~17.0.5", "invariant": "^2.2.4", "md5-file": "^3.2.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-vgJnC82IooAVMy5PxbdFIMNJhW4hKAUyxc5VIiAPPf10vFYw6CqHm+hrehu4ST1I4bvg5PV4uKdPxliebcbgLg=="],
|
"expo-asset": ["expo-asset@11.0.4", "", { "dependencies": { "@expo/image-utils": "^0.6.5", "expo-constants": "~17.0.7", "invariant": "^2.2.4", "md5-file": "^3.2.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-CdIywU0HrR3wsW5c3n0cT3jW9hccZdnqGsRqY+EY/RWzJbDXtDfAQVEiFHO3mDK7oveUwrP2jK/6ZRNek41/sg=="],
|
||||||
|
|
||||||
"expo-background-fetch": ["expo-background-fetch@13.0.5", "", { "dependencies": { "expo-task-manager": "~12.0.5" }, "peerDependencies": { "expo": "*" } }, "sha512-rLRM+rYDRT0fA0Oaet5ibJK3nKVRkfdjXjISHxjUvIE4ktD9pE+UjAPPdjTXZ5CkNb3JyNNhQGJEGpdJC2HLKw=="],
|
"expo-background-fetch": ["expo-background-fetch@13.0.5", "", { "dependencies": { "expo-task-manager": "~12.0.5" }, "peerDependencies": { "expo": "*" } }, "sha512-rLRM+rYDRT0fA0Oaet5ibJK3nKVRkfdjXjISHxjUvIE4ktD9pE+UjAPPdjTXZ5CkNb3JyNNhQGJEGpdJC2HLKw=="],
|
||||||
|
|
||||||
@@ -1124,7 +1126,7 @@
|
|||||||
|
|
||||||
"expo-build-properties": ["expo-build-properties@0.13.2", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-ML2GwBgn0Bo4yPgnSGb7h3XVxCigS/KFdid3xPC2HldEioTP3UewB/2Qa4WBsam9Fb7lAuRyVHAfRoA3swpDzg=="],
|
"expo-build-properties": ["expo-build-properties@0.13.2", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-ML2GwBgn0Bo4yPgnSGb7h3XVxCigS/KFdid3xPC2HldEioTP3UewB/2Qa4WBsam9Fb7lAuRyVHAfRoA3swpDzg=="],
|
||||||
|
|
||||||
"expo-constants": ["expo-constants@17.0.6", "", { "dependencies": { "@expo/config": "~10.0.9", "@expo/env": "~0.4.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-rl3/hBIIkh4XDkCEMzGpmY6kWj2G1TA4Mq2joeyzoFBepJuGjqnGl7phf/71sTTgamQ1hmhKCLRNXMpRqzzqxw=="],
|
"expo-constants": ["expo-constants@17.0.7", "", { "dependencies": { "@expo/config": "~10.0.10", "@expo/env": "~0.4.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-sp5NUiV17I3JblVPIBDgoxgt7JIZS30vcyydCYHxsEoo+aKaeRYXxGYilCvb9lgI6BBwSL24sQ6ZjWsCWoF1VA=="],
|
||||||
|
|
||||||
"expo-crypto": ["expo-crypto@14.0.2", "", { "dependencies": { "base64-js": "^1.3.0" }, "peerDependencies": { "expo": "*" } }, "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ=="],
|
"expo-crypto": ["expo-crypto@14.0.2", "", { "dependencies": { "base64-js": "^1.3.0" }, "peerDependencies": { "expo": "*" } }, "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ=="],
|
||||||
|
|
||||||
@@ -1140,17 +1142,17 @@
|
|||||||
|
|
||||||
"expo-eas-client": ["expo-eas-client@0.13.2", "", {}, "sha512-2RAAGtkO9vseoJZuW4mhJkiNQ6+FfLrX66OTMq4Qj9mRKZV2Uq/ZquxUGIeJyYqBy4vNYeKbuPd2oJtsV9LBGQ=="],
|
"expo-eas-client": ["expo-eas-client@0.13.2", "", {}, "sha512-2RAAGtkO9vseoJZuW4mhJkiNQ6+FfLrX66OTMq4Qj9mRKZV2Uq/ZquxUGIeJyYqBy4vNYeKbuPd2oJtsV9LBGQ=="],
|
||||||
|
|
||||||
"expo-file-system": ["expo-file-system@18.0.10", "", { "dependencies": { "web-streams-polyfill": "^3.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-+GnxkI+J9tOzUQMx+uIOLBEBsO2meyoYHxd87m9oT9M//BpepYqI1AvYBH8YM4dgr9HaeaeLr7z5XFVqfL8tWg=="],
|
"expo-file-system": ["expo-file-system@18.0.11", "", { "dependencies": { "web-streams-polyfill": "^3.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-yDwYfEzWgPXsBZHJW2RJ8Q66ceiFN9Wa5D20pp3fjXVkzPBDwxnYwiPWk4pVmCa5g4X5KYMoMne1pUrsL4OEpg=="],
|
||||||
|
|
||||||
"expo-font": ["expo-font@13.0.3", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-9IdYz+A+b3KvuCYP7DUUXF4VMZjPU+IsvAnLSVJ2TfP6zUD2JjZFx3jeo/cxWRkYk/aLj5+53Te7elTAScNl4Q=="],
|
"expo-font": ["expo-font@13.0.4", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw=="],
|
||||||
|
|
||||||
"expo-haptics": ["expo-haptics@14.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-V81FZ7xRUfqM6uSI6FA1KnZ+QpEKnISqafob/xEfcx1ymwhm4V3snuLWWFjmAz+XaZQTqlYa8z3QbqEXz7G63w=="],
|
"expo-haptics": ["expo-haptics@14.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-V81FZ7xRUfqM6uSI6FA1KnZ+QpEKnISqafob/xEfcx1ymwhm4V3snuLWWFjmAz+XaZQTqlYa8z3QbqEXz7G63w=="],
|
||||||
|
|
||||||
"expo-image": ["expo-image@2.0.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-FAq7uyaTAfLWER3lN+KVAtep7IfGPZN9ygnVKW4GvgnvR4hKhTtZ5WNxiJ18KKLVb4nUKuHOpQeJNnljy3dtmA=="],
|
"expo-image": ["expo-image@2.0.6", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-NHpIZmGnrPbyDadil6eK+sUgyFMQfapEVb7YaGgxSFWBUQ1rSpjqdIQrCD24IZTO9uSH8V+hMh2ROxrAjAixzQ=="],
|
||||||
|
|
||||||
"expo-json-utils": ["expo-json-utils@0.14.0", "", {}, "sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw=="],
|
"expo-json-utils": ["expo-json-utils@0.14.0", "", {}, "sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw=="],
|
||||||
|
|
||||||
"expo-keep-awake": ["expo-keep-awake@14.0.2", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-71XAMnoWjKZrN8J7Q3+u0l9Ytp4OfhNAYz8BCWF1/9aFUw09J3I7Z5DuI3MUsVMa/KWi+XhG+eDUFP8cVA19Uw=="],
|
"expo-keep-awake": ["expo-keep-awake@14.0.3", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-6Jh94G6NvTZfuLnm2vwIpKe3GdOiVBuISl7FI8GqN0/9UOg9E0WXXp5cDcfAG8bn80RfgLJS8P7EPUGTZyOvhg=="],
|
||||||
|
|
||||||
"expo-linear-gradient": ["expo-linear-gradient@14.0.2", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-nvac1sPUfFFJ4mY25UkvubpUV/olrBH+uQw5k+beqSvQaVQiUfFtYzfRr+6HhYBNb4AEsOtpsCRkpDww3M2iGQ=="],
|
"expo-linear-gradient": ["expo-linear-gradient@14.0.2", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-nvac1sPUfFFJ4mY25UkvubpUV/olrBH+uQw5k+beqSvQaVQiUfFtYzfRr+6HhYBNb4AEsOtpsCRkpDww3M2iGQ=="],
|
||||||
|
|
||||||
@@ -1160,7 +1162,7 @@
|
|||||||
|
|
||||||
"expo-manifests": ["expo-manifests@0.15.6", "", { "dependencies": { "@expo/config": "~10.0.9", "expo-json-utils": "~0.14.0" }, "peerDependencies": { "expo": "*" } }, "sha512-z+TFICrijMaqBvcJkVx8WzgmOsV6ZJGvaPNQKZr4DA6uqugFMtvAQVikDjIq7SEc3n7IgPk0GR4ZN3/KnnkeVA=="],
|
"expo-manifests": ["expo-manifests@0.15.6", "", { "dependencies": { "@expo/config": "~10.0.9", "expo-json-utils": "~0.14.0" }, "peerDependencies": { "expo": "*" } }, "sha512-z+TFICrijMaqBvcJkVx8WzgmOsV6ZJGvaPNQKZr4DA6uqugFMtvAQVikDjIq7SEc3n7IgPk0GR4ZN3/KnnkeVA=="],
|
||||||
|
|
||||||
"expo-modules-autolinking": ["expo-modules-autolinking@2.0.7", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "fast-glob": "^3.2.5", "find-up": "^5.0.0", "fs-extra": "^9.1.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-rkGc6a/90AC3q8wSy4V+iIpq6Fd0KXmQICKrvfmSWwrMgJmLfwP4QTrvLYPYOOMjFwNJcTaohcH8vzW/wYKrMg=="],
|
"expo-modules-autolinking": ["expo-modules-autolinking@2.0.8", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "fast-glob": "^3.2.5", "find-up": "^5.0.0", "fs-extra": "^9.1.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-DezgnEYFQYic8hKGhkbztBA3QUmSftjaNDIKNAtS2iGJmzCcNIkatjN2slFDSWjSTNo8gOvPQyMKfyHWFvLpOQ=="],
|
||||||
|
|
||||||
"expo-modules-core": ["expo-modules-core@2.2.2", "", { "dependencies": { "invariant": "^2.2.4" } }, "sha512-SgjK86UD89gKAscRK3bdpn6Ojfs/KU4GujtuFx1wm4JaBjmXH4aakWkItkPlAV2pjIiHJHWQbENL9xjbw/Qr/g=="],
|
"expo-modules-core": ["expo-modules-core@2.2.2", "", { "dependencies": { "invariant": "^2.2.4" } }, "sha512-SgjK86UD89gKAscRK3bdpn6Ojfs/KU4GujtuFx1wm4JaBjmXH4aakWkItkPlAV2pjIiHJHWQbENL9xjbw/Qr/g=="],
|
||||||
|
|
||||||
@@ -1184,7 +1186,7 @@
|
|||||||
|
|
||||||
"expo-task-manager": ["expo-task-manager@12.0.5", "", { "dependencies": { "unimodules-app-loader": "~5.0.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-tDHOBYORA6wuO32NWwz/Egrvn+N6aANHAa0DFs+01VK/IJZfU9D05ZN6M5XYIlZv5ll4GSX1wJZyTCY0HZGapw=="],
|
"expo-task-manager": ["expo-task-manager@12.0.5", "", { "dependencies": { "unimodules-app-loader": "~5.0.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-tDHOBYORA6wuO32NWwz/Egrvn+N6aANHAa0DFs+01VK/IJZfU9D05ZN6M5XYIlZv5ll4GSX1wJZyTCY0HZGapw=="],
|
||||||
|
|
||||||
"expo-updates": ["expo-updates@0.26.18", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~10.0.9", "@expo/config-plugins": "~9.0.15", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "expo-eas-client": "~0.13.2", "expo-manifests": "~0.15.5", "expo-structured-headers": "~4.0.0", "expo-updates-interface": "~1.0.0", "fast-glob": "^3.3.2", "fbemitter": "^3.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-i9on8jMLrDxtr3Jwpmqj14oa4PWxSKYrHhJYK40xATV6qrauTija9R7BkN0hQjD4LpElt5UJW2/YUP30UsTFqA=="],
|
"expo-updates": ["expo-updates@0.26.19", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "expo-eas-client": "~0.13.2", "expo-manifests": "~0.15.6", "expo-structured-headers": "~4.0.0", "expo-updates-interface": "~1.0.0", "fast-glob": "^3.3.2", "fbemitter": "^3.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-h40UrG0n1nCb2na1ffz+mNQtsnr7/BxxK+EtXJSqCaD9PIGaTGe20tasmo1oVskv3s37zfv0x93+6uTjanieQg=="],
|
||||||
|
|
||||||
"expo-updates-interface": ["expo-updates-interface@1.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ=="],
|
"expo-updates-interface": ["expo-updates-interface@1.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ=="],
|
||||||
|
|
||||||
@@ -1206,7 +1208,7 @@
|
|||||||
|
|
||||||
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
|
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
|
||||||
|
|
||||||
"fast-xml-parser": ["fast-xml-parser@4.5.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w=="],
|
"fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
|
"fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
|
||||||
|
|
||||||
@@ -1238,7 +1240,7 @@
|
|||||||
|
|
||||||
"flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="],
|
"flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="],
|
||||||
|
|
||||||
"flow-parser": ["flow-parser@0.261.1", "", {}, "sha512-2l5bBKeVtT+d+1CYSsTLJ+iP2FuoR7zjbDQI/v6dDRiBpx3Lb20Z/tLS37ReX/lcodyGSHC2eA/Nk63hB+mkYg=="],
|
"flow-parser": ["flow-parser@0.261.2", "", {}, "sha512-RtunoakA3YjtpAxPSOBVW6lmP5NYmETwkpAfNkdr8Ovf86ENkbD3mtPWnswFTIUtRvjwv0i8ZSkHK+AzsUg1JA=="],
|
||||||
|
|
||||||
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||||
|
|
||||||
@@ -1248,7 +1250,7 @@
|
|||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
|
"foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
|
||||||
|
|
||||||
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
|
"form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
|
||||||
|
|
||||||
"freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="],
|
"freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="],
|
||||||
|
|
||||||
@@ -1446,7 +1448,7 @@
|
|||||||
|
|
||||||
"join-component": ["join-component@1.1.0", "", {}, "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ=="],
|
"join-component": ["join-component@1.1.0", "", {}, "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ=="],
|
||||||
|
|
||||||
"jotai": ["jotai@2.12.0", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-j5B4NmUw8gbuN7AG4NufWw00rfpm6hexL2CVhKD7juoP2YyD9FEUV5ar921JMvadyrxQhU1NpuKUL3QfsAlVpA=="],
|
"jotai": ["jotai@2.12.1", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-VUW0nMPYIru5g89tdxwr9ftiVdc/nGV9jvHISN8Ucx+m1vI9dBeHemfqYzEuw5XSkmYjD/MEyApN9k6yrATsZQ=="],
|
||||||
|
|
||||||
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
||||||
|
|
||||||
@@ -1748,7 +1750,7 @@
|
|||||||
|
|
||||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.2", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA=="],
|
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||||
|
|
||||||
"postcss-calc": ["postcss-calc@8.2.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.2" } }, "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q=="],
|
"postcss-calc": ["postcss-calc@8.2.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.2" } }, "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q=="],
|
||||||
|
|
||||||
@@ -1818,7 +1820,7 @@
|
|||||||
|
|
||||||
"react-helmet-async": ["react-helmet-async@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "invariant": "^2.2.4", "prop-types": "^15.7.2", "react-fast-compare": "^3.2.0", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": "^16.6.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg=="],
|
"react-helmet-async": ["react-helmet-async@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "invariant": "^2.2.4", "prop-types": "^15.7.2", "react-fast-compare": "^3.2.0", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": "^16.6.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg=="],
|
||||||
|
|
||||||
"react-i18next": ["react-i18next@15.4.0", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw=="],
|
"react-i18next": ["react-i18next@15.4.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw=="],
|
||||||
|
|
||||||
"react-is": ["react-is@19.0.0", "", {}, "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="],
|
"react-is": ["react-is@19.0.0", "", {}, "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="],
|
||||||
|
|
||||||
@@ -1826,11 +1828,11 @@
|
|||||||
|
|
||||||
"react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="],
|
"react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="],
|
||||||
|
|
||||||
"react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.7", "", { "dependencies": { "react-freeze": "^1.0.0", "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-cVQYs4r8Hb9V9oOO/SqsmBaZ7IzE/3Tpvz4mmRjNXKi1cBWC+ZpKTuqRx6EPjBCYTVK+vbAfoTM6IHS+6NVg4w=="],
|
"react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.6", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-N5b3MoSfsEqlmvFyIyL0X0bd+QAtB+cXH1rl/+R2Kr0BefBTC7ZldGcPhgK3FhBbt0vJDpd3kLb/dvmqZd+Eag=="],
|
||||||
|
|
||||||
"react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="],
|
"react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="],
|
||||||
|
|
||||||
"react-native-compressor": ["react-native-compressor@1.10.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-i51DfTwfLcKorWbTXtnPOcQC4SQDuC+DqKkSl9wF9qAUmNS9PtipYZCXOvWShYFnX0mmcWw5vwEp2b2V73PaDQ=="],
|
"react-native-compressor": ["react-native-compressor@1.10.4", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-58gbmJ+8IvsKP8JKK1E8XW5trfQY3dNuH7S0hYw0tSRQc6l0GZ3k8TYtoUbySOc1xcQSrUo51o0Chwe8x7mUTg=="],
|
||||||
|
|
||||||
"react-native-country-flag": ["react-native-country-flag@2.0.2", "", {}, "sha512-5LMWxS79ZQ0Q9ntYgDYzWp794+HcQGXQmzzZNBR1AT7z5HcJHtX7rlk8RHi7RVzfp5gW6plWSZ4dKjRpu/OafQ=="],
|
"react-native-country-flag": ["react-native-country-flag@2.0.2", "", {}, "sha512-5LMWxS79ZQ0Q9ntYgDYzWp794+HcQGXQmzzZNBR1AT7z5HcJHtX7rlk8RHi7RVzfp5gW6plWSZ4dKjRpu/OafQ=="],
|
||||||
|
|
||||||
@@ -1904,7 +1906,7 @@
|
|||||||
|
|
||||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.3", "", { "dependencies": { "process": "^0.11.10", "readable-stream": "^4.7.0" } }, "sha512-In3boYjBnbGVrLuuRu/Ath/H6h1jgk30nAsk/71tCare1dTVoe1oMBGRn5LGf0n3c1BcHwwAqpraxX4AUAP5KA=="],
|
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
@@ -2042,7 +2044,7 @@
|
|||||||
|
|
||||||
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
|
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
|
||||||
|
|
||||||
"stacktrace-parser": ["stacktrace-parser@0.1.10", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg=="],
|
"stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="],
|
||||||
|
|
||||||
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||||
|
|
||||||
@@ -2068,7 +2070,7 @@
|
|||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||||
|
|
||||||
"strnum": ["strnum@1.0.5", "", {}, "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="],
|
"strnum": ["strnum@1.1.1", "", {}, "sha512-O7aCHfYCamLCctjAiaucmE+fHf2DYHkus2OKCn4Wv03sykfFtgeECn505X6K4mPl8CRNd/qurC9guq+ynoN4pw=="],
|
||||||
|
|
||||||
"strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
|
"strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
|
||||||
|
|
||||||
@@ -2192,7 +2194,7 @@
|
|||||||
|
|
||||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||||
|
|
||||||
"uuid": ["uuid@11.0.5", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA=="],
|
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
|
||||||
|
|
||||||
"validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="],
|
"validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="],
|
||||||
|
|
||||||
@@ -2286,7 +2288,7 @@
|
|||||||
|
|
||||||
"@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
"@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||||
|
|
||||||
"@expo/cli/form-data": ["form-data@3.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ=="],
|
"@expo/cli/form-data": ["form-data@3.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.35" } }, "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w=="],
|
||||||
|
|
||||||
"@expo/cli/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"@expo/cli/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
@@ -2294,7 +2296,7 @@
|
|||||||
|
|
||||||
"@expo/cli/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
"@expo/cli/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||||
|
|
||||||
"@expo/cli/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
"@expo/cli/ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="],
|
||||||
|
|
||||||
"@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
|
"@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
|
||||||
|
|
||||||
|
|||||||
@@ -1,99 +1,15 @@
|
|||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
import { useFavorite } from "@/hooks/useFavorite";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { View } from "react-native";
|
||||||
import { useAtom } from "jotai";
|
import { RoundButton } from "@/components/RoundButton";
|
||||||
import { useMemo } from "react";
|
|
||||||
import { TouchableOpacityProps, View, ViewProps } from "react-native";
|
|
||||||
import { RoundButton } from "./RoundButton";
|
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
type: "item" | "series";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddToFavorites: React.FC<Props> = ({ item, type, ...props }) => {
|
export const AddToFavorites = ({ item, ...props }) => {
|
||||||
const queryClient = useQueryClient();
|
const { isFavorite, toggleFavorite, _} = useFavorite(item);
|
||||||
const [api] = useAtom(apiAtom);
|
|
||||||
const [user] = useAtom(userAtom);
|
|
||||||
|
|
||||||
const isFavorite = useMemo(() => {
|
|
||||||
return item.UserData?.IsFavorite;
|
|
||||||
}, [item.UserData?.IsFavorite]);
|
|
||||||
|
|
||||||
const updateItemInQueries = (newData: Partial<BaseItemDto>) => {
|
|
||||||
queryClient.setQueryData<BaseItemDto | undefined>(
|
|
||||||
[type, item.Id],
|
|
||||||
(old) => {
|
|
||||||
if (!old) return old;
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
...newData,
|
|
||||||
UserData: { ...old.UserData, ...newData.UserData },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const markFavoriteMutation = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
if (api && user) {
|
|
||||||
await getUserLibraryApi(api).markFavoriteItem({
|
|
||||||
userId: user.Id,
|
|
||||||
itemId: item.Id!,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMutate: async () => {
|
|
||||||
await queryClient.cancelQueries({ queryKey: [type, item.Id] });
|
|
||||||
const previousItem = queryClient.getQueryData<BaseItemDto>([
|
|
||||||
type,
|
|
||||||
item.Id,
|
|
||||||
]);
|
|
||||||
updateItemInQueries({ UserData: { IsFavorite: true } });
|
|
||||||
|
|
||||||
return { previousItem };
|
|
||||||
},
|
|
||||||
onError: (err, variables, context) => {
|
|
||||||
if (context?.previousItem) {
|
|
||||||
queryClient.setQueryData([type, item.Id], context.previousItem);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: [type, item.Id] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["home", "favorites"] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const unmarkFavoriteMutation = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
if (api && user) {
|
|
||||||
await getUserLibraryApi(api).unmarkFavoriteItem({
|
|
||||||
userId: user.Id,
|
|
||||||
itemId: item.Id!,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMutate: async () => {
|
|
||||||
await queryClient.cancelQueries({ queryKey: [type, item.Id] });
|
|
||||||
const previousItem = queryClient.getQueryData<BaseItemDto>([
|
|
||||||
type,
|
|
||||||
item.Id,
|
|
||||||
]);
|
|
||||||
updateItemInQueries({ UserData: { IsFavorite: false } });
|
|
||||||
|
|
||||||
return { previousItem };
|
|
||||||
},
|
|
||||||
onError: (err, variables, context) => {
|
|
||||||
if (context?.previousItem) {
|
|
||||||
queryClient.setQueryData([type, item.Id], context.previousItem);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: [type, item.Id] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["home", "favorites"] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
@@ -101,13 +17,7 @@ export const AddToFavorites: React.FC<Props> = ({ item, type, ...props }) => {
|
|||||||
size="large"
|
size="large"
|
||||||
icon={isFavorite ? "heart" : "heart-outline"}
|
icon={isFavorite ? "heart" : "heart-outline"}
|
||||||
fillColor={isFavorite ? "primary" : undefined}
|
fillColor={isFavorite ? "primary" : undefined}
|
||||||
onPress={() => {
|
onPress={toggleFavorite}
|
||||||
if (isFavorite) {
|
|
||||||
unmarkFavoriteMutation.mutate();
|
|
||||||
} else {
|
|
||||||
markFavoriteMutation.mutate();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4";
|
|||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { queueActions, queueAtom } from "@/utils/atoms/queue";
|
import { queueActions, queueAtom } from "@/utils/atoms/queue";
|
||||||
import {DownloadMethod, useSettings} from "@/utils/atoms/settings";
|
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
|
||||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server";
|
import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server";
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import { Href, router, useFocusEffect } from "expo-router";
|
import { Href, router, useFocusEffect } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { Alert, View, ViewProps } from "react-native";
|
import { Alert, Platform, View, ViewProps } from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { AudioTrackSelector } from "./AudioTrackSelector";
|
import { AudioTrackSelector } from "./AudioTrackSelector";
|
||||||
import { Bitrate, BitrateSelector } from "./BitrateSelector";
|
import { Bitrate, BitrateSelector } from "./BitrateSelector";
|
||||||
@@ -66,10 +66,12 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
|
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
|
||||||
const [selectedSubtitleStream, setSelectedSubtitleStream] =
|
const [selectedSubtitleStream, setSelectedSubtitleStream] =
|
||||||
useState<number>(0);
|
useState<number>(0);
|
||||||
const [maxBitrate, setMaxBitrate] = useState<Bitrate>(settings?.defaultBitrate ?? {
|
const [maxBitrate, setMaxBitrate] = useState<Bitrate>(
|
||||||
key: "Max",
|
settings?.defaultBitrate ?? {
|
||||||
value: undefined,
|
key: "Max",
|
||||||
});
|
value: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const userCanDownload = useMemo(
|
const userCanDownload = useMemo(
|
||||||
() => user?.Policy?.EnableContentDownloading,
|
() => user?.Policy?.EnableContentDownloading,
|
||||||
@@ -162,7 +164,9 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(t("home.downloads.toasts.you_are_not_allowed_to_download_files"));
|
toast.error(
|
||||||
|
t("home.downloads.toasts.you_are_not_allowed_to_download_files")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
queue,
|
queue,
|
||||||
@@ -333,7 +337,10 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-neutral-300">
|
<Text className="text-neutral-300">
|
||||||
{subtitle || t("item_card.download.download_x_item", {item_count: itemsNotDownloaded.length})}
|
{subtitle ||
|
||||||
|
t("item_card.download.download_x_item", {
|
||||||
|
item_count: itemsNotDownloaded.length,
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex flex-col space-y-2 w-full items-start">
|
<View className="flex flex-col space-y-2 w-full items-start">
|
||||||
@@ -391,12 +398,16 @@ export const DownloadSingleItem: React.FC<{
|
|||||||
size?: "default" | "large";
|
size?: "default" | "large";
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
}> = ({ item, size = "default" }) => {
|
}> = ({ item, size = "default" }) => {
|
||||||
|
if (Platform.isTV) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DownloadItems
|
<DownloadItems
|
||||||
size={size}
|
size={size}
|
||||||
title={item.Type == "Episode"
|
title={
|
||||||
? t("item_card.download.download_episode")
|
item.Type == "Episode"
|
||||||
: t("item_card.download.download_movie")}
|
? t("item_card.download.download_episode")
|
||||||
|
: t("item_card.download.download_movie")
|
||||||
|
}
|
||||||
subtitle={item.Name!}
|
subtitle={item.Name!}
|
||||||
items={[item]}
|
items={[item]}
|
||||||
MissingDownloadIconComponent={() => (
|
MissingDownloadIconComponent={() => (
|
||||||
|
|||||||
@@ -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;
|
if (!tags || tags.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={`flex flex-row flex-wrap gap-1 ${props.className}`} {...props}>
|
<View className={`flex flex-row flex-wrap gap-1 ${props.className}`} {...props}>
|
||||||
{tags.map((tag, idx) => (
|
{tags.map((tag, idx) => (
|
||||||
<View key={idx}>
|
<View key={idx}>
|
||||||
<Tag key={idx} textClass={textClass} text={tag}/>
|
<Tag key={idx} textClass={textClass} text={tag} {...tagProps}/>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarous
|
|||||||
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
|
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
|
||||||
import { useImageColors } from "@/hooks/useImageColors";
|
import { useImageColors } from "@/hooks/useImageColors";
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
||||||
@@ -24,17 +25,16 @@ import {
|
|||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useNavigation } from "expo-router";
|
import { useNavigation } from "expo-router";
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
|
import { AddToFavorites } from "./AddToFavorites";
|
||||||
import { ItemHeader } from "./ItemHeader";
|
import { ItemHeader } from "./ItemHeader";
|
||||||
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
|
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
|
||||||
import { MediaSourceSelector } from "./MediaSourceSelector";
|
import { MediaSourceSelector } from "./MediaSourceSelector";
|
||||||
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
||||||
import { AddToFavorites } from "./AddToFavorites";
|
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
|
||||||
|
|
||||||
export type SelectedOptions = {
|
export type SelectedOptions = {
|
||||||
bitrate: Bitrate;
|
bitrate: Bitrate;
|
||||||
@@ -94,9 +94,11 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
/>
|
/>
|
||||||
{item.Type !== "Program" && (
|
{item.Type !== "Program" && (
|
||||||
<View className="flex flex-row items-center space-x-2">
|
<View className="flex flex-row items-center space-x-2">
|
||||||
<DownloadSingleItem item={item} size="large" />
|
{!Platform.isTV && (
|
||||||
|
<DownloadSingleItem item={item} size="large" />
|
||||||
|
)}
|
||||||
<PlayedStatus items={[item]} size="large" />
|
<PlayedStatus items={[item]} size="large" />
|
||||||
<AddToFavorites item={item} type="item" />
|
<AddToFavorites item={item} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -164,7 +166,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View className="flex flex-col bg-transparent shrink">
|
<View className="flex flex-col bg-transparent shrink">
|
||||||
{/* {!Platform.isTV && ( */}
|
|
||||||
<View className="flex flex-col px-4 w-full space-y-2 pt-2 mb-2 shrink">
|
<View className="flex flex-col px-4 w-full space-y-2 pt-2 mb-2 shrink">
|
||||||
<ItemHeader item={item} className="mb-4" />
|
<ItemHeader item={item} className="mb-4" />
|
||||||
{item.Type !== "Program" && !Platform.isTV && (
|
{item.Type !== "Program" && !Platform.isTV && (
|
||||||
@@ -222,13 +223,11 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* {!Platform.isTV && ( */}
|
|
||||||
<PlayButton
|
<PlayButton
|
||||||
className="grow"
|
className="grow"
|
||||||
selectedOptions={selectedOptions}
|
selectedOptions={selectedOptions}
|
||||||
item={item}
|
item={item}
|
||||||
/>
|
/>
|
||||||
{/* )} */}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{item.Type === "Episode" && (
|
{item.Type === "Episode" && (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { formatBitrate } from "@/utils/bitrate";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
source?: MediaSourceInfo;
|
source?: MediaSourceInfo;
|
||||||
@@ -54,14 +55,18 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
|
|||||||
<BottomSheetScrollView>
|
<BottomSheetScrollView>
|
||||||
<View className="flex flex-col space-y-2 p-4 mb-4">
|
<View className="flex flex-col space-y-2 p-4 mb-4">
|
||||||
<View className="">
|
<View className="">
|
||||||
<Text className="text-lg font-bold mb-4">{t("item_card.video")}</Text>
|
<Text className="text-lg font-bold mb-4">
|
||||||
|
{t("item_card.video")}
|
||||||
|
</Text>
|
||||||
<View className="flex flex-row space-x-2">
|
<View className="flex flex-row space-x-2">
|
||||||
<VideoStreamInfo source={source} />
|
<VideoStreamInfo source={source} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="">
|
<View className="">
|
||||||
<Text className="text-lg font-bold mb-2">{t("item_card.audio")}</Text>
|
<Text className="text-lg font-bold mb-2">
|
||||||
|
{t("item_card.audio")}
|
||||||
|
</Text>
|
||||||
<AudioStreamInfo
|
<AudioStreamInfo
|
||||||
audioStreams={
|
audioStreams={
|
||||||
source?.MediaStreams?.filter(
|
source?.MediaStreams?.filter(
|
||||||
@@ -72,7 +77,9 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="">
|
<View className="">
|
||||||
<Text className="text-lg font-bold mb-2">{t("item_card.subtitles")}</Text>
|
<Text className="text-lg font-bold mb-2">
|
||||||
|
{t("item_card.subtitles")}
|
||||||
|
</Text>
|
||||||
<SubtitleStreamInfo
|
<SubtitleStreamInfo
|
||||||
subtitleStreams={
|
subtitleStreams={
|
||||||
source?.MediaStreams?.filter(
|
source?.MediaStreams?.filter(
|
||||||
@@ -229,12 +236,3 @@ const formatFileSize = (bytes?: number | null) => {
|
|||||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
|
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
|
||||||
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + " " + sizes[i];
|
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + " " + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatBitrate = (bitrate?: number | null) => {
|
|
||||||
if (!bitrate) return "N/A";
|
|
||||||
|
|
||||||
const sizes = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"];
|
|
||||||
if (bitrate === 0) return "0 bps";
|
|
||||||
const i = parseInt(Math.floor(Math.log(bitrate) / Math.log(1000)).toString());
|
|
||||||
return Math.round((bitrate / Math.pow(1000, i)) * 100) / 100 + " " + sizes[i];
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Platform } from "react-native";
|
import { Platform, Pressable } from "react-native";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
@@ -32,9 +32,8 @@ import Animated, {
|
|||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { SelectedOptions } from "./ItemContent";
|
import { SelectedOptions } from "./ItemContent";
|
||||||
const chromecastProfile = !Platform.isTV
|
import { chromecast } from "@/utils/profiles/chromecast";
|
||||||
? require("@/utils/profiles/chromecast")
|
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
|
||||||
: null;
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
|
|
||||||
@@ -72,13 +71,14 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
(q: string, bitrateValue: number | undefined) => {
|
(q: string) => {
|
||||||
router.push(`/player/direct-player?${q}`);
|
router.push(`/player/direct-player?${q}`);
|
||||||
},
|
},
|
||||||
[router]
|
[router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPress = useCallback(async () => {
|
const onPress = useCallback(async () => {
|
||||||
|
console.log("onPress");
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
@@ -94,7 +94,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
goToPlayer(queryString);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,108 +113,111 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
|
|
||||||
switch (selectedIndex) {
|
switch (selectedIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
if (!Platform.isTV) {
|
await CastContext.getPlayServicesState().then(async (state) => {
|
||||||
await CastContext.getPlayServicesState().then(async (state) => {
|
if (state && state !== PlayServicesState.SUCCESS) {
|
||||||
if (state && state !== PlayServicesState.SUCCESS) {
|
CastContext.showPlayServicesErrorDialog(state);
|
||||||
CastContext.showPlayServicesErrorDialog(state);
|
} else {
|
||||||
} else {
|
// Check if user wants H265 for Chromecast
|
||||||
// Get a new URL with the Chromecast device profile:
|
const enableH265 = settings.enableH265ForChromecast;
|
||||||
try {
|
|
||||||
const data = await getStreamUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
deviceProfile: chromecastProfile,
|
|
||||||
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
|
||||||
userId: user?.Id,
|
|
||||||
audioStreamIndex: selectedOptions.audioIndex,
|
|
||||||
maxStreamingBitrate: selectedOptions.bitrate?.value,
|
|
||||||
mediaSourceId: selectedOptions.mediaSource?.Id,
|
|
||||||
subtitleStreamIndex: selectedOptions.subtitleIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data?.url) {
|
// Get a new URL with the Chromecast device profile
|
||||||
console.warn("No URL returned from getStreamUrl", data);
|
try {
|
||||||
Alert.alert(
|
const data = await getStreamUrl({
|
||||||
t("player.client_error"),
|
api,
|
||||||
t("player.could_not_create_stream_for_chromecast")
|
item,
|
||||||
);
|
deviceProfile: enableH265 ? chromecasth265 : chromecast,
|
||||||
return;
|
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
||||||
}
|
userId: user?.Id,
|
||||||
|
audioStreamIndex: selectedOptions.audioIndex,
|
||||||
|
maxStreamingBitrate: selectedOptions.bitrate?.value,
|
||||||
|
mediaSourceId: selectedOptions.mediaSource?.Id,
|
||||||
|
subtitleStreamIndex: selectedOptions.subtitleIndex,
|
||||||
|
});
|
||||||
|
|
||||||
client
|
console.log("URL: ", data?.url, enableH265);
|
||||||
.loadMedia({
|
|
||||||
mediaInfo: {
|
if (!data?.url) {
|
||||||
contentUrl: data?.url,
|
console.warn("No URL returned from getStreamUrl", data);
|
||||||
contentType: "video/mp4",
|
Alert.alert(
|
||||||
metadata:
|
t("player.client_error"),
|
||||||
item.Type === "Episode"
|
t("player.could_not_create_stream_for_chromecast")
|
||||||
? {
|
);
|
||||||
type: "tvShow",
|
return;
|
||||||
title: item.Name || "",
|
|
||||||
episodeNumber: item.IndexNumber || 0,
|
|
||||||
seasonNumber: item.ParentIndexNumber || 0,
|
|
||||||
seriesTitle: item.SeriesName || "",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: getParentBackdropImageUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
quality: 90,
|
|
||||||
width: 2000,
|
|
||||||
})!,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: item.Type === "Movie"
|
|
||||||
? {
|
|
||||||
type: "movie",
|
|
||||||
title: item.Name || "",
|
|
||||||
subtitle: item.Overview || "",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: getPrimaryImageUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
quality: 90,
|
|
||||||
width: 2000,
|
|
||||||
})!,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: "generic",
|
|
||||||
title: item.Name || "",
|
|
||||||
subtitle: item.Overview || "",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: getPrimaryImageUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
quality: 90,
|
|
||||||
width: 2000,
|
|
||||||
})!,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
startTime: 0,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// state is already set when reopening current media, so skip it here.
|
|
||||||
if (isOpeningCurrentlyPlayingMedia) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
CastContext.showExpandedControls();
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client
|
||||||
|
.loadMedia({
|
||||||
|
mediaInfo: {
|
||||||
|
contentUrl: data?.url,
|
||||||
|
contentType: "video/mp4",
|
||||||
|
metadata:
|
||||||
|
item.Type === "Episode"
|
||||||
|
? {
|
||||||
|
type: "tvShow",
|
||||||
|
title: item.Name || "",
|
||||||
|
episodeNumber: item.IndexNumber || 0,
|
||||||
|
seasonNumber: item.ParentIndexNumber || 0,
|
||||||
|
seriesTitle: item.SeriesName || "",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: getParentBackdropImageUrl({
|
||||||
|
api,
|
||||||
|
item,
|
||||||
|
quality: 90,
|
||||||
|
width: 2000,
|
||||||
|
})!,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: item.Type === "Movie"
|
||||||
|
? {
|
||||||
|
type: "movie",
|
||||||
|
title: item.Name || "",
|
||||||
|
subtitle: item.Overview || "",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: getPrimaryImageUrl({
|
||||||
|
api,
|
||||||
|
item,
|
||||||
|
quality: 90,
|
||||||
|
width: 2000,
|
||||||
|
})!,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: "generic",
|
||||||
|
title: item.Name || "",
|
||||||
|
subtitle: item.Overview || "",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: getPrimaryImageUrl({
|
||||||
|
api,
|
||||||
|
item,
|
||||||
|
quality: 90,
|
||||||
|
width: 2000,
|
||||||
|
})!,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: 0,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// state is already set when reopening current media, so skip it here.
|
||||||
|
if (isOpeningCurrentlyPlayingMedia) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CastContext.showExpandedControls();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
goToPlayer(queryString);
|
||||||
break;
|
break;
|
||||||
case cancelButtonIndex:
|
case cancelButtonIndex:
|
||||||
break;
|
break;
|
||||||
@@ -323,75 +326,62 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<TouchableOpacity
|
||||||
<TouchableOpacity
|
disabled={!item}
|
||||||
disabled={!item}
|
accessibilityLabel="Play button"
|
||||||
accessibilityLabel="Play button"
|
accessibilityHint="Tap to play the media"
|
||||||
accessibilityHint="Tap to play the media"
|
onPress={onPress}
|
||||||
onPress={onPress}
|
className={`relative`}
|
||||||
className={`relative`}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<View className="absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden">
|
||||||
<View className="absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden">
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
animatedPrimaryStyle,
|
|
||||||
animatedWidthStyle,
|
|
||||||
{
|
|
||||||
height: "100%",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[animatedAverageStyle, { opacity: 0.5 }]}
|
style={[
|
||||||
className="absolute w-full h-full top-0 left-0 rounded-xl"
|
animatedPrimaryStyle,
|
||||||
|
animatedWidthStyle,
|
||||||
|
{
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<View
|
</View>
|
||||||
style={{
|
|
||||||
borderWidth: 1,
|
<Animated.View
|
||||||
borderColor: colorAtom.primary,
|
style={[animatedAverageStyle, { opacity: 0.5 }]}
|
||||||
borderStyle: "solid",
|
className="absolute w-full h-full top-0 left-0 rounded-xl"
|
||||||
}}
|
/>
|
||||||
className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full "
|
<View
|
||||||
>
|
style={{
|
||||||
<View className="flex flex-row items-center space-x-2">
|
borderWidth: 1,
|
||||||
<Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}>
|
borderColor: colorAtom.primary,
|
||||||
{runtimeTicksToMinutes(item?.RunTimeTicks)}
|
borderStyle: "solid",
|
||||||
</Animated.Text>
|
}}
|
||||||
|
className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full "
|
||||||
|
>
|
||||||
|
<View className="flex flex-row items-center space-x-2">
|
||||||
|
<Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}>
|
||||||
|
{runtimeTicksToMinutes(item?.RunTimeTicks)}
|
||||||
|
</Animated.Text>
|
||||||
|
<Animated.Text style={animatedTextStyle}>
|
||||||
|
<Ionicons name="play-circle" size={24} />
|
||||||
|
</Animated.Text>
|
||||||
|
{client && (
|
||||||
<Animated.Text style={animatedTextStyle}>
|
<Animated.Text style={animatedTextStyle}>
|
||||||
<Ionicons name="play-circle" size={24} />
|
<Feather name="cast" size={22} />
|
||||||
|
<CastButton tintColor="transparent" />
|
||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
{client && (
|
)}
|
||||||
<Animated.Text style={animatedTextStyle}>
|
{!client && settings?.openInVLC && (
|
||||||
<Feather name="cast" size={22} />
|
<Animated.Text style={animatedTextStyle}>
|
||||||
<CastButton tintColor="transparent" />
|
<MaterialCommunityIcons
|
||||||
</Animated.Text>
|
name="vlc"
|
||||||
)}
|
size={18}
|
||||||
{!client && settings?.openInVLC && (
|
color={animatedTextStyle.color}
|
||||||
<Animated.Text style={animatedTextStyle}>
|
/>
|
||||||
<MaterialCommunityIcons
|
</Animated.Text>
|
||||||
name="vlc"
|
)}
|
||||||
size={18}
|
|
||||||
color={animatedTextStyle.color}
|
|
||||||
/>
|
|
||||||
</Animated.Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
{/* <View className="mt-2 flex flex-row items-center">
|
</TouchableOpacity>
|
||||||
<Ionicons
|
|
||||||
name="information-circle"
|
|
||||||
size={12}
|
|
||||||
className=""
|
|
||||||
color={"#9BA1A6"}
|
|
||||||
/>
|
|
||||||
<Text className="text-neutral-500 ml-1">
|
|
||||||
{directStream ? "Direct stream" : "Transcoded stream"}
|
|
||||||
</Text>
|
|
||||||
</View> */}
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ const ANIMATION_DURATION = 500;
|
|||||||
const MIN_PLAYBACK_WIDTH = 15;
|
const MIN_PLAYBACK_WIDTH = 15;
|
||||||
|
|
||||||
export const PlayButton: React.FC<Props> = ({
|
export const PlayButton: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
selectedOptions,
|
selectedOptions,
|
||||||
...props
|
...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -57,13 +57,14 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
(q: string, bitrateValue: number | undefined) => {
|
(q: string) => {
|
||||||
router.push(`/player/direct-player?${q}`);
|
router.push(`/player/direct-player?${q}`);
|
||||||
},
|
},
|
||||||
[router]
|
[router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPress = useCallback(async () => {
|
const onPress = () => {
|
||||||
|
console.log("onpress");
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
@@ -77,17 +78,9 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
goToPlayer(queryString);
|
||||||
return;
|
return;
|
||||||
}, [
|
};
|
||||||
item,
|
|
||||||
settings,
|
|
||||||
api,
|
|
||||||
user,
|
|
||||||
router,
|
|
||||||
showActionSheetWithOptions,
|
|
||||||
selectedOptions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const derivedTargetWidth = useDerivedValue(() => {
|
const derivedTargetWidth = useDerivedValue(() => {
|
||||||
if (!item || !item.RunTimeTicks) return 0;
|
if (!item || !item.RunTimeTicks) return 0;
|
||||||
@@ -95,9 +88,9 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
if (userData && userData.PlaybackPositionTicks) {
|
if (userData && userData.PlaybackPositionTicks) {
|
||||||
return userData.PlaybackPositionTicks > 0
|
return userData.PlaybackPositionTicks > 0
|
||||||
? Math.max(
|
? Math.max(
|
||||||
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
||||||
MIN_PLAYBACK_WIDTH
|
MIN_PLAYBACK_WIDTH
|
||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -179,69 +172,55 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<TouchableOpacity
|
||||||
<TouchableOpacity
|
accessibilityLabel="Play button"
|
||||||
disabled={!item}
|
accessibilityHint="Tap to play the media"
|
||||||
accessibilityLabel="Play button"
|
onPress={onPress}
|
||||||
accessibilityHint="Tap to play the media"
|
className={`relative`}
|
||||||
onPress={onPress}
|
{...props}
|
||||||
className={`relative`}
|
>
|
||||||
{...props}
|
<View className="absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden">
|
||||||
>
|
|
||||||
<View className="absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden">
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
animatedPrimaryStyle,
|
|
||||||
animatedWidthStyle,
|
|
||||||
{
|
|
||||||
height: "100%",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[animatedAverageStyle, { opacity: 0.5 }]}
|
style={[
|
||||||
className="absolute w-full h-full top-0 left-0 rounded-xl"
|
animatedPrimaryStyle,
|
||||||
|
animatedWidthStyle,
|
||||||
|
{
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<View
|
</View>
|
||||||
style={{
|
|
||||||
borderWidth: 1,
|
<Animated.View
|
||||||
borderColor: colorAtom.primary,
|
style={[animatedAverageStyle, { opacity: 0.5 }]}
|
||||||
borderStyle: "solid",
|
className="absolute w-full h-full top-0 left-0 rounded-xl"
|
||||||
}}
|
/>
|
||||||
className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full "
|
<View
|
||||||
>
|
style={{
|
||||||
<View className="flex flex-row items-center space-x-2">
|
borderWidth: 1,
|
||||||
<Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}>
|
borderColor: colorAtom.primary,
|
||||||
{runtimeTicksToMinutes(item?.RunTimeTicks)}
|
borderStyle: "solid",
|
||||||
</Animated.Text>
|
}}
|
||||||
|
className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full "
|
||||||
|
>
|
||||||
|
<View className="flex flex-row items-center space-x-2">
|
||||||
|
<Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}>
|
||||||
|
{runtimeTicksToMinutes(item?.RunTimeTicks)}
|
||||||
|
</Animated.Text>
|
||||||
|
<Animated.Text style={animatedTextStyle}>
|
||||||
|
<Ionicons name="play-circle" size={24} />
|
||||||
|
</Animated.Text>
|
||||||
|
{settings?.openInVLC && (
|
||||||
<Animated.Text style={animatedTextStyle}>
|
<Animated.Text style={animatedTextStyle}>
|
||||||
<Ionicons name="play-circle" size={24} />
|
<MaterialCommunityIcons
|
||||||
|
name="vlc"
|
||||||
|
size={18}
|
||||||
|
color={animatedTextStyle.color}
|
||||||
|
/>
|
||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
{settings?.openInVLC && (
|
)}
|
||||||
<Animated.Text style={animatedTextStyle}>
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="vlc"
|
|
||||||
size={18}
|
|
||||||
color={animatedTextStyle.color}
|
|
||||||
/>
|
|
||||||
</Animated.Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
{/* <View className="mt-2 flex flex-row items-center">
|
</TouchableOpacity>
|
||||||
<Ionicons
|
|
||||||
name="information-circle"
|
|
||||||
size={12}
|
|
||||||
className=""
|
|
||||||
color={"#9BA1A6"}
|
|
||||||
/>
|
|
||||||
<Text className="text-neutral-500 ml-1">
|
|
||||||
{directStream ? "Direct stream" : "Transcoded stream"}
|
|
||||||
</Text>
|
|
||||||
</View> */}
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
|||||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
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 {
|
interface Props extends ViewProps {
|
||||||
item?: BaseItemDto | null;
|
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,
|
result,
|
||||||
}) => {
|
}) => {
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
const { jellyseerrApi, getMediaType } = useJellyseerr();
|
||||||
|
|
||||||
|
const mediaType = useMemo(() => getMediaType(result), [result]);
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ["jellyseerr", result.id, result.mediaType, "ratings"],
|
queryKey: ["jellyseerr", result.id, mediaType, "ratings"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return result.mediaType === MediaType.MOVIE
|
return mediaType === MediaType.MOVIE
|
||||||
? jellyseerrApi?.movieRatings(result.id)
|
? jellyseerrApi?.movieRatings(result.id)
|
||||||
: jellyseerrApi?.tvRatings(result.id);
|
: jellyseerrApi?.tvRatings(result.id);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<View className="flex flex-col " {...props}>
|
<View className="flex flex-col " {...props}>
|
||||||
<Text className="opacity-50 mb-1 text-xs">
|
<Text numberOfLines={1} className="opacity-50 mb-1 text-xs">
|
||||||
{t("item_card.subtitles")}
|
{t("item_card.subtitles")}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface Props<T> {
|
|||||||
title: string | ReactNode;
|
title: string | ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
onSelected: (...item: T[]) => void;
|
onSelected: (...item: T[]) => void;
|
||||||
multi?: boolean;
|
multiple?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dropdown = <T extends unknown>({
|
const Dropdown = <T extends unknown>({
|
||||||
@@ -30,7 +30,7 @@ const Dropdown = <T extends unknown>({
|
|||||||
title,
|
title,
|
||||||
label,
|
label,
|
||||||
onSelected,
|
onSelected,
|
||||||
multi = false,
|
multiple = false,
|
||||||
...props
|
...props
|
||||||
}: PropsWithChildren<Props<T> & ViewProps>) => {
|
}: PropsWithChildren<Props<T> & ViewProps>) => {
|
||||||
if (Platform.isTV) return null;
|
if (Platform.isTV) return null;
|
||||||
@@ -72,7 +72,7 @@ const Dropdown = <T extends unknown>({
|
|||||||
>
|
>
|
||||||
<DropdownMenu.Label>{label}</DropdownMenu.Label>
|
<DropdownMenu.Label>{label}</DropdownMenu.Label>
|
||||||
{data.map((item, idx) =>
|
{data.map((item, idx) =>
|
||||||
multi ? (
|
multiple ? (
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
value={
|
value={
|
||||||
selected?.some((s) => keyExtractor(s) == keyExtractor(item))
|
selected?.some((s) => keyExtractor(s) == keyExtractor(item))
|
||||||
@@ -80,7 +80,7 @@ const Dropdown = <T extends unknown>({
|
|||||||
: "off"
|
: "off"
|
||||||
}
|
}
|
||||||
key={keyExtractor(item)}
|
key={keyExtractor(item)}
|
||||||
onValueChange={(next, previous) =>
|
onValueChange={(next: "on" | "off", previous: "on" | "off") => {
|
||||||
setSelected((p) => {
|
setSelected((p) => {
|
||||||
const prev = p || [];
|
const prev = p || [];
|
||||||
if (next == "on") {
|
if (next == "on") {
|
||||||
@@ -92,7 +92,7 @@ const Dropdown = <T extends unknown>({
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle>
|
<DropdownMenu.ItemTitle>
|
||||||
{titleExtractor(item)}
|
{titleExtractor(item)}
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ import {
|
|||||||
Permission,
|
Permission,
|
||||||
} from "@/utils/jellyseerr/server/lib/permissions";
|
} from "@/utils/jellyseerr/server/lib/permissions";
|
||||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
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 {
|
interface Props extends TouchableOpacityProps {
|
||||||
result: MovieResult | TvResult;
|
result: MovieResult | TvResult | MovieDetails | TvDetails;
|
||||||
mediaTitle: string;
|
mediaTitle: string;
|
||||||
releaseYear: number;
|
releaseYear: number;
|
||||||
canRequest: boolean;
|
canRequest: boolean;
|
||||||
posterSrc: string;
|
posterSrc: string;
|
||||||
|
mediaType: MediaType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||||
@@ -24,6 +27,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
releaseYear,
|
releaseYear,
|
||||||
canRequest,
|
canRequest,
|
||||||
posterSrc,
|
posterSrc,
|
||||||
|
mediaType,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
@@ -46,7 +50,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
() =>
|
() =>
|
||||||
requestMedia(mediaTitle, {
|
requestMedia(mediaTitle, {
|
||||||
mediaId: result.id,
|
mediaId: result.id,
|
||||||
mediaType: result.mediaType,
|
mediaType,
|
||||||
}),
|
}),
|
||||||
[jellyseerrApi, result]
|
[jellyseerrApi, result]
|
||||||
);
|
);
|
||||||
@@ -67,6 +71,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
releaseYear,
|
releaseYear,
|
||||||
canRequest,
|
canRequest,
|
||||||
posterSrc,
|
posterSrc,
|
||||||
|
mediaType
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -83,7 +88,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
key={"content"}
|
key={"content"}
|
||||||
>
|
>
|
||||||
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
|
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
|
||||||
{canRequest && result.mediaType === MediaType.MOVIE && (
|
{canRequest && mediaType === MediaType.MOVIE && (
|
||||||
<ContextMenu.Item
|
<ContextMenu.Item
|
||||||
key="item-1"
|
key="item-1"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { TextProps } from "react-native";
|
import { Platform, TextProps } from "react-native";
|
||||||
import { UITextView } from "react-native-uitextview";
|
import { UITextView } from "react-native-uitextview";
|
||||||
|
import { Text as RNText } from "react-native";
|
||||||
export function Text(
|
export function Text(
|
||||||
props: TextProps & {
|
props: TextProps & {
|
||||||
uiTextView?: boolean;
|
uiTextView?: boolean;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { style, ...otherProps } = props;
|
const { style, ...otherProps } = props;
|
||||||
|
if (Platform.isTV)
|
||||||
return (
|
return (
|
||||||
<UITextView
|
<RNText
|
||||||
allowFontScaling={false}
|
allowFontScaling={false}
|
||||||
style={[{ color: "white" }, style]}
|
style={[{ color: "white" }, style]}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<UITextView
|
||||||
|
allowFontScaling={false}
|
||||||
|
style={[{ color: "white" }, style]}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
|
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
|
||||||
|
import { useFavorite } from "@/hooks/useFavorite";
|
||||||
import {
|
import {
|
||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
BaseItemPerson,
|
BaseItemPerson,
|
||||||
@@ -7,7 +8,6 @@ import { useRouter, useSegments } from "expo-router";
|
|||||||
import { PropsWithChildren, useCallback } from "react";
|
import { PropsWithChildren, useCallback } from "react";
|
||||||
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
|
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
|
||||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -57,14 +57,14 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const markAsPlayedStatus = useMarkAsPlayed([item]);
|
const markAsPlayedStatus = useMarkAsPlayed([item]);
|
||||||
|
const { isFavorite, toggleFavorite } = useFavorite(item);
|
||||||
|
|
||||||
const from = segments[2];
|
const from = segments[2];
|
||||||
|
|
||||||
const showActionSheet = useCallback(() => {
|
const showActionSheet = useCallback(() => {
|
||||||
if (!(item.Type === "Movie" || item.Type === "Episode")) return;
|
if (!(item.Type === "Movie" || item.Type === "Episode" || item.Type === "Series")) return;
|
||||||
|
const options = ["Mark as Played", "Mark as Not Played", isFavorite ? "Unmark as Favorite" : "Mark as Favorite", "Cancel"];
|
||||||
const options = ["Mark as Played", "Mark as Not Played", "Cancel"];
|
const cancelButtonIndex = 3;
|
||||||
const cancelButtonIndex = 2;
|
|
||||||
|
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
@@ -74,14 +74,14 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
async (selectedIndex) => {
|
async (selectedIndex) => {
|
||||||
if (selectedIndex === 0) {
|
if (selectedIndex === 0) {
|
||||||
await markAsPlayedStatus(true);
|
await markAsPlayedStatus(true);
|
||||||
// Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
||||||
} else if (selectedIndex === 1) {
|
} else if (selectedIndex === 1) {
|
||||||
await markAsPlayedStatus(false);
|
await markAsPlayedStatus(false);
|
||||||
// Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
} else if (selectedIndex === 2) {
|
||||||
|
toggleFavorite()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [showActionSheetWithOptions, markAsPlayedStatus]);
|
}, [showActionSheetWithOptions, isFavorite, markAsPlayedStatus]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
from === "(home)" ||
|
from === "(home)" ||
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import {DownloadMethod, useSettings} from "@/utils/atoms/settings";
|
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
import { JobStatus } from "@/utils/optimize-server";
|
import { JobStatus } from "@/utils/optimize-server";
|
||||||
import { formatTimeString } from "@/utils/time";
|
import { formatTimeString } from "@/utils/time";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
const BackGroundDownloader = !Platform.isTV
|
|
||||||
? require("@kesha-antonov/react-native-background-downloader")
|
|
||||||
: null;
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
const FFmpegKitProvider = !Platform.isTV ? require("ffmpeg-kit-react-native") : null;
|
import { t } from "i18next";
|
||||||
import { useAtom } from "jotai";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Platform,
|
Platform,
|
||||||
@@ -21,10 +20,12 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
import { Image } from "expo-image";
|
const BackGroundDownloader = !Platform.isTV
|
||||||
import { useMemo } from "react";
|
? require("@kesha-antonov/react-native-background-downloader")
|
||||||
import { storage } from "@/utils/mmkv";
|
: null;
|
||||||
import { t } from "i18next";
|
const FFmpegKitProvider = !Platform.isTV
|
||||||
|
? require("ffmpeg-kit-react-native")
|
||||||
|
: null;
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
@@ -33,14 +34,20 @@ export const ActiveDownloads: React.FC<Props> = ({ ...props }) => {
|
|||||||
if (processes?.length === 0)
|
if (processes?.length === 0)
|
||||||
return (
|
return (
|
||||||
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
|
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
|
||||||
<Text className="text-lg font-bold">{t("home.downloads.active_download")}</Text>
|
<Text className="text-lg font-bold">
|
||||||
<Text className="opacity-50">{t("home.downloads.no_active_downloads")}</Text>
|
{t("home.downloads.active_download")}
|
||||||
|
</Text>
|
||||||
|
<Text className="opacity-50">
|
||||||
|
{t("home.downloads.no_active_downloads")}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
|
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
|
||||||
<Text className="text-lg font-bold mb-2">{t("home.downloads.active_downloads")}</Text>
|
<Text className="text-lg font-bold mb-2">
|
||||||
|
{t("home.downloads.active_downloads")}
|
||||||
|
</Text>
|
||||||
<View className="space-y-2">
|
<View className="space-y-2">
|
||||||
{processes?.map((p: JobStatus) => (
|
{processes?.map((p: JobStatus) => (
|
||||||
<DownloadCard key={p.item.Id} process={p} />
|
<DownloadCard key={p.item.Id} process={p} />
|
||||||
@@ -81,7 +88,9 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
FFmpegKitProvider.FFmpegKit.cancel(Number(id));
|
FFmpegKitProvider.FFmpegKit.cancel(Number(id));
|
||||||
setProcesses((prev: any[]) => prev.filter((p: { id: string; }) => p.id !== id));
|
setProcesses((prev: any[]) =>
|
||||||
|
prev.filter((p: { id: string }) => p.id !== id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -156,7 +165,9 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
|||||||
<Text className="text-xs">{process.speed?.toFixed(2)}x</Text>
|
<Text className="text-xs">{process.speed?.toFixed(2)}x</Text>
|
||||||
)}
|
)}
|
||||||
{eta(process) && (
|
{eta(process) && (
|
||||||
<Text className="text-xs">{t("home.downloads.eta", {eta: eta(process)})}</Text>
|
<Text className="text-xs">
|
||||||
|
{t("home.downloads.eta", { eta: eta(process) })}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { LoadingSkeleton } from "../search/LoadingSkeleton";
|
|||||||
import { SearchItemWrapper } from "../search/SearchItemWrapper";
|
import { SearchItemWrapper } from "../search/SearchItemWrapper";
|
||||||
import PersonPoster from "./PersonPoster";
|
import PersonPoster from "./PersonPoster";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {uniqBy} from "lodash";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@@ -77,25 +78,28 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
|||||||
|
|
||||||
const jellyseerrMovieResults = useMemo(
|
const jellyseerrMovieResults = useMemo(
|
||||||
() =>
|
() =>
|
||||||
jellyseerrResults?.filter(
|
uniqBy(
|
||||||
(r) => r.mediaType === MediaType.MOVIE
|
jellyseerrResults?.filter((r) => r.mediaType === MediaType.MOVIE) as MovieResult[],
|
||||||
) as MovieResult[],
|
"id"
|
||||||
|
),
|
||||||
[jellyseerrResults]
|
[jellyseerrResults]
|
||||||
);
|
);
|
||||||
|
|
||||||
const jellyseerrTvResults = useMemo(
|
const jellyseerrTvResults = useMemo(
|
||||||
() =>
|
() =>
|
||||||
jellyseerrResults?.filter(
|
uniqBy(
|
||||||
(r) => r.mediaType === MediaType.TV
|
jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[],
|
||||||
) as TvResult[],
|
"id"
|
||||||
|
),
|
||||||
[jellyseerrResults]
|
[jellyseerrResults]
|
||||||
);
|
);
|
||||||
|
|
||||||
const jellyseerrPersonResults = useMemo(
|
const jellyseerrPersonResults = useMemo(
|
||||||
() =>
|
() =>
|
||||||
jellyseerrResults?.filter(
|
uniqBy(
|
||||||
(r) => r.mediaType === "person"
|
jellyseerrResults?.filter((r) => r.mediaType === "person") as PersonResult[],
|
||||||
) as PersonResult[],
|
"id"
|
||||||
|
),
|
||||||
[jellyseerrResults]
|
[jellyseerrResults]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -15,18 +15,22 @@ import { useTranslation } from "react-i18next";
|
|||||||
interface Props {
|
interface Props {
|
||||||
id: number;
|
id: number;
|
||||||
title: string,
|
title: string,
|
||||||
|
requestBody?: MediaRequestBody,
|
||||||
type: MediaType;
|
type: MediaType;
|
||||||
isAnime?: boolean;
|
isAnime?: boolean;
|
||||||
is4k?: boolean;
|
is4k?: boolean;
|
||||||
onRequested?: () => void;
|
onRequested?: () => void;
|
||||||
|
onDismiss?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps, 'id'>>(({
|
const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps, 'id'>>(({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
requestBody,
|
||||||
type,
|
type,
|
||||||
isAnime = false,
|
isAnime = false,
|
||||||
onRequested,
|
onRequested,
|
||||||
|
onDismiss,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr();
|
const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr();
|
||||||
@@ -39,8 +43,6 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [modalRequestProps, setModalRequestProps] = useState<MediaRequestBody>();
|
|
||||||
|
|
||||||
const {data: serviceSettings} = useQuery({
|
const {data: serviceSettings} = useQuery({
|
||||||
queryKey: ["jellyseerr", "request", type, 'service'],
|
queryKey: ["jellyseerr", "request", type, 'service'],
|
||||||
queryFn: async () => jellyseerrApi?.service(type == 'movie' ? 'radarr' : 'sonarr'),
|
queryFn: async () => jellyseerrApi?.service(type == 'movie' ? 'radarr' : 'sonarr'),
|
||||||
@@ -98,16 +100,19 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
: defaultServiceDetails?.server.activeTags
|
: defaultServiceDetails?.server.activeTags
|
||||||
)?.includes(t.id)
|
)?.includes(t.id)
|
||||||
) ?? []
|
) ?? []
|
||||||
|
|
||||||
console.log(tags)
|
|
||||||
return tags
|
return tags
|
||||||
},
|
},
|
||||||
[defaultServiceDetails]
|
[defaultServiceDetails]
|
||||||
);
|
);
|
||||||
|
|
||||||
const seasonTitle = useMemo(
|
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(
|
const request = useCallback(() => {requestMedia(
|
||||||
@@ -117,12 +122,12 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
profileId: defaultProfile.id,
|
profileId: defaultProfile.id,
|
||||||
rootFolder: defaultFolder.path,
|
rootFolder: defaultFolder.path,
|
||||||
tags: defaultTags.map(t => t.id),
|
tags: defaultTags.map(t => t.id),
|
||||||
...modalRequestProps,
|
...requestBody,
|
||||||
...requestOverrides
|
...requestOverrides
|
||||||
},
|
},
|
||||||
onRequested
|
onRequested
|
||||||
)
|
)
|
||||||
}, [modalRequestProps, requestOverrides, defaultProfile, defaultFolder, defaultTags]);
|
}, [requestBody, requestOverrides, defaultProfile, defaultFolder, defaultTags]);
|
||||||
|
|
||||||
const pathTitleExtractor = (item: RootFolder) => `${item.path} (${item.freeSpace.bytesToReadable()})`;
|
const pathTitleExtractor = (item: RootFolder) => `${item.path} (${item.freeSpace.bytesToReadable()})`;
|
||||||
|
|
||||||
@@ -131,7 +136,7 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
enableDynamicSizing
|
enableDynamicSizing
|
||||||
enableDismissOnClose
|
enableDismissOnClose
|
||||||
onDismiss={() => setModalRequestProps(undefined)}
|
onDismiss={onDismiss}
|
||||||
handleIndicatorStyle={{
|
handleIndicatorStyle={{
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
}}
|
}}
|
||||||
@@ -146,89 +151,86 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(data) => {
|
<BottomSheetView>
|
||||||
setModalRequestProps(data?.data as MediaRequestBody)
|
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
||||||
return <BottomSheetView>
|
<View>
|
||||||
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
<Text className="font-bold text-2xl text-neutral-100">{t("jellyseerr.advanced")}</Text>
|
||||||
<View>
|
{seasonTitle &&
|
||||||
<Text className="font-bold text-2xl text-neutral-100">{t("jellyseerr.advanced")}</Text>
|
<Text className="text-neutral-300">{seasonTitle}</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>
|
||||||
</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>
|
</BottomSheetModal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {View} from "react-native";
|
|||||||
import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
|
import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
|
||||||
import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
|
import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
|
||||||
import GenreSlide from "@/components/jellyseerr/discover/GenreSlide";
|
import GenreSlide from "@/components/jellyseerr/discover/GenreSlide";
|
||||||
|
import RecentRequestsSlide from "@/components/jellyseerr/discover/RecentRequestsSlide";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sliders?: DiscoverSlider[];
|
sliders?: DiscoverSlider[];
|
||||||
@@ -25,6 +26,8 @@ const Discover: React.FC<Props> = ({ sliders }) => {
|
|||||||
<View className="flex flex-col space-y-4 mb-8">
|
<View className="flex flex-col space-y-4 mb-8">
|
||||||
{sortedSliders.map(slide => {
|
{sortedSliders.map(slide => {
|
||||||
switch (slide.type) {
|
switch (slide.type) {
|
||||||
|
case DiscoverSliderType.RECENT_REQUESTS:
|
||||||
|
return <RecentRequestsSlide key={slide.id} slide={slide} />
|
||||||
case DiscoverSliderType.NETWORKS:
|
case DiscoverSliderType.NETWORKS:
|
||||||
return <CompanySlide key={slide.id} slide={slide} data={networks}/>
|
return <CompanySlide key={slide.id} slide={slide} data={networks}/>
|
||||||
case DiscoverSliderType.STUDIOS:
|
case DiscoverSliderType.STUDIOS:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
|||||||
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||||
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
|
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
|
||||||
import {ViewProps} from "react-native";
|
import {ViewProps} from "react-native";
|
||||||
|
import {uniqBy} from "lodash";
|
||||||
|
|
||||||
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
const { jellyseerrApi } = useJellyseerr();
|
||||||
@@ -57,7 +58,11 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
const flatData = useMemo(
|
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]
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -74,7 +79,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) =>
|
|||||||
fetchNextPage()
|
fetchNextPage()
|
||||||
}}
|
}}
|
||||||
renderItem={(item) =>
|
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;
|
||||||
@@ -29,7 +29,7 @@ export const ListGroup: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
<View
|
<View
|
||||||
style={[]}
|
style={[]}
|
||||||
className="flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900"
|
className="flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900"
|
||||||
>
|
>
|
||||||
{Children.map(childrenArray, (child, index) => {
|
{Children.map(childrenArray, (child, index) => {
|
||||||
if (isValidElement<{ style?: ViewStyle }>(child)) {
|
if (isValidElement<{ style?: ViewStyle }>(child)) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
className={`flex flex-row items-center justify-between bg-neutral-900 h-11 pr-4 ${
|
className={`flex flex-row items-center justify-between bg-neutral-900 h-11 pr-4 pl-4 ${
|
||||||
disabled ? "opacity-50" : ""
|
disabled ? "opacity-50" : ""
|
||||||
}`}
|
}`}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -55,7 +55,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
className={`flex flex-row items-center justify-between bg-neutral-900 h-11 pr-4 ${
|
className={`flex flex-row items-center justify-between bg-neutral-900 h-11 pr-4 pl-4 ${
|
||||||
disabled ? "opacity-50" : ""
|
disabled ? "opacity-50" : ""
|
||||||
}`}
|
}`}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,28 +1,42 @@
|
|||||||
import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter";
|
import {TouchableJellyseerrRouter} from "@/components/common/JellyseerrItemRouter";
|
||||||
import { Text } from "@/components/common/Text";
|
import {Text} from "@/components/common/Text";
|
||||||
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
|
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
|
||||||
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
|
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
|
||||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
import {useJellyseerr} from "@/hooks/useJellyseerr";
|
||||||
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
|
import {useJellyseerrCanRequest} from "@/utils/_jellyseerr/useJellyseerrCanRequest";
|
||||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||||
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
import {Image} from "expo-image";
|
||||||
import { Image } from "expo-image";
|
import {useMemo} from "react";
|
||||||
import { useMemo } from "react";
|
import {View, ViewProps} from "react-native";
|
||||||
import { View, ViewProps } from "react-native";
|
import Animated, {useAnimatedStyle, useSharedValue, withTiming,} from "react-native-reanimated";
|
||||||
import Animated, {
|
import {TvDetails} from "@/utils/jellyseerr/server/models/Tv";
|
||||||
useAnimatedStyle,
|
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
|
||||||
useSharedValue,
|
import type {DownloadingItem} from "@/utils/jellyseerr/server/lib/downloadtracker";
|
||||||
withTiming,
|
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
||||||
} from "react-native-reanimated";
|
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 {
|
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 JellyseerrPoster: React.FC<Props> = ({
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
item,
|
||||||
|
horizontal,
|
||||||
|
showDownloadInfo,
|
||||||
|
mediaRequest,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { jellyseerrApi, getTitle, getYear, getMediaType, isJellyseerrResult } = useJellyseerr();
|
||||||
const loadingOpacity = useSharedValue(1);
|
const loadingOpacity = useSharedValue(1);
|
||||||
const imageOpacity = useSharedValue(0);
|
const imageOpacity = useSharedValue(0);
|
||||||
|
const {t} = useTranslation();
|
||||||
|
|
||||||
const loadingAnimatedStyle = useAnimatedStyle(() => ({
|
const loadingAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
opacity: loadingOpacity.value,
|
opacity: loadingOpacity.value,
|
||||||
@@ -38,27 +52,64 @@ const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const imageSrc = useMemo(
|
const imageSrc = useMemo(
|
||||||
() => jellyseerrApi?.imageProxy(item.posterPath, "w300_and_h450_face"),
|
() => jellyseerrApi?.imageProxy(
|
||||||
[item, jellyseerrApi]
|
horizontal ? item.backdropPath : item.posterPath,
|
||||||
|
horizontal ? "w1920_and_h800_multi_faces" : "w300_and_h450_face"
|
||||||
|
),
|
||||||
|
[item, jellyseerrApi, horizontal]
|
||||||
);
|
);
|
||||||
|
|
||||||
const title = useMemo(
|
const title = useMemo(() => getTitle(item), [item]);
|
||||||
() => (item.mediaType === MediaType.MOVIE ? item.title : item.name),
|
const releaseYear = useMemo(() => getYear(item), [item]);
|
||||||
[item]
|
const mediaType = useMemo(() => getMediaType(item), [item]);
|
||||||
);
|
|
||||||
|
|
||||||
const releaseYear = useMemo(
|
const size = useMemo(() => horizontal ? 'h-28' : 'w-28', [horizontal])
|
||||||
() =>
|
const ratio = useMemo(() => horizontal ? '15/10' : '10/15', [horizontal])
|
||||||
new Date(
|
|
||||||
item.mediaType === MediaType.MOVIE
|
|
||||||
? item.releaseDate
|
|
||||||
: item.firstAirDate
|
|
||||||
).getFullYear(),
|
|
||||||
[item]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [canRequest] = useJellyseerrCanRequest(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 (
|
return (
|
||||||
<TouchableJellyseerrRouter
|
<TouchableJellyseerrRouter
|
||||||
result={item}
|
result={item}
|
||||||
@@ -66,9 +117,10 @@ const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
|
|||||||
releaseYear={releaseYear}
|
releaseYear={releaseYear}
|
||||||
canRequest={canRequest}
|
canRequest={canRequest}
|
||||||
posterSrc={imageSrc!!}
|
posterSrc={imageSrc!!}
|
||||||
|
mediaType={mediaType}
|
||||||
>
|
>
|
||||||
<View className="flex flex-col w-28 mr-2">
|
<View className={`flex flex-col mr-2 h-auto`}>
|
||||||
<View className="relative rounded-lg overflow-hidden border border-neutral-900 w-28 aspect-[10/15]">
|
<View className={`relative rounded-lg overflow-hidden border border-neutral-900 ${size} aspect-[${ratio}]`}>
|
||||||
<Animated.View style={imageAnimatedStyle}>
|
<Animated.View style={imageAnimatedStyle}>
|
||||||
<Image
|
<Image
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -77,26 +129,65 @@ const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
|
|||||||
cachePolicy={"memory-disk"}
|
cachePolicy={"memory-disk"}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: "10/15",
|
aspectRatio: ratio,
|
||||||
width: "100%",
|
[horizontal ? 'height' : 'width']: "100%"
|
||||||
}}
|
}}
|
||||||
onLoad={handleImageLoad}
|
onLoad={handleImageLoad}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</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
|
<JellyseerrStatusIcon
|
||||||
className="absolute bottom-1 right-1"
|
className="absolute bottom-1 right-1"
|
||||||
showRequestIcon={canRequest}
|
showRequestIcon={canRequest}
|
||||||
mediaStatus={item?.mediaInfo?.status}
|
mediaStatus={mediaRequest?.media?.status || item?.mediaInfo?.status}
|
||||||
/>
|
/>
|
||||||
<JellyseerrMediaIcon
|
<JellyseerrMediaIcon
|
||||||
className="absolute top-1 left-1"
|
className="absolute top-1 left-1"
|
||||||
mediaType={item?.mediaType}
|
mediaType={mediaType}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View className="mt-2 flex flex-col">
|
</View>
|
||||||
<Text numberOfLines={2}>{title}</Text>
|
<View className={`mt-2 flex flex-col ${horizontal ? 'w-44' : 'w-28'}`}>
|
||||||
<Text className="text-xs opacity-50 align-bottom">{releaseYear}</Text>
|
<Text numberOfLines={2}>{title}</Text>
|
||||||
</View>
|
<Text className="text-xs opacity-50 align-bottom">{releaseYear}</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableJellyseerrRouter>
|
</TouchableJellyseerrRouter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -128,14 +128,12 @@ const RenderItem = ({ item, index }: any) => {
|
|||||||
|
|
||||||
const JellyseerrSeasons: React.FC<{
|
const JellyseerrSeasons: React.FC<{
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
result?: TvResult;
|
|
||||||
details?: TvDetails;
|
details?: TvDetails;
|
||||||
hasAdvancedRequest?: boolean,
|
hasAdvancedRequest?: boolean,
|
||||||
onAdvancedRequest?: (data: MediaRequestBody) => void;
|
onAdvancedRequest?: (data: MediaRequestBody) => void;
|
||||||
refetch: (options?: (RefetchOptions | undefined)) => Promise<QueryObserverResult<TvDetails | MovieDetails | undefined, Error>>;
|
refetch: (options?: (RefetchOptions | undefined)) => Promise<QueryObserverResult<TvDetails | MovieDetails | undefined, Error>>;
|
||||||
}> = ({
|
}> = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
result,
|
|
||||||
details,
|
details,
|
||||||
refetch,
|
refetch,
|
||||||
hasAdvancedRequest,
|
hasAdvancedRequest,
|
||||||
@@ -195,7 +193,7 @@ const JellyseerrSeasons: React.FC<{
|
|||||||
return onAdvancedRequest?.(body)
|
return onAdvancedRequest?.(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestMedia(result?.name!!, body, refetch);
|
requestMedia(details.name, body, refetch);
|
||||||
}
|
}
|
||||||
}, [jellyseerrApi, seasons, details, hasAdvancedRequest, onAdvancedRequest]);
|
}, [jellyseerrApi, seasons, details, hasAdvancedRequest, onAdvancedRequest]);
|
||||||
|
|
||||||
@@ -227,7 +225,7 @@ const JellyseerrSeasons: React.FC<{
|
|||||||
return onAdvancedRequest?.(body)
|
return onAdvancedRequest?.(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestMedia(`${result?.name!!}, Season ${seasonNumber}`, body, refetch);
|
requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch);
|
||||||
}
|
}
|
||||||
}, [requestMedia, hasAdvancedRequest, onAdvancedRequest]);
|
}, [requestMedia, hasAdvancedRequest, onAdvancedRequest]);
|
||||||
|
|
||||||
|
|||||||
22
components/settings/ChromecastSettings.tsx
Normal file
22
components/settings/ChromecastSettings.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Switch, View } from "react-native";
|
||||||
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
|
export const ChromecastSettings: React.FC = ({ ...props }) => {
|
||||||
|
const [settings, updateSettings] = useSettings();
|
||||||
|
return (
|
||||||
|
<View {...props}>
|
||||||
|
<ListGroup title={"Chromecast"}>
|
||||||
|
<ListItem title={"Enable H265 for Chromecast"}>
|
||||||
|
<Switch
|
||||||
|
value={settings.enableH265ForChromecast}
|
||||||
|
onValueChange={(enableH265ForChromecast) =>
|
||||||
|
updateSettings({ enableH265ForChromecast })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</ListGroup>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
30
components/settings/Dashboard.tsx
Normal file
30
components/settings/Dashboard.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import React from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSessions, useSessionsProps } from "@/hooks/useSessions";
|
||||||
|
|
||||||
|
export const Dashboard = () => {
|
||||||
|
const [settings, updateSettings] = useSettings();
|
||||||
|
const { sessions = [], isLoading } = useSessions({} as useSessionsProps);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!settings) return null;
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ListGroup title={t("home.settings.dashboard.title")} className="mt-4">
|
||||||
|
<ListItem
|
||||||
|
className={sessions.length != 0 ? "bg-purple-900" : ""}
|
||||||
|
onPress={() => router.push("/settings/dashboard/sessions")}
|
||||||
|
title={t("home.settings.dashboard.sessions_title")}
|
||||||
|
showArrow
|
||||||
|
/>
|
||||||
|
</ListGroup>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybac
|
|||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { eventBus } from "@/utils/eventBus";
|
||||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||||
import { Api } from "@jellyfin/sdk";
|
import { Api } from "@jellyfin/sdk";
|
||||||
import {
|
import {
|
||||||
@@ -24,9 +25,14 @@ import {
|
|||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import NetInfo from "@react-native-community/netinfo";
|
import NetInfo from "@react-native-community/netinfo";
|
||||||
import { QueryFunction, useQuery } from "@tanstack/react-query";
|
import { QueryFunction, useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigation, useRouter } from "expo-router";
|
import {
|
||||||
|
useNavigation,
|
||||||
|
usePathname,
|
||||||
|
useRouter,
|
||||||
|
useSegments,
|
||||||
|
} from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -53,7 +59,7 @@ type MediaListSection = {
|
|||||||
|
|
||||||
type Section = ScrollingCollectionListSection | MediaListSection;
|
type Section = ScrollingCollectionListSection | MediaListSection;
|
||||||
|
|
||||||
export const SettingsIndex = () => {
|
export const HomeIndex = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -77,6 +83,8 @@ export const SettingsIndex = () => {
|
|||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
|
||||||
const { downloadedFiles, cleanCacheDirectory } = useDownload();
|
const { downloadedFiles, cleanCacheDirectory } = useDownload();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hasDownloads = downloadedFiles && downloadedFiles.length > 0;
|
const hasDownloads = downloadedFiles && downloadedFiles.length > 0;
|
||||||
@@ -104,6 +112,18 @@ export const SettingsIndex = () => {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const segments = useSegments();
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = eventBus.on("scrollToTop", () => {
|
||||||
|
if (segments[2] === "(home)")
|
||||||
|
scrollViewRef.current?.scrollTo({ y: -152, animated: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [segments]);
|
||||||
|
|
||||||
const checkConnection = useCallback(async () => {
|
const checkConnection = useCallback(async () => {
|
||||||
setLoadingRetry(true);
|
setLoadingRetry(true);
|
||||||
const state = await NetInfo.fetch();
|
const state = await NetInfo.fetch();
|
||||||
@@ -415,6 +435,8 @@ export const SettingsIndex = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
scrollToOverflowEnabled={true}
|
||||||
|
ref={scrollViewRef}
|
||||||
nestedScrollEnabled
|
nestedScrollEnabled
|
||||||
contentInsetAdjustmentBehavior="automatic"
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
refreshControl={
|
refreshControl={
|
||||||
@@ -50,7 +50,7 @@ type MediaListSection = {
|
|||||||
|
|
||||||
type Section = ScrollingCollectionListSection | MediaListSection;
|
type Section = ScrollingCollectionListSection | MediaListSection;
|
||||||
|
|
||||||
export const SettingsIndex = () => {
|
export const HomeIndex = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Platform } from "react-native";
|
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 { BitrateSelector, BITRATES } from "@/components/BitrateSelector";
|
||||||
import {
|
import {
|
||||||
BACKGROUND_FETCH_TASK,
|
BACKGROUND_FETCH_TASK,
|
||||||
@@ -7,9 +7,7 @@ import {
|
|||||||
unregisterBackgroundFetchAsync,
|
unregisterBackgroundFetchAsync,
|
||||||
} from "@/utils/background-tasks";
|
} from "@/utils/background-tasks";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
const BackgroundFetch = !Platform.isTV
|
const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null;
|
||||||
? require("expo-background-fetch")
|
|
||||||
: null;
|
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
|
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
@@ -22,6 +20,7 @@ import { ListItem } from "../list/ListItem";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import Dropdown from "@/components/common/Dropdown";
|
import Dropdown from "@/components/common/Dropdown";
|
||||||
|
import { isNumber } from "lodash";
|
||||||
|
|
||||||
export const OtherSettings: React.FC = () => {
|
export const OtherSettings: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -84,10 +83,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<DisabledSetting disabled={disabled}>
|
<DisabledSetting disabled={disabled}>
|
||||||
<ListGroup title={t("home.settings.other.other_title")} className="">
|
<ListGroup title={t("home.settings.other.other_title")} className="">
|
||||||
<ListItem
|
<ListItem title={t("home.settings.other.auto_rotate")} disabled={pluginSettings?.autoRotate?.locked}>
|
||||||
title={t("home.settings.other.auto_rotate")}
|
|
||||||
disabled={pluginSettings?.autoRotate?.locked}
|
|
||||||
>
|
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.autoRotate}
|
value={settings.autoRotate}
|
||||||
disabled={pluginSettings?.autoRotate?.locked}
|
disabled={pluginSettings?.autoRotate?.locked}
|
||||||
@@ -97,17 +93,11 @@ export const OtherSettings: React.FC = () => {
|
|||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.video_orientation")}
|
title={t("home.settings.other.video_orientation")}
|
||||||
disabled={
|
disabled={pluginSettings?.defaultVideoOrientation?.locked || settings.autoRotate}
|
||||||
pluginSettings?.defaultVideoOrientation?.locked ||
|
|
||||||
settings.autoRotate
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
data={orientations}
|
data={orientations}
|
||||||
disabled={
|
disabled={pluginSettings?.defaultVideoOrientation?.locked || settings.autoRotate}
|
||||||
pluginSettings?.defaultVideoOrientation?.locked ||
|
|
||||||
settings.autoRotate
|
|
||||||
}
|
|
||||||
keyExtractor={String}
|
keyExtractor={String}
|
||||||
titleExtractor={(item) => ScreenOrientationEnum[item]}
|
titleExtractor={(item) => ScreenOrientationEnum[item]}
|
||||||
title={
|
title={
|
||||||
@@ -115,17 +105,11 @@ export const OtherSettings: React.FC = () => {
|
|||||||
<Text className="mr-1 text-[#8E8D91]">
|
<Text className="mr-1 text-[#8E8D91]">
|
||||||
{t(ScreenOrientationEnum[settings.defaultVideoOrientation])}
|
{t(ScreenOrientationEnum[settings.defaultVideoOrientation])}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons
|
<Ionicons name="chevron-expand-sharp" size={18} color="#5A5960" />
|
||||||
name="chevron-expand-sharp"
|
|
||||||
size={18}
|
|
||||||
color="#5A5960"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
}
|
}
|
||||||
label={t("home.settings.other.orientation")}
|
label={t("home.settings.other.orientation")}
|
||||||
onSelected={(defaultVideoOrientation) =>
|
onSelected={(defaultVideoOrientation) => updateSettings({ defaultVideoOrientation })}
|
||||||
updateSettings({ defaultVideoOrientation })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
@@ -136,27 +120,49 @@ export const OtherSettings: React.FC = () => {
|
|||||||
<Switch
|
<Switch
|
||||||
value={settings.safeAreaInControlsEnabled}
|
value={settings.safeAreaInControlsEnabled}
|
||||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => updateSettings({ safeAreaInControlsEnabled: value })}
|
||||||
updateSettings({ safeAreaInControlsEnabled: value })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</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
|
<ListItem
|
||||||
title={t("home.settings.other.show_custom_menu_links")}
|
title={t("home.settings.other.show_custom_menu_links")}
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
onPress={() =>
|
onPress={() => Linking.openURL("https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links")}
|
||||||
Linking.openURL(
|
|
||||||
"https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.showCustomMenuLinks}
|
value={settings.showCustomMenuLinks}
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => updateSettings({ showCustomMenuLinks: value })}
|
||||||
updateSettings({ showCustomMenuLinks: value })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
@@ -164,10 +170,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
title={t("home.settings.other.hide_libraries")}
|
title={t("home.settings.other.hide_libraries")}
|
||||||
showArrow
|
showArrow
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem title={t("home.settings.other.default_quality")} disabled={pluginSettings?.defaultBitrate?.locked}>
|
||||||
title={t("home.settings.other.default_quality")}
|
|
||||||
disabled={pluginSettings?.defaultBitrate?.locked}
|
|
||||||
>
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
data={BITRATES}
|
data={BITRATES}
|
||||||
disabled={pluginSettings?.defaultBitrate?.locked}
|
disabled={pluginSettings?.defaultBitrate?.locked}
|
||||||
@@ -176,14 +179,8 @@ export const OtherSettings: React.FC = () => {
|
|||||||
selected={settings.defaultBitrate}
|
selected={settings.defaultBitrate}
|
||||||
title={
|
title={
|
||||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||||
<Text className="mr-1 text-[#8E8D91]">
|
<Text className="mr-1 text-[#8E8D91]">{settings.defaultBitrate?.key}</Text>
|
||||||
{settings.defaultBitrate?.key}
|
<Ionicons name="chevron-expand-sharp" size={18} color="#5A5960" />
|
||||||
</Text>
|
|
||||||
<Ionicons
|
|
||||||
name="chevron-expand-sharp"
|
|
||||||
size={18}
|
|
||||||
color="#5A5960"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
}
|
}
|
||||||
label={t("home.settings.other.default_quality")}
|
label={t("home.settings.other.default_quality")}
|
||||||
@@ -197,9 +194,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
<Switch
|
<Switch
|
||||||
value={settings.disableHapticFeedback}
|
value={settings.disableHapticFeedback}
|
||||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
onValueChange={(disableHapticFeedback) =>
|
onValueChange={(disableHapticFeedback) => updateSettings({ disableHapticFeedback })}
|
||||||
updateSettings({ disableHapticFeedback })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { toast } from "sonner-native";
|
|||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {Colors} from "@/constants/Colors";
|
||||||
|
|
||||||
export const StorageSettings = () => {
|
export const StorageSettings = () => {
|
||||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||||
@@ -61,7 +62,7 @@ export const StorageSettings = () => {
|
|||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
width: `${(size.app / size.total) * 100}%`,
|
width: `${(size.app / size.total) * 100}%`,
|
||||||
backgroundColor: "rgb(147 51 234)",
|
backgroundColor: Colors.primaryRGB,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
@@ -70,7 +71,7 @@ export const StorageSettings = () => {
|
|||||||
((size.total - size.remaining - size.app) / size.total) *
|
((size.total - size.remaining - size.app) / size.total) *
|
||||||
100
|
100
|
||||||
}%`,
|
}%`,
|
||||||
backgroundColor: "rgb(192 132 252)",
|
backgroundColor: Colors.primaryLightRGB,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,65 +1,40 @@
|
|||||||
import { Text } from "@/components/common/Text";
|
import {Text} from "@/components/common/Text";
|
||||||
import { Loader } from "@/components/Loader";
|
import {Loader} from "@/components/Loader";
|
||||||
import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes";
|
import {useAdjacentItems} from "@/hooks/useAdjacentEpisodes";
|
||||||
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
import {useCreditSkipper} from "@/hooks/useCreditSkipper";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import {useHaptic} from "@/hooks/useHaptic";
|
||||||
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
|
import {useIntroSkipper} from "@/hooks/useIntroSkipper";
|
||||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
import {useTrickplay} from "@/hooks/useTrickplay";
|
||||||
import {
|
import {TrackInfo, VlcPlayerViewRef,} from "@/modules/VlcPlayer.types";
|
||||||
TrackInfo,
|
import {apiAtom} from "@/providers/JellyfinProvider";
|
||||||
VlcPlayerViewRef,
|
import {useSettings, VideoPlayer} from "@/utils/atoms/settings";
|
||||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
import {getDefaultPlaySettings,} from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import {getItemById} from "@/utils/jellyfin/user-library/getItemById";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import {writeToLog} from "@/utils/log";
|
||||||
import {
|
import {formatTimeString, msToTicks, secondsToMs, ticksToMs, ticksToSeconds,} from "@/utils/time";
|
||||||
getDefaultPlaySettings,
|
import {Ionicons, MaterialIcons} from "@expo/vector-icons";
|
||||||
previousIndexes,
|
import {BaseItemDto, MediaSourceInfo,} from "@jellyfin/sdk/lib/generated-client";
|
||||||
} from "@/utils/jellyfin/getDefaultPlaySettings";
|
import {Image} from "expo-image";
|
||||||
import { getItemById } from "@/utils/jellyfin/user-library/getItemById";
|
import {useLocalSearchParams, useRouter} from "expo-router";
|
||||||
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 * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
import { useAtom } from "jotai";
|
import {useAtom} from "jotai";
|
||||||
import { debounce } from "lodash";
|
import {debounce} from "lodash";
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, {useCallback, useEffect, useRef, useState} from "react";
|
||||||
import {
|
import {Platform, TouchableOpacity, useWindowDimensions, View,} from "react-native";
|
||||||
Platform,
|
import {Slider} from "react-native-awesome-slider";
|
||||||
TouchableOpacity,
|
import {runOnJS, SharedValue, useAnimatedReaction, useSharedValue,} from "react-native-reanimated";
|
||||||
useWindowDimensions,
|
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
||||||
View,
|
import {VideoRef} from "react-native-video";
|
||||||
} 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 AudioSlider from "./AudioSlider";
|
||||||
import BrightnessSlider from "./BrightnessSlider";
|
import BrightnessSlider from "./BrightnessSlider";
|
||||||
import { ControlProvider } from "./contexts/ControlContext";
|
import {ControlProvider} from "./contexts/ControlContext";
|
||||||
import { VideoProvider } from "./contexts/VideoContext";
|
import {VideoProvider} from "./contexts/VideoContext";
|
||||||
import DropdownView from "./dropdown/DropdownView";
|
import DropdownView from "./dropdown/DropdownView";
|
||||||
import { EpisodeList } from "./EpisodeList";
|
import {EpisodeList} from "./EpisodeList";
|
||||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||||
import SkipButton from "./SkipButton";
|
import SkipButton from "./SkipButton";
|
||||||
import { useControlsTimeout } from "./useControlsTimeout";
|
import {useControlsTimeout} from "./useControlsTimeout";
|
||||||
import { VideoTouchOverlay } from "./VideoTouchOverlay";
|
import {VideoTouchOverlay} from "./VideoTouchOverlay";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -87,40 +62,38 @@ interface Props {
|
|||||||
setSubtitleURL?: (url: string, customName: string) => void;
|
setSubtitleURL?: (url: string, customName: string) => void;
|
||||||
setSubtitleTrack?: (index: number) => void;
|
setSubtitleTrack?: (index: number) => void;
|
||||||
setAudioTrack?: (index: number) => void;
|
setAudioTrack?: (index: number) => void;
|
||||||
stop: (() => Promise<void>) | (() => void);
|
|
||||||
isVlc?: boolean;
|
isVlc?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTROLS_TIMEOUT = 4000;
|
const CONTROLS_TIMEOUT = 4000;
|
||||||
|
|
||||||
export const Controls: React.FC<Props> = ({
|
export const Controls: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
seek,
|
seek,
|
||||||
startPictureInPicture,
|
startPictureInPicture,
|
||||||
play,
|
play,
|
||||||
pause,
|
pause,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
isSeeking,
|
isSeeking,
|
||||||
progress,
|
progress,
|
||||||
isBuffering,
|
isBuffering,
|
||||||
cacheProgress,
|
cacheProgress,
|
||||||
showControls,
|
showControls,
|
||||||
setShowControls,
|
setShowControls,
|
||||||
ignoreSafeAreas,
|
ignoreSafeAreas,
|
||||||
setIgnoreSafeAreas,
|
setIgnoreSafeAreas,
|
||||||
mediaSource,
|
mediaSource,
|
||||||
isVideoLoaded,
|
isVideoLoaded,
|
||||||
getAudioTracks,
|
getAudioTracks,
|
||||||
getSubtitleTracks,
|
getSubtitleTracks,
|
||||||
setSubtitleURL,
|
setSubtitleURL,
|
||||||
setSubtitleTrack,
|
setSubtitleTrack,
|
||||||
setAudioTrack,
|
setAudioTrack,
|
||||||
stop,
|
offline = false,
|
||||||
offline = false,
|
enableTrickplay = true,
|
||||||
enableTrickplay = true,
|
isVlc = false,
|
||||||
isVlc = false,
|
}) => {
|
||||||
}) => {
|
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -189,75 +162,60 @@ export const Controls: React.FC<Props> = ({
|
|||||||
isVlc
|
isVlc
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const goToItemCommon = useCallback(
|
||||||
|
(item: BaseItemDto) => {
|
||||||
|
if (!item || !settings) return;
|
||||||
|
|
||||||
|
lightHapticFeedback();
|
||||||
|
|
||||||
|
const previousIndexes = {
|
||||||
|
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||||
|
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
mediaSource: newMediaSource,
|
||||||
|
audioIndex: defaultAudioIndex,
|
||||||
|
subtitleIndex: defaultSubtitleIndex,
|
||||||
|
} = getDefaultPlaySettings(
|
||||||
|
item,
|
||||||
|
settings,
|
||||||
|
previousIndexes,
|
||||||
|
mediaSource ?? undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
itemId: item.Id ?? "",
|
||||||
|
audioIndex: defaultAudioIndex?.toString() ?? "",
|
||||||
|
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
||||||
|
mediaSourceId: newMediaSource?.Id ?? "",
|
||||||
|
bitrateValue: bitrateValue.toString(),
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
|
},
|
||||||
|
[settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router]
|
||||||
|
);
|
||||||
|
|
||||||
const goToPreviousItem = useCallback(() => {
|
const goToPreviousItem = useCallback(() => {
|
||||||
if (!previousItem || !settings) return;
|
if (!previousItem) return;
|
||||||
|
goToItemCommon(previousItem);
|
||||||
lightHapticFeedback();
|
}, [previousItem, goToItemCommon]);
|
||||||
|
|
||||||
const previousIndexes: previousIndexes = {
|
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
|
||||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
mediaSource: newMediaSource,
|
|
||||||
audioIndex: defaultAudioIndex,
|
|
||||||
subtitleIndex: defaultSubtitleIndex,
|
|
||||||
} = getDefaultPlaySettings(
|
|
||||||
previousItem,
|
|
||||||
settings,
|
|
||||||
previousIndexes,
|
|
||||||
mediaSource ?? undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
itemId: previousItem.Id ?? "", // Ensure itemId is a string
|
|
||||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
|
||||||
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
|
||||||
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
|
||||||
bitrateValue: bitrateValue.toString(),
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
stop();
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
|
||||||
}, [previousItem, settings, subtitleIndex, audioIndex]);
|
|
||||||
|
|
||||||
const goToNextItem = useCallback(() => {
|
const goToNextItem = useCallback(() => {
|
||||||
if (!nextItem || !settings) return;
|
if (!nextItem) return;
|
||||||
|
goToItemCommon(nextItem);
|
||||||
|
}, [nextItem, goToItemCommon]);
|
||||||
|
|
||||||
lightHapticFeedback();
|
const goToItem = useCallback(
|
||||||
|
async (itemId: string) => {
|
||||||
const previousIndexes: previousIndexes = {
|
const gotoItem = await getItemById(api, itemId);
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
if (!gotoItem) return;
|
||||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
goToItemCommon(gotoItem);
|
||||||
};
|
},
|
||||||
|
[goToItemCommon, api]
|
||||||
const {
|
);
|
||||||
mediaSource: newMediaSource,
|
|
||||||
audioIndex: defaultAudioIndex,
|
|
||||||
subtitleIndex: defaultSubtitleIndex,
|
|
||||||
} = getDefaultPlaySettings(
|
|
||||||
nextItem,
|
|
||||||
settings,
|
|
||||||
previousIndexes,
|
|
||||||
mediaSource ?? undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
itemId: nextItem.Id ?? "", // Ensure itemId is a string
|
|
||||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
|
||||||
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
|
||||||
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
|
||||||
bitrateValue: bitrateValue.toString(),
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
stop();
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
|
||||||
}, [nextItem, settings, subtitleIndex, audioIndex]);
|
|
||||||
|
|
||||||
const updateTimes = useCallback(
|
const updateTimes = useCallback(
|
||||||
(currentProgress: number, maxValue: number) => {
|
(currentProgress: number, maxValue: number) => {
|
||||||
@@ -381,49 +339,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [settings, isPlaying, isVlc]);
|
}, [settings, isPlaying, isVlc]);
|
||||||
|
|
||||||
const goToItem = useCallback(
|
|
||||||
async (itemId: string) => {
|
|
||||||
try {
|
|
||||||
const gotoItem = await getItemById(api, itemId);
|
|
||||||
if (!settings || !gotoItem) return;
|
|
||||||
|
|
||||||
lightHapticFeedback();
|
|
||||||
|
|
||||||
const previousIndexes: previousIndexes = {
|
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
|
||||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
mediaSource: newMediaSource,
|
|
||||||
audioIndex: defaultAudioIndex,
|
|
||||||
subtitleIndex: defaultSubtitleIndex,
|
|
||||||
} = getDefaultPlaySettings(
|
|
||||||
gotoItem,
|
|
||||||
settings,
|
|
||||||
previousIndexes,
|
|
||||||
mediaSource ?? undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
itemId: gotoItem.Id ?? "", // Ensure itemId is a string
|
|
||||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
|
||||||
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
|
||||||
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
|
||||||
bitrateValue: bitrateValue.toString(),
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
stop();
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in gotoEpisode:", error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[settings, subtitleIndex, audioIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleIgnoreSafeAreas = useCallback(() => {
|
const toggleIgnoreSafeAreas = useCallback(() => {
|
||||||
setIgnoreSafeAreas((prev) => !prev);
|
setIgnoreSafeAreas((prev) => !prev);
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
@@ -497,7 +412,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
}, [trickPlayUrl, trickplayInfo, time]);
|
}, [trickPlayUrl, trickplayInfo, time]);
|
||||||
|
|
||||||
const onClose = async () => {
|
const onClose = async () => {
|
||||||
stop();
|
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
await ScreenOrientation.lockAsync(
|
await ScreenOrientation.lockAsync(
|
||||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||||
@@ -540,20 +454,22 @@ export const Controls: React.FC<Props> = ({
|
|||||||
pointerEvents={showControls ? "auto" : "none"}
|
pointerEvents={showControls ? "auto" : "none"}
|
||||||
className={`flex flex-row w-full pt-2`}
|
className={`flex flex-row w-full pt-2`}
|
||||||
>
|
>
|
||||||
<View className="mr-auto">
|
{!Platform.isTV && (
|
||||||
<VideoProvider
|
<View className="mr-auto">
|
||||||
getAudioTracks={getAudioTracks}
|
<VideoProvider
|
||||||
getSubtitleTracks={getSubtitleTracks}
|
getAudioTracks={getAudioTracks}
|
||||||
setAudioTrack={setAudioTrack}
|
getSubtitleTracks={getSubtitleTracks}
|
||||||
setSubtitleTrack={setSubtitleTrack}
|
setAudioTrack={setAudioTrack}
|
||||||
setSubtitleURL={setSubtitleURL}
|
setSubtitleTrack={setSubtitleTrack}
|
||||||
>
|
setSubtitleURL={setSubtitleURL}
|
||||||
<DropdownView showControls={showControls} />
|
>
|
||||||
</VideoProvider>
|
<DropdownView />
|
||||||
</View>
|
</VideoProvider>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<View className="flex flex-row items-center space-x-2 ">
|
<View className="flex flex-row items-center space-x-2 ">
|
||||||
{!Platform.isTV && (
|
{!Platform.isTV && settings.defaultPlayer == VideoPlayer.VLC_4 && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={startPictureInPicture}
|
onPress={startPictureInPicture}
|
||||||
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||||
@@ -788,8 +704,8 @@ export const Controls: React.FC<Props> = ({
|
|||||||
!nextItem
|
!nextItem
|
||||||
? false
|
? false
|
||||||
: isVlc
|
: isVlc
|
||||||
? remainingTime < 10000
|
? remainingTime < 10000
|
||||||
: remainingTime < 10
|
: remainingTime < 10
|
||||||
}
|
}
|
||||||
onFinish={goToNextItem}
|
onFinish={goToNextItem}
|
||||||
onPress={goToNextItem}
|
onPress={goToNextItem}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TrackInfo } from "@/modules/vlc-player";
|
|
||||||
import {
|
import {
|
||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
import { TrackInfo } from "@/modules/vlc-player";
|
import { TrackInfo } from "@/modules/VlcPlayer.types";
|
||||||
import {
|
import React, { createContext, useContext, useState, ReactNode, useEffect, useMemo } from "react";
|
||||||
BaseItemDto,
|
|
||||||
MediaSourceInfo,
|
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
|
||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useState,
|
|
||||||
ReactNode,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
} from "react";
|
|
||||||
import { useControlContext } from "./ControlContext";
|
import { useControlContext } from "./ControlContext";
|
||||||
import { Track } from "../types";
|
import { Track } from "../types";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
@@ -27,14 +16,8 @@ const VideoContext = createContext<VideoContextProps | undefined>(undefined);
|
|||||||
|
|
||||||
interface VideoProviderProps {
|
interface VideoProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
getAudioTracks:
|
getAudioTracks: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]) | undefined;
|
||||||
| (() => Promise<TrackInfo[] | null>)
|
getSubtitleTracks: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]) | undefined;
|
||||||
| (() => TrackInfo[])
|
|
||||||
| undefined;
|
|
||||||
getSubtitleTracks:
|
|
||||||
| (() => Promise<TrackInfo[] | null>)
|
|
||||||
| (() => TrackInfo[])
|
|
||||||
| undefined;
|
|
||||||
setAudioTrack: ((index: number) => void) | undefined;
|
setAudioTrack: ((index: number) => void) | undefined;
|
||||||
setSubtitleTrack: ((index: number) => void) | undefined;
|
setSubtitleTrack: ((index: number) => void) | undefined;
|
||||||
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
|
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
|
||||||
@@ -55,23 +38,19 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
||||||
const mediaSource = ControlContext?.mediaSource;
|
const mediaSource = ControlContext?.mediaSource;
|
||||||
|
|
||||||
const allSubs =
|
const allSubs = mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || [];
|
||||||
mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || [];
|
|
||||||
|
|
||||||
const { itemId, audioIndex, bitrateValue, subtitleIndex } =
|
const { itemId, audioIndex, bitrateValue, subtitleIndex } = useLocalSearchParams<{
|
||||||
useLocalSearchParams<{
|
itemId: string;
|
||||||
itemId: string;
|
audioIndex: string;
|
||||||
audioIndex: string;
|
subtitleIndex: string;
|
||||||
subtitleIndex: string;
|
mediaSourceId: string;
|
||||||
mediaSourceId: string;
|
bitrateValue: string;
|
||||||
bitrateValue: string;
|
}>();
|
||||||
}>();
|
|
||||||
|
|
||||||
const onTextBasedSubtitle = useMemo(
|
const onTextBasedSubtitle = useMemo(
|
||||||
() =>
|
() =>
|
||||||
allSubs.find(
|
allSubs.find((s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream) || subtitleIndex === "-1",
|
||||||
(s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream
|
|
||||||
) || subtitleIndex === "-1",
|
|
||||||
[allSubs, subtitleIndex]
|
[allSubs, subtitleIndex]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,21 +74,14 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
router.replace(`player/direct-player?${queryParams}`);
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTrackParams = (
|
const setTrackParams = (type: "audio" | "subtitle", index: number, serverIndex: number) => {
|
||||||
type: "audio" | "subtitle",
|
|
||||||
index: number,
|
|
||||||
serverIndex: number
|
|
||||||
) => {
|
|
||||||
const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack;
|
const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack;
|
||||||
const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex";
|
const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex";
|
||||||
|
|
||||||
// If we're transcoding and we're going from a image based subtitle
|
// If we're transcoding and we're going from a image based subtitle
|
||||||
// to a text based subtitle, we need to change the player params.
|
// to a text based subtitle, we need to change the player params.
|
||||||
|
|
||||||
const shouldChangePlayerParams =
|
const shouldChangePlayerParams = type === "subtitle" && mediaSource?.TranscodingUrl && !onTextBasedSubtitle;
|
||||||
type === "subtitle" &&
|
|
||||||
mediaSource?.TranscodingUrl &&
|
|
||||||
!onTextBasedSubtitle;
|
|
||||||
|
|
||||||
console.log("Set player params", index, serverIndex);
|
console.log("Set player params", index, serverIndex);
|
||||||
if (shouldChangePlayerParams) {
|
if (shouldChangePlayerParams) {
|
||||||
@@ -129,23 +101,22 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
if (getSubtitleTracks) {
|
if (getSubtitleTracks) {
|
||||||
const subtitleData = await getSubtitleTracks();
|
const subtitleData = await getSubtitleTracks();
|
||||||
|
|
||||||
|
// Step 1: Move external subs to the end, because VLC puts external subs at the end
|
||||||
|
const sortedSubs = allSubs.sort((a, b) => Number(a.IsExternal) - Number(b.IsExternal));
|
||||||
|
|
||||||
|
// Step 2: Apply VLC indexing logic
|
||||||
let textSubIndex = 0;
|
let textSubIndex = 0;
|
||||||
const subtitles: Track[] = allSubs?.map((sub) => {
|
const processedSubs: Track[] = sortedSubs?.map((sub) => {
|
||||||
// Always increment for non-transcoding subtitles
|
// Always increment for non-transcoding subtitles
|
||||||
// Only increment for text-based subtitles when transcoding
|
// Only increment for text-based subtitles when transcoding
|
||||||
const shouldIncrement =
|
const shouldIncrement = !mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream;
|
||||||
!mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream;
|
|
||||||
|
|
||||||
const displayTitle = sub.DisplayTitle || "Undefined Subtitle";
|
|
||||||
const vlcIndex = subtitleData?.at(textSubIndex)?.index ?? -1;
|
const vlcIndex = subtitleData?.at(textSubIndex)?.index ?? -1;
|
||||||
|
|
||||||
const finalIndex = shouldIncrement ? vlcIndex : sub.Index ?? -1;
|
const finalIndex = shouldIncrement ? vlcIndex : sub.Index ?? -1;
|
||||||
|
|
||||||
if (shouldIncrement) textSubIndex++;
|
if (shouldIncrement) textSubIndex++;
|
||||||
return {
|
return {
|
||||||
name: displayTitle,
|
name: sub.DisplayTitle || "Undefined Subtitle",
|
||||||
index: sub.Index ?? -1,
|
index: sub.Index ?? -1,
|
||||||
originalIndex: finalIndex,
|
|
||||||
setTrack: () =>
|
setTrack: () =>
|
||||||
shouldIncrement
|
shouldIncrement
|
||||||
? setTrackParams("subtitle", finalIndex, sub.Index ?? -1)
|
? setTrackParams("subtitle", finalIndex, sub.Index ?? -1)
|
||||||
@@ -155,6 +126,9 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Step 3: Restore the original order
|
||||||
|
const subtitles: Track[] = processedSubs.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
// Add a "Disable Subtitles" option
|
// Add a "Disable Subtitles" option
|
||||||
subtitles.unshift({
|
subtitles.unshift({
|
||||||
name: "Disable",
|
name: "Disable",
|
||||||
@@ -164,36 +138,25 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
? setTrackParams("subtitle", -1, -1)
|
? setTrackParams("subtitle", -1, -1)
|
||||||
: setPlayerParams({ chosenSubtitleIndex: "-1" }),
|
: setPlayerParams({ chosenSubtitleIndex: "-1" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
setSubtitleTracks(subtitles);
|
setSubtitleTracks(subtitles);
|
||||||
}
|
}
|
||||||
if (
|
if (getAudioTracks) {
|
||||||
getAudioTracks &&
|
|
||||||
(audioTracks === null || audioTracks.length === 0)
|
|
||||||
) {
|
|
||||||
const audioData = await getAudioTracks();
|
const audioData = await getAudioTracks();
|
||||||
if (!audioData) return;
|
|
||||||
|
|
||||||
console.log("audioData", audioData);
|
|
||||||
|
|
||||||
const allAudio =
|
|
||||||
mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || [];
|
|
||||||
|
|
||||||
|
const allAudio = mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || [];
|
||||||
const audioTracks: Track[] = allAudio?.map((audio, idx) => {
|
const audioTracks: Track[] = allAudio?.map((audio, idx) => {
|
||||||
if (!mediaSource?.TranscodingUrl) {
|
if (!mediaSource?.TranscodingUrl) {
|
||||||
const vlcIndex = audioData?.at(idx)?.index ?? -1;
|
const vlcIndex = audioData?.at(idx)?.index ?? -1;
|
||||||
return {
|
return {
|
||||||
name: audio.DisplayTitle ?? "Undefined Audio",
|
name: audio.DisplayTitle ?? "Undefined Audio",
|
||||||
index: audio.Index ?? -1,
|
index: audio.Index ?? -1,
|
||||||
setTrack: () =>
|
setTrack: () => setTrackParams("audio", vlcIndex, audio.Index ?? -1),
|
||||||
setTrackParams("audio", vlcIndex, audio.Index ?? -1),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: audio.DisplayTitle ?? "Undefined Audio",
|
name: audio.DisplayTitle ?? "Undefined Audio",
|
||||||
index: audio.Index ?? -1,
|
index: audio.Index ?? -1,
|
||||||
setTrack: () =>
|
setTrack: () => setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }),
|
||||||
setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setAudioTracks(audioTracks);
|
setAudioTracks(audioTracks);
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
import { TouchableOpacity, Platform } from "react-native";
|
import { TouchableOpacity, Platform } from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||||
import { useVideoContext } from "../contexts/VideoContext";
|
import { useVideoContext } from "../contexts/VideoContext";
|
||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||||
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
|
import { useControlContext } from "../contexts/ControlContext";
|
||||||
|
|
||||||
interface DropdownViewProps {
|
const DropdownView = () => {
|
||||||
showControls: boolean;
|
|
||||||
offline?: boolean; // used to disable external subs for downloads
|
|
||||||
}
|
|
||||||
|
|
||||||
const DropdownView: React.FC<DropdownViewProps> = ({
|
|
||||||
showControls,
|
|
||||||
offline = false,
|
|
||||||
}) => {
|
|
||||||
const videoContext = useVideoContext();
|
const videoContext = useVideoContext();
|
||||||
const { subtitleTracks, audioTracks } = videoContext;
|
const { subtitleTracks, audioTracks } = videoContext;
|
||||||
|
const ControlContext = useControlContext();
|
||||||
|
const [item, mediaSource] = [ControlContext?.item, ControlContext?.mediaSource];
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { subtitleIndex, audioIndex } = useLocalSearchParams<{
|
const { subtitleIndex, audioIndex, bitrateValue } = useLocalSearchParams<{
|
||||||
itemId: string;
|
itemId: string;
|
||||||
audioIndex: string;
|
audioIndex: string;
|
||||||
subtitleIndex: string;
|
subtitleIndex: string;
|
||||||
@@ -25,6 +22,21 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
bitrateValue: string;
|
bitrateValue: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const changeBitrate = useCallback(
|
||||||
|
(bitrate: string) => {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
itemId: item.Id ?? "",
|
||||||
|
audioIndex: audioIndex?.toString() ?? "",
|
||||||
|
subtitleIndex: subtitleIndex.toString() ?? "",
|
||||||
|
mediaSourceId: mediaSource?.Id ?? "",
|
||||||
|
bitrateValue: bitrate.toString(),
|
||||||
|
}).toString();
|
||||||
|
// @ts-expect-error
|
||||||
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
|
},
|
||||||
|
[item, mediaSource, subtitleIndex, audioIndex]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
@@ -42,9 +54,27 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger key="subtitle-trigger">
|
<DropdownMenu.SubTrigger key="qualitytrigger">Quality</DropdownMenu.SubTrigger>
|
||||||
Subtitle
|
<DropdownMenu.SubContent
|
||||||
</DropdownMenu.SubTrigger>
|
alignOffset={-10}
|
||||||
|
avoidCollisions={true}
|
||||||
|
collisionPadding={0}
|
||||||
|
loop={true}
|
||||||
|
sideOffset={10}
|
||||||
|
>
|
||||||
|
{BITRATES?.map((bitrate, idx: number) => (
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
key={`quality-item-${idx}`}
|
||||||
|
value={bitrateValue === (bitrate.value?.toString() ?? "")}
|
||||||
|
onValueChange={() => changeBitrate(bitrate.value?.toString() ?? "")}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>{bitrate.key}</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.SubTrigger key="subtitle-trigger">Subtitle</DropdownMenu.SubTrigger>
|
||||||
<DropdownMenu.SubContent
|
<DropdownMenu.SubContent
|
||||||
alignOffset={-10}
|
alignOffset={-10}
|
||||||
avoidCollisions={true}
|
avoidCollisions={true}
|
||||||
@@ -58,17 +88,13 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
value={subtitleIndex === sub.index.toString()}
|
value={subtitleIndex === sub.index.toString()}
|
||||||
onValueChange={() => sub.setTrack()}
|
onValueChange={() => sub.setTrack()}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>
|
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>{sub.name}</DropdownMenu.ItemTitle>
|
||||||
{sub.name}
|
|
||||||
</DropdownMenu.ItemTitle>
|
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
</DropdownMenu.Sub>
|
</DropdownMenu.Sub>
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger key="audio-trigger">
|
<DropdownMenu.SubTrigger key="audio-trigger">Audio</DropdownMenu.SubTrigger>
|
||||||
Audio
|
|
||||||
</DropdownMenu.SubTrigger>
|
|
||||||
<DropdownMenu.SubContent
|
<DropdownMenu.SubContent
|
||||||
alignOffset={-10}
|
alignOffset={-10}
|
||||||
avoidCollisions={true}
|
avoidCollisions={true}
|
||||||
@@ -82,9 +108,7 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
value={audioIndex === track.index.toString()}
|
value={audioIndex === track.index.toString()}
|
||||||
onValueChange={() => track.setTrack()}
|
onValueChange={() => track.setTrack()}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>{track.name}</DropdownMenu.ItemTitle>
|
||||||
{track.name}
|
|
||||||
</DropdownMenu.ItemTitle>
|
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
TrackInfo,
|
TrackInfo,
|
||||||
VlcPlayerViewRef,
|
VlcPlayerViewRef,
|
||||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
} from "@/modules/VlcPlayer.types";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export const Colors = {
|
export const Colors = {
|
||||||
primary: "#9334E9",
|
primary: "#9334E9",
|
||||||
|
primaryRGB: "rgb(147 51 234)",
|
||||||
|
primaryLightRGB: "rgb(192 132 252)",
|
||||||
text: "#ECEDEE",
|
text: "#ECEDEE",
|
||||||
background: "#151718",
|
background: "#151718",
|
||||||
tint: "#fff",
|
tint: "#fff",
|
||||||
|
|||||||
6
eas.json
6
eas.json
@@ -32,20 +32,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"channel": "0.26.1",
|
"channel": "0.27.0",
|
||||||
"android": {
|
"android": {
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production-apk": {
|
"production-apk": {
|
||||||
"channel": "0.26.1",
|
"channel": "0.27.0",
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk",
|
"buildType": "apk",
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production-apk-tv": {
|
"production-apk-tv": {
|
||||||
"channel": "0.26.1",
|
"channel": "0.27.0",
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk",
|
"buildType": "apk",
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
|
|||||||
109
hooks/useFavorite.ts
Normal file
109
hooks/useFavorite.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { useEffect, useState, useMemo } from "react";
|
||||||
|
|
||||||
|
export const useFavorite = (item: BaseItemDto) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [api] = useAtom(apiAtom);
|
||||||
|
const [user] = useAtom(userAtom);
|
||||||
|
const type = "item";
|
||||||
|
const [isFavorite, setIsFavorite] = useState(item.UserData?.IsFavorite);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsFavorite(item.UserData?.IsFavorite);
|
||||||
|
}, [item.UserData?.IsFavorite]);
|
||||||
|
|
||||||
|
const updateItemInQueries = (newData: Partial<BaseItemDto>) => {
|
||||||
|
queryClient.setQueryData<BaseItemDto | undefined>(
|
||||||
|
[type, item.Id],
|
||||||
|
(old) => {
|
||||||
|
if (!old) return old;
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
...newData,
|
||||||
|
UserData: { ...old.UserData, ...newData.UserData },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const markFavoriteMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (api && user) {
|
||||||
|
await getUserLibraryApi(api).markFavoriteItem({
|
||||||
|
userId: user.Id,
|
||||||
|
itemId: item.Id!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMutate: async () => {
|
||||||
|
await queryClient.cancelQueries({ queryKey: [type, item.Id] });
|
||||||
|
const previousItem = queryClient.getQueryData<BaseItemDto>([
|
||||||
|
type,
|
||||||
|
item.Id,
|
||||||
|
]);
|
||||||
|
updateItemInQueries({ UserData: { IsFavorite: true } });
|
||||||
|
|
||||||
|
return { previousItem };
|
||||||
|
},
|
||||||
|
onError: (err, variables, context) => {
|
||||||
|
if (context?.previousItem) {
|
||||||
|
queryClient.setQueryData([type, item.Id], context.previousItem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [type, item.Id] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["home", "favorites"] });
|
||||||
|
setIsFavorite(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const unmarkFavoriteMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (api && user) {
|
||||||
|
await getUserLibraryApi(api).unmarkFavoriteItem({
|
||||||
|
userId: user.Id,
|
||||||
|
itemId: item.Id!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMutate: async () => {
|
||||||
|
await queryClient.cancelQueries({ queryKey: [type, item.Id] });
|
||||||
|
const previousItem = queryClient.getQueryData<BaseItemDto>([
|
||||||
|
type,
|
||||||
|
item.Id,
|
||||||
|
]);
|
||||||
|
updateItemInQueries({ UserData: { IsFavorite: false } });
|
||||||
|
|
||||||
|
return { previousItem };
|
||||||
|
},
|
||||||
|
onError: (err, variables, context) => {
|
||||||
|
if (context?.previousItem) {
|
||||||
|
queryClient.setQueryData([type, item.Id], context.previousItem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [type, item.Id] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["home", "favorites"] });
|
||||||
|
setIsFavorite(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleFavorite = () => {
|
||||||
|
if (isFavorite) {
|
||||||
|
unmarkFavoriteMutation.mutate();
|
||||||
|
} else {
|
||||||
|
markFavoriteMutation.mutate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFavorite,
|
||||||
|
toggleFavorite,
|
||||||
|
markFavoriteMutation,
|
||||||
|
unmarkFavoriteMutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios, { AxiosError, AxiosInstance } from "axios";
|
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 { storage } from "@/utils/mmkv";
|
||||||
import { inRange } from "lodash";
|
import { inRange } from "lodash";
|
||||||
import { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User";
|
import { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User";
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
MediaType,
|
MediaType,
|
||||||
} from "@/utils/jellyseerr/server/constants/media";
|
} from "@/utils/jellyseerr/server/constants/media";
|
||||||
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
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 { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
||||||
import {
|
import {
|
||||||
SeasonWithEpisodes,
|
SeasonWithEpisodes,
|
||||||
@@ -227,6 +227,23 @@ export class JellyseerrApi {
|
|||||||
.then(({ data }) => data);
|
.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) {
|
async movieDetails(id: number) {
|
||||||
return this.axios
|
return this.axios
|
||||||
?.get<MovieDetails>(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`)
|
?.get<MovieDetails>(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`)
|
||||||
@@ -439,14 +456,34 @@ export const useJellyseerr = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isJellyseerrResult = (
|
const isJellyseerrResult = (
|
||||||
items: any[] | null | undefined
|
items: any | null | undefined
|
||||||
): items is Results[] => {
|
): items is Results => {
|
||||||
return (
|
return (
|
||||||
!items ||
|
items &&
|
||||||
(items.length >= 0 &&
|
Object.hasOwn(items, "mediaType") &&
|
||||||
Object.hasOwn(items[0], "mediaType") &&
|
Object.values(MediaType).includes(items["mediaType"])
|
||||||
Object.values(MediaType).includes(items[0]["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(
|
const jellyseerrRegion = useMemo(
|
||||||
@@ -464,6 +501,9 @@ export const useJellyseerr = () => {
|
|||||||
setJellyseerrUser,
|
setJellyseerrUser,
|
||||||
clearAllJellyseerData,
|
clearAllJellyseerData,
|
||||||
isJellyseerrResult,
|
isJellyseerrResult,
|
||||||
|
getTitle,
|
||||||
|
getYear,
|
||||||
|
getMediaType,
|
||||||
jellyseerrRegion,
|
jellyseerrRegion,
|
||||||
jellyseerrLocale,
|
jellyseerrLocale,
|
||||||
requestMedia,
|
requestMedia,
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import * as FileSystem from "expo-file-system";
|
|||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
// import { FFmpegKit, FFmpegSession, Statistics } from "ffmpeg-kit-react-native";
|
// import { FFmpegKit, FFmpegSession, Statistics } from "ffmpeg-kit-react-native";
|
||||||
const FFMPEGKitReactNative = !Platform.isTV ? require("ffmpeg-kit-react-native") : null;
|
const FFMPEGKitReactNative = !Platform.isTV
|
||||||
|
? require("ffmpeg-kit-react-native")
|
||||||
|
: null;
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
@@ -24,8 +26,10 @@ import { Platform } from "react-native";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession;
|
type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession;
|
||||||
type Statistics = typeof FFMPEGKitReactNative.Statistics
|
type Statistics = typeof FFMPEGKitReactNative.Statistics;
|
||||||
const FFmpegKit = FFMPEGKitReactNative.FFmpegKit;
|
const FFmpegKit = Platform.isTV
|
||||||
|
? null
|
||||||
|
: (FFMPEGKitReactNative.FFmpegKit as typeof FFMPEGKitReactNative.FFmpegKit);
|
||||||
const createFFmpegCommand = (url: string, output: string) => [
|
const createFFmpegCommand = (url: string, output: string) => [
|
||||||
"-y", // overwrite output files without asking
|
"-y", // overwrite output files without asking
|
||||||
"-thread_queue_size 512", // https://ffmpeg.org/ffmpeg.html#toc-Advanced-options
|
"-thread_queue_size 512", // https://ffmpeg.org/ffmpeg.html#toc-Advanced-options
|
||||||
@@ -101,7 +105,10 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setProcesses((prev: any[]) => {
|
setProcesses((prev: any[]) => {
|
||||||
return prev.filter((process: { itemId: string | undefined; }) => process.itemId !== item.Id);
|
return prev.filter(
|
||||||
|
(process: { itemId: string | undefined }) =>
|
||||||
|
process.itemId !== item.Id
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -126,7 +133,7 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
|
|
||||||
if (!item.Id) throw new Error("Item is undefined");
|
if (!item.Id) throw new Error("Item is undefined");
|
||||||
setProcesses((prev: any[]) => {
|
setProcesses((prev: any[]) => {
|
||||||
return prev.map((process: { itemId: string | undefined; }) => {
|
return prev.map((process: { itemId: string | undefined }) => {
|
||||||
if (process.itemId === item.Id) {
|
if (process.itemId === item.Id) {
|
||||||
return {
|
return {
|
||||||
...process,
|
...process,
|
||||||
@@ -161,15 +168,18 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
// First lets save any important assets we want to present to the user offline
|
// First lets save any important assets we want to present to the user offline
|
||||||
await onSaveAssets(api, item);
|
await onSaveAssets(api, item);
|
||||||
|
|
||||||
toast.success(t("home.downloads.toasts.download_started_for", {item: item.Name}), {
|
toast.success(
|
||||||
action: {
|
t("home.downloads.toasts.download_started_for", { item: item.Name }),
|
||||||
label: "Go to download",
|
{
|
||||||
onClick: () => {
|
action: {
|
||||||
router.push("/downloads");
|
label: "Go to download",
|
||||||
toast.dismiss();
|
onClick: () => {
|
||||||
|
router.push("/downloads");
|
||||||
|
toast.dismiss();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const job: JobStatus = {
|
const job: JobStatus = {
|
||||||
@@ -201,7 +211,10 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
Error: ${error.message}, Stack: ${error.stack}`
|
Error: ${error.message}, Stack: ${error.stack}`
|
||||||
);
|
);
|
||||||
setProcesses((prev: any[]) => {
|
setProcesses((prev: any[]) => {
|
||||||
return prev.filter((process: { itemId: string | undefined; }) => process.itemId !== item.Id);
|
return prev.filter(
|
||||||
|
(process: { itemId: string | undefined }) =>
|
||||||
|
process.itemId !== item.Id
|
||||||
|
);
|
||||||
});
|
});
|
||||||
throw error; // Re-throw the error to propagate it to the caller
|
throw error; // Re-throw the error to propagate it to the caller
|
||||||
}
|
}
|
||||||
|
|||||||
33
hooks/useSessions.ts
Normal file
33
hooks/useSessions.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
||||||
|
import { userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
|
||||||
|
export interface useSessionsProps {
|
||||||
|
refetchInterval: number;
|
||||||
|
activeWithinSeconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = 360 }: useSessionsProps) => {
|
||||||
|
const [api] = useAtom(apiAtom);
|
||||||
|
const [user] = useAtom(userAtom);
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ["sessions"],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!api || !user || !user.Policy?.IsAdministrator) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const response = await getSessionApi(api).getSessions({
|
||||||
|
activeWithinSeconds: activeWithinSeconds,
|
||||||
|
});
|
||||||
|
return response.data
|
||||||
|
.filter((s) => s.NowPlayingItem)
|
||||||
|
.sort((a, b) => (b.NowPlayingItem?.Name ?? "").localeCompare(a.NowPlayingItem?.Name ?? ""));
|
||||||
|
},
|
||||||
|
refetchInterval: refetchInterval,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sessions: data, isLoading };
|
||||||
|
};
|
||||||
12
i18n.ts
12
i18n.ts
@@ -5,9 +5,11 @@ import de from "./translations/de.json";
|
|||||||
import en from "./translations/en.json";
|
import en from "./translations/en.json";
|
||||||
import es from "./translations/es.json";
|
import es from "./translations/es.json";
|
||||||
import fr from "./translations/fr.json";
|
import fr from "./translations/fr.json";
|
||||||
|
import it from "./translations/it.json";
|
||||||
|
import ja from "./translations/ja.json";
|
||||||
import nl from "./translations/nl.json";
|
import nl from "./translations/nl.json";
|
||||||
import sv from "./translations/sv.json";
|
import sv from "./translations/sv.json";
|
||||||
import it from "./translations/it.json";
|
import zhCN from './translations/zh-CN.json';
|
||||||
import zhTW from './translations/zh-TW.json';
|
import zhTW from './translations/zh-TW.json';
|
||||||
import { getLocales } from "expo-localization";
|
import { getLocales } from "expo-localization";
|
||||||
|
|
||||||
@@ -16,9 +18,11 @@ export const APP_LANGUAGES = [
|
|||||||
{ label: "English", value: "en" },
|
{ label: "English", value: "en" },
|
||||||
{ label: "Español", value: "es" },
|
{ label: "Español", value: "es" },
|
||||||
{ label: "Français", value: "fr" },
|
{ label: "Français", value: "fr" },
|
||||||
|
{ label: "Italiano", value: "it" },
|
||||||
|
{ label: "日本語", value: "ja" },
|
||||||
{ label: "Nederlands", value: "nl" },
|
{ label: "Nederlands", value: "nl" },
|
||||||
{ label: "Svenska", value: "sv" },
|
{ label: "Svenska", value: "sv" },
|
||||||
{ label: "Italiano", value: "it" },
|
{ label: "简体中文", value: "zh-CN" },
|
||||||
{ label: "繁體中文", value: "zh-TW" },
|
{ label: "繁體中文", value: "zh-TW" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -29,9 +33,11 @@ i18n.use(initReactI18next).init({
|
|||||||
en: { translation: en },
|
en: { translation: en },
|
||||||
es: { translation: es },
|
es: { translation: es },
|
||||||
fr: { translation: fr },
|
fr: { translation: fr },
|
||||||
|
it: { translation: it },
|
||||||
|
ja: { translation: ja },
|
||||||
nl: { translation: nl },
|
nl: { translation: nl },
|
||||||
sv: { translation: sv },
|
sv: { translation: sv },
|
||||||
it: { translation: it },
|
"zh-CN": { translation: zhCN },
|
||||||
"zh-TW": { translation: zhTW },
|
"zh-TW": { translation: zhTW },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
6
login.yaml
Normal file
6
login.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# login.yaml
|
||||||
|
|
||||||
|
appId: your.app.id
|
||||||
|
---
|
||||||
|
- launchApp
|
||||||
|
- tapOn: "Text on the screen"
|
||||||
@@ -6,16 +6,30 @@ import {
|
|||||||
VlcPlayerViewRef,
|
VlcPlayerViewRef,
|
||||||
VlcPlayerSource,
|
VlcPlayerSource,
|
||||||
} from "./VlcPlayer.types";
|
} from "./VlcPlayer.types";
|
||||||
|
import {useSettings, VideoPlayer} from "@/utils/atoms/settings";
|
||||||
|
import {Platform} from "react-native";
|
||||||
|
|
||||||
interface NativeViewRef extends VlcPlayerViewRef {
|
interface NativeViewRef extends VlcPlayerViewRef {
|
||||||
setNativeProps?: (props: Partial<VlcPlayerViewProps>) => void;
|
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
|
// Create a forwarded ref version of the native view
|
||||||
const NativeView = React.forwardRef<NativeViewRef, VlcPlayerViewProps>(
|
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>(
|
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,
|
|
||||||
};
|
|
||||||
@@ -5,19 +5,19 @@ Pod::Spec.new do |s|
|
|||||||
s.description = 'A sample project description'
|
s.description = 'A sample project description'
|
||||||
s.author = ''
|
s.author = ''
|
||||||
s.homepage = 'https://docs.expo.dev/modules/'
|
s.homepage = 'https://docs.expo.dev/modules/'
|
||||||
s.platforms = { :ios => '13.4', :tvos => '13.4' }
|
s.platforms = { :ios => '13.4', :tvos => '16' }
|
||||||
s.source = { git: '' }
|
s.source = { git: '' }
|
||||||
s.static_framework = true
|
s.static_framework = true
|
||||||
|
|
||||||
s.dependency 'ExpoModulesCore'
|
s.dependency 'ExpoModulesCore'
|
||||||
s.ios.dependency 'VLCKit', s.version
|
s.ios.dependency 'VLCKit', s.version
|
||||||
s.tvos.dependency 'VLCKit', s.version
|
s.tvos.dependency 'VLCKit', s.version
|
||||||
|
s.dependency 'Alamofire', '~> 5.10'
|
||||||
|
|
||||||
# Swift/Objective-C compatibility
|
# Swift/Objective-C compatibility
|
||||||
s.pod_target_xcconfig = {
|
s.pod_target_xcconfig = {
|
||||||
'DEFINES_MODULE' => 'YES',
|
'DEFINES_MODULE' => 'YES',
|
||||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||||
}
|
}
|
||||||
|
s.source_files = "*.{h,m,mm,swift,hpp,cpp}"
|
||||||
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import UIKit
|
|||||||
import VLCKit
|
import VLCKit
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
public class VLCPlayerView: UIView {
|
public class VLCPlayerView: UIView {
|
||||||
func setupView(parent: UIView) {
|
func setupView(parent: UIView) {
|
||||||
self.backgroundColor = .black
|
self.backgroundColor = .black
|
||||||
@@ -402,7 +401,7 @@ class VlcPlayerView: ExpoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateVideoProgress() {
|
private func updateVideoProgress() {
|
||||||
guard let media = self.vlc.player.media else { return }
|
guard self.vlc.player.media != nil else { return }
|
||||||
|
|
||||||
let currentTimeMs = self.vlc.player.time.intValue
|
let currentTimeMs = self.vlc.player.time.intValue
|
||||||
let durationMs = self.vlc.player.media?.length.intValue ?? 0
|
let durationMs = self.vlc.player.media?.length.intValue ?? 0
|
||||||
@@ -459,7 +458,9 @@ extension VlcPlayerView: SimpleAppLifecycleListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Current solution to fixing black screen when re-entering application
|
// Current solution to fixing black screen when re-entering application
|
||||||
if let videoTrack = self.vlc.player.videoTracks.first { $0.isSelected == true }, !self.vlc.isMediaPlaying() {
|
if let videoTrack = self.vlc.player.videoTracks.first(where: { $0.isSelected == true }),
|
||||||
|
!self.vlc.isMediaPlaying()
|
||||||
|
{
|
||||||
videoTrack.isSelected = false
|
videoTrack.isSelected = false
|
||||||
videoTrack.isSelectedExclusively = true
|
videoTrack.isSelectedExclusively = true
|
||||||
self.vlc.player.play()
|
self.vlc.player.play()
|
||||||
@@ -477,6 +478,7 @@ extension VLCMediaPlayerState {
|
|||||||
case .paused: return "Paused"
|
case .paused: return "Paused"
|
||||||
case .stopped: return "Stopped"
|
case .stopped: return "Stopped"
|
||||||
case .error: return "Error"
|
case .error: return "Error"
|
||||||
|
case .stopping: return "Stopping"
|
||||||
@unknown default: return "Unknown"
|
@unknown default: return "Unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
"android:tv": "EXPO_TV=1 expo run:android",
|
"android:tv": "EXPO_TV=1 expo run:android",
|
||||||
"prebuild": "EXPO_TV=0 bun run clean",
|
"prebuild": "EXPO_TV=0 bun run clean",
|
||||||
"prebuild:tv": "EXPO_TV=1 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",
|
"test": "jest --watchAll",
|
||||||
"lint": "expo lint",
|
"lint": "expo lint",
|
||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package"
|
||||||
@@ -74,7 +73,7 @@
|
|||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
"react-native-bottom-tabs": "0.8.7",
|
"react-native-bottom-tabs": "0.8.6",
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
"react-native-compressor": "^1.10.3",
|
"react-native-compressor": "^1.10.3",
|
||||||
"react-native-country-flag": "^2.0.2",
|
"react-native-country-flag": "^2.0.2",
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
setJellyfin(
|
setJellyfin(
|
||||||
() =>
|
() =>
|
||||||
new Jellyfin({
|
new Jellyfin({
|
||||||
clientInfo: { name: "Streamyfin", version: "0.26.1" },
|
clientInfo: { name: "Streamyfin", version: "0.27.0" },
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
name: deviceName,
|
name: deviceName,
|
||||||
id,
|
id,
|
||||||
@@ -90,7 +90,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
return {
|
return {
|
||||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||||
Platform.OS === "android" ? "Android" : "iOS"
|
Platform.OS === "android" ? "Android" : "iOS"
|
||||||
}, DeviceId="${deviceId}", Version="0.26.1"`,
|
}, DeviceId="${deviceId}", Version="0.27.0"`,
|
||||||
};
|
};
|
||||||
}, [deviceId]);
|
}, [deviceId]);
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,12 @@
|
|||||||
"OTHER": "Andere",
|
"OTHER": "Andere",
|
||||||
"UNKNOWN": "Unbekannt"
|
"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",
|
"show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen",
|
||||||
"hide_libraries": "Bibliotheken ausblenden",
|
"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.",
|
"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_limit": "TV-Anfragelimit",
|
||||||
"tv_quota_days": "TV-Anfragetage",
|
"tv_quota_days": "TV-Anfragetage",
|
||||||
"reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück",
|
"reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück",
|
||||||
"unlimited": "Unlimitiert"
|
"unlimited": "Unlimitiert",
|
||||||
|
"plus_n_more": "+{{n}} more"
|
||||||
},
|
},
|
||||||
"marlin_search": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "Aktiviere Marlin Search",
|
"enable_marlin_search": "Aktiviere Marlin Search",
|
||||||
@@ -433,7 +439,7 @@
|
|||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"quality_profile": "Qualitätsprofil",
|
"quality_profile": "Qualitätsprofil",
|
||||||
"root_folder": "Root-Ordner",
|
"root_folder": "Root-Ordner",
|
||||||
"season_x": "Staffel {{seasons}}",
|
"season_all": "Season (all)",
|
||||||
"season_number": "Staffel {{season_number}}",
|
"season_number": "Staffel {{season_number}}",
|
||||||
"number_episodes": "{{episode_number}} Episodes",
|
"number_episodes": "{{episode_number}} Episodes",
|
||||||
"born": "Geboren",
|
"born": "Geboren",
|
||||||
|
|||||||
@@ -129,11 +129,16 @@
|
|||||||
"UNKNOWN": "Unknown"
|
"UNKNOWN": "Unknown"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Safe area in controls",
|
"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",
|
"show_custom_menu_links": "Show Custom Menu Links",
|
||||||
"hide_libraries": "Hide Libraries",
|
"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.",
|
"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",
|
"disable_haptic_feedback": "Disable Haptic Feedback",
|
||||||
"default_quality": "Default quality"
|
"default_quality": "Default quality",
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
@@ -147,7 +152,7 @@
|
|||||||
"default": "Default",
|
"default": "Default",
|
||||||
"optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.",
|
"optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.",
|
||||||
"read_more_about_optimized_server": "Read more about the optimize server.",
|
"read_more_about_optimized_server": "Read more about the optimize server.",
|
||||||
"url":"URL",
|
"url": "URL",
|
||||||
"server_url_placeholder": "http(s)://domain.org:port"
|
"server_url_placeholder": "http(s)://domain.org:port"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
@@ -168,7 +173,8 @@
|
|||||||
"tv_quota_limit": "TV quota limit",
|
"tv_quota_limit": "TV quota limit",
|
||||||
"tv_quota_days": "TV quota days",
|
"tv_quota_days": "TV quota days",
|
||||||
"reset_jellyseerr_config_button": "Reset Jellyseerr config",
|
"reset_jellyseerr_config_button": "Reset Jellyseerr config",
|
||||||
"unlimited": "Unlimited"
|
"unlimited": "Unlimited",
|
||||||
|
"plus_n_more": "+{{n}} more"
|
||||||
},
|
},
|
||||||
"marlin_search": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "Enable Marlin Search ",
|
"enable_marlin_search": "Enable Marlin Search ",
|
||||||
@@ -204,14 +210,18 @@
|
|||||||
"app_language_description": "Select the language for the app.",
|
"app_language_description": "Select the language for the app.",
|
||||||
"system": "System"
|
"system": "System"
|
||||||
},
|
},
|
||||||
"toasts":{
|
"toasts": {
|
||||||
"error_deleting_files": "Error deleting files",
|
"error_deleting_files": "Error deleting files",
|
||||||
"background_downloads_enabled": "Background downloads enabled",
|
"background_downloads_enabled": "Background downloads enabled",
|
||||||
"background_downloads_disabled": "Background downloads disabled",
|
"background_downloads_disabled": "Background downloads disabled",
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
"could_not_connect": "Could not connect",
|
"could_not_connect": "Could not connect",
|
||||||
"invalid_url": "Invalid URL"
|
"invalid_url": "Invalid URL"
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"title": "Sessions",
|
||||||
|
"no_active_sessions": "No active sessions"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
@@ -399,7 +409,7 @@
|
|||||||
"for_kids": "For Kids",
|
"for_kids": "For Kids",
|
||||||
"news": "News"
|
"news": "News"
|
||||||
},
|
},
|
||||||
"jellyseerr":{
|
"jellyseerr": {
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
@@ -433,7 +443,7 @@
|
|||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"quality_profile": "Quality Profile",
|
"quality_profile": "Quality Profile",
|
||||||
"root_folder": "Root Folder",
|
"root_folder": "Root Folder",
|
||||||
"season_x": "Season {{seasons}}",
|
"season_all": "Season (all)",
|
||||||
"season_number": "Season {{season_number}}",
|
"season_number": "Season {{season_number}}",
|
||||||
"number_episodes": "{{episode_number}} Episodes",
|
"number_episodes": "{{episode_number}} Episodes",
|
||||||
"born": "Born",
|
"born": "Born",
|
||||||
|
|||||||
@@ -128,7 +128,12 @@
|
|||||||
"OTHER": "Otra",
|
"OTHER": "Otra",
|
||||||
"UNKNOWN": "Desconocida"
|
"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",
|
"show_custom_menu_links": "Mostrar enlaces de menú personalizados",
|
||||||
"hide_libraries": "Ocultar bibliotecas",
|
"hide_libraries": "Ocultar bibliotecas",
|
||||||
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
|
"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_limit": "Límite de cuota de series",
|
||||||
"tv_quota_days": "Días de cuota de series",
|
"tv_quota_days": "Días de cuota de series",
|
||||||
"reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr",
|
"reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr",
|
||||||
"unlimited": "Ilimitado"
|
"unlimited": "Ilimitado",
|
||||||
|
"plus_n_more": "+{{n}} more"
|
||||||
},
|
},
|
||||||
"marlin_search": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "Habilitar búsqueda de Marlin",
|
"enable_marlin_search": "Habilitar búsqueda de Marlin",
|
||||||
@@ -433,7 +439,7 @@
|
|||||||
"tags": "Etiquetas",
|
"tags": "Etiquetas",
|
||||||
"quality_profile": "Perfil de calidad",
|
"quality_profile": "Perfil de calidad",
|
||||||
"root_folder": "Carpeta raíz",
|
"root_folder": "Carpeta raíz",
|
||||||
"season_x": "Temporada {{seasons}}",
|
"season_all": "Season (all)",
|
||||||
"season_number": "Temporada {{season_number}}",
|
"season_number": "Temporada {{season_number}}",
|
||||||
"number_episodes": "{{episode_number}} episodios",
|
"number_episodes": "{{episode_number}} episodios",
|
||||||
"born": "Nacido",
|
"born": "Nacido",
|
||||||
|
|||||||
@@ -128,7 +128,12 @@
|
|||||||
"OTHER": "Autre",
|
"OTHER": "Autre",
|
||||||
"UNKNOWN": "Inconnu"
|
"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",
|
"show_custom_menu_links": "Afficher les liens personnalisés",
|
||||||
"hide_libraries": "Cacher des bibliothèques",
|
"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.",
|
"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_limit": "Limite de quota TV",
|
||||||
"tv_quota_days": "Jours de quota TV",
|
"tv_quota_days": "Jours de quota TV",
|
||||||
"reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr",
|
"reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr",
|
||||||
"unlimited": "Illimité"
|
"unlimited": "Illimité",
|
||||||
|
"plus_n_more": "+{{n}} more"
|
||||||
},
|
},
|
||||||
"marlin_search": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "Activer Marlin Search ",
|
"enable_marlin_search": "Activer Marlin Search ",
|
||||||
@@ -434,7 +440,7 @@
|
|||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"quality_profile": "Profil de qualité",
|
"quality_profile": "Profil de qualité",
|
||||||
"root_folder": "Dossier racine",
|
"root_folder": "Dossier racine",
|
||||||
"season_x": "Saison {{seasons}}",
|
"season_all": "Season (all)",
|
||||||
"season_number": "Saison {{season_number}}",
|
"season_number": "Saison {{season_number}}",
|
||||||
"number_episodes": "{{episode_number}} épisodes",
|
"number_episodes": "{{episode_number}} épisodes",
|
||||||
"born": "Né(e) le",
|
"born": "Né(e) le",
|
||||||
|
|||||||
@@ -1,458 +1,464 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"username_required": "Nome utente è obbligatorio",
|
"username_required": "Nome utente è obbligatorio",
|
||||||
"error_title": "Errore",
|
"error_title": "Errore",
|
||||||
"login_title": "Accesso",
|
"login_title": "Accesso",
|
||||||
"login_to_title": "Accedi a",
|
"login_to_title": "Accedi a",
|
||||||
"username_placeholder": "Nome utente",
|
"username_placeholder": "Nome utente",
|
||||||
"password_placeholder": "Password",
|
"password_placeholder": "Password",
|
||||||
"login_button": "Accedi",
|
"login_button": "Accedi",
|
||||||
"quick_connect": "Connessione Rapida",
|
"quick_connect": "Connessione Rapida",
|
||||||
"enter_code_to_login": "Inserire {{code}} per accedere",
|
"enter_code_to_login": "Inserire {{code}} per accedere",
|
||||||
"failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida",
|
"failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida",
|
||||||
"got_it": "Capito",
|
"got_it": "Capito",
|
||||||
"connection_failed": "Connessione fallita",
|
"connection_failed": "Connessione fallita",
|
||||||
"could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.",
|
"could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.",
|
||||||
"an_unexpected_error_occured": "Si è verificato un errore inaspettato",
|
"an_unexpected_error_occured": "Si è verificato un errore inaspettato",
|
||||||
"change_server": "Cambiare il server",
|
"change_server": "Cambiare il server",
|
||||||
"invalid_username_or_password": "Nome utente o password non validi",
|
"invalid_username_or_password": "Nome utente o password non validi",
|
||||||
"user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere",
|
"user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere",
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi",
|
"server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi",
|
||||||
"server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.",
|
"server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.",
|
||||||
"there_is_a_server_error": "Si è verificato un errore del server",
|
"there_is_a_server_error": "Si è verificato un errore del server",
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?"
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin",
|
||||||
|
"server_url_placeholder": "http(s)://tuo-server.com",
|
||||||
|
"connect_button": "Connetti",
|
||||||
|
"previous_servers": "server precedente",
|
||||||
|
"clear_button": "Cancella",
|
||||||
|
"search_for_local_servers": "Ricerca dei server locali",
|
||||||
|
"searching": "Cercando...",
|
||||||
|
"servers": "Servers"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"no_internet": "Nessun Internet",
|
||||||
|
"no_items": "Nessun oggetto",
|
||||||
|
"no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.",
|
||||||
|
"go_to_downloads": "Vai agli elementi scaricati",
|
||||||
|
"oops": "Oops!",
|
||||||
|
"error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.",
|
||||||
|
"continue_watching": "Continua a guardare",
|
||||||
|
"next_up": "Prossimo",
|
||||||
|
"recently_added_in": "Aggiunti di recente a {{libraryName}}",
|
||||||
|
"suggested_movies": "Film consigliati",
|
||||||
|
"suggested_episodes": "Episodi consigliati",
|
||||||
|
"intro": {
|
||||||
|
"welcome_to_streamyfin": "Benvenuto a Streamyfin",
|
||||||
|
"a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.",
|
||||||
|
"features_title": "Funzioni",
|
||||||
|
"features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:",
|
||||||
|
"jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.",
|
||||||
|
"downloads_feature_title": "Scaricamento",
|
||||||
|
"downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.",
|
||||||
|
"chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.",
|
||||||
|
"centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate",
|
||||||
|
"centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.",
|
||||||
|
"done_button": "Fatto",
|
||||||
|
"go_to_settings_button": "Vai alle impostazioni",
|
||||||
|
"read_more": "Leggi di più"
|
||||||
},
|
},
|
||||||
"server": {
|
"settings": {
|
||||||
"enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin",
|
"settings_title": "Impostazioni",
|
||||||
"server_url_placeholder": "http(s)://tuo-server.com",
|
"log_out_button": "Esci",
|
||||||
"connect_button": "Connetti",
|
"user_info": {
|
||||||
"previous_servers": "server precedente",
|
"user_info_title": "Info utente",
|
||||||
"clear_button": "Cancella",
|
"user": "Utente",
|
||||||
"search_for_local_servers": "Ricerca dei server locali",
|
"server": "Server",
|
||||||
"searching": "Cercando...",
|
"token": "Token",
|
||||||
"servers": "Servers"
|
"app_version": "Versione dell'App"
|
||||||
},
|
|
||||||
"home": {
|
|
||||||
"no_internet": "Nessun Internet",
|
|
||||||
"no_items": "Nessun oggetto",
|
|
||||||
"no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.",
|
|
||||||
"go_to_downloads": "Vai agli elementi scaricati",
|
|
||||||
"oops": "Oops!",
|
|
||||||
"error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.",
|
|
||||||
"continue_watching": "Continua a guardare",
|
|
||||||
"next_up": "Prossimo",
|
|
||||||
"recently_added_in": "Aggiunti di recente a {{libraryName}}",
|
|
||||||
"suggested_movies": "Film consigliati",
|
|
||||||
"suggested_episodes": "Episodi consigliati",
|
|
||||||
"intro": {
|
|
||||||
"welcome_to_streamyfin": "Benvenuto a Streamyfin",
|
|
||||||
"a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.",
|
|
||||||
"features_title": "Funzioni",
|
|
||||||
"features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:",
|
|
||||||
"jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.",
|
|
||||||
"downloads_feature_title": "Scaricamento",
|
|
||||||
"downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.",
|
|
||||||
"chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.",
|
|
||||||
"centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate",
|
|
||||||
"centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.",
|
|
||||||
"done_button": "Fatto",
|
|
||||||
"go_to_settings_button": "Vai alle impostazioni",
|
|
||||||
"read_more": "Leggi di più"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"quick_connect": {
|
||||||
"settings_title": "Impostazioni",
|
"quick_connect_title": "Connessione Rapida",
|
||||||
"log_out_button": "Esci",
|
"authorize_button": "Autorizza Connessione Rapida",
|
||||||
"user_info": {
|
"enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...",
|
||||||
"user_info_title": "Info utente",
|
"success": "Successo",
|
||||||
"user": "Utente",
|
"quick_connect_autorized": "Connessione Rapida autorizzata",
|
||||||
"server": "Server",
|
"error": "Errore",
|
||||||
"token": "Token",
|
"invalid_code": "Codice invalido",
|
||||||
"app_version": "Versione dell'App"
|
"authorize": "Autorizza"
|
||||||
},
|
},
|
||||||
"quick_connect": {
|
"media_controls": {
|
||||||
"quick_connect_title": "Connessione Rapida",
|
"media_controls_title": "Controlli multimediali",
|
||||||
"authorize_button": "Autorizza Connessione Rapida",
|
"forward_skip_length": "Lunghezza del salto in avanti",
|
||||||
"enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...",
|
"rewind_length": "Lunghezza del riavvolgimento",
|
||||||
"success": "Successo",
|
"seconds_unit": "s"
|
||||||
"quick_connect_autorized": "Connessione Rapida autorizzata",
|
},
|
||||||
"error": "Errore",
|
"audio": {
|
||||||
"invalid_code": "Codice invalido",
|
"audio_title": "Audio",
|
||||||
"authorize": "Autorizza"
|
"set_audio_track": "Imposta la traccia audio dall'elemento precedente",
|
||||||
},
|
"audio_language": "Lingua Audio",
|
||||||
"media_controls": {
|
"audio_hint": "Scegli la lingua audio predefinita.",
|
||||||
"media_controls_title": "Controlli multimediali",
|
"none": "Nessuno",
|
||||||
"forward_skip_length": "Lunghezza del salto in avanti",
|
"language": "Lingua"
|
||||||
"rewind_length": "Lunghezza del riavvolgimento",
|
},
|
||||||
"seconds_unit": "s"
|
"subtitles": {
|
||||||
},
|
"subtitle_title": "Sottotitoli",
|
||||||
"audio": {
|
"subtitle_language": "Lingua dei sottotitoli",
|
||||||
"audio_title": "Audio",
|
"subtitle_mode": "Modalità dei sottotitoli",
|
||||||
"set_audio_track": "Imposta la traccia audio dall'elemento precedente",
|
"set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente",
|
||||||
"audio_language": "Lingua Audio",
|
"subtitle_size": "Dimensione dei sottotitoli",
|
||||||
"audio_hint": "Scegli la lingua audio predefinita.",
|
"subtitle_hint": "Configura la preferenza dei sottotitoli.",
|
||||||
"none": "Nessuno",
|
"none": "Nessuno",
|
||||||
"language": "Lingua"
|
"language": "Lingua",
|
||||||
},
|
"loading": "Caricamento",
|
||||||
"subtitles": {
|
"modes": {
|
||||||
"subtitle_title": "Sottotitoli",
|
"Default": "Predefinito",
|
||||||
"subtitle_language": "Lingua dei sottotitoli",
|
"Smart": "Intelligente",
|
||||||
"subtitle_mode": "Modalità dei sottotitoli",
|
"Always": "Sempre",
|
||||||
"set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente",
|
"None": "Nessuno",
|
||||||
"subtitle_size": "Dimensione dei sottotitoli",
|
"OnlyForced": "Solo forzati"
|
||||||
"subtitle_hint": "Configura la preferenza dei sottotitoli.",
|
|
||||||
"none": "Nessuno",
|
|
||||||
"language": "Lingua",
|
|
||||||
"loading": "Caricamento",
|
|
||||||
"modes": {
|
|
||||||
"Default": "Predefinito",
|
|
||||||
"Smart": "Intelligente",
|
|
||||||
"Always": "Sempre",
|
|
||||||
"None": "Nessuno",
|
|
||||||
"OnlyForced": "Solo forzati"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"other": {
|
|
||||||
"other_title": "Altro",
|
|
||||||
"auto_rotate": "Rotazione automatica",
|
|
||||||
"video_orientation": "Orientamento del video",
|
|
||||||
"orientation": "Orientamento",
|
|
||||||
"orientations": {
|
|
||||||
"DEFAULT": "Predefinito",
|
|
||||||
"ALL": "Tutto",
|
|
||||||
"PORTRAIT": "Verticale",
|
|
||||||
"PORTRAIT_UP": "Verticale sopra",
|
|
||||||
"PORTRAIT_DOWN": "Verticale sotto",
|
|
||||||
"LANDSCAPE": "Orizzontale",
|
|
||||||
"LANDSCAPE_LEFT": "Orizzontale sinitra",
|
|
||||||
"LANDSCAPE_RIGHT": "Orizzontale destra",
|
|
||||||
"OTHER": "Altro",
|
|
||||||
"UNKNOWN": "Sconosciuto"
|
|
||||||
},
|
|
||||||
"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.",
|
|
||||||
"disable_haptic_feedback": "Disabilita il feedback aptico",
|
|
||||||
"default_quality": "Qualità predefinita"
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "Scaricamento",
|
|
||||||
"download_method": "Metodo per lo scaricamento",
|
|
||||||
"remux_max_download": "Numero di Remux da scaricare al massimo",
|
|
||||||
"auto_download": "Scaricamento automatico",
|
|
||||||
"optimized_versions_server": "Versioni del server di ottimizzazione",
|
|
||||||
"save_button": "Salva",
|
|
||||||
"optimized_server": "Server di ottimizzazione",
|
|
||||||
"optimized": "Ottimizzato",
|
|
||||||
"default": "Predefinito",
|
|
||||||
"optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.",
|
|
||||||
"read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.",
|
|
||||||
"url":"URL",
|
|
||||||
"server_url_placeholder": "http(s)://dominio.org:porta"
|
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"plugins_title": "Plugin",
|
|
||||||
"jellyseerr": {
|
|
||||||
"jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.",
|
|
||||||
"server_url": "URL del Server",
|
|
||||||
"server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)",
|
|
||||||
"server_url_placeholder": "URL di Jellyseerr...",
|
|
||||||
"password": "Password",
|
|
||||||
"password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin",
|
|
||||||
"save_button": "Salva",
|
|
||||||
"clear_button": "Cancella",
|
|
||||||
"login_button": "Accedi",
|
|
||||||
"total_media_requests": "Totale di richieste di media",
|
|
||||||
"movie_quota_limit": "Limite di quota per i film",
|
|
||||||
"movie_quota_days": "Giorni di quota per i film",
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"marlin_search": {
|
|
||||||
"enable_marlin_search": "Abilita la ricerca Marlin ",
|
|
||||||
"url": "URL",
|
|
||||||
"server_url_placeholder": "http(s)://dominio.org:porta",
|
|
||||||
"marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.",
|
|
||||||
"read_more_about_marlin": "Leggi di più su Marlin.",
|
|
||||||
"save_button": "Salva",
|
|
||||||
"toasts": {
|
|
||||||
"saved": "Salvato"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"storage": {
|
|
||||||
"storage_title": "Spazio",
|
|
||||||
"app_usage": "App {{usedSpace}}%",
|
|
||||||
"device_usage": "Dispositivo {{availableSpace}}%",
|
|
||||||
"size_used": "{{used}} di {{total}} usato",
|
|
||||||
"delete_all_downloaded_files": "Cancella Tutti i File Scaricati"
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"show_intro": "Mostra intro",
|
|
||||||
"reset_intro": "Ripristina intro"
|
|
||||||
},
|
|
||||||
"logs": {
|
|
||||||
"logs_title": "Log",
|
|
||||||
"no_logs_available": "Nessun log disponibile",
|
|
||||||
"delete_all_logs": "Cancella tutti i log"
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"title": "Lingue",
|
|
||||||
"app_language": "Lingua dell'App",
|
|
||||||
"app_language_description": "Selezione la lingua dell'app.",
|
|
||||||
"system": "Sistema"
|
|
||||||
},
|
|
||||||
"toasts":{
|
|
||||||
"error_deleting_files": "Errore nella cancellazione dei file",
|
|
||||||
"background_downloads_enabled": "Scaricamento in background abilitato",
|
|
||||||
"background_downloads_disabled": "Scaricamento in background disabilitato",
|
|
||||||
"connected": "Connesso",
|
|
||||||
"could_not_connect": "Non è stato possibile connettersi",
|
|
||||||
"invalid_url": "URL invalido"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"other_title": "Altro",
|
||||||
|
"auto_rotate": "Rotazione automatica",
|
||||||
|
"video_orientation": "Orientamento del video",
|
||||||
|
"orientation": "Orientamento",
|
||||||
|
"orientations": {
|
||||||
|
"DEFAULT": "Predefinito",
|
||||||
|
"ALL": "Tutto",
|
||||||
|
"PORTRAIT": "Verticale",
|
||||||
|
"PORTRAIT_UP": "Verticale sopra",
|
||||||
|
"PORTRAIT_DOWN": "Verticale sotto",
|
||||||
|
"LANDSCAPE": "Orizzontale",
|
||||||
|
"LANDSCAPE_LEFT": "Orizzontale sinitra",
|
||||||
|
"LANDSCAPE_RIGHT": "Orizzontale destra",
|
||||||
|
"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)"
|
||||||
|
},
|
||||||
|
"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.",
|
||||||
|
"disable_haptic_feedback": "Disabilita il feedback aptico",
|
||||||
|
"default_quality": "Qualità predefinita"
|
||||||
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Scaricati",
|
"downloads_title": "Scaricamento",
|
||||||
"tvseries": "Serie TV",
|
"download_method": "Metodo per lo scaricamento",
|
||||||
"movies": "Film",
|
"remux_max_download": "Numero di Remux da scaricare al massimo",
|
||||||
"queue": "Coda",
|
"auto_download": "Scaricamento automatico",
|
||||||
"queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app",
|
"optimized_versions_server": "Versioni del server di ottimizzazione",
|
||||||
"no_items_in_queue": "Nessun elemento in coda",
|
"save_button": "Salva",
|
||||||
"no_downloaded_items": "Nessun elemento scaricato",
|
"optimized_server": "Server di ottimizzazione",
|
||||||
"delete_all_movies_button": "Cancella tutti i film",
|
"optimized": "Ottimizzato",
|
||||||
"delete_all_tvseries_button": "Cancella tutte le serie TV",
|
"default": "Predefinito",
|
||||||
"delete_all_button": "Cancella tutti",
|
"optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.",
|
||||||
"active_download": "Scaricamento in corso",
|
"read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.",
|
||||||
"no_active_downloads": "Nessun scaricamento in corso",
|
"url": "URL",
|
||||||
"active_downloads": "Scaricamenti in corso",
|
"server_url_placeholder": "http(s)://dominio.org:porta"
|
||||||
"new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti",
|
},
|
||||||
"new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.",
|
"plugins": {
|
||||||
"back": "Indietro",
|
"plugins_title": "Plugin",
|
||||||
"delete": "Cancella",
|
"jellyseerr": {
|
||||||
"something_went_wrong": "Qualcosa è andato storto",
|
"jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin",
|
"server_url": "URL del Server",
|
||||||
"eta": "ETA {{eta}}",
|
"server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)",
|
||||||
"methods": "Metodi",
|
"server_url_placeholder": "URL di Jellyseerr...",
|
||||||
"toasts": {
|
"password": "Password",
|
||||||
"you_are_not_allowed_to_download_files": "Non è consentito scaricare file.",
|
"password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin",
|
||||||
"deleted_all_movies_successfully": "Cancellati tutti i film con successo!",
|
"save_button": "Salva",
|
||||||
"failed_to_delete_all_movies": "Impossibile eliminare tutti i film",
|
"clear_button": "Cancella",
|
||||||
"deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!",
|
"login_button": "Accedi",
|
||||||
"failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV",
|
"total_media_requests": "Totale di richieste di media",
|
||||||
"download_cancelled": "Scaricamento annullato",
|
"movie_quota_limit": "Limite di quota per i film",
|
||||||
"could_not_cancel_download": "Impossibile annullare lo scaricamento",
|
"movie_quota_days": "Giorni di quota per i film",
|
||||||
"download_completed": "Scaricamento completato",
|
"tv_quota_limit": "Limite di quota per le serie TV",
|
||||||
"download_started_for": "Scaricamento iniziato per {{item}}",
|
"tv_quota_days": "Giorni di quota per le serie TV",
|
||||||
"item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato",
|
"reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr",
|
||||||
"download_stated_for_item": "Scaricamento iniziato per {{item}}",
|
"unlimited": "Illimitato",
|
||||||
"download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}",
|
"plus_n_more": "+{{n}} more"
|
||||||
"download_completed_for_item": "Scaricamento completato per {{item}}",
|
},
|
||||||
"queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione",
|
"marlin_search": {
|
||||||
"failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}",
|
"enable_marlin_search": "Abilita la ricerca Marlin ",
|
||||||
"server_responded_with_status_code": "Server responded with status {{statusCode}}",
|
"url": "URL",
|
||||||
"no_response_received_from_server": "No response received from the server",
|
"server_url_placeholder": "http(s)://dominio.org:porta",
|
||||||
"error_setting_up_the_request": "Error setting up the request",
|
"marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.",
|
||||||
"failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto",
|
"read_more_about_marlin": "Leggi di più su Marlin.",
|
||||||
"all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.",
|
"save_button": "Salva",
|
||||||
"an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi",
|
"toasts": {
|
||||||
"go_to_downloads": "Vai agli elementi scaricati"
|
"saved": "Salvato"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"search_here": "Cerca qui...",
|
|
||||||
"search": "Cerca...",
|
|
||||||
"x_items": "{{count}} elementi",
|
|
||||||
"library": "Libreria",
|
|
||||||
"discover": "Scopri",
|
|
||||||
"no_results": "Nessun risultato",
|
|
||||||
"no_results_found_for": "Nessun risultato trovato per",
|
|
||||||
"movies": "Film",
|
|
||||||
"series": "Serie",
|
|
||||||
"episodes": "Episodi",
|
|
||||||
"collections": "Collezioni",
|
|
||||||
"actors": "Attori",
|
|
||||||
"request_movies": "Film Richiesti",
|
|
||||||
"request_series": "Serie Richieste",
|
|
||||||
"recently_added": "Aggiunti di Recente",
|
|
||||||
"recent_requests": "Richiesti di Recente",
|
|
||||||
"plex_watchlist": "Plex Watchlist",
|
|
||||||
"trending": "In tendenza",
|
|
||||||
"popular_movies": "Film Popolari",
|
|
||||||
"movie_genres": "Generi Film",
|
|
||||||
"upcoming_movies": "Film in arrivo",
|
|
||||||
"studios": "Studio",
|
|
||||||
"popular_tv": "Serie Popolari",
|
|
||||||
"tv_genres": "Generi Televisivi",
|
|
||||||
"upcoming_tv": "Serie in Arrivo",
|
|
||||||
"networks": "Network",
|
|
||||||
"tmdb_movie_keyword": "TMDB Parola chiave del film",
|
|
||||||
"tmdb_movie_genre": "TMDB Genere Film",
|
|
||||||
"tmdb_tv_keyword": "TMDB Parola chiave della serie",
|
|
||||||
"tmdb_tv_genre": "TMDB Genere Televisivo",
|
|
||||||
"tmdb_search": "TMDB Cerca",
|
|
||||||
"tmdb_studio": "TMDB Studio",
|
|
||||||
"tmdb_network": "TMDB Network",
|
|
||||||
"tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film",
|
|
||||||
"tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie"
|
|
||||||
},
|
|
||||||
"library": {
|
|
||||||
"no_items_found": "Nessun elemento trovato",
|
|
||||||
"no_results": "Nessun risultato",
|
|
||||||
"no_libraries_found": "Nessuna libreria trovata",
|
|
||||||
"item_types": {
|
|
||||||
"movies": "film",
|
|
||||||
"series": "serie TV",
|
|
||||||
"boxsets": "cofanetti",
|
|
||||||
"items": "elementi"
|
|
||||||
},
|
},
|
||||||
"options": {
|
"storage": {
|
||||||
"display": "Display",
|
"storage_title": "Spazio",
|
||||||
"row": "Fila",
|
"app_usage": "App {{usedSpace}}%",
|
||||||
"list": "Lista",
|
"device_usage": "Dispositivo {{availableSpace}}%",
|
||||||
"image_style": "Stile dell'immagine",
|
"size_used": "{{used}} di {{total}} usato",
|
||||||
"poster": "Poster",
|
"delete_all_downloaded_files": "Cancella Tutti i File Scaricati"
|
||||||
"cover": "Cover",
|
},
|
||||||
"show_titles": "Mostra titoli",
|
"intro": {
|
||||||
"show_stats": "Mostra statistiche"
|
"show_intro": "Mostra intro",
|
||||||
|
"reset_intro": "Ripristina intro"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"logs_title": "Log",
|
||||||
|
"no_logs_available": "Nessun log disponibile",
|
||||||
|
"delete_all_logs": "Cancella tutti i log"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"title": "Lingue",
|
||||||
|
"app_language": "Lingua dell'App",
|
||||||
|
"app_language_description": "Selezione la lingua dell'app.",
|
||||||
|
"system": "Sistema"
|
||||||
},
|
},
|
||||||
"filters": {
|
|
||||||
"genres": "Generi",
|
|
||||||
"years": "Anni",
|
|
||||||
"sort_by": "Ordina per",
|
|
||||||
"sort_order": "Criterio di ordinamento",
|
|
||||||
"tags": "Tag"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"favorites": {
|
|
||||||
"series": "Serie TV",
|
|
||||||
"movies": "Film",
|
|
||||||
"episodes": "Episodi",
|
|
||||||
"videos": "Video",
|
|
||||||
"boxsets": "Boxset",
|
|
||||||
"playlists": "Playlist"
|
|
||||||
},
|
|
||||||
"custom_links": {
|
|
||||||
"no_links": "Nessun link"
|
|
||||||
},
|
|
||||||
"player": {
|
|
||||||
"error": "Errore",
|
|
||||||
"failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream",
|
|
||||||
"an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.",
|
|
||||||
"client_error": "Errore del client",
|
|
||||||
"could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast",
|
|
||||||
"message_from_server": "Messaggio dal server: {{messagge}}",
|
|
||||||
"video_has_finished_playing": "La riproduzione del video è terminata!",
|
|
||||||
"no_video_source": "Nessuna sorgente video...",
|
|
||||||
"next_episode": "Prossimo Episodio",
|
|
||||||
"refresh_tracks": "Aggiorna tracce",
|
|
||||||
"subtitle_tracks": "Tracce di sottotitoli:",
|
|
||||||
"audio_tracks": "Tracce audio:",
|
|
||||||
"playback_state": "Stato della riproduzione:",
|
|
||||||
"no_data_available": "Nessun dato disponibile",
|
|
||||||
"index": "Indice:"
|
|
||||||
},
|
|
||||||
"item_card": {
|
|
||||||
"next_up": "Il prossimo",
|
|
||||||
"no_items_to_display": "Nessun elemento da visualizzare",
|
|
||||||
"cast_and_crew": "Cast e Equipaggio",
|
|
||||||
"series": "Serie",
|
|
||||||
"seasons": "Stagioni",
|
|
||||||
"season": "Stagione",
|
|
||||||
"no_episodes_for_this_season": "Nessun episodio per questa stagione",
|
|
||||||
"overview": "Panoramica",
|
|
||||||
"more_with": "Altri con {{name}}",
|
|
||||||
"similar_items": "Elementi simili",
|
|
||||||
"no_similar_items_found": "Non sono stati trovati elementi simili",
|
|
||||||
"video": "Video",
|
|
||||||
"more_details": "Più dettagli",
|
|
||||||
"quality": "Qualità",
|
|
||||||
"audio": "Audio",
|
|
||||||
"subtitles": "Sottotitoli",
|
|
||||||
"show_more": "Mostra di più",
|
|
||||||
"show_less": "Mostra di meno",
|
|
||||||
"appeared_in": "Apparso in",
|
|
||||||
"could_not_load_item": "Impossibile caricare l'elemento",
|
|
||||||
"none": "Nessuno",
|
|
||||||
"download": {
|
|
||||||
"download_season": "Scarica Stagione",
|
|
||||||
"download_series": "Scarica Serie",
|
|
||||||
"download_episode": "Scarica Episodio",
|
|
||||||
"download_movie": "Scarica Film",
|
|
||||||
"download_x_item": "Scarica {{item_count}} elementi",
|
|
||||||
"download_button": "Scarica",
|
|
||||||
"using_optimized_server": "Utilizzando il server di ottimizzazione",
|
|
||||||
"using_default_method": "Utilizzando il metodo predefinito"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"live_tv": {
|
|
||||||
"next": "Prossimo",
|
|
||||||
"previous": "Precedente",
|
|
||||||
"live_tv": "TV in diretta",
|
|
||||||
"coming_soon": "Prossimamente",
|
|
||||||
"on_now": "In onda ora",
|
|
||||||
"shows": "Programmi",
|
|
||||||
"movies": "Film",
|
|
||||||
"sports": "Sport",
|
|
||||||
"for_kids": "Per Bambini",
|
|
||||||
"news": "Notiziari"
|
|
||||||
},
|
|
||||||
"jellyseerr":{
|
|
||||||
"confirm": "Conferma",
|
|
||||||
"cancel": "Cancella",
|
|
||||||
"yes": "Si",
|
|
||||||
"whats_wrong": "Cosa c'è che non va?",
|
|
||||||
"issue_type": "Tipo di problema",
|
|
||||||
"select_an_issue": "Seleziona un problema",
|
|
||||||
"types": "Tipi",
|
|
||||||
"describe_the_issue": "(facoltativo) Descrivere il problema...",
|
|
||||||
"submit_button": "Invia",
|
|
||||||
"report_issue_button": "Segnalare il problema",
|
|
||||||
"request_button": "Richiedi",
|
|
||||||
"are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?",
|
|
||||||
"failed_to_login": "Accesso non riuscito",
|
|
||||||
"cast": "Cast",
|
|
||||||
"details": "Dettagli",
|
|
||||||
"status": "Stato",
|
|
||||||
"original_title": "Titolo originale",
|
|
||||||
"series_type": "Tipo di Serie",
|
|
||||||
"release_dates": "Date di Uscita",
|
|
||||||
"first_air_date": "Prima Data di Messa in Onda",
|
|
||||||
"next_air_date": "Prossima Data di Messa in Onda",
|
|
||||||
"revenue": "Ricavi",
|
|
||||||
"budget": "Budget",
|
|
||||||
"original_language": "Lingua Originale",
|
|
||||||
"production_country": "Paese di Produzione",
|
|
||||||
"studios": "Studio",
|
|
||||||
"network": "Network",
|
|
||||||
"currently_streaming_on": "Attualmente in streaming su",
|
|
||||||
"advanced": "Avanzate",
|
|
||||||
"request_as": "Richiedi Come",
|
|
||||||
"tags": "Tag",
|
|
||||||
"quality_profile": "Profilo qualità",
|
|
||||||
"root_folder": "Cartella radice",
|
|
||||||
"season_x": "Stagione {{seasons}}",
|
|
||||||
"season_number": "Stagione {{season_number}}",
|
|
||||||
"number_episodes": "{{episode_number}} Episodio",
|
|
||||||
"born": "Nato",
|
|
||||||
"appearances": "Aspetto",
|
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.",
|
"error_deleting_files": "Errore nella cancellazione dei file",
|
||||||
"jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.",
|
"background_downloads_enabled": "Scaricamento in background abilitato",
|
||||||
"failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr",
|
"background_downloads_disabled": "Scaricamento in background disabilitato",
|
||||||
"issue_submitted": "Problema inviato!",
|
"connected": "Connesso",
|
||||||
"requested_item": "Richiesto {{item}}!",
|
"could_not_connect": "Non è stato possibile connettersi",
|
||||||
"you_dont_have_permission_to_request": "Non hai il permesso di richiedere!",
|
"invalid_url": "URL invalido"
|
||||||
"something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tabs": {
|
"downloads": {
|
||||||
"home": "Home",
|
"downloads_title": "Scaricati",
|
||||||
"search": "Cerca",
|
"tvseries": "Serie TV",
|
||||||
"library": "Libreria",
|
"movies": "Film",
|
||||||
"custom_links": "Collegamenti personalizzati",
|
"queue": "Coda",
|
||||||
"favorites": "Preferiti"
|
"queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app",
|
||||||
|
"no_items_in_queue": "Nessun elemento in coda",
|
||||||
|
"no_downloaded_items": "Nessun elemento scaricato",
|
||||||
|
"delete_all_movies_button": "Cancella tutti i film",
|
||||||
|
"delete_all_tvseries_button": "Cancella tutte le serie TV",
|
||||||
|
"delete_all_button": "Cancella tutti",
|
||||||
|
"active_download": "Scaricamento in corso",
|
||||||
|
"no_active_downloads": "Nessun scaricamento in corso",
|
||||||
|
"active_downloads": "Scaricamenti in corso",
|
||||||
|
"new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti",
|
||||||
|
"new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.",
|
||||||
|
"back": "Indietro",
|
||||||
|
"delete": "Cancella",
|
||||||
|
"something_went_wrong": "Qualcosa è andato storto",
|
||||||
|
"could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin",
|
||||||
|
"eta": "ETA {{eta}}",
|
||||||
|
"methods": "Metodi",
|
||||||
|
"toasts": {
|
||||||
|
"you_are_not_allowed_to_download_files": "Non è consentito scaricare file.",
|
||||||
|
"deleted_all_movies_successfully": "Cancellati tutti i film con successo!",
|
||||||
|
"failed_to_delete_all_movies": "Impossibile eliminare tutti i film",
|
||||||
|
"deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!",
|
||||||
|
"failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV",
|
||||||
|
"download_cancelled": "Scaricamento annullato",
|
||||||
|
"could_not_cancel_download": "Impossibile annullare lo scaricamento",
|
||||||
|
"download_completed": "Scaricamento completato",
|
||||||
|
"download_started_for": "Scaricamento iniziato per {{item}}",
|
||||||
|
"item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato",
|
||||||
|
"download_stated_for_item": "Scaricamento iniziato per {{item}}",
|
||||||
|
"download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}",
|
||||||
|
"download_completed_for_item": "Scaricamento completato per {{item}}",
|
||||||
|
"queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione",
|
||||||
|
"failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}",
|
||||||
|
"server_responded_with_status_code": "Server responded with status {{statusCode}}",
|
||||||
|
"no_response_received_from_server": "No response received from the server",
|
||||||
|
"error_setting_up_the_request": "Error setting up the request",
|
||||||
|
"failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto",
|
||||||
|
"all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.",
|
||||||
|
"an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi",
|
||||||
|
"go_to_downloads": "Vai agli elementi scaricati"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"search_here": "Cerca qui...",
|
||||||
|
"search": "Cerca...",
|
||||||
|
"x_items": "{{count}} elementi",
|
||||||
|
"library": "Libreria",
|
||||||
|
"discover": "Scopri",
|
||||||
|
"no_results": "Nessun risultato",
|
||||||
|
"no_results_found_for": "Nessun risultato trovato per",
|
||||||
|
"movies": "Film",
|
||||||
|
"series": "Serie",
|
||||||
|
"episodes": "Episodi",
|
||||||
|
"collections": "Collezioni",
|
||||||
|
"actors": "Attori",
|
||||||
|
"request_movies": "Film Richiesti",
|
||||||
|
"request_series": "Serie Richieste",
|
||||||
|
"recently_added": "Aggiunti di Recente",
|
||||||
|
"recent_requests": "Richiesti di Recente",
|
||||||
|
"plex_watchlist": "Plex Watchlist",
|
||||||
|
"trending": "In tendenza",
|
||||||
|
"popular_movies": "Film Popolari",
|
||||||
|
"movie_genres": "Generi Film",
|
||||||
|
"upcoming_movies": "Film in arrivo",
|
||||||
|
"studios": "Studio",
|
||||||
|
"popular_tv": "Serie Popolari",
|
||||||
|
"tv_genres": "Generi Televisivi",
|
||||||
|
"upcoming_tv": "Serie in Arrivo",
|
||||||
|
"networks": "Network",
|
||||||
|
"tmdb_movie_keyword": "TMDB Parola chiave del film",
|
||||||
|
"tmdb_movie_genre": "TMDB Genere Film",
|
||||||
|
"tmdb_tv_keyword": "TMDB Parola chiave della serie",
|
||||||
|
"tmdb_tv_genre": "TMDB Genere Televisivo",
|
||||||
|
"tmdb_search": "TMDB Cerca",
|
||||||
|
"tmdb_studio": "TMDB Studio",
|
||||||
|
"tmdb_network": "TMDB Network",
|
||||||
|
"tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film",
|
||||||
|
"tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"no_items_found": "Nessun elemento trovato",
|
||||||
|
"no_results": "Nessun risultato",
|
||||||
|
"no_libraries_found": "Nessuna libreria trovata",
|
||||||
|
"item_types": {
|
||||||
|
"movies": "film",
|
||||||
|
"series": "serie TV",
|
||||||
|
"boxsets": "cofanetti",
|
||||||
|
"items": "elementi"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"display": "Display",
|
||||||
|
"row": "Fila",
|
||||||
|
"list": "Lista",
|
||||||
|
"image_style": "Stile dell'immagine",
|
||||||
|
"poster": "Poster",
|
||||||
|
"cover": "Cover",
|
||||||
|
"show_titles": "Mostra titoli",
|
||||||
|
"show_stats": "Mostra statistiche"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"genres": "Generi",
|
||||||
|
"years": "Anni",
|
||||||
|
"sort_by": "Ordina per",
|
||||||
|
"sort_order": "Criterio di ordinamento",
|
||||||
|
"tags": "Tag"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"series": "Serie TV",
|
||||||
|
"movies": "Film",
|
||||||
|
"episodes": "Episodi",
|
||||||
|
"videos": "Video",
|
||||||
|
"boxsets": "Boxset",
|
||||||
|
"playlists": "Playlist"
|
||||||
|
},
|
||||||
|
"custom_links": {
|
||||||
|
"no_links": "Nessun link"
|
||||||
|
},
|
||||||
|
"player": {
|
||||||
|
"error": "Errore",
|
||||||
|
"failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream",
|
||||||
|
"an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.",
|
||||||
|
"client_error": "Errore del client",
|
||||||
|
"could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast",
|
||||||
|
"message_from_server": "Messaggio dal server",
|
||||||
|
"video_has_finished_playing": "La riproduzione del video è terminata!",
|
||||||
|
"no_video_source": "Nessuna sorgente video...",
|
||||||
|
"next_episode": "Prossimo Episodio",
|
||||||
|
"refresh_tracks": "Aggiorna tracce",
|
||||||
|
"subtitle_tracks": "Tracce di sottotitoli:",
|
||||||
|
"audio_tracks": "Tracce audio:",
|
||||||
|
"playback_state": "Stato della riproduzione:",
|
||||||
|
"no_data_available": "Nessun dato disponibile",
|
||||||
|
"index": "Indice:"
|
||||||
|
},
|
||||||
|
"item_card": {
|
||||||
|
"next_up": "Il prossimo",
|
||||||
|
"no_items_to_display": "Nessun elemento da visualizzare",
|
||||||
|
"cast_and_crew": "Cast e Equipaggio",
|
||||||
|
"series": "Serie",
|
||||||
|
"seasons": "Stagioni",
|
||||||
|
"season": "Stagione",
|
||||||
|
"no_episodes_for_this_season": "Nessun episodio per questa stagione",
|
||||||
|
"overview": "Panoramica",
|
||||||
|
"more_with": "Altri con {{name}}",
|
||||||
|
"similar_items": "Elementi simili",
|
||||||
|
"no_similar_items_found": "Non sono stati trovati elementi simili",
|
||||||
|
"video": "Video",
|
||||||
|
"more_details": "Più dettagli",
|
||||||
|
"quality": "Qualità",
|
||||||
|
"audio": "Audio",
|
||||||
|
"subtitles": "Sottotitoli",
|
||||||
|
"show_more": "Mostra di più",
|
||||||
|
"show_less": "Mostra di meno",
|
||||||
|
"appeared_in": "Apparso in",
|
||||||
|
"could_not_load_item": "Impossibile caricare l'elemento",
|
||||||
|
"none": "Nessuno",
|
||||||
|
"download": {
|
||||||
|
"download_season": "Scarica Stagione",
|
||||||
|
"download_series": "Scarica Serie",
|
||||||
|
"download_episode": "Scarica Episodio",
|
||||||
|
"download_movie": "Scarica Film",
|
||||||
|
"download_x_item": "Scarica {{item_count}} elementi",
|
||||||
|
"download_button": "Scarica",
|
||||||
|
"using_optimized_server": "Utilizzando il server di ottimizzazione",
|
||||||
|
"using_default_method": "Utilizzando il metodo predefinito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"live_tv": {
|
||||||
|
"next": "Prossimo",
|
||||||
|
"previous": "Precedente",
|
||||||
|
"live_tv": "TV in diretta",
|
||||||
|
"coming_soon": "Prossimamente",
|
||||||
|
"on_now": "In onda ora",
|
||||||
|
"shows": "Programmi",
|
||||||
|
"movies": "Film",
|
||||||
|
"sports": "Sport",
|
||||||
|
"for_kids": "Per Bambini",
|
||||||
|
"news": "Notiziari"
|
||||||
|
},
|
||||||
|
"jellyseerr": {
|
||||||
|
"confirm": "Conferma",
|
||||||
|
"cancel": "Cancella",
|
||||||
|
"yes": "Si",
|
||||||
|
"whats_wrong": "Cosa c'è che non va?",
|
||||||
|
"issue_type": "Tipo di problema",
|
||||||
|
"select_an_issue": "Seleziona un problema",
|
||||||
|
"types": "Tipi",
|
||||||
|
"describe_the_issue": "(facoltativo) Descrivere il problema...",
|
||||||
|
"submit_button": "Invia",
|
||||||
|
"report_issue_button": "Segnalare il problema",
|
||||||
|
"request_button": "Richiedi",
|
||||||
|
"are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?",
|
||||||
|
"failed_to_login": "Accesso non riuscito",
|
||||||
|
"cast": "Cast",
|
||||||
|
"details": "Dettagli",
|
||||||
|
"status": "Stato",
|
||||||
|
"original_title": "Titolo originale",
|
||||||
|
"series_type": "Tipo di Serie",
|
||||||
|
"release_dates": "Date di Uscita",
|
||||||
|
"first_air_date": "Prima Data di Messa in Onda",
|
||||||
|
"next_air_date": "Prossima Data di Messa in Onda",
|
||||||
|
"revenue": "Ricavi",
|
||||||
|
"budget": "Budget",
|
||||||
|
"original_language": "Lingua Originale",
|
||||||
|
"production_country": "Paese di Produzione",
|
||||||
|
"studios": "Studio",
|
||||||
|
"network": "Network",
|
||||||
|
"currently_streaming_on": "Attualmente in streaming su",
|
||||||
|
"advanced": "Avanzate",
|
||||||
|
"request_as": "Richiedi Come",
|
||||||
|
"tags": "Tag",
|
||||||
|
"quality_profile": "Profilo qualità",
|
||||||
|
"root_folder": "Cartella radice",
|
||||||
|
"season_all": "Season (all)",
|
||||||
|
"season_number": "Stagione {{season_number}}",
|
||||||
|
"number_episodes": "{{episode_number}} Episodio",
|
||||||
|
"born": "Nato",
|
||||||
|
"appearances": "Aspetto",
|
||||||
|
"toasts": {
|
||||||
|
"jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.",
|
||||||
|
"jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.",
|
||||||
|
"failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr",
|
||||||
|
"issue_submitted": "Problema inviato!",
|
||||||
|
"requested_item": "Richiesto {{item}}!",
|
||||||
|
"you_dont_have_permission_to_request": "Non hai il permesso di richiedere!",
|
||||||
|
"something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"home": "Home",
|
||||||
|
"search": "Cerca",
|
||||||
|
"library": "Libreria",
|
||||||
|
"custom_links": "Collegamenti personalizzati",
|
||||||
|
"favorites": "Preferiti"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
463
translations/ja.json
Normal file
463
translations/ja.json
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
{
|
||||||
|
"login": {
|
||||||
|
"username_required": "ユーザー名は必須です",
|
||||||
|
"error_title": "エラー",
|
||||||
|
"login_title": "ログイン",
|
||||||
|
"login_to_title": "ログイン先",
|
||||||
|
"username_placeholder": "ユーザー名",
|
||||||
|
"password_placeholder": "パスワード",
|
||||||
|
"login_button": "ログイン",
|
||||||
|
"quick_connect": "クイックコネクト",
|
||||||
|
"enter_code_to_login": "ログインするにはコード {{code}} を入力してください",
|
||||||
|
"failed_to_initiate_quick_connect": "クイックコネクトを開始できませんでした",
|
||||||
|
"got_it": "了解",
|
||||||
|
"connection_failed": "接続に失敗しました",
|
||||||
|
"could_not_connect_to_server": "サーバーに接続できませんでした。URLとネットワーク接続を確認してください。",
|
||||||
|
"an_unexpected_error_occured": "予期しないエラーが発生しました",
|
||||||
|
"change_server": "サーバーの変更",
|
||||||
|
"invalid_username_or_password": "ユーザー名またはパスワードが無効です",
|
||||||
|
"user_does_not_have_permission_to_log_in": "ユーザーにログイン権限がありません",
|
||||||
|
"server_is_taking_too_long_to_respond_try_again_later": "サーバーの応答に時間がかかりすぎています。しばらくしてからもう一度お試しください。",
|
||||||
|
"server_received_too_many_requests_try_again_later": "サーバーにリクエストが多すぎます。後でもう一度お試しください。",
|
||||||
|
"there_is_a_server_error": "サーバーエラーが発生しました",
|
||||||
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "予期しないエラーが発生しました。サーバーのURLを正しく入力しましたか?"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"enter_url_to_jellyfin_server": "JellyfinサーバーのURLを入力してください",
|
||||||
|
"server_url_placeholder": "http(s)://your-server.com",
|
||||||
|
"connect_button": "接続",
|
||||||
|
"previous_servers": "前のサーバー",
|
||||||
|
"clear_button": "クリア",
|
||||||
|
"search_for_local_servers": "ローカルサーバーを検索",
|
||||||
|
"searching": "検索中...",
|
||||||
|
"servers": "サーバー"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"no_internet": "インターネット接続がありません",
|
||||||
|
"no_items": "アイテムはありません",
|
||||||
|
"no_internet_message": "心配しないでください。\nダウンロードしたコンテンツは引き続き視聴できます。",
|
||||||
|
"go_to_downloads": "ダウンロードに移動",
|
||||||
|
"oops": "おっと!",
|
||||||
|
"error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。",
|
||||||
|
"continue_watching": "続きを見る",
|
||||||
|
"next_up": "次の動画",
|
||||||
|
"recently_added_in": "{{libraryName}}に最近追加された",
|
||||||
|
"suggested_movies": "おすすめ映画",
|
||||||
|
"suggested_episodes": "おすすめエピソード",
|
||||||
|
"intro": {
|
||||||
|
"welcome_to_streamyfin": "Streamyfinへようこそ",
|
||||||
|
"a_free_and_open_source_client_for_jellyfin": "Jellyfinのためのフリーでオープンソースのクライアント。",
|
||||||
|
"features_title": "特長",
|
||||||
|
"features_description": "Streamyfinには多くの機能があり、設定メニューで見つけることができるさまざまなソフトウェアと統合されています。これには以下が含まれます。",
|
||||||
|
"jellyseerr_feature_description": "Jellyseerrインスタンスに接続し、アプリ内で直接映画をリクエストします。",
|
||||||
|
"downloads_feature_title": "ダウンロード",
|
||||||
|
"downloads_feature_description": "映画やテレビ番組をダウンロードしてオフラインで視聴します。デフォルトの方法を使用するか、バックグラウンドでファイルをダウンロードするために最適化されたサーバーをインストールしてください。",
|
||||||
|
"chromecast_feature_description": "映画とテレビ番組をChromecastデバイスにキャストします。",
|
||||||
|
"centralised_settings_plugin_title": "集中設定プラグイン",
|
||||||
|
"centralised_settings_plugin_description": "Jellyfinサーバーから設定を構成します。すべてのユーザーのすべてのクライアント設定は自動的に同期されます。",
|
||||||
|
"done_button": "完了",
|
||||||
|
"go_to_settings_button": "設定に移動",
|
||||||
|
"read_more": "続きを読む"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"settings_title": "設定",
|
||||||
|
"log_out_button": "ログアウト",
|
||||||
|
"user_info": {
|
||||||
|
"user_info_title": "ユーザー情報",
|
||||||
|
"user": "ユーザー",
|
||||||
|
"server": "サーバー",
|
||||||
|
"token": "トークン",
|
||||||
|
"app_version": "アプリバージョン"
|
||||||
|
},
|
||||||
|
"quick_connect": {
|
||||||
|
"quick_connect_title": "クイックコネクト",
|
||||||
|
"authorize_button": "クイックコネクトを承認する",
|
||||||
|
"enter_the_quick_connect_code": "クイックコネクトコードを入力...",
|
||||||
|
"success": "成功しました",
|
||||||
|
"quick_connect_autorized": "クイックコネクトが承認されました",
|
||||||
|
"error": "エラー",
|
||||||
|
"invalid_code": "無効なコードです",
|
||||||
|
"authorize": "承認"
|
||||||
|
},
|
||||||
|
"media_controls": {
|
||||||
|
"media_controls_title": "メディアコントロール",
|
||||||
|
"forward_skip_length": "スキップの長さ",
|
||||||
|
"rewind_length": "巻き戻しの長さ",
|
||||||
|
"seconds_unit": "s"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"audio_title": "オーディオ",
|
||||||
|
"set_audio_track": "前のアイテムからオーディオトラックを設定",
|
||||||
|
"audio_language": "オーディオ言語",
|
||||||
|
"audio_hint": "デフォルトのオーディオ言語を選択します。",
|
||||||
|
"none": "なし",
|
||||||
|
"language": "言語"
|
||||||
|
},
|
||||||
|
"subtitles": {
|
||||||
|
"subtitle_title": "字幕",
|
||||||
|
"subtitle_language": "字幕の言語",
|
||||||
|
"subtitle_mode": "字幕モード",
|
||||||
|
"set_subtitle_track": "前のアイテムから字幕トラックを設定",
|
||||||
|
"subtitle_size": "字幕サイズ",
|
||||||
|
"subtitle_hint": "字幕設定を構成します。",
|
||||||
|
"none": "なし",
|
||||||
|
"language": "言語",
|
||||||
|
"loading": "ロード中",
|
||||||
|
"modes": {
|
||||||
|
"Default": "デフォルト",
|
||||||
|
"Smart": "スマート",
|
||||||
|
"Always": "常に",
|
||||||
|
"None": "なし",
|
||||||
|
"OnlyForced": "強制のみ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"other": {
|
||||||
|
"other_title": "その他",
|
||||||
|
"auto_rotate": "画面の自動回転",
|
||||||
|
"video_orientation": "動画の向き",
|
||||||
|
"orientation": "向き",
|
||||||
|
"orientations": {
|
||||||
|
"DEFAULT": "デフォルト",
|
||||||
|
"ALL": "すべて",
|
||||||
|
"PORTRAIT": "縦",
|
||||||
|
"PORTRAIT_UP": "縦向き(上)",
|
||||||
|
"PORTRAIT_DOWN": "縦方向",
|
||||||
|
"LANDSCAPE": "横方向",
|
||||||
|
"LANDSCAPE_LEFT": "横方向 左",
|
||||||
|
"LANDSCAPE_RIGHT": "横方向 右",
|
||||||
|
"OTHER": "その他",
|
||||||
|
"UNKNOWN": "不明"
|
||||||
|
},
|
||||||
|
"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": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。",
|
||||||
|
"disable_haptic_feedback": "触覚フィードバックを無効にする"
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"downloads_title": "ダウンロード",
|
||||||
|
"download_method": "ダウンロード方法",
|
||||||
|
"remux_max_download": "Remux最大ダウンロード数",
|
||||||
|
"auto_download": "自動ダウンロード",
|
||||||
|
"optimized_versions_server": "Optimized versionsサーバー",
|
||||||
|
"save_button": "保存",
|
||||||
|
"optimized_server": "Optimizedサーバー",
|
||||||
|
"optimized": "最適化",
|
||||||
|
"default": "デフォルト",
|
||||||
|
"optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。",
|
||||||
|
"read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。",
|
||||||
|
"url": "URL",
|
||||||
|
"server_url_placeholder": "http(s)://domain.org:ポート"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"plugins_title": "プラグイン",
|
||||||
|
"jellyseerr": {
|
||||||
|
"jellyseerr_warning": "この統合はまだ初期段階です。状況が変化する可能性があります。",
|
||||||
|
"server_url": "サーバーURL",
|
||||||
|
"server_url_hint": "例: http(s)://your-host.url\n(必要に応じてポートを追加)",
|
||||||
|
"server_url_placeholder": "Jellyseerr URL...",
|
||||||
|
"password": "パスワード",
|
||||||
|
"password_placeholder": "Jellyfinユーザー {{username}} のパスワードを入力してください",
|
||||||
|
"save_button": "保存",
|
||||||
|
"clear_button": "クリア",
|
||||||
|
"login_button": "ログイン",
|
||||||
|
"total_media_requests": "メディアリクエストの合計",
|
||||||
|
"movie_quota_limit": "映画のクオータ制限",
|
||||||
|
"movie_quota_days": "映画のクオータ日数",
|
||||||
|
"tv_quota_limit": "テレビのクオータ制限",
|
||||||
|
"tv_quota_days": "テレビのクオータ日数",
|
||||||
|
"reset_jellyseerr_config_button": "Jellyseerrの設定をリセット",
|
||||||
|
"unlimited": "無制限",
|
||||||
|
"plus_n_more": "+{{n}} more"
|
||||||
|
},
|
||||||
|
"marlin_search": {
|
||||||
|
"enable_marlin_search": "マーリン検索を有効にする ",
|
||||||
|
"url": "URL",
|
||||||
|
"server_url_placeholder": "http(s)://domain.org:ポート",
|
||||||
|
"marlin_search_hint": "MarlinサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。",
|
||||||
|
"read_more_about_marlin": "Marlinについて詳しく読む。",
|
||||||
|
"save_button": "保存",
|
||||||
|
"toasts": {
|
||||||
|
"saved": "保存しました"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"storage_title": "ストレージ",
|
||||||
|
"app_usage": "アプリ {{usedSpace}}%",
|
||||||
|
"phone_usage": "電話 {{availableSpace}}%",
|
||||||
|
"size_used": "{{used}} / {{total}} 使用済み",
|
||||||
|
"delete_all_downloaded_files": "すべてのダウンロードファイルを削除"
|
||||||
|
},
|
||||||
|
"intro": {
|
||||||
|
"show_intro": "イントロを表示",
|
||||||
|
"reset_intro": "イントロをリセット"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"logs_title": "ログ",
|
||||||
|
"no_logs_available": "ログがありません",
|
||||||
|
"delete_all_logs": "すべてのログを削除"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"title": "言語",
|
||||||
|
"app_language": "アプリの言語",
|
||||||
|
"app_language_description": "アプリの言語を選択。",
|
||||||
|
"system": "システム"
|
||||||
|
},
|
||||||
|
"toasts": {
|
||||||
|
"error_deleting_files": "ファイルの削除エラー",
|
||||||
|
"background_downloads_enabled": "バックグラウンドでのダウンロードは有効です",
|
||||||
|
"background_downloads_disabled": "バックグラウンドでのダウンロードは無効です",
|
||||||
|
"connected": "接続済み",
|
||||||
|
"could_not_connect": "接続できません",
|
||||||
|
"invalid_url": "無効なURL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"downloads_title": "ダウンロード",
|
||||||
|
"tvseries": "TVシリーズ",
|
||||||
|
"movies": "映画",
|
||||||
|
"queue": "キュー",
|
||||||
|
"queue_hint": "アプリを再起動するとキューとダウンロードは失われます",
|
||||||
|
"no_items_in_queue": "キューにアイテムがありません",
|
||||||
|
"no_downloaded_items": "ダウンロードしたアイテムはありません",
|
||||||
|
"delete_all_movies_button": "すべての映画を削除",
|
||||||
|
"delete_all_tvseries_button": "すべてのシリーズを削除",
|
||||||
|
"delete_all_button": "すべて削除",
|
||||||
|
"active_download": "アクティブなダウンロード",
|
||||||
|
"no_active_downloads": "アクティブなダウンロードはありません",
|
||||||
|
"active_downloads": "アクティブなダウンロード",
|
||||||
|
"new_app_version_requires_re_download": "新しいアプリバージョンでは再ダウンロードが必要です",
|
||||||
|
"new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。",
|
||||||
|
"back": "戻る",
|
||||||
|
"delete": "削除",
|
||||||
|
"something_went_wrong": "問題が発生しました",
|
||||||
|
"could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした",
|
||||||
|
"eta": "ETA {{eta}}",
|
||||||
|
"methods": "方法",
|
||||||
|
"toasts": {
|
||||||
|
"you_are_not_allowed_to_download_files": "ファイルをダウンロードする権限がありません。",
|
||||||
|
"deleted_all_movies_successfully": "すべての映画を正常に削除しました!",
|
||||||
|
"failed_to_delete_all_movies": "すべての映画を削除できませんでした",
|
||||||
|
"deleted_all_tvseries_successfully": "すべてのシリーズを正常に削除しました!",
|
||||||
|
"failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした",
|
||||||
|
"download_cancelled": "ダウンロードをキャンセルしました",
|
||||||
|
"could_not_cancel_download": "ダウンロードをキャンセルできませんでした",
|
||||||
|
"download_completed": "ダウンロードが完了しました",
|
||||||
|
"download_started_for": "{{item}}のダウンロードが開始されました",
|
||||||
|
"item_is_ready_to_be_downloaded": "{{item}}をダウンロードする準備ができました",
|
||||||
|
"download_stated_for_item": "{{item}}のダウンロードが開始されました",
|
||||||
|
"download_failed_for_item": "{{item}}のダウンロードに失敗しました - {{error}}",
|
||||||
|
"download_completed_for_item": "{{item}}のダウンロードが完了しました",
|
||||||
|
"queued_item_for_optimization": "{{item}}をoptimizeのキューに追加しました",
|
||||||
|
"failed_to_start_download_for_item": "{{item}}のダウンロードを開始できませんでした: {{message}}",
|
||||||
|
"server_responded_with_status_code": "サーバーはステータス{{statusCode}}で応答しました",
|
||||||
|
"no_response_received_from_server": "サーバーからの応答がありません",
|
||||||
|
"error_setting_up_the_request": "リクエストの設定中にエラーが発生しました",
|
||||||
|
"failed_to_start_download_for_item_unexpected_error": "{{item}}のダウンロードを開始できませんでした: 予期しないエラーが発生しました",
|
||||||
|
"all_files_folders_and_jobs_deleted_successfully": "すべてのファイル、フォルダ、ジョブが正常に削除されました",
|
||||||
|
"an_error_occured_while_deleting_files_and_jobs": "ファイルとジョブの削除中にエラーが発生しました",
|
||||||
|
"go_to_downloads": "ダウンロードに移動"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"search_here": "ここを検索...",
|
||||||
|
"search": "検索...",
|
||||||
|
"x_items": "{{count}}のアイテム",
|
||||||
|
"library": "ライブラリ",
|
||||||
|
"discover": "見つける",
|
||||||
|
"no_results": "結果はありません",
|
||||||
|
"no_results_found_for": "結果が見つかりませんでした:",
|
||||||
|
"movies": "映画",
|
||||||
|
"series": "シリーズ",
|
||||||
|
"episodes": "エピソード",
|
||||||
|
"collections": "コレクション",
|
||||||
|
"actors": "俳優",
|
||||||
|
"request_movies": "映画をリクエスト",
|
||||||
|
"request_series": "シリーズをリクエスト",
|
||||||
|
"recently_added": "最近の追加",
|
||||||
|
"recent_requests": "最近のリクエスト",
|
||||||
|
"plex_watchlist": "Plexウォッチリスト",
|
||||||
|
"trending": "トレンド",
|
||||||
|
"popular_movies": "人気の映画",
|
||||||
|
"movie_genres": "映画のジャンル",
|
||||||
|
"upcoming_movies": "今後リリースされる映画",
|
||||||
|
"studios": "制作会社",
|
||||||
|
"popular_tv": "人気のテレビ番組",
|
||||||
|
"tv_genres": "シリーズのジャンル",
|
||||||
|
"upcoming_tv": "今後リリースされるシリーズ",
|
||||||
|
"networks": "ネットワーク",
|
||||||
|
"tmdb_movie_keyword": "TMDB映画キーワード",
|
||||||
|
"tmdb_movie_genre": "TMDB映画ジャンル",
|
||||||
|
"tmdb_tv_keyword": "TMDBシリーズキーワード",
|
||||||
|
"tmdb_tv_genre": "TMDBシリーズジャンル",
|
||||||
|
"tmdb_search": "TMDB検索",
|
||||||
|
"tmdb_studio": "TMDB 制作会社",
|
||||||
|
"tmdb_network": "TMDB ネットワーク",
|
||||||
|
"tmdb_movie_streaming_services": "TMDB映画ストリーミングサービス",
|
||||||
|
"tmdb_tv_streaming_services": "TMDBシリーズストリーミングサービス"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"no_items_found": "アイテムが見つかりません",
|
||||||
|
"no_results": "検索結果はありません",
|
||||||
|
"no_libraries_found": "ライブラリが見つかりません",
|
||||||
|
"item_types": {
|
||||||
|
"movies": "映画",
|
||||||
|
"series": "シリーズ",
|
||||||
|
"boxsets": "ボックスセット",
|
||||||
|
"items": "アイテム"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"display": "表示",
|
||||||
|
"row": "行",
|
||||||
|
"list": "リスト",
|
||||||
|
"image_style": "画像のスタイル",
|
||||||
|
"poster": "ポスター",
|
||||||
|
"cover": "カバー",
|
||||||
|
"show_titles": "タイトルの表示",
|
||||||
|
"show_stats": "統計を表示"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"genres": "ジャンル",
|
||||||
|
"years": "年",
|
||||||
|
"sort_by": "ソート",
|
||||||
|
"sort_order": "ソート順",
|
||||||
|
"tags": "タグ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"series": "シリーズ",
|
||||||
|
"movies": "映画",
|
||||||
|
"episodes": "エピソード",
|
||||||
|
"videos": "ビデオ",
|
||||||
|
"boxsets": "ボックスセット",
|
||||||
|
"playlists": "プレイリスト"
|
||||||
|
},
|
||||||
|
"custom_links": {
|
||||||
|
"no_links": "リンクがありません"
|
||||||
|
},
|
||||||
|
"player": {
|
||||||
|
"error": "エラー",
|
||||||
|
"failed_to_get_stream_url": "ストリームURLを取得できませんでした",
|
||||||
|
"an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。",
|
||||||
|
"client_error": "クライアントエラー",
|
||||||
|
"could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした",
|
||||||
|
"message_from_server": "サーバーからのメッセージ",
|
||||||
|
"video_has_finished_playing": "ビデオの再生が終了しました!",
|
||||||
|
"no_video_source": "動画ソースがありません...",
|
||||||
|
"next_episode": "次のエピソード",
|
||||||
|
"refresh_tracks": "トラックを更新",
|
||||||
|
"subtitle_tracks": "字幕トラック:",
|
||||||
|
"audio_tracks": "音声トラック:",
|
||||||
|
"playback_state": "再生状態:",
|
||||||
|
"no_data_available": "データなし",
|
||||||
|
"index": "インデックス:"
|
||||||
|
},
|
||||||
|
"item_card": {
|
||||||
|
"next_up": "次",
|
||||||
|
"no_items_to_display": "表示するアイテムがありません",
|
||||||
|
"cast_and_crew": "キャスト&クルー",
|
||||||
|
"series": "シリーズ",
|
||||||
|
"seasons": "シーズン",
|
||||||
|
"season": "シーズン",
|
||||||
|
"no_episodes_for_this_season": "このシーズンのエピソードはありません",
|
||||||
|
"overview": "ストーリー",
|
||||||
|
"more_with": "{{name}}の詳細",
|
||||||
|
"similar_items": "類似アイテム",
|
||||||
|
"no_similar_items_found": "類似のアイテムは見つかりませんでした",
|
||||||
|
"video": "映像",
|
||||||
|
"more_details": "さらに詳細を表示",
|
||||||
|
"quality": "画質",
|
||||||
|
"audio": "音声",
|
||||||
|
"subtitles": "字幕",
|
||||||
|
"show_more": "もっと見る",
|
||||||
|
"show_less": "少なく表示",
|
||||||
|
"appeared_in": "出演作品",
|
||||||
|
"could_not_load_item": "アイテムを読み込めませんでした",
|
||||||
|
"none": "なし",
|
||||||
|
"download": {
|
||||||
|
"download_season": "シーズンをダウンロード",
|
||||||
|
"download_series": "シリーズをダウンロード",
|
||||||
|
"download_episode": "エピソードをダウンロード",
|
||||||
|
"download_movie": "映画をダウンロード",
|
||||||
|
"download_x_item": "{{item_count}}のアイテムをダウンロード",
|
||||||
|
"download_button": "ダウンロード",
|
||||||
|
"using_optimized_server": "Optimizeサーバーを使用する",
|
||||||
|
"using_default_method": "デフォルトの方法を使用"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"live_tv": {
|
||||||
|
"next": "次",
|
||||||
|
"previous": "前",
|
||||||
|
"live_tv": "ライブTV",
|
||||||
|
"coming_soon": "近日公開",
|
||||||
|
"on_now": "現在",
|
||||||
|
"shows": "表示",
|
||||||
|
"movies": "映画",
|
||||||
|
"sports": "スポーツ",
|
||||||
|
"for_kids": "子供向け",
|
||||||
|
"news": "ニュース"
|
||||||
|
},
|
||||||
|
"jellyseerr": {
|
||||||
|
"confirm": "確認",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"yes": "はい",
|
||||||
|
"whats_wrong": "どうしましたか?",
|
||||||
|
"issue_type": "問題の種類",
|
||||||
|
"select_an_issue": "問題を選択",
|
||||||
|
"types": "種類",
|
||||||
|
"describe_the_issue": "(オプション) 問題を説明してください...",
|
||||||
|
"submit_button": "送信",
|
||||||
|
"report_issue_button": "チケットを報告",
|
||||||
|
"request_button": "リクエスト",
|
||||||
|
"are_you_sure_you_want_to_request_all_seasons": "すべてのシーズンをリクエストしてもよろしいですか?",
|
||||||
|
"failed_to_login": "ログインに失敗しました",
|
||||||
|
"cast": "出演者",
|
||||||
|
"details": "詳細",
|
||||||
|
"status": "状態",
|
||||||
|
"original_title": "原題",
|
||||||
|
"series_type": "シリーズタイプ",
|
||||||
|
"release_dates": "公開日",
|
||||||
|
"first_air_date": "初放送日",
|
||||||
|
"next_air_date": "次回放送日",
|
||||||
|
"revenue": "収益",
|
||||||
|
"budget": "予算",
|
||||||
|
"original_language": "オリジナルの言語",
|
||||||
|
"production_country": "制作国",
|
||||||
|
"studios": "制作会社",
|
||||||
|
"network": "ネットワーク",
|
||||||
|
"currently_streaming_on": "ストリーミング中",
|
||||||
|
"advanced": "詳細",
|
||||||
|
"request_as": "別ユーザーとしてリクエスト",
|
||||||
|
"tags": "タグ",
|
||||||
|
"quality_profile": "画質プロファイル",
|
||||||
|
"root_folder": "ルートフォルダ",
|
||||||
|
"season_all": "Season (all)",
|
||||||
|
"season_number": "シーズン{{season_number}}",
|
||||||
|
"number_episodes": "エピソード{{episode_number}}",
|
||||||
|
"born": "生まれ",
|
||||||
|
"appearances": "出演",
|
||||||
|
"toasts": {
|
||||||
|
"jellyseer_does_not_meet_requirements": "Jellyseerrサーバーは最小バージョン要件を満たしていません。少なくとも 2.0.0 に更新してください。",
|
||||||
|
"jellyseerr_test_failed": "Jellyseerrテストに失敗しました。もう一度お試しください。",
|
||||||
|
"failed_to_test_jellyseerr_server_url": "JellyseerrサーバーのURLをテストに失敗しました",
|
||||||
|
"issue_submitted": "チケットを送信しました!",
|
||||||
|
"requested_item": "{{item}}をリクエスト!",
|
||||||
|
"you_dont_have_permission_to_request": "リクエストする権限がありません!",
|
||||||
|
"something_went_wrong_requesting_media": "メディアのリクエスト中に問題が発生しました。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"home": "ホーム",
|
||||||
|
"search": "検索",
|
||||||
|
"library": "ライブラリ",
|
||||||
|
"custom_links": "カスタムリンク",
|
||||||
|
"favorites": "お気に入り"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,13 +9,13 @@
|
|||||||
"login_button": "Aanmelden",
|
"login_button": "Aanmelden",
|
||||||
"quick_connect": "Snel Verbinden",
|
"quick_connect": "Snel Verbinden",
|
||||||
"enter_code_to_login": "Vul code {{code}} in om aan te melden",
|
"enter_code_to_login": "Vul code {{code}} in om aan te melden",
|
||||||
"failed_to_initiate_quick_connect": "Gefaald om Snel Verbinden op te starten",
|
"failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten",
|
||||||
"got_it": "Begrepen",
|
"got_it": "Begrepen",
|
||||||
"connection_failed": "Verbinding gefaald",
|
"connection_failed": "Verbinding mislukt",
|
||||||
"could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.",
|
"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",
|
"an_unexpected_error_occured": "Er is een onverwachte fout opgetreden",
|
||||||
"change_server": "Verander server",
|
"change_server": "Server wijzigen",
|
||||||
"invalid_username_or_password": "Ongeldige gebruikersnaam of wachtwoord",
|
"invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord",
|
||||||
"user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden",
|
"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_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",
|
"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",
|
"continue_watching": "Verder Kijken",
|
||||||
"next_up": "Volgende",
|
"next_up": "Volgende",
|
||||||
"recently_added_in": "Recent toegevoegd in {{libraryName}}",
|
"recently_added_in": "Recent toegevoegd in {{libraryName}}",
|
||||||
"suggested_movies": "Voorgestelde Films",
|
"suggested_movies": "Voorgestelde films",
|
||||||
"suggested_episodes": "Voorgestelde Afleveringen",
|
"suggested_episodes": "Voorgestelde Afleveringen",
|
||||||
"intro": {
|
"intro": {
|
||||||
"welcome_to_streamyfin": "Welkom bij Streamyfin",
|
"welcome_to_streamyfin": "Welkom bij Streamyfin",
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen",
|
"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.",
|
"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",
|
"done_button": "Gedaan",
|
||||||
"go_to_settings_button": "Go naar instellingen",
|
"go_to_settings_button": "Ga naar instellingen",
|
||||||
"read_more": "Lees meer"
|
"read_more": "Lees meer"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"media_controls": {
|
"media_controls": {
|
||||||
"media_controls_title": "Media Bedieningen",
|
"media_controls_title": "Media Bedieningen",
|
||||||
"forward_skip_length": "Duur voorwaarts overslaan",
|
"forward_skip_length": "Duur voorwaarts overslaan",
|
||||||
"rewind_length": "Duur terugspeolen",
|
"rewind_length": "Duur terugspoelen",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
"subtitles": {
|
"subtitles": {
|
||||||
"subtitle_title": "Ondertitels",
|
"subtitle_title": "Ondertitels",
|
||||||
"subtitle_language": "Ondertitel taal",
|
"subtitle_language": "Ondertitel taal",
|
||||||
"subtitle_mode": "Ondertitle Modus",
|
"subtitle_mode": "Ondertitelmodus",
|
||||||
"set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item",
|
"set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item",
|
||||||
"subtitle_size": "Ondertitel Grootte",
|
"subtitle_size": "Ondertitel Grootte",
|
||||||
"subtitle_hint": "Stel ondertitel voorkeuren in.",
|
"subtitle_hint": "Stel ondertitel voorkeuren in.",
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"Smart": "Slim",
|
"Smart": "Slim",
|
||||||
"Always": "Altijd",
|
"Always": "Altijd",
|
||||||
"None": "Geen",
|
"None": "Geen",
|
||||||
"OnlyForced": "Alleen Geforceeerd"
|
"OnlyForced": "Alleen Geforceerd"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"other": {
|
"other": {
|
||||||
@@ -128,26 +128,31 @@
|
|||||||
"OTHER": "Andere",
|
"OTHER": "Andere",
|
||||||
"UNKNOWN": "Onbekend"
|
"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",
|
"show_custom_menu_links": "Aangepaste menulinks tonen",
|
||||||
"hide_libraries": "Verberg Bibliotheken",
|
"hide_libraries": "Verberg Bibliotheken",
|
||||||
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheek tab en hoofdpagina onderdelen.",
|
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
|
||||||
"disable_haptic_feedback": "Haptische feedback uitschakelen",
|
"disable_haptic_feedback": "Haptische feedback uitschakelen",
|
||||||
"default_quality": "Standaard kwaliteit"
|
"default_quality": "Standaard kwaliteit"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"download_method": "Download methode",
|
"download_method": "Download methode",
|
||||||
"remux_max_download": "Remux max download",
|
"remux_max_download": "Maximale Remux-download",
|
||||||
"auto_download": "Auto download",
|
"auto_download": "Auto download",
|
||||||
"optimized_versions_server": "Geoptimaliseerde server versies",
|
"optimized_versions_server": "Geoptimaliseerde server versies",
|
||||||
"save_button": "Opslaan",
|
"save_button": "Opslaan",
|
||||||
"optimized_server": "Geoptimailseerde Server",
|
"optimized_server": "Geoptimaliseerde Server",
|
||||||
"optimized": "Geoptimaliseerd",
|
"optimized": "Geoptimaliseerd",
|
||||||
"default": "Standaard",
|
"default": "Standaard",
|
||||||
"optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.",
|
"optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.",
|
||||||
"read_more_about_optimized_server": "Lees meer over de optimalisatieserver.",
|
"read_more_about_optimized_server": "Lees meer over de optimalisatieserver.",
|
||||||
"url":"URL",
|
"url": "URL",
|
||||||
"server_url_placeholder": "http(s)://domein.org:poort"
|
"server_url_placeholder": "http(s)://domein.org:poort"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
@@ -161,17 +166,18 @@
|
|||||||
"password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}",
|
"password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}",
|
||||||
"save_button": "Opslaan",
|
"save_button": "Opslaan",
|
||||||
"clear_button": "Wissen",
|
"clear_button": "Wissen",
|
||||||
"login_button": "Aannmelden",
|
"login_button": "Aanmelden",
|
||||||
"total_media_requests": "Totaal aantal mediaverzoeken",
|
"total_media_requests": "Totaal aantal mediaverzoeken",
|
||||||
"movie_quota_limit": "Limiet filmquota",
|
"movie_quota_limit": "Limiet filmquota",
|
||||||
"movie_quota_days": "Filmquota dagen",
|
"movie_quota_days": "Filmquota dagen",
|
||||||
"tv_quota_limit": "Limiet serie quota",
|
"tv_quota_limit": "Limiet serie quota",
|
||||||
"tv_quota_days": "Serie Quota dagen",
|
"tv_quota_days": "Serie Quota dagen",
|
||||||
"reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen",
|
"reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen",
|
||||||
"unlimited": "Ongelimiteerd"
|
"unlimited": "Ongelimiteerd",
|
||||||
|
"plus_n_more": "+{{n}} more"
|
||||||
},
|
},
|
||||||
"marlin_search": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "Marlin Search inschakeln ",
|
"enable_marlin_search": "Marlin Search inschakelen ",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"server_url_placeholder": "http(s)://domein.org:poort",
|
"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.",
|
"marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.",
|
||||||
@@ -204,8 +210,8 @@
|
|||||||
"app_language_description": "Selecteer een taal voor de app.",
|
"app_language_description": "Selecteer een taal voor de app.",
|
||||||
"system": "Systeem"
|
"system": "Systeem"
|
||||||
},
|
},
|
||||||
"toasts":{
|
"toasts": {
|
||||||
"error_deleting_files": "Fout bij het verwijden van bestanden",
|
"error_deleting_files": "Fout bij het verwijderen van bestanden",
|
||||||
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
|
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
|
||||||
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld",
|
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld",
|
||||||
"connected": "Verbonden",
|
"connected": "Verbonden",
|
||||||
@@ -237,7 +243,7 @@
|
|||||||
"methods": "Methoden",
|
"methods": "Methoden",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.",
|
"you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.",
|
||||||
"deleted_all_movies_successfully": "Alle filns succesvol verwijderd!",
|
"deleted_all_movies_successfully": "Alle films succesvol verwijderd!",
|
||||||
"failed_to_delete_all_movies": "Alle films zijn niet verwijderd",
|
"failed_to_delete_all_movies": "Alle films zijn niet verwijderd",
|
||||||
"deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!",
|
"deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!",
|
||||||
"failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd",
|
"failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd",
|
||||||
@@ -280,18 +286,18 @@
|
|||||||
"recent_requests": "Recent Aangevraagd",
|
"recent_requests": "Recent Aangevraagd",
|
||||||
"plex_watchlist": "Plex Kijklijst",
|
"plex_watchlist": "Plex Kijklijst",
|
||||||
"trending": "Trending",
|
"trending": "Trending",
|
||||||
"popular_movies": "Populaire Films",
|
"popular_movies": "Populaire films",
|
||||||
"movie_genres": "Film Genres",
|
"movie_genres": "Film Genres",
|
||||||
"upcoming_movies": "Aankomende Movies",
|
"upcoming_movies": "Aankomende films",
|
||||||
"studios": "Studios",
|
"studios": "Studios",
|
||||||
"popular_tv": "Populaire TV",
|
"popular_tv": "Populaire TV",
|
||||||
"tv_genres": "TV Genres",
|
"tv_genres": "TV Genres",
|
||||||
"upcoming_tv": "Opkomend TV",
|
"upcoming_tv": "Aankomende TV",
|
||||||
"networks": "Netwerken",
|
"networks": "Netwerken",
|
||||||
"tmdb_movie_keyword": "TMDB Film Trefwoord",
|
"tmdb_movie_keyword": "TMDB Film Trefwoord",
|
||||||
"tmdb_movie_genre": "TMDB Film Genre",
|
"tmdb_movie_genre": "TMDB Filmgenres",
|
||||||
"tmdb_tv_keyword": "TMDB TV Trefwoord",
|
"tmdb_tv_keyword": "TMDB TV Trefwoord",
|
||||||
"tmdb_tv_genre": "TMDB TV Genre",
|
"tmdb_tv_genre": "TMDB TV-Genres",
|
||||||
"tmdb_search": "TMDB Zoeken",
|
"tmdb_search": "TMDB Zoeken",
|
||||||
"tmdb_studio": "TMDB Studio",
|
"tmdb_studio": "TMDB Studio",
|
||||||
"tmdb_network": "TMDB Netwerk",
|
"tmdb_network": "TMDB Netwerk",
|
||||||
@@ -303,9 +309,9 @@
|
|||||||
"no_results": "Geen resultaten",
|
"no_results": "Geen resultaten",
|
||||||
"no_libraries_found": "Geen bibliotheken gevonden",
|
"no_libraries_found": "Geen bibliotheken gevonden",
|
||||||
"item_types": {
|
"item_types": {
|
||||||
"movies": "films",
|
"movies": "Films",
|
||||||
"series": "series",
|
"series": "Series",
|
||||||
"boxsets": "box sets",
|
"boxsets": "Boxsets",
|
||||||
"items": "items"
|
"items": "items"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -343,9 +349,9 @@
|
|||||||
"an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.",
|
"an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.",
|
||||||
"client_error": "Fout van de client",
|
"client_error": "Fout van de client",
|
||||||
"could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast",
|
"could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast",
|
||||||
"message_from_server": "Bericht van de server: {{message}}",
|
"message_from_server": "Bericht van de server",
|
||||||
"video_has_finished_playing": "Video is gedaan met spelen!",
|
"video_has_finished_playing": "Video is gedaan met spelen!",
|
||||||
"no_video_source": "Geen video bron...",
|
"no_video_source": "Geen videobron...",
|
||||||
"next_episode": "Volgende Aflevering",
|
"next_episode": "Volgende Aflevering",
|
||||||
"refresh_tracks": "Tracks verversen",
|
"refresh_tracks": "Tracks verversen",
|
||||||
"subtitle_tracks": "Ondertitel Tracks:",
|
"subtitle_tracks": "Ondertitel Tracks:",
|
||||||
@@ -372,7 +378,7 @@
|
|||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Ondertitel",
|
"subtitles": "Ondertitel",
|
||||||
"show_more": "Toon meer",
|
"show_more": "Toon meer",
|
||||||
"show_less": "Toon minden",
|
"show_less": "Toon minder",
|
||||||
"appeared_in": "Verschenen in",
|
"appeared_in": "Verschenen in",
|
||||||
"could_not_load_item": "Kon item niet laden",
|
"could_not_load_item": "Kon item niet laden",
|
||||||
"none": "Geen",
|
"none": "Geen",
|
||||||
@@ -399,7 +405,7 @@
|
|||||||
"for_kids": "Voor kinderen",
|
"for_kids": "Voor kinderen",
|
||||||
"news": "Nieuws"
|
"news": "Nieuws"
|
||||||
},
|
},
|
||||||
"jellyseerr":{
|
"jellyseerr": {
|
||||||
"confirm": "Bevestig",
|
"confirm": "Bevestig",
|
||||||
"cancel": "Annuleer",
|
"cancel": "Annuleer",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
@@ -417,7 +423,7 @@
|
|||||||
"details": "Details",
|
"details": "Details",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"original_title": "Originele titel",
|
"original_title": "Originele titel",
|
||||||
"series_type": "Serie Type",
|
"series_type": "Serietype",
|
||||||
"release_dates": "Verschijningsdatums",
|
"release_dates": "Verschijningsdatums",
|
||||||
"first_air_date": "Eerste uitzenddatum",
|
"first_air_date": "Eerste uitzenddatum",
|
||||||
"next_air_date": "Volgende uitzenddatum",
|
"next_air_date": "Volgende uitzenddatum",
|
||||||
@@ -433,19 +439,19 @@
|
|||||||
"tags": "Labels",
|
"tags": "Labels",
|
||||||
"quality_profile": "Kwaliteitsprofiel",
|
"quality_profile": "Kwaliteitsprofiel",
|
||||||
"root_folder": "Hoofdmap",
|
"root_folder": "Hoofdmap",
|
||||||
"season_x": "Seizoen {{seasons}}",
|
"season_all": "Season (all)",
|
||||||
"season_number": "Seizoen {{season_number}}",
|
"season_number": "Seizoen {{season_number}}",
|
||||||
"number_episodes": "{{episode_number}} Afleveringen",
|
"number_episodes": "{{episode_number}} Afleveringen",
|
||||||
"born": "Geboren",
|
"born": "Geboren",
|
||||||
"appearances": "Verschijningen",
|
"appearances": "Verschijningen",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0",
|
||||||
"jellyseerr_test_failed": "Jellyseerr test gefaald. Probeer opnieuw.",
|
"jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.",
|
||||||
"failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url",
|
"failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url",
|
||||||
"issue_submitted": "Probleem ingediend!",
|
"issue_submitted": "Probleem ingediend!",
|
||||||
"requested_item": "{{item}} aangevraagd!",
|
"requested_item": "{{item}} aangevraagd!",
|
||||||
"you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!",
|
"you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!",
|
||||||
"something_went_wrong_requesting_media": "Er ging iets iets mis met het aavragen van media!"
|
"something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
|
|||||||
@@ -128,7 +128,12 @@
|
|||||||
"OTHER": "Diğer",
|
"OTHER": "Diğer",
|
||||||
"UNKNOWN": "Bilinmeyen"
|
"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",
|
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
|
||||||
"hide_libraries": "Kütüphaneleri Gizle",
|
"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.",
|
"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_limit": "TV kota limiti",
|
||||||
"tv_quota_days": "TV kota günleri",
|
"tv_quota_days": "TV kota günleri",
|
||||||
"reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla",
|
"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": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "Marlin Aramasını Etkinleştir ",
|
"enable_marlin_search": "Marlin Aramasını Etkinleştir ",
|
||||||
@@ -432,7 +438,7 @@
|
|||||||
"tags": "Etiketler",
|
"tags": "Etiketler",
|
||||||
"quality_profile": "Kalite Profili",
|
"quality_profile": "Kalite Profili",
|
||||||
"root_folder": "Kök Klasör",
|
"root_folder": "Kök Klasör",
|
||||||
"season_x": "Sezon {{seasons}}",
|
"season_all": "Season (all)",
|
||||||
"season_number": "Sezon {{season_number}}",
|
"season_number": "Sezon {{season_number}}",
|
||||||
"number_episodes": "Bölüm {{episode_number}}",
|
"number_episodes": "Bölüm {{episode_number}}",
|
||||||
"born": "Doğum",
|
"born": "Doğum",
|
||||||
|
|||||||
463
translations/zh-CN.json
Normal file
463
translations/zh-CN.json
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
{
|
||||||
|
"login": {
|
||||||
|
"username_required": "需要用户名",
|
||||||
|
"error_title": "错误",
|
||||||
|
"login_title": "登录",
|
||||||
|
"login_to_title": "登录至",
|
||||||
|
"username_placeholder": "用户名",
|
||||||
|
"password_placeholder": "密码",
|
||||||
|
"login_button": "登录",
|
||||||
|
"quick_connect": "快速连接",
|
||||||
|
"enter_code_to_login": "输入代码 {{code}} 以登录",
|
||||||
|
"failed_to_initiate_quick_connect": "无法启动快速连接",
|
||||||
|
"got_it": "了解",
|
||||||
|
"connection_failed": "连接失败",
|
||||||
|
"could_not_connect_to_server": "无法连接到服务器。请检查 URL 和您的网络连接。",
|
||||||
|
"an_unexpected_error_occured": "发生意外错误",
|
||||||
|
"change_server": "更改服务器",
|
||||||
|
"invalid_username_or_password": "无效的用户名或密码",
|
||||||
|
"user_does_not_have_permission_to_log_in": "用户没有登录权限",
|
||||||
|
"server_is_taking_too_long_to_respond_try_again_later": "服务器长时间未响应,请稍后再试",
|
||||||
|
"server_received_too_many_requests_try_again_later": "服务器收到过多请求,请稍后再试。",
|
||||||
|
"there_is_a_server_error": "服务器出错",
|
||||||
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "发生意外错误。您是否正确输入了服务器 URL?"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"enter_url_to_jellyfin_server": "输入您的 Jellyfin 服务器 URL",
|
||||||
|
"server_url_placeholder": "http(s)://your-server.com",
|
||||||
|
"connect_button": "连接",
|
||||||
|
"previous_servers": "上一个服务器",
|
||||||
|
"clear_button": "清除",
|
||||||
|
"search_for_local_servers": "搜索本地服务器",
|
||||||
|
"searching": "搜索中...",
|
||||||
|
"servers": "服务器"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"no_internet": "无网络",
|
||||||
|
"no_items": "无项目",
|
||||||
|
"no_internet_message": "别担心,您仍可以观看\n已下载的项目。",
|
||||||
|
"go_to_downloads": "前往下载",
|
||||||
|
"oops": "哎呀!",
|
||||||
|
"error_message": "出错了。\n请注销重新登录。",
|
||||||
|
"continue_watching": "继续观看",
|
||||||
|
"next_up": "下一个",
|
||||||
|
"recently_added_in": "最近添加于 {{libraryName}}",
|
||||||
|
"suggested_movies": "推荐电影",
|
||||||
|
"suggested_episodes": "推荐剧集",
|
||||||
|
"intro": {
|
||||||
|
"welcome_to_streamyfin": "欢迎来到 Streamyfin",
|
||||||
|
"a_free_and_open_source_client_for_jellyfin": "一个免费且开源的 Jellyfin 客户端。",
|
||||||
|
"features_title": "功能",
|
||||||
|
"features_description": "Streamyfin 拥有许多功能,并与多种服务整合,您可以在设置菜单中找到这些功能,包括:",
|
||||||
|
"jellyseerr_feature_description": "连接到您的 Jellyseerr 实例并直接在应用中请求电影。",
|
||||||
|
"downloads_feature_title": "下载",
|
||||||
|
"downloads_feature_description": "下载电影和节目以离线观看。使用默认方法或安装 Optimized Server 以在后台下载文件。",
|
||||||
|
"chromecast_feature_description": "将电影和节目投屏到您的 Chromecast 设备。",
|
||||||
|
"centralised_settings_plugin_title": "统一设置插件",
|
||||||
|
"centralised_settings_plugin_description": "从 Jellyfin 服务器上的统一位置改变设置。所有用户的所有客户端设置将会自动同步。",
|
||||||
|
"done_button": "完成",
|
||||||
|
"go_to_settings_button": "前往设置",
|
||||||
|
"read_more": "了解更多"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"settings_title": "设置",
|
||||||
|
"log_out_button": "登出",
|
||||||
|
"user_info": {
|
||||||
|
"user_info_title": "用户信息",
|
||||||
|
"user": "用户",
|
||||||
|
"server": "服务器",
|
||||||
|
"token": "密钥",
|
||||||
|
"app_version": "应用版本"
|
||||||
|
},
|
||||||
|
"quick_connect": {
|
||||||
|
"quick_connect_title": "快速连接",
|
||||||
|
"authorize_button": "授权快速连接",
|
||||||
|
"enter_the_quick_connect_code": "输入快速连接代码...",
|
||||||
|
"success": "成功",
|
||||||
|
"quick_connect_autorized": "快速连接已授权",
|
||||||
|
"error": "错误",
|
||||||
|
"invalid_code": "无效代码",
|
||||||
|
"authorize": "授权"
|
||||||
|
},
|
||||||
|
"media_controls": {
|
||||||
|
"media_controls_title": "媒体控制",
|
||||||
|
"forward_skip_length": "快进时长",
|
||||||
|
"rewind_length": "快退时长",
|
||||||
|
"seconds_unit": "秒"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"audio_title": "音频",
|
||||||
|
"set_audio_track": "从上一个项目设置音轨",
|
||||||
|
"audio_language": "音频语言",
|
||||||
|
"audio_hint": "选择默认音频语言。",
|
||||||
|
"none": "无",
|
||||||
|
"language": "语言"
|
||||||
|
},
|
||||||
|
"subtitles": {
|
||||||
|
"subtitle_title": "字幕",
|
||||||
|
"subtitle_language": "字幕语言",
|
||||||
|
"subtitle_mode": "字幕模式",
|
||||||
|
"set_subtitle_track": "从上一个项目设置字幕",
|
||||||
|
"subtitle_size": "字幕大小",
|
||||||
|
"subtitle_hint": "设置字幕偏好。",
|
||||||
|
"none": "无",
|
||||||
|
"language": "语言",
|
||||||
|
"loading": "加载中",
|
||||||
|
"modes": {
|
||||||
|
"Default": "默认",
|
||||||
|
"Smart": "智能",
|
||||||
|
"Always": "总是",
|
||||||
|
"None": "无",
|
||||||
|
"OnlyForced": "仅强制字幕"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"other": {
|
||||||
|
"other_title": "其他",
|
||||||
|
"auto_rotate": "自动旋转",
|
||||||
|
"video_orientation": "视频方向",
|
||||||
|
"orientation": "方向",
|
||||||
|
"orientations": {
|
||||||
|
"DEFAULT": "默认",
|
||||||
|
"ALL": "全部",
|
||||||
|
"PORTRAIT": "纵向",
|
||||||
|
"PORTRAIT_UP": "纵向向上",
|
||||||
|
"PORTRAIT_DOWN": "纵向向下",
|
||||||
|
"LANDSCAPE": "横向",
|
||||||
|
"LANDSCAPE_LEFT": "横向左",
|
||||||
|
"LANDSCAPE_RIGHT": "横向右",
|
||||||
|
"OTHER": "其他",
|
||||||
|
"UNKNOWN": "未知"
|
||||||
|
},
|
||||||
|
"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": "选择您想从媒体库页面和主页隐藏的媒体库。",
|
||||||
|
"disable_haptic_feedback": "禁用触觉反馈"
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"downloads_title": "下载",
|
||||||
|
"download_method": "下载方法",
|
||||||
|
"remux_max_download": "Remux 最大下载",
|
||||||
|
"auto_download": "自动下载",
|
||||||
|
"optimized_versions_server": "Optimized Version 服务器",
|
||||||
|
"save_button": "保存",
|
||||||
|
"optimized_server": "Optimized Server",
|
||||||
|
"optimized": "已优化",
|
||||||
|
"default": "默认",
|
||||||
|
"optimized_version_hint": "输入 Optimized Server 的 URL。URL 应包括 http(s) 和端口 (可选)。",
|
||||||
|
"read_more_about_optimized_server": "查看更多关于 Optimized Server 的信息。",
|
||||||
|
"url": "URL",
|
||||||
|
"server_url_placeholder": "http(s)://domain.org:port"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"plugins_title": "插件",
|
||||||
|
"jellyseerr": {
|
||||||
|
"jellyseerr_warning": "此插件处于早期阶段,功能可能会有变化。",
|
||||||
|
"server_url": "服务器 URL",
|
||||||
|
"server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)",
|
||||||
|
"server_url_placeholder": "Jellyseerr URL...",
|
||||||
|
"password": "密码",
|
||||||
|
"password_placeholder": "输入 Jellyfin 用户 {{username}} 的密码",
|
||||||
|
"save_button": "保存",
|
||||||
|
"clear_button": "清除",
|
||||||
|
"login_button": "登录",
|
||||||
|
"total_media_requests": "总媒体请求",
|
||||||
|
"movie_quota_limit": "电影配额限制",
|
||||||
|
"movie_quota_days": "电影配额天数",
|
||||||
|
"tv_quota_limit": "剧集配额限制",
|
||||||
|
"tv_quota_days": "剧集配额天数",
|
||||||
|
"reset_jellyseerr_config_button": "重置 Jellyseerr 设置",
|
||||||
|
"unlimited": "无限制",
|
||||||
|
"plus_n_more": "+{{n}} more"
|
||||||
|
},
|
||||||
|
"marlin_search": {
|
||||||
|
"enable_marlin_search": "启用 Marlin 搜索",
|
||||||
|
"url": "URL",
|
||||||
|
"server_url_placeholder": "http(s)://domain.org:port",
|
||||||
|
"marlin_search_hint": "输入 Marlin 服务器的 URL。URL 应包括 http(s) 和端口 (可选)。",
|
||||||
|
"read_more_about_marlin": "查看更多关于 Marlin 的信息。",
|
||||||
|
"save_button": "保存",
|
||||||
|
"toasts": {
|
||||||
|
"saved": "已保存"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"storage_title": "存储",
|
||||||
|
"app_usage": "应用 {{usedSpace}}%",
|
||||||
|
"device_usage": "设备 {{availableSpace}}%",
|
||||||
|
"size_used": "已使用 {{used}} / {{total}}",
|
||||||
|
"delete_all_downloaded_files": "删除所有已下载文件"
|
||||||
|
},
|
||||||
|
"intro": {
|
||||||
|
"show_intro": "显示介绍",
|
||||||
|
"reset_intro": "重置介绍"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"logs_title": "日志",
|
||||||
|
"no_logs_available": "无可用日志",
|
||||||
|
"delete_all_logs": "删除所有日志"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"title": "语言",
|
||||||
|
"app_language": "应用语言",
|
||||||
|
"app_language_description": "选择应用的语言。",
|
||||||
|
"system": "系统"
|
||||||
|
},
|
||||||
|
"toasts": {
|
||||||
|
"error_deleting_files": "删除文件时出错",
|
||||||
|
"background_downloads_enabled": "后台下载已启用",
|
||||||
|
"background_downloads_disabled": "后台下载已禁用",
|
||||||
|
"connected": "已连接",
|
||||||
|
"could_not_connect": "无法连接",
|
||||||
|
"invalid_url": "无效 URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"downloads_title": "下载",
|
||||||
|
"tvseries": "剧集",
|
||||||
|
"movies": "电影",
|
||||||
|
"queue": "队列",
|
||||||
|
"queue_hint": "应用重启后队列和下载将会丢失",
|
||||||
|
"no_items_in_queue": "队列中无项目",
|
||||||
|
"no_downloaded_items": "无已下载项目",
|
||||||
|
"delete_all_movies_button": "删除所有电影",
|
||||||
|
"delete_all_tvseries_button": "删除所有剧集",
|
||||||
|
"delete_all_button": "删除全部",
|
||||||
|
"active_download": "活跃下载",
|
||||||
|
"no_active_downloads": "无活跃下载",
|
||||||
|
"active_downloads": "活跃下载",
|
||||||
|
"new_app_version_requires_re_download": "更新版本需要重新下载",
|
||||||
|
"new_app_version_requires_re_download_description": "更新版本需要重新下载内容。请删除所有已下载项后重试。",
|
||||||
|
"back": "返回",
|
||||||
|
"delete": "删除",
|
||||||
|
"something_went_wrong": "出现问题",
|
||||||
|
"could_not_get_stream_url_from_jellyfin": "无法从 Jellyfin 获取串流 URL",
|
||||||
|
"eta": "预计完成时间 {{eta}}",
|
||||||
|
"methods": "方法",
|
||||||
|
"toasts": {
|
||||||
|
"you_are_not_allowed_to_download_files": "您无权下载文件。",
|
||||||
|
"deleted_all_movies_successfully": "成功删除所有电影!",
|
||||||
|
"failed_to_delete_all_movies": "删除所有电影失败",
|
||||||
|
"deleted_all_tvseries_successfully": "成功删除所有剧集!",
|
||||||
|
"failed_to_delete_all_tvseries": "删除所有剧集失败",
|
||||||
|
"download_cancelled": "下载已取消",
|
||||||
|
"could_not_cancel_download": "无法取消下载",
|
||||||
|
"download_completed": "下载完成",
|
||||||
|
"download_started_for": "开始下载 {{item}}",
|
||||||
|
"item_is_ready_to_be_downloaded": "{{item}} 准备好下载",
|
||||||
|
"download_stated_for_item": "开始下载 {{item}}",
|
||||||
|
"download_failed_for_item": "下载失败 {{item}} - {{error}}",
|
||||||
|
"download_completed_for_item": "下载完成 {{item}}",
|
||||||
|
"queued_item_for_optimization": "已将 {{item}} 队列进行优化",
|
||||||
|
"failed_to_start_download_for_item": "无法开始下载 {{item}}: {{message}}",
|
||||||
|
"server_responded_with_status_code": "服务器响应状态 {{statusCode}}",
|
||||||
|
"no_response_received_from_server": "未收到服务器响应",
|
||||||
|
"error_setting_up_the_request": "设置请求时出错",
|
||||||
|
"failed_to_start_download_for_item_unexpected_error": "无法开始下载 {{item}}: 发生意外错误",
|
||||||
|
"all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夹和任务成功删除",
|
||||||
|
"an_error_occured_while_deleting_files_and_jobs": "删除文件和任务时发生错误",
|
||||||
|
"go_to_downloads": "前往下载"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"search_here": "在此搜索...",
|
||||||
|
"search": "搜索...",
|
||||||
|
"x_items": "{{count}} 项目",
|
||||||
|
"library": "媒体库",
|
||||||
|
"discover": "发现",
|
||||||
|
"no_results": "没有结果",
|
||||||
|
"no_results_found_for": "未找到结果",
|
||||||
|
"movies": "电影",
|
||||||
|
"series": "剧集",
|
||||||
|
"episodes": "单集",
|
||||||
|
"collections": "收藏",
|
||||||
|
"actors": "演员",
|
||||||
|
"request_movies": "请求电影",
|
||||||
|
"request_series": "请求系列",
|
||||||
|
"recently_added": "最近添加",
|
||||||
|
"recent_requests": "最近请求",
|
||||||
|
"plex_watchlist": "Plex 观影清单",
|
||||||
|
"trending": "趋势",
|
||||||
|
"popular_movies": "热门电影",
|
||||||
|
"movie_genres": "电影类型",
|
||||||
|
"upcoming_movies": "即将上映的电影",
|
||||||
|
"studios": "工作室",
|
||||||
|
"popular_tv": "热门电影",
|
||||||
|
"tv_genres": "剧集类型",
|
||||||
|
"upcoming_tv": "即将上映的剧集",
|
||||||
|
"networks": "网络",
|
||||||
|
"tmdb_movie_keyword": "TMDB 电影关键词",
|
||||||
|
"tmdb_movie_genre": "TMDB 电影类型",
|
||||||
|
"tmdb_tv_keyword": "TMDB 剧集关键词",
|
||||||
|
"tmdb_tv_genre": "TMDB 剧集类型",
|
||||||
|
"tmdb_search": "TMDB 搜索",
|
||||||
|
"tmdb_studio": "TMDB 工作室",
|
||||||
|
"tmdb_network": "TMDB 网络",
|
||||||
|
"tmdb_movie_streaming_services": "TMDB 电影流媒体服务",
|
||||||
|
"tmdb_tv_streaming_services": "TMDB 剧集流媒体服务"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"no_items_found": "未找到项目",
|
||||||
|
"no_results": "没有结果",
|
||||||
|
"no_libraries_found": "未找到媒体库",
|
||||||
|
"item_types": {
|
||||||
|
"movies": "电影",
|
||||||
|
"series": "剧集",
|
||||||
|
"boxsets": "套装",
|
||||||
|
"items": "项"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"display": "显示",
|
||||||
|
"row": "行",
|
||||||
|
"list": "列表",
|
||||||
|
"image_style": "图片样式",
|
||||||
|
"poster": "海报",
|
||||||
|
"cover": "封面",
|
||||||
|
"show_titles": "显示标题",
|
||||||
|
"show_stats": "显示统计"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"genres": "类型",
|
||||||
|
"years": "年份",
|
||||||
|
"sort_by": "排序依据",
|
||||||
|
"sort_order": "排序顺序",
|
||||||
|
"tags": "标签"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"series": "剧集",
|
||||||
|
"movies": "电影",
|
||||||
|
"episodes": "单集",
|
||||||
|
"videos": "视频",
|
||||||
|
"boxsets": "套装",
|
||||||
|
"playlists": "播放列表"
|
||||||
|
},
|
||||||
|
"custom_links": {
|
||||||
|
"no_links": "无链接"
|
||||||
|
},
|
||||||
|
"player": {
|
||||||
|
"error": "错误",
|
||||||
|
"failed_to_get_stream_url": "无法获取流 URL",
|
||||||
|
"an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。",
|
||||||
|
"client_error": "客户端错误",
|
||||||
|
"could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流",
|
||||||
|
"message_from_server": "来自服务器的消息",
|
||||||
|
"video_has_finished_playing": "视频播放完成!",
|
||||||
|
"no_video_source": "无视频来源...",
|
||||||
|
"next_episode": "下一集",
|
||||||
|
"refresh_tracks": "刷新轨道",
|
||||||
|
"subtitle_tracks": "字幕轨道:",
|
||||||
|
"audio_tracks": "音频轨道:",
|
||||||
|
"playback_state": "播放状态:",
|
||||||
|
"no_data_available": "无可用数据",
|
||||||
|
"index": "索引:"
|
||||||
|
},
|
||||||
|
"item_card": {
|
||||||
|
"next_up": "下一个",
|
||||||
|
"no_items_to_display": "无项目显示",
|
||||||
|
"cast_and_crew": "演员和工作人员",
|
||||||
|
"series": "剧集",
|
||||||
|
"seasons": "季",
|
||||||
|
"season": "季",
|
||||||
|
"no_episodes_for_this_season": "本季无剧集",
|
||||||
|
"overview": "概览",
|
||||||
|
"more_with": "更多 {{name}} 的作品",
|
||||||
|
"similar_items": "类似项目",
|
||||||
|
"no_similar_items_found": "未找到类似项目",
|
||||||
|
"video": "视频",
|
||||||
|
"more_details": "更多详情",
|
||||||
|
"quality": "质量",
|
||||||
|
"audio": "音频",
|
||||||
|
"subtitles": "字幕",
|
||||||
|
"show_more": "显示更多",
|
||||||
|
"show_less": "显示更少",
|
||||||
|
"appeared_in": "出现于",
|
||||||
|
"could_not_load_item": "无法加载项目",
|
||||||
|
"none": "无",
|
||||||
|
"download": {
|
||||||
|
"download_season": "下载季",
|
||||||
|
"download_series": "下载剧集",
|
||||||
|
"download_episode": "下载单集",
|
||||||
|
"download_movie": "下载电影",
|
||||||
|
"download_x_item": "下载 {{item_count}} 项目",
|
||||||
|
"download_button": "下载",
|
||||||
|
"using_optimized_server": "使用 Optimized Server",
|
||||||
|
"using_default_method": "使用默认方法"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"live_tv": {
|
||||||
|
"next": "下一个",
|
||||||
|
"previous": "上一个",
|
||||||
|
"live_tv": "直播电视",
|
||||||
|
"coming_soon": "即将播出",
|
||||||
|
"on_now": "正在播放",
|
||||||
|
"shows": "节目",
|
||||||
|
"movies": "电影",
|
||||||
|
"sports": "体育",
|
||||||
|
"for_kids": "儿童",
|
||||||
|
"news": "新闻"
|
||||||
|
},
|
||||||
|
"jellyseerr": {
|
||||||
|
"confirm": "确认",
|
||||||
|
"cancel": "取消",
|
||||||
|
"yes": "是",
|
||||||
|
"whats_wrong": "出了什么问题?",
|
||||||
|
"issue_type": "问题类型",
|
||||||
|
"select_an_issue": "选择一个问题",
|
||||||
|
"types": "类型",
|
||||||
|
"describe_the_issue": "(可选)描述问题...",
|
||||||
|
"submit_button": "提交",
|
||||||
|
"report_issue_button": "报告问题",
|
||||||
|
"request_button": "请求",
|
||||||
|
"are_you_sure_you_want_to_request_all_seasons": "您确定要请求所有季度的剧集吗?",
|
||||||
|
"failed_to_login": "登录失败",
|
||||||
|
"cast": "演员",
|
||||||
|
"details": "详情",
|
||||||
|
"status": "状态",
|
||||||
|
"original_title": "原标题",
|
||||||
|
"series_type": "剧集类型",
|
||||||
|
"release_dates": "发行日期",
|
||||||
|
"first_air_date": "首次播出日期",
|
||||||
|
"next_air_date": "下次播出日期",
|
||||||
|
"revenue": "收入",
|
||||||
|
"budget": "预算",
|
||||||
|
"original_language": "原始语言",
|
||||||
|
"production_country": "制作国家/地区",
|
||||||
|
"studios": "工作室",
|
||||||
|
"network": "网络",
|
||||||
|
"currently_streaming_on": "目前在以下流媒体上播放",
|
||||||
|
"advanced": "高级设置",
|
||||||
|
"request_as": "选择用户以请求",
|
||||||
|
"tags": "标签",
|
||||||
|
"quality_profile": "质量配置文件",
|
||||||
|
"root_folder": "根文件夹",
|
||||||
|
"season_all": "Season (all)",
|
||||||
|
"season_number": "第 {{season_number}} 季",
|
||||||
|
"number_episodes": "{{episode_number}} 集",
|
||||||
|
"born": "出生",
|
||||||
|
"appearances": "出场",
|
||||||
|
"toasts": {
|
||||||
|
"jellyseer_does_not_meet_requirements": "Jellyseerr 服务器不符合最低版本要求!请使用 2.0.0 及以上版本",
|
||||||
|
"jellyseerr_test_failed": "Jellyseerr 测试失败。请重试。",
|
||||||
|
"failed_to_test_jellyseerr_server_url": "无法测试 Jellyseerr 服务器 URL",
|
||||||
|
"issue_submitted": "问题已提交!",
|
||||||
|
"requested_item": "已请求 {{item}}!",
|
||||||
|
"you_dont_have_permission_to_request": "您无权请求媒体!",
|
||||||
|
"something_went_wrong_requesting_media": "请求媒体时出了些问题!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"home": "主页",
|
||||||
|
"search": "搜索",
|
||||||
|
"library": "媒体库",
|
||||||
|
"custom_links": "自定义链接",
|
||||||
|
"favorites": "收藏"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
},
|
},
|
||||||
"media_controls": {
|
"media_controls": {
|
||||||
"media_controls_title": "媒體控制",
|
"media_controls_title": "媒體控制",
|
||||||
"forward_skip_length": "前進跳過長度",
|
"forward_skip_length": "快進秒數",
|
||||||
"rewind_length": "倒帶長度",
|
"rewind_length": "倒帶秒數",
|
||||||
"seconds_unit": "秒"
|
"seconds_unit": "秒"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"Smart": "智能",
|
"Smart": "智能",
|
||||||
"Always": "總是",
|
"Always": "總是",
|
||||||
"None": "無",
|
"None": "無",
|
||||||
"OnlyForced": "僅強制"
|
"OnlyForced": "僅強制字幕"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"other": {
|
"other": {
|
||||||
@@ -128,7 +128,12 @@
|
|||||||
"OTHER": "其他",
|
"OTHER": "其他",
|
||||||
"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_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
|
||||||
@@ -142,7 +147,7 @@
|
|||||||
"optimized_versions_server": "Optimized Version 伺服器",
|
"optimized_versions_server": "Optimized Version 伺服器",
|
||||||
"save_button": "保存",
|
"save_button": "保存",
|
||||||
"optimized_server": "Optimized Server",
|
"optimized_server": "Optimized Server",
|
||||||
"optimized": "優化",
|
"optimized": "已優化",
|
||||||
"default": "默認",
|
"default": "默認",
|
||||||
"optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。",
|
"optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。",
|
||||||
"read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。",
|
"read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。",
|
||||||
@@ -152,7 +157,7 @@
|
|||||||
"plugins": {
|
"plugins": {
|
||||||
"plugins_title": "插件",
|
"plugins_title": "插件",
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"jellyseerr_warning": "此集成處於早期階段。功能可能會有變化。",
|
"jellyseerr_warning": "此插件處於早期階段。功能可能會有變化。",
|
||||||
"server_url": "伺服器 URL",
|
"server_url": "伺服器 URL",
|
||||||
"server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)",
|
"server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)",
|
||||||
"server_url_placeholder": "Jellyseerr URL...",
|
"server_url_placeholder": "Jellyseerr URL...",
|
||||||
@@ -167,7 +172,8 @@
|
|||||||
"tv_quota_limit": "電視配額限制",
|
"tv_quota_limit": "電視配額限制",
|
||||||
"tv_quota_days": "電視配額天數",
|
"tv_quota_days": "電視配額天數",
|
||||||
"reset_jellyseerr_config_button": "重置 Jellyseerr 配置",
|
"reset_jellyseerr_config_button": "重置 Jellyseerr 配置",
|
||||||
"unlimited": "無限制"
|
"unlimited": "無限制",
|
||||||
|
"plus_n_more": "+{{n}} more"
|
||||||
},
|
},
|
||||||
"marlin_search": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "啟用 Marlin 搜索",
|
"enable_marlin_search": "啟用 Marlin 搜索",
|
||||||
@@ -342,7 +348,7 @@
|
|||||||
"an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。",
|
"an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。",
|
||||||
"client_error": "客戶端錯誤",
|
"client_error": "客戶端錯誤",
|
||||||
"could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流",
|
"could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流",
|
||||||
"message_from_server": "來自伺服器的消息:{{message}}",
|
"message_from_server": "來自伺服器的消息",
|
||||||
"video_has_finished_playing": "影片播放完畢!",
|
"video_has_finished_playing": "影片播放完畢!",
|
||||||
"no_video_source": "無影片來源...",
|
"no_video_source": "無影片來源...",
|
||||||
"next_episode": "下一集",
|
"next_episode": "下一集",
|
||||||
@@ -410,7 +416,7 @@
|
|||||||
"submit_button": "提交",
|
"submit_button": "提交",
|
||||||
"report_issue_button": "報告問題",
|
"report_issue_button": "報告問題",
|
||||||
"request_button": "請求",
|
"request_button": "請求",
|
||||||
"are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的節目嗎?",
|
"are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的劇集嗎?",
|
||||||
"failed_to_login": "登入失敗",
|
"failed_to_login": "登入失敗",
|
||||||
"cast": "演員",
|
"cast": "演員",
|
||||||
"details": "詳情",
|
"details": "詳情",
|
||||||
@@ -427,18 +433,18 @@
|
|||||||
"studios": "工作室",
|
"studios": "工作室",
|
||||||
"network": "網絡",
|
"network": "網絡",
|
||||||
"currently_streaming_on": "目前在以下流媒體上播放",
|
"currently_streaming_on": "目前在以下流媒體上播放",
|
||||||
"advanced": "高級",
|
"advanced": "高級設定",
|
||||||
"request_as": "請求為",
|
"request_as": "選擇用戶以作請求",
|
||||||
"tags": "標籤",
|
"tags": "標籤",
|
||||||
"quality_profile": "質量配置文件",
|
"quality_profile": "質量配置文件",
|
||||||
"root_folder": "根文件夾",
|
"root_folder": "根文件夾",
|
||||||
"season_x": "第 {{seasons}} 季",
|
"season_all": "Season (all)",
|
||||||
"season_number": "第 {{season_number}} 季",
|
"season_number": "第 {{season_number}} 季",
|
||||||
"number_episodes": "{{episode_number}} 集",
|
"number_episodes": "{{episode_number}} 集",
|
||||||
"born": "出生",
|
"born": "出生",
|
||||||
"appearances": "出場",
|
"appearances": "出場",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請更新至至少 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請使用 2.0.0 及以上版本。",
|
||||||
"jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。",
|
"jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。",
|
||||||
"failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL",
|
"failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL",
|
||||||
"issue_submitted": "問題已提交!",
|
"issue_submitted": "問題已提交!",
|
||||||
@@ -450,7 +456,7 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"home": "主頁",
|
"home": "主頁",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"library": "庫",
|
"library": "媒體庫",
|
||||||
"custom_links": "自定義鏈接",
|
"custom_links": "自定義鏈接",
|
||||||
"favorites": "收藏"
|
"favorites": "收藏"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { Bitrate, BITRATES } from "@/components/BitrateSelector";
|
import { Bitrate, BITRATES } from "@/components/BitrateSelector";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { writeInfoLog } from "@/utils/log";
|
import { writeInfoLog } from "@/utils/log";
|
||||||
|
import {Video} from "@/utils/jellyseerr/server/models/Movie";
|
||||||
|
|
||||||
const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004";
|
const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004";
|
||||||
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS";
|
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS";
|
||||||
@@ -112,6 +113,12 @@ export type HomeSectionNextUpResolver = {
|
|||||||
enableRewatching?: boolean;
|
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 = {
|
export type Settings = {
|
||||||
home?: Home | null;
|
home?: Home | null;
|
||||||
autoRotate?: boolean;
|
autoRotate?: boolean;
|
||||||
@@ -145,6 +152,8 @@ export type Settings = {
|
|||||||
safeAreaInControlsEnabled: boolean;
|
safeAreaInControlsEnabled: boolean;
|
||||||
jellyseerrServerUrl?: string;
|
jellyseerrServerUrl?: string;
|
||||||
hiddenLibraries?: string[];
|
hiddenLibraries?: string[];
|
||||||
|
enableH265ForChromecast: boolean;
|
||||||
|
defaultPlayer: VideoPlayer;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Lockable<T> {
|
export interface Lockable<T> {
|
||||||
@@ -198,6 +207,8 @@ const defaultValues: Settings = {
|
|||||||
safeAreaInControlsEnabled: true,
|
safeAreaInControlsEnabled: true,
|
||||||
jellyseerrServerUrl: undefined,
|
jellyseerrServerUrl: undefined,
|
||||||
hiddenLibraries: [],
|
hiddenLibraries: [],
|
||||||
|
enableH265ForChromecast: false,
|
||||||
|
defaultPlayer: VideoPlayer.VLC_3, // ios only setting. does not matter what this is for android
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadSettings = (): Partial<Settings> => {
|
const loadSettings = (): Partial<Settings> => {
|
||||||
|
|||||||
8
utils/bitrate.ts
Normal file
8
utils/bitrate.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const formatBitrate = (bitrate?: number | null) => {
|
||||||
|
if (!bitrate) return "N/A";
|
||||||
|
|
||||||
|
const sizes = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"];
|
||||||
|
if (bitrate === 0) return "0 bps";
|
||||||
|
const i = parseInt(Math.floor(Math.log(bitrate) / Math.log(1000)).toString());
|
||||||
|
return Math.round((bitrate / Math.pow(1000, i)) * 100) / 100 + " " + sizes[i];
|
||||||
|
};
|
||||||
26
utils/eventBus.ts
Normal file
26
utils/eventBus.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
type Listener<T = void> = (data?: T) => void;
|
||||||
|
|
||||||
|
class EventBus {
|
||||||
|
private listeners: Record<string, Listener<any>[]> = {};
|
||||||
|
|
||||||
|
on<T = void>(event: string, callback: Listener<T>): () => void {
|
||||||
|
if (!this.listeners[event]) {
|
||||||
|
this.listeners[event] = [];
|
||||||
|
}
|
||||||
|
this.listeners[event].push(callback);
|
||||||
|
return () => this.off(event, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
off<T = void>(event: string, callback: Listener<T>): void {
|
||||||
|
if (!this.listeners[event]) return;
|
||||||
|
this.listeners[event] = this.listeners[event].filter(
|
||||||
|
(fn) => fn !== callback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<T = void>(event: string, data?: T): void {
|
||||||
|
this.listeners[event]?.forEach((callback) => callback(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventBus = new EventBus();
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
PlaybackInfoResponse,
|
PlaybackInfoResponse,
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import { Alert } from "react-native";
|
||||||
|
|
||||||
export const getStreamUrl = async ({
|
export const getStreamUrl = async ({
|
||||||
api,
|
api,
|
||||||
@@ -80,7 +81,6 @@ export const getStreamUrl = async ({
|
|||||||
|
|
||||||
const res2 = await getMediaInfoApi(api).getPlaybackInfo(
|
const res2 = await getMediaInfoApi(api).getPlaybackInfo(
|
||||||
{
|
{
|
||||||
userId,
|
|
||||||
itemId: item.Id!,
|
itemId: item.Id!,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -148,4 +148,8 @@ export const getStreamUrl = async ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Alert.alert("Error", "Could not play this item");
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models";
|
import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
|
||||||
export const chromecastProfile: DeviceProfile = {
|
export const chromecast: DeviceProfile = {
|
||||||
Name: "Chromecast Video Profile",
|
Name: "Chromecast Video Profile",
|
||||||
MaxStreamingBitrate: 8000000, // 8 Mbps
|
MaxStreamingBitrate: 16000000, // 16 Mbps
|
||||||
MaxStaticBitrate: 8000000, // 8 Mbps
|
MaxStaticBitrate: 16000000, // 16 Mbps
|
||||||
MusicStreamingTranscodingBitrate: 384000, // 384 kbps
|
MusicStreamingTranscodingBitrate: 384000, // 384 kbps
|
||||||
CodecProfiles: [
|
CodecProfiles: [
|
||||||
{
|
{
|
||||||
@@ -60,6 +60,7 @@ export const chromecastProfile: DeviceProfile = {
|
|||||||
Protocol: "http",
|
Protocol: "http",
|
||||||
Context: "Streaming",
|
Context: "Streaming",
|
||||||
MaxAudioChannels: "2",
|
MaxAudioChannels: "2",
|
||||||
|
MinSegments: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Container: "mp3",
|
Container: "mp3",
|
||||||
|
|||||||
92
utils/profiles/chromecasth265.ts
Normal file
92
utils/profiles/chromecasth265.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
|
||||||
|
export const chromecasth265: DeviceProfile = {
|
||||||
|
Name: "Chromecast Video Profile",
|
||||||
|
MaxStreamingBitrate: 16000000, // 16Mbps
|
||||||
|
MaxStaticBitrate: 16000000, // 16 Mbps
|
||||||
|
MusicStreamingTranscodingBitrate: 384000, // 384 kbps
|
||||||
|
CodecProfiles: [
|
||||||
|
{
|
||||||
|
Type: "Video",
|
||||||
|
Codec: "hevc,h264",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "Audio",
|
||||||
|
Codec: "aac,mp3,flac,opus,vorbis",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ContainerProfiles: [],
|
||||||
|
DirectPlayProfiles: [
|
||||||
|
{
|
||||||
|
Container: "mp4,mkv",
|
||||||
|
Type: "Video",
|
||||||
|
VideoCodec: "hevc,h264",
|
||||||
|
AudioCodec: "aac,mp3,opus,vorbis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "mp3",
|
||||||
|
Type: "Audio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "aac",
|
||||||
|
Type: "Audio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "flac",
|
||||||
|
Type: "Audio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "wav",
|
||||||
|
Type: "Audio",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
TranscodingProfiles: [
|
||||||
|
{
|
||||||
|
Container: "ts",
|
||||||
|
Type: "Video",
|
||||||
|
VideoCodec: "hevc,h264",
|
||||||
|
AudioCodec: "aac,mp3",
|
||||||
|
Protocol: "hls",
|
||||||
|
Context: "Streaming",
|
||||||
|
MaxAudioChannels: "2",
|
||||||
|
MinSegments: 2,
|
||||||
|
BreakOnNonKeyFrames: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "mp4,mkv",
|
||||||
|
Type: "Video",
|
||||||
|
VideoCodec: "hevc,h264",
|
||||||
|
AudioCodec: "aac",
|
||||||
|
Protocol: "http",
|
||||||
|
Context: "Streaming",
|
||||||
|
MaxAudioChannels: "2",
|
||||||
|
MinSegments: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "mp3",
|
||||||
|
Type: "Audio",
|
||||||
|
AudioCodec: "mp3",
|
||||||
|
Protocol: "http",
|
||||||
|
Context: "Streaming",
|
||||||
|
MaxAudioChannels: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "aac",
|
||||||
|
Type: "Audio",
|
||||||
|
AudioCodec: "aac",
|
||||||
|
Protocol: "http",
|
||||||
|
Context: "Streaming",
|
||||||
|
MaxAudioChannels: "2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SubtitleProfiles: [
|
||||||
|
{
|
||||||
|
Format: "vtt",
|
||||||
|
Method: "Encode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: "vtt",
|
||||||
|
Method: "Encode",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -28,7 +28,7 @@ export default {
|
|||||||
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
|
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
|
||||||
VideoCodec:
|
VideoCodec:
|
||||||
"h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video",
|
"h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video",
|
||||||
AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma",
|
AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma,dts",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Type: MediaTypes.Audio,
|
Type: MediaTypes.Audio,
|
||||||
|
|||||||
Reference in New Issue
Block a user