import classNames from "classnames"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useAsyncFn } from "react-use"; import { base64ToBuffer, decryptData, encryptData, } from "@/backend/accounts/crypto"; import { getSessions, updateSession } from "@/backend/accounts/sessions"; import { updateSettings } from "@/backend/accounts/settings"; import { editUser } from "@/backend/accounts/user"; import { Button } from "@/components/buttons/Button"; import { WideContainer } from "@/components/layout/WideContainer"; import { UserIcons } from "@/components/UserIcon"; import { Heading1 } from "@/components/utils/Text"; import { Transition } from "@/components/utils/Transition"; import { useAuth } from "@/hooks/auth/useAuth"; import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useIsMobile } from "@/hooks/useIsMobile"; import { useSettingsState } from "@/hooks/useSettingsState"; import { AccountActionsPart } from "@/pages/parts/settings/AccountActionsPart"; import { AccountEditPart } from "@/pages/parts/settings/AccountEditPart"; import { CaptionsPart } from "@/pages/parts/settings/CaptionsPart"; import { ConnectionsPart } from "@/pages/parts/settings/ConnectionsPart"; import { DeviceListPart } from "@/pages/parts/settings/DeviceListPart"; import { RegisterCalloutPart } from "@/pages/parts/settings/RegisterCalloutPart"; import { SidebarPart } from "@/pages/parts/settings/SidebarPart"; import { ThemePart } from "@/pages/parts/settings/ThemePart"; import { PageTitle } from "@/pages/parts/util/PageTitle"; import { AccountWithToken, useAuthStore } from "@/stores/auth"; import { useLanguageStore } from "@/stores/language"; import { usePreferencesStore } from "@/stores/preferences"; import { useSubtitleStore } from "@/stores/subtitles"; import { usePreviewThemeStore, useThemeStore } from "@/stores/theme"; import { SubPageLayout } from "./layouts/SubPageLayout"; import { PreferencesPart } from "./parts/settings/PreferencesPart"; function SettingsLayout(props: { children: React.ReactNode }) { const { isMobile } = useIsMobile(); return ( {props.children} ); } export function AccountSettings(props: { account: AccountWithToken; deviceName: string; setDeviceName: (s: string) => void; colorA: string; setColorA: (s: string) => void; colorB: string; setColorB: (s: string) => void; userIcon: UserIcons; setUserIcon: (s: UserIcons) => void; }) { const url = useBackendUrl(); const { account } = props; const [sessionsResult, execSessions] = useAsyncFn(() => { if (!url) return Promise.resolve([]); return getSessions(url, account); }, [account, url]); useEffect(() => { execSessions(); }, [execSessions]); return ( <> > ); } export function SettingsPage() { const { t } = useTranslation(); const activeTheme = useThemeStore((s) => s.theme); const setTheme = useThemeStore((s) => s.setTheme); const previewTheme = usePreviewThemeStore((s) => s.previewTheme); const setPreviewTheme = usePreviewThemeStore((s) => s.setPreviewTheme); const appLanguage = useLanguageStore((s) => s.language); const setAppLanguage = useLanguageStore((s) => s.setLanguage); const subStyling = useSubtitleStore((s) => s.styling); const setSubStyling = useSubtitleStore((s) => s.updateStyling); const proxySet = useAuthStore((s) => s.proxySet); const setProxySet = useAuthStore((s) => s.setProxySet); const backendUrlSetting = useAuthStore((s) => s.backendUrl); const setBackendUrl = useAuthStore((s) => s.setBackendUrl); const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails); const setEnableThumbnails = usePreferencesStore((s) => s.setEnableThumbnails); const account = useAuthStore((s) => s.account); const updateProfile = useAuthStore((s) => s.setAccountProfile); const updateDeviceName = useAuthStore((s) => s.updateDeviceName); const decryptedName = useMemo(() => { if (!account) return ""; return decryptData(account.deviceName, base64ToBuffer(account.seed)); }, [account]); const backendUrl = useBackendUrl(); const { logout } = useAuth(); const user = useAuthStore(); const state = useSettingsState( activeTheme, appLanguage, subStyling, decryptedName, proxySet, backendUrlSetting, account?.profile, enableThumbnails, ); useEffect(() => { setPreviewTheme(activeTheme ?? "default"); }, [setPreviewTheme, activeTheme]); useEffect(() => { // Clear preview theme on unmount return () => { setPreviewTheme(null); }; }, [setPreviewTheme]); const setThemeWithPreview = useCallback( (theme: string) => { state.theme.set(theme === "default" ? null : theme); setPreviewTheme(theme); }, [state.theme, setPreviewTheme], ); const saveChanges = useCallback(async () => { if (account && backendUrl) { if ( state.appLanguage.changed || state.theme.changed || state.proxyUrls.changed ) { await updateSettings(backendUrl, account, { applicationLanguage: state.appLanguage.state, applicationTheme: state.theme.state, proxyUrls: state.proxyUrls.state?.filter((v) => v !== "") ?? null, }); } if (state.deviceName.changed) { const newDeviceName = await encryptData( state.deviceName.state, base64ToBuffer(account.seed), ); await updateSession(backendUrl, account, { deviceName: newDeviceName, }); updateDeviceName(newDeviceName); } if (state.profile.changed) { await editUser(backendUrl, account, { profile: state.profile.state, }); } } setEnableThumbnails(state.enableThumbnails.state); setAppLanguage(state.appLanguage.state); setTheme(state.theme.state); setSubStyling(state.subtitleStyling.state); setProxySet(state.proxyUrls.state?.filter((v) => v !== "") ?? null); if (state.profile.state) { updateProfile(state.profile.state); } // when backend url gets changed, log the user out first if (state.backendUrl.changed) { await logout(); let url = state.backendUrl.state; if (url && !url.startsWith("http://") && !url.startsWith("https://")) { url = `https://${url}`; } setBackendUrl(url); } }, [ state, account, backendUrl, setEnableThumbnails, setAppLanguage, setTheme, setSubStyling, updateDeviceName, updateProfile, setProxySet, setBackendUrl, logout, ]); return ( {t("settings.account.title")} {user.account && state.profile.state ? ( { state.profile.set((s) => (s ? { ...s, colorA: v } : undefined)); }} colorB={state.profile.state.colorB} setColorB={(v) => state.profile.set((s) => (s ? { ...s, colorB: v } : undefined)) } userIcon={state.profile.state.icon as any} setUserIcon={(v) => state.profile.set((s) => (s ? { ...s, icon: v } : undefined)) } /> ) : ( )} {t("settings.unsaved")} {t("settings.reset")} {t("settings.save")} ); } export default SettingsPage;
{t("settings.unsaved")}