diff --git a/src/App.jsx b/src/App.jsx index 4a3b2b84..a2fa6802 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -20,7 +20,7 @@ import Devices from './pages/Devices/Devices'; import Device from './pages/Device/Device'; import Scenes from './pages/Scenes/Scenes'; import Settings from './pages/Settings/Settings'; -import Integrations from './pages/Integrations'; +import Integrations from './pages/Integrations/Integrations'; import ScrollToTop from './utils/scrollToTop'; const curacaoDarkTheme = createMuiTheme({ diff --git a/src/components/Bars/BarBottom.jsx b/src/components/Bars/BarBottom.jsx index 3d9ca17a..93f18887 100644 --- a/src/components/Bars/BarBottom.jsx +++ b/src/components/Bars/BarBottom.jsx @@ -8,17 +8,17 @@ import HomeIcon from '@material-ui/icons/Home'; import Wallpaper from '@material-ui/icons/Wallpaper'; import { useLocation, Link } from 'react-router-dom'; import Backdrop from '@material-ui/core/Backdrop'; -import Fab from '@material-ui/core/Fab'; -import AddIcon from '@material-ui/icons/Add'; import SpeedDial from '@material-ui/lab/SpeedDial'; import SpeedDialIcon from '@material-ui/lab/SpeedDialIcon'; import SpeedDialAction from '@material-ui/lab/SpeedDialAction'; import useStore from '../../utils/apiStore'; -// import SettingsInputSvideoIcon from "@material-ui/icons/SettingsInputSvideo"; +import SettingsInputSvideoIcon from "@material-ui/icons/SettingsInputSvideo"; +import DeveloperMode from "@material-ui/icons/DeveloperMode"; import SettingsInputComponent from '@material-ui/icons/SettingsInputComponent'; import AddSceneDialog from '../../pages/Scenes/AddSceneDialog'; import AddDeviceDialog from '../../pages/Devices/AddDeviceDialog'; import AddVirtualDialog from '../../pages/Devices/AddVirtualDialog'; +import AddIntegrationDialog from '../../pages/Integrations/AddIntegration'; const useStyles = makeStyles((theme) => ({ root: { @@ -40,7 +40,7 @@ const useStyles = makeStyles((theme) => ({ left: '50%', transform: 'translateX(-50%)', '&.MuiSpeedDial-directionUp, &.MuiSpeedDial-directionLeft': { - bottom: theme.spacing(2) + 15, + bottom: theme.spacing(2) + 25, // right: theme.spacing(2), }, }, @@ -56,6 +56,8 @@ export default function LabelBottomNavigation() { const setDialogOpenAddScene = useStore((state) => state.setDialogOpenAddScene); const setDialogOpenAddDevice = useStore((state) => state.setDialogOpenAddDevice); const setDialogOpenAddVirtual = useStore((state) => state.setDialogOpenAddVirtual); + const setDialogOpenAddIntegration = useStore((state) => state.setDialogOpenAddIntegration); + const isTouch = (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) @@ -66,6 +68,10 @@ export default function LabelBottomNavigation() { { icon: , name: 'Add Scene', action: () => setDialogOpenAddScene(true) }, ]; + if (parseInt(window.localStorage.getItem('BladeMod')) > 10) { + actions.push({ icon: , name: 'Add Integration', action: () => setDialogOpenAddIntegration(true) }) + } + const handleClose = () => { setOpen(false); }; @@ -90,7 +96,7 @@ export default function LabelBottomNavigation() { } /> - - } /> - - {/* } - /> */} - + {parseInt(window.localStorage.getItem('BladeMod')) > 10 && ( + } + /> + )} - + {parseInt(window.localStorage.getItem('BladeMod')) > 10 && ( + } + /> + )} + + ariaLabel="SpeedDial example" + className={classes.speedDial} + // className={classes.fabButton} + hidden={false} + icon={} + onClose={handleClose} + onOpen={handleOpen} + open={open} + direction={"up"} + > + {actions.map((action) => ( + handleAction(action.action)} + /> + ))} + ); diff --git a/src/pages/Devices/Devices.jsx b/src/pages/Devices/Devices.jsx index de24379f..dc55b1c0 100644 --- a/src/pages/Devices/Devices.jsx +++ b/src/pages/Devices/Devices.jsx @@ -19,18 +19,15 @@ const Devices = () => { const getDisplays = useStore((state) => state.getDisplays); const displays = useStore((state) => state.displays); - - - useEffect(() => { getDisplays(); }, [getDisplays]); return (
- {displays && Object.keys(displays).map((display, i) => ( + {displays && Object.keys(displays).length ? Object.keys(displays).map((display, i) => ( - ))} + )) : (<>No devices yet)}
); }; diff --git a/src/pages/Integrations.jsx b/src/pages/Integrations.jsx deleted file mode 100644 index c5e9eed3..00000000 --- a/src/pages/Integrations.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect } from 'react'; -import useStore from '../utils/apiStore'; - -const Integrations = () => { - const getIntegrations = useStore((state) => state.getIntegrations); - const integrations = useStore((state) => state.integrations); - useEffect(() => { - getIntegrations(); - }, [getIntegrations]); - return ( -
-

Integrations

- {integrations - && Object.keys(integrations).map((s, i) =>
{s}
)} -
- ); -}; - -export default Integrations; diff --git a/src/pages/Integrations/AddIntegration.jsx b/src/pages/Integrations/AddIntegration.jsx new file mode 100644 index 00000000..c3b1bcc4 --- /dev/null +++ b/src/pages/Integrations/AddIntegration.jsx @@ -0,0 +1,199 @@ +import { useState, useEffect } from "react"; +import { makeStyles } from "@material-ui/core/styles"; + +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Select from "@material-ui/core/Select"; +import MenuItem from "@material-ui/core/MenuItem"; +import Button from "@material-ui/core/Button"; +import useStore from "../../utils/apiStore"; +import BladeSchemaFormNew from "../../components/SchemaForm/BladeSchemaFormNew"; +import { Divider } from "@material-ui/core"; + +const useStyles = makeStyles((theme) => ({ + wrapper: { + minWidth: "200px", + padding: "16px 1.2rem 6px 1.2rem", + border: "1px solid #999", + borderRadius: "10px", + position: "relative", + margin: "1rem 0", + display: "flex", + alignItems: "center", + "@media (max-width: 580px)": { + width: "100%", + margin: "0.5rem 0", + }, + "& > label": { + top: "-0.7rem", + display: "flex", + alignItems: "center", + left: "1rem", + padding: "0 0.3rem", + position: "absolute", + fontVariant: "all-small-caps", + backgroundColor: theme.palette.background.paper, + boxSizing: "border-box", + }, + }, +})); + +const AddIntegrationDialog = () => { + const classes = useStyles(); + + const getIntegrations = useStore((state) => state.getIntegrations); + + const addIntegration = useStore((state) => state.addIntegration); + const updateIntegration = useStore((state) => state.updateIntegration); + const integrations = useStore((state) => state.integrations); + + const open = useStore((state) => state.dialogs.addIntegration?.open || false); + + const integrationId = useStore((state) => state.dialogs.addIntegration?.edit || false); + const initial = integrations[integrationId] || { type: "", config: {} }; + + const setDialogOpenAddIntegration = useStore( + (state) => state.setDialogOpenAddIntegration + ); + + const integrationsTypes = useStore((state) => state.schemas?.integrations); + const showSnackbar = useStore((state) => state.showSnackbar); + const [integrationType, setIntegrationType] = useState(""); + const [model, setModel] = useState({}); + + const currentSchema = integrationType ? integrationsTypes[integrationType].schema : {}; + + const handleClose = () => { + setDialogOpenAddIntegration(false); + }; + const handleAddDevice = (e) => { + const cleanedModel = Object.fromEntries( + Object.entries(model).filter(([_, v]) => v !== "") + ); + const defaultModel = {}; + + for (const key in currentSchema.properties) { + currentSchema.properties[key].default !== undefined + ? (defaultModel[key] = currentSchema.properties[key].default) + : undefined; + } + + const valid = currentSchema.required.every((val) => + Object.keys({ ...defaultModel, ...cleanedModel }).includes(val) + ); + + if (!valid) { + showSnackbar({ + message: "Please fill in all required fields.", + messageType: "warning", + }); + } else { + if ( + initial.config && + Object.keys(initial.config).length === 0 && + initial.config.constructor === Object + ) { + // console.log("ADDING"); + addIntegration({ + type: integrationType, + config: { ...defaultModel, ...cleanedModel }, + }).then((res) => { + if (res !== "failed") { + setDialogOpenAddIntegration(false); + getIntegrations(); + } else { + } + }); + } else { + // console.log("EDITING"); + updateIntegration({ + id: integrationId, + type: integrationType, + config: { ...model }, + }).then((res) => { + if (res !== "failed") { + setDialogOpenAddIntegration(false); + getIntegrations(); + } else { + } + }); + } + } + }; + const handleTypeChange = (value, initial = {}) => { + setIntegrationType(value); + setModel(initial); + }; + const handleModelChange = (config) => { + setModel({ ...model, ...config }); + }; + + useEffect(() => { + handleTypeChange(initial.type, initial.config); + }, [initial.type]); + + return ( + + + {initial.config && + Object.keys(initial.config).length === 0 && + initial.config.constructor === Object + ? "Add" + : "Edit"}{" "} + {integrationType.toUpperCase()} Integration + + + + To add an interation to LedFx, please first select the type of integration you + wish to add then provide the necessary configuration. + +
+ + +
+ + {model && ( + + )} +
+ + + + +
+ ); +}; + +export default AddIntegrationDialog; diff --git a/src/pages/Integrations/IntegrationCard.jsx b/src/pages/Integrations/IntegrationCard.jsx new file mode 100644 index 00000000..a3357db5 --- /dev/null +++ b/src/pages/Integrations/IntegrationCard.jsx @@ -0,0 +1,239 @@ +import { useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; +import Card from '@material-ui/core/Card'; +import Icon from '@material-ui/core/Icon'; +import Button from '@material-ui/core/Button'; +import EditIcon from '@material-ui/icons/Edit'; +import AddIcon from '@material-ui/icons/Add'; +import VisibilityIcon from '@material-ui/icons/Visibility'; +import TuneIcon from '@material-ui/icons/Tune'; +import BuildIcon from '@material-ui/icons/Build'; +import { NavLink } from 'react-router-dom'; +import Wled from '../../assets/Wled'; +import useStore from '../../utils/apiStore'; +import { camelToSnake } from '../../utils/helpers'; +import Popover from '../../components/Popover'; +import EditVirtuals from '../Devices/EditVirtuals'; +import Collapse from '@material-ui/core/Collapse'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import IconButton from '@material-ui/core/IconButton'; +import { CardActions, CardHeader, Switch } from '@material-ui/core'; + +const useStyles = makeStyles((theme) => ({ + integrationCardPortrait: { + padding: '1rem', + margin: '0.5rem', + display: 'flex', + alignItems: 'flex-start', + flexDirection: 'column', + width: '290px', + justifyContent: 'space-between', + '@media (max-width: 580px)': { + width: '87vw', + height: 'unset', + } + }, + integrationLink: { + flexGrow: 0, + padding: '0 0.5rem', + textDecoration: 'none', + fontSize: 'large', + color: 'inherit', + + '&:hover': { + color: theme.palette.primary.main, + }, + }, + integrationIcon: { + margingBottom: '4px', + marginRight: '0.5rem', + position: 'relative', + fontSize: '50px', + }, + integrationCardContainer: { + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + width: '100%', + height: '100%', + justifyContent: 'space-between', + '@media (max-width: 580px)': { + flexDirection: 'row', + }, + }, + iconMedia: { + height: 140, + display: 'flex', + alignItems: 'center', + margin: '0 auto', + fontSize: 100, + '& > span:before': { + position: 'relative', + }, + }, + editButton: { + minWidth: 32, + marginLeft: theme.spacing(1), + '@media (max-width: 580px)': { + minWidth: 'unset', + }, + }, + editButtonMobile: { + minWidth: 32, + marginLeft: theme.spacing(1), + '@media (max-width: 580px)': { + minWidth: 'unset', + flexGrow: 1, + }, + }, + expand: { + display: 'none', + transform: 'rotate(0deg)', + marginLeft: 'auto', + transition: theme.transitions.create('transform', { + duration: theme.transitions.duration.shortest, + }), + '@media (max-width: 580px)': { + display: 'block' + }, + }, + expandOpen: { + transform: 'rotate(180deg)', + }, + buttonBar: { + '@media (max-width: 580px)': { + display: 'none' + }, + }, + buttonBarMobile: { + width: '100%', + textAlign: 'right', + }, + buttonBarMobileWrapper: { + display: 'flex', + margin: '0 -1rem -1rem -1rem', + padding: '0.5rem 0.5rem 1.5rem 0.5rem', + background: 'rgba(0,0,0,0.4)', + '& > div, & > button': { + flexGrow: 1, + flexBasis: '30%' + }, + '& > div > button': { + width: '100%' + } + }, +})); + +const IntegrationCard = ({ integration }) => { + + const classes = useStyles(); + const getIntegrations = useStore((state) => state.getIntegrations); + const integrations = useStore((state) => state.integrations); + const deleteIntegration = useStore((state) => state.deleteIntegration); + const toggleIntegration = useStore((state) => state.toggleIntegration); + const setDialogOpenAddIntegration = useStore((state) => state.setDialogOpenAddIntegration); + + const [expanded, setExpanded] = useState(false); + const variant = 'outlined'; + const color = 'inherit'; + + const handleExpandClick = () => { + setExpanded(!expanded); + }; + + const handleDeleteDevice = (integration) => { + deleteIntegration(integrations[integration].id).then(() => { + getIntegrations(); + }); + }; + + const handleEditIntegration = (integration) => { + setDialogOpenAddIntegration(true, integration) + }; + const handleActivateIntegration = (integration) => { + toggleIntegration({ + id: integration.id + }).then(() => getIntegrations()) + }; + + return ( + + handleActivateIntegration(integrations[integration])} /> + } + /> + +
+ + + +
+ handleDeleteDevice(integration)} + className={classes.editButton} + /> + + + +
+ +
+ + +
+ handleDeleteDevice(integration)} + className={classes.editButton} + /> + + + +
+
+
+ +
+ ) +} + +export default IntegrationCard diff --git a/src/pages/Integrations/Integrations.jsx b/src/pages/Integrations/Integrations.jsx new file mode 100644 index 00000000..b7cc161b --- /dev/null +++ b/src/pages/Integrations/Integrations.jsx @@ -0,0 +1,34 @@ +import { useEffect } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import useStore from '../../utils/apiStore'; +import IntegrationCard from './IntegrationCard'; + +const useStyles = makeStyles((theme) => ({ + cardWrapper: { + display: 'flex', flexWrap: 'wrap', margin: '-0.5rem', + }, + '@media (max-width: 580px)' : { + cardWrapper:{ + justifyContent: 'center' + } + } +})); + +const Integrations = () => { + const classes = useStyles(); + const getIntegrations = useStore((state) => state.getIntegrations); + const integrations = useStore((state) => state.integrations); + + useEffect(() => { + getIntegrations(); + }, [getIntegrations]); + return ( +
+ {integrations && Object.keys(integrations).length ? Object.keys(integrations).map((integration, i) => ( + + )) : (<>No integrations yet.)} +
+ ); +}; + +export default Integrations; diff --git a/src/pages/Scenes/Scenes.jsx b/src/pages/Scenes/Scenes.jsx index d0fc6379..26ecf1c8 100644 --- a/src/pages/Scenes/Scenes.jsx +++ b/src/pages/Scenes/Scenes.jsx @@ -93,7 +93,7 @@ const Scenes = () => { }, [getScenes]); return ( - {scenes && Object.keys(scenes).map((s, i) => ( + {scenes && Object.keys(scenes).length ? Object.keys(scenes).map((s, i) => ( handleActivateScene({ id: s })}> @@ -109,7 +109,7 @@ const Scenes = () => { - ))} + )) : (<>No Scenes yet.)} ); }; diff --git a/src/utils/apiStore.jsx b/src/utils/apiStore.jsx index c5e77b80..02aa6a4f 100644 --- a/src/utils/apiStore.jsx +++ b/src/utils/apiStore.jsx @@ -36,6 +36,10 @@ const useStore = create( open: false, edit: {}, }, + addIntegration: { + open: false, + edit: {}, + }, }, setDialogOpen: (open, edit = false) => set((state) => ({ dialogs: { @@ -73,6 +77,15 @@ const useStore = create( }, }, })), + setDialogOpenAddIntegration: (open, edit = false) => set((state) => ({ + dialogs: { + ...state.dialogs, + addIntegration: { + open, + edit, + }, + }, + })), ui: { snackbar: { isOpen: false, @@ -147,6 +160,7 @@ const useStore = create( // set({ dialogs: { nohost: { open: true } } }); } }, + displays: {}, getDisplays: async () => { const resp = await Ledfx('/api/displays', set); @@ -352,6 +366,7 @@ const useStore = create( // set({ dialogs: { nohost: { open: true } } }); } }, + scenes: {}, getScenes: async () => { const resp = await Ledfx('/api/scenes', set); @@ -411,6 +426,7 @@ const useStore = create( console.log('problems while adding Scene', resp); } }, + integrations: {}, getIntegrations: async () => { const resp = await Ledfx('/api/integrations', set); @@ -420,6 +436,67 @@ const useStore = create( // set({ dialogs: { nohost: { open: true } } }); } }, + addIntegration: async (config) => { + const resp = await Ledfx( + `/api/integrations`, + set, + 'POST', + config, + ); + if (resp) { + // set({ presets: resp.preset }); + console.log(resp); + return resp; + } else { + // set({ dialogs: { nohost: { open: true } } }); + } + }, + updateIntegration: async (config) => { + const resp = await Ledfx( + `/api/integrations`, + set, + 'POST', + config, + ); + if (resp) { + // set({ presets: resp.preset }); + console.log(resp); + return resp; + } else { + // set({ dialogs: { nohost: { open: true } } }); + } + }, + toggleIntegration: async (config) => { + const resp = await Ledfx( + `/api/integrations`, + set, + 'PUT', + config, + ); + if (resp) { + // set({ presets: resp.preset }); + console.log(resp); + return resp; + } else { + // set({ dialogs: { nohost: { open: true } } }); + } + }, + deleteIntegration: async (config) => { + console.log("YZ", config) + const resp = await Ledfx( + `/api/integrations`, + set, + 'DELETE', + {data: {id: config}} + ); + if (resp) { + // set({ presets: resp.preset }); + console.log(resp); + } else { + // set({ dialogs: { nohost: { open: true } } }); + } + }, + schemas: {}, getSchemas: async () => { const resp = await Ledfx('/api/schema', set); @@ -429,6 +506,7 @@ const useStore = create( // set({ dialogs: { nohost: { open: true } } }); } }, + config: {}, getSystemConfig: async () => { const resp = await Ledfx('/api/config', set); @@ -488,26 +566,12 @@ const useStore = create( const resp = await Ledfx('/api/find_devices', set, 'POST', {}); if (resp && resp.status === 'success') { console.log(resp) - } - // if (resp && resp.status === 'success') { - // set({ - // settings: get().settings, - // ...{ - // "settings": { - // audio_inputs: get().settings.audio_inputs, - // "active_device_index": parseInt(index), - // } - // }, - - // }); - // } - else { + } else { set({ dialogs: { nohost: { open: true } } }); } }, - togglePause: async () => { const resp = await Ledfx('/api/displays', set, 'PUT', {}); if (resp && resp.data) {