Browse Source

ui stuff from laptop

beelzebub
Gisle Aune 1 year ago
parent
commit
da8fd03e8a
  1. 3561
      frontend/package-lock.json
  2. 2
      frontend/package.json
  3. 23
      frontend/src/lib/client/lucifer.ts
  4. 23
      frontend/src/lib/components/ColorPalette.svelte
  5. 194
      frontend/src/lib/components/Lamp.svelte
  6. 75
      frontend/src/lib/components/Toolbar.svelte
  7. 54
      frontend/src/lib/contexts/SelectContext.svelte
  8. 57
      frontend/src/lib/contexts/StateContext.svelte
  9. 12
      frontend/src/lib/models/color.ts
  10. 59
      frontend/src/lib/models/device.ts
  11. 60
      frontend/src/lib/models/palette.ts
  12. 5
      frontend/src/lib/models/uistate.ts
  13. 10
      frontend/src/routes/+layout.svelte
  14. 21
      frontend/src/routes/+page.svelte

3561
frontend/package-lock.json
File diff suppressed because it is too large
View File

2
frontend/package.json

@ -19,8 +19,10 @@
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"node-sass": "^8.0.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"sass": "^1.57.1",
"svelte": "^3.54.0",
"svelte-check": "^2.9.2",
"tslib": "^2.4.1",

23
frontend/src/lib/client/lucifer.ts

@ -0,0 +1,23 @@
import type UIState from "$lib/models/uistate";
export default async function fetchLucifer<T>(path: string, init?: RequestInit): Promise<T> {
const url = import.meta.env.VITE_LUCIFER4_BACKEND_URL + "/" + path;
console.log(url);
const res = await fetch(url, init);
if (res.status !== 200) {
if (res.headers.get("Content-Type")?.includes("application/json")) {
throw await res.json();
} else {
throw await res.text();
}
}
const json = await res.json();
return json as T;
}
export async function fetchUIState(): Promise<UIState> {
return fetchLucifer("state");
}

23
frontend/src/lib/components/ColorPalette.svelte

