Load jingles and beds from the sidebar (fixes #16)
This commit is contained in:
parent
0b93cbca37
commit
5737ea276b
6 changed files with 380 additions and 183 deletions
57
src/api.ts
57
src/api.ts
|
@ -1,36 +1,29 @@
|
|||
import qs from "qs";
|
||||
import { convertModelToFormData, urlEncode } from "./lib/utils";
|
||||
|
||||
export const MYRADIO_NON_API_BASE = process.env.REACT_APP_MYRADIO_NONAPI_BASE || "https://ury.org.uk/myradio-staging";
|
||||
export const MYRADIO_NON_API_BASE =
|
||||
process.env.REACT_APP_MYRADIO_NONAPI_BASE ||
|
||||
"https://ury.org.uk/myradio-staging";
|
||||
export const MYRADIO_BASE_URL =
|
||||
process.env.REACT_APP_MYRADIO_BASE || "https://ury.org.uk/api-staging/v2";
|
||||
const MYRADIO_API_KEY = process.env.REACT_APP_MYRADIO_KEY!;
|
||||
|
||||
class ApiException extends Error {}
|
||||
|
||||
export async function myradioApiRequest(
|
||||
endpoint: string,
|
||||
export async function myradioRequest(
|
||||
url: string,
|
||||
method: "GET" | "POST" | "PUT",
|
||||
params: any
|
||||
): Promise<any> {
|
||||
): Promise<Response> {
|
||||
let req = null;
|
||||
if (method === "GET") {
|
||||
req = fetch(
|
||||
MYRADIO_BASE_URL +
|
||||
endpoint +
|
||||
qs.stringify(
|
||||
{
|
||||
...params,
|
||||
api_key: MYRADIO_API_KEY
|
||||
},
|
||||
{ addQueryPrefix: true }
|
||||
),
|
||||
{ credentials: "include" }
|
||||
);
|
||||
req = fetch(url + qs.stringify(params, { addQueryPrefix: true }), {
|
||||
credentials: "include"
|
||||
});
|
||||
} else {
|
||||
const body = JSON.stringify(params);
|
||||
console.log(body);
|
||||
req = fetch(MYRADIO_BASE_URL + endpoint + "?api_key=" + MYRADIO_API_KEY, {
|
||||
req = fetch(url, {
|
||||
method,
|
||||
body,
|
||||
headers: {
|
||||
|
@ -39,7 +32,16 @@ export async function myradioApiRequest(
|
|||
credentials: "include"
|
||||
});
|
||||
}
|
||||
const json = await (await req).json();
|
||||
return await req;
|
||||
}
|
||||
|
||||
export async function myradioApiRequest(
|
||||
endpoint: string,
|
||||
method: "GET" | "POST" | "PUT",
|
||||
params: any
|
||||
): Promise<any> {
|
||||
const res = await myradioRequest(MYRADIO_BASE_URL + endpoint, method, params);
|
||||
const json = await res.json();
|
||||
if (json.status === "OK") {
|
||||
return json.payload;
|
||||
} else {
|
||||
|
@ -77,16 +79,23 @@ interface TimeslotItemCentral {
|
|||
album: Album;
|
||||
}
|
||||
|
||||
interface TimeslotItemAux {
|
||||
export interface AuxItem {
|
||||
type: "aux";
|
||||
artist: null;
|
||||
intro: null;
|
||||
summary: string;
|
||||
title: string;
|
||||
managedid: number;
|
||||
length: string;
|
||||
trackid: number;
|
||||
expirydate: boolean | string;
|
||||
expired: boolean;
|
||||
recordid: string;
|
||||
auxid: string;
|
||||
}
|
||||
|
||||
interface TimeslotItemAux extends AuxItem {
|
||||
type: "aux";
|
||||
}
|
||||
|
||||
export type TimeslotItem = TimeslotItemBase &
|
||||
(TimeslotItemCentral | TimeslotItemAux);
|
||||
|
||||
|
@ -160,6 +169,12 @@ export function searchForTracks(
|
|||
});
|
||||
}
|
||||
|
||||
export function loadAuxLibrary(libraryId: string): Promise<AuxItem[]> {
|
||||
return myradioRequest(MYRADIO_NON_API_BASE + "/NIPSWeb/load_aux_lib", "GET", {
|
||||
libraryid: libraryId
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export type UpdateOp =
|
||||
| {
|
||||
op: "MoveItem";
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
import Between from "between.js";
|
||||
import { PlanItem } from "../showplanner/state";
|
||||
import Keys from "keymaster";
|
||||
import { Track, MYRADIO_NON_API_BASE } from "../api";
|
||||
import { Track, MYRADIO_NON_API_BASE, AuxItem } from "../api";
|
||||
import { AppThunk } from "../store";
|
||||
import { RootState } from "../rootReducer";
|
||||
import WaveSurfer from "wavesurfer.js";
|
||||
|
@ -40,7 +40,7 @@ type MicVolumePresetEnum = "off" | "full";
|
|||
type MicErrorEnum = "NO_PERMISSION" | "NOT_SECURE_CONTEXT" | "UNKNOWN";
|
||||
|
||||
interface PlayerState {
|
||||
loadedItem: PlanItem | Track | null;
|
||||
loadedItem: PlanItem | Track | AuxItem | null;
|
||||
loading: boolean;
|
||||
state: PlayerStateEnum;
|
||||
volume: number;
|
||||
|
@ -123,7 +123,7 @@ const mixerState = createSlice({
|
|||
reducers: {
|
||||
loadItem(
|
||||
state,
|
||||
action: PayloadAction<{ player: number; item: PlanItem | Track }>
|
||||
action: PayloadAction<{ player: number; item: PlanItem | Track | AuxItem }>
|
||||
) {
|
||||
state.players[action.payload.player].loadedItem =
|
||||
action.payload.item;
|
||||
|
@ -241,7 +241,7 @@ export default mixerState.reducer;
|
|||
|
||||
export const load = (
|
||||
player: number,
|
||||
item: PlanItem | Track
|
||||
item: PlanItem | Track | AuxItem
|
||||
): AppThunk => async (dispatch, getState) => {
|
||||
if (typeof wavesurfers[player] !== "undefined") {
|
||||
if (wavesurfers[player].isPlaying()) {
|
||||
|
@ -261,7 +261,7 @@ export const load = (
|
|||
item.album.recordid +
|
||||
"&trackid=" +
|
||||
item.trackid;
|
||||
} else if ("type" in item && item.type == "aux") {
|
||||
} else if ("managedid" in item) {
|
||||
url =
|
||||
MYRADIO_NON_API_BASE +
|
||||
"/NIPSWeb/managed_play?managedid=" +
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { memo } from "react";
|
||||
import { PlanItem, itemId } from "./state";
|
||||
import { Track } from "../api";
|
||||
import { Track, AuxItem } from "../api";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "../rootReducer";
|
||||
|
||||
|
@ -15,7 +15,7 @@ export const Item = memo(function Item({
|
|||
index,
|
||||
column
|
||||
}: {
|
||||
item: PlanItem | Track;
|
||||
item: PlanItem | Track | AuxItem;
|
||||
index: number;
|
||||
column: number;
|
||||
}) {
|
||||
|
@ -60,7 +60,7 @@ export const Item = memo(function Item({
|
|||
{"artist" in x && " - " + x.artist}
|
||||
<small className=
|
||||
{"border rounded border-danger text-danger p-1 m-1" + (
|
||||
x.clean === false ? "" : " d-none")}>
|
||||
"clean" in x && x.clean === false ? "" : " d-none")}>
|
||||
Explicit
|
||||
</small>
|
||||
<code>
|
||||
|
|
|
@ -37,9 +37,7 @@ import * as MixerState from "../mixer/state";
|
|||
|
||||
import appLogo from "../assets/images/webstudio.svg";
|
||||
import { Item, TS_ITEM_MENU_ID } from "./Item";
|
||||
import { CentralMusicLibrary, CML_CACHE } from "./libraries";
|
||||
|
||||
|
||||
import { CentralMusicLibrary, CML_CACHE, AuxLibrary, AUX_CACHE } from "./libraries";
|
||||
|
||||
const USE_REAL_GAIN_VALUE = false;
|
||||
|
||||
|
@ -50,85 +48,136 @@ function Player({ id }: { id: number }) {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<div className={(playerState.loadedItem !== null && playerState.loading == false) ? "player loaded" : "player"}>
|
||||
|
||||
|
||||
<div
|
||||
className={
|
||||
playerState.loadedItem !== null && playerState.loading == false
|
||||
? "player loaded"
|
||||
: "player"
|
||||
}
|
||||
>
|
||||
<div className="card text-center">
|
||||
<div className="row m-0 p-1 card-header channelButtons">
|
||||
<button
|
||||
className={(playerState.autoAdvance ? "btn-primary" : "btn-outline-secondary") + " btn btn-sm col-4 sp-play-on-load"}
|
||||
className={
|
||||
(playerState.autoAdvance
|
||||
? "btn-primary"
|
||||
: "btn-outline-secondary") + " btn btn-sm col-4 sp-play-on-load"
|
||||
}
|
||||
onClick={() => dispatch(MixerState.toggleAutoAdvance(id))}
|
||||
>
|
||||
<i className="fa fa-level-down-alt"></i>
|
||||
Auto Advance
|
||||
<i className="fa fa-level-down-alt"></i> Auto Advance
|
||||
</button>
|
||||
<button
|
||||
className={(playerState.playOnLoad ? "btn-primary": "btn-outline-secondary") + " btn btn-sm col-4 sp-play-on-load"}
|
||||
className={
|
||||
(playerState.playOnLoad
|
||||
? "btn-primary"
|
||||
: "btn-outline-secondary") + " btn btn-sm col-4 sp-play-on-load"
|
||||
}
|
||||
onClick={() => dispatch(MixerState.togglePlayOnLoad(id))}
|
||||
>
|
||||
<i className="far fa-play-circle"></i>
|
||||
Play on Load
|
||||
<i className="far fa-play-circle"></i> Play on Load
|
||||
</button>
|
||||
<button
|
||||
className={(playerState.repeat != "none" ? "btn-primary" : "btn-outline-secondary") + " btn btn-sm col-4 sp-play-on-load"}
|
||||
className={
|
||||
(playerState.repeat != "none"
|
||||
? "btn-primary"
|
||||
: "btn-outline-secondary") + " btn btn-sm col-4 sp-play-on-load"
|
||||
}
|
||||
onClick={() => dispatch(MixerState.toggleRepeat(id))}
|
||||
>
|
||||
<i className="fa fa-redo"></i>
|
||||
Repeat {playerState.repeat}
|
||||
<i className="fa fa-redo"></i> Repeat {playerState.repeat}
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-body p-0">
|
||||
<span className="card-title">
|
||||
<strong>
|
||||
{playerState.loadedItem !== null
|
||||
&& playerState.loading === false
|
||||
{playerState.loadedItem !== null && !playerState.loading
|
||||
? playerState.loadedItem.title
|
||||
: (playerState.loading ? `LOADING` : "No Media Selected")}
|
||||
: playerState.loading
|
||||
? `LOADING`
|
||||
: "No Media Selected"}
|
||||
</strong>
|
||||
<small className=
|
||||
{"border rounded border-danger text-danger p-1 m-1" + (
|
||||
playerState.loadedItem !== null
|
||||
&& playerState.loading === false
|
||||
&& playerState.loadedItem.clean === false ? "" : " d-none")}>
|
||||
<small
|
||||
className={
|
||||
"border rounded border-danger text-danger p-1 m-1" +
|
||||
(playerState.loadedItem !== null &&
|
||||
!playerState.loading &&
|
||||
"clean" in playerState.loadedItem &&
|
||||
!playerState.loadedItem.clean
|
||||
? ""
|
||||
: " d-none")
|
||||
}
|
||||
>
|
||||
Explicit
|
||||
</small>
|
||||
</span><br />
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-muted">
|
||||
{playerState.loadedItem !== null
|
||||
&& playerState.loading === false
|
||||
? playerState.loadedItem.artist
|
||||
: ""}
|
||||
{playerState.loadedItem !== null && !playerState.loading
|
||||
? "artist" in playerState.loadedItem &&
|
||||
playerState.loadedItem.artist
|
||||
: ""}
|
||||
|
||||
</span>
|
||||
<div className="mediaButtons">
|
||||
<button
|
||||
onClick={() => dispatch(MixerState.play(id))}
|
||||
className={(playerState.state === "playing" ? ((playerState.timeRemaining <= 15) ? "sp-state-playing sp-ending-soon" : "sp-state-playing") : "")}
|
||||
className={
|
||||
playerState.state === "playing"
|
||||
? playerState.timeRemaining <= 15
|
||||
? "sp-state-playing sp-ending-soon"
|
||||
: "sp-state-playing"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<i className="fas fa-play"></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => dispatch(MixerState.pause(id))}
|
||||
className={playerState.state === "paused" ? "sp-state-paused" : ""}
|
||||
className={
|
||||
playerState.state === "paused" ? "sp-state-paused" : ""
|
||||
}
|
||||
>
|
||||
<i className="fas fa-pause"></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => dispatch(MixerState.stop(id))}
|
||||
className={playerState.state === "stopped" ? "sp-state-stopped" : ""}
|
||||
className={
|
||||
playerState.state === "stopped" ? "sp-state-stopped" : ""
|
||||
}
|
||||
>
|
||||
<i className="fas fa-stop"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-0 card-footer waveform" >
|
||||
|
||||
<span id={"current-" + id} className="m-0 current bypass-click">{secToHHMM(playerState.timeCurrent)}</span>
|
||||
<span id={"length-" + id} className="m-0 length bypass-click">{secToHHMM(playerState.timeLength)}</span>
|
||||
<span id={"remaining-" + id} className="m-0 remaining bypass-click">{secToHHMM(playerState.timeRemaining)}</span>
|
||||
<span className="m-0 intro bypass-click">{playerState.loadedItem !== null ? secToHHMM(playerState.loadedItem.intro ? playerState.loadedItem.intro : 0) : "00:00:00"} - in</span>
|
||||
<div className="p-0 card-footer waveform">
|
||||
<span id={"current-" + id} className="m-0 current bypass-click">
|
||||
{secToHHMM(playerState.timeCurrent)}
|
||||
</span>
|
||||
<span id={"length-" + id} className="m-0 length bypass-click">
|
||||
{secToHHMM(playerState.timeLength)}
|
||||
</span>
|
||||
<span id={"remaining-" + id} className="m-0 remaining bypass-click">
|
||||
{secToHHMM(playerState.timeRemaining)}
|
||||
</span>
|
||||
{playerState.loadedItem !== null &&
|
||||
"intro" in playerState.loadedItem && (
|
||||
<span className="m-0 intro bypass-click">
|
||||
{playerState.loadedItem !== null
|
||||
? secToHHMM(
|
||||
playerState.loadedItem.intro
|
||||
? playerState.loadedItem.intro
|
||||
: 0
|
||||
)
|
||||
: "00:00:00"}{" "}
|
||||
- in
|
||||
</span>
|
||||
)}
|
||||
<span className="m-0 outro bypass-click">out - 00:00:00</span>
|
||||
{(playerState.loadedItem !== null && playerState.timeLength === 0) && <span className="m-0 loading bypass-click">LOADING</span>}
|
||||
{playerState.loadedItem !== null && playerState.timeLength === 0 && (
|
||||
<span className="m-0 loading bypass-click">LOADING</span>
|
||||
)}
|
||||
<div className="m-0 graph" id={"waveform-" + id}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -191,6 +240,23 @@ function Column({ id, data }: { id: number; data: PlanItem[] }) {
|
|||
);
|
||||
}
|
||||
|
||||
// TODO: this shouldn't have to be hardcoded
|
||||
const AUX_LIBRARIES: {[key: string]: string} = {
|
||||
"aux-11": "Ambiences/Soundscapes",
|
||||
"aux-3": "Artist Drops",
|
||||
"aux-1": "Beds",
|
||||
"aux-7": "Daily News Bulletins",
|
||||
"aux-13": "Event Resources",
|
||||
"aux-2": "Jingles",
|
||||
"aux-4": "News",
|
||||
"aux-5": "Presenter Idents",
|
||||
"aux-6": "Promos",
|
||||
"aux-12": "Roses 2018",
|
||||
"aux-10": "Sound Effects",
|
||||
"aux-8": "Speech",
|
||||
"aux-9": "Teasers"
|
||||
};
|
||||
|
||||
function LibraryColumn() {
|
||||
const [sauce, setSauce] = useState("None");
|
||||
return (
|
||||
|
@ -205,11 +271,15 @@ function LibraryColumn() {
|
|||
Choose a library
|
||||
</option>
|
||||
<option value={"CentralMusicLibrary"}>Central Music Library</option>
|
||||
<option disabled>Resources</option>
|
||||
{Object.keys(AUX_LIBRARIES).map(libId => <option key={libId} value={libId}>{AUX_LIBRARIES[libId]}</option>)}
|
||||
</select>
|
||||
<div className="border-top my-3"></div>
|
||||
{sauce === "CentralMusicLibrary" && <CentralMusicLibrary />}
|
||||
|
||||
<span className={sauce === "None" ? "mt-5 text-center text-muted" : "d-none"}>
|
||||
{sauce.startsWith("aux-") && <AuxLibrary libraryId={sauce} />}
|
||||
<span
|
||||
className={sauce === "None" ? "mt-5 text-center text-muted" : "d-none"}
|
||||
>
|
||||
<i className="far fa-2x fa-caret-square-down"></i>
|
||||
<br />
|
||||
Select a library to search.
|
||||
|
@ -243,10 +313,7 @@ function MicControl() {
|
|||
<div
|
||||
className="sp-mixer-buttons-backdrop"
|
||||
style={{
|
||||
width:
|
||||
(USE_REAL_GAIN_VALUE ? state.gain : state.volume) *
|
||||
100 +
|
||||
"%"
|
||||
width: (USE_REAL_GAIN_VALUE ? state.gain : state.volume) * 100 + "%"
|
||||
}}
|
||||
></div>
|
||||
<button onClick={() => dispatch(MixerState.setMicVolume("off"))}>
|
||||
|
@ -260,20 +327,30 @@ function MicControl() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
function NavBar() {
|
||||
const userName = "Matthew Stratford";
|
||||
|
||||
return (
|
||||
|
||||
<header className="navbar navbar-ury navbar-expand-md p-0 bd-navbar">
|
||||
<nav className="container">
|
||||
<button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#collapsed" aria-controls="collapsed" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#collapsed"
|
||||
aria-controls="collapsed"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div className="navbar-nav">
|
||||
<a className="navbar-brand" href="/">
|
||||
<img src="//ury.org.uk/myradio/img/URY.svg" height="30" alt="University Radio York Logo" />
|
||||
<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" href="/">
|
||||
|
@ -283,25 +360,46 @@ function NavBar() {
|
|||
|
||||
<ul className="nav navbar-nav navbar-right">
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" target="_blank" href="https://ury.org.uk/myradio/MyRadio/timeslot/?next=/webstudio">
|
||||
<span className="fa fa-clock-o"></span>
|
||||
Timeslot Time
|
||||
<a
|
||||
className="nav-link"
|
||||
target="_blank"
|
||||
href="https://ury.org.uk/myradio/MyRadio/timeslot/?next=/webstudio"
|
||||
>
|
||||
<span className="fa fa-clock-o"></span> Timeslot Time
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item dropdown">
|
||||
<a className="nav-link dropdown-toggle" href="https://ury.org.uk/myradio/Profile/default/" id="dropdown07" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a
|
||||
className="nav-link dropdown-toggle"
|
||||
href="https://ury.org.uk/myradio/Profile/default/"
|
||||
id="dropdown07"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span className="fa fa-user-o"></span>
|
||||
{userName}
|
||||
</a>
|
||||
<div className="dropdown-menu" aria-labelledby="dropdown07">
|
||||
<a className="dropdown-item" target="_blank" href="https://ury.org.uk/myradio/Profile/default/">My Profile</a>
|
||||
<a className="dropdown-item" target="_blank" href="https://ury.org.uk/myradio/MyRadio/logout/">Logout</a>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
target="_blank"
|
||||
href="https://ury.org.uk/myradio/Profile/default/"
|
||||
>
|
||||
My Profile
|
||||
</a>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
target="_blank"
|
||||
href="https://ury.org.uk/myradio/MyRadio/logout/"
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -323,11 +421,11 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
|
|||
}, [timeslotId]);
|
||||
|
||||
function toggleSidebar() {
|
||||
var element = document.getElementById('sidebar');
|
||||
var element = document.getElementById("sidebar");
|
||||
if (element) {
|
||||
element.classList.toggle('active')
|
||||
element.classList.toggle("active");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function onDragEnd(result: DropResult, provider: ResponderProvided) {
|
||||
if (!result.destination) {
|
||||
|
@ -349,6 +447,19 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
|
|||
...data
|
||||
};
|
||||
dispatch(addItem(timeslotId, newItem));
|
||||
} else if (result.draggableId[0] === "A") {
|
||||
// this is an aux resource
|
||||
// TODO: this is ugly, should be in redux
|
||||
const data = AUX_CACHE[result.draggableId];
|
||||
const newItem: TimeslotItem = {
|
||||
type: "aux",
|
||||
timeslotitemid: "CHANGEME" + Math.random(),
|
||||
channel: parseInt(result.destination.droppableId, 10),
|
||||
weight: result.destination.index,
|
||||
clean: true,
|
||||
...data
|
||||
};
|
||||
dispatch(addItem(timeslotId, newItem));
|
||||
} else {
|
||||
// this is a normal move (ghosts aren't draggable)
|
||||
dispatch(
|
||||
|
@ -399,7 +510,12 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
|
|||
<Column id={1} data={showplan} />
|
||||
<Column id={2} data={showplan} />
|
||||
<div className="sp-main-col sidebar-toggle">
|
||||
<button id="sidebarCollapse" className="btn btn-sm ml-auto" type="button" onClick={() => toggleSidebar()}>
|
||||
<button
|
||||
id="sidebarCollapse"
|
||||
className="btn btn-sm ml-auto"
|
||||
type="button"
|
||||
onClick={() => toggleSidebar()}
|
||||
>
|
||||
<i className="fas fa-align-justify"></i> Show Sidebar
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import useDebounce from "../lib/useDebounce";
|
||||
import { Track, searchForTracks } from "../api";
|
||||
import { Track, searchForTracks, loadAuxLibrary, AuxItem } from "../api";
|
||||
import { itemId } from "./state";
|
||||
import { Droppable } from "react-beautiful-dnd";
|
||||
import { Item } from "./Item";
|
||||
|
@ -14,7 +14,11 @@ export function CentralMusicLibrary() {
|
|||
const debouncedArtist = useDebounce(artist, 1000);
|
||||
const [items, setItems] = useState<Track[]>([]);
|
||||
|
||||
type searchingStateEnum = "searching" | "not-searching" | "results" | "no-results";
|
||||
type searchingStateEnum =
|
||||
| "searching"
|
||||
| "not-searching"
|
||||
| "results"
|
||||
| "no-results";
|
||||
const [state, setState] = useState<searchingStateEnum>("not-searching");
|
||||
useEffect(() => {
|
||||
if (debouncedTrack === "" && debouncedArtist === "") {
|
||||
|
@ -55,33 +59,92 @@ export function CentralMusicLibrary() {
|
|||
value={artist}
|
||||
onChange={e => setArtist(e.target.value)}
|
||||
/>
|
||||
<span className={state !== "results" ? "mt-5 text-center text-muted" : "d-none"}>
|
||||
<i className=
|
||||
{"fa fa-2x " +
|
||||
(state === "not-searching"
|
||||
? "fa-search" :
|
||||
state === "searching"
|
||||
? "fa-cog fa-spin" :
|
||||
state === "no-results"
|
||||
? "fa-times-circle" :
|
||||
"d-none"
|
||||
)
|
||||
}></i><br />
|
||||
{
|
||||
state === "not-searching"
|
||||
? "Enter a search term." :
|
||||
state === "searching"
|
||||
? "Searching..." :
|
||||
state === "no-results"
|
||||
? "No results." :
|
||||
""
|
||||
<span
|
||||
className={
|
||||
state !== "results"
|
||||
? "mt-5 text-center text-muted"
|
||||
: "d-none"
|
||||
}
|
||||
>
|
||||
<i
|
||||
className={
|
||||
"fa fa-2x " +
|
||||
(state === "not-searching"
|
||||
? "fa-search"
|
||||
: state === "searching"
|
||||
? "fa-cog fa-spin"
|
||||
: state === "no-results"
|
||||
? "fa-times-circle"
|
||||
: "d-none")
|
||||
}
|
||||
></i>
|
||||
<br />
|
||||
{state === "not-searching"
|
||||
? "Enter a search term."
|
||||
: state === "searching"
|
||||
? "Searching..."
|
||||
: state === "no-results"
|
||||
? "No results."
|
||||
: ""}
|
||||
</span>
|
||||
<Droppable droppableId="$CML">
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{items.map((item, index) => (
|
||||
<Item key={itemId(item)} item={item} index={index} column={-1} />
|
||||
<Item
|
||||
key={itemId(item)}
|
||||
item={item}
|
||||
index={index}
|
||||
column={-1}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const AUX_CACHE: { [auxid: string]: AuxItem } = {};
|
||||
|
||||
export function AuxLibrary({ libraryId }: { libraryId: string }) {
|
||||
const [title, setTitle] = useState("");
|
||||
const debouncedTitle = useDebounce(title, 1000);
|
||||
const [items, setItems] = useState<AuxItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const libItems = await loadAuxLibrary(libraryId);
|
||||
libItems.forEach(item => {
|
||||
const id = itemId(item);
|
||||
if (!(id in AUX_CACHE)) {
|
||||
AUX_CACHE[id] = item;
|
||||
}
|
||||
});
|
||||
setItems(libItems);
|
||||
}
|
||||
load();
|
||||
}, [libraryId]);
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder="Filter..."
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
/>
|
||||
<Droppable droppableId="$CML">
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{items.map((item, index) => (
|
||||
<Item
|
||||
key={itemId(item)}
|
||||
item={item}
|
||||
index={index}
|
||||
column={-1}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TimeslotItem, Track, Showplan } from "../api";
|
||||
import { TimeslotItem, Track, Showplan, AuxItem } from "../api";
|
||||
import * as api from "../api";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { AppThunk } from "../store";
|
||||
|
@ -20,10 +20,13 @@ export type PlanItem = TimeslotItem | ItemGhost;
|
|||
|
||||
export type Plan = PlanItem[][];
|
||||
|
||||
export function itemId(item: PlanItem | Track) {
|
||||
export function itemId(item: PlanItem | Track | AuxItem) {
|
||||
if ("timeslotitemid" in item) {
|
||||
return item.timeslotitemid;
|
||||
}
|
||||
if ("auxid" in item) {
|
||||
return "A" + item.auxid;
|
||||
}
|
||||
if ("ghostid" in item) {
|
||||
return "G" + item.ghostid;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue