Merge pull request #154 from UniversityRadioYork/mstratford-save-tidies

A couple extra bits for UI Reshuffle and Saving.
This commit is contained in:
Matthew Stratford 2020-10-06 13:33:32 +01:00 committed by GitHub
commit 0615569eb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 239 additions and 94 deletions

View file

@ -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 {

View file

@ -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;

View file

@ -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;
}

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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" }} />
&nbsp; 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>

View file

@ -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}