@ -0,0 +1,23 @@
<script lang="ts">
import { rgbString } from "$lib/models/color";
import type { Palette } from "$lib/models/palette";
export let palette: Palette
</script>
<div class="palette">
{#each palette as p (p.value)}
<div title={p.label} class="color" style="background-color: {rgbString(p.rgb)}"></div>
{/each}
</div>
<style lang="sass">
div.palette
display: flex
flex-direction: row
div.color
margin: 0.05em
width: 1ch
height: 0.5em
</style>

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

@ -1,76 +1,162 @@
<script lang="ts">
import type { ColorRGB } from "../models/color";
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";
export let selected: boolean = false;
export let title: string;
export let color: ColorRGB;
export let intensity: number;
export let device: Device;
const {selectedMap, toggleSelection} = getSelectedContext();
function onSelect() {
toggleSelection(device.id);
}
// Process device
let deviceTitle: string;
let iconColor: ColorRGB | null;
let barColor: ColorRGB | null;
let barPercentage: number | null;
let roundboiText: string | null;
$: {
// TODO: Fix device.name on the backend
const nameAlias = device.aliases.find(a => a.startsWith("lucifer:name:"));
if (nameAlias != null) {
deviceTitle = nameAlias.slice("lucifer:name:".length);
} else {
deviceTitle = "";
}
barPercentage = null;
barColor = null;
iconColor = null;
if (device.hwState) {
const hws = device.hwState;
const sflags = hws.supportFlags;
if (deviceTitle == "") {
deviceTitle = device.hwState.internalName;
}
if (sflags & SupportFlags.Color) {
iconColor = device.activeColorRgb;
} else {
iconColor = null;
}
if (sflags & SupportFlags.SensorPresence) {
barColor = rgb(0.5,0.5,0.5);
barPercentage = Math.max(0, (300 - (device.sensors.lastMotion||300))/300) * 100;
}
if (sflags & SupportFlags.Temperature) {
barColor = rgb(1.000,0.2,0.2);
barPercentage = Math.min(1, Math.max(0, (device.desiredState.temperature||0) - 5) / 35) * 100;
}
if (sflags & SupportFlags.Intensity) {
if (sflags & SupportFlags.Color) {
barColor = device.activeColorRgb;
} else {
barColor = rgb(1.000,0.671,0.355);
}
barPercentage = device.desiredState.intensity;
}
if (sflags & SupportFlags.SensorTemperature && !!device.sensors.temperature) {
roundboiText = `${device.sensors.temperature.toFixed(0)}°`
}
}
}
// Make title visible
let displayTitle: string;
$: {
if (title.length > 12) {
let last = title.split(" ").pop() || title;
if (deviceTitle.length > 12) {
let last = deviceTitle.split(" ").pop() || deviceTitle;
if (last.length > 4) {
last = last.slice(-3);
}
displayTitle = `${(title.split(" ").shift()||"").slice(0, 10 - last.length)}... ${last}`
displayTitle = `${(deviceTitle.split(" ").shift()||"").slice(0, 10 - last.length)}... ${last}`
} else {
displayTitle = title;
displayTitle = deviceTitle;
}
}
</script>
<div class="lamp" class:selected>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="lamp" class:selected={$selectedMap[device.id]} on:click={onSelect}>
<div class="row">
<div style="background-color: rgb({color.red*255},{color.green*255},{color.blue*255})" class="roundboi"></div>
{#if iconColor != null}
<div style="background-color: rgb({iconColor.red*255},{iconColor.green*255},{iconColor.blue*255})" class="roundboi"></div>
{:else}
<div style="background-color: #223)" class="roundboi">
{#if !!roundboiText}
<div class="roundboi-text">{roundboiText}</div>
{/if}
</div>
{/if}
<div class="title">{displayTitle}</div>
</div>
<div class="flatboi">
<div style="width: {intensity*100}%; background-color: rgb({color.red*255},{color.green*255},{color.blue*255})" class="flatboi2"></div>
{#if barColor != null && barPercentage != null}
<div style="width: {barPercentage*100}%; background-color: rgb({barColor.red*255},{barColor.green*255},{barColor.blue*255})" class="flatboi2"></div>
{/if}
</div>
</div>
<style>
div.lamp {
width: 15ch;
height: 2em;
margin: 0.25em;
background: #18181c;
color: #84888f;
border-radius: 0.25em;
border-top-right-radius: 1em;
overflow: hidden;
}
div.lamp.selected {
background: #282833;
color: #cde;
}
div.lamp > div.row {
display: flex;
}
div.lamp > div.row > div.roundboi {
width: 1.25em;
height: 1.25em;
margin-left: 0.25em;
margin-top: 0.30em;
border-radius: 2em;
box-sizing: border-box;
border: 0.5px solid #000;
}
div.lamp > div.row > div.title {
font-size: 0.9em;
text-align: left;
margin-top: 0.4em;
margin-left: 0.75ch;
margin-right: 1ch;
height: 1.6em;
}
div.lamp > div.flatboi {
height: 0.2em;
background-color: #040408;
}
div.lamp > div.flatboi > div.flatboi2 {
height: 0.2em;
}
<style lang="sass">
div.lamp
cursor: pointer
width: 19ch
height: 2em
margin: 0.25em
background: #18181c
color: #84888f
border-radius: 0.25em
border-top-right-radius: 1em
overflow: hidden
box-shadow: 1px 1px 1px #000
@media screen and (max-width: 700px)
width: 100%
&.selected
background: #282833
color: #cde
> div.row
display: flex
> div.roundboi
width: 1.25em
height: 1.25em
margin-left: 0.25em
margin-top: 0.30em
border-radius: 2em
box-sizing: border-box
border: 0.5px solid #000
> div.roundboi-text
color: #abc
margin-top: 0.2em
font-size: 0.8em
> div.title
font-size: 0.9em
text-align: left
margin-top: 0.4em
margin-left: 0.75ch
margin-right: 1ch
height: 1.6em
> div.flatboi
height: 0.2em
> div.flatboi2
height: 0.2em
</style>

75
frontend/src/lib/components/Toolbar.svelte

@ -0,0 +1,75 @@
<script lang="ts">
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
import { getStateContext } from "$lib/contexts/StateContext.svelte";
import type Device from "$lib/models/device";
import { KELVIN_PALETTE, RGB_PALETTE } from "$lib/models/palette";
import ColorPalette from "./ColorPalette.svelte";
const {deviceList} = getStateContext();
const {selectedMap} = getSelectedContext();
let selected: Device[];
let unselected: Device[];
$: selected = $deviceList.filter(d => !!$selectedMap[d.id]);
$: unselected = $deviceList.filter(d => !$selectedMap[d.id]);
let assignment: string;
$: {
assignment = "";
if (selected.length > 0) {
for (const alias of selected[0].aliases) {
if (!selected.find(d => !d.aliases.includes(alias)) && !unselected.find(d => d.aliases.includes(alias))) {
assignment = alias;
break;
}
}
if (assignment === "") {
if (selected.length == 1) {
assignment = selected[0].id;
} else {
let prefix = selected[0].id;
for (const device of selected) {
for (let i = 0; i < prefix.length; i++) {
if (device.id.charAt(i) !== prefix.charAt(i)) {
prefix = prefix.slice(0, i);
break;
}
}
}
if (prefix.length > 0) {
assignment = `${prefix}{${selected.map(d => d.id.slice(prefix.length)).join("|")}}`
}
}
if (assignment === "") {
assignment = `{${selected.map(d => d.id).join("|")}}`
}
}
}
}
</script>
<div class="toolbar">
<ColorPalette palette={RGB_PALETTE} />
</div>
<style lang="sass">
div.toolbar
position: fixed
bottom: 0
width: 100%
height: 2em
padding: 0.35em 1ch
box-sizing: border-box
font-size: 1.5em
background: #18181c
color: #abc
box-shadow: 1px 1px 1px #000
display: flex
flex-direction: row
flex-wrap: wrap
</style>

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

@ -0,0 +1,54 @@
<script lang="ts" context="module">
import { getContext, setContext } from "svelte";
import { writable, type Readable } from "svelte/store";
import { getStateContext } from "./StateContext.svelte";
const ctxKey = {ctx: "SelectContext"};
export interface SelectContextData {
toggleSelection(id: string): void
selectedList: Readable<string[]>
selectedMap: Readable<{[id:string]: boolean}>
}
export function getSelectedContext(): SelectContextData {
return getContext(ctxKey) as SelectContextData;
}
</script>
<script lang="ts">
const selectedList = writable<string[]>([]);
const selectedMap = writable<{[id:string]: boolean}>({});
const {state} = getStateContext();
function toggleSelection(id: string) {
selectedMap.update(m => ({...m, [id]: !m[id]}));
}
// Effect: remove non-existent devices
$: {
const newMap = {...$selectedMap};
for (const id in newMap) {
if (!Object.hasOwn(newMap, id)) {
continue;
}
if (!$state?.devices[id]) {
delete newMap[id];
}
}
$selectedMap = newMap;
}
// Effect: sync list
$: $selectedList = Object.keys($selectedMap).filter(id => !!$selectedMap[id]);
setContext(ctxKey, {
selectedList: {subscribe: selectedList.subscribe},
selectedMap: {subscribe: selectedMap.subscribe},
toggleSelection,
});
</script>
<slot></slot>

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

@ -0,0 +1,57 @@
<script lang="ts" context="module">
import { fetchUIState } from "$lib/client/lucifer";
import type Device from "$lib/models/device";
import type UIState from "$lib/models/uistate";
import { getContext, onMount, setContext } from "svelte";
import { derived, writable, type Readable } from "svelte/store";
const ctxKey = {ctx: "StateContext"};
export interface StateContextData {
reload(): Promise<void>
state: Readable<UIState | null>
error: Readable<string | null>
deviceList: Readable<Device[]>
}
export function getStateContext(): StateContextData {
return getContext(ctxKey) as StateContextData;
}
</script>
<script lang="ts">
const state = writable<UIState | null>(null);
const error = writable<string | null>(null);
const deviceList = derived(state, state => {
if (state == null) {
return [];
}
return Object.keys(state.devices)
.map(k => state.devices[k])
.sort((a,b) => a.id.localeCompare(b.id));
})
async function reload() {
error.set(null);
try {
const newState = await fetchUIState();
state.set(newState);
} catch(err) {
error.set(err?.toString ? err.toString() : String(err))
}
}
onMount(() => { reload(); });
setContext<StateContextData>(ctxKey, {
reload,
error: { subscribe: error.subscribe },
state: { subscribe: state.subscribe },
deviceList,
});
</script>
<slot></slot>

12
frontend/src/lib/models/color.ts

@ -4,6 +4,18 @@ export interface ColorRGB {
blue: number
}
export enum ColorFlags {
CFlagXY = 1 << 0,
CFlagRGB = 1 << 1,
CFlagHS = 1 << 2,
CFlagHSK = 1 << 3,
CFlagKelvin = 1 << 4,
}
export function rgbString({red,green,blue}: ColorRGB) {
return `rgb(${red*255},${green*255},${blue*255})`
}
export function rgb(red: number, green: number, blue: number): ColorRGB {
return {red, green, blue};
}

59
frontend/src/lib/models/device.ts

@ -0,0 +1,59 @@
import type { ColorFlags, ColorRGB } from "./color"
export default interface Device {
id: string
name: string
hwMetadata: HardwareMetadata | null
hwState: HardwareState | null
activeColorRgb: ColorRGB | null
desiredState: State,
aliases: string[],
assignment: string,
sensors: Sensors
}
export interface Sensors {
lastMotion?: number
temperature?: number
}
export interface HardwareState {
id: string
internalName: string
supportFlags: SupportFlags
colorFlags: ColorFlags
buttons: string[] | null
state: State
batteryPercentage: number
unreachable: boolean
}
export interface HardwareMetadata {
firmwareVersion?: string
}
export interface State {
power: boolean | null
temperature: number | null
intensity: number | null
color: string | null
}
export enum SupportFlags {
Power = 1 << 0,
Intensity = 1 << 1,
Color = 1 << 2,
Temperature = 1 << 3,
SensorButtons = 1 << 4,
SensorTemperature = 1 << 5,
SensorLightLevel = 1 << 6,
SensorPresence = 1 << 7,
}
export const BLANK_STATE: State = (Object.seal||(v=>v))({
power: null,
temperature: null,
intensity: null,
color: null,
});

60
frontend/src/lib/models/palette.ts

@ -0,0 +1,60 @@
import { rgb, type ColorRGB } from "./color"
export type Palette = { value: string, label: string, rgb: ColorRGB }[];
export const RGB_PALETTE: Palette = [
{value: "hs:235.00,0.550", label: "Sky Blue", rgb: rgb(0.450,0.496,1.000) },
{value: "hs:235.00,0.330", label: "Sky Blue 2", rgb: rgb(0.670,0.698,1.000) },
{value: "xy:0.2721,0.1908", label: "Lilac", rgb: rgb(0.792,0.500,1.000) },
{value: "hs:220,0.1", label: "Bluish White", rgb: rgb(0.900,0.933,1.000)}
]
export const KELVIN_PALETTE: Palette = [
{value: "k:1500", label: "1500 K", rgb: rgb(1.000,0.427,0.000)},
{value: "k:1625", label: "1625 K", rgb: rgb(1.000,0.469,0.000)},
{value: "k:1750", label: "1750 K", rgb: rgb(1.000,0.484,0.000)},
{value: "k:1875", label: "1875 K", rgb: rgb(1.000,0.499,0.000)},
{value: "k:2000", label: "2000 K", rgb: rgb(1.000,0.541,0.071)},
{value: "k:2125", label: "2125 K", rgb: rgb(1.000,0.571,0.162)},
{value: "k:2250", label: "2250 K", rgb: rgb(1.000,0.586,0.193)},
{value: "k:2375", label: "2375 K", rgb: rgb(1.000,0.601,0.221)},
{value: "k:2500", label: "2500 K", rgb: rgb(1.000,0.631,0.282)},
{value: "k:2625", label: "2625 K", rgb: rgb(1.000,0.659,0.333)},
{value: "k:2750", label: "2750 K", rgb: rgb(1.000,0.671,0.355)},
{value: "k:2875", label: "2875 K", rgb: rgb(1.000,0.682,0.376)},
{value: "k:3000", label: "3000 K", rgb: rgb(1.000,0.706,0.420)},
{value: "k:3125", label: "3125 K", rgb: rgb(1.000,0.730,0.465)},
{value: "k:3250", label: "3250 K", rgb: rgb(1.000,0.739,0.482)},
{value: "k:3375", label: "3375 K", rgb: rgb(1.000,0.748,0.500)},
{value: "k:3500", label: "3500 K", rgb: rgb(1.000,0.769,0.537)},
{value: "k:3625", label: "3625 K", rgb: rgb(1.000,0.786,0.575)},
{value: "k:3750", label: "3750 K", rgb: rgb(1.000,0.794,0.590)},
{value: "k:3875", label: "3875 K", rgb: rgb(1.000,0.802,0.606)},
{value: "k:4000", label: "4000 K", rgb: rgb(1.000,0.820,0.639)},
{value: "k:4125", label: "4125 K", rgb: rgb(1.000,0.833,0.673)},
{value: "k:4250", label: "4250 K", rgb: rgb(1.000,0.839,0.686)},
{value: "k:4375", label: "4375 K", rgb: rgb(1.000,0.845,0.699)},
{value: "k:4500", label: "4500 K", rgb: rgb(1.000,0.859,0.729)},
{value: "k:4625", label: "4625 K", rgb: rgb(1.000,0.873,0.757)},
{value: "k:4750", label: "4750 K", rgb: rgb(1.000,0.879,0.768)},
{value: "k:4875", label: "4875 K", rgb: rgb(1.000,0.884,0.780)},
{value: "k:5000", label: "5000 K", rgb: rgb(1.000,0.894,0.808)},
{value: "k:5125", label: "5125 K", rgb: rgb(1.000,0.908,0.832)},
{value: "k:5250", label: "5250 K", rgb: rgb(1.000,0.912,0.843)},
{value: "k:5375", label: "5375 K", rgb: rgb(1.000,0.916,0.854)},
{value: "k:5500", label: "5500 K", rgb: rgb(1.000,0.925,0.878)},
{value: "k:5625", label: "5625 K", rgb: rgb(1.000,0.936,0.899)},
{value: "k:5750", label: "5750 K", rgb: rgb(1.000,0.939,0.908)},
{value: "k:5875", label: "5875 K", rgb: rgb(1.000,0.943,0.917)},
{value: "k:6000", label: "6000 K", rgb: rgb(1.000,0.953,0.937)},
{value: "k:6125", label: "6125 K", rgb: rgb(1.000,0.960,0.958)},
{value: "k:6250", label: "6250 K", rgb: rgb(1.000,0.963,0.965)},
{value: "k:6375", label: "6375 K", rgb: rgb(1.000,0.967,0.973)},
{value: "k:6500", label: "6500 K", rgb: rgb(1.000,0.976,0.992)},
{value: "k:7000", label: "7000 K", rgb: rgb(0.961,0.953,1.000)},
{value: "k:8000", label: "8000 K", rgb: rgb(0.890,0.914,1.000)},
{value: "k:9000", label: "9000 K", rgb: rgb(0.839,0.882,1.000)},
{value: "k:10000", label: "10000 K", rgb: rgb(0.812,0.855,1.000)},
{value: "k:11000", label: "11000 K", rgb: rgb(0.784,0.835,1.000)},
{value: "k:12000", label: "12000 K", rgb: rgb(0.765,0.820,1.000)},
];

5
frontend/src/lib/models/uistate.ts

@ -0,0 +1,5 @@
import type Device from "./device";
export default interface UIState {
devices: {[id: string]: Device}
}

10
frontend/src/routes/+layout.svelte

@ -0,0 +1,10 @@
<script lang="ts">
import SelectContext from "$lib/contexts/SelectContext.svelte";
import StateContext from "$lib/contexts/StateContext.svelte";
</script>
<StateContext>
<SelectContext>
<slot></slot>
</SelectContext>
</StateContext>

21
frontend/src/routes/+page.svelte

@ -1,22 +1,17 @@
<script lang="ts">
import Lamp from "$lib/components/Lamp.svelte";
import { rgb } from "$lib/models/color";
import Toolbar from "$lib/components/Toolbar.svelte";
import { getStateContext } from "$lib/contexts/StateContext.svelte";
const {deviceList} = getStateContext();
</script>
<div class="page">
<Lamp title="Hexagon 1" color={rgb(1.000,0.541,0.071)} intensity={0.3} />
<Lamp title="Hexagon 2" color={rgb(1.000,0.541,0.071)} intensity={0.4} />
<Lamp title="Hexagon 3" color={rgb(1.000,0.541,0.071)} intensity={0.5} />
<Lamp title="Hexagon 4" color={rgb(1.000,0.631,0.282)} selected intensity={0.1} />
<Lamp title="Lamp with an equally absurd name" color={rgb(0.500,0.542,1.000)} selected intensity={0.3} />
<Lamp title="Hexagon 5" color={rgb(1.000,0.631,0.282)} selected intensity={0.66} />
<Lamp title="Hexagon 6" color={rgb(1.000,0.700,0.408)} selected intensity={0.92} />
<Lamp title="Hexagon 7" color={rgb(1.000,0.700,0.408)} intensity={1} />
<Lamp title="Hexagon 8" color={rgb(1.000,0.700,0.408)} intensity={0.9} />
<Lamp title="Hexagon 9" color={rgb(1.000,0.700,0.408)} intensity={0.5} />
<Lamp title="Signe Table Lamp 1" color={rgb(0.765,0.820,1.000)} selected intensity={0.79} />
{#each $deviceList as device (device.id) }
<Lamp device={device} />
{/each}
</div>
<Toolbar />
<style>
div.page {

Loading…
Cancel
Save