From 4fc7d2d475abbf6d86b19f4cfe46582d0e6bae2b Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Wed, 6 Sep 2023 20:25:57 +0200 Subject: [PATCH] room groups and grouping. --- frontend/src/lib/components/Lamp.svelte | 23 ++- frontend/src/lib/components/MetaLamp.svelte | 104 +++++++++++ frontend/src/lib/components/RoomHeader.svelte | 25 +++ .../src/lib/contexts/SelectContext.svelte | 12 ++ frontend/src/lib/contexts/StateContext.svelte | 174 ++++++++++-------- frontend/src/routes/+page.svelte | 35 +++- services/httpapiv1/service.go | 28 ++- 7 files changed, 313 insertions(+), 88 deletions(-) create mode 100644 frontend/src/lib/components/MetaLamp.svelte create mode 100644 frontend/src/lib/components/RoomHeader.svelte diff --git a/frontend/src/lib/components/Lamp.svelte b/frontend/src/lib/components/Lamp.svelte index fcf9697..54c77d6 100644 --- a/frontend/src/lib/components/Lamp.svelte +++ b/frontend/src/lib/components/Lamp.svelte @@ -136,9 +136,23 @@ overflow: hidden box-shadow: 1px 1px 1px #000 + @media screen and (max-width: 1921px) + width: calc(20% - 1ch) + + @media screen and (max-width: 1600px) + width: calc(25% - 1ch) + + @media screen and (max-width: 1200px) + width: calc(33.3333333% - 1ch) + @media screen and (max-width: 700px) - width: 100% + width: calc(50% - 1ch) + margin-top: 0.75ch + margin-bottom: 0.75ch + @media screen and (max-width: 380px) + width: 100% + &.selected background: #282833 color: #cde @@ -176,8 +190,13 @@ height: 0.2em &.compact - width: 3ch + width: 3.1ch border-top-right-radius: 0.25em + margin: 0.25ch 0.25ch + background: #282833 + + &.selected + background: #3c3c50 div.row margin-left: -0.1ch div.title diff --git a/frontend/src/lib/components/MetaLamp.svelte b/frontend/src/lib/components/MetaLamp.svelte new file mode 100644 index 0000000..4bb447c --- /dev/null +++ b/frontend/src/lib/components/MetaLamp.svelte @@ -0,0 +1,104 @@ + + + + + +
+
+
{name}
+
+
+ {#each devices as device (device.id)} + + {/each} +
+
+
+
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/frontend/src/lib/components/RoomHeader.svelte b/frontend/src/lib/components/RoomHeader.svelte new file mode 100644 index 0000000..a133b89 --- /dev/null +++ b/frontend/src/lib/components/RoomHeader.svelte @@ -0,0 +1,25 @@ + + +
+ +
{name}
+
+ + \ No newline at end of file diff --git a/frontend/src/lib/contexts/SelectContext.svelte b/frontend/src/lib/contexts/SelectContext.svelte index 7aa692c..d831ef4 100644 --- a/frontend/src/lib/contexts/SelectContext.svelte +++ b/frontend/src/lib/contexts/SelectContext.svelte @@ -7,6 +7,7 @@ export interface SelectContextData { toggleSelection(id: string): void + toggleMultiSelection(ids: string[]): void selectedList: Readable selectedMap: Readable<{[id:string]: boolean}> } @@ -26,6 +27,16 @@ selectedMap.update(m => ({...m, [id]: !m[id]})); } + function toggleMultiSelection(ids: string[]) { + selectedMap.update(m => { + if (ids.find(i => !m[i])) { + return ids.reduce((m, i) => ({...m, [i]: true}), m) + } else { + return ids.reduce((m, i) => ({...m, [i]: false}), m) + } + }); + } + // Effect: remove non-existent devices $: { const newMap = {...$selectedMap}; @@ -48,6 +59,7 @@ selectedList: {subscribe: selectedList.subscribe}, selectedMap: {subscribe: selectedMap.subscribe}, toggleSelection, + toggleMultiSelection, }); diff --git a/frontend/src/lib/contexts/StateContext.svelte b/frontend/src/lib/contexts/StateContext.svelte index fe7aed8..7f5511e 100644 --- a/frontend/src/lib/contexts/StateContext.svelte +++ b/frontend/src/lib/contexts/StateContext.svelte @@ -14,7 +14,7 @@ state: Readable error: Readable deviceList: Readable - roomList: Readable<{name: string, devices: Device[]}[]> + roomList: Readable<{name: string, groups: {name: string, devices: Device[]}[], devices: Device[]}[]> } export function getStateContext(): StateContextData { @@ -44,22 +44,44 @@ const roomList = derived(state, state => { const roomMap: Record = {}; + const groupMap: Record = {}; for (const id in state.devices) { if (!state.devices.hasOwnProperty(id)) { continue; } const roomAlias = state.devices[id].aliases.find(a => a.startsWith("lucifer:room:")); + const groupAlias = state.devices[id].aliases.find(a => a.startsWith("lucifer:group:")); const roomName = roomAlias?.slice("lucifer:room:".length) || "Unroomed"; + const groupName = groupAlias?.slice("lucifer:group:".length) || ""; if (!roomMap[roomName]) { roomMap[roomName] = []; } + if (groupAlias) { + groupMap[id] = groupName; + } + roomMap[roomName].push(state.devices[id]); } return Object.keys(roomMap) - .map(k => ({ name: k, devices: roomMap[k] })) + .map(k => ({ + name: k, + devices: roomMap[k].filter(d => !groupMap[d.id]).sort((a,b) => a.name.localeCompare(b.name)), + groups: roomMap[k].filter(d => !!groupMap[d.id]).reduce<{name: string, devices: Device[]}[]>((p, d) => { + const g = p.find(e => e.name === groupMap[d.id]); + if (g == null) { + p.push({name:groupMap[d.id], devices: [d]}) + } else { + g.devices.push(d); + } + + return p; + }, []) + .map(g => ({...g, devices: g.devices.sort((a,b) => a.name.localeCompare(b.name))})) + .sort((a,b) => a.name.localeCompare(b.name)) + })) .sort((a,b) => a.name.localeCompare(b.name)); }); @@ -83,103 +105,105 @@ const currSocket = new WebSocket(url); currSocket.onmessage = (msg) => { - const patch: UIStatePatch = JSON.parse(msg.data); + const patches: UIStatePatch[] = JSON.parse(msg.data); state.update(s => { - if (patch.device) { - if (patch.device.delete) { - const devices = {...s.devices}; - delete devices[patch.device.id]; - return {...s, devices}; - } else { - return { - ...s, - devices: { - ...s.devices, - [patch.device.id]: ((patch) => { - if (patch.addAlias) { - const aliases = [...(s.devices[patch.id].aliases || [])]; - const exclPrefix = ["lucifer:icon:", "lucifer:group:", "lucifer:name:"].find(p => patch.addAlias?.startsWith(p)); - const exclExisting = exclPrefix && aliases.find(a => a.startsWith(exclPrefix)); - - if (patch.addAlias.startsWith("lucifer:name:")) { - patch.name = patch.addAlias.slice("lucifer:name:".length) - } - if (patch.addAlias.startsWith("lucifer:icon:")) { - patch.icon = patch.addAlias.slice("lucifer:icon:".length) as IconName - } - - if (exclExisting) { + for (const patch of patches) { + if (patch.device) { + if (patch.device.delete) { + const devices = {...s.devices}; + delete devices[patch.device.id]; + s = {...s, devices}; + } else { + s = { + ...s, + devices: { + ...s.devices, + [patch.device.id]: ((patch) => { + if (patch.addAlias) { + const aliases = [...(s.devices[patch.id].aliases || [])]; + const exclPrefix = ["lucifer:icon:", "lucifer:group:", "lucifer:name:"].find(p => patch.addAlias?.startsWith(p)); + const exclExisting = exclPrefix && aliases.find(a => a.startsWith(exclPrefix)); + + if (patch.addAlias.startsWith("lucifer:name:")) { + patch.name = patch.addAlias.slice("lucifer:name:".length) + } + if (patch.addAlias.startsWith("lucifer:icon:")) { + patch.icon = patch.addAlias.slice("lucifer:icon:".length) as IconName + } + + if (exclExisting) { + return { + ...s.devices[patch.id], + aliases: [...aliases.filter(a => a != exclExisting), patch.addAlias].sort(), + name: patch.name || s.devices[patch.id].name, + icon: patch.icon || s.devices[patch.id].icon, + }; + } else { + return { + ...s.devices[patch.id], + aliases: [...aliases, patch.addAlias].sort(), + name: patch.name || s.devices[patch.id].name, + icon: patch.icon || s.devices[patch.id].icon, + }; + } + } else if (patch.removeAlias != null) { + const aliases = [...(s.devices[patch.id].aliases || [])]; return { ...s.devices[patch.id], - aliases: [...aliases.filter(a => a != exclExisting), patch.addAlias].sort(), - name: patch.name || s.devices[patch.id].name, - icon: patch.icon || s.devices[patch.id].icon, + aliases: aliases.filter(a => a !== patch.removeAlias), }; } else { return { ...s.devices[patch.id], - aliases: [...aliases, patch.addAlias].sort(), - name: patch.name || s.devices[patch.id].name, - icon: patch.icon || s.devices[patch.id].icon, + ...patch, }; } - } else if (patch.removeAlias != null) { - const aliases = [...(s.devices[patch.id].aliases || [])]; - return { - ...s.devices[patch.id], - aliases: aliases.filter(a => a !== patch.removeAlias), - }; - } else { - return { - ...s.devices[patch.id], - ...patch, - }; - } - })(patch.device) + })(patch.device) + } } } } - } - if (patch.assignment) { - if (patch.assignment.delete) { - const assignments = {...s.assignments}; - delete assignments[patch.assignment.id]; - return {...s, assignments}; - } else { - return { - ...s, - assignments: { - ...s.assignments, - [patch.assignment.id]: { - ...s.assignments[patch.assignment.id], - ...patch.assignment, + if (patch.assignment) { + if (patch.assignment.delete) { + const assignments = {...s.assignments}; + delete assignments[patch.assignment.id]; + s = {...s, assignments}; + } else { + s = { + ...s, + assignments: { + ...s.assignments, + [patch.assignment.id]: { + ...s.assignments[patch.assignment.id], + ...patch.assignment, + } } } } } - } - if (patch.script) { - if (patch.script.delete) { - const scripts = {...s.scripts}; - delete scripts[patch.script.id]; - return {...s, scripts}; - } else { - return { - ...s, - scripts: { - ...s.scripts, - [patch.script.id]: { - ...s.scripts[patch.script.id], - ...patch.script, + if (patch.script) { + if (patch.script.delete) { + const scripts = {...s.scripts}; + delete scripts[patch.script.id]; + s = {...s, scripts}; + } else { + s = { + ...s, + scripts: { + ...s.scripts, + [patch.script.id]: { + ...s.scripts[patch.script.id], + ...patch.script, + } } } } } } - + return s; }) } diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index c41775b..c724401 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,31 +1,48 @@
- {#each $deviceList as device (device.id) } - + {#each $roomList as room (room.name)} + +
+ {#each room.devices as device (device.id) } + + {/each} +
+
+ {#each room.groups as group (group.name)} + + {/each} +
{/each} -
-
- +
\ No newline at end of file diff --git a/services/httpapiv1/service.go b/services/httpapiv1/service.go index 4a6d83f..5d4cc55 100644 --- a/services/httpapiv1/service.go +++ b/services/httpapiv1/service.go @@ -15,6 +15,7 @@ import ( "net" "net/http" "sync" + "time" ) var zeroUUID = uuid.UUID{ @@ -57,11 +58,34 @@ func New(addr string) (lucifer3.Service, error) { defer svc.removeSub(sub) defer ws.Close() - for patch := range sub { - err := ws.WriteJSON(patch) + lastSent := time.Now() + patches := make([]uistate.Patch, 0, 128) + for { + patch := <-sub + since := time.Now().Sub(lastSent) + patches = append(patches[:0], patch) + + if since < time.Millisecond*950 { + waitCh := time.NewTimer(time.Millisecond*1000 - since) + waiting := true + + for waiting { + select { + case patch, ok := <-sub: + patches = append(patches, patch) + waiting = ok + case <-waitCh.C: + waiting = false + } + } + } + + err := ws.WriteJSON(patches) if err != nil { break } + + lastSent = time.Now() } return nil