Browse Source

room groups and grouping.

beelzebub
Gisle Aune 1 year ago
parent
commit
4fc7d2d475
  1. 21
      frontend/src/lib/components/Lamp.svelte
  2. 104
      frontend/src/lib/components/MetaLamp.svelte
  3. 25
      frontend/src/lib/components/RoomHeader.svelte
  4. 12
      frontend/src/lib/contexts/SelectContext.svelte
  5. 42
      frontend/src/lib/contexts/StateContext.svelte
  6. 33
      frontend/src/routes/+page.svelte
  7. 28
      services/httpapiv1/service.go

21
frontend/src/lib/components/Lamp.svelte

@ -136,7 +136,21 @@
overflow: hidden overflow: hidden
box-shadow: 1px 1px 1px #000 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) @media screen and (max-width: 700px)
width: calc(50% - 1ch)
margin-top: 0.75ch
margin-bottom: 0.75ch
@media screen and (max-width: 380px)
width: 100% width: 100%
&.selected &.selected
@ -176,8 +190,13 @@
height: 0.2em height: 0.2em
&.compact &.compact
width: 3ch
width: 3.1ch
border-top-right-radius: 0.25em border-top-right-radius: 0.25em
margin: 0.25ch 0.25ch
background: #282833
&.selected
background: #3c3c50
div.row div.row
margin-left: -0.1ch margin-left: -0.1ch
div.title div.title

104
frontend/src/lib/components/MetaLamp.svelte

@ -0,0 +1,104 @@
<script lang="ts" context="module">
export const DARK_COLOR = rgb(0.5176470588235295/2, 0.5333333333333333/2, 0.5607843137254902/1.75);
export const DARK_COLOR_SELECTED = rgb(0.5176470588235295/1.5, 0.5333333333333333/1.5, 0.5607843137254902/1.25);
</script>
<script lang="ts">
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
import type Device from "$lib/models/device";
import { SupportFlags } from "$lib/models/device";
import { rgb, type ColorRGB } from "../models/color";
import DeviceIcon from "./DeviceIcon.svelte";
import Lamp from "./Lamp.svelte";
export let name: string
export let devices: Device[];
let selected: boolean;
const {selectedMap, toggleMultiSelection} = getSelectedContext();
function onSelect() {
toggleMultiSelection(devices.map(d => d.id));
}
$: selected = !devices.find(d => !$selectedMap[d.id])
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="metalamp" class:selected>
<div class="row">
<div class="title" on:click={onSelect}>{name}</div>
</div>
<div class="row">
{#each devices as device (device.id)}
<Lamp compact device={device} />
{/each}
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
</div>
</div>
<style lang="sass">
div.metalamp
user-select: none
cursor: pointer
width: 19ch
margin: 0.5ch
background: #18181c
color: #84888f
border-radius: 0.5ch
border-top-right-radius: 1em
overflow: hidden
box-shadow: 1px 1px 1px #000
padding-bottom: 0.5ch
@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: calc(50% - 1ch)
margin-top: 0.75ch
margin-bottom: 0.75ch
@media screen and (max-width: 380px)
width: 100%
&.selected
background: #282833
color: #cde
> div.row
width: 100%
padding: 0 0.4ch
box-sizing: border-box
width: 100%
display: flex
flex-direction: row
flex-wrap: wrap
align-content: center
justify-content: space-between
> div.title
font-size: 0.75em
text-align: left
margin-top: 0.4em
margin-right: 1ch
height: 1.6em
user-select: none
margin-left: 0.4ch
</style>

25
frontend/src/lib/components/RoomHeader.svelte

@ -0,0 +1,25 @@
<script lang="ts">
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
export let name: string;
export let devices: {id: string}[];
function toggleAll() {
toggleMultiSelection(devices.map(d => d.id));
}
const {toggleMultiSelection} = getSelectedContext();
</script>
<div class="room-header">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click={toggleAll} class="name">{name}</div>
</div>
<style lang="sass">
div.room-header
user-select: none
font-size: 2rem
color: #abc
border-bottom: 1px solid #123
</style>

12
frontend/src/lib/contexts/SelectContext.svelte

@ -7,6 +7,7 @@
export interface SelectContextData { export interface SelectContextData {
toggleSelection(id: string): void toggleSelection(id: string): void
toggleMultiSelection(ids: string[]): void
selectedList: Readable<string[]> selectedList: Readable<string[]>
selectedMap: Readable<{[id:string]: boolean}> selectedMap: Readable<{[id:string]: boolean}>
} }
@ -26,6 +27,16 @@
selectedMap.update(m => ({...m, [id]: !m[id]})); 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 // Effect: remove non-existent devices
$: { $: {
const newMap = {...$selectedMap}; const newMap = {...$selectedMap};
@ -48,6 +59,7 @@
selectedList: {subscribe: selectedList.subscribe}, selectedList: {subscribe: selectedList.subscribe},
selectedMap: {subscribe: selectedMap.subscribe}, selectedMap: {subscribe: selectedMap.subscribe},
toggleSelection, toggleSelection,
toggleMultiSelection,
}); });
</script> </script>

42
frontend/src/lib/contexts/StateContext.svelte

