Fix sticky sidebar + new design for app information + gorgegous new dropdown + bunch of small bug fixes + fix encryption not supporting utf8

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-11-25 17:09:01 +01:00
parent 8cdedbfca6
commit 7bc3bb1416
16 changed files with 158 additions and 119 deletions

View File

@ -32,7 +32,7 @@
"react-helmet-async": "^1.3.0", "react-helmet-async": "^1.3.0",
"react-i18next": "^12.1.1", "react-i18next": "^12.1.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-stickynode": "^4.1.0", "react-sticky-el": "^2.1.0",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"subsrt-ts": "^2.1.1", "subsrt-ts": "^2.1.1",

42
pnpm-lock.yaml generated
View File

@ -95,9 +95,9 @@ dependencies:
react-router-dom: react-router-dom:
specifier: ^5.2.0 specifier: ^5.2.0
version: 5.3.4(react@17.0.2) version: 5.3.4(react@17.0.2)
react-stickynode: react-sticky-el:
specifier: ^4.1.0 specifier: ^2.1.0
version: 4.1.0(react-dom@17.0.2)(react@17.0.2) version: 2.1.0(react-dom@17.0.2)(react@17.0.2)
react-use: react-use:
specifier: ^17.4.0 specifier: ^17.4.0
version: 17.4.0(react-dom@17.0.2)(react@17.0.2) version: 17.4.0(react-dom@17.0.2)(react@17.0.2)
@ -3657,10 +3657,6 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/eventemitter3@3.1.2:
resolution: {integrity: sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==}
dev: false
/fast-deep-equal@3.1.3: /fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@ -4595,6 +4591,7 @@ packages:
/lodash@4.17.21: /lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true
/loose-envify@1.4.0: /loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
@ -4994,10 +4991,6 @@ packages:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
dev: true dev: true
/performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
dev: false
/picocolors@1.0.0: /picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
dev: true dev: true
@ -5165,12 +5158,6 @@ packages:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true dev: true
/raf@3.4.1:
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
dependencies:
performance-now: 2.1.0
dev: false
/randombytes@2.1.0: /randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
dependencies: dependencies:
@ -5286,19 +5273,14 @@ packages:
tiny-warning: 1.0.3 tiny-warning: 1.0.3
dev: false dev: false
/react-stickynode@4.1.0(react-dom@17.0.2)(react@17.0.2): /react-sticky-el@2.1.0(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ==} resolution: {integrity: sha512-oo+a2GedF4QMfCfm20e9gD+RuuQp/ngvwGMUXAXpST+h4WnmKhuv7x6MQ4X/e3AHiLYgE0zDyJo1Pzo8m51KpA==}
peerDependencies: peerDependencies:
react: ^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 react: '>=16.3.0'
react-dom: ^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: '>=16.3.0'
dependencies: dependencies:
classnames: 2.3.2
core-js: 3.32.1
prop-types: 15.8.1
react: 17.0.2 react: 17.0.2
react-dom: 17.0.2(react@17.0.2) react-dom: 17.0.2(react@17.0.2)
shallowequal: 1.1.0
subscribe-ui-event: 2.0.7
dev: false dev: false
/react-universal-interface@0.6.2(react@17.0.2)(tslib@2.6.2): /react-universal-interface@0.6.2(react@17.0.2)(tslib@2.6.2):
@ -5796,14 +5778,6 @@ packages:
resolution: {integrity: sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==} resolution: {integrity: sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==}
dev: false dev: false
/subscribe-ui-event@2.0.7:
resolution: {integrity: sha512-Acrtf9XXl6lpyHAWYeRD1xTPUQHDERfL4GHeNuYAtZMc4Z8Us2iDBP0Fn3xiRvkQ1FO+hx+qRLmPEwiZxp7FDQ==}
dependencies:
eventemitter3: 3.1.2
lodash: 4.17.21
raf: 3.4.1
dev: false
/subsrt-ts@2.1.1: /subsrt-ts@2.1.1:
resolution: {integrity: sha512-E+GiLNG4L82yRDswd4ys34OUfJLNN6ZBdtefE7ftn/WJchjvyJ9dNXuXYviNglrqiCqNyayGGUZE3v9aL7zIYg==} resolution: {integrity: sha512-E+GiLNG4L82yRDswd4ys34OUfJLNN6ZBdtefE7ftn/WJchjvyJ9dNXuXYviNglrqiCqNyayGGUZE3v9aL7zIYg==}
hasBin: true hasBin: true

View File

