diff --git a/src/backend/accounts/settings.ts b/src/backend/accounts/settings.ts
index c749795b..bda2624c 100644
--- a/src/backend/accounts/settings.ts
+++ b/src/backend/accounts/settings.ts
@@ -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(
diff --git a/src/components/text/Link.tsx b/src/components/text/Link.tsx
index 0956b307..b2c6fb93 100644
--- a/src/components/text/Link.tsx
+++ b/src/components/text/Link.tsx
@@ -15,7 +15,12 @@ export function MwLink(props: {
);
- if (isExternal) return {content};
+ if (isExternal)
+ return (
+
+ {content}
+
+ );
if (isInternal) return {content};
return (
props.onClick && props.onClick()}>{content}
diff --git a/src/hooks/auth/useAuthData.ts b/src/hooks/auth/useAuthData.ts
index ae33faac..aa243f4e 100644
--- a/src/hooks/auth/useAuthData.ts
+++ b/src/hooks/auth/useAuthData.ts
@@ -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,
],
);
diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts
index 1fc4891d..bdb782b8 100644
--- a/src/hooks/useSettingsState.ts
+++ b/src/hooks/useSettingsState.ts
@@ -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,
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index 51fe938b..fddc5b73 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -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}
/>
diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx
index da3a7caf..831c4f59 100644
--- a/src/pages/onboarding/Onboarding.tsx
+++ b/src/pages/onboarding/Onboarding.tsx
@@ -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 (
+
+
+
+
+
+ Optional: FED API (Febbox) UI token
+
+
+
+ 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.
+
+
+
+
+ setIsExpanded(!isExpanded)}
+ enabled={isExpanded}
+ />
+
+
+ {isExpanded ? (
+ <>
+
+
+
+
+
+ To get your UI token:
+
+ 1. Go to
+ febbox.com
+ {" "}
+ and log in with Google
+
+ 2. Open DevTools or inspect the page
+
+ 3. Go to Application tab → Cookies
+
+ 4. Copy the "ui" cookie.
+
+ 5. Close the tab, but do NOT logout!
+
+
+
+ (This is not a sensitive login cookie or account token)
+
+
+
+
+
+ {t("settings.connections.febbox.tokenLabel", "Token")}
+
+ {
+ setFebboxToken(newToken);
+ }}
+ value={febboxToken ?? ""}
+ placeholder="eyABCdE..."
+ />
+ >
+ ) : null}
+
+
+ );
+}
export function OnboardingPage() {
const navigate = useNavigateOnboarding();
const skipModal = useModal("skip");
@@ -97,6 +177,15 @@ export function OnboardingPage() {
might be slower due to shared bandwidth.
+ Optional FED API (Febbox) UI token
+
+ 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's API.
+
+
If you have more questions on how this works, feel free to ask
on the{" "}
)}
+
+
);
diff --git a/src/pages/parts/auth/VerifyPassphrasePart.tsx b/src/pages/parts/auth/VerifyPassphrasePart.tsx
index 1d6ea62a..c6b5d672 100644
--- a/src/pages/parts/auth/VerifyPassphrasePart.tsx
+++ b/src/pages/parts/auth/VerifyPassphrasePart.tsx
@@ -80,6 +80,7 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
defaultSubtitleLanguage: defaultSubtitleLanguage ?? undefined,
applicationTheme: applicationTheme ?? undefined,
proxyUrls: undefined,
+ febboxToken: undefined,
});
await restore(account);
diff --git a/src/pages/parts/settings/ConnectionsPart.tsx b/src/pages/parts/settings/ConnectionsPart.tsx
index bbbc8eb4..efc72cff 100644
--- a/src/pages/parts/settings/ConnectionsPart.tsx
+++ b/src/pages/parts/settings/ConnectionsPart.tsx
@@ -22,6 +22,11 @@ interface BackendEditProps {
setBackendUrl: Dispatch>;
}
+interface FebboxTokenProps {
+ febboxToken: string | null;
+ setFebboxToken: Dispatch>;
+}
+
function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
const { t } = useTranslation();
const add = useCallback(() => {
@@ -54,7 +59,7 @@ function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
{t("settings.connections.workers.label")}
-
+
Proxy documentation
@@ -125,7 +130,7 @@ function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
{t("settings.connections.server.label")}
-
+
Backend documentation
@@ -135,7 +140,7 @@ function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
{user.account && (
-
+
{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 (
+
+
+
+
FED API (Febbox) UI token
+
+
+ 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.
+
+
+
+
+ setFebboxToken((s) => (s === null ? "" : null))}
+ enabled={febboxToken !== null}
+ />
+
+
+ {febboxToken !== null ? (
+ <>
+
+
+
+
+
+ To get your UI token:
+
+ 1. Go to febbox.com and
+ log in with Google
+
+ 2. Open DevTools or inspect the page
+
+ 3. Go to Application tab → Cookies
+
+ 4. Copy the "ui" cookie.
+
+ 5. Close the tab, but do NOT logout!
+
+
+
+ (This is not a sensitive login cookie or account token)
+
+
+
+
+
+ {t("settings.connections.febbox.tokenLabel", "Token")}
+
+ {
+ setFebboxToken(newToken);
+ }}
+ value={febboxToken ?? ""}
+ placeholder="eyABCdE..."
+ />
+ >
+ ) : null}
+
+ );
+}
+
+export function ConnectionsPart(
+ props: BackendEditProps & ProxyEditProps & FebboxTokenProps,
+) {
const { t } = useTranslation();
return (
@@ -184,6 +257,10 @@ export function ConnectionsPart(props: BackendEditProps & ProxyEditProps) {
backendUrl={props.backendUrl}
setBackendUrl={props.setBackendUrl}
/>
+
);
diff --git a/src/stores/auth/index.ts b/src/stores/auth/index.ts
index 0a3c007a..6bf17383 100644
--- a/src/stores/auth/index.ts
+++ b/src/stores/auth/index.ts
@@ -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);
+ }
+ }
+ },
},
),
);