235 lines
5.6 KiB
TypeScript
235 lines
5.6 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { DSP, AppPage } from '../../types/types';
|
|
import AnimatedLogo from '../../assets/icons/AnimatedLogo';
|
|
import { PlusIcon } from '../../assets/icons/PlusIcon';
|
|
import { CrownIcon } from '../../assets/icons/CrownIcon';
|
|
import Led from '../Led/Led';
|
|
import { CloseIcon } from '../../assets/icons/CloseIcon';
|
|
|
|
import './Topbar.css';
|
|
|
|
function getStep(count: number) {
|
|
if (count <= 4) return 4;
|
|
if (count <= 6) return 6;
|
|
if (count <= 8) return 8;
|
|
return 10;
|
|
}
|
|
|
|
function Tab({
|
|
dsp,
|
|
active,
|
|
dimmed,
|
|
step,
|
|
onSelect,
|
|
onRemove,
|
|
onConnect,
|
|
onDisconnect,
|
|
}: {
|
|
dsp: DSP;
|
|
active: boolean;
|
|
dimmed: boolean;
|
|
step: number;
|
|
onSelect: () => void;
|
|
onRemove: () => void;
|
|
onConnect: (dsp: DSP) => void;
|
|
onDisconnect: (dsp: DSP) => void;
|
|
}) {
|
|
const handleLedClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
|
|
if (dsp.status === 'connected') {
|
|
onDisconnect(dsp);
|
|
} else {
|
|
onConnect(dsp);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
className={
|
|
'tab' +
|
|
(active ? ' tab--active' : '') +
|
|
(dimmed ? ' tab--dimmed' : '')
|
|
}
|
|
data-step={step}
|
|
onClick={onSelect}
|
|
>
|
|
<span
|
|
className="tab-led"
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={handleLedClick}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (dsp.status === 'connected') {
|
|
onDisconnect(dsp);
|
|
} else {
|
|
onConnect(dsp);
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<Led status={dsp.status} />
|
|
</span>
|
|
|
|
<span className="tab-name" title={dsp.name}>
|
|
{dsp.name}
|
|
</span>
|
|
|
|
<span
|
|
className="tab-close"
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-label={`Remove ${dsp.name}`}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onRemove();
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onRemove();
|
|
}
|
|
}}
|
|
>
|
|
<CloseIcon />
|
|
</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function TopBar({
|
|
dsps,
|
|
selected,
|
|
page,
|
|
setPage,
|
|
setSelect,
|
|
onSelect,
|
|
onRemove,
|
|
onAddClick,
|
|
onConnect,
|
|
onDisconnect,
|
|
}: {
|
|
dsps: DSP[];
|
|
selected: number | null;
|
|
page: AppPage;
|
|
setPage: (page: AppPage) => void;
|
|
setSelect: (select: number | null) => void;
|
|
onSelect: (id: number) => void;
|
|
onRemove: (dsp: DSP) => void;
|
|
onAddClick: () => void;
|
|
onConnect: (dsp: DSP) => void;
|
|
onDisconnect: (dsp: DSP) => void;
|
|
}) {
|
|
const hasTabs = dsps.length > 0;
|
|
const tabstripRef = useRef<HTMLDivElement>(null);
|
|
const prevCount = useRef(dsps.length);
|
|
const step = getStep(dsps.length);
|
|
|
|
const updateFade = () => {
|
|
const el = tabstripRef.current;
|
|
if (!el) return;
|
|
const atStart = el.scrollLeft <= 1;
|
|
const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
|
|
el.dataset.fadeStart = String(!atStart);
|
|
el.dataset.fadeEnd = String(!atEnd && el.scrollWidth > el.clientWidth);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const el = tabstripRef.current;
|
|
if (!el) return;
|
|
const onWheel = (e: WheelEvent) => {
|
|
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
el.scrollLeft += e.deltaY;
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
el.addEventListener('wheel', onWheel, { passive: false });
|
|
return () => el.removeEventListener('wheel', onWheel);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
updateFade();
|
|
const el = tabstripRef.current;
|
|
el?.addEventListener('scroll', updateFade);
|
|
window.addEventListener('resize', updateFade);
|
|
return () => {
|
|
el?.removeEventListener('scroll', updateFade);
|
|
window.removeEventListener('resize', updateFade);
|
|
};
|
|
}, [dsps.length]);
|
|
|
|
useEffect(() => {
|
|
if (dsps.length > prevCount.current && tabstripRef.current) {
|
|
tabstripRef.current.scrollTo({
|
|
left: tabstripRef.current.scrollWidth,
|
|
behavior: 'smooth',
|
|
});
|
|
}
|
|
prevCount.current = dsps.length;
|
|
}, [dsps.length]);
|
|
|
|
return (
|
|
<header className="topbar">
|
|
<button
|
|
className="logo-slot"
|
|
onClick={() => {
|
|
setSelect(null);
|
|
setPage('home');
|
|
}}
|
|
aria-label="Go to home"
|
|
>
|
|
<AnimatedLogo />
|
|
</button>
|
|
|
|
<div className="tabstrip" ref={tabstripRef}>
|
|
{dsps.map((dsp) => (
|
|
<Tab
|
|
key={dsp.id}
|
|
dsp={dsp}
|
|
step={step}
|
|
active={selected === dsp.id && !(page == 'add-dsp')}
|
|
dimmed={page === 'add-dsp'}
|
|
onSelect={() => onSelect(dsp.id)}
|
|
onRemove={() => onRemove(dsp)}
|
|
onConnect={onConnect}
|
|
onDisconnect={onDisconnect}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div className="tabstrip-actions">
|
|
{hasTabs ? (
|
|
<button className="tab-add tab-action" aria-label="Add DSP" onClick={onAddClick}>
|
|
<PlusIcon className="icon-tab-add" />
|
|
</button>
|
|
) : (
|
|
<button className="tab-add-empty tab-action" onClick={onAddClick}>
|
|
<PlusIcon className="icon-tab-add-empty" />
|
|
<span>Add DSP</span>
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
className="crown-action tab-action"
|
|
title="Credits & Support"
|
|
aria-label="Credits & Support"
|
|
onClick={() => {
|
|
setPage('credits');
|
|
setSelect(null);
|
|
}}
|
|
>
|
|
<CrownIcon className="icon-crown" />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
export default TopBar;
|