ui stuff from laptop

This commit is contained in:
2023-02-04 11:15:22 +01:00
parent d2b2244e2f
commit da8fd03e8a
14 changed files with 4009 additions and 67 deletions
+3481
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -19,8 +19,10 @@
"eslint": "^8.28.0", "eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"node-sass": "^8.0.0",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1", "prettier-plugin-svelte": "^2.8.1",
"sass": "^1.57.1",
"svelte": "^3.54.0", "svelte": "^3.54.0",
"svelte-check": "^2.9.2", "svelte-check": "^2.9.2",
"tslib": "^2.4.1", "tslib": "^2.4.1",
+23
View File
@@ -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");
}
@@ -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>
+140 -54
View File
@@ -1,76 +1,162 @@
<script lang="ts"> <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 device: Device;
export let title: string;
export let color: ColorRGB;
export let intensity: number;
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; let displayTitle: string;
$: { $: {
if (title.length > 12) { if (deviceTitle.length > 12) {
let last = title.split(" ").pop() || title; let last = deviceTitle.split(" ").pop() || deviceTitle;
if (last.length > 4) { if (last.length > 4) {
last = last.slice(-3); 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 { } else {
displayTitle = title; displayTitle = deviceTitle;
} }
} }
</script> </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 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 class="title">{displayTitle}</div>
</div> </div>
<div class="flatboi"> <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>
</div> </div>
<style> <style lang="sass">
div.lamp { div.lamp
width: 15ch; cursor: pointer
height: 2em; width: 19ch
margin: 0.25em; height: 2em
background: #18181c; margin: 0.25em
color: #84888f; background: #18181c
border-radius: 0.25em; color: #84888f
border-top-right-radius: 1em; border-radius: 0.25em
overflow: hidden; border-top-right-radius: 1em
} overflow: hidden
div.lamp.selected { box-shadow: 1px 1px 1px #000
background: #282833;
color: #cde; @media screen and (max-width: 700px)
} width: 100%
div.lamp > div.row {
display: flex; &.selected
} background: #282833
div.lamp > div.row > div.roundboi { color: #cde
width: 1.25em;
height: 1.25em; > div.row
margin-left: 0.25em; display: flex
margin-top: 0.30em;
border-radius: 2em; > div.roundboi
box-sizing: border-box; width: 1.25em
border: 0.5px solid #000; height: 1.25em
} margin-left: 0.25em
div.lamp > div.row > div.title { margin-top: 0.30em
font-size: 0.9em; border-radius: 2em
text-align: left; box-sizing: border-box
margin-top: 0.4em; border: 0.5px solid #000
margin-left: 0.75ch;
margin-right: 1ch; > div.roundboi-text
height: 1.6em; color: #abc
} margin-top: 0.2em
div.lamp > div.flatboi { font-size: 0.8em
height: 0.2em;
background-color: #040408; > div.title
} font-size: 0.9em
div.lamp > div.flatboi > div.flatboi2 { text-align: left
height: 0.2em; 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> </style>
@@ -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>
@@ -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>
@@ -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
View File
@@ -4,6 +4,18 @@ export interface ColorRGB {
blue: number 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 { export function rgb(red: number, green: number, blue: number): ColorRGB {
return {red, green, blue}; return {red, green, blue};
} }
+59
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,5 @@
import type Device from "./device";
export default interface UIState {
devices: {[id: string]: Device}
}
+10
View File
@@ -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>
+7 -12
View File
@@ -1,22 +1,17 @@
<script lang="ts"> <script lang="ts">
import Lamp from "$lib/components/Lamp.svelte"; 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> </script>
<div class="page"> <div class="page">
<Lamp title="Hexagon 1" color={rgb(1.000,0.541,0.071)} intensity={0.3} /> {#each $deviceList as device (device.id) }
<Lamp title="Hexagon 2" color={rgb(1.000,0.541,0.071)} intensity={0.4} /> <Lamp device={device} />
<Lamp title="Hexagon 3" color={rgb(1.000,0.541,0.071)} intensity={0.5} /> {/each}
<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} />
</div> </div>
<Toolbar />
<style> <style>
div.page { div.page {