mirror of
https://github.com/p-stream/p-stream.git
synced 2026-05-14 21:41:25 +00:00
885 lines
29 KiB
TypeScript
885 lines
29 KiB
TypeScript
import {
|
|
Dispatch,
|
|
SetStateAction,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { Trans, useTranslation } from "react-i18next";
|
|
|
|
import { Button } from "@/components/buttons/Button";
|
|
import { Toggle } from "@/components/buttons/Toggle";
|
|
import { BackendSelector } from "@/components/form/BackendSelector";
|
|
import { Dropdown } from "@/components/form/Dropdown";
|
|
import { Icon, Icons } from "@/components/Icon";
|
|
import { SettingsCard } from "@/components/layout/SettingsCard";
|
|
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
|
|
import {
|
|
StatusCircle,
|
|
StatusCircleProps,
|
|
} from "@/components/player/internals/StatusCircle";
|
|
import { MwLink } from "@/components/text/Link";
|
|
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
|
import { Divider } from "@/components/utils/Divider";
|
|
import { Heading1, Heading2, Paragraph } from "@/components/utils/Text";
|
|
import { useIsDesktopApp } from "@/hooks/useIsDesktopApp";
|
|
import {
|
|
SetupPart,
|
|
Status,
|
|
fetchFebboxQuota,
|
|
testFebboxKey,
|
|
testTorboxToken,
|
|
testdebridToken,
|
|
} from "@/pages/parts/settings/SetupPart";
|
|
import { conf } from "@/setup/config";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import { usePreferencesStore } from "@/stores/preferences";
|
|
import { useTraktStore } from "@/stores/trakt/store";
|
|
|
|
import { RegionSelectorPart } from "./RegionSelectorPart";
|
|
|
|
interface ProxyEditProps {
|
|
proxyUrls: string[] | null;
|
|
setProxyUrls: Dispatch<SetStateAction<string[] | null>>;
|
|
proxyTmdb: boolean;
|
|
setProxyTmdb: Dispatch<SetStateAction<boolean>>;
|
|
}
|
|
|
|
interface BackendEditProps {
|
|
backendUrl: string | null;
|
|
setBackendUrl: Dispatch<SetStateAction<string | null>>;
|
|
}
|
|
|
|
interface FebboxKeyProps {
|
|
febboxKey: string | null;
|
|
setFebboxKey: (value: string | null) => void;
|
|
}
|
|
|
|
interface DebridProps {
|
|
debridToken: string | null;
|
|
setdebridToken: (value: string | null) => void;
|
|
debridService: string;
|
|
setdebridService: (value: string) => void;
|
|
// eslint-disable-next-line react/no-unused-prop-types
|
|
mode?: "onboarding" | "settings";
|
|
}
|
|
|
|
interface TIDBKeyProps {
|
|
tidbKey: string | null;
|
|
setTIDBKey: (value: string | null) => void;
|
|
}
|
|
|
|
function ProxyEdit({
|
|
proxyUrls,
|
|
setProxyUrls,
|
|
proxyTmdb,
|
|
setProxyTmdb,
|
|
}: ProxyEditProps) {
|
|
const { t } = useTranslation();
|
|
const add = useCallback(() => {
|
|
setProxyUrls((s) => [...(s ?? []), ""]);
|
|
}, [setProxyUrls]);
|
|
|
|
const changeItem = useCallback(
|
|
(index: number, val: string) => {
|
|
setProxyUrls((s) => [
|
|
...(s ?? []).map((v, i) => {
|
|
if (i !== index) return v;
|
|
return val;
|
|
}),
|
|
]);
|
|
},
|
|
[setProxyUrls],
|
|
);
|
|
|
|
const removeItem = useCallback(
|
|
(index: number) => {
|
|
setProxyUrls((s) => [...(s ?? []).filter((v, i) => i !== index)]);
|
|
},
|
|
[setProxyUrls],
|
|
);
|
|
|
|
const toggleProxyUrls = useCallback(() => {
|
|
const newValue = proxyUrls === null ? [] : null;
|
|
setProxyUrls(newValue);
|
|
// Disable TMDB proxying when proxy workers are disabled
|
|
if (newValue === null) setProxyTmdb(false);
|
|
}, [proxyUrls, setProxyUrls, setProxyTmdb]);
|
|
|
|
return (
|
|
<SettingsCard>
|
|
<div className="flex justify-between items-center gap-4">
|
|
<div className="my-3">
|
|
<p className="text-white font-bold mb-3">
|
|
{t("settings.connections.workers.label")}
|
|
</p>
|
|
<p className="max-w-[30rem] font-medium">
|
|
<Trans i18nKey="settings.connections.workers.description">
|
|
<MwLink to="https://docs.pstream.mov/proxy/deploy">
|
|
{t("settings.connections.workers.documentation")}
|
|
</MwLink>
|
|
</Trans>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Toggle onClick={toggleProxyUrls} enabled={proxyUrls !== null} />
|
|
</div>
|
|
</div>
|
|
{proxyUrls !== null ? (
|
|
<>
|
|
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
|
<p className="text-white font-bold mb-3">
|
|
{t("settings.connections.workers.urlLabel")}
|
|
</p>
|
|
|
|
<div className="my-6 space-y-2 max-w-md">
|
|
{(proxyUrls?.length ?? 0) === 0 ? (
|
|
<p>{t("settings.connections.workers.emptyState")}</p>
|
|
) : null}
|
|
{(proxyUrls ?? []).map((v, i) => (
|
|
<div
|
|
// not the best but we can live with it
|
|
// eslint-disable-next-line react/no-array-index-key
|
|
key={i}
|
|
className="grid grid-cols-[1fr,auto] items-center gap-2"
|
|
>
|
|
<AuthInputBox
|
|
value={v}
|
|
onChange={(val) => changeItem(i, val)}
|
|
placeholder={
|
|
t("settings.connections.workers.urlPlaceholder") ??
|
|
undefined
|
|
}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeItem(i)}
|
|
className="h-full scale-90 hover:scale-100 rounded-full aspect-square bg-authentication-inputBg hover:bg-authentication-inputBgHover flex justify-center items-center transition-transform duration-200 hover:text-white cursor-pointer"
|
|
>
|
|
<Icon className="text-xl" icon={Icons.X} />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<Button theme="purple" onClick={add}>
|
|
{t("settings.connections.workers.addButton")}
|
|
</Button>
|
|
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
|
|
|
<div className="flex justify-between items-center gap-4">
|
|
<div className="my-3">
|
|
<p className="text-white font-bold mb-3">
|
|
{t("settings.connections.workers.proxyTMDB.title")}
|
|
</p>
|
|
<p className="max-w-[30rem] font-medium">
|
|
{t("settings.connections.workers.proxyTMDB.description")}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Toggle
|
|
enabled={proxyTmdb}
|
|
onClick={() => setProxyTmdb(!proxyTmdb)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
</SettingsCard>
|
|
);
|
|
}
|
|
|
|
function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
|
|
const { t } = useTranslation();
|
|
const user = useAuthStore();
|
|
const config = conf();
|
|
const availableBackends =
|
|
config.BACKEND_URLS.length > 0
|
|
? config.BACKEND_URLS
|
|
: config.BACKEND_URL
|
|
? [config.BACKEND_URL]
|
|
: [];
|
|
const currentBackendUrl =
|
|
backendUrl ?? (availableBackends.length > 0 ? availableBackends[0] : null);
|
|
const [pendingBackendUrl, setPendingBackendUrl] = useState<string | null>(
|
|
currentBackendUrl,
|
|
);
|
|
const confirmationModal = useModal("backend-change-confirmation");
|
|
|
|
const handleBackendSelect = (url: string | null) => {
|
|
if (!user.account) {
|
|
// No account - just update without confirmation
|
|
setBackendUrl(url);
|
|
setPendingBackendUrl(url);
|
|
} else if (url !== currentBackendUrl) {
|
|
// User is logged in and changing backend - show confirmation
|
|
setPendingBackendUrl(url);
|
|
confirmationModal.show();
|
|
} else {
|
|
// Same backend - just update
|
|
setBackendUrl(url);
|
|
setPendingBackendUrl(url);
|
|
}
|
|
};
|
|
|
|
const handleConfirmChange = () => {
|
|
setBackendUrl(pendingBackendUrl);
|
|
confirmationModal.hide();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<SettingsCard>
|
|
<div className="my-3">
|
|
<p className="text-white font-bold mb-3">
|
|
{t("settings.connections.server.label")}
|
|
</p>
|
|
<p className="max-w-[30rem] font-medium">
|
|
<Trans i18nKey="settings.connections.server.description">
|
|
<MwLink to="https://docs.pstream.mov/backend/deploy">
|
|
{t("settings.connections.server.documentation")}
|
|
</MwLink>
|
|
</Trans>
|
|
</p>
|
|
{user.account && (
|
|
<div>
|
|
<br />
|
|
<p className="max-w-[30rem] font-medium">
|
|
<Trans i18nKey="settings.connections.server.migration.description">
|
|
<MwLink to="/migration">
|
|
{t("settings.connections.server.migration.link")}
|
|
</MwLink>
|
|
</Trans>
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{(availableBackends.length > 0 || currentBackendUrl) && (
|
|
<>
|
|
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
|
<p className="text-white font-bold mb-3">
|
|
{t("settings.connections.server.selectBackend")}
|
|
</p>
|
|
{availableBackends.length > 0 ? (
|
|
<BackendSelector
|
|
selectedUrl={currentBackendUrl}
|
|
onSelect={handleBackendSelect}
|
|
availableUrls={availableBackends}
|
|
showCustom
|
|
/>
|
|
) : (
|
|
<AuthInputBox
|
|
onChange={setBackendUrl}
|
|
value={backendUrl ?? ""}
|
|
placeholder="https://"
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</SettingsCard>
|
|
{user.account && (
|
|
<Modal id={confirmationModal.id}>
|
|
<ModalCard>
|
|
<Heading2 className="!mt-0 !mb-4">
|
|
{t("settings.connections.server.changeWarningTitle")}
|
|
</Heading2>
|
|
<Paragraph className="!mt-1 !mb-6">
|
|
{t("settings.connections.server.changeWarning")}
|
|
</Paragraph>
|
|
<div className="flex justify-end gap-3">
|
|
<Button theme="secondary" onClick={confirmationModal.hide}>
|
|
{t("settings.connections.server.cancel")}
|
|
</Button>
|
|
<Button theme="purple" onClick={handleConfirmChange}>
|
|
{t("settings.connections.server.confirm")}
|
|
</Button>
|
|
</div>
|
|
</ModalCard>
|
|
</Modal>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
async function getFebboxKeyStatus(febboxKey: string | null) {
|
|
if (febboxKey) {
|
|
const status: Status = await testFebboxKey(febboxKey);
|
|
const quota = await fetchFebboxQuota(febboxKey);
|
|
return { status, quota };
|
|
}
|
|
return { status: "unset" as Status, quota: null };
|
|
}
|
|
|
|
interface FebboxSetupProps extends FebboxKeyProps {
|
|
mode: "onboarding" | "settings";
|
|
}
|
|
|
|
export function FebboxSetup({
|
|
febboxKey,
|
|
setFebboxKey,
|
|
mode,
|
|
}: FebboxSetupProps) {
|
|
const { t } = useTranslation();
|
|
const [showVideo, setShowVideo] = useState(false);
|
|
const user = useAuthStore();
|
|
const preferences = usePreferencesStore();
|
|
const exampleModal = useModal("febbox-example");
|
|
|
|
// Initialize expansion state for onboarding mode
|
|
const [isFebboxExpanded, setIsFebboxExpanded] = useState(
|
|
mode === "onboarding" && febboxKey !== null && febboxKey !== "",
|
|
);
|
|
|
|
// Expand when key is set in onboarding mode
|
|
useEffect(() => {
|
|
if (mode === "onboarding" && febboxKey && febboxKey.length > 0) {
|
|
setIsFebboxExpanded(true);
|
|
}
|
|
}, [febboxKey, mode]);
|
|
|
|
// Enable febbox token when account is loaded in settings mode
|
|
useEffect(() => {
|
|
if (
|
|
mode === "settings" &&
|
|
user.account &&
|
|
febboxKey === null &&
|
|
preferences.febboxKey
|
|
) {
|
|
setFebboxKey(preferences.febboxKey);
|
|
}
|
|
}, [user.account, febboxKey, preferences.febboxKey, setFebboxKey, mode]);
|
|
|
|
const [status, setStatus] = useState<Status>("unset");
|
|
const [quota, setQuota] = useState<any>(null);
|
|
const statusMap: Record<Status, StatusCircleProps["type"]> = {
|
|
error: "error",
|
|
success: "success",
|
|
unset: "noresult",
|
|
api_down: "error",
|
|
invalid_token: "error",
|
|
};
|
|
|
|
useEffect(() => {
|
|
const checkTokenStatus = async () => {
|
|
const result = await getFebboxKeyStatus(febboxKey);
|
|
setStatus(result.status);
|
|
setQuota(result.quota);
|
|
};
|
|
checkTokenStatus();
|
|
}, [febboxKey]);
|
|
|
|
// Toggle handler based on mode
|
|
const toggleFebboxExpanded = () => {
|
|
if (mode === "onboarding") {
|
|
// Onboarding mode: expand/collapse, preserve key
|
|
if (isFebboxExpanded) {
|
|
setFebboxKey("");
|
|
setIsFebboxExpanded(false);
|
|
} else {
|
|
setIsFebboxExpanded(true);
|
|
}
|
|
} else {
|
|
// Settings mode: enable/disable
|
|
setFebboxKey(febboxKey === null ? "" : null);
|
|
}
|
|
};
|
|
|
|
// Determine if content is visible
|
|
const isFebboxVisible =
|
|
mode === "onboarding" ? isFebboxExpanded : febboxKey !== null;
|
|
|
|
if (conf().ALLOW_FEBBOX_KEY) {
|
|
return (
|
|
<>
|
|
<SettingsCard>
|
|
<div className="flex justify-between items-center gap-4">
|
|
<div className="my-3">
|
|
<p className="text-white font-bold mb-3">
|
|
{t("fedapi.onboarding.title")}
|
|
</p>
|
|
<p className="max-w-[30rem] font-medium">
|
|
<Trans i18nKey="fedapi.onboarding.description" />
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Toggle
|
|
onClick={toggleFebboxExpanded}
|
|
enabled={
|
|
mode === "onboarding" ? isFebboxExpanded : febboxKey !== null
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{isFebboxVisible ? (
|
|
<>
|
|
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
|
|
|
<div className="my-3">
|
|
<p className="max-w-[30rem] font-medium">
|
|
{t("fedapi.setup.title")}
|
|
<br />
|
|
<div
|
|
onClick={() => setShowVideo(!showVideo)}
|
|
className="flex items-center justify-between p-1 px-2 my-2 w-fit border border-type-secondary rounded-lg cursor-pointer text-type-secondary hover:text-white transition-colors duration-200"
|
|
>
|
|
<span className="text-sm">
|
|
{showVideo
|
|
? t("fedapi.setup.hideVideo")
|
|
: t("fedapi.setup.showVideo")}
|
|
</span>
|
|
{showVideo ? (
|
|
<Icon icon={Icons.CHEVRON_UP} className="pl-1" />
|
|
) : (
|
|
<Icon icon={Icons.CHEVRON_DOWN} className="pl-1" />
|
|
)}
|
|
</div>
|
|
{showVideo && (
|
|
<>
|
|
<div className="relative pt-[56.25%] mt-2">
|
|
<iframe
|
|
src="https://player.vimeo.com/video/1059834885?h=c3ab398d42&badge=0&autopause=0&player_id=0&app_id=58479"
|
|
allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media"
|
|
className="absolute top-0 left-0 w-full h-full border border-type-secondary rounded-lg bg-black"
|
|
title="P-Stream FED API Setup Tutorial"
|
|
/>
|
|
</div>
|
|
<br />
|
|
</>
|
|
)}
|
|
<Trans i18nKey="fedapi.setup.step.1">
|
|
<MwLink url="https://febbox.com" />
|
|
</Trans>
|
|
<br />
|
|
<Trans i18nKey="fedapi.setup.step.2" />
|
|
<br />
|
|
<Trans i18nKey="fedapi.setup.step.3" />
|
|
<br />
|
|
<Trans i18nKey="fedapi.setup.step.4" />{" "}
|
|
<button
|
|
type="button"
|
|
onClick={exampleModal.show}
|
|
className="text-type-link hover:text-type-linkHover"
|
|
>
|
|
<Trans i18nKey="fedapi.setup.tokenExample.button" />
|
|
</button>
|
|
<br />
|
|
<Trans i18nKey="fedapi.setup.step.5" />
|
|
</p>
|
|
</div>
|
|
|
|
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
|
<p className="text-white font-bold mb-3">
|
|
{mode === "settings"
|
|
? t("settings.connections.febbox.tokenLabel", "Token")
|
|
: t("fedapi.setup.tokenLabel")}
|
|
</p>
|
|
<div className="flex md:flex-row flex-col items-center w-full gap-4">
|
|
<div className="flex items-center w-full">
|
|
<StatusCircle type={statusMap[status]} className="mx-2" />
|
|
<AuthInputBox
|
|
onChange={(newToken) => {
|
|
setFebboxKey(newToken);
|
|
}}
|
|
value={febboxKey ?? ""}
|
|
placeholder="eyJ0eXAi..."
|
|
passwordToggleable
|
|
className="flex-grow"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<RegionSelectorPart />
|
|
</div>
|
|
</div>
|
|
{status === "error" && (
|
|
<p className="text-type-danger mt-4">
|
|
{t("fedapi.status.failure")}
|
|
</p>
|
|
)}
|
|
{status === "api_down" && (
|
|
<p className="text-type-danger mt-4">
|
|
{t("fedapi.status.api_down")}
|
|
</p>
|
|
)}
|
|
{status === "invalid_token" && (
|
|
<p className="text-type-danger mt-4">
|
|
{t("fedapi.status.invalid_token")}
|
|
</p>
|
|
)}
|
|
{status === "success" &&
|
|
quota &&
|
|
(() => {
|
|
if (!quota?.data?.flow) return null;
|
|
const {
|
|
traffic_usage: used,
|
|
traffic_limit: limit,
|
|
reset_at: reset,
|
|
} = quota.data.flow;
|
|
return (
|
|
<>
|
|
<p className="text-sm text-green-500 mt-2">
|
|
{t("fedapi.setup.traffic", { used, limit, reset })}
|
|
</p>
|
|
<p className="max-w-[30rem] text-xs opacity-70 mt-2">
|
|
{t("fedapi.setup.trafficExplanation")}
|
|
</p>
|
|
</>
|
|
);
|
|
})()}
|
|
<div className="flex justify-between items-center gap-4 mt-6">
|
|
<div className="my-3">
|
|
<p className="max-w-[32rem] font-medium">
|
|
{t("fedapi.setup.useMp4")}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Toggle
|
|
onClick={() =>
|
|
preferences.setFebboxUseMp4(!preferences.febboxUseMp4)
|
|
}
|
|
enabled={preferences.febboxUseMp4}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
</SettingsCard>
|
|
<Modal id={exampleModal.id}>
|
|
<ModalCard>
|
|
<Heading2 className="!mt-0 !mb-4 !text-2xl">
|
|
{t("fedapi.setup.tokenExample.title")}
|
|
</Heading2>
|
|
<Paragraph className="!mt-1 !mb-6">
|
|
{t("fedapi.setup.tokenExample.description")}
|
|
</Paragraph>
|
|
<div className="bg-authentication-inputBg p-4 rounded-lg mb-6 font-mono text-sm break-all">
|
|
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDc1MTI2MTksIm5iZiI6MTc0NzUxMjYxOSwiZXhwIjoxNzc4NjE2NjM5LCJkYXRhIjp7InVpZCI6NTI1NTc3LCsudujeI6IjE4NTQ4NmEwMzBjMGNlMWJjY2IzYWJjMjI2OTYwYzQ4dhdhs.qkuTF2aVPu54S0RFJS_ca7rlHuGz_Fe6kWkBydYQoCg
|
|
</div>
|
|
<Paragraph className="!mt-1 !mb-6 text-type-danger">
|
|
{t("fedapi.setup.tokenExample.warning")}
|
|
</Paragraph>
|
|
<div className="flex justify-end">
|
|
<Button theme="secondary" onClick={exampleModal.hide}>
|
|
{t("fedapi.setup.tokenExample.close")}
|
|
</Button>
|
|
</div>
|
|
</ModalCard>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
async function getdebridTokenStatus(
|
|
debridToken: string | null,
|
|
debridService: string,
|
|
) {
|
|
if (debridToken) {
|
|
const status: Status =
|
|
debridService === "torbox"
|
|
? await testTorboxToken(debridToken)
|
|
: await testdebridToken(debridToken);
|
|
return status;
|
|
}
|
|
return "unset";
|
|
}
|
|
|
|
export function DebridEdit({
|
|
debridToken,
|
|
setdebridToken,
|
|
debridService,
|
|
setdebridService,
|
|
mode = "settings",
|
|
}: DebridProps) {
|
|
const { t } = useTranslation();
|
|
const user = useAuthStore();
|
|
const preferences = usePreferencesStore();
|
|
|
|
// Initialize expansion state for onboarding mode
|
|
const [isDebridExpanded, setIsDebridExpanded] = useState(
|
|
mode === "onboarding" && debridToken !== null && debridToken !== "",
|
|
);
|
|
|
|
// Expand when key is set in onboarding mode
|
|
useEffect(() => {
|
|
if (mode === "onboarding" && debridToken && debridToken.length > 0) {
|
|
setIsDebridExpanded(true);
|
|
}
|
|
}, [debridToken, mode]);
|
|
|
|
// Enable Real Debrid token when account is loaded and we have a token
|
|
useEffect(() => {
|
|
if (user.account && debridToken === null && preferences.debridToken) {
|
|
setdebridToken(preferences.debridToken);
|
|
}
|
|
}, [user.account, debridToken, preferences.debridToken, setdebridToken]);
|
|
|
|
// Determine if content is visible
|
|
const isDebridVisible =
|
|
mode === "onboarding" ? isDebridExpanded : debridToken !== null;
|
|
|
|
// Toggle handler based on mode
|
|
const toggleDebridExpanded = () => {
|
|
if (mode === "onboarding") {
|
|
// Onboarding mode: expand/collapse, preserve key
|
|
if (isDebridExpanded) {
|
|
setdebridToken("");
|
|
setIsDebridExpanded(false);
|
|
} else {
|
|
setIsDebridExpanded(true);
|
|
}
|
|
} else {
|
|
// Settings mode: enable/disable
|
|
setdebridToken(debridToken === null ? "" : null);
|
|
}
|
|
};
|
|
|
|
const [status, setStatus] = useState<Status>("unset");
|
|
const statusMap: Record<Status, StatusCircleProps["type"]> = {
|
|
error: "error",
|
|
success: "success",
|
|
unset: "noresult",
|
|
api_down: "error",
|
|
invalid_token: "error",
|
|
};
|
|
|
|
useEffect(() => {
|
|
const checkTokenStatus = async () => {
|
|
const result = await getdebridTokenStatus(debridToken, debridService);
|
|
setStatus(result);
|
|
};
|
|
checkTokenStatus();
|
|
}, [debridToken, debridService]);
|
|
|
|
if (conf().ALLOW_DEBRID_KEY) {
|
|
return (
|
|
<SettingsCard>
|
|
<div className="flex justify-between items-center gap-4">
|
|
<div className="my-3">
|
|
<p className="text-white font-bold mb-3">{t("debrid.title")}</p>
|
|
<Trans i18nKey="debrid.description">
|
|
<MwLink to="https://real-debrid.com/" />
|
|
{/* fifth's referral code */}
|
|
<MwLink to="https://torbox.app/subscription?referral=3f665ece-0405-4012-9db7-c6f90e8567e1" />
|
|
</Trans>
|
|
<p className="text-type-danger mt-2 max-w-[30rem]">
|
|
{t("debrid.notice")}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Toggle onClick={toggleDebridExpanded} enabled={isDebridVisible} />
|
|
</div>
|
|
</div>
|
|
{isDebridVisible ? (
|
|
<>
|
|
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
|
<p className="text-white font-bold mb-3">
|
|
{t("debrid.tokenLabel")}
|
|
</p>
|
|
<div className="flex md:flex-row flex-col items-center w-full gap-4">
|
|
<div className="flex items-center w-full">
|
|
<StatusCircle type={statusMap[status]} className="mx-2 mr-4" />
|
|
<AuthInputBox
|
|
onChange={(newToken) => {
|
|
setdebridToken(newToken);
|
|
}}
|
|
value={debridToken ?? ""}
|
|
placeholder="ABC123..."
|
|
passwordToggleable
|
|
className="flex-grow"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<Dropdown
|
|
options={[
|
|
{
|
|
id: "realdebrid",
|
|
name: t("debrid.serviceOptions.realdebrid"),
|
|
},
|
|
{
|
|
id: "torbox",
|
|
name: t("debrid.serviceOptions.torbox"),
|
|
},
|
|
]}
|
|
selectedItem={{
|
|
id: debridService,
|
|
name: t(`debrid.serviceOptions.${debridService}`),
|
|
}}
|
|
setSelectedItem={(item) => setdebridService(item.id)}
|
|
direction="up"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{status === "error" && (
|
|
<p className="text-type-danger mt-4">
|
|
{t("debrid.status.failure")}
|
|
</p>
|
|
)}
|
|
{status === "api_down" && (
|
|
<p className="text-type-danger mt-4">
|
|
{t("debrid.status.api_down")}
|
|
</p>
|
|
)}
|
|
{status === "invalid_token" && (
|
|
<p className="text-type-danger mt-4">
|
|
{t("debrid.status.invalid_token")}
|
|
</p>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</SettingsCard>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function TIDBEdit({ tidbKey, setTIDBKey }: TIDBKeyProps) {
|
|
const { t } = useTranslation();
|
|
const preferences = usePreferencesStore();
|
|
const initializedRef = useRef(false);
|
|
|
|
// Enable TIDB key when component loads
|
|
useEffect(() => {
|
|
if (!initializedRef.current && tidbKey === null && preferences.tidbKey) {
|
|
initializedRef.current = true;
|
|
setTIDBKey(preferences.tidbKey);
|
|
}
|
|
}, [tidbKey, preferences.tidbKey, setTIDBKey]);
|
|
|
|
return (
|
|
<SettingsCard>
|
|
<div className="my-3">
|
|
<p className="text-white font-bold mb-3">TheIntroDB</p>
|
|
<p className="max-w-[40rem] font-medium mb-6">
|
|
<Trans i18nKey="settings.connections.tidb.description">
|
|
<MwLink to="https://theintrodb.org/" />
|
|
</Trans>
|
|
</p>
|
|
<p className="text-white font-bold mb-3">
|
|
{t("settings.connections.tidb.tokenLabel")}
|
|
</p>
|
|
<div className="flex items-center w-full">
|
|
<AuthInputBox
|
|
onChange={(newToken) => {
|
|
setTIDBKey(newToken);
|
|
}}
|
|
value={tidbKey ?? ""}
|
|
placeholder="theintrodb:user..."
|
|
passwordToggleable
|
|
className="flex-grow"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</SettingsCard>
|
|
);
|
|
}
|
|
|
|
export function TraktEdit() {
|
|
const { t } = useTranslation();
|
|
const { user, status, logout, error } = useTraktStore();
|
|
const config = conf();
|
|
|
|
const connect = () => {
|
|
const redirectUri =
|
|
config.TRAKT_REDIRECT_URI ??
|
|
`${window.location.origin}${window.location.pathname}`;
|
|
const params = new URLSearchParams({
|
|
response_type: "code",
|
|
client_id: config.TRAKT_CLIENT_ID ?? "",
|
|
redirect_uri: redirectUri,
|
|
});
|
|
window.location.href = `https://trakt.tv/oauth/authorize?${params.toString()}`;
|
|
};
|
|
|
|
if (!config.TRAKT_CLIENT_ID || !config.TRAKT_CLIENT_SECRET) return null;
|
|
|
|
return (
|
|
<SettingsCard>
|
|
<div className="flex justify-between items-center gap-4">
|
|
<div className="my-3">
|
|
<p className="text-white font-bold mb-3">
|
|
{t("settings.connections.trakt.title")}
|
|
</p>
|
|
<p className="max-w-[30rem] font-medium">
|
|
{t("settings.connections.trakt.description")}
|
|
</p>
|
|
{error && <p className="text-type-danger mt-2">{error}</p>}
|
|
</div>
|
|
<div>
|
|
{user ? (
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
{user.images?.avatar?.full && (
|
|
<img
|
|
src={user.images.avatar.full}
|
|
alt={user.username}
|
|
className="w-8 h-8 rounded-full"
|
|
/>
|
|
)}
|
|
<span className="font-bold">{user.name || user.username}</span>
|
|
</div>
|
|
<Button theme="danger" onClick={logout}>
|
|
{t("settings.connections.trakt.disconnect")}
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<Button
|
|
theme="purple"
|
|
onClick={connect}
|
|
disabled={status === "syncing"}
|
|
>
|
|
{status === "syncing"
|
|
? t("settings.connections.trakt.syncing")
|
|
: t("settings.connections.trakt.connect")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</SettingsCard>
|
|
);
|
|
}
|
|
|
|
export function ConnectionsPart(
|
|
props: BackendEditProps &
|
|
ProxyEditProps &
|
|
FebboxKeyProps &
|
|
DebridProps &
|
|
TIDBKeyProps,
|
|
) {
|
|
const { t } = useTranslation();
|
|
const isDesktopApp = useIsDesktopApp();
|
|
return (
|
|
<div>
|
|
<Heading1 border>{t("settings.connections.title")}</Heading1>
|
|
<div className="space-y-6">
|
|
<SetupPart />
|
|
{!isDesktopApp && (
|
|
<ProxyEdit
|
|
proxyUrls={props.proxyUrls}
|
|
setProxyUrls={props.setProxyUrls}
|
|
proxyTmdb={props.proxyTmdb}
|
|
setProxyTmdb={props.setProxyTmdb}
|
|
/>
|
|
)}
|
|
<BackendEdit
|
|
backendUrl={props.backendUrl}
|
|
setBackendUrl={props.setBackendUrl}
|
|
/>
|
|
<FebboxSetup
|
|
febboxKey={props.febboxKey}
|
|
setFebboxKey={props.setFebboxKey}
|
|
mode="settings"
|
|
/>
|
|
<DebridEdit
|
|
debridToken={props.debridToken}
|
|
setdebridToken={props.setdebridToken}
|
|
debridService={props.debridService}
|
|
setdebridService={props.setdebridService}
|
|
mode="settings"
|
|
/>
|
|
<TIDBEdit tidbKey={props.tidbKey} setTIDBKey={props.setTIDBKey} />
|
|
<TraktEdit />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|