mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-14 18:58:49 -05:00
undo undo
This commit is contained in:
@@ -3,26 +3,29 @@ import * as FaIcons from 'react-icons/fa';
|
||||
import { FaCube, FaLock } from 'react-icons/fa';
|
||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext';
|
||||
import CardFrame from '../CardFrame';
|
||||
// Each control keeps its own JSX file; this panel only selects its renderer.
|
||||
import ReadOnlyControl from './controls/ReadOnlyControl';
|
||||
import ToggleControl from './controls/ToggleControl';
|
||||
import ActionControls from './ActionControls';
|
||||
import NumberControl from './controls/NumberControl';
|
||||
import TextControl from './controls/TextControl';
|
||||
import SelectControl from './controls/SelectControl';
|
||||
import ButtonControl from './controls/ButtonControl';
|
||||
|
||||
function ActivityTile({ entity, connected, controlsReady, allowed, admin }) {
|
||||
const controls = { readOnly: ReadOnlyControl, toggle: ToggleControl, number: NumberControl, text: TextControl, select: SelectControl, button: ButtonControl };
|
||||
|
||||
|
||||
function ActivityTile({ entity, connected, allowed, admin }) {
|
||||
const { homeAssistantActivityAct } = useSessionActions();
|
||||
const onAction = useCallback((action, values) => homeAssistantActivityAct(entity.id, action, values), [homeAssistantActivityAct, entity.id]);
|
||||
// Icon/color inputs follow social-link conventions. A translucent fill keeps
|
||||
// text legible even with bright colors and preserves the surrounding theme.
|
||||
const onChange = useCallback((value) => homeAssistantActivityAct(entity.id, value), [homeAssistantActivityAct, entity.id]);
|
||||
// Resolve precisely the same Font Awesome names accepted by social links.
|
||||
// Unknown names remain usable with a neutral fallback rather than an error.
|
||||
const candidate = FaIcons[entity.icon?.trim()];
|
||||
const Icon = typeof candidate === 'function' ? candidate : FaCube;
|
||||
// Translucent fills preserve text contrast; an amber border still identifies
|
||||
// locked tiles regardless of the admin's chosen color.
|
||||
const color = /^#[0-9a-f]{6}$/i.test(entity.color) ? entity.color : '#3b82f6';
|
||||
const disabled = !connected || !controlsReady || !entity.available || !allowed || (entity.locked && !admin);
|
||||
const domain = entity.id.split('.')[0];
|
||||
const turnOn = entity.actions.find((action) => action.domain === domain && action.service === 'turn_on' && !action.fields.some((field) => field.required));
|
||||
const turnOff = entity.actions.find((action) => action.domain === domain && action.service === 'turn_off' && !action.fields.some((field) => field.required));
|
||||
const power = turnOn && turnOff;
|
||||
// Pair the universal on/off actions into one control. Other capabilities
|
||||
// continue to be rendered from metadata, including optional turn_on inputs.
|
||||
const actions = entity.actions.filter((action) => !(power && action.service === 'toggle'));
|
||||
const Control = controls[entity.type] || ReadOnlyControl;
|
||||
const disabled = !connected || !entity.available || !allowed || (entity.locked && !admin);
|
||||
return <div className="flex min-w-0 flex-col gap-0.5 rounded border px-0.5 py-0.5"
|
||||
style={{ borderColor: entity.locked ? '#b45309' : `${color}aa`, backgroundColor: `${color}33` }}>
|
||||
<div className="-mx-0.5 -mt-0.5 flex min-h-5 min-w-0 items-center gap-0.5 rounded-t px-0.5 py-0.5 text-white" style={{ backgroundColor: `${color}66` }}>
|
||||
@@ -30,22 +33,8 @@ function ActivityTile({ entity, connected, controlsReady, allowed, admin }) {
|
||||
<span title={entity.name} className="min-w-0 flex-1 truncate text-[0.78rem] font-semibold leading-none">{entity.name}</span>
|
||||
{entity.locked ? <span title={admin ? 'Locked for users; admins can still control' : 'Locked'} className="flex items-center gap-0.5 text-[0.65rem] text-amber-200"><FaLock aria-hidden="true" />Locked</span> : null}
|
||||
</div>
|
||||
{/* On/off is already represented by the power control, and stateless
|
||||
actions need no misleading unknown-state label beside their button. */}
|
||||
{(!power || !['on', 'off'].includes(entity.state)) && (entity.state !== 'unknown' || !entity.actions.length || !entity.available)
|
||||
? <ReadOnlyControl entity={entity} /> : null}
|
||||
{power ? <ToggleControl entity={{ name: `${entity.name}: Power`, state: !['off', 'unknown', 'unavailable'].includes(entity.state) }} disabled={disabled}
|
||||
onChange={(on) => onAction(on ? turnOn.id : turnOff.id, {})} /> : null}
|
||||
<div className="flex min-w-0 flex-wrap gap-0.5">
|
||||
{actions.map((action) => <ActionControls key={action.id} action={action} entityName={entity.name} disabled={disabled}
|
||||
onAction={onAction} hideButton={Boolean(power && (action === turnOn || action === turnOff))} />)}
|
||||
</div>
|
||||
{entity.unsupported.length ? <details className="text-[0.65rem] text-slate-400"><summary className="cursor-pointer">Other actions</summary>
|
||||
<p>These actions need inputs this panel cannot display: {entity.unsupported.join(', ')}.</p>
|
||||
</details> : null}
|
||||
{entity.details.length ? <details className="text-[0.65rem] text-slate-400"><summary className="cursor-pointer">Details</summary>
|
||||
{entity.details.map((detail) => <div key={detail.name} className="flex min-w-0 justify-between gap-1"><span>{detail.name}</span><span className="min-w-0 break-words text-right">{detail.value}</span></div>)}
|
||||
</details> : null}
|
||||
{!entity.available && entity.type !== 'readOnly' ? <span className="text-xs text-amber-200">Unavailable</span> : null}
|
||||
<Control entity={entity} disabled={disabled} onChange={onChange} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -57,16 +46,17 @@ export default function HomeAssistantActivitiesPanel() {
|
||||
const admin = role === 'admin' || role === 'lockdown';
|
||||
const allowed = ['user', 'admin', 'lockdown'].includes(role) && (mode !== 'admin' || admin) && (mode !== 'lockdown' || role === 'lockdown');
|
||||
if (!state?.enabled || !state.items.length) return null;
|
||||
// Cancel local typing timers on either browser or HA disconnection; cached
|
||||
// session values are still useful to read but must not authorize new writes.
|
||||
// Session data survives a browser disconnect. Disable immediately so local
|
||||
// debounce timers are cancelled even when HA itself remains connected.
|
||||
const connected = state.connected && socketConnected;
|
||||
// Reuse the room-control auto-fit grid and compact tile rhythm, while keeping
|
||||
// all data and commands in the Activities namespace on both device layouts.
|
||||
return <CardFrame title="Home Assistant" bodyClassName="space-y-0.5 text-sm"
|
||||
actions={<span className={`rounded px-1 py-0.5 text-xs font-semibold leading-none ${connected ? 'bg-emerald-900 text-emerald-100' : 'bg-amber-900 text-amber-100'}`}>{connected ? 'Connected' : 'Offline'}</span>}>
|
||||
{!connected ? <p className="px-0.5 text-xs text-amber-200">{socketConnected ? 'Home Assistant is offline.' : 'Server disconnected.'} Values may be out of date.</p> : null}
|
||||
{connected && !state.controlsReady ? <p className="px-0.5 text-xs text-slate-400">Waiting for available controls.</p> : null}
|
||||
{!allowed ? <p className="px-0.5 text-xs text-slate-400">Controls are read-only with your current access.</p> : null}
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(9rem,100%),1fr))] items-start gap-0.5">
|
||||
{state.items.map((entity) => <ActivityTile key={entity.id} entity={entity} connected={connected} controlsReady={state.controlsReady} allowed={allowed} admin={admin} />)}
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(9rem,100%),1fr))] gap-0.5">
|
||||
{state.items.map((entity) => <ActivityTile key={entity.id} entity={entity} connected={connected} allowed={allowed} admin={admin} />)}
|
||||
</div>
|
||||
</CardFrame>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user