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-i18next": "^12.1.1",
"react-router-dom": "^5.2.0",
"react-stickynode": "^4.1.0",
"react-sticky-el": "^2.1.0",
"react-use": "^17.4.0",
"slugify": "^1.6.6",
"subsrt-ts": "^2.1.1",

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import classNames from "classnames";
import { useMemo } from "react";
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
import { Icon, Icons } from "@/components/Icon";
import { UserIcon } from "@/components/UserIcon";
import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart";
@ -42,16 +44,40 @@ export function UserAvatar(props: {
sizeClass?: string;
iconClass?: string;
bottom?: React.ReactNode;
withName?: boolean;
}) {
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 (
<Avatar
profile={auth.account.profile}
sizeClass={props.sizeClass ?? "w-[2rem] h-[2rem]"}
iconClass={props.iconClass}
bottom={props.bottom}
/>
<>
<Avatar
profile={auth.account.profile}
sizeClass={props.sizeClass ?? "w-[2rem] h-[2rem]"}
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 (
<div className="relative is-dropdown">
<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}
onClick={toggleOpen}
onKeyUp={(evt) => evt.key === "Enter" && toggleOpen()}
>
{props.children}
<Icon
className={classNames(
"text-xl transition-transform duration-100",
open ? "rotate-180" : ""
)}
icon={Icons.CHEVRON_DOWN}
/>
</div>
<Transition animation="slide-down" show={open}>
<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 className="relative">
<LinksDropdown>
{loggedIn ? <UserAvatar /> : <NoUserAvatar />}
{loggedIn ? <UserAvatar withName /> : <NoUserAvatar />}
</LinksDropdown>
</div>
</div>

View File

@ -10,7 +10,7 @@ export function Heading1(props: TextProps) {
return (
<h1
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.className ?? "",
].join(" ")}
@ -24,7 +24,7 @@ export function Heading2(props: TextProps) {
return (
<h2
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.className ?? "",
].join(" ")}
@ -38,7 +38,7 @@ export function Heading3(props: TextProps) {
return (
<h2
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.className ?? "",
].join(" ")}

View File

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

View File

@ -87,7 +87,9 @@ function AuthWrapper() {
if (status.error)
return (
<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>
);
return <App />;

View File

@ -42,7 +42,7 @@ function SettingsLayout(props: { children: React.ReactNode }) {
<div
className={classNames(
"grid gap-12",
isMobile ? "grid-cols-1" : "lg:grid-cols-[310px,1fr]"
isMobile ? "grid-cols-1" : "lg:grid-cols-[280px,1fr]"
)}
>
<SidebarPart />
@ -240,16 +240,24 @@ export function SettingsPage() {
</div>
</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 ${
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"
}`}
>
<p className="text-type-danger">You have unsaved changes</p>
<div className="space-x-6">
<Button theme="secondary" onClick={state.reset}>
<div className="space-x-3 w-full md:w-auto flex">
<Button
className="w-full md:w-auto"
theme="secondary"
onClick={state.reset}
>
Reset
</Button>
<Button theme="purple" onClick={saveChanges}>
<Button
className="w-full md:w-auto"
theme="purple"
onClick={saveChanges}
>
Save
</Button>
</div>

View File

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

View File

@ -11,7 +11,7 @@ import { Menu } from "@/components/player/internals/ContextMenu";
import { CaptionCue } from "@/components/player/Player";
import { Transition } from "@/components/Transition";
import { Heading1 } from "@/components/utils/Text";
import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles";
import { SubtitleStyling } from "@/stores/subtitles";
export function CaptionPreview(props: {
fullscreen?: boolean;
@ -24,7 +24,7 @@ export function CaptionPreview(props: {
className={classNames({
"pointer-events-none overflow-hidden w-full rounded": true,
"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}>
@ -71,7 +71,7 @@ export function CaptionsPart(props: {
return (
<div>
<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">
<CaptionSetting
label="Background opacity"

View File

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

View File

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

View File

@ -120,7 +120,7 @@ export function ThemePart(props: {
return (
<div>
<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 */}
<ThemePreview
name="Default"

View File

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