Merge pull request #154 from UniversityRadioYork/mstratford-save-tidies
A couple extra bits for UI Reshuffle and Saving.
This commit is contained in:
commit
0615569eb8
10 changed files with 239 additions and 94 deletions
34
src/App.css
34
src/App.css
|
@ -99,20 +99,42 @@ button {
|
|||
flex: 1;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.timing-buttons .delete {
|
||||
max-width: 3em;
|
||||
color: gray;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.timing-buttons .intro {
|
||||
border-color: rgba(125, 0, 255, 0.8);
|
||||
color: rgba(125, 0, 255, 0.8);
|
||||
border-color: rgb(125, 0, 255);
|
||||
color: rgb(125, 0, 255);
|
||||
}
|
||||
|
||||
.timing-buttons .cue {
|
||||
border-color: rgba(0, 100, 0, 0.9);
|
||||
color: rgba(0, 100, 0, 0.9);
|
||||
border-color: rgb(0, 100, 0);
|
||||
color: rgb(0, 100, 0);
|
||||
}
|
||||
|
||||
.timing-buttons .outro {
|
||||
border-color: rgba(255, 0, 0, 0.7);
|
||||
color: rgba(255, 0, 0, 0.7);
|
||||
border-color: rgb(255, 0, 0);
|
||||
color: rgb(255, 0, 0);
|
||||
}
|
||||
|
||||
.timing-buttons.not-central .intro,
|
||||
.timing-buttons.not-central .outro {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.timing-buttons.text-light > * {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.timing-buttons.text-light .intro {
|
||||
border-color: rgb(179, 115, 248);
|
||||
}
|
||||
|
||||
.timing-buttons.text-light .cue {
|
||||
border-color: rgb(0, 255, 0);
|
||||
}
|
||||
|
||||
.waveform span {
|
||||
|
|
16
src/App.scss
16
src/App.scss
|
@ -76,6 +76,8 @@ $number-of-channels: 3;
|
|||
background: black;
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
border-left: 1px solid gray;
|
||||
border-right: 1px solid gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +101,13 @@ $number-of-channels: 3;
|
|||
}
|
||||
.mic-control {
|
||||
background: var(--sidebar-background);
|
||||
padding: 0 0 0.4rem 0;
|
||||
position: relative;
|
||||
.toggle {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
top: 0.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
#sidebar-toggle {
|
||||
|
@ -127,6 +135,12 @@ $number-of-channels: 3;
|
|||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
#importerIframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.sp-mic-live {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
|
@ -102,7 +102,8 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) {
|
|||
if ("outro" in this.wavesurfer.regions.list) {
|
||||
// If the outro is set to 0, we assume that's no outro.
|
||||
if (startTime === 0) {
|
||||
delete this.wavesurfer.regions.list.outro;
|
||||
// Can't just delete the outro, so set it to the end of the track to hide it.
|
||||
this.wavesurfer.regions.list.outro.start = this.wavesurfer.regions.list.outro.end;
|
||||
} else {
|
||||
this.wavesurfer.regions.list.outro.start = startTime;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
FaBroadcastTower,
|
||||
FaSpinner,
|
||||
FaExclamationTriangle,
|
||||
FaCog,
|
||||
} from "react-icons/fa";
|
||||
|
||||
import { RootState } from "../rootReducer";
|
||||
|
@ -24,6 +25,8 @@ import { ConnectionStateEnum } from "../broadcast/streamer";
|
|||
import { VUMeter } from "../optionsMenu/helpers/VUMeter";
|
||||
import { getShowplan } from "../showplanner/state";
|
||||
|
||||
import * as OptionsMenuState from "../optionsMenu/state";
|
||||
|
||||
function nicifyConnectionState(state: ConnectionStateEnum): string {
|
||||
switch (state) {
|
||||
case "CONNECTED":
|
||||
|
@ -238,7 +241,7 @@ export function NavBarMain() {
|
|||
"btn rounded-0 pt-2 pb-1 nav-item nav-link " +
|
||||
(broadcastState.recordingState === "CONNECTED"
|
||||
? "btn-outline-danger active"
|
||||
: "btn-outline-warning")
|
||||
: "btn-outline-light")
|
||||
}
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
|
@ -270,6 +273,13 @@ export function NavBarMain() {
|
|||
)}
|
||||
</li>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<li className="nav-item px-2 nav-vu">
|
||||
<VUMeter
|
||||
width={235}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { FaTimes, FaFileImport } from "react-icons/fa";
|
||||
import Modal from "react-modal";
|
||||
import { Button } from "reactstrap";
|
||||
|
@ -11,6 +11,22 @@ interface ImporterProps {
|
|||
// TODO: This needs updating to actually either provide the weighting channel values (less preferred)
|
||||
// or update the importer to work that out itself.
|
||||
export function ImporterModal(props: ImporterProps) {
|
||||
// Add support for closing the modal when the importer wants to reload the show plan.
|
||||
// There is a similar listener in showplanner/index.tsx to actually reload the show plan.
|
||||
useEffect(() => {
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
if (!event.origin.includes("ury.org.uk")) {
|
||||
return;
|
||||
}
|
||||
if (event.data === "reload_showplan") {
|
||||
props.close();
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Modal isOpen={props.isOpen} onRequestClose={props.close}>
|
||||
<div>
|
||||
|
@ -28,8 +44,8 @@ export function ImporterModal(props: ImporterProps) {
|
|||
</div>
|
||||
<hr />
|
||||
<iframe
|
||||
id="uploadIframe"
|
||||
src="https://ury.org.uk/myradio/NIPSWeb/import/"
|
||||
id="importerIframe"
|
||||
src={process.env.REACT_APP_MYRADIO_NONAPI_BASE + "/NIPSWeb/import/"}
|
||||
frameBorder="0"
|
||||
title="Import From Showplan"
|
||||
></iframe>
|
||||
|
|
|
@ -27,7 +27,9 @@ export function LibraryUploadModal(props: LibraryUploadProps) {
|
|||
<hr />
|
||||
<iframe
|
||||
id="uploadIframe"
|
||||
src="https://ury.org.uk/myradio/NIPSWeb/manage_library/"
|
||||
src={
|
||||
process.env.REACT_APP_MYRADIO_NONAPI_BASE + "/NIPSWeb/manage_library/"
|
||||
}
|
||||
frameBorder="0"
|
||||
title="Upload to Library"
|
||||
></iframe>
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
FaPlay,
|
||||
FaPause,
|
||||
FaStop,
|
||||
FaTrash,
|
||||
} from "react-icons/fa";
|
||||
import { omit } from "lodash";
|
||||
import { RootState } from "../rootReducer";
|
||||
|
@ -64,10 +65,12 @@ const setTrackIntro = (
|
|||
track: api.Track,
|
||||
secs: number,
|
||||
player: number
|
||||
): AppThunk => async (dispatch) => {
|
||||
): AppThunk => async (dispatch, getState) => {
|
||||
try {
|
||||
dispatch(MixerState.setLoadedItemIntro(player, secs));
|
||||
await api.setTrackIntro(track.trackid, secs);
|
||||
if (getState().settings.saveShowPlanChanges) {
|
||||
await api.setTrackIntro(track.trackid, secs);
|
||||
}
|
||||
dispatch(ShowPlanState.setItemTimings({ item: track, intro: secs }));
|
||||
} catch (e) {
|
||||
dispatch(ShowPlanState.planSaveError("Failed saving track intro."));
|
||||
|
@ -79,10 +82,12 @@ const setTrackOutro = (
|
|||
track: api.Track,
|
||||
secs: number,
|
||||
player: number
|
||||
): AppThunk => async (dispatch) => {
|
||||
): AppThunk => async (dispatch, getState) => {
|
||||
try {
|
||||
dispatch(MixerState.setLoadedItemOutro(player, secs));
|
||||
await api.setTrackOutro(track.trackid, secs);
|
||||
if (getState().settings.saveShowPlanChanges) {
|
||||
await api.setTrackOutro(track.trackid, secs);
|
||||
}
|
||||
dispatch(ShowPlanState.setItemTimings({ item: track, outro: secs }));
|
||||
} catch (e) {
|
||||
dispatch(ShowPlanState.planSaveError("Failed saving track outro."));
|
||||
|
@ -94,10 +99,12 @@ const setTrackCue = (
|
|||
item: api.TimeslotItem,
|
||||
secs: number,
|
||||
player: number
|
||||
): AppThunk => async (dispatch) => {
|
||||
): AppThunk => async (dispatch, getState) => {
|
||||
try {
|
||||
dispatch(MixerState.setLoadedItemCue(player, secs));
|
||||
await api.setTimeslotItemCue(item.timeslotitemid, secs);
|
||||
if (getState().settings.saveShowPlanChanges) {
|
||||
await api.setTimeslotItemCue(item.timeslotitemid, secs);
|
||||
}
|
||||
dispatch(ShowPlanState.setItemTimings({ item, cue: secs }));
|
||||
} catch (e) {
|
||||
dispatch(ShowPlanState.planSaveError("Failed saving track cue."));
|
||||
|
@ -108,14 +115,30 @@ const setTrackCue = (
|
|||
function TimingButtons({ id }: { id: number }) {
|
||||
const dispatch = useDispatch();
|
||||
const state = useSelector((state: RootState) => state.mixer.players[id]);
|
||||
const [showDeleteMenu, setShowDeleteMenu] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="timing-buttons">
|
||||
<div className="label">Set Marker:</div>
|
||||
<div
|
||||
className={
|
||||
"timing-buttons " +
|
||||
(state.loadedItem && state.loadedItem.type !== "central"
|
||||
? "not-central"
|
||||
: "") +
|
||||
(showDeleteMenu ? " bg-dark text-light" : "")
|
||||
}
|
||||
>
|
||||
<div className="label">{showDeleteMenu ? "Delete:" : "Set"} Marker:</div>
|
||||
<div
|
||||
className="intro btn btn-sm btn-outline-secondary rounded-0"
|
||||
onClick={() => {
|
||||
if (state.loadedItem?.type === "central") {
|
||||
dispatch(setTrackIntro(state.loadedItem, state.timeCurrent, id));
|
||||
dispatch(
|
||||
setTrackIntro(
|
||||
state.loadedItem,
|
||||
showDeleteMenu ? 0 : state.timeCurrent,
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -125,7 +148,13 @@ function TimingButtons({ id }: { id: number }) {
|
|||
className="cue btn btn-sm btn-outline-secondary rounded-0"
|
||||
onClick={() => {
|
||||
if (state.loadedItem && "timeslotitemid" in state.loadedItem) {
|
||||
dispatch(setTrackCue(state.loadedItem, state.timeCurrent, id));
|
||||
dispatch(
|
||||
setTrackCue(
|
||||
state.loadedItem,
|
||||
showDeleteMenu ? 0 : state.timeCurrent,
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -135,12 +164,29 @@ function TimingButtons({ id }: { id: number }) {
|
|||
className="outro btn btn-sm btn-outline-secondary rounded-0"
|
||||
onClick={() => {
|
||||
if (state.loadedItem?.type === "central") {
|
||||
dispatch(setTrackOutro(state.loadedItem, state.timeCurrent, id));
|
||||
dispatch(
|
||||
setTrackOutro(
|
||||
state.loadedItem,
|
||||
showDeleteMenu ? 0 : state.timeCurrent,
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Outro
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"delete btn btn-sm btn-outline-secondary rounded-0" +
|
||||
(showDeleteMenu ? " active" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
setShowDeleteMenu(!showDeleteMenu);
|
||||
}}
|
||||
>
|
||||
<FaTrash />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React from "react";
|
||||
import { FaTimes } from "react-icons/fa";
|
||||
import Modal from "react-modal";
|
||||
import { Button } from "reactstrap";
|
||||
|
||||
interface WelcomeModalProps {
|
||||
isOpen: boolean;
|
||||
|
@ -9,7 +11,15 @@ interface WelcomeModalProps {
|
|||
export function WelcomeModal(props: WelcomeModalProps) {
|
||||
return (
|
||||
<Modal isOpen={props.isOpen} onRequestClose={props.close}>
|
||||
<h1>Welcome to WebStudio!</h1>
|
||||
<h1 className="d-inline">Welcome to WebStudio!</h1>
|
||||
<Button
|
||||
onClick={props.close}
|
||||
className="float-right pt-1"
|
||||
color="primary"
|
||||
>
|
||||
<FaTimes />
|
||||
</Button>
|
||||
<hr className="mt-1 mb-3" />
|
||||
<p>
|
||||
As you are not WebStudio Trained, you will be able to access all
|
||||
WebStudio features except going live. If you want to go live, ask in
|
||||
|
@ -20,11 +30,6 @@ export function WelcomeModal(props: WelcomeModalProps) {
|
|||
Computing in #remote-broadcasting.
|
||||
</p>
|
||||
<p>Thank you, and have fun!</p>
|
||||
<div>
|
||||
<button className="btn btn-primary" onClick={props.close}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ import React, { useState, useReducer, useEffect } from "react";
|
|||
import { ContextMenu, MenuItem } from "react-contextmenu";
|
||||
import { useBeforeunload } from "react-beforeunload";
|
||||
import {
|
||||
FaAlignJustify,
|
||||
FaBookOpen,
|
||||
FaFileImport,
|
||||
FaBars,
|
||||
FaMicrophone,
|
||||
FaTrash,
|
||||
FaUpload,
|
||||
|
@ -134,7 +134,7 @@ function LibraryColumn() {
|
|||
</div>
|
||||
<div className="px-2">
|
||||
<select
|
||||
className="form-control"
|
||||
className="form-control form-control-sm"
|
||||
style={{ flex: "none" }}
|
||||
value={sauce}
|
||||
onChange={(e) => setSauce(e.target.value)}
|
||||
|
@ -198,63 +198,76 @@ function MicControl() {
|
|||
|
||||
return (
|
||||
<div className="mic-control">
|
||||
<h2>
|
||||
<FaMicrophone className="mx-1" size={28} />
|
||||
Microphone
|
||||
</h2>
|
||||
{!state.open && (
|
||||
<p className="alert-info p-2 mb-0">
|
||||
The microphone has not been setup. Go to options.
|
||||
</p>
|
||||
)}
|
||||
{proMode && (
|
||||
<span
|
||||
id="micLiveTimer"
|
||||
className={state.open && state.volume > 0 ? "live" : ""}
|
||||
>
|
||||
<span className="text">Mic Live: </span>
|
||||
{state.open && state.volume > 0 ? (
|
||||
<Stopwatch
|
||||
seconds={0}
|
||||
minutes={0}
|
||||
hours={0}
|
||||
render={({ formatted }) => {
|
||||
return <span>{formatted}</span>;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
"00:00:00"
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<div id="micMeter">
|
||||
<VUMeter
|
||||
width={250}
|
||||
height={40}
|
||||
source="mic-final"
|
||||
range={[-40, 3]}
|
||||
greenRange={[-10, -5]}
|
||||
stereo={proMode}
|
||||
<div data-toggle="collapse" data-target="#mic-control-menu">
|
||||
<h2>
|
||||
<FaMicrophone className="mx-1" size={28} />
|
||||
Microphone
|
||||
</h2>
|
||||
<FaBars
|
||||
className="toggle mx-0 mt-2 text-muted"
|
||||
title="Toggle Microphone Menu"
|
||||
size={20}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mixer-buttons ${!state.open && "disabled"}`}>
|
||||
<div
|
||||
className="mixer-buttons-backdrop"
|
||||
style={{
|
||||
width: state.volume * 100 + "%",
|
||||
}}
|
||||
></div>
|
||||
<button onClick={() => dispatch(MixerState.setMicVolume("off"))}>
|
||||
Off
|
||||
</button>
|
||||
<button onClick={() => dispatch(MixerState.setMicVolume("full"))}>
|
||||
Full
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={() => dispatch(OptionsMenuState.open())}>
|
||||
Options
|
||||
</button>
|
||||
<div id="mic-control-menu" className="collapse show">
|
||||
{!state.open && (
|
||||
<p className="alert-info p-2 mb-0">
|
||||
The microphone has not been setup. Go to{" "}
|
||||
<button
|
||||
className="btn btn-link m-0 mb-1 p-0"
|
||||
onClick={() => dispatch(OptionsMenuState.open())}
|
||||
>
|
||||
{" "}
|
||||
options
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
{state.open && proMode && (
|
||||
<span id="micLiveTimer" className={state.volume > 0 ? "live" : ""}>
|
||||
<span className="text">Mic Live: </span>
|
||||
{state.volume > 0 ? (
|
||||
<Stopwatch
|
||||
seconds={0}
|
||||
minutes={0}
|
||||
hours={0}
|
||||
render={({ formatted }) => {
|
||||
return <span>{formatted}</span>;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
"00:00:00"
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{state.open && (
|
||||
<>
|
||||
<div id="micMeter">
|
||||
<VUMeter
|
||||
width={250}
|
||||
height={40}
|
||||
source="mic-final"
|
||||
range={[-40, 3]}
|
||||
greenRange={[-10, -5]}
|
||||
stereo={proMode}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mixer-buttons ${!state.open && "disabled"}`}>
|
||||
<div
|
||||
className="mixer-buttons-backdrop"
|
||||
style={{
|
||||
width: state.volume * 100 + "%",
|
||||
}}
|
||||
></div>
|
||||
<button onClick={() => dispatch(MixerState.setMicVolume("off"))}>
|
||||
Off
|
||||
</button>
|
||||
<button onClick={() => dispatch(MixerState.setMicVolume("full"))}>
|
||||
Full
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -358,6 +371,23 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
|
|||
dispatch(removeItem(timeslotId, data.id));
|
||||
}
|
||||
|
||||
// Add support for reloading the show plan from the iFrames.
|
||||
// There is a similar listener in showplanner/ImporterModal.tsx to handle closing the iframe.
|
||||
useEffect(() => {
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
if (!event.origin.includes("ury.org.uk")) {
|
||||
return;
|
||||
}
|
||||
if (event.data === "reload_showplan") {
|
||||
session.currentTimeslot !== null &&
|
||||
dispatch(getShowplan(session.currentTimeslot.timeslot_id));
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
if (showplan === null) {
|
||||
return (
|
||||
<LoadingDialogue
|
||||
|
@ -383,7 +413,7 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
|
|||
className="btn btn-outline-dark btn-sm mb-0"
|
||||
onClick={() => toggleSidebar()}
|
||||
>
|
||||
<FaAlignJustify style={{ verticalAlign: "text-bottom" }} />
|
||||
<FaBars style={{ verticalAlign: "text-bottom" }} />
|
||||
Toggle Sidebar
|
||||
</span>
|
||||
<div id="sidebar">
|
||||
|
@ -394,7 +424,6 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
|
|||
</DragDropContext>
|
||||
</div>
|
||||
<ContextMenu id={TS_ITEM_MENU_ID}>
|
||||
<MenuItem onClick={onCtxRemoveClick}>Remove</MenuItem>
|
||||
<MenuItem onClick={onCtxRemoveClick}>
|
||||
<FaTrash /> Remove
|
||||
</MenuItem>
|
||||
|
|
|
@ -57,14 +57,14 @@ export function CentralMusicLibrary() {
|
|||
<div className="library library-central">
|
||||
<span className="px-2">
|
||||
<input
|
||||
className="form-control"
|
||||
className="form-control form-control-sm"
|
||||
type="text"
|
||||
placeholder="Filter by track..."
|
||||
value={track}
|
||||
onChange={(e) => setTrack(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="form-control mt-2"
|
||||
className="form-control form-control-sm mt-2"
|
||||
type="text"
|
||||
placeholder="Filter by artist..."
|
||||
value={artist}
|
||||
|
@ -124,14 +124,14 @@ export function ManagedPlaylistLibrary({ libraryId }: { libraryId: string }) {
|
|||
<div className="library library-central">
|
||||
<span className="px-2">
|
||||
<input
|
||||
className="form-control"
|
||||
className="form-control form-control-sm"
|
||||
type="text"
|
||||
placeholder="Filter by track..."
|
||||
value={track}
|
||||
onChange={(e) => setTrack(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="form-control mt-2"
|
||||
className="form-control form-control-sm mt-2"
|
||||
type="text"
|
||||
placeholder="Filter by artist..."
|
||||
value={artist}
|
||||
|
@ -211,7 +211,7 @@ export function AuxLibrary({ libraryId }: { libraryId: string }) {
|
|||
<div className="library library-aux">
|
||||
<span className="px-2">
|
||||
<input
|
||||
className="form-control"
|
||||
className="form-control form-control-sm"
|
||||
type="text"
|
||||
placeholder="Filter..."
|
||||
value={searchQuery}
|
||||
|
|
Loading…
Reference in a new issue