404 lines
11 KiB
TypeScript
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 />
|
|
|
|
{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 />
|
|
|
|
{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>×</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>
|
|
</>
|
|
);
|
|
}
|