mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-11 03:15:07 +01:00
make settings page fully functional
This commit is contained in:
parent
b38e5768e3
commit
a9abe14810
@ -12,6 +12,10 @@ export interface SessionResponse {
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
export interface SessionUpdate {
|
||||
deviceName: string;
|
||||
}
|
||||
|
||||
export async function getSessions(url: string, account: AccountWithToken) {
|
||||
return ofetch<SessionResponse[]>(`/users/${account.userId}/sessions`, {
|
||||
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(
|
||||
url: string,
|
||||
token: string,
|
||||
|
@ -5,7 +5,7 @@ import { AccountWithToken } from "@/stores/auth";
|
||||
|
||||
export interface SettingsInput {
|
||||
applicationLanguage?: string;
|
||||
applicationTheme?: string;
|
||||
applicationTheme?: string | null;
|
||||
defaultSubtitleLanguage?: string;
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,14 @@ export interface UserResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserEdit {
|
||||
profile?: {
|
||||
colorA: string;
|
||||
colorB: string;
|
||||
icon: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BookmarkResponse {
|
||||
tmdbId: string;
|
||||
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(
|
||||
url: string,
|
||||
account: AccountWithToken
|
||||
|
@ -9,20 +9,31 @@ export interface AvatarProps {
|
||||
profile: AccountProfile["profile"];
|
||||
sizeClass?: string;
|
||||
iconClass?: string;
|
||||
bottom?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Avatar(props: AvatarProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
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})`,
|
||||
}}
|
||||
>
|
||||
<UserIcon className={props.iconClass} icon={props.profile.icon as any} />
|
||||
<div className="relative inline-block">
|
||||
<div
|
||||
className={classNames(
|
||||
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})`,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -35,18 +46,12 @@ export function UserAvatar(props: {
|
||||
const auth = useAuthStore();
|
||||
if (!auth.account) return null;
|
||||
return (
|
||||
<div className="relative inline-block">
|
||||
<Avatar
|
||||
profile={auth.account.profile}
|
||||
sizeClass={props.sizeClass ?? "w-[2rem] h-[2rem]"}
|
||||
iconClass={props.iconClass}
|
||||
/>
|
||||
{props.bottom ? (
|
||||
<div className="absolute bottom-0 left-1/2 transform translate-y-1/2 -translate-x-1/2">
|
||||
{props.bottom}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Avatar
|
||||
profile={auth.account.profile}
|
||||
sizeClass={props.sizeClass ?? "w-[2rem] h-[2rem]"}
|
||||
iconClass={props.iconClass}
|
||||
bottom={props.bottom}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { useCopyToClipboard, useMountedState } from "react-use";
|
||||
|
||||
import { Icon, Icons } from "./Icon";
|
||||
|
||||
export function PassphaseDisplay(props: { mnemonic: string }) {
|
||||
export function PassphraseDisplay(props: { mnemonic: string }) {
|
||||
const individualWords = props.mnemonic.split(" ");
|
||||
|
||||
const [, copy] = useCopyToClipboard();
|
||||
@ -23,7 +23,7 @@ export function PassphaseDisplay(props: { mnemonic: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border 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
|
||||
type="button"
|
||||
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>
|
||||
</div>
|
||||
<div className="px-4 py-4 grid grid-cols-4 gap-2">
|
||||
{individualWords.map((word) => (
|
||||
{individualWords.map((word, i) => (
|
||||
<div
|
||||
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}
|
||||
</div>
|
||||
|
@ -1,11 +1,18 @@
|
||||
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";
|
||||
|
||||
export function useDerived<T>(
|
||||
initial: T
|
||||
): [T, (v: T) => void, () => void, boolean] {
|
||||
): [T, Dispatch<SetStateAction<T>>, () => void, boolean] {
|
||||
const [overwrite, setOverwrite] = useState<T | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
setOverwrite(undefined);
|
||||
@ -14,19 +21,39 @@ export function useDerived<T>(
|
||||
() => !isEqual(overwrite, initial) && overwrite !== undefined,
|
||||
[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 reset = useCallback(() => setOverwrite(undefined), [setOverwrite]);
|
||||
|
||||
return [data, setOverwrite, reset, changed];
|
||||
return [data, setter, reset, changed];
|
||||
}
|
||||
|
||||
export function useSettingsState(
|
||||
theme: string | null,
|
||||
appLanguage: string,
|
||||
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 [
|
||||
appLanguageState,
|
||||
@ -42,22 +69,27 @@ export function useSettingsState(
|
||||
resetDeviceName,
|
||||
deviceNameChanged,
|
||||
] = useDerived(deviceName);
|
||||
const [profileState, setProfileState, resetProfile, profileChanged] =
|
||||
useDerived(profile);
|
||||
|
||||
function reset() {
|
||||
resetTheme();
|
||||
resetAppLanguage();
|
||||
resetSubStyling();
|
||||
resetProxyUrls();
|
||||
resetBackendUrl();
|
||||
resetDeviceName();
|
||||
resetProfile();
|
||||
}
|
||||
|
||||
const changed = useMemo(
|
||||
() =>
|
||||
themeChanged ||
|
||||
appLanguageChanged ||
|
||||
subStylingChanged ||
|
||||
deviceNameChanged,
|
||||
[themeChanged, appLanguageChanged, subStylingChanged, deviceNameChanged]
|
||||
);
|
||||
const changed =
|
||||
themeChanged ||
|
||||
appLanguageChanged ||
|
||||
subStylingChanged ||
|
||||
deviceNameChanged ||
|
||||
backendUrlChanged ||
|
||||
proxyUrlsChanged ||
|
||||
profileChanged;
|
||||
|
||||
return {
|
||||
reset,
|
||||
@ -65,18 +97,37 @@ export function useSettingsState(
|
||||
theme: {
|
||||
state: themeState,
|
||||
set: setTheme,
|
||||
changed: themeChanged,
|
||||
},
|
||||
appLanguage: {
|
||||
state: appLanguageState,
|
||||
set: setAppLanguage,
|
||||
changed: appLanguageChanged,
|
||||
},
|
||||
subtitleStyling: {
|
||||
state: subStylingState,
|
||||
set: setSubStyling,
|
||||
changed: subStylingChanged,
|
||||
},
|
||||
deviceName: {
|
||||
state: deviceNameState,
|
||||
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 { useAsyncFn } from "react-use";
|
||||
|
||||
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
|
||||
import { getSessions } from "@/backend/accounts/sessions";
|
||||
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/Button";
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { UserIcons } from "@/components/UserIcon";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
import { useAuth } from "@/hooks/auth/useAuth";
|
||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
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 { account } = props;
|
||||
const [sessionsResult, execSessions] = useAsyncFn(() => {
|
||||
@ -57,7 +74,16 @@ export function AccountSettings(props: { account: AccountWithToken }) {
|
||||
|
||||
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
|
||||
error={!!sessionsResult.error}
|
||||
loading={sessionsResult.loading}
|
||||
@ -79,7 +105,15 @@ export function SettingsPage() {
|
||||
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 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));
|
||||
@ -87,29 +121,71 @@ export function SettingsPage() {
|
||||
|
||||
const backendUrl = useBackendUrl();
|
||||
|
||||
const { logout } = useAuth();
|
||||
const user = useAuthStore();
|
||||
|
||||
const state = useSettingsState(
|
||||
activeTheme,
|
||||
appLanguage,
|
||||
subStyling,
|
||||
decryptedName
|
||||
decryptedName,
|
||||
proxySet,
|
||||
backendUrlSetting,
|
||||
account?.profile
|
||||
);
|
||||
|
||||
const saveChanges = useCallback(async () => {
|
||||
console.log(state);
|
||||
|
||||
if (account) {
|
||||
await updateSettings(backendUrl, account, {
|
||||
applicationLanguage: state.appLanguage.state,
|
||||
applicationTheme: state.theme.state ?? undefined,
|
||||
});
|
||||
if (state.appLanguage.changed || state.theme.changed) {
|
||||
await updateSettings(backendUrl, account, {
|
||||
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);
|
||||
setTheme(state.theme.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 (
|
||||
<SubPageLayout>
|
||||
<SettingsLayout>
|
||||
@ -117,8 +193,24 @@ export function SettingsPage() {
|
||||
<Heading1 border className="!mb-0">
|
||||
Account
|
||||
</Heading1>
|
||||
{user.account ? (
|
||||
<AccountSettings account={user.account} />
|
||||
{user.account && state.profile.state ? (
|
||||
<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 />
|
||||
)}
|
||||
@ -139,7 +231,12 @@ export function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
</SettingsLayout>
|
||||
<div
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
LargeCardButtons,
|
||||
LargeCardText,
|
||||
} from "@/components/layout/LargeCard";
|
||||
import { PassphaseDisplay } from "@/components/PassphraseDisplay";
|
||||
import { PassphraseDisplay } from "@/components/PassphraseDisplay";
|
||||
|
||||
interface PassphraseGeneratePartProps {
|
||||
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
|
||||
wall of shame™️
|
||||
</LargeCardText>
|
||||
<PassphaseDisplay mnemonic={mnemonic} />
|
||||
<PassphraseDisplay mnemonic={mnemonic} />
|
||||
|
||||
<LargeCardButtons>
|
||||
<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 { Icon, Icons } from "@/components/Icon";
|
||||
import { SettingsCard } from "@/components/layout/SettingsCard";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
||||
import { UserIcons } from "@/components/UserIcon";
|
||||
import { useAuth } from "@/hooks/auth/useAuth";
|
||||
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 profileEditModal = useModal("profile-edit");
|
||||
|
||||
@ -16,10 +26,21 @@ export function AccountEditPart() {
|
||||
<ProfileEditModal
|
||||
id={profileEditModal.id}
|
||||
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>
|
||||
<UserAvatar
|
||||
<Avatar
|
||||
profile={{
|
||||
colorA: props.colorA,
|
||||
colorB: props.colorB,
|
||||
icon: props.userIcon,
|
||||
}}
|
||||
iconClass="text-5xl"
|
||||
sizeClass="w-32 h-32"
|
||||
bottom={
|
||||
@ -36,9 +57,13 @@ export function AccountEditPart() {
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
<Button theme="purple">Save account</Button>
|
||||
<Button theme="danger" onClick={logout}>
|
||||
Log out
|
||||
</Button>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Dispatch, SetStateAction, useCallback } from "react";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Toggle } from "@/components/buttons/Toggle";
|
||||
@ -8,45 +8,38 @@ import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
||||
import { Divider } from "@/components/utils/Divider";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
|
||||
let idNum = 0;
|
||||
|
||||
interface ProxyItem {
|
||||
url: string;
|
||||
id: number;
|
||||
interface ProxyEditProps {
|
||||
proxyUrls: string[] | null;
|
||||
setProxyUrls: Dispatch<SetStateAction<string[] | null>>;
|
||||
}
|
||||
|
||||
function ProxyEdit() {
|
||||
const [customWorkers, setCustomWorkers] = useState<ProxyItem[] | null>(null);
|
||||
interface BackendEditProps {
|
||||
backendUrl: string | null;
|
||||
setBackendUrl: Dispatch<SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
|
||||
const add = useCallback(() => {
|
||||
idNum += 1;
|
||||
setCustomWorkers((s) => [
|
||||
...(s ?? []),
|
||||
{
|
||||
id: idNum,
|
||||
url: "",
|
||||
},
|
||||
]);
|
||||
}, [setCustomWorkers]);
|
||||
setProxyUrls((s) => [...(s ?? []), ""]);
|
||||
}, [setProxyUrls]);
|
||||
|
||||
const changeItem = useCallback(
|
||||
(id: number, val: string) => {
|
||||
setCustomWorkers((s) => [
|
||||
...(s ?? []).map((v) => {
|
||||
if (v.id !== id) return v;
|
||||
v.url = val;
|
||||
return v;
|
||||
(index: number, val: string) => {
|
||||
setProxyUrls((s) => [
|
||||
...(s ?? []).map((v, i) => {
|
||||
if (i !== index) return v;
|
||||
return val;
|
||||
}),
|
||||
]);
|
||||
},
|
||||
[setCustomWorkers]
|
||||
[setProxyUrls]
|
||||
);
|
||||
|
||||
const removeItem = useCallback(
|
||||
(id: number) => {
|
||||
setCustomWorkers((s) => [...(s ?? []).filter((v) => v.id !== id)]);
|
||||
(index: number) => {
|
||||
setProxyUrls((s) => [...(s ?? []).filter((v, i) => i !== index)]);
|
||||
},
|
||||
[setCustomWorkers]
|
||||
[setProxyUrls]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -61,30 +54,35 @@ function ProxyEdit() {
|
||||
</div>
|
||||
<div>
|
||||
<Toggle
|
||||
onClick={() => setCustomWorkers((s) => (s === null ? [] : null))}
|
||||
enabled={customWorkers !== null}
|
||||
onClick={() => setProxyUrls((s) => (s === null ? [] : null))}
|
||||
enabled={proxyUrls !== null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{customWorkers !== null ? (
|
||||
{proxyUrls !== null ? (
|
||||
<>
|
||||
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
||||
<p className="text-white font-bold mb-3">Worker URLs</p>
|
||||
|
||||
<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>
|
||||
) : null}
|
||||
{(customWorkers ?? []).map((v) => (
|
||||
<div className="grid grid-cols-[1fr,auto] items-center gap-2">
|
||||
{(proxyUrls ?? []).map((v, i) => (
|
||||
<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
|
||||
value={v.url}
|
||||
onChange={(val) => changeItem(v.id, val)}
|
||||
value={v}
|
||||
onChange={(val) => changeItem(i, val)}
|
||||
placeholder="https://"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
<Icon className="text-xl" icon={Icons.X} />
|
||||
@ -102,9 +100,7 @@ function ProxyEdit() {
|
||||
);
|
||||
}
|
||||
|
||||
function BackendEdit() {
|
||||
const [customBackendUrl, setCustomBackendUrl] = useState<string | null>(null);
|
||||
|
||||
function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
|
||||
return (
|
||||
<SettingsCard>
|
||||
<div className="flex justify-between items-center">
|
||||
@ -117,32 +113,35 @@ function BackendEdit() {
|
||||
</div>
|
||||
<div>
|
||||
<Toggle
|
||||
onClick={() => setCustomBackendUrl((s) => (s === null ? "" : null))}
|
||||
enabled={customBackendUrl !== null}
|
||||
onClick={() => setBackendUrl((s) => (s === null ? "" : null))}
|
||||
enabled={backendUrl !== null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{customBackendUrl !== null ? (
|
||||
{backendUrl !== null ? (
|
||||
<>
|
||||
<Divider marginClass="my-6 px-8 box-content -mx-8" />
|
||||
<p className="text-white font-bold mb-3">Custom server URL</p>
|
||||
<AuthInputBox
|
||||
onChange={setCustomBackendUrl}
|
||||
value={customBackendUrl ?? ""}
|
||||
/>
|
||||
<AuthInputBox onChange={setBackendUrl} value={backendUrl ?? ""} />
|
||||
</>
|
||||
) : null}
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConnectionsPart() {
|
||||
export function ConnectionsPart(props: BackendEditProps & ProxyEditProps) {
|
||||
return (
|
||||
<div>
|
||||
<Heading1 border>Connections</Heading1>
|
||||
<div className="space-y-6">
|
||||
<ProxyEdit />
|
||||
<BackendEdit />
|
||||
<ProxyEdit
|
||||
proxyUrls={props.proxyUrls}
|
||||
setProxyUrls={props.setProxyUrls}
|
||||
/>
|
||||
<BackendEdit
|
||||
backendUrl={props.backendUrl}
|
||||
setBackendUrl={props.setBackendUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { ColorPicker } from "@/components/form/ColorPicker";
|
||||
import { IconPicker } from "@/components/form/IconPicker";
|
||||
@ -10,28 +8,34 @@ import { Heading2 } from "@/components/utils/Text";
|
||||
export interface ProfileEditModalProps {
|
||||
id: string;
|
||||
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) {
|
||||
const [colorA, setColorA] = useState("#2E65CF");
|
||||
const [colorB, setColorB] = useState("#2E65CF");
|
||||
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER);
|
||||
|
||||
return (
|
||||
<Modal id={props.id}>
|
||||
<ModalCard>
|
||||
<Heading2 className="!mt-0">Edit profile picture</Heading2>
|
||||
<div className="space-y-6">
|
||||
<ColorPicker label="First color" value={colorA} onInput={setColorA} />
|
||||
<ColorPicker
|
||||
label="First color"
|
||||
value={props.colorA}
|
||||
onInput={props.setColorA}
|
||||
/>
|
||||
<ColorPicker
|
||||
label="Second color"
|
||||
value={colorB}
|
||||
onInput={setColorB}
|
||||
value={props.colorB}
|
||||
onInput={props.setColorB}
|
||||
/>
|
||||
<IconPicker
|
||||
label="User icon"
|
||||
value={userIcon}
|
||||
onInput={setUserIcon}
|
||||
value={props.userIcon}
|
||||
onInput={props.setUserIcon}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center mt-8">
|
||||
|
@ -26,7 +26,9 @@ interface AuthStore {
|
||||
setAccount(acc: AccountWithToken): void;
|
||||
updateDeviceName(deviceName: string): void;
|
||||
updateAccount(acc: Account): void;
|
||||
setAccountProfile(acc: Account["profile"]): void;
|
||||
setBackendUrl(url: null | string): void;
|
||||
setProxySet(urls: null | string[]): void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create(
|
||||
@ -50,6 +52,18 @@ export const useAuthStore = create(
|
||||
s.backendUrl = v;
|
||||
});
|
||||
},
|
||||
setProxySet(urls) {
|
||||
set((s) => {
|
||||
s.proxySet = urls;
|
||||
});
|
||||
},
|
||||
setAccountProfile(profile) {
|
||||
set((s) => {
|
||||
if (s.account) {
|
||||
s.account.profile = profile;
|
||||
}
|
||||
});
|
||||
},
|
||||
updateAccount(acc) {
|
||||
set((s) => {
|
||||
if (!s.account) return;
|
||||
|
Loading…
Reference in New Issue
Block a user