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); + } + } + }, }, ), );