Skip to content

Commit

Permalink
Add settings UI for BeatSaber data source
Browse files Browse the repository at this point in the history
  • Loading branch information
DJDavid98 committed Oct 19, 2023
1 parent 0c00527 commit edb228a
Show file tree
Hide file tree
Showing 18 changed files with 273 additions and 140 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ whose work I myself used during the creation of this overlay.
* Pulsoid logo is based on the Pulsoid App icon. This project is not affiliated with Pulsoid in any
way, shape, or form.
* Bouncy icon made by [KisuPantteri](https://www.twitch.tv/KisuPantteri)
* Beat Saber UI font: [Teko](https://fonts.google.com/specimen/Teko)
* Overlay UI font: [Kalam](https://fonts.google.com/specimen/Kalam)

### Appreciation

Expand Down
6 changes: 3 additions & 3 deletions src/js/BeatSaverMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ export const BeatSaverMap: FC<BeatSaverMapProps> = ({ mapId, inChat }) => {
return <div className={classNames(styles['beat-saver-map'], { [styles['in-chat']]: inChat })}>
<div className={styles['beat-saver-song-info']}>
<SongInfo
name={data?.metadata?.songName ?? (isLoading ? '' : 'Could not retrieve song information')}
author={isLoading ? 'Loading map data…' : data?.metadata?.songAuthorName}
name={data?.metadata?.songName ?? (isLoading ? '' : 'Unknown Song')}
author={isLoading ? 'Loading map data…' : (data?.metadata?.songAuthorName ?? 'Unknown Artist')}
duration={data?.metadata?.duration}
mapper={data?.metadata?.levelAuthorName}
subName={data?.metadata?.songSubName}
url={publishedVersion?.coverURL}
bsr={data?.id}
bsr={data?.id ?? mapId}
difficulty={publishedVersion?.diffs?.filter(diff => diff.characteristic === 'Standard').map(diff => mapDifficulty(diff.difficulty)).join(', ')}
/>
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/js/ExternalLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { FC, PropsWithChildren } from 'react';

export const ExternalLink: FC<PropsWithChildren<{ href: string }>> = ({ href, children }) =>
<a href={href} target="_blank" rel="noreferrer noopener">{children}</a>;
13 changes: 8 additions & 5 deletions src/js/beat-saber/SongDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,22 @@ export const SongDetails: FunctionComponent<SongDetails> = ({

const items = [];
if (difficulty) {
items.push(<span className="difficulty">{mapDifficulty(difficulty)}</span>);
items.push(<span
key={items.length}
className="difficulty"
>{mapDifficulty(difficulty)}</span>);
}
if (duration) {
items.push(<span className="duration">{df.format(duration)}</span>);
items.push(<span key={items.length} className="duration">{df.format(duration)}</span>);
}
if (star) {
items.push(<span className="star">{nf.format(star)}</span>);
items.push(<span key={items.length} className="star">{nf.format(star)}</span>);
}
if (pp) {
items.push(<span className="pp">{nf.format(pp)}pp</span>);
items.push(<span key={items.length} className="pp">{nf.format(pp)}pp</span>);
}
if (bsr) {
items.push(<span className="bsr">!bsr {mapDifficulty(bsr)}</span>);
items.push(<span key={items.length} className="bsr">!bsr {bsr}</span>);
}

if (items.length === 0) return null;
Expand Down
3 changes: 2 additions & 1 deletion src/js/model/settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RemovableElementId } from './removable-element-id';
import { BeatSaberDataSource } from '../beat-saber/BeatSaber';
import { FC } from 'react';

export enum SettingName {
PULSOID_TOKEN = 'pulsoidToken',
Expand Down Expand Up @@ -54,5 +55,5 @@ export enum SettingsPage {
export interface SettingsPageOptions {
name: string;
icon: string;
disabled?: boolean;
component?: FC;
}
46 changes: 46 additions & 0 deletions src/js/settings/LabelledInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as styles from '../../scss/modules/labelledInput.module.scss';
import {
ChangeEventHandler,
forwardRef,
ForwardRefRenderFunction,
PropsWithChildren,
useId
} from 'react';

export interface LabelledInputProps extends PropsWithChildren {
type: 'checkbox' | 'radio';
name?: string;
value?: string;
checked?: boolean;
displayName: string;
onChange: ChangeEventHandler<HTMLInputElement>;
}

const LabelledInputComponent: ForwardRefRenderFunction<HTMLInputElement, LabelledInputProps> = ({
type,
name,
value,
checked,
displayName,
children,
onChange
}, ref) => {
const id = useId();
return <div className={styles['labelled-input']}>
<input
id={id}
type={type}
name={name}
value={value}
checked={checked}
onChange={onChange}
ref={ref}
/>
<label htmlFor={id}>
<span>{displayName}</span>
{children}
</label>
</div>;
};

export const LabelledInput = forwardRef(LabelledInputComponent);
38 changes: 5 additions & 33 deletions src/js/settings/SettingsDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { FC, ReactNode, useEffect, useRef } from 'react';
import { FC, useEffect, useRef } from 'react';
import { SettingsPage } from '../model/settings';
import { settingPages, SettingsNavigation } from './SettingsNavigation';
import { SettingsPageElements } from './pages/SettingsPageElements';
import { SettingsPageHeartRate } from './pages/SettingsPageHeartRate';
import * as styles from '../../scss/modules/SettingsDialog.module.scss';
import { SettingsPageImportExport } from './pages/SettingsPageImportExport';
import { SettingsPageChatOverlay } from './pages/SettingsPageChatOverlay';
import { SettingsPageCredits } from './pages/SettingsPageCredits';
import { SettingsPageObsIntegration } from './pages/SettingsPageObsIntegration';

interface SettingsDialogProps {
isOpen: boolean;
page?: SettingsPage;
close: VoidFunction;
}

const FallbackSettingsPage: FC = () => <p><em>There are no settings in this section yet.</em></p>;

export const SettingsDialog: FC<SettingsDialogProps> = ({
page = SettingsPage.ELEMENTS,
isOpen,
Expand Down Expand Up @@ -42,31 +38,7 @@ export const SettingsDialog: FC<SettingsDialogProps> = ({
});
}, [close]);

let settingsPage: ReactNode;
// noinspection JSUnreachableSwitchBranches
switch (page) {
case SettingsPage.ELEMENTS:
settingsPage = <SettingsPageElements />;
break;
case SettingsPage.HEART_RATE:
settingsPage = <SettingsPageHeartRate />;
break;
case SettingsPage.IMPORT_EXPORT:
settingsPage = <SettingsPageImportExport />;
break;
case SettingsPage.CHAT_OVERLAY:
settingsPage = <SettingsPageChatOverlay />;
break;
case SettingsPage.CREDITS:
settingsPage = <SettingsPageCredits />;
break;
case SettingsPage.OBS_INTEGRATION:
settingsPage = <SettingsPageObsIntegration />;
break;
default:
settingsPage = <p><em>There are no settings in this section yet.</em></p>;
break;
}
const SettingsPage = settingPages[page].component ?? FallbackSettingsPage;

return <dialog className={styles['settings-dialog'] + ' '} ref={dialogRef} hidden={!isOpen}>
<SettingsNavigation currentPage={page} />
Expand All @@ -75,7 +47,7 @@ export const SettingsDialog: FC<SettingsDialogProps> = ({
<span className={styles['muted']}>Settings /</span>
{` ${settingPages[page].name}`}
</h1>
{isOpen && settingsPage}
{isOpen && <SettingsPage />}
</div>
<div className={styles['close-button-wrap']}>
<button className={styles['close-button']} type="button" onClick={close}>
Expand Down
23 changes: 17 additions & 6 deletions src/js/settings/SettingsNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,58 @@ import { SettingsPage, SettingsPageOptions } from '../model/settings';
import { useSettings } from '../contexts/settings-context';
import * as styles from '../../scss/modules/SettingsNavigation.module.scss';
import classNames from 'classnames';
import { SettingsPageElements } from './pages/SettingsPageElements';
import { SettingsPageBeatSaber } from './pages/SettingsPageBeatSaber';
import { SettingsPageHeartRate } from './pages/SettingsPageHeartRate';
import { SettingsPageChatOverlay } from './pages/SettingsPageChatOverlay';
import { SettingsPageImportExport } from './pages/SettingsPageImportExport';
import { SettingsPageObsIntegration } from './pages/SettingsPageObsIntegration';
import { SettingsPageCredits } from './pages/SettingsPageCredits';

// TODO More sophisticated iconography
export const settingPages: Record<SettingsPage, SettingsPageOptions> = {
[SettingsPage.ELEMENTS]: {
name: 'Elements',
icon: '👁️'
icon: '👁️',
component: SettingsPageElements,
},
[SettingsPage.BEAT_SABER]: {
name: 'Beat Saber',
icon: '🔽',
disabled: true,
component: SettingsPageBeatSaber,
},
[SettingsPage.HEART_RATE]: {
name: 'Heart Rate',
icon: '❤️'
icon: '❤️',
component: SettingsPageHeartRate,
},
[SettingsPage.CHANNEL_BUG]: {
name: 'Channel Bug',
icon: '🐞',
disabled: true,
},
[SettingsPage.CHAT_OVERLAY]: {
name: 'Chat Overlay',
icon: '💬',
component: SettingsPageChatOverlay,
},
[SettingsPage.BOUNCY]: {
name: 'Bouncy',
icon: '🏀',
disabled: true,
},
[SettingsPage.IMPORT_EXPORT]: {
name: 'Import / Export',
icon: '💾',
component: SettingsPageImportExport,
},
[SettingsPage.OBS_INTEGRATION]: {
name: 'OBS Integration',
icon: '🔴',
component: SettingsPageObsIntegration,
},
[SettingsPage.CREDITS]: {
name: 'Credits',
icon: 'ℹ️',
component: SettingsPageCredits,
},
};
export const settingsPageNames = Object.keys(settingPages) as SettingsPage[];
Expand All @@ -58,7 +69,7 @@ export const SettingsNavigation: FC<{ currentPage: SettingsPage }> = ({ currentP
className={classNames(styles['nav-button'], { [styles['nav-current']]: current })}
key={pageName}
onClick={() => openSettings(pageName)}
disabled={pageOptions.disabled}
disabled={!pageOptions.component}
>
{pageOptions.icon}&nbsp;{pageOptions.name}
</button>;
Expand Down
42 changes: 21 additions & 21 deletions src/js/settings/pages/ElementSettingsTree.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ChangeEventHandler, FC, useCallback, useId } from 'react';
import { ChangeEventHandler, FC, useCallback } from 'react';
import {
isRemovableElementId,
RemovableElementId,
RemovableElementsTree
} from '../../model/removable-element-id';
import * as styles from '../../../scss/modules/ElementSettingsTree.module.scss';
import { LabelledInput } from '../LabelledInput';

export interface ElementSettingsTreeProps {
tree: RemovableElementsTree;
Expand All @@ -14,31 +15,30 @@ export interface ElementSettingsTreeProps {
level?: number;
}

export const ElementSettingsTree: FC<ElementSettingsTreeProps> = ({ rootElementId, tree, disabledElements, setElementEnabled, level = 0 }) => {
export const ElementSettingsTree: FC<ElementSettingsTreeProps> = ({
rootElementId,
tree,
disabledElements,
setElementEnabled,
level = 0
}) => {
const meta = tree[rootElementId];
const levelId = useId();
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback((e) => {
const { checked, dataset: { element } } = e.target;
if (isRemovableElementId(element)) {
setElementEnabled(element, checked);
const { checked, value } = e.target;
if (isRemovableElementId(value)) {
setElementEnabled(value, checked);
}
}, [setElementEnabled]);
return <div className={styles['element-settings-tree']} data-level={level}>
{level > 0 && <>
<div className={styles['meta']}>
<input
id={levelId}
type="checkbox"
data-element={rootElementId}
checked={!disabledElements.has(rootElementId)}
onChange={handleChange}
/>
<label htmlFor={levelId}>
<span>{meta.name}</span>
{meta.description && <p>{meta.description}</p>}
</label>
</div>
</>}
{level > 0 && <LabelledInput
type="checkbox"
value={rootElementId}
checked={!disabledElements.has(rootElementId)}
onChange={handleChange}
displayName={meta.name}
>
{meta.description && <p>{meta.description}</p>}
</LabelledInput>}
{meta.children?.map(childId => <ElementSettingsTree
key={childId}
tree={tree}
Expand Down
Loading

0 comments on commit edb228a

Please sign in to comment.