diff --git a/src/Components/Cards/Base.tsx b/src/Components/Cards/Base.tsx index e61a55047..078abd417 100644 --- a/src/Components/Cards/Base.tsx +++ b/src/Components/Cards/Base.tsx @@ -1,21 +1,27 @@ // @flow -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import PropTypes from 'prop-types'; import { makeStyles, Theme, useTheme } from '@material-ui/core/styles'; import ButtonBase from '@material-ui/core/ButtonBase'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; +import Dialog from '@material-ui/core/Dialog'; import Grid from '@material-ui/core/Grid'; import IconButton from '@material-ui/core/IconButton'; import Typography from '@material-ui/core/Typography'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; import ArrowDownwardsIcon from '@material-ui/icons/ArrowDownward'; import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; +import CloseIcon from '@material-ui/icons/Close'; import DeleteIcon from '@material-ui/icons/Delete'; import EditIcon from '@material-ui/icons/Edit'; import { CardProps } from '../Configuration/Config'; -import { HomeAssistantChangeProps } from '../HomeAssistant/HomeAssistant'; +import { + HomeAssistantChangeProps, + entitySizes +} from '../HomeAssistant/HomeAssistant'; import ConfirmDialog from '../Utils/ConfirmDialog'; import EditCard from '../Configuration/EditCard/EditCard'; import Entity from '../HomeAssistant/Cards/Entity'; @@ -27,6 +33,11 @@ const useStyles = makeStyles((theme: Theme) => ({ root: { overflow: 'visible' }, + buttonExpand: { + position: 'absolute', + top: theme.spacing(1.2), + right: theme.spacing(0.8) + }, card: { flex: 1 }, @@ -61,7 +72,7 @@ const useStyles = makeStyles((theme: Theme) => ({ margin: 4 }, title: { - minHeight: 20, + minHeight: theme.spacing(3), fontWeight: 400, lineHeight: 1.2 }, @@ -75,15 +86,116 @@ export interface BaseProps HomeAssistantChangeProps { card: CardProps; editing: number; + expandable: boolean; + handleCloseExpand?: () => void; handleDelete?: () => void; handleMoveDown?: () => void; handleMoveUp?: () => void; handleUpdate: (data: CardProps) => void; } +let holdTimeout: NodeJS.Timeout; function Base(props: BaseProps) { const [deleteConfirm, setDeleteConfirm] = React.useState(false); const [editCard, setEditCard] = React.useState(false); + const [expandable, setExpandable] = React.useState(false); + const [expandCard, setExpandCard] = React.useState(false); + const [height, setHeight] = React.useState(); + const [width, setWidth] = React.useState(); + const [toggleable, setToggleable] = React.useState(); + + const classes = useStyles(); + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('xs')); + + const handleSetHeight = useCallback( + (cardSize: number, entitySizeKey?: string) => { + let h = + !props.expandable && entitySizeKey + ? entitySizes[entitySizeKey].height * cardSize + : props.editing === 2 || !props.card.height + ? 'initial' + : isNaN(props.card.height) + ? props.card.height + : props.card.height * cardSize || cardSize; + if (h) setHeight(h); + }, + [props.card.height, props.editing, props.expandable] + ); + + const handleSetWidth = useCallback( + (cardSize: number, entitySizeKey?: string) => { + let w = + !props.expandable && entitySizeKey + ? entitySizes[entitySizeKey].width * cardSize + : props.editing === 2 || !props.card.width + ? -1 + : props.card.width * cardSize || cardSize; + if (w !== cardSize) { + // Adjust for margins + w = props.card.width + ? w + (props.card.width - 1) * theme.spacing(1) + : w; + } + if (w) setWidth(w); + }, + [props.card.width, props.editing, props.expandable, theme] + ); + + const handleSetToggleable = useCallback(() => { + setToggleable( + props.editing === 1 + ? false + : !props.card.disabled && props.card.toggleable + ); + }, [props.card.disabled, props.card.toggleable, props.editing]); + + const handleSetExpandable = useCallback( + (entitySizeKey: string) => { + if (entitySizeKey) + if (props.card.height && props.card.width) + setExpandable( + props.card.height < entitySizes[entitySizeKey].height || + props.card.width < entitySizes[entitySizeKey].width + ); + else if (props.card.width) + setExpandable(props.card.width < entitySizes[entitySizeKey].width); + }, + [props.card.height, props.card.width] + ); + + useEffect(() => { + const cardSize = theme.breakpoints.down('sm') ? 140 : 120; + + const entitySizeKey = + props.card.entity && + Object.keys(entitySizes).find( + (domain: string) => domain === props.card.entity!.split('.')[0] + ); + handleSetHeight(cardSize, entitySizeKey); + handleSetWidth(cardSize, entitySizeKey); + + handleSetToggleable(); + + if ( + props.expandable && + props.card.type === 'entity' && + props.card.entity && + entitySizeKey + ) + handleSetExpandable(entitySizeKey); + }, [ + props.card.entity, + props.card.type, + props.expandable, + handleSetExpandable, + handleSetHeight, + handleSetToggleable, + handleSetWidth, + height, + theme.breakpoints, + width + ]); function handleDeleteConfirm() { setDeleteConfirm(true); @@ -125,27 +237,26 @@ function Base(props: BaseProps) { setEditCard(false); } - const classes = useStyles(); - const theme = useTheme(); + function handleExpand() { + setExpandCard(true); + } - const cardSize = theme.breakpoints.down('sm') ? 140 : 120; + function handleCloseExpand() { + setExpandCard(false); + } - let height = - props.editing === 2 || !props.card.height - ? 'initial' - : isNaN(props.card.height) - ? props.card.height - : props.card.height! * cardSize || cardSize; - if (props.card.title === 'test') console.log('height:', height); - let width = - props.editing === 2 ? -1 : props.card.width! * cardSize || cardSize; - if (width !== cardSize) { - // Adjust for margins - width = props.card.width! * theme.spacing(1) + width; + function handleHold() { + if (expandable) { + handleHoldCancel(); + holdTimeout = setTimeout(() => { + handleExpand(); + }, 1000); + } } - const toggleable = - props.editing === 1 ? false : !props.card.disabled && props.card.toggleable; + function handleHoldCancel() { + if (holdTimeout) clearTimeout(holdTimeout); + } return ( @@ -157,7 +268,10 @@ function Base(props: BaseProps) { cursor: !toggleable ? 'unset' : 'pointer', userSelect: !toggleable ? 'text' : 'none' }} - onClick={toggleable ? handleHassToggle : undefined}> + onClick={toggleable ? handleHassToggle : undefined} + onMouseDown={handleHold} + onMouseUp={handleHoldCancel} + onMouseLeave={handleHoldCancel}> - {props.card.title && ( - - {props.card.title} - - )} + + + {props.card.title && ( + + {props.card.title} + + )} + + + {!props.expandable && ( + + + + )} + + {props.card.type === 'entity' && ( + {expandCard && ( + + + + + + )} ); } diff --git a/src/Components/Configuration/Config.tsx b/src/Components/Configuration/Config.tsx index e1e66344e..f48d757f9 100644 --- a/src/Components/Configuration/Config.tsx +++ b/src/Components/Configuration/Config.tsx @@ -61,7 +61,7 @@ export type CardProps = { key: string; group: string; type: string; - width: number; + width?: number; height?: number; square?: boolean; padding?: number | string; diff --git a/src/Components/Configuration/EditCard/EditCard.tsx b/src/Components/Configuration/EditCard/EditCard.tsx index 6869bd6f9..cc1749fb6 100644 --- a/src/Components/Configuration/EditCard/EditCard.tsx +++ b/src/Components/Configuration/EditCard/EditCard.tsx @@ -148,7 +148,7 @@ function EditCard(props: EditCardProps) { container alignContent="center" justify="space-around"> - + diff --git a/src/Components/HomeAssistant/Cards/AlarmPanel.tsx b/src/Components/HomeAssistant/Cards/AlarmPanel.tsx index 861349e16..2fe316eb7 100644 --- a/src/Components/HomeAssistant/Cards/AlarmPanel.tsx +++ b/src/Components/HomeAssistant/Cards/AlarmPanel.tsx @@ -104,74 +104,76 @@ function AlarmPanel(props: AlarmPanelProps) { {properCase(entity.state)} - {props.card.width > 1 && ( - - {!armed && ( - - - - )} - {!armed && ( - - - - )} - {armed && ( - - - - )} - - )} - {attributes.code_arm_required && props.card.width > 1 && ( - - - - )} + {!props.card.width || + (props.card.width > 1 && ( + + {!armed && ( + + + + )} + {!armed && ( + + + + )} + {armed && ( + + + + )} + + ))} + {attributes.code_arm_required && + (!props.card.width || props.card.width > 1) && ( + + + + )} {attributes.code_arm_required && - props.card.width > 1 && - props.card.height! > 1 && ( + (!props.card.width || props.card.width > 1) && + (!props.card.height || props.card.height > 1) && ( - {props.card.width > 1 && ( + {(!props.card.width || props.card.width > 1) && ( {attributes.temperature ? ( )} - {props.card.width > 1 && props.card.height! > 1 && ( - - {attributes.hvac_modes.map((mode: string, key: number) => { - let icon: string | undefined = - mode === 'off' - ? 'mdi-power' - : mode === 'heat' - ? 'mdi-fire' - : mode === 'cool' - ? 'mdi-snowflake' - : mode === 'heat_cool' - ? 'mdi-autorenew' - : mode === 'auto' - ? 'mdi-calendar-repeat' - : mode === 'dry' - ? 'mdi-water-percent' - : mode === 'fan_only' - ? 'mdi-fan' - : undefined; - if (icon) + {(!props.card.width || props.card.width > 1) && + (!props.card.height || props.card.height > 1) && ( + + {attributes.hvac_modes.map((mode: string, key: number) => { + let icon: string | undefined = + mode === 'off' + ? 'mdi-power' + : mode === 'heat' + ? 'mdi-fire' + : mode === 'cool' + ? 'mdi-snowflake' + : mode === 'heat_cool' + ? 'mdi-autorenew' + : mode === 'auto' + ? 'mdi-calendar-repeat' + : mode === 'dry' + ? 'mdi-water-percent' + : mode === 'fan_only' + ? 'mdi-fan' + : undefined; + if (icon) + return ( + + handleHvacChange(mode)}> + + + + ); return ( - handleHvacChange(mode)}> + {mode} + + + ); + })} + {attributes.away_mode && ( + + + handleAwayToggle()}> - ); - return ( - - - ); - })} - {attributes.away_mode && ( - - - handleAwayToggle()}> - - - - - )} - - )} + )} + + )} ); } diff --git a/src/Components/HomeAssistant/Cards/Cover.tsx b/src/Components/HomeAssistant/Cards/Cover.tsx index 17e675217..6cd9fc699 100644 --- a/src/Components/HomeAssistant/Cards/Cover.tsx +++ b/src/Components/HomeAssistant/Cards/Cover.tsx @@ -137,7 +137,7 @@ function Cover(props: CoverProps) { {attributes.current_tilt_position !== undefined && - props.card.width > 1 ? ( + (!props.card.width || props.card.width > 1) ? ( + justify="center" + spacing={1}> )} - {controls - .splice(0, props.card.height! > 1 ? props.card.height! : 0) - .map(control => control)} + {!props.card.height + ? controls + : controls + .splice(0, props.card.height > 1 ? props.card.height : 0) + .map(control => control)} ); } diff --git a/src/Components/HomeAssistant/Cards/Media.tsx b/src/Components/HomeAssistant/Cards/Media.tsx index 9e85c7a46..226f67df9 100644 --- a/src/Components/HomeAssistant/Cards/Media.tsx +++ b/src/Components/HomeAssistant/Cards/Media.tsx @@ -130,7 +130,7 @@ function Media(props: MediaProps) { return (
- {props.card.height! > 1 && ( + {(!props.card.height || props.card.height > 1) && (
{attributes.media_title && ( @@ -142,7 +142,7 @@ function Media(props: MediaProps) { )}
)} - {props.card.height! > 1 && ( + {(!props.card.height || props.card.height > 1) && (
- {props.card.width > 1 && state !== 'off' && ( + {(!props.card.width || props.card.width > 1) && state !== 'off' && ( handleChange('next')}> - {props.card.width > 1 && state !== 'off' && ( + {(!props.card.width || props.card.width > 1) && state !== 'off' && ( 1 ? 4 : 12} + xs={!props.card.width || props.card.width > 1 ? 4 : 12} container direction="column" alignContent="center" @@ -211,7 +211,9 @@ function Weather(props: WeatherProps) { - {props.card.width > 1 || props.card.height! > 1 ? ( + {!props.card.width || + props.card.width > 1 || + (!props.card.height || props.card.height > 1) ? ( {Object.keys(attributes) .filter(i => typeof attributes[i] == 'number') @@ -232,52 +234,53 @@ function Weather(props: WeatherProps) { ) : null} - {props.card.width > 1 && props.card.height! > 1 && ( - - {attributes.forecast.map((w: object | any, key: number) => { - const datetime = moment(w.datetime); - const icon = weatherMap[w.condition]; - return ( -
- - {datetime.format('ddd')} -
- {datetime.format('h a')} -
+ {(!props.card.width || props.card.width > 1) && + (!props.card.height || props.card.height > 1) && ( + + {attributes.forecast.map((w: object | any, key: number) => { + const datetime = moment(w.datetime); + const icon = weatherMap[w.condition]; + return ( +
+ + {datetime.format('ddd')} +
+ {datetime.format('h a')} +
- - - + + + - - {w.temperature} - {getUnit('temperature')} - - - {w.precipitation} - -
- ); - })} -
- )} + + {w.temperature} + {getUnit('temperature')} + + + {w.precipitation} + +
+ ); + })} +
+ )} ); } diff --git a/src/Components/HomeAssistant/HomeAssistant.tsx b/src/Components/HomeAssistant/HomeAssistant.tsx index 8d6ade9ec..7e50640d1 100644 --- a/src/Components/HomeAssistant/HomeAssistant.tsx +++ b/src/Components/HomeAssistant/HomeAssistant.tsx @@ -41,6 +41,32 @@ export interface HomeAssistantChangeProps extends HomeAssistantEntityProps { ) => void; } +export const entitySizes: { + [key: string]: { height: number; width: number }; +} = { + air_quality: { height: 1, width: 1 }, + alarm_control_panel: { height: 2, width: 2 }, + binary_sensor: { height: 1, width: 1 }, + camera: { height: 1, width: 1 }, + climate: { height: 2, width: 2 }, + cover: { height: 1, width: 2 }, + device_tracker: { height: 1, width: 1 }, + fan: { height: 1, width: 2 }, + geo_location: { height: 1, width: 1 }, + input_boolean: { height: 1, width: 1 }, + input_select: { height: 1, width: 1 }, + light: { height: 3, width: 2 }, + lock: { height: 1, width: 1 }, + media_player: { height: 2, width: 2 }, + remote: { height: 1, width: 1 }, + scene: { height: 1, width: 1 }, + script: { height: 1, width: 1 }, + sensor: { height: 1, width: 1 }, + sun: { height: 1, width: 1 }, + switch: { height: 1, width: 1 }, + weather: { height: 2, width: 3 } +}; + let connection: Connection, auth: Auth; export function loadTokens() { diff --git a/src/Components/Overview/Overview.tsx b/src/Components/Overview/Overview.tsx index e903528d9..dbb8f01fa 100644 --- a/src/Components/Overview/Overview.tsx +++ b/src/Components/Overview/Overview.tsx @@ -234,7 +234,7 @@ function Overview(props: OverviewProps) { justify="flex-start" alignContent="flex-start" style={{ - width: groupWidth * group.width + theme.spacing(6.5) + width: group.width * (groupWidth + theme.spacing(2)) }}>