Compare commits

...

6 Commits

Author SHA1 Message Date
4d1254753b Fixed React issue.
All checks were successful
homepanel/HomePanel/pipeline/head This commit looks good
2026-02-13 01:48:22 +00:00
f96c6e792f Improve animation timing. 2026-02-13 01:48:12 +00:00
c614354c09 Refactor dashboard. 2026-02-13 01:48:04 +00:00
6ec351a8e5 Fixed overflow. 2026-02-13 01:47:55 +00:00
69998ac347 Use correct animated collapse. 2026-02-13 01:46:18 +00:00
ec754fbaa4 Changed title transform origin. 2026-02-13 01:35:39 +00:00
5 changed files with 206 additions and 180 deletions

View File

@ -23,7 +23,7 @@ import EditWidget from "./admin/EditWidget";
import SlideView from "./dashboard/controls/SlideView";
import DashboardFlex from "./dashboard/controls/DashboardFlex";
import { useSearchParams } from "react-router-dom";
import AnimatedCollapse from "./admin/controls/AnimatedCollapse";
import AnimatedCollapse from "./dashboard/controls/AnimatedCollapse";
import EntityDashboard from "./dashboard/controls/EntityDashboard";
import { WidgetGroup } from "./dashboard/controls/WidgetGroup";
@ -37,15 +37,12 @@ function Dashboard({
onChange,
onSave,
}) {
const {
data,
subscribeToPanelUpdate,
isConnected,
connectionStatus,
} = useWebSocket();
const { data, subscribeToPanelUpdate, isConnected, connectionStatus } =
useWebSocket();
const deviceContext = useDeviceSafe();
const panelSelectorVisible = deviceContext?.panelSelectorVisible ?? false;
const setPanelSelectorVisible = deviceContext?.setPanelSelectorVisible ?? (() => {});
const setPanelSelectorVisible =
deviceContext?.setPanelSelectorVisible ?? (() => {});
const isInDeviceContext = !!deviceContext;
const hpAppContext = useHPAppSafe();
@ -71,7 +68,7 @@ function Dashboard({
const [isEntityDashboardOpen, setIsEntityDashboardOpen] = useState(false);
const [entityDashboardEntity, setEntityDashboardEntity] = useState(null);
const [isPreview, setIsPreview] = useState(false);
// Set device brightness via native app context instead of URL query
useEffect(() => {
if (editMode == true) {
@ -154,12 +151,10 @@ function Dashboard({
const currentEntities = panel.entities || [];
const exists = currentEntities.some(
(e) => (typeof e === "string" ? e : e.entityId) === entity.entityId
(e) => (typeof e === "string" ? e : e.entityId) === entity.entityId,
);
if (!exists) {
onChange({
...panel,
entities: [...currentEntities, entity],
@ -178,7 +173,9 @@ function Dashboard({
// Generate a unique ID for the widget if it doesn't have one
const widgetWithId = {
...widget,
id: widget.id || `widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
id:
widget.id ||
`widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
};
onChange({
...panel,
@ -192,7 +189,7 @@ function Dashboard({
const currentEntities = panel.entities || [];
const updatedEntities = currentEntities.filter(
(e) => (typeof e === "string" ? e : e.entityId) !== entity.entityId
(e) => (typeof e === "string" ? e : e.entityId) !== entity.entityId,
);
onChange({
@ -235,7 +232,7 @@ function Dashboard({
setLocalEntities(reorderedEntities);
setUnsavedChanges(true);
},
[panel, onChange]
[panel, onChange],
);
const handleDeleteWidget = useCallback(
@ -255,7 +252,7 @@ function Dashboard({
});
setUnsavedChanges(true);
},
[panel, onChange]
[panel, onChange],
);
const handleWidgetEdit = useCallback((widget) => {
@ -263,28 +260,31 @@ function Dashboard({
setIsEditWidgetModalOpen(true);
}, []);
const handleWidgetUpdate = useCallback((updatedWidget) => {
if (!updatedWidget || !panel || !onChange) return;
const handleWidgetUpdate = useCallback(
(updatedWidget) => {
if (!updatedWidget || !panel || !onChange) return;
const currentWidgets = panel.widgets || [];
const widgetId = updatedWidget.id || updatedWidget.type;
const updatedWidgets = currentWidgets.map((w) => {
const wId = w.id || w.type;
if (wId === widgetId) {
return {
...w,
...updatedWidget,
};
}
return w;
});
const currentWidgets = panel.widgets || [];
const widgetId = updatedWidget.id || updatedWidget.type;
const updatedWidgets = currentWidgets.map((w) => {
const wId = w.id || w.type;
if (wId === widgetId) {
return {
...w,
...updatedWidget,
};
}
return w;
});
onChange({
...panel,
widgets: updatedWidgets,
});
setUnsavedChanges(true);
}, [panel, onChange]);
onChange({
...panel,
widgets: updatedWidgets,
});
setUnsavedChanges(true);
},
[panel, onChange],
);
const handleReorderWidgets = useCallback(
(reorderedWidgets) => {
@ -296,7 +296,7 @@ function Dashboard({
});
setUnsavedChanges(true);
},
[panel, onChange]
[panel, onChange],
);
const handleCancel = () => {
@ -351,136 +351,159 @@ function Dashboard({
return (
<>
<div ref={dashboardRef} style={{ width, height }}>
<DashboardContainer
width="100%"
height="100%"
bordered={editMode && !isFullscreen}
colors={colors}
showColors={connectionStatus == "connected"}
editTools={
editMode && (
<DashboardEditTools
onAddWidget={() => setIsAddWidgetModalOpen(true)}
onAddEntity={() => setIsAddEntityModalOpen(true)}
onSave={() => {
onSave?.(panel);
setUnsavedChanges(false);
}}
onCancel={handleCancel}
onFullscreen={toggleFullscreen}
isFullscreen={isFullscreen}
unsavedChanges={unsavedChanges}
isPreview={isPreview}
onPreview={() => setIsPreview(!isPreview)}
/>
)
}
>
<div
className={`dashboard-content${
!isContentVisible ? " dashboard-content-hidden" : ""
} ${
isEntityDashboardOpen ? " dashboard-content-entity-dashboard-open" : ""
}`}
>
<div style={{ position: "relative" }}>
<DashboardFlex style={{ marginBottom: "35px" }}>
<DashboardTitle onPanelSelect={isInDeviceContext ? () => {
setPanelSelectorVisible(!panelSelectorVisible);
} : undefined}>
{title
}</DashboardTitle>
<WidgetGroup
widgets={panel?.widgets || []}
panelName={title}
containerId="widgets-container"
editMode={editMode ? !isPreview : false}
onDelete={handleDeleteWidget}
onEdit={handleWidgetEdit}
onUpdate={handleWidgetUpdate}
onReorder={handleReorderWidgets}
isContentVisible={isContentVisible}
/>
</DashboardFlex>
{isInDeviceContext && (
<DashboardPanelSelector
isOpen={panelSelectorVisible}
onClose={() => setPanelSelectorVisible(false)}
onPanelSelect={(selectedPanel, index) => {
// Panel selection is handled by DeviceContext via setCurrentPanel
setPanelSelectorVisible(false);
<div ref={dashboardRef} style={{ width, height }}>
<DashboardContainer
width="100%"
height="100%"
bordered={editMode && !isFullscreen}
colors={colors}
showColors={connectionStatus == "connected"}
editTools={
editMode && (
<DashboardEditTools
onAddWidget={() => setIsAddWidgetModalOpen(true)}
onAddEntity={() => setIsAddEntityModalOpen(true)}
onSave={() => {
onSave?.(panel);
setUnsavedChanges(false);
}}
onCancel={handleCancel}
onFullscreen={toggleFullscreen}
isFullscreen={isFullscreen}
unsavedChanges={unsavedChanges}
isPreview={isPreview}
onPreview={() => setIsPreview(!isPreview)}
/>
)}
)
}
>
<div
className={`dashboard-content${
!isContentVisible ? " dashboard-content-hidden" : ""
} ${
isEntityDashboardOpen
? " dashboard-content-entity-dashboard-open"
: ""
}`}
>
<div style={{ position: "relative" }}>
<DashboardFlex style={{ marginBottom: "35px" }}>
<DashboardTitle
onPanelSelect={
isInDeviceContext
? () => {
setPanelSelectorVisible(!panelSelectorVisible);
}
: undefined
}
>
{title}
</DashboardTitle>
<WidgetGroup
widgets={panel?.widgets || []}
panelName={title}
containerId="widgets-container"
editMode={editMode ? !isPreview : false}
onDelete={handleDeleteWidget}
onEdit={handleWidgetEdit}
onUpdate={handleWidgetUpdate}
onReorder={handleReorderWidgets}
isContentVisible={isContentVisible}
/>
</DashboardFlex>
{isInDeviceContext && (
<DashboardPanelSelector
isOpen={panelSelectorVisible}
onClose={() => setPanelSelectorVisible(false)}
onPanelSelect={(selectedPanel, index) => {
// Panel selection is handled by DeviceContext via setCurrentPanel
setPanelSelectorVisible(false);
}}
/>
)}
</div>
<ConnectionStatus showIfConnected={false} />
<AnimatedCollapse
visible={connectionStatus == "connected" && !panelSelectorVisible}
style={{ overflow: "visible" }}
>
<SlideView ref={slideViewRef} className="views-container">
{/* Main View: All Entities */}
<SlideView.Item>
<EntityGroupContainer>
<EntityGroup
entities={entities}
type="controls"
containerId="controls-container"
editMode={editMode ? !isPreview : false}
panelName={title}
onDelete={handleDeleteEntity}
onUpdate={handleEntityUpdate}
onReorder={handleReorderEntities}
onShowDashboard={handleOpenEntityDashboard}
isContentVisible={isContentVisible}
/>
<EntityGroup
entities={entities}
type="sensors"
containerId="sensors-container"
onClick={() => {}}
editMode={editMode ? !isPreview : false}
panelName={title}
onDelete={handleDeleteEntity}
onUpdate={handleEntityUpdate}
onReorder={handleReorderEntities}
isContentVisible={isContentVisible}
/>
</EntityGroupContainer>
</SlideView.Item>
{/* Media Player Detail Views */}
{entities
.filter((e) => {
const entityId = e.entityId;
return entityId?.startsWith("media_player.");
})
.map((entity) => {
const entityId = entity.entityId;
return (
<SlideView.Item key={`slide-${entityId}`}>
<MediaPlayerSlide entity={entity} />
</SlideView.Item>
);
})}
</SlideView>
</AnimatedCollapse>
</div>
<ConnectionStatus showIfConnected={false} />
<AnimatedCollapse visible={connectionStatus == "connected" && !panelSelectorVisible} style={{ overflow: "visible" }}>
<SlideView ref={slideViewRef} className="views-container">
{/* Main View: All Entities */}
<SlideView.Item>
<EntityGroupContainer>
<EntityGroup
entities={entities}
type="controls"
containerId="controls-container"
editMode={editMode ? !isPreview : false}
panelName={title}
onDelete={handleDeleteEntity}
onUpdate={handleEntityUpdate}
onReorder={handleReorderEntities}
onShowDashboard={handleOpenEntityDashboard}
isContentVisible={isContentVisible}
/>
<EntityGroup
entities={entities}
type="sensors"
containerId="sensors-container"
onClick={() => {}}
editMode={editMode ? !isPreview : false}
panelName={title}
onDelete={handleDeleteEntity}
onUpdate={handleEntityUpdate}
onReorder={handleReorderEntities}
isContentVisible={isContentVisible}
/>
</EntityGroupContainer>
</SlideView.Item>
{/* Media Player Detail Views */}
{entities
.filter((e) => {
const entityId = e.entityId;
return entityId?.startsWith("media_player.");
})
.map((entity) => {
const entityId = entity.entityId;
return (
<SlideView.Item key={`slide-${entityId}`}>
<MediaPlayerSlide entity={entity} />
</SlideView.Item>
);
})}
</SlideView>
</AnimatedCollapse>
</div>
</DashboardContainer>
</div>
<AddEntity isOpen={isAddEntityModalOpen} onClose={() => setIsAddEntityModalOpen(false)} onAdd={handleAddEntity} />
<AddWidget isOpen={isAddWidgetModalOpen} onClose={() => setIsAddWidgetModalOpen(false)} onAdd={handleAddWidget} />
<EditWidget
isOpen={isEditWidgetModalOpen}
onClose={() => {
setIsEditWidgetModalOpen(false);
setEditingWidget(null);
}}
widget={editingWidget}
onUpdate={handleWidgetUpdate}
/>
<EntityDashboard isOpen={isEntityDashboardOpen} onClose={handleCloseEntityDashboard} entity={entityDashboardEntity} />
</DashboardContainer>
</div>
<AddEntity
isOpen={isAddEntityModalOpen}
onClose={() => setIsAddEntityModalOpen(false)}
onAdd={handleAddEntity}
/>
<AddWidget
isOpen={isAddWidgetModalOpen}
onClose={() => setIsAddWidgetModalOpen(false)}
onAdd={handleAddWidget}
/>
<EditWidget
isOpen={isEditWidgetModalOpen}
onClose={() => {
setIsEditWidgetModalOpen(false);
setEditingWidget(null);
}}
widget={editingWidget}
onUpdate={handleWidgetUpdate}
/>
<EntityDashboard
isOpen={isEntityDashboardOpen}
onClose={handleCloseEntityDashboard}
entity={entityDashboardEntity}
/>
</>
);
);
}
export default Dashboard;

View File

@ -117,7 +117,7 @@ function DashboardCarousel({
console.log("transitioning", isTransitioning);
setIsTransitioning(false);
timeoutRef.current = null;
}, transitionDuration);
}, transitionDuration + 500);
}, [currentPanelIndex, panels, activeSlot, slot1, slot2, transitionDuration]);
useEffect(() => {
@ -199,7 +199,7 @@ function DashboardCarousel({
width,
height,
position: "relative",
"--dashboard-carousel-duration": `${transitionDuration}ms`,
"--dashboard-carousel-duration": `${transitionDuration - 500}ms`,
}}
>
<div className="dashboard-carousel-item dashboard-carousel-visible">
@ -225,7 +225,7 @@ function DashboardCarousel({
width,
height,
position: "relative",
"--dashboard-carousel-duration": `${transitionDuration}ms`,
"--dashboard-carousel-duration": `${transitionDuration - 500}ms`,
}}
>
{slot1 && (

View File

@ -1,5 +1,7 @@
.animated-collapse {
transition: opacity 150ms ease-in-out, transform 150ms ease-in-out,
transition:
opacity 150ms ease-in-out,
transform 150ms ease-in-out,
max-height 150ms ease-in-out;
transform: translateY(10px);
opacity: 0;
@ -10,6 +12,7 @@
.animated-collapse.visible {
opacity: 1;
transform: translateY(0);
overflow: visible;
}
.animated-collapse.hidden {

View File

@ -7,7 +7,7 @@
.dashboard-title-clickable {
cursor: pointer;
transition: transform 0.2s ease;
transform-origin: left center;
transform-origin: center center;
}
.dashboard-title-clickable:active {

View File

@ -108,21 +108,21 @@ export const Entity = ({
// onChange handler called when WebSocket updates the entity
const onChange = useCallback(
(updatedEntity) => {
setEntityState((prev) => {
const newState = {
...prev,
...updatedEntity,
};
// Call onUpdate callback when entity is updated via WebSocket
if (onUpdate) {
onUpdate(newState);
}
return newState;
});
setEntityState((prev) => ({
...prev,
...updatedEntity,
}));
},
[onUpdate]
[]
);
// Call onUpdate callback after entityState changes (outside of render)
useEffect(() => {
if (onUpdate) {
onUpdate(entityState);
}
}, [entityState, onUpdate]);
// Subscribe to entity updates
// Including isConnected as a dependency ensures we resubscribe on reconnection
useEffect(() => {