mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
Merge branch 'develop' of https://github.com/streamyfin/streamyfin into develop
This commit is contained in:
7
app.json
7
app.json
@@ -119,6 +119,13 @@
|
||||
"image": "./assets/images/StreamyFinFinal.png",
|
||||
"imageWidth": 100
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-notifications",
|
||||
{
|
||||
"icon": "./assets/images/notification.png",
|
||||
"color": "#9333EA"
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Ionicons, Feather } from "@expo/vector-icons";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const Chromecast = !Platform.isTV ? require("@/components/Chromecast") : null;
|
||||
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
|
||||
import { useAtom } from "jotai";
|
||||
import { userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSessions, useSessionsProps } from "@/hooks/useSessions";
|
||||
@@ -25,7 +25,7 @@ export default function IndexLayout() {
|
||||
headerLargeStyle: {
|
||||
backgroundColor: "black",
|
||||
},
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
headerShadowVisible: false,
|
||||
headerRight: () => (
|
||||
<View className="flex flex-row items-center space-x-2">
|
||||
@@ -113,7 +113,7 @@ export default function IndexLayout() {
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerBlurEffect: "prominent",
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
@@ -137,7 +137,7 @@ const SettingsButton = () => {
|
||||
|
||||
const SessionsButton = () => {
|
||||
const router = useRouter();
|
||||
const { sessions = [], _ } = useSessions({} as useSessionsProps);
|
||||
const { sessions = [] } = useSessions({} as useSessionsProps);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -146,7 +146,7 @@ const SessionsButton = () => {
|
||||
}}
|
||||
>
|
||||
<View className="mr-4">
|
||||
<Ionicons
|
||||
<Ionicons
|
||||
name="play-circle"
|
||||
color={sessions.length === 0 ? "white" : "#9333ea"}
|
||||
size={25}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
BACKGROUND_FETCH_TASK_SESSIONS,
|
||||
registerBackgroundFetchAsyncSessions,
|
||||
} from "@/utils/background-tasks";
|
||||
import { LogProvider, writeToLog } from "@/utils/log";
|
||||
import {LogProvider, writeErrorLog, writeToLog} from "@/utils/log";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
|
||||
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||
@@ -30,7 +30,7 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
|
||||
import { getLocales } from "expo-localization";
|
||||
import { Provider as JotaiProvider } from "jotai";
|
||||
import { useEffect, useRef } from "react";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { Appearance, AppState } from "react-native";
|
||||
import { SystemBars } from "react-native-edge-to-edge";
|
||||
@@ -41,6 +41,9 @@ import { useAtom } from "jotai";
|
||||
import { userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
||||
import { store } from "@/utils/store";
|
||||
import {EventSubscription} from "expo-modules-core";
|
||||
import {ExpoPushToken} from "expo-notifications/build/Tokens.types";
|
||||
import {Notification, NotificationResponse} from "expo-notifications/build/Notifications.types";
|
||||
|
||||
if (!Platform.isTV) {
|
||||
Notifications.setNotificationHandler({
|
||||
@@ -258,6 +261,7 @@ const queryClient = new QueryClient({
|
||||
function Layout() {
|
||||
const [settings] = useSettings();
|
||||
const [user] = useAtom(userAtom);
|
||||
const [api] = useAtom(apiAtom);
|
||||
const appState = useRef(AppState.currentState);
|
||||
const segments = useSegments();
|
||||
|
||||
@@ -268,13 +272,58 @@ function Layout() {
|
||||
if (!Platform.isTV) {
|
||||
useNotificationObserver();
|
||||
|
||||
const [expoPushToken, setExpoPushToken] = useState<ExpoPushToken>();
|
||||
const notificationListener = useRef<EventSubscription>();
|
||||
const responseListener = useRef<EventSubscription>();
|
||||
|
||||
useEffect(() => {
|
||||
checkAndRequestPermissions();
|
||||
(async () => {
|
||||
if (!Platform.isTV && user && user.Policy?.IsAdministrator) {
|
||||
registerBackgroundFetchAsyncSessions();
|
||||
}
|
||||
})();
|
||||
if (expoPushToken && api && user) {
|
||||
api?.post("/Streamyfin/device", {
|
||||
token: expoPushToken.data,
|
||||
deviceId: getOrSetDeviceId(),
|
||||
userId: user.Id
|
||||
}).then(_ => console.log("Posted expo push token"))
|
||||
.catch(_ => writeErrorLog("Failed to push expo push token to plugin"))
|
||||
}
|
||||
else console.log("No token available")
|
||||
}, [api, expoPushToken, user]);
|
||||
|
||||
async function registerNotifications() {
|
||||
if (Platform.OS === 'android') {
|
||||
console.log("Setting android notification channel 'default'")
|
||||
await Notifications?.setNotificationChannelAsync('default', {
|
||||
name: 'default'
|
||||
});
|
||||
}
|
||||
|
||||
await checkAndRequestPermissions();
|
||||
|
||||
if (!Platform.isTV && user && user.Policy?.IsAdministrator) {
|
||||
await registerBackgroundFetchAsyncSessions();
|
||||
}
|
||||
|
||||
Notifications?.getExpoPushTokenAsync()
|
||||
.then((token: ExpoPushToken) => token && setExpoPushToken(token))
|
||||
.catch((reason: any) => console.log("Failed to get token", reason));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
registerNotifications()
|
||||
|
||||
notificationListener.current = Notifications?.addNotificationReceivedListener((notification: Notification) => {
|
||||
console.log("Notification received while app running", notification);
|
||||
});
|
||||
|
||||
responseListener.current = Notifications?.addNotificationResponseReceivedListener((response: NotificationResponse) => {
|
||||
console.log("Notification interacted with", response);
|
||||
});
|
||||
|
||||
return () => {
|
||||
notificationListener.current &&
|
||||
Notifications?.removeNotificationSubscription(notificationListener.current);
|
||||
responseListener.current &&
|
||||
Notifications?.removeNotificationSubscription(responseListener.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { Image } from "expo-image";
|
||||
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useAtomValue } from "jotai";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
@@ -58,7 +58,7 @@ const Login: React.FC = () => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (_apiUrl) {
|
||||
setServer({
|
||||
await setServer({
|
||||
address: _apiUrl,
|
||||
});
|
||||
|
||||
@@ -181,7 +181,7 @@ const Login: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setServer({ address: url });
|
||||
await setServer({ address: url });
|
||||
}, []);
|
||||
|
||||
const handleQuickConnect = async () => {
|
||||
@@ -237,11 +237,12 @@ const Login: React.FC = () => {
|
||||
setCredentials({ ...credentials, username: text })
|
||||
}
|
||||
value={credentials.username}
|
||||
secureTextEntry={false}
|
||||
keyboardType="default"
|
||||
returnKeyType="done"
|
||||
autoCapitalize="none"
|
||||
textContentType="username"
|
||||
// Changed from username to oneTimeCode because it is a known issue in RN
|
||||
// https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037
|
||||
textContentType="oneTimeCode"
|
||||
clearButtonMode="while-editing"
|
||||
maxLength={500}
|
||||
/>
|
||||
@@ -324,17 +325,17 @@ const Login: React.FC = () => {
|
||||
{t("server.connect_button")}
|
||||
</Button>
|
||||
<JellyfinServerDiscovery
|
||||
onServerSelect={(server) => {
|
||||
onServerSelect={async (server) => {
|
||||
setServerURL(server.address);
|
||||
if (server.serverName) {
|
||||
setServerName(server.serverName);
|
||||
}
|
||||
handleConnect(server.address);
|
||||
await handleConnect(server.address);
|
||||
}}
|
||||
/>
|
||||
<PreviousServersList
|
||||
onServerSelect={(s) => {
|
||||
handleConnect(s.address);
|
||||
onServerSelect={async (s) => {
|
||||
await handleConnect(s.address);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
BIN
assets/images/notification.png
Normal file
BIN
assets/images/notification.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -13,6 +13,10 @@ declare module "@jellyfin/sdk" {
|
||||
data: D,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<AxiosResponse<T>>;
|
||||
delete<T, D = any>(
|
||||
url: string,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<AxiosResponse<T>>;
|
||||
getStreamyfinPluginConfig(): Promise<AxiosResponse<StreamyfinPluginConfig>>;
|
||||
}
|
||||
}
|
||||
@@ -32,9 +36,18 @@ Api.prototype.post = function <T, D = any>(
|
||||
data: D,
|
||||
config: AxiosRequestConfig<D>
|
||||
): Promise<AxiosResponse<T>> {
|
||||
return this.axiosInstance.post<T>(`${this.basePath}${url}`, {
|
||||
return this.axiosInstance.post<T>(`${this.basePath}${url}`, data, {
|
||||
...(config || {}),
|
||||
headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader },
|
||||
});
|
||||
};
|
||||
|
||||
Api.prototype.delete = function <T, D = any>(
|
||||
url: string,
|
||||
config: AxiosRequestConfig<D>
|
||||
): Promise<AxiosResponse<T>> {
|
||||
return this.axiosInstance.delete<T>(`${this.basePath}${url}`, {
|
||||
...(config || {}),
|
||||
data,
|
||||
headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useFavorite } from "@/hooks/useFavorite";
|
||||
import { View } from "react-native";
|
||||
import {View, ViewProps} from "react-native";
|
||||
import { RoundButton } from "@/components/RoundButton";
|
||||
import {FC} from "react";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
item: BaseItemDto;
|
||||
}
|
||||
|
||||
export const AddToFavorites = ({ item, ...props }) => {
|
||||
const { isFavorite, toggleFavorite, _} = useFavorite(item);
|
||||
|
||||
export const AddToFavorites:FC<Props> = ({ item, ...props }) => {
|
||||
const { isFavorite, toggleFavorite } = useFavorite(item);
|
||||
|
||||
return (
|
||||
<View {...props}>
|
||||
<RoundButton
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useImageColors } from "@/hooks/useImageColors";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { getItemImage } from "@/utils/getItemImage";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { Image, ImageProps, ImageSource } from "expo-image";
|
||||
import { Image, ImageProps } from "expo-image";
|
||||
import { useAtom } from "jotai";
|
||||
import { useMemo } from "react";
|
||||
import { View } from "react-native";
|
||||
import {FC, useMemo} from "react";
|
||||
import { View, ViewProps } from "react-native";
|
||||
|
||||
interface Props extends ImageProps {
|
||||
item: BaseItemDto;
|
||||
@@ -25,7 +24,7 @@ interface Props extends ImageProps {
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
export const ItemImage: React.FC<Props> = ({
|
||||
export const ItemImage: FC<Props> = ({
|
||||
item,
|
||||
variant = "Primary",
|
||||
quality = 90,
|
||||
@@ -53,7 +52,7 @@ export const ItemImage: React.FC<Props> = ({
|
||||
if (!source?.uri)
|
||||
return (
|
||||
<View
|
||||
{...props}
|
||||
{...props as ViewProps}
|
||||
className="flex flex-col items-center justify-center border border-neutral-800 bg-neutral-900"
|
||||
>
|
||||
<Ionicons
|
||||
|
||||
@@ -2,24 +2,24 @@ import React, { useEffect, useRef } from "react";
|
||||
import { View, StyleSheet, Platform } from "react-native";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
// import { VolumeManager } from "react-native-volume-manager";
|
||||
const VolumeManager = !Platform.isTV
|
||||
? require("react-native-volume-manager")
|
||||
: null;
|
||||
const VolumeManager = Platform.isTV ? null : require("react-native-volume-manager");
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { VolumeResult } from "react-native-volume-manager";
|
||||
|
||||
interface AudioSliderProps {
|
||||
setVisibility: (show: boolean) => void;
|
||||
}
|
||||
|
||||
const AudioSlider: React.FC<AudioSliderProps> = ({ setVisibility }) => {
|
||||
if (Platform.isTV) return;
|
||||
if (Platform.isTV) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volume = useSharedValue<number>(50); // Explicitly type as number
|
||||
const min = useSharedValue<number>(0); // Explicitly type as number
|
||||
const max = useSharedValue<number>(100); // Explicitly type as number
|
||||
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null); // Use a ref to store the timeout ID
|
||||
const timeoutRef = useRef<number | null>(null); // Use a ref to store the timeout ID
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInitialVolume = async () => {
|
||||
@@ -50,7 +50,7 @@ const AudioSlider: React.FC<AudioSliderProps> = ({ setVisibility }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const volumeListener = VolumeManager.addVolumeListener((result) => {
|
||||
const volumeListener = VolumeManager.addVolumeListener((result: VolumeResult) => {
|
||||
volume.value = result.volume * 100;
|
||||
setVisibility(true);
|
||||
|
||||
|
||||
3
i18n.ts
3
i18n.ts
@@ -10,6 +10,7 @@ import ja from "./translations/ja.json";
|
||||
import tr from "./translations/tr.json";
|
||||
import nl from "./translations/nl.json";
|
||||
import sv from "./translations/sv.json";
|
||||
import ua from "./translations/ua.json"
|
||||
import zhCN from './translations/zh-CN.json';
|
||||
import zhTW from './translations/zh-TW.json';
|
||||
import { getLocales } from "expo-localization";
|
||||
@@ -24,6 +25,7 @@ export const APP_LANGUAGES = [
|
||||
{ label: "Türkçe", value: "tr" },
|
||||
{ label: "Nederlands", value: "nl" },
|
||||
{ label: "Svenska", value: "sv" },
|
||||
{ label: "Українська", value: "ua" },
|
||||
{ label: "简体中文", value: "zh-CN" },
|
||||
{ label: "繁體中文", value: "zh-TW" },
|
||||
];
|
||||
@@ -40,6 +42,7 @@ i18n.use(initReactI18next).init({
|
||||
nl: { translation: nl },
|
||||
sv: { translation: sv },
|
||||
tr: { translation: tr },
|
||||
ua: { translation: ua },
|
||||
"zh-CN": { translation: zhCN },
|
||||
"zh-TW": { translation: zhTW },
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Platform } from "react-native";
|
||||
import { getDeviceName } from "react-native-device-info";
|
||||
import uuid from "react-native-uuid";
|
||||
import {writeErrorLog, writeInfoLog} from "@/utils/log";
|
||||
|
||||
interface Server {
|
||||
address: string;
|
||||
@@ -286,6 +287,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
api?.delete(`/Streamyfin/device/${deviceId}`)
|
||||
.then(r => writeInfoLog("Deleted expo push token for device"))
|
||||
.catch(e => writeErrorLog(`Failed to delete expo push token for device`))
|
||||
|
||||
storage.delete("token");
|
||||
setUser(null);
|
||||
setApi(null);
|
||||
|
||||
@@ -7,16 +7,13 @@ import React, {
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { Alert, AppState, AppStateStatus } from "react-native";
|
||||
import { AppState, AppStateStatus } from "react-native";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
apiAtom,
|
||||
getOrSetDeviceId,
|
||||
userAtom,
|
||||
} from "@/providers/JellyfinProvider";
|
||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import native from "@/utils/profiles/native";
|
||||
|
||||
interface WebSocketProviderProps {
|
||||
children: ReactNode;
|
||||
@@ -30,7 +27,6 @@ interface WebSocketContextType {
|
||||
const WebSocketContext = createContext<WebSocketContextType | null>(null);
|
||||
|
||||
export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
||||
const user = useAtomValue(userAtom);
|
||||
const api = useAtomValue(apiAtom);
|
||||
const [ws, setWs] = useState<WebSocket | null>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
@@ -40,7 +36,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
||||
}, []);
|
||||
|
||||
const connectWebSocket = useCallback(() => {
|
||||
if (!deviceId || !api?.accessToken) return;
|
||||
if (!deviceId || !api?.accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const protocol = api.basePath.includes("https") ? "wss" : "ws";
|
||||
const url = `${protocol}://${api.basePath
|
||||
@@ -50,7 +48,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
||||
}&deviceId=${deviceId}`;
|
||||
|
||||
const newWebSocket = new WebSocket(url);
|
||||
let keepAliveInterval: NodeJS.Timeout | null = null;
|
||||
let keepAliveInterval: number | null = null;
|
||||
|
||||
newWebSocket.onopen = () => {
|
||||
setIsConnected(true);
|
||||
@@ -67,14 +65,18 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
||||
};
|
||||
|
||||
newWebSocket.onclose = () => {
|
||||
if (keepAliveInterval) clearInterval(keepAliveInterval);
|
||||
if (keepAliveInterval) {
|
||||
clearInterval(keepAliveInterval);
|
||||
}
|
||||
setIsConnected(false);
|
||||
};
|
||||
|
||||
setWs(newWebSocket);
|
||||
|
||||
return () => {
|
||||
if (keepAliveInterval) clearInterval(keepAliveInterval);
|
||||
if (keepAliveInterval) {
|
||||
clearInterval(keepAliveInterval);
|
||||
}
|
||||
newWebSocket.close();
|
||||
};
|
||||
}, [api, deviceId]);
|
||||
@@ -85,7 +87,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
||||
}, [connectWebSocket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!deviceId || !api || !api?.accessToken) return;
|
||||
if (!deviceId || !api || !api?.accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
await getSessionApi(api).postFullCapabilities({
|
||||
|
||||
475
translations/ua.json
Normal file
475
translations/ua.json
Normal file
@@ -0,0 +1,475 @@
|
||||
{
|
||||
"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": "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": "Введіть URL вашого Jellyfin сервера",
|
||||
"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": "Вільний і open-source клієнт для 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",
|
||||
"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": "Smart",
|
||||
"Always": "Завжди",
|
||||
"None": "Някий",
|
||||
"OnlyForced": "Виключно Форсовані"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"other_title": "Інші",
|
||||
"follow_device_orientation": "Дотримуйтесь орієнтації пристрою",
|
||||
"video_orientation": "Орієнтація відео",
|
||||
"orientation": "Orientation",
|
||||
"orientations": {
|
||||
"DEFAULT": "За змовчуванням",
|
||||
"ALL": "Всі",
|
||||
"PORTRAIT": "Портретна",
|
||||
"PORTRAIT_UP": "Портретна Догори",
|
||||
"PORTRAIT_DOWN": "Портретна Донизу",
|
||||
"LANDSCAPE": "Альбомна",
|
||||
"LANDSCAPE_LEFT": "Альбомна Ліва",
|
||||
"LANDSCAPE_RIGHT": "Альбомна Права",
|
||||
"OTHER": "Інше",
|
||||
"UNKNOWN": "Невідомо"
|
||||
},
|
||||
"safe_area_in_controls": "Безпечна зона в елементах керування",
|
||||
"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": "Вимкнути тактильний зворотний зв'язок",
|
||||
"default_quality": "Якість за замовченням"
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Завантаження",
|
||||
"download_method": "Метод завантаження",
|
||||
"remux_max_download": "Remux max download",
|
||||
"auto_download": "Авто-завантаження",
|
||||
"optimized_versions_server": "Optimized versions server",
|
||||
"save_button": "Зберегти",
|
||||
"optimized_server": "Оптимізований Сервер",
|
||||
"optimized": "Оптимізований",
|
||||
"default": "За замовченням",
|
||||
"optimized_version_hint": "Введіть URL-адресу сервера для оптимізації. URL-адреса має містити http або https і, за бажанням, порт.",
|
||||
"read_more_about_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}} ще",
|
||||
"order_by": {
|
||||
"DEFAULT": "За замовченням",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Кількість голосів і середнє",
|
||||
"POPULARITY": "Популярність"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Увімкнути Marlin Search ",
|
||||
"url": "URL",
|
||||
"server_url_placeholder": "http(s)://domain.org:port",
|
||||
"marlin_search_hint": "Введіть URL-адресу сервера Marlin. Адреса повинна містити http або https і, за бажанням, порт.",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"title": "Сесії",
|
||||
"no_active_sessions": "Нема активних сесій"
|
||||
},
|
||||
"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": "Не вдалося отримати URL-адресу потоку від Jellyfin",
|
||||
"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}} в черзі на оптимізацію",
|
||||
"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": "Порядок сортування",
|
||||
"asc": "За зростанням",
|
||||
"desc": "За спаданням",
|
||||
"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": "Повідомлення від серверу: {{message}}",
|
||||
"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": "Використовуючи сервер оптимізації",
|
||||
"using_default_method": "Використовуючи метод за замовченням"
|
||||
}
|
||||
},
|
||||
"live_tv": {
|
||||
"next": "Наступний",
|
||||
"previous": "Попередній",
|
||||
"live_tv": "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_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": "Не вдалося перевірити URL-адресу сервера jellyseerr",
|
||||
"issue_submitted": "Звіт про проблему відправлено",
|
||||
"requested_item": "Запитано {{item}}!",
|
||||
"you_dont_have_permission_to_request": "У вас нема дозволу на запит медіа!",
|
||||
"something_went_wrong_requesting_media": "Щось пішло не так під час запиту медіа!"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"home": "Головна",
|
||||
"search": "Пошук",
|
||||
"library": "Медіатека",
|
||||
"custom_links": "Ваші Посилання",
|
||||
"favorites": "Улюблене"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Settings } from "@/utils/atoms/settings";
|
||||
import ios from "@/utils/profiles/ios";
|
||||
import native from "@/utils/profiles/native";
|
||||
import old from "@/utils/profiles/old";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
import { useMemo } from "react";
|
||||
import { getAuthHeaders } from "../jellyfin";
|
||||
import iosFmp4 from "@/utils/profiles/iosFmp4";
|
||||
|
||||
interface PostCapabilitiesParams {
|
||||
api: Api | null | undefined;
|
||||
@@ -25,7 +21,6 @@ export const postCapabilities = async ({
|
||||
api,
|
||||
itemId,
|
||||
sessionId,
|
||||
deviceProfile,
|
||||
}: PostCapabilitiesParams): Promise<AxiosResponse> => {
|
||||
if (!api || !itemId || !sessionId) {
|
||||
throw new Error("Missing parameters for marking item as not played");
|
||||
|
||||
Reference in New Issue
Block a user