diff --git a/.env b/.env index 93620cf..25e0b91 100644 --- a/.env +++ b/.env @@ -4,5 +4,4 @@ REACT_APP_MYRADIO_NONAPI_BASE=https://ury.org.uk/myradio-staging REACT_APP_MYRADIO_BASE=https://ury.org.uk/api-staging/v2 REACT_APP_BROADCAST_API_BASE=https://ury.org.uk/webstudio/api/v1 REACT_APP_WS_URL=wss://ury.org.uk/webstudio/api/stream -REACT_APP_BAPSICLE_INTERFACE=true REACT_APP_SENTRY_PUBLIC_DSN=https://7bdc2a7a1eb24ae080eb1d3af75e6307@o578586.ingest.sentry.io/5734903 diff --git a/public/index.html b/public/index.html index 34d7a36..01c3316 100644 --- a/public/index.html +++ b/public/index.html @@ -38,6 +38,8 @@ To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. + + TODO: Ideally we shouldn't rely on CDN's during runtime. --> diff --git a/src/App.scss b/src/App.scss index f78b9ff..137be5d 100644 --- a/src/App.scss +++ b/src/App.scss @@ -17,7 +17,7 @@ $number-of-channels: 3; .logo { height: 50px; } - .logo-big { + .logo-big-bapsicle { height: 30vh; } } diff --git a/src/api.ts b/src/api.ts index b4d6b2c..98c5a1d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -205,14 +205,13 @@ export function searchForTracks( artist, title, }); - } else { - return myradioApiRequest("/track/search", "GET", { - artist, - title, - limit: 100, - digitised: true, - }); } + return myradioApiRequest("/track/search", "GET", { + artist, + title, + limit: 100, + digitised: true, + }); } export function getTimeslots(): Promise> { diff --git a/src/bapsiclesession/index.tsx b/src/bapsiclesession/index.tsx index 6f467ba..bbe1e83 100644 --- a/src/bapsiclesession/index.tsx +++ b/src/bapsiclesession/index.tsx @@ -12,7 +12,7 @@ export function ConnectionDialogue({ error }: { error: String | null }) { return (
- BAPS Logo + BAPS Logo
diff --git a/src/index.tsx b/src/index.tsx index 24e910e..f53915e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,6 @@ import "./index.css"; import App from "./App"; import * as serviceWorker from "./serviceWorkerLoader"; - import * as Sentry from "@sentry/react"; import { Integrations } from "@sentry/tracing"; diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index 97cbe4f..cf85ee3 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -289,6 +289,8 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { instance.emit("pause"); }); wavesurfer.on("seek", () => { + // This is used to prevent infinite loops when bapsicle tells wavesurfer to change position, + // since otherwise it would send an change update back to the bapsicle server again (as if the user seeked intentionally). if (instance.ignore_next_seek) { instance.ignore_next_seek = false; } else { diff --git a/src/mixer/state.ts b/src/mixer/state.ts index 8118648..05047e7 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -379,6 +379,7 @@ export const setLoadedItemOutro = ( }; export const seek = (player: number, time_s: number): AppThunk => async () => { + // Used only by Bapsicle to update the wavesurfer seek position. const playerInstance = await audioEngine.getPlayer(player); if (playerInstance) { @@ -426,6 +427,7 @@ export const load = ( const currentItem = getState().mixer.players[player].loadedItem; if (currentItem !== null && itemId(currentItem) === itemId(item)) { if (process.env.REACT_APP_BAPSICLE_INTERFACE) { + // In BAPS, another client could have updated the audio markers etc, so we have to still check and update them. // The cue/intro/outro point(s) have changed. if ( "cue" in currentItem && @@ -567,6 +569,7 @@ export const load = ( dispatch(mixerState.actions.itemLoadComplete({ player })); if (!process.env.REACT_APP_BAPSICLE_INTERFACE) { + // BAPS server will give these values already on load based on it's own calculation, not wavesurfer's. dispatch( mixerState.actions.setTimeLength({ player, @@ -585,6 +588,7 @@ export const load = ( const state = getState().mixer.players[player]; if (!process.env.REACT_APP_BAPSICLE_INTERFACE) { + // Client doesn't do any audio playing in BAPS. if (state.playOnLoad) { playerInstance.play(); } @@ -603,16 +607,18 @@ export const load = ( }); if (process.env.REACT_APP_BAPSICLE_INTERFACE) { + // If user manually seeks on the waveform, just direct that to the webserver. playerInstance.on("timeChangeSeek", (time) => { - if ( - Math.abs(time - getState().mixer.players[player].timeCurrent) > 0.5 - ) { - sendBAPSicleChannel({ - channel: player, - command: "SEEK", - time: time, - }); - } + // Limit + //if ( + // Math.abs(time - getState().mixer.players[player].timeCurrent) > 0.5 + //) { + sendBAPSicleChannel({ + channel: player, + command: "SEEK", + time: time, + }); + //} }); } else { playerInstance.on("play", () => { @@ -1261,10 +1267,40 @@ export const mixerKeyboardShortcutsMiddleware: Middleware< store.dispatch(handleKeyboardShortcut(store, 2, "STOP")); }); - Keys("x", () => { - const state = store.getState().mixer.mic; - store.dispatch(setMicVolume(state.volume === 1 ? "off" : "full")); - }); + if (!process.env.REACT_APP_BAPSICLE_INTERFACE) { + Keys("a", () => { + store.dispatch(setVolume(0, "off")); + }); + Keys("s", () => { + store.dispatch(setVolume(0, "bed")); + }); + Keys("d", () => { + store.dispatch(setVolume(0, "full")); + }); + Keys("f", () => { + store.dispatch(setVolume(1, "off")); + }); + Keys("g", () => { + store.dispatch(setVolume(1, "bed")); + }); + Keys("h", () => { + store.dispatch(setVolume(1, "full")); + }); + Keys("j", () => { + store.dispatch(setVolume(2, "off")); + }); + Keys("k", () => { + store.dispatch(setVolume(2, "bed")); + }); + Keys("l", () => { + store.dispatch(setVolume(2, "full")); + }); + + Keys("x", () => { + const state = store.getState().mixer.mic; + store.dispatch(setMicVolume(state.volume === 1 ? "off" : "full")); + }); + } return (next) => (action) => next(action); }; diff --git a/src/showplanner/Item.tsx b/src/showplanner/Item.tsx index 3fc2a1d..f68f356 100644 --- a/src/showplanner/Item.tsx +++ b/src/showplanner/Item.tsx @@ -50,8 +50,7 @@ export const Item = memo(function Item({ function triggerClick() { if (column > -1) { - console.log("Clicking to load:", x); - + // TODO: move this into mixer state if we can. if (process.env.REACT_APP_BAPSICLE_INTERFACE) { sendBAPSicleChannel({ channel: column, diff --git a/src/showplanner/Player.tsx b/src/showplanner/Player.tsx index e0a8206..1419b4a 100644 --- a/src/showplanner/Player.tsx +++ b/src/showplanner/Player.tsx @@ -81,6 +81,7 @@ const setTrackIntro = ( secs: number, player: number ): AppThunk => async (dispatch, getState) => { + // TODO Move into MixerState if (process.env.REACT_APP_BAPSICLE_INTERFACE) { let marker = { name: "Intro", diff --git a/src/showplanner/index.tsx b/src/showplanner/index.tsx index 7911b7e..d22439a 100644 --- a/src/showplanner/index.tsx +++ b/src/showplanner/index.tsx @@ -50,8 +50,6 @@ import Modal from "react-modal"; import { Sidebar } from "./sidebar"; import { PLAYER_ID_PREVIEW } from "../mixer/audio"; -import { sendBAPSicleChannel } from "../bapsicle"; - function Channel({ id, data }: { id: number; data: PlanItem[] }) { return (
@@ -112,6 +110,7 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) { useEffect(() => { if (!process.env.REACT_APP_BAPSICLE_INTERFACE) { + // In BAPS, we'll load in the show plan from a async message from server. dispatch(getShowplan(timeslotId)); } }, [dispatch, timeslotId]); @@ -148,15 +147,7 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) { cue: 0, ...data, }; - if (process.env.REACT_APP_BAPSICLE_INTERFACE) { - sendBAPSicleChannel({ - channel: newItem.channel, - command: "ADD", - newItem: newItem, - }); - } else { - dispatch(addItem(timeslotId, newItem)); - } + dispatch(addItem(timeslotId, newItem)); increment(null); } else if (result.draggableId[0] === "A") { // this is an aux resource @@ -171,15 +162,7 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) { cue: 0, ...data, } as any; - if (process.env.REACT_APP_BAPSICLE_INTERFACE) { - sendBAPSicleChannel({ - channel: newItem.channel, - command: "ADD", - newItem: newItem, - }); - } else { - dispatch(addItem(timeslotId, newItem)); - } + dispatch(addItem(timeslotId, newItem)); increment(null); } else { // this is a normal move (ghosts aren't draggable) @@ -204,28 +187,6 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) { } } - async function onCtxRemoveClick( - e: any, - data: { id: string; column: number; index: number } - ) { - if (process.env.REACT_APP_BAPSICLE_INTERFACE) { - sendBAPSicleChannel({ - channel: data.column, - command: "REMOVE", - weight: data.index, - }); - } else { - dispatch(removeItem(timeslotId, data.id)); - } - } - - async function onCtxUnPlayedClick( - e: any, - data: { id: string; column: number; index: number } - ) { - dispatch(setItemPlayed(data.id.toString(), false, data.column)); - } - // 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(() => { @@ -275,9 +236,9 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) { Remove - dispatch(setItemPlayed((args.props as any).id, false)) - } + onClick={(args) => { + dispatch(setItemPlayed((args.props as any).id, false)); + }} > Mark Unplayed diff --git a/src/showplanner/state.ts b/src/showplanner/state.ts index fb8ba7b..44b6128 100644 --- a/src/showplanner/state.ts +++ b/src/showplanner/state.ts @@ -99,6 +99,8 @@ const showplan = createSlice({ state, action: PayloadAction<{ channel: Number; planItems: PlanItem[] }> ) { + // This is used for BAPSicle only to read in individual channels of show plan into the show state from the server. + // TODO: Does this need to be this complicated? var newItems = state.plan?.filter( (item) => item.channel !== action.payload.channel ); @@ -263,11 +265,7 @@ export const setItemPlayed = ( played: boolean ): AppThunk => async (dispatch, getState) => { // The server handles marking things played - // TODO: Add support in BAPSicle for marking things played if (process.env.REACT_APP_BAPSICLE_INTERFACE) { - if (played) { - return; - } var weight = -1; var player = null; @@ -282,13 +280,15 @@ export const setItemPlayed = ( if (player) { sendBAPSicleChannel({ channel: player, - command: "RESETPLAYED", + command: "SETPLAYED", weight: weight, + played: played, }); } else { sendBAPSicleChannel({ - command: "RESETPLAYED", + command: "SETPLAYED", weight: weight, + played: played, }); } return; @@ -448,6 +448,15 @@ export const addItem = ( timeslotId: number, newItemData: TimeslotItem ): AppThunk => async (dispatch, getState) => { + if (process.env.REACT_APP_BAPSICLE_INTERFACE) { + sendBAPSicleChannel({ + channel: newItemData.channel, + command: "ADD", + newItem: newItemData, + }); + return; + } + dispatch(showplan.actions.setPlanSaving(true)); console.log("New Weight: " + newItemData.weight); const plan = cloneDeep(getState().showplan.plan!); @@ -527,18 +536,19 @@ export const addItem = ( }, } ); - - const lastResult = result[result.length - 1]; // this is the add op - const newItemId = lastResult.timeslotitemid!; - - newItemData.timeslotitemid = newItemId; - dispatch( - showplan.actions.replaceGhost({ - ghostId: "G" + ghostId, - newItemData, - }) - ); + dispatch(showplan.actions.planSaveError("Failed to update show plan.")); + return; } + const lastResult = result[result.length - 1]; // this is the add op + const newItemId = lastResult.timeslotitemid!; + + newItemData.timeslotitemid = newItemId; + dispatch( + showplan.actions.replaceGhost({ + ghostId: "G" + ghostId, + newItemData, + }) + ); } else { // Just add it straight to the show plan without updating the server. dispatch(showplan.actions.addItem(newItemData)); @@ -551,10 +561,24 @@ export const removeItem = ( timeslotId: number, itemid: string ): AppThunk => async (dispatch, getState) => { - dispatch(showplan.actions.setPlanSaving(true)); // This is a simplified version of the second case of moveItem const plan = cloneDeep(getState().showplan.plan!); const item = plan.find((x) => itemId(x) === itemid)!; + + if (process.env.REACT_APP_BAPSICLE_INTERFACE) { + // Server handles deletion, short circuit. + if (item) { + sendBAPSicleChannel({ + channel: item.channel, + command: "REMOVE", + weight: item.weight, + }); + } + return; + } + + dispatch(showplan.actions.setPlanSaving(true)); + const planColumn = plan .filter((x) => x.channel === item.channel) .sort((a, b) => a.weight - b.weight); @@ -578,27 +602,25 @@ export const removeItem = ( weight: movingItem.weight - 1, }); movingItem.weight -= 1; + } - if (!process.env.REACT_APP_BAPSICLE_INTERFACE) { - if (getState().settings.saveShowPlanChanges) { - const result = await api.updateShowplan(timeslotId, ops); - if (!result.every((x) => x.status)) { - Sentry.captureException( - new Error("Showplan update failure [removeItem]"), - { - contexts: { - updateShowplan: { - ops, - result, - }, + if (!process.env.REACT_APP_BAPSICLE_INTERFACE) { + if (getState().settings.saveShowPlanChanges) { + const result = await api.updateShowplan(timeslotId, ops); + if (!result.every((x) => x.status)) { + Sentry.captureException( + new Error("Showplan update failure [removeItem]"), + { + contexts: { + updateShowplan: { + ops, + result, }, - } - ); - dispatch( - showplan.actions.planSaveError("Failed to update show plan.") - ); - return; - } + }, + } + ); + dispatch(showplan.actions.planSaveError("Failed to update show plan.")); + return; } } } diff --git a/src/store.ts b/src/store.ts index f88ddb4..a6f233f 100644 --- a/src/store.ts +++ b/src/store.ts @@ -20,7 +20,6 @@ import { bapsicleMiddleware } from "./bapsicle"; import * as Sentry from "@sentry/react"; - // const ACTION_HISTORY_MAX_SIZE = 20; // const actionHistory: Array = [];