mirror of
https://github.com/sussy-code/smov.git
synced 2025-01-17 01:51:24 +01:00
make settings page fully functional
This commit is contained in:
parent
b38e5768e3
commit
a9abe14810
12 changed files with 359 additions and 121 deletions
|
@ -12,6 +12,10 @@ export interface SessionResponse {
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionUpdate {
|
||||||
|
deviceName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSessions(url: string, account: AccountWithToken) {
|
export async function getSessions(url: string, account: AccountWithToken) {
|
||||||
return ofetch<SessionResponse[]>(`/users/${account.userId}/sessions`, {
|
return ofetch<SessionResponse[]>(`/users/${account.userId}/sessions`, {
|
||||||
headers: getAuthHeaders(account.token),
|
headers: getAuthHeaders(account.token),
|
||||||
|
@ -19,6 +23,19 @@ export async function getSessions(url: string, account: AccountWithToken) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateSession(
|
||||||
|
url: string,
|
||||||
|
account: AccountWithToken,
|
||||||
|
update: SessionUpdate
|
||||||
|
) {
|
||||||
|
return ofetch<SessionResponse[]>(`/sessions/${account.sessionId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: getAuthHeaders(account.token),
|
||||||
|
body: update,
|
||||||
|
baseURL: url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function removeSession(
|
export async function removeSession(
|
||||||
url: string,
|
url: string,
|
||||||
token: string,
|
token: string,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { AccountWithToken } from "@/stores/auth";
|
||||||
|
|
||||||
export interface SettingsInput {
|
export interface SettingsInput {
|
||||||
applicationLanguage?: string;
|
applicationLanguage?: string;
|
||||||
applicationTheme?: string;
|
applicationTheme?: string | null;
|
||||||
defaultSubtitleLanguage?: string;
|
defaultSubtitleLanguage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,14 @@ export interface UserResponse {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserEdit {
|
||||||
|
profile?: {
|
||||||
|
colorA: string;
|
||||||
|
colorB: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface BookmarkResponse {
|
export interface BookmarkResponse {
|
||||||
tmdbId: string;
|
tmdbId: string;
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -122,6 +130,22 @@ export async function getUser(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function editUser(
|
||||||
|
url: string,
|
||||||
|
account: AccountWithToken,
|
||||||
|
object: UserEdit
|
||||||
|
): Promise<{ user: UserResponse; session: SessionResponse }> {
|
||||||
|
return ofetch<{ user: UserResponse; session: SessionResponse }>(
|
||||||
|
`/users/${account.userId}`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
headers: getAuthHeaders(account.token),
|
||||||
|
body: object,
|
||||||
|
baseURL: url,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteUser(
|
export async function deleteUser(
|
||||||
url: string,
|
url: string,
|
||||||
account: AccountWithToken
|
account: AccountWithToken
|
||||||
|
|
|
@ -9,20 +9,31 @@ export interface AvatarProps {
|
||||||
profile: AccountProfile["profile"];
|
profile: AccountProfile["profile"];
|
||||||
sizeClass?: string;
|
sizeClass?: string;
|
||||||
iconClass?: string;
|
iconClass?: string;
|
||||||
|
bottom?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Avatar(props: AvatarProps) {
|
export function Avatar(props: AvatarProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative inline-block">
|
||||||
className={classNames(
|
<div
|
||||||
props.sizeClass,
|
className={classNames(
|
||||||
"rounded-full overflow-hidden flex items-center justify-center text-white"
|
props.sizeClass,
|
||||||
)}
|
"rounded-full overflow-hidden flex items-center justify-center text-white"
|
||||||
style={{
|
)}
|
||||||
background: `linear-gradient(to bottom right, ${props.profile.colorA}, ${props.profile.colorB})`,
|
style={{
|
||||||
}}
|
background: `linear-gradient(to bottom right, ${props.profile.colorA}, ${props.profile.colorB})`,
|
||||||
>
|
}}
|
||||||
<UserIcon className={props.iconClass} icon={props.profile.icon as any} />
|
>
|
||||||
|
<UserIcon
|
||||||
|
className={props.iconClass}
|
||||||
|
icon={props.profile.icon as any}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{props.bottom ? (
|
||||||
|
<div className="absolute bottom-0 left-1/2 transform translate-y-1/2 -translate-x-1/2">
|
||||||
|
{props.bottom}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,18 +46,12 @@ export function UserAvatar(props: {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
if (!auth.account) return null;
|
if (!auth.account) return null;
|
||||||
return (
|
return (
|
||||||
<div className="relative inline-block">
|
<Avatar
|
||||||
<Avatar
|
profile={auth.account.profile}
|
||||||
profile={auth.account.profile}
|
sizeClass={props.sizeClass ?? "w-[2rem] h-[2rem]"}
|
||||||
sizeClass={props.sizeClass ?? "w-[2rem] h-[2rem]"}
|
iconClass={props.iconClass}
|
||||||
iconClass={props.iconClass}
|
bottom={props.bottom}
|
||||||
/>
|
/>
|
||||||
{props.bottom ? (
|
|
||||||
<div className="absolute bottom-0 left-1/2 transform translate-y-1/2 -translate-x-1/2">
|
|
||||||
{props.bottom}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useCopyToClipboard, useMountedState } from "react-use";
|
||||||
|
|
||||||
import { Icon, Icons } from "./Icon";
|
import { Icon, Icons } from "./Icon";
|
||||||
|
|
||||||
export function PassphaseDisplay(props: { mnemonic: string }) {
|
export function PassphraseDisplay(props: { mnemonic: string }) {
|
||||||
const individualWords = props.mnemonic.split(" ");
|
const individualWords = props.mnemonic.split(" ");
|
||||||
|
|
||||||
const [, copy] = useCopyToClipboard();
|
const [, copy] = useCopyToClipboard();
|
||||||
|
@ -23,7 +23,7 @@ export function PassphaseDisplay(props: { mnemonic: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-authentication-border/50 ">
|
<div className="rounded-lg border border-authentication-border/50 ">
|
||||||
<div className="px-4 py-2 flex justify-between border-b border-authentication-border/50">
|
<div className="px-4 py-2 flex justify-between border-b border-authentication-border/50">
|
||||||
<p className="font-bold text-sm text-white">Passphase</p>
|
<p className="font-bold text-sm text-white">Passphrase</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-authentication-copyText hover:text-authentication-copyTextHover transition-colors flex gap-2 items-center cursor-pointer"
|
className="text-authentication-copyText hover:text-authentication-copyTextHover transition-colors flex gap-2 items-center cursor-pointer"
|
||||||
|
@ -37,10 +37,12 @@ export function PassphaseDisplay(props: { mnemonic: string }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-4 grid grid-cols-4 gap-2">
|
<div className="px-4 py-4 grid grid-cols-4 gap-2">
|
||||||
{individualWords.map((word) => (
|
{individualWords.map((word, i) => (
|
||||||
<div
|
<div
|
||||||
className="px-4 rounded-md py-2 bg-authentication-wordBackground text-white font-medium text-center"
|
className="px-4 rounded-md py-2 bg-authentication-wordBackground text-white font-medium text-center"
|
||||||
key={word}
|
// this doesn't get rerendered nor does it have state so its fine
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={i}
|
||||||
>
|
>
|
||||||
{word}
|
{word}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
import isEqual from "lodash.isequal";
|
import isEqual from "lodash.isequal";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
import { SubtitleStyling } from "@/stores/subtitles";
|
import { SubtitleStyling } from "@/stores/subtitles";
|
||||||
|
|
||||||
export function useDerived<T>(
|
export function useDerived<T>(
|
||||||
initial: T
|
initial: T
|
||||||
): [T, (v: T) => void, () => void, boolean] {
|
): [T, Dispatch<SetStateAction<T>>, () => void, boolean] {
|
||||||
const [overwrite, setOverwrite] = useState<T | undefined>(undefined);
|
const [overwrite, setOverwrite] = useState<T | undefined>(undefined);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOverwrite(undefined);
|
setOverwrite(undefined);
|
||||||
|
@ -14,19 +21,39 @@ export function useDerived<T>(
|
||||||
() => !isEqual(overwrite, initial) && overwrite !== undefined,
|
() => !isEqual(overwrite, initial) && overwrite !== undefined,
|
||||||
[overwrite, initial]
|
[overwrite, initial]
|
||||||
);
|
);
|
||||||
|
const setter = useCallback<Dispatch<SetStateAction<T>>>(
|
||||||
|
(inp) => {
|
||||||
|
if (!(inp instanceof Function)) setOverwrite(inp);
|
||||||
|
else setOverwrite((s) => inp(s ?? initial));
|
||||||
|
},
|
||||||
|
[initial, setOverwrite]
|
||||||
|
);
|
||||||
const data = overwrite === undefined ? initial : overwrite;
|
const data = overwrite === undefined ? initial : overwrite;
|
||||||
|
|
||||||
const reset = useCallback(() => setOverwrite(undefined), [setOverwrite]);
|
const reset = useCallback(() => setOverwrite(undefined), [setOverwrite]);
|
||||||
|
|
||||||
return [data, setOverwrite, reset, changed];
|
return [data, setter, reset, changed];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSettingsState(
|
export function useSettingsState(
|
||||||
theme: string | null,
|
theme: string | null,
|
||||||
appLanguage: string,
|
appLanguage: string,
|
||||||
subtitleStyling: SubtitleStyling,
|
subtitleStyling: SubtitleStyling,
|
||||||
deviceName?: string
|
deviceName: string,
|
||||||
|
proxyUrls: string[] | null,
|
||||||
|
backendUrl: string | null,
|
||||||
|
profile:
|
||||||
|
| {
|
||||||
|
colorA: string;
|
||||||
|
colorB: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
) {
|
) {
|
||||||
|
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
||||||
|
useDerived(proxyUrls);
|
||||||
|
const [backendUrlState, setBackendUrl, resetBackendUrl, backendUrlChanged] =
|
||||||
|
useDerived(backendUrl);
|
||||||
const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme);
|
const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme);
|
||||||
const [
|
const [
|
||||||
appLanguageState,
|
appLanguageState,
|
||||||
|
@ -42,22 +69,27 @@ export function useSettingsState(
|
||||||
resetDeviceName,
|
resetDeviceName,
|
||||||
deviceNameChanged,
|
deviceNameChanged,
|
||||||
] = useDerived(deviceName);
|
] = useDerived(deviceName);
|
||||||
|
const [profileState, setProfileState, resetProfile, profileChanged] =
|
||||||
|
useDerived(profile);
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
resetTheme();
|
resetTheme();
|
||||||
resetAppLanguage();
|
resetAppLanguage();
|
||||||
resetSubStyling();
|
resetSubStyling();
|
||||||
|
resetProxyUrls();
|
||||||
|
resetBackendUrl();
|
||||||
resetDeviceName();
|
resetDeviceName();
|
||||||
|
resetProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
const changed = useMemo(
|
const changed =
|
||||||
() =>
|
themeChanged ||
|
||||||
themeChanged ||
|
appLanguageChanged ||
|
||||||
appLanguageChanged ||
|
subStylingChanged ||
|
||||||
subStylingChanged ||
|
deviceNameChanged ||
|
||||||
deviceNameChanged,
|
backendUrlChanged ||
|
||||||
[themeChanged, appLanguageChanged, subStylingChanged, deviceNameChanged]
|
proxyUrlsChanged ||
|
||||||
);
|
profileChanged;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reset,
|
reset,
|
||||||
|
@ -65,18 +97,37 @@ export function useSettingsState(
|
||||||
theme: {
|
theme: {
|
||||||
state: themeState,
|
state: themeState,
|
||||||
set: setTheme,
|
set: setTheme,
|
||||||
|
changed: themeChanged,
|
||||||
},
|
},
|
||||||
appLanguage: {
|
appLanguage: {
|
||||||
state: appLanguageState,
|
state: appLanguageState,
|
||||||
set: setAppLanguage,
|
set: setAppLanguage,
|
||||||
|
changed: appLanguageChanged,
|
||||||
},
|
},
|
||||||
subtitleStyling: {
|
subtitleStyling: {
|
||||||
state: subStylingState,
|
state: subStylingState,
|
||||||
set: setSubStyling,
|
set: setSubStyling,
|
||||||
|
changed: subStylingChanged,
|
||||||
},
|
},
|
||||||
deviceName: {
|
deviceName: {
|
||||||
state: deviceNameState,
|
state: deviceNameState,
|
||||||
set: setDeviceNameState,
|
set: setDeviceNameState,
|
||||||
|
changed: deviceNameChanged,
|
||||||
|
},
|
||||||
|
proxyUrls: {
|
||||||
|
state: proxyUrlsState,
|
||||||
|
set: setProxyUrls,
|
||||||
|
changed: proxyUrlsChanged,
|
||||||
|
},
|
||||||
|
backendUrl: {
|
||||||
|
state: backendUrlState,
|
||||||
|
set: setBackendUrl,
|
||||||
|
changed: backendUrlChanged,
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
state: profileState,
|
||||||
|
set: setProfileState,
|
||||||
|
changed: profileChanged,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,19 @@ import classNames from "classnames";
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
|
import {
|
||||||
import { getSessions } from "@/backend/accounts/sessions";
|
base64ToBuffer,
|
||||||
|
decryptData,
|
||||||
|
encryptData,
|
||||||
|
} from "@/backend/accounts/crypto";
|
||||||
|
import { getSessions, updateSession } from "@/backend/accounts/sessions";
|
||||||
import { updateSettings } from "@/backend/accounts/settings";
|
import { updateSettings } from "@/backend/accounts/settings";
|
||||||
|
import { editUser } from "@/backend/accounts/user";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { WideContainer } from "@/components/layout/WideContainer";
|
import { WideContainer } from "@/components/layout/WideContainer";
|
||||||
|
import { UserIcons } from "@/components/UserIcon";
|
||||||
import { Heading1 } from "@/components/utils/Text";
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
|
import { useAuth } from "@/hooks/auth/useAuth";
|
||||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { useSettingsState } from "@/hooks/useSettingsState";
|
import { useSettingsState } from "@/hooks/useSettingsState";
|
||||||
|
@ -45,7 +52,17 @@ function SettingsLayout(props: { children: React.ReactNode }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccountSettings(props: { account: AccountWithToken }) {
|
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 url = useBackendUrl();
|
||||||
const { account } = props;
|
const { account } = props;
|
||||||
const [sessionsResult, execSessions] = useAsyncFn(() => {
|
const [sessionsResult, execSessions] = useAsyncFn(() => {
|
||||||
|
@ -57,7 +74,16 @@ export function AccountSettings(props: { account: AccountWithToken }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AccountEditPart />
|
<AccountEditPart
|
||||||
|
deviceName={props.deviceName}
|
||||||
|
setDeviceName={props.setDeviceName}
|
||||||
|
colorA={props.colorA}
|
||||||
|
setColorA={props.setColorA}
|
||||||
|
colorB={props.colorB}
|
||||||
|
setColorB={props.setColorB}
|
||||||
|
userIcon={props.userIcon}
|
||||||
|
setUserIcon={props.setUserIcon}
|
||||||
|
/>
|
||||||
<DeviceListPart
|
<DeviceListPart
|
||||||
error={!!sessionsResult.error}
|
error={!!sessionsResult.error}
|
||||||
loading={sessionsResult.loading}
|
loading={sessionsResult.loading}
|
||||||
|
@ -79,7 +105,15 @@ export function SettingsPage() {
|
||||||
const subStyling = useSubtitleStore((s) => s.styling);
|
const subStyling = useSubtitleStore((s) => s.styling);
|
||||||
const setSubStyling = useSubtitleStore((s) => s.updateStyling);
|
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 account = useAuthStore((s) => s.account);
|
const account = useAuthStore((s) => s.account);
|
||||||
|
const updateProfile = useAuthStore((s) => s.setAccountProfile);
|
||||||
|
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
|
||||||
const decryptedName = useMemo(() => {
|
const decryptedName = useMemo(() => {
|
||||||
if (!account) return "";
|
if (!account) return "";
|
||||||
return decryptData(account.deviceName, base64ToBuffer(account.seed));
|
return decryptData(account.deviceName, base64ToBuffer(account.seed));
|
||||||
|
@ -87,29 +121,71 @@ export function SettingsPage() {
|
||||||
|
|
||||||
const backendUrl = useBackendUrl();
|
const backendUrl = useBackendUrl();
|
||||||
|
|
||||||
|
const { logout } = useAuth();
|
||||||
const user = useAuthStore();
|
const user = useAuthStore();
|
||||||
|
|
||||||
const state = useSettingsState(
|
const state = useSettingsState(
|
||||||
activeTheme,
|
activeTheme,
|
||||||
appLanguage,
|
appLanguage,
|
||||||
subStyling,
|
subStyling,
|
||||||
decryptedName
|
decryptedName,
|
||||||
|
proxySet,
|
||||||
|
backendUrlSetting,
|
||||||
|
account?.profile
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveChanges = useCallback(async () => {
|
const saveChanges = useCallback(async () => {
|
||||||
console.log(state);
|
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
await updateSettings(backendUrl, account, {
|
if (state.appLanguage.changed || state.theme.changed) {
|
||||||
applicationLanguage: state.appLanguage.state,
|
await updateSettings(backendUrl, account, {
|
||||||
applicationTheme: state.theme.state ?? undefined,
|
applicationLanguage: state.appLanguage.state,
|
||||||
});
|
applicationTheme: state.theme.state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setAppLanguage(state.appLanguage.state);
|
setAppLanguage(state.appLanguage.state);
|
||||||
setTheme(state.theme.state);
|
setTheme(state.theme.state);
|
||||||
setSubStyling(state.subtitleStyling.state);
|
setSubStyling(state.subtitleStyling.state);
|
||||||
}, [state, account, backendUrl, setAppLanguage, setTheme, setSubStyling]);
|
setProxySet(state.proxyUrls.state);
|
||||||
|
|
||||||
|
if (state.profile.state) {
|
||||||
|
updateProfile(state.profile.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// when backend url gets changed, log the user out first
|
||||||
|
if (state.backendUrl.changed) {
|
||||||
|
await logout();
|
||||||
|
setBackendUrl(state.backendUrl.state);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
state,
|
||||||
|
account,
|
||||||
|
backendUrl,
|
||||||
|
setAppLanguage,
|
||||||
|
setTheme,
|
||||||
|
setSubStyling,
|
||||||
|
updateDeviceName,
|
||||||
|
updateProfile,
|
||||||
|
setProxySet,
|
||||||
|
setBackendUrl,
|
||||||
|
logout,
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
|
@ -117,8 +193,24 @@ export function SettingsPage() {
|
||||||
<Heading1 border className="!mb-0">
|
<Heading1 border className="!mb-0">
|
||||||
Account
|
Account
|
||||||
</Heading1>
|
</Heading1>
|
||||||
{user.account ? (
|
{user.account && state.profile.state ? (
|
||||||
<AccountSettings account={user.account} />
|
<AccountSettings
|
||||||
|
account={user.account}
|
||||||
|
deviceName={state.deviceName.state}
|
||||||
|
setDeviceName={state.deviceName.set}
|
||||||
|
colorA={state.profile.state.colorA}
|
||||||
|
setColorA={(v) => {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RegisterCalloutPart />
|
<RegisterCalloutPart />
|
||||||
)}
|
)}
|
||||||
|
@ -139,7 +231,12 @@ export function SettingsPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="settings-connection" className="mt-48">
|
<div id="settings-connection" className="mt-48">
|
||||||
<ConnectionsPart />
|
<ConnectionsPart
|
||||||
|
backendUrl={state.backendUrl.state}
|
||||||
|
setBackendUrl={state.backendUrl.set}
|
||||||
|
proxyUrls={state.proxyUrls.state}
|
||||||
|
setProxyUrls={state.proxyUrls.set}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
LargeCardButtons,
|
LargeCardButtons,
|
||||||
LargeCardText,
|
LargeCardText,
|
||||||
} from "@/components/layout/LargeCard";
|
} from "@/components/layout/LargeCard";
|
||||||
import { PassphaseDisplay } from "@/components/PassphraseDisplay";
|
import { PassphraseDisplay } from "@/components/PassphraseDisplay";
|
||||||
|
|
||||||
interface PassphraseGeneratePartProps {
|
interface PassphraseGeneratePartProps {
|
||||||
onNext?: (mnemonic: string) => void;
|
onNext?: (mnemonic: string) => void;
|
||||||
|
@ -23,7 +23,7 @@ export function PassphraseGeneratePart(props: PassphraseGeneratePartProps) {
|
||||||
If you lose this, you're a silly goose and will be posted on the
|
If you lose this, you're a silly goose and will be posted on the
|
||||||
wall of shame™️
|
wall of shame™️
|
||||||
</LargeCardText>
|
</LargeCardText>
|
||||||
<PassphaseDisplay mnemonic={mnemonic} />
|
<PassphraseDisplay mnemonic={mnemonic} />
|
||||||
|
|
||||||
<LargeCardButtons>
|
<LargeCardButtons>
|
||||||
<Button theme="purple" onClick={() => props.onNext?.(mnemonic)}>
|
<Button theme="purple" onClick={() => props.onNext?.(mnemonic)}>
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
import { UserAvatar } from "@/components/Avatar";
|
import { Avatar } from "@/components/Avatar";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { SettingsCard } from "@/components/layout/SettingsCard";
|
import { SettingsCard } from "@/components/layout/SettingsCard";
|
||||||
import { useModal } from "@/components/overlays/Modal";
|
import { useModal } from "@/components/overlays/Modal";
|
||||||
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
||||||
|
import { UserIcons } from "@/components/UserIcon";
|
||||||
import { useAuth } from "@/hooks/auth/useAuth";
|
import { useAuth } from "@/hooks/auth/useAuth";
|
||||||
import { ProfileEditModal } from "@/pages/parts/settings/ProfileEditModal";
|
import { ProfileEditModal } from "@/pages/parts/settings/ProfileEditModal";
|
||||||
|
|
||||||
export function AccountEditPart() {
|
export function AccountEditPart(props: {
|
||||||
|
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 { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
const profileEditModal = useModal("profile-edit");
|
const profileEditModal = useModal("profile-edit");
|
||||||
|
|
||||||
|
@ -16,10 +26,21 @@ export function AccountEditPart() {
|
||||||
<ProfileEditModal
|
<ProfileEditModal
|
||||||
id={profileEditModal.id}
|
id={profileEditModal.id}
|
||||||
close={profileEditModal.hide}
|
close={profileEditModal.hide}
|
||||||
|
colorA={props.colorA}
|
||||||
|
setColorA={props.setColorA}
|
||||||
|
colorB={props.colorB}
|
||||||
|
setColorB={props.setColorB}
|
||||||
|
userIcon={props.userIcon}
|
||||||
|
setUserIcon={props.setUserIcon}
|
||||||
/>
|
/>
|
||||||
<div className="grid lg:grid-cols-[auto,1fr] gap-8">
|
<div className="grid lg:grid-cols-[auto,1fr] gap-8">
|
||||||
<div>
|
<div>
|
||||||
<UserAvatar
|
<Avatar
|
||||||
|
profile={{
|
||||||
|
colorA: props.colorA,
|
||||||
|
colorB: props.colorB,
|
||||||
|
icon: props.userIcon,
|
||||||
|
}}
|
||||||
iconClass="text-5xl"
|
iconClass="text-5xl"
|
||||||
sizeClass="w-32 h-32"
|
sizeClass="w-32 h-32"
|
||||||
bottom={
|
bottom={
|
||||||
|
@ -36,9 +57,13 @@ export function AccountEditPart() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="space-y-8 max-w-xs">
|
<div className="space-y-8 max-w-xs">
|
||||||
<AuthInputBox label="Device name" placeholder="Fremen tablet" />
|
<AuthInputBox
|
||||||
|
label="Device name"
|
||||||
|
placeholder="Fremen tablet"
|
||||||
|
value={props.deviceName}
|
||||||
|
onChange={(value) => props.setDeviceName(value)}
|
||||||
|
/>
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Button theme="purple">Save account</Button>
|
|
||||||
<Button theme="danger" onClick={logout}>
|
<Button theme="danger" onClick={logout}>
|
||||||
Log out
|
Log out
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useState } from "react";
|
import { Dispatch, SetStateAction, useCallback } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Toggle } from "@/components/buttons/Toggle";
|
import { Toggle } from "@/components/buttons/Toggle";
|
||||||
|
@ -8,45 +8,38 @@ import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
||||||
import { Divider } from "@/components/utils/Divider";
|
import { Divider } from "@/components/utils/Divider";
|
||||||
import { Heading1 } from "@/components/utils/Text";
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
|
|
||||||
let idNum = 0;
|
interface ProxyEditProps {
|
||||||
|
proxyUrls: string[] | null;
|
||||||
interface ProxyItem {
|
setProxyUrls: Dispatch<SetStateAction<string[] | null>>;
|
||||||
url: string;
|
|
||||||
id: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProxyEdit() {
|
interface BackendEditProps {
|
||||||
const [customWorkers, setCustomWorkers] = useState<ProxyItem[] | null>(null);
|
backendUrl: string | null;
|
||||||
|
setBackendUrl: Dispatch<SetStateAction<string | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
|
||||||
const add = useCallback(() => {
|
const add = useCallback(() => {
|
||||||
idNum += 1;
|
setProxyUrls((s) => [...(s ?? []), ""]);
|
||||||
setCustomWorkers((s) => [
|
}, [setProxyUrls]);
|
||||||
...(s ?? []),
|
|
||||||
{
|
|
||||||
id: idNum,
|
|
||||||
url: "",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}, [setCustomWorkers]);
|
|
||||||
|
|
||||||
const changeItem = useCallback(
|
const changeItem = useCallback(
|
||||||
(id: number, val: string) => {
|
(index: number, val: string) => {
|
||||||
setCustomWorkers((s) => [
|
setProxyUrls((s) => [
|
||||||
...(s ?? []).map((v) => {
|
...(s ?? []).map((v, i) => {
|
||||||
if (v.id !== id) return v;
|
if (i !== index) return v;
|
||||||
v.url = val;
|
return val;
|
||||||
return v;
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
[setCustomWorkers]
|
[setProxyUrls]
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeItem = useCallback(
|
const removeItem = useCallback(
|
||||||
(id: number) => {
|
(index: number) => {
|
||||||
setCustomWorkers((s) => [...(s ?? []).filter((v) => v.id !== id)]);
|
setProxyUrls((s) => [...(s ?? []).filter((v, i) => i !== index)]);
|
||||||
},
|
},
|
||||||
[setCustomWorkers]
|
[setProxyUrls]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -61,30 +54,35 @@ function ProxyEdit() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Toggle
|
<Toggle
|
||||||
onClick={() => setCustomWorkers((s) => (s === null ? [] : null))}
|
onClick={() => setProxyUrls((s) => (s === null ? [] : null))}
|
||||||
enabled={customWorkers !== null}
|
enabled={proxyUrls !== null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{customWorkers !== null ? (
|
{proxyUrls !== null ? (
|
||||||
<>
|
<>
|
||||||
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
||||||
<p className="text-white font-bold mb-3">Worker URLs</p>
|
<p className="text-white font-bold mb-3">Worker URLs</p>
|
||||||
|
|
||||||
<div className="my-6 space-y-2 max-w-md">
|
<div className="my-6 space-y-2 max-w-md">
|
||||||
{(customWorkers?.length ?? 0) === 0 ? (
|
{(proxyUrls?.length ?? 0) === 0 ? (
|
||||||
<p>No workers yet, add one below</p>
|
<p>No workers yet, add one below</p>
|
||||||
) : null}
|
) : null}
|
||||||
{(customWorkers ?? []).map((v) => (
|
{(proxyUrls ?? []).map((v, i) => (
|
||||||
<div className="grid grid-cols-[1fr,auto] items-center gap-2">
|
<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
|
<AuthInputBox
|
||||||
value={v.url}
|
value={v}
|
||||||
onChange={(val) => changeItem(v.id, val)}
|
onChange={(val) => changeItem(i, val)}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeItem(v.id)}
|
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"
|
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} />
|
<Icon className="text-xl" icon={Icons.X} />
|
||||||
|
@ -102,9 +100,7 @@ function ProxyEdit() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BackendEdit() {
|
function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
|
||||||
const [customBackendUrl, setCustomBackendUrl] = useState<string | null>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsCard>
|
<SettingsCard>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
|
@ -117,32 +113,35 @@ function BackendEdit() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Toggle
|
<Toggle
|
||||||
onClick={() => setCustomBackendUrl((s) => (s === null ? "" : null))}
|
onClick={() => setBackendUrl((s) => (s === null ? "" : null))}
|
||||||
enabled={customBackendUrl !== null}
|
enabled={backendUrl !== null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{customBackendUrl !== null ? (
|
{backendUrl !== null ? (
|
||||||
<>
|
<>
|
||||||
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
||||||
<p className="text-white font-bold mb-3">Custom server URL</p>
|
<p className="text-white font-bold mb-3">Custom server URL</p>
|
||||||
<AuthInputBox
|
<AuthInputBox onChange={setBackendUrl} value={backendUrl ?? ""} />
|
||||||
onChange={setCustomBackendUrl}
|
|
||||||
value={customBackendUrl ?? ""}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConnectionsPart() {
|
export function ConnectionsPart(props: BackendEditProps & ProxyEditProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Heading1 border>Connections</Heading1>
|
<Heading1 border>Connections</Heading1>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<ProxyEdit />
|
<ProxyEdit
|
||||||
<BackendEdit />
|
proxyUrls={props.proxyUrls}
|
||||||
|
setProxyUrls={props.setProxyUrls}
|
||||||
|
/>
|
||||||
|
<BackendEdit
|
||||||
|
backendUrl={props.backendUrl}
|
||||||
|
setBackendUrl={props.setBackendUrl}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { ColorPicker } from "@/components/form/ColorPicker";
|
import { ColorPicker } from "@/components/form/ColorPicker";
|
||||||
import { IconPicker } from "@/components/form/IconPicker";
|
import { IconPicker } from "@/components/form/IconPicker";
|
||||||
|
@ -10,28 +8,34 @@ import { Heading2 } from "@/components/utils/Text";
|
||||||
export interface ProfileEditModalProps {
|
export interface ProfileEditModalProps {
|
||||||
id: string;
|
id: string;
|
||||||
close?: () => void;
|
close?: () => void;
|
||||||
|
colorA: string;
|
||||||
|
setColorA: (s: string) => void;
|
||||||
|
colorB: string;
|
||||||
|
setColorB: (s: string) => void;
|
||||||
|
userIcon: UserIcons;
|
||||||
|
setUserIcon: (s: UserIcons) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileEditModal(props: ProfileEditModalProps) {
|
export function ProfileEditModal(props: ProfileEditModalProps) {
|
||||||
const [colorA, setColorA] = useState("#2E65CF");
|
|
||||||
const [colorB, setColorB] = useState("#2E65CF");
|
|
||||||
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal id={props.id}>
|
<Modal id={props.id}>
|
||||||
<ModalCard>
|
<ModalCard>
|
||||||
<Heading2 className="!mt-0">Edit profile picture</Heading2>
|
<Heading2 className="!mt-0">Edit profile picture</Heading2>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<ColorPicker label="First color" value={colorA} onInput={setColorA} />
|
<ColorPicker
|
||||||
|
label="First color"
|
||||||
|
value={props.colorA}
|
||||||
|
onInput={props.setColorA}
|
||||||
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Second color"
|
label="Second color"
|
||||||
value={colorB}
|
value={props.colorB}
|
||||||
onInput={setColorB}
|
onInput={props.setColorB}
|
||||||
/>
|
/>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
label="User icon"
|
label="User icon"
|
||||||
value={userIcon}
|
value={props.userIcon}
|
||||||
onInput={setUserIcon}
|
onInput={props.setUserIcon}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center mt-8">
|
<div className="flex justify-center mt-8">
|
||||||
|
|
|
@ -26,7 +26,9 @@ interface AuthStore {
|
||||||
setAccount(acc: AccountWithToken): void;
|
setAccount(acc: AccountWithToken): void;
|
||||||
updateDeviceName(deviceName: string): void;
|
updateDeviceName(deviceName: string): void;
|
||||||
updateAccount(acc: Account): void;
|
updateAccount(acc: Account): void;
|
||||||
|
setAccountProfile(acc: Account["profile"]): void;
|
||||||
setBackendUrl(url: null | string): void;
|
setBackendUrl(url: null | string): void;
|
||||||
|
setProxySet(urls: null | string[]): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create(
|
export const useAuthStore = create(
|
||||||
|
@ -50,6 +52,18 @@ export const useAuthStore = create(
|
||||||
s.backendUrl = v;
|
s.backendUrl = v;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setProxySet(urls) {
|
||||||
|
set((s) => {
|
||||||
|
s.proxySet = urls;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setAccountProfile(profile) {
|
||||||
|
set((s) => {
|
||||||
|
if (s.account) {
|
||||||
|
s.account.profile = profile;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
updateAccount(acc) {
|
updateAccount(acc) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
if (!s.account) return;
|
if (!s.account) return;
|
||||||
|
|
Loading…
Reference in a new issue