cleaned up the folders

This commit is contained in:
Luke Berndt
2023-01-18 21:27:02 -05:00
parent 9edc48ef37
commit fef88f3517
13 changed files with 17 additions and 17 deletions

View File

@@ -0,0 +1,6 @@
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list {
padding-left: 0;
padding-right: 0;
}

View File

@@ -0,0 +1,62 @@
import React, { useState } from "react";
import {
Modal,
Button,
Icon
} from "semantic-ui-react";
import "./CalendarModal.css";
import { setDateFilter } from "../../features/callPlayer/callPlayerSlice";
import DatePicker from 'react-datepicker';
import { useDispatch } from 'react-redux'
import { subDays } from 'date-fns'
import 'react-datepicker/dist/react-datepicker.css';
function CalendarModal(props) {
const [startDate, setStartDate] = useState(new Date());
const dispatch = useDispatch()
const onClose = props.onClose;
const handleDateChange = (date) => {
setStartDate(date);
}
const handleClose = () => this.props.onClose(false);
const handleDone = (onClose) => {
dispatch(setDateFilter(startDate.getTime()));
onClose(true);
}
return (
<Modal open={props.open} onClose={handleClose} centered={false} size="tiny">
<Modal.Header>Select a Date & Time</Modal.Header>
<Modal.Content >
<Modal.Description>
<DatePicker
selected={startDate}
onChange={handleDateChange}
maxDate={new Date()}
minDate={subDays(new Date(), process.env.REACT_APP_ARCHIVE_DAYS)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="LLL"
inline
/>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={()=>handleDone(onClose)} >
<Icon name='checkmark' /> Done
</Button>
</Modal.Actions>
</Modal>
)
}
export default CalendarModal;

View File

@@ -0,0 +1,96 @@
import React from "react";
import { useCallLink } from "./CallLinks";
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../../features/api/apiSlice'
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
import {
Header,
Divider,
List,
Segment,
Statistic,
Icon,
Menu
} from "semantic-ui-react";
// ----------------------------------------------------
function CallInfo(props) {
const { shortName } = useParams();
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
let srcList = "";
let callLength = "-";
let callFreq = "-";
let callDate = "-";
let callTime = "-";
let talkgroupNum = "-";
let header = "Call Info"
if (props.call) {
const currentCall = props.call;
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 (
<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>
</div>
);
}
export default CallInfo;

View File

@@ -0,0 +1,82 @@
import React, { useState } from "react";
import {
Table,
Icon,
Label
} from "semantic-ui-react";
import { addStar, removeStar } from "../../features/calls/callsSlice";
import { useDispatch } from 'react-redux'
const CallItem = (props) => {
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();
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
if (!starClicked) {
setStarClicked(true);
dispatch(addStar(props.call._id));
} else {
setStarClicked(false);
dispatch(removeStar(props.call._id));
}
}
let rowSelected = {};
let starButton;
let starClickable = {};
if (!starClicked) {
starClickable = { link: true };
}
if (!call.star && starVisible) {
starButton = <Icon name='star outline' />
}
if (call.star) {
starButton = <Icon name='star' />
}
if (call.star && call.star > 1) {
starButton = (<Icon.Group >
<Icon {...starClickable} name='star' />
<Label circular color='red' size='mini' floating>
{call.star}
</Label>
</Icon.Group>)
}
if (activeCall) {
rowSelected = {
positive: true,
color: "blue",
key: "blue",
inverted: "true"
}
}
let talkgroup;
if ((typeof props.talkgroups == 'undefined') || (typeof props.talkgroups[call.talkgroupNum] == 'undefined')) {
talkgroup = call.talkgroupNum;
} else {
talkgroup = props.talkgroups[call.talkgroupNum].description;
}
return (
<Table.Row onClick={(e) => props.onClick({ call: call }, e)} {...rowSelected}>
<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>
);
}
export default CallItem;

View File

@@ -0,0 +1,36 @@
import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
export function useCallLink(currentCall) {
const filterType = useSelector((state) => state.callPlayer.filterType);
const filterGroupId = useSelector((state) => state.callPlayer.filterGroupId);
const filterTalkgroups = useSelector((state) => state.callPlayer.filterTalkgroups);
const shortName = useSelector((state) => state.callPlayer.shortName);
if (currentCall) {
let search = ""
const time = new Date(currentCall.time);
const callTime = time.toLocaleTimeString();
const callDate = time.toLocaleDateString();
switch (filterType) {
case 1:
search = `filter-type=group&filter-code=${filterGroupId}&`;
break;
case 2:
search = `filter-type=talkgroup&filter-code=${filterTalkgroups}&`;
break;
default:
case 0:
break;
}
const callLink = "/system/" + shortName + "?" + search + "call-id=" + currentCall._id + "&time=" + (time.getTime() + 1);
const callDownload = currentCall.url;
const callTweet = "https://twitter.com/intent/tweet?url=" + encodeURIComponent(document.location.origin + callLink) + "&via=" + encodeURIComponent("OpenMHz");
return { callLink, callDownload, callTweet }
} else {
return { callLink: "", callDownload: "", callTweet: "" }
}
}

View File

@@ -0,0 +1,3 @@
.active.tab {
min-height: 250px;
}

View File

@@ -0,0 +1,146 @@
import React, { useState } from "react";
import { setGroupFilter, setTalkgroupFilter, setAllFilter, setStarredFilter } from "../../features/callPlayer/callPlayerSlice";
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../../features/api/apiSlice'
import { useSelector, useDispatch } from 'react-redux'
import {
Modal,
Button,
Icon,
Tab,
Header,
Dropdown,
Checkbox,
Divider
} from "semantic-ui-react";
import "./FilterModal.css";
function FilterModal(props) {
const globalFilterStarred = useSelector((state) => state.callPlayer.filterStarred);
const { data:groupsData, isSuccess:isGroupsSuccess } = useGetGroupsQuery(props.shortName);
const { data:talkgroupsData, isSuccess:isTalkgroupsSuccess } = useGetTalkgroupsQuery(props.shortName);
const [selectedTalkgroup, setSelectedTalkgroup] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(false);
const [filterStarred, setFilterStarred] = useState(false);
const [activeTab, setActiveTab] = useState(0);
const dispatch = useDispatch()
const handleTabChange = (e, data) => setActiveTab(data.activeIndex);
const handleGroupChange = (e, { name, value }) => setSelectedGroup(value);
const handleTalkgroupChange = (e, { name, value }) => setSelectedTalkgroup(value);
const toggleStarred = () => setFilterStarred(!filterStarred);
const handleClose = () => props.onClose(false);
const onClose = props.onClose;
function handleDone(onClose) {
switch (activeTab) {
case 1:
if (selectedGroup) {
dispatch(setGroupFilter(selectedGroup));
props.onClose(true);
} else {
props.onClose(false);
}
break;
case 2:
if (selectedTalkgroup) {
dispatch(setTalkgroupFilter(selectedTalkgroup))
props.onClose(true);
} else {
props.onClose(false);
}
break;
default:
case 0:
dispatch(setAllFilter());
props.onClose(true);
break;
}
if (globalFilterStarred !== filterStarred) {
dispatch(setStarredFilter(filterStarred));
}
}
let talkgroupList = [];
if (isTalkgroupsSuccess) {
for (const num in talkgroupsData["talkgroups"]) {
const talkgroup = talkgroupsData["talkgroups"][num];
const obj = {
key: talkgroup.num,
value: talkgroup.num,
text: talkgroup.description
}
talkgroupList.push(obj);
}
}
let groupList = [];
if (isGroupsSuccess) {
for (const num in groupsData) {
const group = groupsData[num]
const obj = {
key: group._id,
value: group._id,
text: group.groupName
}
groupList.push(obj);
}
}
const panes = [
{
menuItem: 'All', render: () => {
return (
<Tab.Pane attached={false}>
<Header>All Calls</Header>
<p>Dispaly all of the calls.</p>
</Tab.Pane>
)
}
},
{
menuItem: 'Groups', render: () => {
return (
<Tab.Pane attached={false}>
<Dropdown placeholder='Groups' fluid selection options={groupList} value={selectedGroup} name='selectedGroup' onChange={handleGroupChange} />
</Tab.Pane>
)
}
},
{
menuItem: 'Talkgroups', render: () => {
return (
<Tab.Pane attached={false}>
<Dropdown placeholder='Talkgroups' fluid multiple selection options={talkgroupList} value={selectedTalkgroup} name='selectedTalkgroup' onChange={handleTalkgroupChange} />
</Tab.Pane>
)
}
},
]
return (
<Modal open={props.open} onClose={handleClose} centered={false} size="tiny">
<Modal.Header>Select a Filter</Modal.Header>
<Modal.Content >
<Modal.Description>
<Tab menu={{ pointing: true }} panes={panes} defaultActiveIndex={activeTab} onTabChange={handleTabChange} />
<Divider />
<Checkbox label='Show only Starred calls' checked={filterStarred} name='filterStarred' onChange={toggleStarred} />
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={()=> handleDone(onClose)} >
<Icon name='checkmark' /> Done
</Button>
</Modal.Actions>
</Modal>
)
}
export default FilterModal;

View File

@@ -0,0 +1,73 @@
import React, { Component, useState } from "react";
import { useGetGroupsQuery } from '../../features/api/apiSlice'
import { setGroupFilter } from "../../features/callPlayer/callPlayerSlice";
import { useSelector, useDispatch } from 'react-redux'
import {
Modal,
Button,
Icon,
Dropdown
} from "semantic-ui-react";
import "./FilterModal.css";
function GroupModal (props) {
const shortName = useSelector((state) => state.callPlayer.shortName);
const { data, isError, isLoading, isSuccess,error } = useGetGroupsQuery(props.shortName);
const [open, setOpen] = useState(false);
const [selectedGroup, setSelectedGroup] = useState(false);
const dispatch = useDispatch()
const onClose = props.onClose;
function handleDone(onClose) {
if (selectedGroup) {
dispatch(setGroupFilter(selectedGroup))
//dispatch({type: types.SET_GROUP_FILTER, groupId: selectedGroup })
onClose(true);
} else {
onClose(false);
}
}
let groupOptions = []
if (isSuccess) {
for (const num in data) {
const group = data[num];
const obj = {
key: group._id,
value: group._id,
text: group.groupName
}
groupOptions.push(obj);
}
}
return (
<Modal open={props.open} onClose={() => setOpen(false)} onOpen={() => setOpen(true)} size='tiny'>
<Modal.Header>Select a Group</Modal.Header>
<Modal.Content >
<Modal.Description>
<p>Choose the type of calls you want to listen to</p>
<Dropdown placeholder='Group...' fluid selection options={groupOptions} name='selectedGroup' onChange={(e, data) => setSelectedGroup(data.value)} />
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={()=> handleDone(onClose)} >
<Icon name='headphones' /> Listen
</Button>
</Modal.Actions>
</Modal>
)
}
export default GroupModal;

View File

@@ -0,0 +1,60 @@
import React, { useEffect, useRef } from "react";
import CallItem from "./CallItem";
import {
Icon,
Table,
Ref
} from "semantic-ui-react";
import "../CallPlayer.css";
// ----------------------------------------------------
const ListCalls = (props) => {
const activeCallRef = useRef();
//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
useEffect(() => {
if (activeCallRef.current) {
activeCallRef.current.scrollIntoView({
block: "center",
});
}
});
const callsData = props.callsData;
return (
<Table id="calls" unstackable >
<Table.Header >
<Table.Row>
<Table.HeaderCell>Len</Table.HeaderCell>
<Table.HeaderCell>Talkgroup</Table.HeaderCell>
<Table.HeaderCell>Time</Table.HeaderCell>
<Table.HeaderCell><Icon name='star' /></Table.HeaderCell>
</Table.Row>
</Table.Header>
{callsData &&
<Table.Body>
{
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>)
} else {
return <CallItem activeCall={false} call={props.callsData.entities[callId]} talkgroups={props.talkgroups} key={index} onClick={props.playCall}/>
}
})
}
</Table.Body>
}
</Table>
);
}
export default ListCalls;

