refactorings + basis of script editor.
This commit is contained in:
@@ -1,29 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/services"
|
||||
"git.aiterp.net/lucifer3/server/services/effectenforcer"
|
||||
"git.aiterp.net/lucifer3/server/services/hue"
|
||||
"git.aiterp.net/lucifer3/server/services/mill"
|
||||
"git.aiterp.net/lucifer3/server/services/nanoleaf"
|
||||
"git.aiterp.net/lucifer3/server/services/tradfri"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bus := lucifer3.EventBus{}
|
||||
|
||||
resolver := services.NewResolver()
|
||||
sceneMap := services.NewSceneMap(resolver)
|
||||
|
||||
bus.JoinPrivileged(resolver)
|
||||
bus.JoinPrivileged(sceneMap)
|
||||
bus.Join(effectenforcer.NewService(resolver, sceneMap))
|
||||
bus.Join(nanoleaf.NewService())
|
||||
bus.Join(hue.NewService())
|
||||
bus.Join(tradfri.NewService())
|
||||
bus.Join(mill.NewService())
|
||||
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"image"
|
||||
color2 "image/color"
|
||||
"image/png"
|
||||
"math"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 1000, 1000))
|
||||
for y := 0; y < 1000; y += 1 {
|
||||
for x := 0; x < 1000; x += 1 {
|
||||
vecX := float64(x-500) / 500.0
|
||||
vecY := float64(y-500) / 500.0
|
||||
dist := math.Sqrt(vecX*vecX + vecY*vecY)
|
||||
angle := math.Mod((math.Atan2(vecY/dist, vecX/dist)*(180/math.Pi))+360+90, 360)
|
||||
|
||||
if dist >= 0.9 && dist < 1 && angle > 2.5 && angle < 357.5 {
|
||||
k := 1000 + int(11000*((angle-2.5)/355))
|
||||
c := color.Color{K: &k}
|
||||
rgb, _ := c.ToRGB()
|
||||
img.Set(x, y, color2.RGBA{
|
||||
R: uint8(rgb.RGB.Red * 255.0),
|
||||
G: uint8(rgb.RGB.Green * 255.0),
|
||||
B: uint8(rgb.RGB.Blue * 255.0),
|
||||
A: 255,
|
||||
})
|
||||
} else if dist < 0.85 {
|
||||
rgb := (color.HueSat{Hue: math.Mod(angle+180, 360), Sat: dist / 0.85}).ToRGB()
|
||||
|
||||
img.Set(x, y, color2.RGBA{
|
||||
R: uint8(rgb.Red * 255.0),
|
||||
G: uint8(rgb.Green * 255.0),
|
||||
B: uint8(rgb.Blue * 255.0),
|
||||
A: 255,
|
||||
})
|
||||
} else {
|
||||
img.Set(x, y, color2.RGBA{R: 0, G: 0, B: 0, A: 0})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = png.Encode(os.Stdout, img)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"image"
|
||||
color2 "image/color"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 500, 500))
|
||||
for y := 0; y < 500; y += 1 {
|
||||
for x := 0; x < 500; x += 1 {
|
||||
rgb := (color.XY{X: float64(x) / 499, Y: float64(y) / 499}).ToRGB()
|
||||
|
||||
if y == 300 {
|
||||
log.Println(rgb.Red, rgb.Green, rgb.Blue)
|
||||
}
|
||||
|
||||
img.Set(x, y, color2.RGBA{
|
||||
R: uint8(rgb.Red * 255.0),
|
||||
G: uint8(rgb.Green * 255.0),
|
||||
B: uint8(rgb.Blue * 255.0),
|
||||
A: 255,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_ = png.Encode(os.Stdout, img)
|
||||
}
|
||||
+4
-4
@@ -8,10 +8,10 @@ import (
|
||||
)
|
||||
|
||||
type Manual struct {
|
||||
Power *bool `json:"power,omitempty"`
|
||||
Color *color.Color `json:"color,omitempty"`
|
||||
Intensity *float64 `json:"intensity,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
Power *bool `json:"power"`
|
||||
Color *color.Color `json:"color"`
|
||||
Intensity *float64 `json:"intensity"`
|
||||
Temperature *float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
func (e Manual) EffectDescription() string {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="darkreader" content="we've already gone dark" />
|
||||
<style>
|
||||
body, html {
|
||||
background: #111114;
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
<script lang="ts" context="module">
|
||||
const cache: Record<string, ColorRGB> = {};
|
||||
|
||||
function getColor(color: string) {
|
||||
if (cache.hasOwnProperty(color)) {
|
||||
return Promise.resolve(cache[color]);
|
||||
}
|
||||
|
||||
return fetchColor(color).then(r => {
|
||||
cache[color] = r.rgb || {red: 255, green: 255, blue: 255};
|
||||
return cache[color];
|
||||
}).catch(err => {
|
||||
delete cache[color];
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { State } from "$lib/models/device";
|
||||
import Icon from "./Icon.svelte";
|
||||
import type { ColorRGB } from '$lib/models/color';
|
||||
import { fetchColor } from '$lib/client/lucifer';
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let value: State;
|
||||
export let deletable: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function togglePower() {
|
||||
switch (value.power) {
|
||||
case null: value.power = true; break;
|
||||
case true: value.power = false; break;
|
||||
case false: value.power = null; break;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleIntensity() {
|
||||
if (value.intensity === null) {
|
||||
value.intensity = 0.5;
|
||||
} else {
|
||||
value.intensity = null;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTemperature() {
|
||||
if (value.temperature === null) {
|
||||
value.temperature = 20;
|
||||
} else {
|
||||
value.temperature = null;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleColor() {
|
||||
switch (value.color?.split(":")[0]) {
|
||||
case undefined: value.color = "k:2750"; break;
|
||||
case "k": value.color = "hs:180,0.5"; break;
|
||||
case "hs": value.color = "rgb:1.000,0.800,0.066"; break;
|
||||
case "rgb": value.color = "xy:0.2000,0.2000"; break;
|
||||
case "xy": value.color = null; break;
|
||||
}
|
||||
}
|
||||
|
||||
function computeColorInputs(color: string | null) {
|
||||
const kind = color?.split(":")[0];
|
||||
const values = color?.split(":")[1]?.split(",").map(v => parseFloat(v)) || [0,0];
|
||||
|
||||
switch (kind) {
|
||||
case undefined: colorKind = "null"; break;
|
||||
case "k": colorKind = "k"; colorX = values[0]; colorY = values[1]||0; break;
|
||||
case "hs": colorKind = "hs"; colorX = values[0]; colorY = values[1]||0; break;
|
||||
case "xy": colorKind = "xy"; colorX = values[0]; colorY = values[1]||0; break;
|
||||
case "rgb": colorKind = "rgb"; colorX = values[0]; colorY = values[1]||0, colorZ = values[2]||0; break;
|
||||
}
|
||||
|
||||
computedColor = color;
|
||||
}
|
||||
|
||||
function updateColor(kind: string, x: number, y: number, z: number) {
|
||||
x = x || 0;
|
||||
y = y || 0;
|
||||
z = z || 0;
|
||||
|
||||
switch (kind) {
|
||||
case "k": value.color = `k:${x.toFixed(0)}`; break;
|
||||
case "xy": value.color = `xy:${x.toFixed(4)},${y.toFixed(4)}`; break;
|
||||
case "hs": value.color = `hs:${x.toFixed(0)},${y.toFixed(3)}`; break;
|
||||
case "rgb": value.color = `rgb:${x.toFixed(3)},${y.toFixed(3)},${z.toFixed(3)}`; break;
|
||||
}
|
||||
|
||||
computedColor = value.color;
|
||||
}
|
||||
|
||||
let intensityColor = "none";
|
||||
$: intensityColor = value.intensity !== null ? "off" : "none";
|
||||
|
||||
let temperatureColor = "none";
|
||||
$: temperatureColor = value.temperature !== null ? "off" : "none";
|
||||
|
||||
let powerColor = "";
|
||||
$: if (value.power != null) {
|
||||
powerColor = value.power ? "on" : "off"
|
||||
} else {
|
||||
powerColor = "none"
|
||||
}
|
||||
|
||||
let computedColor: string | null = "";
|
||||
let colorButton = "none";
|
||||
let colorKind = "null";
|
||||
let colorX = 0;
|
||||
let colorY = 0;
|
||||
let colorZ = 0;
|
||||
|
||||
$: if (value.color !== computedColor) {
|
||||
computeColorInputs(value.color);
|
||||
}
|
||||
|
||||
$: updateColor(colorKind, colorX, colorY, colorZ);
|
||||
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
let rgb = "";
|
||||
$: if (value.color !== null) {
|
||||
let before = value.color;
|
||||
|
||||
if (timeout !== null) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
if (value.color !== null) {
|
||||
getColor(value.color).then(v => {
|
||||
if (value.color === before) {
|
||||
rgb = `rgb(${v.red*255}, ${v.green*255}, ${v.blue*255})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 50)
|
||||
} else {
|
||||
rgb = "hsl(240, 8%, 21%)";
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="assignment-state">
|
||||
<div class="option {powerColor}"><Icon on:click={togglePower} block name="power" /></div>
|
||||
<div class="option {intensityColor}">
|
||||
<Icon on:click={toggleIntensity} block name="cirlce_notch" />
|
||||
{#if value.intensity != null}
|
||||
<input class="custom" type="number" min={0} max={1} step={0.01} bind:value={value.intensity} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="option {temperatureColor}">
|
||||
<Icon on:click={toggleTemperature} block name="temperature_half" />
|
||||
{#if value.temperature != null}
|
||||
<input class="custom" type="number" min={10} max={40} step={0.5} bind:value={value.temperature} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="option {colorButton}" style="--color: {rgb}">
|
||||
<Icon on:click={toggleColor} block name="palette" />
|
||||
{#if colorKind !== "null"}
|
||||
{#if colorKind === "k"}
|
||||
<div class="color-input">
|
||||
<label for="color_x">Kelvin</label>
|
||||
<input class="custom" name="color_x" type="number" min={1000} max={12000} step={10} bind:value={colorX} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if colorKind === "hs"}
|
||||
<div class="color-input">
|
||||
<label for="color_x">Hue</label>
|
||||
<input class="custom" name="color_x" type="number" bind:value={colorX} />
|
||||
</div>
|
||||
<div class="color-input">
|
||||
<label for="color_y">Sat</label>
|
||||
<input class="custom" name="color_y" type="number" min={0} max={1} step={0.01} bind:value={colorY} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if colorKind === "xy"}
|
||||
<div class="color-input">
|
||||
<label for="color_x">X</label>
|
||||
<input class="custom" name="color_x" type="number" min={0} max={1} step={0.0001} bind:value={colorX} />
|
||||
</div>
|
||||
<div class="color-input">
|
||||
<label for="color_y">Y</label>
|
||||
<input class="custom" name="color_y" type="number" min={0} max={1} step={0.0001} bind:value={colorY} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if colorKind === "rgb"}
|
||||
<div class="color-input">
|
||||
<label class="short" for="color_x">Red</label>
|
||||
<input class="custom short" name="color_x" type="number" min={0} max={1} step={0.001} bind:value={colorX} />
|
||||
</div>
|
||||
<div class="color-input">
|
||||
<label class="short" for="color_y">Green</label>
|
||||
<input class="custom short" name="color_y" type="number" min={0} max={1} step={0.001} bind:value={colorY} />
|
||||
</div>
|
||||
<div class="color-input">
|
||||
<label class="short" for="color_z">Blue</label>
|
||||
<input class="custom short" name="color_z" type="number" min={0} max={1} step={0.001} bind:value={colorZ} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if deletable}
|
||||
<div class="option red">
|
||||
<Icon on:click={() => dispatch("delete")} block name="trash" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import "$lib/css/colors.sass"
|
||||
|
||||
div.assignment-state
|
||||
display: flex
|
||||
user-select: none
|
||||
font-size: 1rem
|
||||
width: 100%
|
||||
flex-wrap: wrap
|
||||
|
||||
@media screen and (max-width: 749px)
|
||||
font-size: 0.66rem
|
||||
|
||||
> div.option
|
||||
box-shadow: 1px 1px 1px #000
|
||||
display: flex
|
||||
margin: 0.25em 0.25ch
|
||||
cursor: pointer
|
||||
|
||||
:global(.icon)
|
||||
padding: 0.1em 0.5ch
|
||||
padding-top: 0.35em
|
||||
color: var(--color)
|
||||
|
||||
input, :global(div.rangeSlider)
|
||||
width: 4rem
|
||||
font-size: 0.9rem
|
||||
padding-left: 0
|
||||
background: $color-main1
|
||||
outline: none
|
||||
border: none
|
||||
text-align: center
|
||||
font-size: 1rem
|
||||
color: $color-main9
|
||||
&::-webkit-inner-spin-button
|
||||
-webkit-appearance: none
|
||||
margin: 0
|
||||
|
||||
moz-appearance: textfield
|
||||
|
||||
&:focus
|
||||
color: $color-main13
|
||||
|
||||
> div.color-input
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
> label
|
||||
font-size: 0.5em
|
||||
color: $color-main5
|
||||
text-align: center
|
||||
margin: 0
|
||||
line-height: 1em
|
||||
margin-top: 0.15rem !important
|
||||
padding-top: 0
|
||||
margin-bottom: -0.2em
|
||||
width: 2.5rem
|
||||
|
||||
&.short
|
||||
width: 2.2rem
|
||||
|
||||
|
||||
> input
|
||||
width: 2.5rem
|
||||
font-size: 0.8em
|
||||
background: none
|
||||
|
||||
&.short
|
||||
width: 2.2rem
|
||||
|
||||
|
||||
&.none
|
||||
background: $color-main1
|
||||
color: $color-main2
|
||||
&.off
|
||||
background-color: $color-main2
|
||||
color: $color-main6
|
||||
&.on
|
||||
background-color: $color-main2
|
||||
color: #00FF00
|
||||
&.red
|
||||
background-color: $color-main2-red
|
||||
color: $color-main6-redder
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script lang="ts" context="module">
|
||||
export const selectedColorPicker = writable<any>(null);</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Color } from "$lib/models/color";
|
||||
import { rgb2hsv } from "$lib/utils/color";
|
||||
import { tick } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export let color: Color;
|
||||
export let id: any = null;
|
||||
|
||||
let xy: boolean;
|
||||
let x: number;
|
||||
let y: number;
|
||||
|
||||
$: {
|
||||
if (color.xy) {
|
||||
xy = true;
|
||||
x = color.xy.x;
|
||||
y = color.xy.y;
|
||||
} else if (color.hs) {
|
||||
xy = false;
|
||||
x = Math.sin((360-color.hs.hue) * (Math.PI / 180)) * (color.hs.sat * 0.85);
|
||||
y = Math.cos((360-color.hs.hue) * (Math.PI / 180)) * (color.hs.sat * 0.85);
|
||||
} else if (color.k) {
|
||||
const angle = 2.5 + ((color.k - 1000) / 11000) * 355;
|
||||
|
||||
x = Math.sin((180-angle) * (Math.PI / 180)) * 0.95;
|
||||
y = Math.cos((180-angle) * (Math.PI / 180)) * 0.95;
|
||||
} else if (color.rgb) {
|
||||
const hs = rgb2hsv(color.rgb);
|
||||
|
||||
xy = false;
|
||||
x = Math.sin((360-hs.hue) * (Math.PI / 180)) * (hs.sat * 0.85);
|
||||
y = Math.cos((360-hs.hue) * (Math.PI / 180)) * (hs.sat * 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
function onClickColor(event: MouseEvent & { currentTarget: EventTarget & HTMLDivElement; }) {
|
||||
if (xy) {
|
||||
const x = Math.floor((event.offsetX / event.currentTarget.offsetWidth) * 1000) / 1000;
|
||||
const y = Math.floor((event.offsetY / event.currentTarget.offsetHeight) * 1000) / 1000;
|
||||
color = { xy: { x, y } };
|
||||
} else {
|
||||
const x = 2 * ((event.offsetX / event.currentTarget.offsetWidth) - 0.505);
|
||||
const y = 2 * ((event.offsetY / event.currentTarget.offsetHeight) - 0.505);
|
||||
|
||||
const dist = Math.sqrt((x*x)+(y*y));
|
||||
const angle = ((Math.atan2(y/dist, x/dist)*(180/Math.PI))+360) % 360.0
|
||||
|
||||
if (dist > 0.9) {
|
||||
color = { k: Math.max(Math.min(Math.floor((((((angle + 90) % 360)-2.5)/355) * 11000) + 1000), 12000), 1000) }
|
||||
} else {
|
||||
color = { hs: { hue: Math.floor((270+angle)%360), sat: Math.floor(Math.min(dist / 0.85, 1) * 100) / 100 } };
|
||||
}
|
||||
}
|
||||
|
||||
tick().then(() => {
|
||||
tick().then(() => { $selectedColorPicker = id; });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $selectedColorPicker == id}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="color-picker" on:click={onClickColor}>
|
||||
{#if xy}
|
||||
<img draggable="false" alt="color wheel" src="/color-xy.png" />
|
||||
<div class="dot" style="left: calc({x} * 10ch); top: calc({y-1} * 10ch)">
|
||||
<div class="ring"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<img draggable="false" alt="color wheel" src="/color-hsk.png" />
|
||||
<div class="dot" style="left: calc({((x/2)+0.5)} * 10ch + 0.5px); top: calc({((y/2)+0.5)-1} * 10ch + 0.5px)">
|
||||
<div class="ring"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
@import "$lib/css/colors.sass"
|
||||
|
||||
div.color-picker
|
||||
position: relative
|
||||
width: 10ch
|
||||
height: 10ch
|
||||
|
||||
> img
|
||||
width: 10ch
|
||||
height: 10ch
|
||||
|
||||
div.dot
|
||||
position: relative
|
||||
z-index: 10
|
||||
pointer-events: none
|
||||
|
||||
div.ring
|
||||
position: relative
|
||||
left: -0.525ch
|
||||
top: -0.95ch
|
||||
width: 1ch
|
||||
height: 1ch
|
||||
border: 2px solid $color-main4
|
||||
box-sizing: border-box
|
||||
border-radius: 1ch
|
||||
</style>
|
||||
@@ -54,6 +54,7 @@
|
||||
import { faCircleDot } from "@fortawesome/free-solid-svg-icons/faCircleDot";
|
||||
import { faMasksTheater } from "@fortawesome/free-solid-svg-icons/faMasksTheater";
|
||||
import { faTag } from "@fortawesome/free-solid-svg-icons/faTag";
|
||||
import { faFilter } from "@fortawesome/free-solid-svg-icons/faFilter";
|
||||
|
||||
const icons = {
|
||||
"clock": faClock,
|
||||
@@ -88,6 +89,7 @@
|
||||
"circle_dot": faCircleDot,
|
||||
"masks_theater": faMasksTheater,
|
||||
"tag": faTag,
|
||||
"filter": faFilter,
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" context="module">
|
||||
let nextId = 0;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { rgbString, type Color, stringifyColor, parseColor } from "$lib/models/color";
|
||||
import { hsToHsl, kToRgb, xyToRgb } from "$lib/utils/color";
|
||||
import ColorPicker, { selectedColorPicker } from "../ColorPicker.svelte";
|
||||
import BFormOption from "./BFormOption.svelte";
|
||||
import BFormParameter from "./BFormParameter.svelte";
|
||||
|
||||
export let value: string | null;
|
||||
|
||||
let colorPickerId = `BFormColorOption:${++nextId}`
|
||||
|
||||
let color: Color | null;
|
||||
$: color = parseColor(value);
|
||||
$: submit(color);
|
||||
|
||||
function toggle(event: MouseEvent & { currentTarget: EventTarget & HTMLDivElement; }) {
|
||||
if (event.shiftKey) {
|
||||
if (color !== null) {
|
||||
if ($selectedColorPicker !== colorPickerId) {
|
||||
$selectedColorPicker = colorPickerId;
|
||||
} else {
|
||||
$selectedColorPicker = null;
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (color === null || (color.hs == null && color.k == null)) {
|
||||
color = { k: 2750 }
|
||||
$selectedColorPicker = colorPickerId;
|
||||
} else {
|
||||
color = null;
|
||||
|
||||
if ($selectedColorPicker === colorPickerId) {
|
||||
$selectedColorPicker = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function submit(color: Color | null) {
|
||||
value = stringifyColor(color);
|
||||
}
|
||||
|
||||
let colorStr = "";
|
||||
$: {
|
||||
if (color !== null) {
|
||||
if (color.rgb != null) {
|
||||
colorStr = rgbString(color.rgb);
|
||||
} else if (color.hs != null) {
|
||||
colorStr = hsToHsl(color.hs);
|
||||
} else if (color.xy != null) {
|
||||
colorStr = rgbString(xyToRgb(color.xy));
|
||||
} else {
|
||||
colorStr = rgbString(kToRgb(color.k || 2750));
|
||||
}
|
||||
} else {
|
||||
colorStr = "#000000";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<BFormOption on:click={toggle} icon="palette" state={(color !== null) || null} color={colorStr}>
|
||||
{#if color != null}
|
||||
<div class="picker-wrapper">
|
||||
<ColorPicker bind:color={color} id={colorPickerId} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if color?.xy != null}
|
||||
<BFormParameter narrower label="X" type="number" max={1} step={0.01} bind:value={color.xy.x} />
|
||||
<BFormParameter narrower label="Y" type="number" max={1} step={0.01} bind:value={color.xy.y} />
|
||||
{/if}
|
||||
{#if color?.hs != null}
|
||||
<BFormParameter narrower label="Hue" type="number" max={360} step={1} bind:value={color.hs.hue} />
|
||||
<BFormParameter narrower label="Sat" type="number" max={1} step={0.01} bind:value={color.hs.sat} />
|
||||
{/if}
|
||||
{#if color?.rgb != null}
|
||||
<BFormParameter narrowest label="R" type="number" max={1} step={0.01} bind:value={color.rgb.red} />
|
||||
<BFormParameter narrowest label="G" type="number" max={1} step={0.01} bind:value={color.rgb.green} />
|
||||
<BFormParameter narrowest label="B" type="number" max={1} step={0.01} bind:value={color.rgb.blue} />
|
||||
{/if}
|
||||
{#if color?.k != null}
|
||||
<BFormParameter narrow label="Kelvin" type="number" min={1000} max={12000} step={50} bind:value={color.k} />
|
||||
{/if}
|
||||
</BFormOption>
|
||||
|
||||
<style lang="sass">
|
||||
div.picker-wrapper
|
||||
position: relative
|
||||
left: 0.5ch
|
||||
top: -11ch
|
||||
width: 0
|
||||
height: 0
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import BFormOption from "./BFormOption.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<BFormOption on:click={() => dispatch("delete")} red state icon=trash></BFormOption>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import BFormOption from "./BFormOption.svelte";
|
||||
import BFormParameter from "./BFormParameter.svelte";
|
||||
|
||||
export let value: number | null;
|
||||
|
||||
function update(percentage: number) {
|
||||
if (value !== null) {
|
||||
value = percentage / 100;
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (value === null) {
|
||||
value = 0.50;
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
}
|
||||
|
||||
let percentage: number
|
||||
$: percentage = Math.max(Math.min((value || 0) * 100, 100), 0);
|
||||
$: update(percentage);
|
||||
</script>
|
||||
|
||||
<BFormOption on:click={toggle} icon="cirlce_notch" state={(value !== null) || null}>
|
||||
<BFormParameter type="number" label="%" bind:value={percentage} min={0} max={100} step={1} narrower />
|
||||
</BFormOption>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="bform-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
div.bform-body
|
||||
display: flex
|
||||
user-select: none
|
||||
font-size: 1rem
|
||||
width: 100%
|
||||
flex-wrap: wrap
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts" context="module">
|
||||
import { getContext, setContext } from "svelte";
|
||||
|
||||
export const ctxKey = {};
|
||||
|
||||
export function getBFormOptionEnabled() {
|
||||
return getContext(ctxKey) as Readable<boolean>
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Icon, { type IconName } from "../Icon.svelte";
|
||||
import { writable, type Readable } from "svelte/store";
|
||||
|
||||
export let state: boolean | null = false;
|
||||
export let color: string = "hsl(240, 8%, 56%)";
|
||||
export let icon: IconName = "check";
|
||||
export let unclickable: boolean = false;
|
||||
export let red: boolean = false;
|
||||
|
||||
const enabled = writable(state === true);
|
||||
$: $enabled = state === true;
|
||||
|
||||
setContext(ctxKey, { subscribe: enabled.subscribe });
|
||||
</script>
|
||||
|
||||
<div class="bform-option" class:unclickable class:on={state === true} class:off={state === false} style="--color: {color}">
|
||||
<Icon on:click block name={icon} />
|
||||
<div class="bform-option-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import "$lib/css/colors.sass"
|
||||
|
||||
div.bform-option
|
||||
box-shadow: 1px 1px 1px #000
|
||||
display: flex
|
||||
margin: 0.25em 0.25ch
|
||||
min-height: 1.9rem
|
||||
cursor: pointer
|
||||
background: $color-main1
|
||||
|
||||
:global(.icon)
|
||||
padding: 0.1em 0.5ch
|
||||
padding-top: 0.45em
|
||||
color: $color-main2
|
||||
|
||||
&.off
|
||||
background-color: $color-main2
|
||||
:global(.icon)
|
||||
color: $color-main4
|
||||
|
||||
&.on
|
||||
background-color: $color-main2
|
||||
:global(.icon)
|
||||
color: var(--color)
|
||||
|
||||
&.red
|
||||
background-color: $color-main2-red
|
||||
:global(.icon)
|
||||
color: $color-main6-redder
|
||||
|
||||
&.unclickable
|
||||
cursor: default
|
||||
|
||||
div.bform-option-body
|
||||
display: flex
|
||||
flex-direction: row
|
||||
cursor: default
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { getBFormOptionEnabled } from "./BFormOption.svelte";
|
||||
|
||||
export let label: string;
|
||||
export let type: "number" | "text" | "select";
|
||||
export let value: number | string;
|
||||
export let wide: boolean = false;
|
||||
export let narrow: boolean = false;
|
||||
export let narrower: boolean = false;
|
||||
export let narrowest: boolean = false;
|
||||
export let min: number = 0;
|
||||
export let max: number = 100;
|
||||
export let step: number = 1;
|
||||
export let options: {value: string | number, label: string}[] = [];
|
||||
|
||||
const enabled = getBFormOptionEnabled();
|
||||
</script>
|
||||
|
||||
{#if $enabled}
|
||||
<div class="bform-parameter" class:wide class:narrow class:narrower class:narrowest>
|
||||
<label for="input_{label}">{label}</label>
|
||||
{#if type === "number"}
|
||||
<input class="custom" on:blur on:focus type="number" bind:value={value} {min} {max} {step} />
|
||||
{:else if type === "text"}
|
||||
<input class="custom" on:blur on:focus type="text" bind:value={value} />
|
||||
{:else if type === "select"}
|
||||
<select class="custom" bind:value={value}>
|
||||
{#each options as opt (opt.value)}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
@import "$lib/css/colors.sass"
|
||||
|
||||
div.bform-parameter
|
||||
display: flex
|
||||
flex-direction: column
|
||||
background: $color-main1
|
||||
height: 100%
|
||||
|
||||
> label
|
||||
font-size: 0.5em
|
||||
color: $color-main5
|
||||
text-align: center
|
||||
margin: 0
|
||||
line-height: 1em
|
||||
margin-top: 0.15rem !important
|
||||
padding-top: 0
|
||||
margin-bottom: -0em
|
||||
width: 10rem
|
||||
|
||||
> input, > select
|
||||
width: 10rem
|
||||
font-size: 0.9rem
|
||||
padding-left: 0
|
||||
background: none
|
||||
outline: none
|
||||
border: none
|
||||
text-align: center
|
||||
font-size: 1rem
|
||||
color: $color-main9
|
||||
|
||||
> option
|
||||
background: $color-main1
|
||||
|
||||
&::-webkit-inner-spin-button
|
||||
-webkit-appearance: none
|
||||
margin: 0
|
||||
|
||||
-webkit-appearance: none
|
||||
-moz-appearance: none
|
||||
moz-appearance: none
|
||||
appearance: none
|
||||
|
||||
&:focus
|
||||
color: $color-main13
|
||||
|
||||
> select
|
||||
margin-top: 0.08rem
|
||||
|
||||
&.wide
|
||||
> input, > label, > select
|
||||
text-align: left
|
||||
width: 20rem
|
||||
> label
|
||||
padding-left: 0.2rch
|
||||
|
||||
&.narrow
|
||||
> input, > label, > select
|
||||
width: calc(6rem + 0.22ch)
|
||||
|
||||
&.narrower
|
||||
> input, > label, > select
|
||||
width: 3rem
|
||||
|
||||
&.narrowest
|
||||
> input, > label, > select
|
||||
width: calc(2rem - 0.11ch)
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import BFormOption from "./BFormOption.svelte";
|
||||
|
||||
export let value: boolean | null;
|
||||
|
||||
function toggle() {
|
||||
if (value === null) {
|
||||
value = true;
|
||||
} else if (value === true) {
|
||||
value = false;
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<BFormOption icon="power" on:click={toggle} state={value} color="#00FF00"></BFormOption>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import BFormOption from "./BFormOption.svelte";
|
||||
import BFormParameter from "./BFormParameter.svelte";
|
||||
|
||||
export let value: number | null;
|
||||
|
||||
function toggle() {
|
||||
if (value === null) {
|
||||
value = 20;
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<BFormOption on:click={toggle} icon="temperature_half" state={(value !== null) || null}>
|
||||
{#if value != null}
|
||||
<BFormParameter type="number" label="Celsius" bind:value={value} min={10} max={32} step={1} narrower />
|
||||
{/if}
|
||||
</BFormOption>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { State } from "$lib/models/device";
|
||||
import type { ColorRGB } from '$lib/models/color';
|
||||
import BFormLine from "../bforms/BFormLine.svelte";
|
||||
import BFormTemperatureOption from "../bforms/BFormTemperatureOption.svelte";
|
||||
import BFormColorOption from "../bforms/BFormColorOption.svelte";
|
||||
import BFormIntensityOption from "../bforms/BFormIntensityOption.svelte";
|
||||
import BFormPowerOption from "../bforms/BFormPowerOption.svelte";
|
||||
import BFormDeleteOption from "../bforms/BFormDeleteOption.svelte";
|
||||
|
||||
export let value: State;
|
||||
export let deletable: boolean = false;
|
||||
|
||||
$: console.log(value);
|
||||
</script>
|
||||
|
||||
<BFormLine>
|
||||
<BFormPowerOption bind:value={value.power} />
|
||||
<BFormIntensityOption bind:value={value.intensity} />
|
||||
<BFormColorOption bind:value={value.color} />
|
||||
<BFormTemperatureOption bind:value={value.temperature} />
|
||||
{#if deletable}
|
||||
<BFormDeleteOption on:delete />
|
||||
{/if}
|
||||
</BFormLine>
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts" context="module">
|
||||
import { dev } from "$app/environment";
|
||||
import { fetchUIState } from "$lib/client/lucifer";
|
||||
import type { DeviceIconName } from "$lib/components/DeviceIcon.svelte";
|
||||
import type Assignment from "$lib/models/assignment";
|
||||
import type Device from "$lib/models/device";
|
||||
import type { UIStatePatch } from "$lib/models/uistate";
|
||||
import type { UIStatePatch, UIStatePatch2 } from "$lib/models/uistate";
|
||||
import type UIState from "$lib/models/uistate";
|
||||
import { getContext, onMount, setContext } from "svelte";
|
||||
import { derived, writable, type Readable } from "svelte/store";
|
||||
@@ -110,136 +111,61 @@
|
||||
}
|
||||
|
||||
async function connectSocket() {
|
||||
let url = `ws://${window.location.host}/subscribe`;
|
||||
let url = `ws://${window.location.host}/subscribe-simple`;
|
||||
if (import.meta.env.VITE_LUCIFER4_BACKEND_URL != null) {
|
||||
url = import.meta.env.VITE_LUCIFER4_BACKEND_URL.replace("http", "ws") +"/subscribe";
|
||||
url = import.meta.env.VITE_LUCIFER4_BACKEND_URL.replace("http", "ws") +"/subscribe-simple";
|
||||
}
|
||||
|
||||
const currSocket = new WebSocket(url);
|
||||
|
||||
currSocket.onmessage = (msg) => {
|
||||
const patches: UIStatePatch[] = JSON.parse(msg.data);
|
||||
const patch: UIStatePatch2 = JSON.parse(msg.data);
|
||||
|
||||
state.update(s => {
|
||||
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));
|
||||
state.update(state => {
|
||||
if (patch.full != null) {
|
||||
state = patch.full;
|
||||
}
|
||||
|
||||
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 DeviceIconName
|
||||
}
|
||||
|
||||
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 !== patch.removeAlias),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...s.devices[patch.id],
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
})(patch.device)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const deviceId in patch.devices) {
|
||||
if (!patch.devices.hasOwnProperty(deviceId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (patch.assignment) {
|
||||
if (patch.assignment.delete) {
|
||||
const assignments = {...s.assignments};
|
||||
delete assignments[patch.assignment.id];
|
||||
s = {...s, assignments};
|
||||
} else if (patch.assignment.addDeviceId) {
|
||||
s = {
|
||||
...s,
|
||||
assignments: {
|
||||
...s.assignments,
|
||||
[patch.assignment.id]: {
|
||||
...s.assignments[patch.assignment.id],
|
||||
deviceIds: [...s.assignments[patch.assignment.id].deviceIds || [], patch.assignment.addDeviceId],
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (patch.assignment.removeDeviceId) {
|
||||
s = {
|
||||
...s,
|
||||
assignments: {
|
||||
...s.assignments,
|
||||
[patch.assignment.id]: {
|
||||
...s.assignments[patch.assignment.id],
|
||||
deviceIds: s.assignments[patch.assignment.id].deviceIds.map(id => id === patch.assignment.removeDeviceId ? "" : id),
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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];
|
||||
s = {...s, scripts};
|
||||
} else {
|
||||
s = {
|
||||
...s,
|
||||
scripts: {
|
||||
...s.scripts,
|
||||
[patch.script.id]: {
|
||||
...s.scripts[patch.script.id],
|
||||
...patch.script,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
state.devices = {...state.devices}
|
||||
if (patch.devices[deviceId] != null) {
|
||||
state.devices[deviceId] = patch.devices[deviceId]
|
||||
} else {
|
||||
delete state.devices[deviceId]
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
for (const assignmentId in patch.assignments) {
|
||||
if (!patch.assignments.hasOwnProperty(assignmentId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
state.assignments = {...state.assignments}
|
||||
if (patch.assignments[assignmentId] != null) {
|
||||
state.assignments[assignmentId] = patch.assignments[assignmentId]
|
||||
} else {
|
||||
delete state.assignments[assignmentId]
|
||||
}
|
||||
}
|
||||
|
||||
for (const scriptName in patch.scripts) {
|
||||
if (!patch.scripts.hasOwnProperty(scriptName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
state.scripts = {...state.scripts}
|
||||
if (patch.scripts[scriptName] != null) {
|
||||
state.scripts[scriptName] = patch.scripts[scriptName]
|
||||
} else {
|
||||
delete state.scripts[scriptName]
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -266,15 +192,11 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
reload();
|
||||
const interval = setInterval(reload, 60000);
|
||||
|
||||
connectSocket();
|
||||
|
||||
window.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState == "visible") {
|
||||
console.log("Reconnecting");
|
||||
reload();
|
||||
connectSocket();
|
||||
} else {
|
||||
if (socket != null) {
|
||||
@@ -283,8 +205,6 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
setContext<StateContextData>(ctxKey, {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { runCommand } from "$lib/client/lucifer";
|
||||
import AssignmentState from "$lib/components/AssignmentState.svelte";
|
||||
import AssignmentState from "$lib/components/scripting/ScriptAssignmentState.svelte";
|
||||
import Button from "$lib/components/Button.svelte";
|
||||
import Checkbox from "$lib/components/Checkbox.svelte";
|
||||
import type { DeviceIconName } from "$lib/components/DeviceIcon.svelte";
|
||||
@@ -18,6 +18,7 @@
|
||||
import { toEffectRaw, type EffectRaw, fromEffectRaw } from "$lib/models/assignment";
|
||||
import type { DeviceEditOp } from "$lib/models/device";
|
||||
import { iconName } from "@fortawesome/free-solid-svg-icons/faQuestion";
|
||||
import ScriptAssignmentState from "$lib/components/scripting/ScriptAssignmentState.svelte";
|
||||
|
||||
const { modal } = getModalContext();
|
||||
const { selectedMasks, selectedMap, selectedList } = getSelectedContext();
|
||||
@@ -241,48 +242,6 @@
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<ModalSection bind:expanded={enableRename} title="Rename">
|
||||
<label for="name">New Name</label>
|
||||
<input type="text" name="name" bind:value={newName} />
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableRoom} title="Change Room">
|
||||
<label for="newRoom">Select Room</label>
|
||||
<select bind:value={newRoom}>
|
||||
{#each roomOptions as roomOption}
|
||||
<option value={roomOption}>{roomOption}</option>
|
||||
{/each}
|
||||
<option value="">Create Room</option>
|
||||
</select>
|
||||
{#if newRoom == ""}
|
||||
<label for="customRoom">New Room</label>
|
||||
<input type="text" name="customRoom" bind:value={customRoom} />
|
||||
{/if}
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableGroup} title="Change Group">
|
||||
<label for="newGroup">Select Group</label>
|
||||
<select bind:value={newGroup}>
|
||||
{#each groupOptions as groupOption}
|
||||
<option value={groupOption}>{groupOption}</option>
|
||||
{/each}
|
||||
<option value="">Create Group</option>
|
||||
</select>
|
||||
{#if newGroup == ""}
|
||||
<label for="customGroup">New Group</label>
|
||||
<input type="text" name="customGroup" bind:value={customGroup} />
|
||||
{/if}
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableIcon} title="Change Icon">
|
||||
<label for="icon">New Icon</label>
|
||||
<DeviceIconSelector bind:value={newIcon} />
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableTag} title="Change Tags">
|
||||
<label for="icon">Tags</label>
|
||||
<TagInput bind:value={newTags} />
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableRole} title="Change Roles">
|
||||
<label for="icon">Roles</label>
|
||||
<TagInput bind:value={newRoles} />
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableAssign} title="Assign">
|
||||
<HSplit reverse>
|
||||
<HSplitPart>
|
||||
@@ -334,12 +293,54 @@
|
||||
<HSplitPart weight={1.0}>
|
||||
<label for="states">States</label>
|
||||
{#each newEffect.states as state, i }
|
||||
<AssignmentState deletable bind:value={state} on:delete={() => removeEffectState(i)} />
|
||||
<ScriptAssignmentState deletable bind:value={state} on:delete={() => removeEffectState(i)} />
|
||||
{/each}
|
||||
<Button on:click={addEffectState} icon><Icon name="plus" /></Button>
|
||||
</HSplitPart>
|
||||
</HSplit>
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableRename} title="Rename">
|
||||
<label for="name">New Name</label>
|
||||
<input type="text" name="name" bind:value={newName} />
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableRoom} title="Change Room">
|
||||
<label for="newRoom">Select Room</label>
|
||||
<select bind:value={newRoom}>
|
||||
{#each roomOptions as roomOption}
|
||||
<option value={roomOption}>{roomOption}</option>
|
||||
{/each}
|
||||
<option value="">Create Room</option>
|
||||
</select>
|
||||
{#if newRoom == ""}
|
||||
<label for="customRoom">New Room</label>
|
||||
<input type="text" name="customRoom" bind:value={customRoom} />
|
||||
{/if}
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableGroup} title="Change Group">
|
||||
<label for="newGroup">Select Group</label>
|
||||
<select bind:value={newGroup}>
|
||||
{#each groupOptions as groupOption}
|
||||
<option value={groupOption}>{groupOption}</option>
|
||||
{/each}
|
||||
<option value="">Create Group</option>
|
||||
</select>
|
||||
{#if newGroup == ""}
|
||||
<label for="customGroup">New Group</label>
|
||||
<input type="text" name="customGroup" bind:value={customGroup} />
|
||||
{/if}
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableIcon} title="Change Icon">
|
||||
<label for="icon">New Icon</label>
|
||||
<DeviceIconSelector bind:value={newIcon} />
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableTag} title="Change Tags">
|
||||
<label for="icon">Tags</label>
|
||||
<TagInput bind:value={newTags} />
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableRole} title="Change Roles">
|
||||
<label for="icon">Roles</label>
|
||||
<TagInput bind:value={newRoles} />
|
||||
</ModalSection>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -29,6 +29,40 @@ export enum ColorFlags {
|
||||
CFlagKelvin = 1 << 4,
|
||||
}
|
||||
|
||||
export function parseColor(s?: string | null): Color | null {
|
||||
if (!s) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [kind, value] = s.split(":");
|
||||
const [x,y,z] = value.split("|")[0].split(",").map(v => parseFloat(v));
|
||||
|
||||
switch (kind) {
|
||||
case "hsk": return { hs: { hue: x, sat: y }, k: z };
|
||||
case "hs": return { hs: { hue: x, sat: y } };
|
||||
case "rgb": return { rgb: { red: x, green: y, blue: z } };
|
||||
case "k": return { k: x };
|
||||
case "xy": return { xy: {x, y} };
|
||||
default: throw new Error(`Unknown color: ${s}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function stringifyColor(c?: Color | null): string | null {
|
||||
if (c?.hs && c?.k) {
|
||||
return `hsk:${c.hs.hue},${c.hs.sat},${c.k}`;
|
||||
} else if (c?.hs) {
|
||||
return `hs:${c.hs.hue},${c.hs.sat}`;
|
||||
} else if (c?.rgb) {
|
||||
return `rgb:${c.rgb.red},${c.rgb.green},${c.rgb.blue}`;
|
||||
} else if (c?.xy) {
|
||||
return `xy:${c.xy.x},${c.xy.y}`;
|
||||
} else if (c?.k) {
|
||||
return `k:${c.k}`
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function rgbString({red,green,blue}: ColorRGB) {
|
||||
return `rgb(${red*255},${green*255},${blue*255})`
|
||||
}
|
||||
|
||||
@@ -13,3 +13,7 @@ export interface UIStatePatch {
|
||||
assignment: Partial<Assignment> & { id: string, delete?: boolean, addDeviceId?: string, removeDeviceId?: string }
|
||||
script: Partial<Script> & { id: string, delete?: boolean }
|
||||
}
|
||||
|
||||
export interface UIStatePatch2 extends UIState {
|
||||
full?: UIState
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,7 @@
|
||||
import type { ColorRGB } from "$lib/models/color";
|
||||
import type { ColorHS, ColorRGB, ColorXY } from "$lib/models/color";
|
||||
|
||||
import XY_MAP from "./color-xy.json";
|
||||
import K_MAP from "./color-k.json";
|
||||
|
||||
export function rgbToHex(rgb: ColorRGB): string {
|
||||
const r = Math.min(Math.round(rgb.red * 255), 255).toString(16).padStart(2, "0");
|
||||
@@ -6,3 +9,76 @@ export function rgbToHex(rgb: ColorRGB): string {
|
||||
const b = Math.min(Math.round(rgb.blue * 255), 255).toString(16).padStart(2, "0");
|
||||
return ["#", r, g, b].join("");
|
||||
}
|
||||
|
||||
export function kToRgb(k: number) {
|
||||
if (k < 1000) {
|
||||
k = 1000;
|
||||
}
|
||||
if (k > 12000) {
|
||||
k = 12000;
|
||||
}
|
||||
|
||||
const key = (Math.round(k / 50) * 50).toFixed(0);
|
||||
|
||||
return (K_MAP as Record<string, ColorRGB>)[key];
|
||||
}
|
||||
|
||||
export function xyToRgb(xy: ColorXY) {
|
||||
const x = (Math.round((xy.x||0) * 100) / 100);
|
||||
const y = (Math.round((xy.y||0) * 100) / 100);
|
||||
|
||||
return (XY_MAP as Record<string, ColorRGB>)[`${x.toFixed(2)},${y.toFixed(2)}`];
|
||||
}
|
||||
|
||||
export function hsToHsl(hs: ColorHS): string {
|
||||
const l=1-(hs.sat/2);
|
||||
const m=Math.min(l,1-l);
|
||||
|
||||
return `hsl(${hs.hue}, ${(m?(1-l)/m:0)*100}%, ${l*100}%)`;
|
||||
}
|
||||
|
||||
export function rgb2hsv (rgb: ColorRGB): ColorHS {
|
||||
const r = rgb.red;
|
||||
const g = rgb.green;
|
||||
const b = rgb.blue;
|
||||
|
||||
let rabs: number, gabs: number, babs: number;
|
||||
let rr: number, gg: number, bb: number;
|
||||
let h: number = 0, s: number = 0, v: number;
|
||||
let diff: number;
|
||||
|
||||
const diffc = (c: number) => (v - c) / 6 / diff + 1 / 2;
|
||||
const percentRoundFn = (num: number) => Math.round(num * 100) / 100;
|
||||
|
||||
rabs = r / 255;
|
||||
gabs = g / 255;
|
||||
babs = b / 255;
|
||||
v = Math.max(rabs, gabs, babs),
|
||||
diff = v - Math.min(rabs, gabs, babs);
|
||||
if (diff == 0) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
s = diff / v;
|
||||
rr = diffc(rabs);
|
||||
gg = diffc(gabs);
|
||||
bb = diffc(babs);
|
||||
|
||||
if (rabs === v) {
|
||||
h = bb - gg;
|
||||
} else if (gabs === v) {
|
||||
h = (1 / 3) + rr - bb;
|
||||
} else if (babs === v) {
|
||||
h = (2 / 3) + gg - rr;
|
||||
}
|
||||
if (h < 0) {
|
||||
h += 1;
|
||||
} else if (h > 1) {
|
||||
h -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hue: Math.round(h * 360),
|
||||
sat: s,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import AssignmentState from "$lib/components/AssignmentState.svelte";
|
||||
import Lamp from "$lib/components/Lamp.svelte";
|
||||
import MetaLamp from "$lib/components/MetaLamp.svelte";
|
||||
import RoomHeader from "$lib/components/RoomHeader.svelte";
|
||||
@@ -8,24 +7,13 @@
|
||||
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
||||
import DeviceModal from "$lib/modals/DeviceModal.svelte";
|
||||
|
||||
const {selectedList} = getSelectedContext();
|
||||
const {roomList} = getStateContext();
|
||||
const {modal} = getModalContext();
|
||||
|
||||
function handleKeyPress(e: KeyboardEvent) {
|
||||
if ($modal.kind === "closed" && e.shiftKey) {
|
||||
if ($modal.kind === "closed" && $selectedList.length && !e.ctrlKey && !e.shiftKey) {
|
||||
switch (e.key.toLocaleLowerCase()) {
|
||||
case 'a':
|
||||
modal.set({kind: "device.edit", op: "assign"});
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'r':
|
||||
modal.set({kind: "device.edit", op: "rename"});
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'i':
|
||||
modal.set({kind: "device.edit", op: "change_icon"});
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'e':
|
||||
modal.set({kind: "device.edit", op: "none"});
|
||||
e.preventDefault();
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@@ -116,7 +116,7 @@ func (col *Color) ToRGB() (col2 Color, ok bool) {
|
||||
col2 = Color{RGB: &rgb}
|
||||
ok = true
|
||||
} else if col.K != nil {
|
||||
rgb := kToRGB(*col.K)
|
||||
rgb := KtoRGB(*col.K)
|
||||
col2 = Color{RGB: &rgb}
|
||||
ok = true
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func (col *Color) ToHS() (col2 Color, ok bool) {
|
||||
col2 = Color{HS: &hs}
|
||||
ok = true
|
||||
} else if col.K != nil {
|
||||
hs := kToRGB(*col.K).ToHS()
|
||||
hs := KtoRGB(*col.K).ToHS()
|
||||
col2 = Color{HS: &hs}
|
||||
ok = true
|
||||
}
|
||||
@@ -202,7 +202,7 @@ func (col *Color) ToXY() (col2 Color, ok bool) {
|
||||
col2 = Color{XY: &xy}
|
||||
ok = true
|
||||
} else if col.K != nil {
|
||||
xy := kToRGB(*col.K).ToXY()
|
||||
xy := KtoRGB(*col.K).ToXY()
|
||||
col2 = Color{XY: &xy}
|
||||
ok = true
|
||||
}
|
||||
@@ -506,7 +506,7 @@ func hex2digit(h byte) int {
|
||||
}
|
||||
}
|
||||
|
||||
func kToRGB(kelvin int) RGB {
|
||||
func KtoRGB(kelvin int) RGB {
|
||||
if kelvin < 1000 {
|
||||
kelvin = 1000
|
||||
} else if kelvin > 12000 {
|
||||
@@ -523,9 +523,9 @@ func kToRGB(kelvin int) RGB {
|
||||
ceilRGB := kelvinRGBTable[ceil]
|
||||
|
||||
return RGB{
|
||||
Red: (floorRGB.Red * fac) + (ceilRGB.Red * (1 - fac)),
|
||||
Green: (floorRGB.Green * fac) + (ceilRGB.Green * (1 - fac)),
|
||||
Blue: (floorRGB.Blue * fac) + (ceilRGB.Blue * (1 - fac)),
|
||||
Red: (floorRGB.Red * (1 - fac)) + (ceilRGB.Red * fac),
|
||||
Green: (floorRGB.Green * (1 - fac)) + (ceilRGB.Green * fac),
|
||||
Blue: (floorRGB.Blue * (1 - fac)) + (ceilRGB.Blue * fac),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,9 +161,9 @@ type XY struct {
|
||||
}
|
||||
|
||||
func (xy XY) ToRGB() RGB {
|
||||
h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
|
||||
h, s, _ := colorful.Xyy(xy.X, math.Max(xy.Y, 0.0001), 0.5).Hsv()
|
||||
c := colorful.Hsv(math.Mod(h, 360), s, 1)
|
||||
return RGB{Red: c.R, Green: c.G, Blue: c.B}
|
||||
return RGB{Red: math.Max(c.R, 0), Green: math.Max(c.G, 0), Blue: math.Max(c.B, 0)}
|
||||
}
|
||||
|
||||
func (xy XY) ToHS() HueSat {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"git.aiterp.net/lucifer3/server/services/script"
|
||||
"git.aiterp.net/lucifer3/server/services/uistate"
|
||||
"github.com/google/uuid"
|
||||
@@ -110,6 +111,86 @@ func New(addr string) (lucifer3.Service, error) {
|
||||
return nil
|
||||
})
|
||||
|
||||
e.GET("/subscribe-simple", func(c echo.Context) error {
|
||||
type ChangedPatch struct {
|
||||
Devices map[string]*uistate.Device `json:"devices"`
|
||||
Assignments map[uuid.UUID]*uistate.Assignment `json:"assignments"`
|
||||
Scripts map[string][]script.Line `json:"scripts"`
|
||||
Full *uistate.Data `json:"full,omitempty"`
|
||||
}
|
||||
|
||||
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sub := make(chan uistate.Patch, 64)
|
||||
svc.addSub(sub)
|
||||
|
||||
defer svc.removeSub(sub)
|
||||
defer ws.Close()
|
||||
|
||||
svc.mu.Lock()
|
||||
err = ws.WriteJSON(ChangedPatch{Full: &svc.data})
|
||||
svc.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patches := make([]uistate.Patch, 0, 128)
|
||||
for {
|
||||
patch := <-sub
|
||||
patches = append(patches[:0], patch)
|
||||
waitCh := time.After(time.Millisecond * 330)
|
||||
waiting := true
|
||||
for waiting {
|
||||
select {
|
||||
case patch, ok := <-sub:
|
||||
patches = append(patches, patch)
|
||||
waiting = ok
|
||||
case <-waitCh:
|
||||
waiting = false
|
||||
}
|
||||
}
|
||||
|
||||
statePatch := ChangedPatch{
|
||||
Devices: make(map[string]*uistate.Device),
|
||||
Assignments: make(map[uuid.UUID]*uistate.Assignment),
|
||||
Scripts: make(map[string][]script.Line),
|
||||
Full: nil,
|
||||
}
|
||||
|
||||
svc.mu.Lock()
|
||||
for _, patch := range patches {
|
||||
if patch.Device != nil {
|
||||
if patch.Device.Delete {
|
||||
statePatch.Devices[patch.Device.ID] = nil
|
||||
} else {
|
||||
statePatch.Devices[patch.Device.ID] = gentools.Ptr(svc.data.Devices[patch.Device.ID])
|
||||
}
|
||||
}
|
||||
if patch.Assignment != nil {
|
||||
if patch.Assignment.Delete {
|
||||
statePatch.Assignments[patch.Assignment.ID] = nil
|
||||
} else {
|
||||
statePatch.Assignments[patch.Assignment.ID] = gentools.Ptr(svc.data.Assignments[patch.Assignment.ID])
|
||||
}
|
||||
}
|
||||
if patch.Script != nil {
|
||||
statePatch.Scripts[patch.Script.Name] = svc.data.Scripts[patch.Script.Name]
|
||||
}
|
||||
}
|
||||
svc.mu.Unlock()
|
||||
|
||||
err := ws.WriteJSON(statePatch)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
e.POST("/command", func(c echo.Context) error {
|
||||
var input commandInput
|
||||
err := c.Bind(&input)
|
||||
|
||||
Reference in New Issue
Block a user