Add custom febbox ui token support

This commit is contained in:
Pas 2025-02-15 16:22:37 -07:00
parent cb10a604ff
commit 9d42c4e8d1
9 changed files with 251 additions and 6 deletions

View file

@ -8,6 +8,7 @@ export interface SettingsInput {
applicationTheme?: string | null;
defaultSubtitleLanguage?: string;
proxyUrls?: string[] | null;
febboxToken?: string | null;
}
export interface SettingsResponse {
@ -15,6 +16,7 @@ export interface SettingsResponse {
applicationLanguage?: string | null;
defaultSubtitleLanguage?: string | null;
proxyUrls?: string[] | null;
febboxToken?: string | null;
}
export function updateSettings(

View file

@ -15,7 +15,12 @@ export function MwLink(props: {
</span>
);
if (isExternal) return <a href={props.url}>{content}</a>;
if (isExternal)
return (
<a href={props.url} target="_blank" rel="noreferrer">
{content}
</a>
);
if (isInternal) return <LinkRouter to={props.to ?? ""}>{content}</LinkRouter>;
return (
<span onClick={() => props.onClick && props.onClick()}>{content}</span>

View file

@ -21,6 +21,7 @@ export function useAuthData() {
const setAccount = useAuthStore((s) => s.setAccount);
const removeAccount = useAuthStore((s) => s.removeAccount);
const setProxySet = useAuthStore((s) => s.setProxySet);
const setFebboxToken = useAuthStore((s) => s.setFebboxToken);
const clearBookmarks = useBookmarkStore((s) => s.clear);
const clearProgress = useProgressStore((s) => s.clear);
const setTheme = useThemeStore((s) => s.setTheme);
@ -85,6 +86,10 @@ export function useAuthData() {
if (settings.proxyUrls) {
setProxySet(settings.proxyUrls);
}
if (settings.febboxToken) {
setFebboxToken(settings.febboxToken);
}
},
[
replaceBookmarks,
@ -93,6 +98,7 @@ export function useAuthData() {
importSubtitleLanguage,
setTheme,
setProxySet,
setFebboxToken,
],
);

View file

@ -43,6 +43,7 @@ export function useSettingsState(
deviceName: string,
proxyUrls: string[] | null,
backendUrl: string | null,
febboxToken: string | null,
profile:
| {
colorA: string;
@ -60,6 +61,12 @@ export function useSettingsState(
useDerived(proxyUrls);
const [backendUrlState, setBackendUrl, resetBackendUrl, backendUrlChanged] =
useDerived(backendUrl);
const [
febboxTokenState,
setFebboxToken,
resetFebboxToken,
febboxTokenChanged,
] = useDerived(febboxToken);
const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme);
const setPreviewTheme = usePreviewThemeStore((s) => s.setPreviewTheme);
const resetPreviewTheme = useCallback(
@ -120,6 +127,7 @@ export function useSettingsState(
resetSubStyling();
resetProxyUrls();
resetBackendUrl();
resetFebboxToken();
resetDeviceName();
resetProfile();
resetEnableThumbnails();
@ -136,6 +144,7 @@ export function useSettingsState(
deviceNameChanged ||
backendUrlChanged ||
proxyUrlsChanged ||
febboxTokenChanged ||
profileChanged ||
enableThumbnailsChanged ||
enableAutoplayChanged ||
@ -176,6 +185,11 @@ export function useSettingsState(
set: setBackendUrl,
changed: backendUrlChanged,
},
febboxToken: {
state: febboxTokenState,
set: setFebboxToken,
changed: febboxTokenChanged,
},
profile: {
state: profileState,
set: setProfileState,

View file

@ -131,6 +131,9 @@ export function SettingsPage() {
const backendUrlSetting = useAuthStore((s) => s.backendUrl);
const setBackendUrl = useAuthStore((s) => s.setBackendUrl);
const febboxToken = useAuthStore((s) => s.febboxToken);
const setFebboxToken = useAuthStore((s) => s.setFebboxToken);
const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails);
const setEnableThumbnails = usePreferencesStore((s) => s.setEnableThumbnails);
@ -168,6 +171,7 @@ export function SettingsPage() {
decryptedName,
proxySet,
backendUrlSetting,
febboxToken,
account?.profile,
enableThumbnails,
enableAutoplay,
@ -216,12 +220,14 @@ export function SettingsPage() {
if (
state.appLanguage.changed ||
state.theme.changed ||
state.proxyUrls.changed
state.proxyUrls.changed ||
state.febboxToken.changed
) {
await updateSettings(backendUrl, account, {
applicationLanguage: state.appLanguage.state,
applicationTheme: state.theme.state,
proxyUrls: state.proxyUrls.state?.filter((v) => v !== "") ?? null,
febboxToken: state.febboxToken.state,
});
}
if (state.deviceName.changed) {
@ -250,6 +256,7 @@ export function SettingsPage() {
setSubStyling(state.subtitleStyling.state);
setProxySet(state.proxyUrls.state?.filter((v) => v !== "") ?? null);
setEnableSourceOrder(state.enableSourceOrder.state);
setFebboxToken(state.febboxToken.state);
if (state.profile.state) {
updateProfile(state.profile.state);
@ -270,6 +277,7 @@ export function SettingsPage() {
account,
backendUrl,
setEnableThumbnails,
setFebboxToken,
state,
setEnableAutoplay,
setEnableDiscover,
@ -352,6 +360,8 @@ export function SettingsPage() {
setBackendUrl={state.backendUrl.set}
proxyUrls={state.proxyUrls.state}
setProxyUrls={state.proxyUrls.set}
febboxToken={state.febboxToken.state}
setFebboxToken={state.febboxToken.set}
/>
</div>
</SettingsLayout>

View file

@ -2,11 +2,16 @@ import { useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { Toggle } from "@/components/buttons/Toggle";
import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard";
import { Stepper } from "@/components/layout/Stepper";
import { BiggerCenterContainer } from "@/components/layout/ThinContainer";
import { VerticalLine } from "@/components/layout/VerticalLine";
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
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 { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
import {
@ -20,10 +25,85 @@ import {
MiniCardContent,
} from "@/pages/onboarding/utils";
import { PageTitle } from "@/pages/parts/util/PageTitle";
import { useAuthStore } from "@/stores/auth";
import { getProxyUrls } from "@/utils/proxyUrls";
import { PopupModal } from "../parts/home/PopupModal";
export function OptionalDropdown() {
const { t } = useTranslation();
const [isExpanded, setIsExpanded] = useState(false);
const febboxToken = useAuthStore((s) => s.febboxToken);
const setFebboxToken = useAuthStore((s) => s.setFebboxToken);
return (
<div className="mt-12">
<SettingsCard>
<div className="flex justify-between items-center gap-4">
<div className="my-3">
<p className="text-white font-bold mb-3">
Optional: FED API (Febbox) UI token
</p>
<p className="max-w-[30rem] font-medium">
<Trans i18nKey="settings.connections.febbox.description">
Bringing your own UI token allows you to get faster 4K streams.
We only have a limited number of tokens, so bringing your own
helps speed your streams when traffic is high.
</Trans>
</p>
</div>
<div>
<Toggle
onClick={() => setIsExpanded(!isExpanded)}
enabled={isExpanded}
/>
</div>
</div>
{isExpanded ? (
<>
<Divider marginClass="my-6 px-8 box-content -mx-8" />
<div className="my-3">
<p className="max-w-[30rem] font-medium">
<Trans i18nKey="settings.connections.febbox.description">
To get your UI token:
<br />
1. Go to <MwLink to="https://febbox.com">
febbox.com
</MwLink>{" "}
and log in with Google
<br />
2. Open DevTools or inspect the page
<br />
3. Go to Application tab Cookies
<br />
4. Copy the &quot;ui&quot; cookie.
<br />
5. Close the tab, but do NOT logout!
</Trans>
</p>
<p className="text-xs mt-2">
(This is not a sensitive login cookie or account token)
</p>
</div>
<Divider marginClass="my-6 px-8 box-content -mx-8" />
<p className="text-white font-bold mb-3">
{t("settings.connections.febbox.tokenLabel", "Token")}
</p>
<AuthInputBox
onChange={(newToken) => {
setFebboxToken(newToken);
}}
value={febboxToken ?? ""}
placeholder="eyABCdE..."
/>
</>
) : null}
</SettingsCard>
</div>
);
}
export function OnboardingPage() {
const navigate = useNavigateOnboarding();
const skipModal = useModal("skip");
@ -97,6 +177,15 @@ export function OnboardingPage() {
might be slower due to shared bandwidth.
<br />
<br />
<strong>Optional FED API (Febbox) UI token</strong>
<br />
Bringing your own UI token allows you to get faster 4K streams.
Each Febbox account has 100gb/mo bandwidth and we only have a
limited ammount of accounts. By bringing your own you get that
all to yourself! This is not an account token and is only used
to get stream links from Febbox&apos;s API.
<br />
<br />
If you have more questions on how this works, feel free to ask
on the{" "}
<a
@ -239,6 +328,8 @@ export function OnboardingPage() {
</Card>
)}
</div>
<OptionalDropdown />
</BiggerCenterContainer>
</MinimalPageLayout>
);

View file

@ -80,6 +80,7 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
defaultSubtitleLanguage: defaultSubtitleLanguage ?? undefined,
applicationTheme: applicationTheme ?? undefined,
proxyUrls: undefined,
febboxToken: undefined,
});
await restore(account);

View file

@ -22,6 +22,11 @@ interface BackendEditProps {
setBackendUrl: Dispatch<SetStateAction<string | null>>;
}
interface FebboxTokenProps {
febboxToken: string | null;
setFebboxToken: Dispatch<SetStateAction<string | null>>;
}
function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
const { t } = useTranslation();
const add = useCallback(() => {
@ -54,7 +59,7 @@ function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
<p className="text-white font-bold mb-3">
{t("settings.connections.workers.label")}
</p>
<p className="max-w-[20rem] font-medium">
<p className="max-w-[30rem] font-medium">
<Trans i18nKey="settings.connections.workers.description">
<MwLink to="https://docs.undi.rest/proxy/deploy">
Proxy documentation
@ -125,7 +130,7 @@ function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
<p className="text-white font-bold mb-3">
{t("settings.connections.server.label")}
</p>
<p className="max-w-[20rem] font-medium">
<p className="max-w-[30rem] font-medium">
<Trans i18nKey="settings.connections.server.description">
<MwLink to="https://docs.undi.rest/backend/deploy">
Backend documentation
@ -135,7 +140,7 @@ function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
{user.account && (
<div>
<br />
<p className="max-w-[20rem] font-medium">
<p className="max-w-[30rem] font-medium">
<Trans i18nKey="settings.connections.server.migration.description">
<MwLink to="/migration">
{t("settings.connections.server.migration.link")}
@ -169,7 +174,75 @@ function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
);
}
export function ConnectionsPart(props: BackendEditProps & ProxyEditProps) {
function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
const { t } = useTranslation();
return (
<SettingsCard>
<div className="flex justify-between items-center gap-4">
<div className="my-3">
<p className="text-white font-bold mb-3">FED API (Febbox) UI token</p>
<p className="max-w-[30rem] font-medium">
<Trans i18nKey="settings.connections.febbox.description">
Bringing your own UI token allows you to get faster 4K streams. We
only have a limited number of tokens, so bringing your own helps
speed your streams when traffic is high.
</Trans>
</p>
</div>
<div>
<Toggle
onClick={() => setFebboxToken((s) => (s === null ? "" : null))}
enabled={febboxToken !== null}
/>
</div>
</div>
{febboxToken !== null ? (
<>
<Divider marginClass="my-6 px-8 box-content -mx-8" />
<div className="my-3">
<p className="max-w-[30rem] font-medium">
<Trans i18nKey="settings.connections.febbox.description">
To get your UI token:
<br />
1. Go to <MwLink to="https://febbox.com">febbox.com</MwLink> and
log in with Google
<br />
2. Open DevTools or inspect the page
<br />
3. Go to Application tab Cookies
<br />
4. Copy the &quot;ui&quot; cookie.
<br />
5. Close the tab, but do NOT logout!
</Trans>
</p>
<p className="text-xs mt-2">
(This is not a sensitive login cookie or account token)
</p>
</div>
<Divider marginClass="my-6 px-8 box-content -mx-8" />
<p className="text-white font-bold mb-3">
{t("settings.connections.febbox.tokenLabel", "Token")}
</p>
<AuthInputBox
onChange={(newToken) => {
setFebboxToken(newToken);
}}
value={febboxToken ?? ""}
placeholder="eyABCdE..."
/>
</>
) : null}
</SettingsCard>
);
}
export function ConnectionsPart(
props: BackendEditProps & ProxyEditProps & FebboxTokenProps,
) {
const { t } = useTranslation();
return (
<div>
@ -184,6 +257,10 @@ export function ConnectionsPart(props: BackendEditProps & ProxyEditProps) {
backendUrl={props.backendUrl}
setBackendUrl={props.setBackendUrl}
/>
<FebboxTokenEdit
febboxToken={props.febboxToken}
setFebboxToken={props.setFebboxToken}
/>
</div>
</div>
);

View file

@ -22,6 +22,7 @@ interface AuthStore {
account: null | AccountWithToken;
backendUrl: null | string;
proxySet: null | string[];
febboxToken: null | string;
removeAccount(): void;
setAccount(acc: AccountWithToken): void;
updateDeviceName(deviceName: string): void;
@ -29,6 +30,7 @@ interface AuthStore {
setAccountProfile(acc: Account["profile"]): void;
setBackendUrl(url: null | string): void;
setProxySet(urls: null | string[]): void;
setFebboxToken(token: null | string): void;
}
export const useAuthStore = create(
@ -37,6 +39,7 @@ export const useAuthStore = create(
account: null,
backendUrl: null,
proxySet: null,
febboxToken: null,
setAccount(acc) {
set((s) => {
s.account = acc;
@ -57,6 +60,20 @@ export const useAuthStore = create(
s.proxySet = urls;
});
},
setFebboxToken(token) {
set((s) => {
s.febboxToken = token;
});
try {
if (token === null) {
localStorage.removeItem("febbox_ui_token");
} else {
localStorage.setItem("febbox_ui_token", token);
}
} catch (e) {
console.warn("Failed to access localStorage:", e);
}
},
setAccountProfile(profile) {
set((s) => {
if (s.account) {
@ -82,6 +99,28 @@ export const useAuthStore = create(
})),
{
name: "__MW::auth",
migrate: (persistedState: any) => {
// Migration from localStorage to Zustand store
if (!persistedState.febboxToken) {
try {
const storedToken = localStorage.getItem("febbox_ui_token");
if (storedToken) persistedState.febboxToken = storedToken;
} catch (e) {
console.warn("LocalStorage access failed during migration:", e);
}
}
return persistedState;
},
onRehydrateStorage: () => (state) => {
// After store rehydration
if (state?.febboxToken) {
try {
localStorage.setItem("febbox_ui_token", state.febboxToken);
} catch (e) {
console.warn("Failed to sync token to localStorage:", e);
}
}
},
},
),
);