@ -70,7 +70,7 @@ export function base64ToBuffer(data: string) {
return forge.util.binary.base64.decode(data); return forge.util.binary.base64.decode(data);
} }
export function base64ToStringBugger(data: string) { export function base64ToStringBuffer(data: string) {
return forge.util.createBuffer(base64ToBuffer(data)); return forge.util.createBuffer(base64ToBuffer(data));
} }
@ -97,7 +97,7 @@ export async function encryptData(data: string, secret: Uint8Array) {
iv, iv,
tagLength: 128, tagLength: 128,
}); });
cipher.update(forge.util.createBuffer(data)); cipher.update(forge.util.createBuffer(data, "utf8"));
cipher.finish(); cipher.finish();
const encryptedData = cipher.output; const encryptedData = cipher.output;
@ -118,11 +118,11 @@ export function decryptData(data: string, secret: Uint8Array) {
forge.util.createBuffer(secret) forge.util.createBuffer(secret)
); );
decipher.start({ decipher.start({
iv: base64ToStringBugger(iv), iv: base64ToStringBuffer(iv),
tag: base64ToStringBugger(tag), tag: base64ToStringBuffer(tag),
tagLength: 128, tagLength: 128,
}); });
decipher.update(base64ToStringBugger(encryptedData)); decipher.update(base64ToStringBuffer(encryptedData));
const pass = decipher.finish(); const pass = decipher.finish();
if (!pass) throw new Error("Error decrypting data"); if (!pass) throw new Error("Error decrypting data");

View File

@ -1,5 +1,7 @@
import classNames from "classnames"; import classNames from "classnames";
import { useMemo } from "react";
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { UserIcon } from "@/components/UserIcon"; import { UserIcon } from "@/components/UserIcon";
import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart"; import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart";
@ -42,16 +44,40 @@ export function UserAvatar(props: {
sizeClass?: string; sizeClass?: string;
iconClass?: string; iconClass?: string;
bottom?: React.ReactNode; bottom?: React.ReactNode;
withName?: boolean;
}) { }) {
const auth = useAuthStore(); const auth = useAuthStore();
if (!auth.account) return null;
const bufferSeed = useMemo(
() =>
auth.account && auth.account.seed
? base64ToBuffer(auth.account.seed)
: null,
[auth]
);
if (!auth.account || auth.account === null) return null;
const deviceName = bufferSeed
? decryptData(auth.account.deviceName, bufferSeed)
: "...";
return ( return (
<Avatar <>
profile={auth.account.profile} <Avatar
sizeClass={props.sizeClass ?? "w-[2rem] h-[2rem]"} profile={auth.account.profile}
iconClass={props.iconClass} sizeClass={props.sizeClass ?? "w-[2rem] h-[2rem]"}
bottom={props.bottom} iconClass={props.iconClass}
/> bottom={props.bottom}
/>
{props.withName && bufferSeed ? (
<span>
{deviceName.length >= 20
? `${deviceName.slice(0, 20 - 1)}`
: deviceName}
</span>
) : null}
</>
); );
} }

View File

