WebStudio/src/navbar/index.tsx

404 lines
11 KiB
TypeScript

import React, { useRef, useEffect, useState } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import {
FaCircle,
FaRegClock,
FaRegUser,
FaBroadcastTower,
FaSpinner,
FaExclamationTriangle,
FaCog,
FaHeadphonesAlt,
} from "react-icons/fa";
import { RootState } from "../rootReducer";
import * as BroadcastState from "../broadcast/state";
import appLogo from "../assets/images/webstudio.svg";
import myradioLogo from "../assets/images/myradio.svg";
import { MYRADIO_NON_API_BASE } from "../api";
import "./navbar.scss";
import { closeAlert } from "./state";
import { ConnectionStateEnum } from "../broadcast/streamer";
import { VUMeter } from "../optionsMenu/helpers/VUMeter";
import { getShowplan, setItemPlayed } from "../showplanner/state";
import * as OptionsMenuState from "../optionsMenu/state";
import { setChannelPFL } from "../mixer/state";
import { secToHHMM, useInterval } from "../lib/utils";
import { Timelord } from "./timelord";
function nicifyConnectionState(state: ConnectionStateEnum): string {
switch (state) {
case "CONNECTED":
return "Connected!";
case "CONNECTING":
return "Connecting to server...";
case "CONNECTION_LOST":
return "Connection lost!";
case "CONNECTION_LOST_RECONNECTING":
return "Connection lost. Reconnecting...";
case "NOT_CONNECTED":
return "Not Connected";
case "LIVE":
return "Live!";
default:
console.warn("unhandled", state);
return state as string;
}
}
export function NavBarMyRadio() {
const dispatch = useDispatch();
const sessionState = useSelector((state: RootState) => state.session);
const redirect_url = encodeURIComponent(window.location.toString());
return (
<>
<div className="navbar-nav navbar-left">
<a className="navbar-brand" href="/">
<img
src="//ury.org.uk/myradio/img/URY.svg"
height="30"
alt="University Radio York Logo"
/>
</a>
<span className="navbar-brand divider"></span>
<a
className="navbar-brand logo-hover"
href={MYRADIO_NON_API_BASE}
title="Back to MyRadio"
>
<img className="logo-webstudio" src={appLogo} alt="Web Studio Logo" />
<img className="logo-myradio" src={myradioLogo} alt="MyRadio Logo" />
</a>
</div>
<ul className="nav navbar-nav navbar-right">
<li className="nav-item dropdown">
<a
className="nav-link dropdown-toggle"
href={
MYRADIO_NON_API_BASE + "/MyRadio/timeslot/?next=" + redirect_url
}
id="timeslotDropdown"
data-toggle="dropdown"
aria-expanded="false"
>
<FaRegClock />
&nbsp;
{sessionState.currentTimeslot &&
sessionState.currentTimeslot.start_time}
</a>
<div className="dropdown-menu" aria-labelledby="timeslotDropdown">
<a
className="dropdown-item"
href={
MYRADIO_NON_API_BASE + "/MyRadio/timeslot/?next=" + redirect_url
}
>
Switch Timeslot
</a>
<button
className="dropdown-item"
onClick={() =>
sessionState.currentTimeslot !== null &&
dispatch(getShowplan(sessionState.currentTimeslot.timeslot_id))
}
>
Reload Show Plan
</button>
<button
className="dropdown-item"
onClick={() =>
sessionState.currentTimeslot !== null &&
dispatch(setItemPlayed({ itemId: "all", played: false }))
}
>
Mark All Items Unplayed
</button>
<h6 className="dropdown-header">
{sessionState.currentTimeslot?.title}
</h6>
<h6 className="dropdown-header">
ID: {sessionState.currentTimeslot?.timeslot_id}
</h6>
</div>
</li>
<li className="nav-item navbar-profile dropdown">
<a
className="nav-link dropdown-toggle"
href={MYRADIO_NON_API_BASE + "/Profile/default/"}
id="dropdown07"
data-toggle="dropdown"
aria-expanded="false"
>
<FaRegUser />
&nbsp;
{sessionState.currentUser?.fname} {sessionState.currentUser?.sname}
</a>
<div className="dropdown-menu" aria-labelledby="dropdown07">
<a
className="dropdown-item"
target="_blank"
rel="noopener noreferrer"
href={MYRADIO_NON_API_BASE + "/Profile/default/"}
>
My Profile
</a>
<a
className="dropdown-item"
href={MYRADIO_NON_API_BASE + "/MyRadio/logout/"}
>
Logout
</a>
</div>
</li>
</ul>
</>
);
}
export function NavBarMain() {
return (
<>
<ul className="nav navbar-nav navbar-left">
<Timelord />
<SavingAlert />
</ul>
<ul className="nav navbar-nav navbar-right mr-0 pr-0">
<RegisterButton />
<RecordingButton />
<OptionsButton />
<MeterBridge />
</ul>
</>
);
}
function SavingAlert() {
const { planSaveError, planSaving } = useSelector(
(state: RootState) => state.showplan
);
return (
<>
{planSaving && (
<li className="btn rounded-0 py-2 nav-item alert-info">
<FaSpinner className="nav-spin mb-1" /> Saving show plan...
</li>
)}
{planSaveError && (
<li className="btn rounded-0 py-2 nav-item alert-danger">
<FaExclamationTriangle className="p-0 mr-1" />
{planSaveError}
</li>
)}
</>
);
}
function RegisterButton() {
const dispatch = useDispatch();
const broadcastState = useSelector((state: RootState) => state.broadcast);
const [connectButtonAnimating, setConnectButtonAnimating] = useState(false);
const prevRegistrationStage = useRef(broadcastState.stage);
useEffect(() => {
if (broadcastState.stage !== prevRegistrationStage.current) {
setConnectButtonAnimating(false);
}
prevRegistrationStage.current = broadcastState.stage;
}, [broadcastState.stage]);
return (
<>
<li className="nav-item" style={{ color: "white" }}>
<div className="nav-link">
<b>{nicifyConnectionState(broadcastState.connectionState)}</b>
</div>
</li>
<li
className="btn btn-outline-light rounded-0 pt-2 pb-1 nav-item nav-link connect"
onClick={() => {
setConnectButtonAnimating(true);
switch (broadcastState.stage) {
case "NOT_REGISTERED":
dispatch(BroadcastState.goOnAir());
break;
case "REGISTERED":
dispatch(BroadcastState.cancelTimeslot());
break;
}
}}
>
{connectButtonAnimating ? (
<>
<FaBroadcastTower size={17} className="mr-2" />
<FaSpinner size={17} className="nav-spin mr-2" />
</>
) : (
<>
<FaBroadcastTower size={17} className="mr-2" />
{broadcastState.stage === "NOT_REGISTERED" && "Register"}
{broadcastState.stage === "REGISTERED" && "Stop"}
</>
)}
</li>
</>
);
}
function RecordingButton() {
const recordingState = useSelector(
(state: RootState) => state.broadcast.recordingState
);
const enableRecording = useSelector(
(state: RootState) => state.settings.enableRecording
);
const [count, setCount] = useState(0);
// Make a persistant recording counter.
useInterval(() => {
if (recordingState !== "CONNECTED") {
setCount(0);
} else {
setCount((c) => c + 1);
}
}, 1000);
const dispatch = useDispatch();
return (
<>
{enableRecording && (
<li
className={
"btn rounded-0 pt-2 pb-1 nav-item nav-link " +
(recordingState === "CONNECTED"
? "btn-outline-danger active"
: "btn-outline-light")
}
onClick={() =>
dispatch(
recordingState === "NOT_CONNECTED"
? BroadcastState.startRecording()
: BroadcastState.stopRecording()
)
}
>
<FaCircle
size={17}
className={
recordingState === "CONNECTED" ? "rec-blink" : "rec-stop"
}
/>{" "}
{recordingState === "CONNECTED" ? secToHHMM(count) : "Record"}
</li>
)}
</>
);
}
function OptionsButton() {
const dispatch = useDispatch();
return (
<li
className="btn btn-outline-light rounded-0 pt-2 pb-1 nav-item nav-link"
onClick={() => dispatch(OptionsMenuState.open())}
>
<FaCog size={17} /> Options
</li>
);
}
function MeterBridge() {
const dispatch = useDispatch();
const playerPFLs = useSelector(
(state: RootState) => state.mixer.players.map((x) => x.pfl),
shallowEqual
);
const isPFL = useSelector((state) => playerPFLs).some((x) => x === true);
return (
<>
{isPFL && (
<li
className="btn btn-danger rounded-0 pt-2 pb-1 nav-item nav-link clear-pfl"
onClick={() => dispatch(setChannelPFL(-1, false))}
>
<FaHeadphonesAlt size={17} /> Clear PFL
</li>
)}
<li className={"nav-item px-2 nav-vu" + (isPFL ? " pfl-live" : "")}>
{isPFL && (
<VUMeter
width={235}
height={34}
source="pfl"
range={[-40, 3]}
stereo={true}
/>
)}
{!isPFL && (
<VUMeter
width={235}
height={40}
source="master"
range={[-40, 3]}
stereo={true}
/>
)}
</li>
</>
);
}
function AlertBar() {
const state = useSelector((state: RootState) => state.navbar.currentAlert);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const dispatch = useDispatch();
useEffect(() => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
if (typeof state?.closure === "number") {
timeoutRef.current = setTimeout(() => {
dispatch(closeAlert());
}, state.closure);
}
}, [dispatch, state]);
return (
<div
className={`alertbar alert alert-${state?.color} ${
state !== null ? "visible" : ""
}`}
>
{state?.content}
{state?.closure !== null && (
<button
className="close"
aria-label="Dismiss"
onClick={() => dispatch(closeAlert())}
>
<span aria-hidden>&times;</span>
</button>
)}
</div>
);
}
export function CombinedNavAlertBar() {
return (
<>
<AlertBar />
<header className="navbar navbar-myradio navbar-expand-sm hover-menu p-0 bd-navbar">
<span className="hover-label hover-label-hide text-light text-center">
Hover for MyRadio Menu
</span>
<nav className="container-fluid px-0">
<NavBarMyRadio />
</nav>
</header>
<header className="navbar navbar-main navbar-expand-sm p-0 bd-navbar">
<nav className="container-fluid px-0">
<NavBarMain />
</nav>
</header>
</>
);
}