Merge pull request #132 from openmhz/playlists

Playlists
This commit is contained in:
Luke Berndt
2023-01-23 22:16:19 -05:00
committed by GitHub
30 changed files with 5635 additions and 74 deletions

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import { useCallLink } from "./CallLinks";
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../../features/api/apiSlice'
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
@@ -9,9 +9,11 @@ import {
Segment,
Statistic,
Icon,
Menu
Menu,
Tab
} from "semantic-ui-react";
import PlaylistBuilder from "./PlaylistBuilder"
import CallInfoPane from "./CallInfoPane"
// ----------------------------------------------------
function CallInfo(props) {
const { shortName } = useParams();
@@ -44,51 +46,35 @@ function CallInfo(props) {
talkgroupNum = currentCall.talkgroupNum;
}
const {callLink,callDownload,callTweet} = useCallLink(props.call)
const { callLink, callDownload, callTweet } = useCallLink(props.call)
const [activeTab, setActiveTab] = useState(0);
const handleTabChange = (e, data) => setActiveTab(data.activeIndex);
const panes = [
{
menuItem: { key: 'info', icon: 'info', content: 'Call Info' }, render: () => {
return (
<Tab.Pane attached='bottom'>
<CallInfoPane call={props.call} />
</Tab.Pane>
)
}
},
{
menuItem: { key: 'list', icon: 'list', content: 'Create Event' }, render: () => {
return (
<PlaylistBuilder />
)
}
}
]
return (
<div>
<Segment padded attached>
<Header as='h1'>{header}</Header>
<List bulleted horizontal link>
{srcList}
</List>
<Divider />
<Statistic size='small'>
<Statistic.Label>Seconds</Statistic.Label>
<Statistic.Value>{callLength}</Statistic.Value>
</Statistic>
<Statistic size='small'>
<Statistic.Label>Talkgroup</Statistic.Label>
<Statistic.Value>{talkgroupNum}</Statistic.Value>
</Statistic>
<List divided verticalAlign='middle'>
<List.Item>
<Icon name="wait" />
<List.Content>
{callTime}
</List.Content>
</List.Item>
<List.Item>
<Icon name="calendar outline" />
<List.Content>
{callDate}
</List.Content>
</List.Item>
<List.Item>
<Icon name="cubes" />
<List.Content>
{callFreq} MHz
</List.Content>
</List.Item>
</List>
</Segment>
<Menu attached='bottom'>
<a href={callTweet}><Menu.Item name="tweet" ><Icon name='twitter' />Tweet</Menu.Item></a>
<a href={callDownload}><Menu.Item name="download"><Icon name="download" />Download</Menu.Item></a>
<a href={callLink}><Menu.Item name="link"><Icon name="at" />Link</Menu.Item></a>
</Menu>
<Tab menu={{ attached: 'top', fluid: true, widths: 2 }} panes={panes} defaultActiveIndex={activeTab} onTabChange={handleTabChange} />
</div>
);
}

View File

@@ -0,0 +1,98 @@
import React, { useState } from "react";
import { useCallLink } from "./CallLinks";
import {skipToken} from '@reduxjs/toolkit/query'
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../../features/api/apiSlice'
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
import {
Header,
Divider,
List,
Segment,
Statistic,
Icon,
Menu,
Tab
} from "semantic-ui-react";
import PlaylistBuilder from "./PlaylistBuilder"
// ----------------------------------------------------
function CallInfoPane(props) {
let srcList = "";
let callLength = "-";
let callFreq = "-";
let callDate = "-";
let callTime = "-";
let talkgroupNum = "-";
let header = "Call Info"
const currentCall = props.call?props.call:false;
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(currentCall?currentCall.shortName:skipToken);
if (currentCall) {
if ((talkgroupsData) && talkgroupsData.talkgroups[currentCall.talkgroupNum]) {
header = talkgroupsData.talkgroups[currentCall.talkgroupNum].description;
}
const time = new Date(currentCall.time);
callTime = time.toLocaleTimeString();
callDate = time.toLocaleDateString();
if (currentCall.freq) {
const freq = currentCall.freq / 1000000;
callFreq = Math.round(freq * 1000) / 1000;
}
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}[{source.pos}]</List.Item>);
callLength = currentCall.len;
talkgroupNum = currentCall.talkgroupNum;
}
const { callLink, callDownload, callTweet } = useCallLink(props.call)
return (
<>
<Header as='h1'>{header}</Header>
<List bulleted horizontal link>
{srcList}
</List>
<Divider />
<Statistic size='small'>
<Statistic.Label>Seconds</Statistic.Label>
<Statistic.Value>{callLength}</Statistic.Value>
</Statistic>
<Statistic size='small'>
<Statistic.Label>Talkgroup</Statistic.Label>
<Statistic.Value>{talkgroupNum}</Statistic.Value>
</Statistic>
<List divided verticalAlign='middle'>
<List.Item>
<Icon name="wait" />
<List.Content>
{callTime}
</List.Content>
</List.Item>
<List.Item>
<Icon name="calendar outline" />
<List.Content>
{callDate}
</List.Content>
</List.Item>
<List.Item>
<Icon name="cubes" />
<List.Content>
{callFreq} MHz
</List.Content>
</List.Item>
</List>
<Divider />
<Menu secondary fluid widths={2}>
<Menu.Item name="download" href={callDownload}><Icon name="download" />Download</Menu.Item>
<Menu.Item name="link" href={callLink}><Icon name="at" />Link</Menu.Item>
</Menu>
</>
);
}
export default CallInfoPane;