View File

@@ -0,0 +1,117 @@
import React, { useEffect, useState } from "react";
import {
Menu,
Icon,
Progress,
Label
} from "semantic-ui-react";
import ReactAudioPlayer from 'react-audio-player'
const MediaPlayer = (props) => {
const audioRef = React.createRef();
const [sourceIndex, setSourceIndex] = useState(0);
const [playProgress, setPlayProgress] = useState(0);
const [playTime, setPlayTime] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const parentHandlePlayPause = props.onPlayPause
const handlePause = () => { setIsPlaying(false); parentHandlePlayPause(false); }
const handlePlay = () => { setIsPlaying(true); parentHandlePlayPause(true); }
const playPause = () => { const audio = audioRef.current.audioEl.current; if (isPlaying) { audio.pause(); } else { audio.play(); } }
const call = props.call;
useEffect(() => {
const audio = audioRef.current.audioEl.current;
const onEnded = props.onEnded;
setSourceIndex(0);
if (call) {
audio.src = call.url;
const playPromise = audio.play();
// In browsers that don’t yet support this functionality,
// playPromise won’t be defined.
if (playPromise !== undefined) {
playPromise.then(function () {
}).catch(function (error) {
console.log("Automatic playback failed: " + error);
onEnded();
// Show a UI element to let the user manually start playback.
});
} else {
audio.src = false;
}
}
}, [call]);
const updatePlayProgress = () => {
const audio = audioRef.current.audioEl.current;
const { currentTime, duration } = audio;
// this checks to see if it should display the next Source ID
if (call && ((call.srcList.length - 1) >= (sourceIndex + 1)) && (currentTime > call.srcList[sourceIndex + 1].pos)) {
setSourceIndex(sourceIndex + 1);
}
// updates the play percentage progress and current playing time
setPlayProgress(currentTime / duration * 100);
setPlayTime(Math.floor(currentTime));
}
let playEnabled = { "disabled": true }
let sourceId = "-";
if (call) {
if (call.srcList.length > sourceIndex) {
sourceId = call.srcList[sourceIndex].src;
}
playEnabled = {};
}
return (
<Menu.Menu>
<ReactAudioPlayer
ref={audioRef}
onPause={handlePause}
onPlay={handlePlay}
listenInterval={100}
onListen={updatePlayProgress}
onEnded={props.onEnded}
autoPlay
/>
<Menu.Item onClick={playPause} >
{
isPlaying
? (<Icon name="pause" />)
: (<Icon name="play" />)
}
</Menu.Item>
<Menu.Item>
<Progress inverted percent={playProgress} />
<Label color="black">
{playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
)
}
export default MediaPlayer;

View File

@@ -0,0 +1,41 @@
import {useState} from 'react';
import {
Modal,
Button,
Icon
} from "semantic-ui-react";
import "./FilterModal.css";
function SupportModal(props) {
const [open, setOpen] = useState(false);
const handleClose = () => props.onClose();
return (
<Modal open={open} onClose={() => setOpen(false)} onOpen={() => setOpen(true)} trigger={<div><Icon name="coffee" />Support OpenMHz</div>} size='tiny' >
<Modal.Header>Support OpenMHz</Modal.Header>
<Modal.Content image>
<Icon size='massive' name="coffee" />
<Modal.Description>
<p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
<p>No worries if not, I am not having problems keeping it going.</p>
<a href="https://github.com/sponsors/robotastic"><Button>
Support
</Button></a>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setOpen(false)} >
Done
</Button>
</Modal.Actions>
</Modal>
)
}
export default SupportModal;