|
|
@@ -4,305 +4,21 @@ import * as React from 'react';
|
|
|
import {createRoot} from 'react-dom/client';
|
|
|
|
|
|
import {
|
|
|
- TextInput,
|
|
|
- Text,
|
|
|
- PasswordInput,
|
|
|
- Button,
|
|
|
Group,
|
|
|
- Space,
|
|
|
Tabs,
|
|
|
Stack,
|
|
|
- CloseButton,
|
|
|
- Card,
|
|
|
Title,
|
|
|
- Center,
|
|
|
- Accordion,
|
|
|
- SimpleGrid,
|
|
|
Anchor,
|
|
|
- ActionIcon,
|
|
|
- LoadingOverlay,
|
|
|
- NumberInput,
|
|
|
- List,
|
|
|
MantineProvider,
|
|
|
+ Space,
|
|
|
} from '@mantine/core';
|
|
|
-import {useColorScheme} from '@mantine/hooks';
|
|
|
-
|
|
|
-import {IconAlertCircle} from "@tabler/icons";
|
|
|
-
|
|
|
-import {
|
|
|
- DragDropContext,
|
|
|
- Droppable,
|
|
|
- Draggable,
|
|
|
-} from "react-beautiful-dnd";
|
|
|
-
|
|
|
-import * as api from "./api";
|
|
|
-
|
|
|
-const container = document.getElementById('root');
|
|
|
-const root = createRoot(container!);
|
|
|
-
|
|
|
-interface ConnectionProps {
|
|
|
- server: string;
|
|
|
- user: string;
|
|
|
- pass: string;
|
|
|
-}
|
|
|
-
|
|
|
-interface LoginProps {
|
|
|
- onLogin: (c: ConnectionProps) => void;
|
|
|
-}
|
|
|
-
|
|
|
-function LoginForm(props: LoginProps) {
|
|
|
- const [server, setServer] = React.useState<string>("https://msdl-testing.uantwerpen.be");
|
|
|
- const [user, setUser] = React.useState<string>("joeri+1@uantwerpen.be");
|
|
|
- const [pass, setPass] = React.useState<string>("drovio123");
|
|
|
-
|
|
|
- return <form>
|
|
|
- <SimpleGrid cols={2} style={{maxWidth: 1000}}>
|
|
|
- <div>
|
|
|
- <Title order={5}>Credentials</Title>
|
|
|
- <Space h="md"/>
|
|
|
- <TextInput
|
|
|
- label="Drovio User (of CDF room)"
|
|
|
- value={user}
|
|
|
- onChange={e => setUser(e.currentTarget.value)}
|
|
|
- error={/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(user) ? "Invalid email" : undefined}
|
|
|
- />
|
|
|
- <Space h="md"/>
|
|
|
- <PasswordInput
|
|
|
- label="Drovio Password"
|
|
|
- value={pass}
|
|
|
- onChange={e => setPass(e.currentTarget.value)}
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <Title order={5}>Advanced</Title>
|
|
|
- <Space h="md"/>
|
|
|
- <TextInput
|
|
|
- label="Server"
|
|
|
- value={server}
|
|
|
- onChange={e => setServer(e.currentTarget.value)}
|
|
|
- />
|
|
|
- </div>
|
|
|
- </SimpleGrid>
|
|
|
- <Space h="xl"/>
|
|
|
- <Button disabled={user.length === 0} onClick={() => props.onLogin({server, user, pass})}>Login</Button>
|
|
|
- </form>;
|
|
|
-}
|
|
|
-
|
|
|
-type ConnectingState = {
|
|
|
- state: "connecting";
|
|
|
-};
|
|
|
-
|
|
|
-type ErrorState = {
|
|
|
- state: "error";
|
|
|
- msg: string;
|
|
|
-};
|
|
|
-
|
|
|
-type SuccessState = {
|
|
|
- state: "success";
|
|
|
- accessToken: string;
|
|
|
- updating: boolean; // did we just send an uncompleted request to the server to change the layout?
|
|
|
-};
|
|
|
-
|
|
|
-type ConnectionState =
|
|
|
- | ConnectingState
|
|
|
- | ErrorState
|
|
|
- | SuccessState;
|
|
|
-
|
|
|
-interface ItemProps {
|
|
|
- children?: React.ReactNode,
|
|
|
- id: string,
|
|
|
- style?: object,
|
|
|
- dragging?: boolean,
|
|
|
-}
|
|
|
-
|
|
|
-function Item(props: ItemProps) {
|
|
|
- const style: React.CSSProperties = {
|
|
|
- cursor: props.dragging ? 'grabbing' : 'grab',
|
|
|
- // height: '100%',
|
|
|
- boxShadow: props.dragging ? "0px 0px 30px rgba(0,0,0,0.7)" : "0px 0px 5px rgba(0,0,0,0.5)",
|
|
|
- backgroundColor: "white",
|
|
|
- userSelect: 'none',
|
|
|
- margin: 20,
|
|
|
- minWidth: 200,
|
|
|
- height: 120,
|
|
|
-
|
|
|
- ...props.style,
|
|
|
- };
|
|
|
|
|
|
- return <Center {...props} style={style}>{props.id}</Center>
|
|
|
-}
|
|
|
-
|
|
|
-type LayoutType = "row" | "grid";
|
|
|
-
|
|
|
-type Items = {videowall: Array<string>, hidden: Array<string>}
|
|
|
-
|
|
|
-interface VideoWallControlProps {
|
|
|
- items: Items;
|
|
|
- setItems: (callback: (Items) => Items) => void;
|
|
|
- freeze: boolean;
|
|
|
-
|
|
|
- onChange: (Items) => void;
|
|
|
-}
|
|
|
-
|
|
|
-function VideoWallControl(props: VideoWallControlProps) {
|
|
|
- function onDragEnd(result) {
|
|
|
- if (!result.destination) {
|
|
|
- return;
|
|
|
- }
|
|
|
- props.setItems(items => {
|
|
|
- const newItems = {
|
|
|
- videowall: items.videowall.slice(),
|
|
|
- hidden: items.hidden.slice(),
|
|
|
- }
|
|
|
- const removed = newItems[result.source.droppableId].splice(result.source.index, 1);
|
|
|
- newItems[result.destination.droppableId].splice(result.destination.index, 0, ...removed);
|
|
|
-
|
|
|
- props.onChange(newItems);
|
|
|
- return newItems;
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- return <>
|
|
|
- <DragDropContext onDragEnd={onDragEnd}>
|
|
|
- <div style={{position:"relative"}}>
|
|
|
- <LoadingOverlay visible={false} overlayBlur={2}/>
|
|
|
- <Title order={5}>Video Wall</Title>
|
|
|
- <Space h="md"/>
|
|
|
- <Droppable droppableId="videowall" direction="horizontal">
|
|
|
- {(provided, snapshot) => (
|
|
|
- <div ref={provided.innerRef} {...provided.droppableProps} style={{height: 160, backgroundColor: '#0096a9', position:"relative"}}>
|
|
|
- <Group spacing={0} style={{height: '100%'}}>
|
|
|
- {props.items.videowall.map((item, index) => (
|
|
|
- <Draggable key={item} draggableId={item} index={index} style={{height: '100%'}}>
|
|
|
- {(provided, snapshot) =>
|
|
|
- <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
|
|
- <Item id={item} dragging={snapshot.isDragging}/>
|
|
|
- </div>}
|
|
|
- </Draggable>
|
|
|
- ))}
|
|
|
- {props.items.videowall.length === 0 ? <Center style={{width:"100%",height:"100%"}}><Text color="white">No active screenshares.</Text></Center> : <></>}
|
|
|
- <div/>
|
|
|
- </Group>
|
|
|
- {provided.placeholder}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </Droppable>
|
|
|
- <Space h="xl"/>
|
|
|
- <Title order={5}>Hidden Screenshares</Title>
|
|
|
- <Space h="md"/>
|
|
|
- <Droppable droppableId="hidden" direction="horizontal">
|
|
|
- {(provided, snapshot) => (
|
|
|
- <div ref={provided.innerRef} {...provided.droppableProps} style={{height: 160, backgroundColor: '#555059', position:"relative"}}>
|
|
|
- <Group spacing={0} style={{height: '100%'}}>
|
|
|
- {props.items.hidden.map((item, index) => (
|
|
|
- <Draggable key={item} draggableId={item} index={index}>
|
|
|
- {(provided, snapshot) =>
|
|
|
- <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
|
|
- <Item id={item} dragging={snapshot.isDragging}/>
|
|
|
- </div>}
|
|
|
- </Draggable>
|
|
|
- ))}
|
|
|
- {props.items.hidden.length === 0 ? <Center style={{width:"100%",height:"100%"}}><Text color="white">No hidden screenshares.</Text></Center> : <></>}
|
|
|
- <div/>
|
|
|
- </Group>
|
|
|
- {provided.placeholder}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </Droppable>
|
|
|
- </div>
|
|
|
- </DragDropContext>
|
|
|
- </>;
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-function ConnectionTab(props: ConnectionProps) {
|
|
|
- const [state, setState] = React.useState<ConnectionState>({state: "connecting"});
|
|
|
- const [items, setItems] = React.useState<Items>({videowall: [], hidden: []});
|
|
|
-
|
|
|
- function refreshScreenShares(accessToken) {
|
|
|
- return api.getScreenShares({
|
|
|
- server: props.server,
|
|
|
- userEmail: props.user,
|
|
|
- accessToken,
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- function attemptConnect() {
|
|
|
- api.login({
|
|
|
- server: props.server,
|
|
|
- userEmail: props.user,
|
|
|
- password: props.pass,
|
|
|
- })
|
|
|
- .then(accessToken => {
|
|
|
- return refreshScreenShares(accessToken)
|
|
|
- .then(setItems)
|
|
|
- .then(() => {
|
|
|
- setState({state: 'success', accessToken, updating: false});
|
|
|
- });
|
|
|
- })
|
|
|
- .catch(err => {
|
|
|
- setState({state: 'error', msg: err.message});
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- React.useEffect(() => {
|
|
|
- attemptConnect();
|
|
|
- }, []);
|
|
|
-
|
|
|
- const [pollInterval, setPollInterval] = React.useState<number>(1000);
|
|
|
+import {useColorScheme} from '@mantine/hooks';
|
|
|
|
|
|
- React.useEffect(() => {
|
|
|
- let canceled = false;
|
|
|
- if (state.state === "success" && !state.updating) {
|
|
|
- const accessToken = state.accessToken;
|
|
|
- setTimeout(() => {
|
|
|
- refreshScreenShares(accessToken)
|
|
|
- .then(items => {
|
|
|
- console.log("setItems due to POLL");
|
|
|
- if (!canceled) setItems(items);
|
|
|
- });
|
|
|
- }, pollInterval);
|
|
|
- }
|
|
|
- return function cleanup() {
|
|
|
- canceled = true;
|
|
|
- }
|
|
|
- }, [state, items, pollInterval]);
|
|
|
+import {IconExternalLink} from "@tabler/icons";
|
|
|
|
|
|
- switch (state.state) {
|
|
|
- case "connecting":
|
|
|
- return <Text>Connecting...</Text>;
|
|
|
- case "success":
|
|
|
- return <>
|
|
|
- <VideoWallControl {...{items, setItems}} freeze={state.updating} onChange={items => {
|
|
|
- // setItems(items);
|
|
|
- console.log("SEND UPDATE");
|
|
|
- setState(oldState => ({...oldState, updating: true}));
|
|
|
- api.setScreenShares({
|
|
|
- server: props.server,
|
|
|
- userEmail: props.user,
|
|
|
- accessToken: state.accessToken,
|
|
|
- visibleShares: items.videowall,
|
|
|
- })
|
|
|
- .then(result => {
|
|
|
- console.log("UPDATE RESULT:", result);
|
|
|
- setState(oldState => ({...oldState, updating: false}));
|
|
|
- })
|
|
|
- }}/>
|
|
|
- <Space h="xl"/>
|
|
|
- <Title order={5}>Advanced</Title>
|
|
|
- <Space h="md"/>
|
|
|
- <NumberInput label="Polling interval (ms)" value={pollInterval} onChange={setPollInterval} step={100}/>
|
|
|
- </>;
|
|
|
- case "error":
|
|
|
- return <>
|
|
|
- <List style={{color:"red"}} icon={<IconAlertCircle/>}>
|
|
|
- <List.Item>Error {state.msg}</List.Item>
|
|
|
- </List>
|
|
|
- <Space h="xl"/>
|
|
|
- <Button onClick={attemptConnect}>Retry</Button>
|
|
|
- </>;
|
|
|
- }
|
|
|
-}
|
|
|
+import {ConnectionTab, ConnectionProps} from "./connectiontab";
|
|
|
+import {LoginForm} from "./loginform";
|
|
|
|
|
|
function App() {
|
|
|
const [connections, setConnections] = React.useState<Array<ConnectionProps>>([]);
|
|
|
@@ -318,10 +34,12 @@ function App() {
|
|
|
return (
|
|
|
<MantineProvider theme={{colorScheme: preferredColorScheme}} withGlobalStyles withNormalizeCSS>
|
|
|
<Stack style={{userSelect: 'none'}}>
|
|
|
- {/*<Group position="left"></Group>*/}
|
|
|
<Group position="left" style={{padding:10}}>
|
|
|
- <Title order={3}>CDF Mod Tool</Title>
|
|
|
- <img src="https://www.flandersmake.be/themes/custom/dropsolid-theme-flex-8/logo.svg" style={{width:100, position: "absolute", right: 10, top:10}}/>
|
|
|
+ <Title order={3}>CDF Moderator</Title>
|
|
|
+ <Group style={{position: "absolute", right:8, top:8}}>
|
|
|
+ <img src="logoFM.svg" style={{height:40}}/>
|
|
|
+ {/*<img src="logoUA.svg" style={{height:45}}/>*/}
|
|
|
+ </Group>
|
|
|
</Group>
|
|
|
<Tabs value={currentTab} onTabChange={setCurrentTab}>
|
|
|
<Tabs.List>
|
|
|
@@ -348,10 +66,20 @@ function App() {
|
|
|
<ConnectionTab server={c.server} user={c.user} pass={c.pass}/>
|
|
|
</Tabs.Panel>)}
|
|
|
</Tabs>
|
|
|
+ {/* Make sure we can always scroll down far enough, such that nothing is hidden behind the viewport-fixed source code link: */}
|
|
|
+ <Space h="lg"/>
|
|
|
</Stack>
|
|
|
+ <Anchor href="https://msdl.uantwerpen.be/git/jexelmans/cdf-modtool"
|
|
|
+ target="_blank"
|
|
|
+ style={{position: "fixed", bottom: 8, right: 8}}>
|
|
|
+ <IconExternalLink size={14} style={{marginRight: 4}}/>Source Code
|
|
|
+ </Anchor>
|
|
|
+
|
|
|
</MantineProvider>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+const container = document.getElementById('root');
|
|
|
+const root = createRoot(container!);
|
|
|
|
|
|
root.render( <App/> );
|