View File

@@ -7,16 +7,21 @@ import {
import { addStar, removeStar } from "../../features/calls/callsSlice";
import { useDispatch } from 'react-redux'
const CallItem = (props) => {
const call = props.call;
const shortName = props.shortName;
const talkgroups = props.talkgroups;
const activeCall = props.activeCall;
const [starVisible, setStarVisible] = useState(false);
const [starClicked, setStarClicked] = useState(false);
const dispatch = useDispatch();
const call = props.call;
const time = new Date(call.time);
const activeCall = props.activeCall;
const handleStarClicked = (e) => {
e.preventDefault();
@@ -34,6 +39,23 @@ const CallItem = (props) => {
let rowSelected = {};
let starButton;
let starClickable = {};
const onDragStart = (event) => {
// It receives a DragEvent
// which inherits properties from
// MouseEvent and Event
// so we can access the element
// through event.currentTarget
// Later, we'll save
// in a hook variable
// the item being dragged
const callId = event.currentTarget.dataset.callid;
event.dataTransfer.setData("call-id", callId);
console.log("Dragging: " + callId);
}
if (!starClicked) {
starClickable = { link: true };
}
@@ -62,20 +84,18 @@ const CallItem = (props) => {
}
}
let talkgroup;
if ((typeof props.talkgroups == 'undefined') || (typeof props.talkgroups[call.talkgroupNum] == 'undefined')) {
if ((typeof talkgroups== 'undefined') || (typeof talkgroups.talkgroups[call.talkgroupNum] == 'undefined')) {
talkgroup = call.talkgroupNum;
} else {
talkgroup = props.talkgroups[call.talkgroupNum].description;
talkgroup = talkgroups.talkgroups[call.talkgroupNum].description;
}
return (
<Table.Row onClick={(e) => props.onClick({ call: call }, e)} {...rowSelected}>
<Table.Row draggable="true" onClick={(e) => props.onClick({ call: call }, e)} {...rowSelected} onDragStart={onDragStart} data-callid={call._id}>
<Table.Cell> {call.len} </Table.Cell>
<Table.Cell> {talkgroup} </Table.Cell>
<Table.Cell> {time.toLocaleTimeString()} </Table.Cell>
<Table.Cell onMouseEnter={() => setStarVisible(true)} onMouseLeave={() => setStarVisible(false)} onClick={handleStarClicked}>{starButton}</Table.Cell>
</Table.Row>
);
}

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useRef } from "react";
import { useParams } from 'react-router-dom';
import CallItem from "./CallItem";
import {
Icon,
@@ -6,11 +7,13 @@ import {
Ref
} from "semantic-ui-react";
import "../CallPlayer.css";
import { useGetTalkgroupsQuery } from '../../features/api/apiSlice'
// ----------------------------------------------------
const ListCalls = (props) => {
const activeCallRef = useRef();
const { shortName } = useParams();
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
@@ -42,9 +45,9 @@ const ListCalls = (props) => {
{
props.callsData.ids.map((callId, index) => {
if (callId === props.activeCallId) {
return (<Ref innerRef={activeCallRef} key={index} ><CallItem activeCall={true} call={props.callsData.entities[callId]} talkgroups={props.talkgroups} key={index} onClick={props.playCall}/></Ref>)
return (<Ref innerRef={activeCallRef} key={index} ><CallItem activeCall={true} call={props.callsData.entities[callId]} talkgroups={talkgroupsData} key={index} onClick={props.playCall}/></Ref>)
} else {
return <CallItem activeCall={false} call={props.callsData.entities[callId]} talkgroups={props.talkgroups} key={index} onClick={props.playCall}/>
return <CallItem activeCall={false} call={props.callsData.entities[callId]} talkgroups={talkgroupsData} key={index} onClick={props.playCall}/>
}
})

View File

@@ -0,0 +1,185 @@
import { useSelector, useDispatch } from 'react-redux'
import { useParams } from 'react-router-dom';
import React, { useState } from "react";
import {
Header,
Divider,
List,
ListItem,
Segment,
Statistic,
Icon,
Menu,
Tab,
Table,
Modal,
Button,
Image,
Form,
Message
} from "semantic-ui-react";
import { useAddNewEventMutation, useGetTalkgroupsQuery } from '../../features/api/apiSlice'
function PlaylistBuilder(props) {
const { shortName } = useParams();
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
const [addNewEvent, { isLoading }] = useAddNewEventMutation()
const { loading: callsLoading, data: callsData } = useSelector((state) => state.calls);
const [playlist, setPlaylist] = useState([]);
const [open, setOpen] = React.useState(false);
const [title, setTitle] = useState("")
const [description, setDescription] = useState("")
const handleTitleChange = (e, { name, value }) => setTitle(value);
const handleDescriptionChange = (e, { name, value }) => setDescription(value);
const compareCalls = (a, b) => {
const aTimestamp = new Date(a.time).getTime()
const bTimestamp = new Date(b.time).getTime()
if (aTimestamp > bTimestamp) {
return -1
} else {
return 1
}
}
const addCall = (call) => {
const found = playlist.find(c => c._id == call._id);
if (!found) {
playlist.push(call);
setPlaylist([...playlist.sort(compareCalls)])
}
}
const onDragOver = (event) => {
// It also receives a DragEvent.
// Later, we'll read the position
// of the item from event.currentTarget
// and store the updated list state
// We need to prevent the default behavior
// of this event, in order for the onDrop
// event to fire.
// It may sound weird, but the default is
// to cancel out the drop.
event.preventDefault();
}
const onDrop = (event) => {
// Here, we will:
// - update the rendered list
// - and reset the DnD state
var callId = event.dataTransfer.getData("call-id");
console.log("Dropping: " + callId);
addCall(callsData.entities[callId])
}
let listItems = "";
listItems = playlist.map((call, index) => {
let tgAlpha = call.talkgroupNum;
if (talkgroupsData) {
const talkgroup = talkgroupsData.talkgroups[call.talkgroupNum]
tgAlpha = talkgroup.alpha?talkgroup.alpha:tgAlpha;
}
const time = new Date(call.time);
const callTime = time.toLocaleTimeString();
const callDate = time.toLocaleDateString();
return ( <Table.Row key={index}>
<Table.Cell>{call.len} </Table.Cell>
<Table.Cell>{tgAlpha}</Table.Cell>
<Table.Cell>{callTime}</Table.Cell>
</Table.Row>
)
})
const submitPlaylist = () => {
setOpen(true)
}
const handleSubmit = () => {
console.log(title)
setOpen(false)
const callIds = playlist.map( (call) => call._id)
const event = {
title,
description,
callIds
}
addNewEvent(event)
setTitle("")
setDescription("")
setPlaylist([])
}
let content = (<Header as="h3" icon textAlign='center' style={{paddingTop: "3em"}}><Icon name='target' />Drag Calls Here</Header>)
if (playlist.length > 0) {
content = (<Table basic="very">
<Table.Body>
{listItems}
</Table.Body>
</Table>)
}
return (
<>
<Modal
onClose={() => setOpen(false)}
onOpen={() => setOpen(true)}
open={open}
>
<Modal.Header>Submit an Event</Modal.Header>
<Modal.Content>
<Modal.Description>
<Message icon>
<Icon name='warning sign' />
<Message.Content>The Events feature is a work in progress! Things may break, change, or get deleted. Contact me with ideas or problems: luke@robotastic.com</Message.Content>
</Message>
<Header>Event</Header>
<Form>
<Form.Input
name="title"
value={title}
label="Title"
onChange={handleTitleChange}
/>
<Form.TextArea
name="description"
value={description}
label="Description"
placeholder='Please describe the event'
onChange={handleDescriptionChange}
/>
</Form>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button color='black' onClick={() => setOpen(false)}>
Nope
</Button>
<Button
content="Submit"
labelPosition='right'
icon='checkmark'
onClick={handleSubmit}
positive
/>
</Modal.Actions>
</Modal>
<Tab.Pane attached='bottom' onDragOver={onDragOver} onDrop={onDrop}>
{content}
</Tab.Pane>
<Menu fluid widths={2}>
<Menu.Item name="clear" onClick={() => setPlaylist([])}><Icon name='delete' />Clear</Menu.Item>
<Menu.Item active name="submit" onClick={submitPlaylist}><Icon name="cloud upload" />Submit</Menu.Item>
</Menu>
</>
)
}
export default PlaylistBuilder;