@ -14,7 +14,7 @@
state: Readable<UIState> state: Readable<UIState>
error: Readable<string | null> error: Readable<string | null>
deviceList: Readable<Device[]> deviceList: Readable<Device[]>
roomList: Readable<{name: string, devices: Device[]}[]>
roomList: Readable<{name: string, groups: {name: string, devices: Device[]}[], devices: Device[]}[]>
} }
export function getStateContext(): StateContextData { export function getStateContext(): StateContextData {
@ -44,22 +44,44 @@
const roomList = derived(state, state => { const roomList = derived(state, state => {
const roomMap: Record<string, Device[]> = {}; const roomMap: Record<string, Device[]> = {};
const groupMap: Record<string, string> = {};
for (const id in state.devices) { for (const id in state.devices) {
if (!state.devices.hasOwnProperty(id)) { if (!state.devices.hasOwnProperty(id)) {
continue; continue;
} }
const roomAlias = state.devices[id].aliases.find(a => a.startsWith("lucifer:room:")); 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 roomName = roomAlias?.slice("lucifer:room:".length) || "Unroomed";
const groupName = groupAlias?.slice("lucifer:group:".length) || "";
if (!roomMap[roomName]) { if (!roomMap[roomName]) {
roomMap[roomName] = []; roomMap[roomName] = [];
} }
if (groupAlias) {
groupMap[id] = groupName;
}
roomMap[roomName].push(state.devices[id]); roomMap[roomName].push(state.devices[id]);
} }
return Object.keys(roomMap) 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)); .sort((a,b) => a.name.localeCompare(b.name));
}); });
@ -83,16 +105,17 @@
const currSocket = new WebSocket(url); const currSocket = new WebSocket(url);
currSocket.onmessage = (msg) => { currSocket.onmessage = (msg) => {
const patch: UIStatePatch = JSON.parse(msg.data);
const patches: UIStatePatch[] = JSON.parse(msg.data);
state.update(s => { state.update(s => {
for (const patch of patches) {
if (patch.device) { if (patch.device) {
if (patch.device.delete) { if (patch.device.delete) {
const devices = {...s.devices}; const devices = {...s.devices};
delete devices[patch.device.id]; delete devices[patch.device.id];
return {...s, devices};
s = {...s, devices};
} else { } else {
return {
s = {
...s, ...s,
devices: { devices: {
...s.devices, ...s.devices,
@ -146,9 +169,9 @@
if (patch.assignment.delete) { if (patch.assignment.delete) {
const assignments = {...s.assignments}; const assignments = {...s.assignments};
delete assignments[patch.assignment.id]; delete assignments[patch.assignment.id];
return {...s, assignments};
s = {...s, assignments};
} else { } else {
return {
s = {
...s, ...s,
assignments: { assignments: {
...s.assignments, ...s.assignments,
@ -165,9 +188,9 @@
if (patch.script.delete) { if (patch.script.delete) {
const scripts = {...s.scripts}; const scripts = {...s.scripts};
delete scripts[patch.script.id]; delete scripts[patch.script.id];
return {...s, scripts};
s = {...s, scripts};
} else { } else {
return {
s = {
...s, ...s,
scripts: { scripts: {
...s.scripts, ...s.scripts,
@ -179,6 +202,7 @@
} }
} }
} }
}
return s; return s;
}) })

33
frontend/src/routes/+page.svelte

@ -1,31 +1,48 @@
<script lang="ts"> <script lang="ts">
import DeviceIcon from "$lib/components/DeviceIcon.svelte"; import DeviceIcon from "$lib/components/DeviceIcon.svelte";
import Lamp, { DARK_COLOR } from "$lib/components/Lamp.svelte"; import Lamp, { DARK_COLOR } from "$lib/components/Lamp.svelte";
import MetaLamp from "$lib/components/MetaLamp.svelte";
import RoomHeader from "$lib/components/RoomHeader.svelte";
import { getStateContext } from "$lib/contexts/StateContext.svelte"; import { getStateContext } from "$lib/contexts/StateContext.svelte";
import { rgb } from "$lib/models/color"; import { rgb } from "$lib/models/color";
const {deviceList, roomList} = getStateContext();
const {roomList} = getStateContext();
$: console.log($roomList);
</script> </script>
<div class="page"> <div class="page">
{#each $deviceList as device (device.id) }
<Lamp compact={device.name.includes("Square")} device={device} />
{#each $roomList as room (room.name)}
<RoomHeader devices={room.devices} name={room.name} />
<div class="devices">
{#each room.devices as device (device.id) }
<Lamp device={device} />
{/each} {/each}
</div>
<div class="page" style="font-size: 8em">
<DeviceIcon name="hue_go" brightColor={rgb(0.517,0.537,1.000)} darkColor={DARK_COLOR} />
</div>
<div class="devices">
{#each room.groups as group (group.name)}
<MetaLamp name={group.name} devices={group.devices} />
{/each}
</div>
{/each}
</div> </div>
<style> <style>
div.page { div.page {
margin: 0 auto; margin: 0 auto;
width: 100ch;
width: 200ch;
max-width: 95%; max-width: 95%;
margin-top: 1em; margin-top: 1em;
font-size: 2em;
font-size: 1.33rem;
}
div.devices {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
} }
div.devices + div.devices {
margin-top: 0.25em;
}
</style> </style>

28
services/httpapiv1/service.go

@ -15,6 +15,7 @@ import (
"net" "net"
"net/http" "net/http"
"sync" "sync"
"time"
) )
var zeroUUID = uuid.UUID{ var zeroUUID = uuid.UUID{
@ -57,11 +58,34 @@ func New(addr string) (lucifer3.Service, error) {
defer svc.removeSub(sub) defer svc.removeSub(sub)
defer ws.Close() 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 { if err != nil {
break break
} }
lastSent = time.Now()
} }
return nil return nil

Loading…
Cancel
Save