Add more settings saving logic + add connections section to settings + fix broken modals

This commit is contained in:
mrjvs 2023-11-24 18:39:40 +01:00
parent 5a5f3e8b8c
commit e62238459c
12 changed files with 264 additions and 26 deletions

View File

@ -44,7 +44,7 @@ module.exports = {
"react/destructuring-assignment": "off",
"no-underscore-dangle": "off",
"@typescript-eslint/no-explicit-any": "off",
"no-console": ["error", { allow: ["warn", "error"] }],
"no-console": ["warn", { allow: ["warn", "error", "debug", "info"] }],
"@typescript-eslint/no-this-alias": "off",
"import/prefer-default-export": "off",
"@typescript-eslint/no-empty-function": "off",

View File

@ -22,6 +22,7 @@
"hls.js": "^1.0.7",
"i18next": "^22.4.5",
"immer": "^10.0.2",
"lodash.isequal": "^4.5.0",
"node-forge": "^1.3.1",
"ofetch": "^1.0.0",
"react": "^17.0.2",
@ -67,6 +68,7 @@
"@types/crypto-js": "^4.1.1",
"@types/dompurify": "^2.4.0",
"@types/fscreen": "^1.0.1",
"@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.7",
"@types/node": "^17.0.15",
"@types/pako": "^2.0.0",

View File

@ -65,6 +65,9 @@ dependencies:
immer:
specifier: ^10.0.2
version: 10.0.2
lodash.isequal:
specifier: ^4.5.0
version: 4.5.0
node-forge:
specifier: ^1.3.1
version: 1.3.1
@ -130,6 +133,9 @@ devDependencies:
'@types/fscreen':
specifier: ^1.0.1
version: 1.0.1
'@types/lodash.isequal':
specifier: ^4.5.8
version: 4.5.8
'@types/lodash.throttle':
specifier: ^4.1.7
version: 4.1.7
@ -2132,6 +2138,12 @@ packages:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
/@types/lodash.isequal@4.5.8:
resolution: {integrity: sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==}
dependencies:
'@types/lodash': 4.14.197
dev: true
/@types/lodash.throttle@4.1.7:
resolution: {integrity: sha512-znwGDpjCHQ4FpLLx19w4OXDqq8+OvREa05H89obtSyXyOFKL3dDjCslsmfBz0T2FU8dmf5Wx1QvogbINiGIu9g==}
dependencies:
@ -4561,6 +4573,10 @@ packages:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
dev: true
/lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
dev: false
/lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true

View File

@ -55,7 +55,7 @@ export function OverlayPortal(props: {
onDeactivate: close,
}}
>
<div className="popout-wrapper absolute overflow-hidden pointer-events-auto inset-0 z-[999] select-none">
<div className="popout-wrapper fixed overflow-hidden pointer-events-auto inset-0 z-[999] select-none">
<Transition animation="fade" isChild>
<div
onClick={close}

View File

@ -72,10 +72,10 @@ export function useChromecast() {
// setup event listener
function listenToEvents(e: cast.framework.RemotePlayerChangedEvent) {
console.log("chromecast event", e);
console.debug("chromecast event", e);
}
function connectionChanged(e: cast.framework.RemotePlayerChangedEvent) {
console.log("chromecast event connection changed", e);
console.info("chromecast event connection changed", e);
}
remotePlayerController.current.addEventListener(
cast.framework.RemotePlayerEventType.PLAYER_STATE_CHANGED,

View File

@ -1,3 +1,4 @@
import isEqual from "lodash.isequal";
import { useCallback, useEffect, useMemo, useState } from "react";
import { SubtitleStyling } from "@/stores/subtitles";
@ -9,8 +10,10 @@ export function useDerived<T>(
useEffect(() => {
setOverwrite(undefined);
}, [initial]);
const changed = overwrite !== initial && overwrite !== undefined;
const changed = useMemo(
() => !isEqual(overwrite, initial) && overwrite !== undefined,
[overwrite, initial]
);
const data = overwrite === undefined ? initial : overwrite;
const reset = useCallback(() => setOverwrite(undefined), [setOverwrite]);

View File

@ -1,8 +1,10 @@
import classNames from "classnames";
import { useEffect } from "react";
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 { updateSettings } from "@/backend/accounts/settings";
import { Button } from "@/components/Button";
import { WideContainer } from "@/components/layout/WideContainer";
import { Heading1 } from "@/components/utils/Text";
@ -12,6 +14,7 @@ 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";
@ -71,10 +74,18 @@ export function SettingsPage() {
const setTheme = useThemeStore((s) => s.setTheme);
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 deviceName = useAuthStore((s) => s.account?.deviceName);
const account = useAuthStore((s) => s.account);
const decryptedName = useMemo(() => {
if (!account) return "";
return decryptData(account.deviceName, base64ToBuffer(account.seed));
}, [account]);
const backendUrl = useBackendUrl();
const user = useAuthStore();
@ -82,9 +93,23 @@ export function SettingsPage() {
activeTheme,
appLanguage,
subStyling,
deviceName
decryptedName
);
const saveChanges = useCallback(async () => {
console.log(state);
if (account) {
await updateSettings(backendUrl, account, {
applicationLanguage: state.appLanguage.state,
applicationTheme: state.theme.state ?? undefined,
});
}
setAppLanguage(state.appLanguage.state);
setTheme(state.theme.state);
setSubStyling(state.subtitleStyling.state);
}, [state, account, backendUrl, setAppLanguage, setTheme, setSubStyling]);
return (
<SubPageLayout>
<SettingsLayout>
@ -110,20 +135,28 @@ export function SettingsPage() {
<div id="settings-captions" className="mt-48">
<CaptionsPart
styling={state.subtitleStyling.state}
setStyling={(s) => s}
setStyling={state.subtitleStyling.set}
/>
</div>
</SettingsLayout>
{state.changed ? (
<div className="bg-settings-saveBar-background border-t border-settings-card-border/50 py-4 w-full fixed bottom-0 flex justify-between px-8 items-center">
<p className="text-type-danger">You have unsaved changes</p>
<div className="space-x-6">
<Button theme="secondary">Reset</Button>
<Button theme="purple">Save</Button>
</div>
<div id="settings-connection" className="mt-48">
<ConnectionsPart />
</div>
) : null}
</SettingsLayout>
<div
className={`bg-settings-saveBar-background border-t border-settings-card-border/50 py-4 transition-opacity w-full fixed bottom-0 flex justify-between px-8 items-center ${
state.changed ? "opacity-100" : "opacity-0"
}`}
>
<p className="text-type-danger">You have unsaved changes</p>
<div className="space-x-6">
<Button theme="secondary" onClick={state.reset}>
Reset
</Button>
<Button theme="purple" onClick={saveChanges}>
Save
</Button>
</div>
</div>
</SubPageLayout>
);
}

View File

@ -13,7 +13,10 @@ export function AccountEditPart() {
return (
<SettingsCard paddingClass="px-8 py-10" className="!mt-8">
<ProfileEditModal id={profileEditModal.id} />
<ProfileEditModal
id={profileEditModal.id}
close={profileEditModal.hide}
/>
<div className="grid lg:grid-cols-[auto,1fr] gap-8">
<div>
<UserAvatar

View File

@ -0,0 +1,149 @@
import { useCallback, useState } from "react";
import { Button } from "@/components/Button";
import { Toggle } from "@/components/buttons/Toggle";
import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard";
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;
}
function ProxyEdit() {
const [customWorkers, setCustomWorkers] = useState<ProxyItem[] | null>(null);
const add = useCallback(() => {
idNum += 1;
setCustomWorkers((s) => [
...(s ?? []),
{
id: idNum,
url: "",
},
]);
}, [setCustomWorkers]);
const changeItem = useCallback(
(id: number, val: string) => {
setCustomWorkers((s) => [
...(s ?? []).map((v) => {
if (v.id !== id) return v;
v.url = val;
return v;
}),
]);
},
[setCustomWorkers]
);
const removeItem = useCallback(
(id: number) => {
setCustomWorkers((s) => [...(s ?? []).filter((v) => v.id !== id)]);
},
[setCustomWorkers]
);
return (
<SettingsCard>
<div className="flex justify-between items-center">
<div className="my-3">
<p className="text-white font-bold mb-3">Use custom proxy workers</p>
<p className="max-w-[20rem] font-medium">
To make the application function, all traffic is routed through
proxies. Enable this if you want to bring your own workers.
</p>
</div>
<div>
<Toggle
onClick={() => setCustomWorkers((s) => (s === null ? [] : null))}
enabled={customWorkers !== null}
/>
</div>
</div>
{customWorkers !== 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 ? (
<p>No workers yet, add one below</p>
) : null}
{(customWorkers ?? []).map((v) => (
<div className="grid grid-cols-[1fr,auto] items-center gap-2">
<AuthInputBox
value={v.url}
onChange={(val) => changeItem(v.id, val)}
placeholder="https://"
/>
<button
type="button"
onClick={() => removeItem(v.id)}
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} />
</button>
</div>
))}
</div>
<Button theme="purple" onClick={add}>
Add new worker
</Button>
</>
) : null}
</SettingsCard>
);
}
function BackendEdit() {
const [customBackendUrl, setCustomBackendUrl] = useState<string | null>(null);
return (
<SettingsCard>
<div className="flex justify-between items-center">
<div className="my-3">
<p className="text-white font-bold mb-3">Custom server</p>
<p className="max-w-[20rem] font-medium">
To make the application function, all traffic is routed through
proxies. Enable this if you want to bring your own workers.
</p>
</div>
<div>
<Toggle
onClick={() => setCustomBackendUrl((s) => (s === null ? "" : null))}
enabled={customBackendUrl !== null}
/>
</div>
</div>
{customBackendUrl !== 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 ?? ""}
/>
</>
) : null}
</SettingsCard>
);
}
export function ConnectionsPart() {
return (
<div>
<Heading1 border>Connections</Heading1>
<div className="space-y-6">
<ProxyEdit />
<BackendEdit />
</div>
</div>
);
}

View File

@ -1,14 +1,44 @@
import { useState } from "react";
import { Button } from "@/components/Button";
import { ColorPicker } from "@/components/form/ColorPicker";
import { IconPicker } from "@/components/form/IconPicker";
import { Modal, ModalCard } from "@/components/overlays/Modal";
import { UserIcons } from "@/components/UserIcon";
import { Heading2 } from "@/components/utils/Text";
export function ProfileEditModal(props: { id: string }) {
export interface ProfileEditModalProps {
id: string;
close?: () => 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?</Heading2>
<p>I am existing</p>
<Button theme="danger">Update</Button>
<Heading2 className="!mt-0">Edit profile picture</Heading2>
<div className="space-y-6">
<ColorPicker label="First color" value={colorA} onInput={setColorA} />
<ColorPicker
label="Second color"
value={colorB}
onInput={setColorB}
/>
<IconPicker
label="User icon"
value={userIcon}
onInput={setUserIcon}
/>
</div>
<div className="flex justify-center mt-8">
<Button theme="purple" className="!px-20" onClick={props.close}>
Finish editing
</Button>
</div>
</ModalCard>
</Modal>
);

View File

@ -18,9 +18,10 @@ export function SidebarPart() {
const settingLinks = [
{ text: "Account", id: "settings-account", icon: Icons.USER },
{ text: "Locale", id: "settings-locale", icon: Icons.LINK },
{ text: "Locale", id: "settings-locale", icon: Icons.BOOKMARK },
{ text: "Appearance", id: "settings-appearance", icon: Icons.GITHUB },
{ text: "Captions", id: "settings-captions", icon: Icons.CAPTIONS },
{ text: "Connections", id: "settings-connection", icon: Icons.LINK },
];
useEffect(() => {

View File

@ -109,6 +109,7 @@ export const defaultTheme = {
authentication: {
border: "#393954",
inputBg: "#171728",
inputBgHover: "#171726",
wordBackground: "#171728",
copyText: "#58587A",
copyTextHover: "#8888AA",