@ -107,12 +107,19 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
return ( return (
<div className="relative is-dropdown"> <div className="relative is-dropdown">
<div <div
className="cursor-pointer tabbable rounded-full" className="cursor-pointer tabbable rounded-full flex gap-2 text-white items-center py-2 px-3 bg-pill-background bg-opacity-50"
tabIndex={0} tabIndex={0}
onClick={toggleOpen} onClick={toggleOpen}
onKeyUp={(evt) => evt.key === "Enter" && toggleOpen()} onKeyUp={(evt) => evt.key === "Enter" && toggleOpen()}
> >
{props.children} {props.children}
<Icon
className={classNames(
"text-xl transition-transform duration-100",
open ? "rotate-180" : ""
)}
icon={Icons.CHEVRON_DOWN}
/>
</div> </div>
<Transition animation="slide-down" show={open}> <Transition animation="slide-down" show={open}>
<div className="rounded-lg absolute w-64 bg-dropdown-altBackground top-full mt-3 right-0"> <div className="rounded-lg absolute w-64 bg-dropdown-altBackground top-full mt-3 right-0">

View File

@ -101,7 +101,7 @@ export function Navigation(props: NavigationProps) {
</div> </div>
<div className="relative"> <div className="relative">
<LinksDropdown> <LinksDropdown>
{loggedIn ? <UserAvatar /> : <NoUserAvatar />} {loggedIn ? <UserAvatar withName /> : <NoUserAvatar />}
</LinksDropdown> </LinksDropdown>
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ export function Heading1(props: TextProps) {
return ( return (
<h1 <h1
className={[ className={[
"text-5xl font-bold text-white mb-9", "text-3xl lg:text-5xl font-bold text-white mb-9",
props.border ? borderClass : null, props.border ? borderClass : null,
props.className ?? "", props.className ?? "",
].join(" ")} ].join(" ")}
@ -24,7 +24,7 @@ export function Heading2(props: TextProps) {
return ( return (
<h2 <h2
className={[ className={[
"text-3xl font-bold text-white mt-20 mb-9", "text-xl lg:text-3xl font-bold text-white mt-20 mb-9",
props.border ? borderClass : null, props.border ? borderClass : null,
props.className ?? "", props.className ?? "",
].join(" ")} ].join(" ")}
@ -38,7 +38,7 @@ export function Heading3(props: TextProps) {
return ( return (
<h2 <h2
className={[ className={[
"text-xl font-bold text-white mb-3", "text-lg lg:text-xl font-bold text-white mb-3",
props.border ? borderClass : null, props.border ? borderClass : null,
props.className ?? "", props.className ?? "",
].join(" ")} ].join(" ")}

View File

@ -164,7 +164,8 @@ export function useAuth() {
const anyError: any = err; const anyError: any = err;
if ( if (
anyError?.response?.status === 401 || anyError?.response?.status === 401 ||
anyError?.response?.status === 403 anyError?.response?.status === 403 ||
anyError?.response?.status === 400
) { ) {
await logout(); await logout();
return; return;

View File

@ -87,7 +87,9 @@ function AuthWrapper() {
if (status.error) if (status.error)
return ( return (
<ErrorScreen showResetButton={backendUrl !== userBackendUrl}> <ErrorScreen showResetButton={backendUrl !== userBackendUrl}>
Failed to fetch user data. Try resetting the backend URL. {backendUrl !== userBackendUrl
? "Failed to fetch user data. Try resetting the backend URL"
: "Failed to fetch user data."}
</ErrorScreen> </ErrorScreen>
); );
return <App />; return <App />;

View File

@ -42,7 +42,7 @@ function SettingsLayout(props: { children: React.ReactNode }) {
<div <div
className={classNames( className={classNames(
"grid gap-12", "grid gap-12",
isMobile ? "grid-cols-1" : "lg:grid-cols-[310px,1fr]" isMobile ? "grid-cols-1" : "lg:grid-cols-[280px,1fr]"
)} )}
> >
<SidebarPart /> <SidebarPart />
@ -240,16 +240,24 @@ export function SettingsPage() {
</div> </div>
</SettingsLayout> </SettingsLayout>
<div <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 ${ className={`bg-settings-saveBar-background border-t border-settings-card-border/50 py-4 transition-opacity w-full fixed bottom-0 flex justify-between flex-col md:flex-row px-8 items-start md:items-center gap-3 ${
state.changed ? "opacity-100" : "opacity-0" state.changed ? "opacity-100" : "opacity-0"
}`} }`}
> >
<p className="text-type-danger">You have unsaved changes</p> <p className="text-type-danger">You have unsaved changes</p>
<div className="space-x-6"> <div className="space-x-3 w-full md:w-auto flex">
<Button theme="secondary" onClick={state.reset}> <Button
className="w-full md:w-auto"
theme="secondary"
onClick={state.reset}
>
Reset Reset
</Button> </Button>
<Button theme="purple" onClick={saveChanges}> <Button
className="w-full md:w-auto"
theme="purple"
onClick={saveChanges}
>
Save Save
</Button> </Button>
</div> </div>

View File

@ -1,5 +1,5 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import Sticky from "react-stickynode"; import Sticky from "react-sticky-el";
import { ThinContainer } from "@/components/layout/ThinContainer"; import { ThinContainer } from "@/components/layout/ThinContainer";
import { SearchBarInput } from "@/components/SearchBar"; import { SearchBarInput } from "@/components/SearchBar";
@ -19,10 +19,9 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
const [, setShowBg] = useState(false); const [, setShowBg] = useState(false);
const bannerSize = useBannerSize(); const bannerSize = useBannerSize();
const stickStateChanged = useCallback( const stickStateChanged = useCallback(
({ status }: Sticky.Status) => { (isFixed) => {
const val = status === Sticky.STATUS_FIXED; setShowBg(isFixed);
setShowBg(val); setIsSticky(isFixed);
setIsSticky(val);
}, },
[setShowBg, setIsSticky] [setShowBg, setIsSticky]
); );
@ -40,11 +39,13 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
<div className="relative z-10 mb-16"> <div className="relative z-10 mb-16">
<HeroTitle className="mx-auto max-w-xs">{title}</HeroTitle> <HeroTitle className="mx-auto max-w-xs">{title}</HeroTitle>
</div> </div>
<div className="relative z-30"> <div className="relative h-20 z-30">
<Sticky <Sticky
enabled topOffset={-16 + bannerSize}
top={16 + bannerSize} stickyStyle={{
onStateChange={stickStateChanged} paddingTop: `${16 + bannerSize}px`,
}}
onFixedToggle={stickStateChanged}
> >
<SearchBarInput <SearchBarInput
onChange={setSearch} onChange={setSearch}

View File

@ -11,7 +11,7 @@ import { Menu } from "@/components/player/internals/ContextMenu";
import { CaptionCue } from "@/components/player/Player"; import { CaptionCue } from "@/components/player/Player";
import { Transition } from "@/components/Transition"; import { Transition } from "@/components/Transition";
import { Heading1 } from "@/components/utils/Text"; import { Heading1 } from "@/components/utils/Text";
import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles"; import { SubtitleStyling } from "@/stores/subtitles";
export function CaptionPreview(props: { export function CaptionPreview(props: {
fullscreen?: boolean; fullscreen?: boolean;
@ -24,7 +24,7 @@ export function CaptionPreview(props: {
className={classNames({ className={classNames({
"pointer-events-none overflow-hidden w-full rounded": true, "pointer-events-none overflow-hidden w-full rounded": true,
"aspect-video relative": !props.fullscreen, "aspect-video relative": !props.fullscreen,
"fixed inset-0 z-50": props.fullscreen, "fixed inset-0 z-[60]": props.fullscreen,
})} })}
> >
<Transition animation="fade" show={props.show}> <Transition animation="fade" show={props.show}>
@ -71,7 +71,7 @@ export function CaptionsPart(props: {
return ( return (
<div> <div>
<Heading1 border>Captions</Heading1> <Heading1 border>Captions</Heading1>
<div className="grid grid-cols-[1fr,356px] gap-8"> <div className="grid md:grid-cols-[1fr,356px] gap-8">
<div className="space-y-6"> <div className="space-y-6">
<CaptionSetting <CaptionSetting
label="Background opacity" label="Background opacity"

View File

@ -44,7 +44,7 @@ function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
return ( return (
<SettingsCard> <SettingsCard>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center gap-4">
<div className="my-3"> <div className="my-3">
<p className="text-white font-bold mb-3">Use custom proxy workers</p> <p className="text-white font-bold mb-3">Use custom proxy workers</p>
<p className="max-w-[20rem] font-medium"> <p className="max-w-[20rem] font-medium">
@ -103,7 +103,7 @@ function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) { function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
return ( return (
<SettingsCard> <SettingsCard>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center gap-4">
<div className="my-3"> <div className="my-3">
<p className="text-white font-bold mb-3">Custom server</p> <p className="text-white font-bold mb-3">Custom server</p>
<p className="max-w-[20rem] font-medium"> <p className="max-w-[20rem] font-medium">

View File

@ -1,6 +1,5 @@
import classNames from "classnames";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import Sticky from "react-stickynode"; import Sticky from "react-sticky-el";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
import { getBackendMeta } from "@/backend/accounts/meta"; import { getBackendMeta } from "@/backend/accounts/meta";
@ -10,34 +9,25 @@ import { Divider } from "@/components/utils/Divider";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { useIsMobile } from "@/hooks/useIsMobile"; import { useIsMobile } from "@/hooks/useIsMobile";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth";
function BackendUrl(props: { url: string }) { const rem = 16;
const url = props.url.replace(/https?:\/\//, "");
function SecureBadge(props: { url: string }) {
const secure = props.url.startsWith("https://"); const secure = props.url.startsWith("https://");
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-1 -mx-1 ml-3 px-1 rounded bg-largeCard-background font-bold">
<div <Icon icon={secure ? Icons.LOCK : Icons.UNLOCK} />
title={secure ? "Secure" : "Insecure"} Secure
className={classNames(
"w-5 min-w-[1.25rem] h-5 rounded flex justify-center items-center",
secure ? "bg-emerald-200/30 text-white" : "bg-red-600/20 text-white"
)}
>
<Icon
className="opacity-50"
icon={secure ? Icons.LOCK : Icons.UNLOCK}
/>
</div>
{url}
</div> </div>
); );
} }
export function SidebarPart() { export function SidebarPart() {
const { isMobile } = useIsMobile(); const { isMobile } = useIsMobile();
const { account } = useAuthStore();
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
const hostname = location.hostname; const hostname = location.hostname;
const rem = 16;
const [activeLink, setActiveLink] = useState(""); const [activeLink, setActiveLink] = useState("");
const settingLinks = [ const settingLinks = [
@ -54,7 +44,6 @@ export function SidebarPart() {
return getBackendMeta(backendUrl); return getBackendMeta(backendUrl);
}, [backendUrl]); }, [backendUrl]);
// TODO loading/error state for backend
useEffect(() => { useEffect(() => {
function recheck() { function recheck() {
const windowHeight = const windowHeight =
@ -97,11 +86,13 @@ export function SidebarPart() {
}, []); }, []);
return ( return (
<div> <div className="text-settings-sidebar-type-inactive sidebar-boundary">
<Sticky <Sticky
enabled={!isMobile} topOffset={-6 * rem}
top={10 * rem} // 10rem stickyClassName="pt-[6rem]"
className="text-settings-sidebar-type-inactive" disabled={isMobile}
hideOnBoundaryHit={false}
boundaryElement=".sidebar-boundary"
> >
<div className="hidden lg:block"> <div className="hidden lg:block">
<SidebarSection title="Settings"> <SidebarSection title="Settings">
@ -119,28 +110,56 @@ export function SidebarPart() {
<Divider /> <Divider />
</div> </div>
<SidebarSection className="text-sm" title="App information"> <SidebarSection className="text-sm" title="App information">
<div className="flex justify-between items-center space-x-3"> <div className="px-3 py-3.5 rounded-lg bg-largeCard-background bg-opacity-50 grid grid-cols-2 gap-4">
<span>Version</span> {/* Hostname */}
<span>{conf().APP_VERSION}</span> <div className="col-span-2 space-y-1">
<p className="text-type-dimmed font-medium">Hostname</p>
<p className="text-white">{hostname}</p>
</div>
{/* Backend URL */}
<div className="col-span-2 space-y-1">
<p className="text-type-dimmed font-medium flex items-center">
Backend URL
<SecureBadge url={backendUrl} />
</p>
<p className="text-white">
{backendUrl.replace(/https?:\/\//, "")}
</p>
</div>
{/* User ID */}
<div className="col-span-2 space-y-1">
<p className="text-type-dimmed font-medium">User ID</p>
<p className="text-white">{account?.userId ?? "Not logged in"}</p>
</div>
{/* App version */}
<div className="col-span-1 space-y-1">
<p className="text-type-dimmed font-medium">App version</p>
<p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-block">
{conf().APP_VERSION}
</p>
</div>
{/* Backend version */}
<div className="col-span-1 space-y-1">
<p className="text-type-dimmed font-medium">Backend version</p>
<p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-flex items-center gap-1">
{backendMeta.error ? (
<Icon
icon={Icons.WARNING}
className="text-type-danger text-base"
/>
) : null}
{backendMeta.loading ? (
<div className="h-4 w-12 bg-type-dimmed/20 rounded" />
) : (
backendMeta?.value?.version || "Unknown"
)}
</p>
</div>
</div> </div>
<div className="flex justify-between items-center space-x-3">
<span>Domain</span>
<span className="text-right">{hostname}</span>
</div>
{backendMeta.value ? (
<>
<div className="flex justify-between items-center space-x-3">
<span>Backend Version</span>
<span>{backendMeta.value.version}</span>
</div>
<div className="flex justify-between items-center space-x-3">
<span className="whitespace-nowrap">Backend URL</span>
<span className="text-right">
<BackendUrl url={backendUrl} />
</span>
</div>
</>
) : null}
</SidebarSection> </SidebarSection>
</Sticky> </Sticky>
</div> </div>

View File

@ -120,7 +120,7 @@ export function ThemePart(props: {
return ( return (
<div> <div>
<Heading1 border>Appearance</Heading1> <Heading1 border>Appearance</Heading1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-6 max-w-[700px]"> <div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]">
{/* default theme */} {/* default theme */}
<ThemePreview <ThemePreview
name="Default" name="Default"

View File

@ -120,6 +120,7 @@ export const defaultTheme = {
settings: { settings: {
sidebar: { sidebar: {
activeLink: "#171728", activeLink: "#171728",
badge: "#0A0A12",
type: { type: {
secondary: "#4B395F", secondary: "#4B395F",