Gisle Aune
1 year ago
48 changed files with 1684 additions and 91 deletions
-
43effects/animation.go
-
4effects/gradient.go
-
5effects/serializable.go
-
3effects/vrange.go
-
101frontend/package-lock.json
-
5frontend/package.json
-
14frontend/src/lib/client/lucifer.ts
-
294frontend/src/lib/components/AssignmentState.svelte
-
31frontend/src/lib/components/Button.svelte
-
117frontend/src/lib/components/Checkbox.svelte
-
22frontend/src/lib/components/DeviceIcon.svelte
-
43frontend/src/lib/components/DeviceIconSelector.svelte
-
18frontend/src/lib/components/HSplit.svelte
-
18frontend/src/lib/components/HSplitPart.svelte
-
97frontend/src/lib/components/Icon.svelte
-
45frontend/src/lib/components/Lamp.svelte
-
32frontend/src/lib/components/Modal.svelte
-
15frontend/src/lib/components/ModalBody.svelte
-
59frontend/src/lib/components/ModalSection.svelte
-
92frontend/src/lib/components/TagInput.svelte
-
0frontend/src/lib/components/icons/shape_hexagon.svg
-
0frontend/src/lib/components/icons/shape_square.svg
-
0frontend/src/lib/components/icons/shape_triangle.svg
-
3frontend/src/lib/contexts/ModalContext.svelte
-
8frontend/src/lib/contexts/SelectContext.svelte
-
40frontend/src/lib/contexts/StateContext.svelte
-
4frontend/src/lib/css/colors.sass
-
345frontend/src/lib/modals/DeviceModal.svelte
-
54frontend/src/lib/models/assignment.ts
-
17frontend/src/lib/models/color.ts
-
17frontend/src/lib/models/command.ts
-
10frontend/src/lib/models/device.ts
-
2frontend/src/lib/models/uistate.ts
-
1frontend/src/routes/+layout.ts
-
44frontend/src/routes/+page.svelte
-
2frontend/svelte.config.js
-
17services/effectenforcer/service.go
-
19services/httpapiv1/service.go
-
32services/hue/bridge.go
-
9services/mysqldb/migrations/20230909214407_script_trigger_column_name.sql
-
1services/mysqldb/mysqlgen/models.go
-
9services/mysqldb/mysqlgen/script.sql.go
-
4services/mysqldb/queries/script.sql
-
4services/mysqldb/service.go
-
18services/nanoleaf/data.go
-
1services/script/trigger.go
-
10services/uistate/data.go
@ -0,0 +1,43 @@ |
|||||
|
package effects |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"git.aiterp.net/lucifer3/server/device" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type Solid struct { |
||||
|
States []device.State `json:"states,omitempty"` |
||||
|
AnimationMS int64 `json:"animationMs,omitempty"` |
||||
|
Interleave int `json:"interleave,omitempty"` |
||||
|
} |
||||
|
|
||||
|
func (e Solid) State(_, length, round int) device.State { |
||||
|
if len(e.States) == 0 { |
||||
|
return device.State{} |
||||
|
} |
||||
|
if len(e.States) == 1 { |
||||
|
return e.States[0] |
||||
|
} |
||||
|
|
||||
|
interleave := e.Interleave + 1 |
||||
|
if interleave < 1 { |
||||
|
interleave = 1 |
||||
|
} |
||||
|
|
||||
|
if interleave > 1 { |
||||
|
calcIndex := round % (len(e.States) * interleave) |
||||
|
return gradientState(append(e.States, e.States[0]), e.Interleave != 0, calcIndex, len(e.States)*interleave+1) |
||||
|
} else { |
||||
|
calcIndex := round % len(e.States) |
||||
|
return gradientState(e.States, e.Interleave != 0, calcIndex, len(e.States)*interleave) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (e Solid) Frequency() time.Duration { |
||||
|
return time.Duration(e.AnimationMS) * time.Millisecond |
||||
|
} |
||||
|
|
||||
|
func (e Solid) EffectDescription() string { |
||||
|
return fmt.Sprintf("Solid(states:%s, anim:%dms, interleave:%d)", statesDescription(e.States), e.AnimationMS, e.Interleave) |
||||
|
} |
@ -0,0 +1,294 @@ |
|||||
|
<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,31 @@ |
|||||
|
<script lang="ts"> |
||||
|
export let red: boolean = false; |
||||
|
export let icon: boolean = false; |
||||
|
</script> |
||||
|
|
||||
|
<!-- svelte-ignore a11y-click-events-have-key-events --> |
||||
|
<div class:red class:icon class="button" on:click><slot></slot></div> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
@import "$lib/css/colors.sass" |
||||
|
|
||||
|
div.button |
||||
|
box-shadow: 1px 1px 1px #000 |
||||
|
margin: 0.25em 0.25ch |
||||
|
cursor: pointer |
||||
|
user-select: none |
||||
|
font-size: 1rem |
||||
|
background-color: $color-main2 |
||||
|
color: $color-main6 |
||||
|
|
||||
|
> :global(.icon) |
||||
|
padding: 0.1em 0.5ch |
||||
|
padding-top: 0.35em |
||||
|
|
||||
|
&.icon |
||||
|
width: 2.8ch |
||||
|
|
||||
|
&.red |
||||
|
background-color: $color-main2-red |
||||
|
color: $color-main6-redder |
||||
|
</style> |
@ -0,0 +1,117 @@ |
|||||
|
<script lang="ts"> |
||||
|
import { createEventDispatcher } from "svelte"; |
||||
|
|
||||
|
import Icon, { type IconName } from "./Icon.svelte"; |
||||
|
|
||||
|
export let tabIndex: number | undefined | null = void(0); |
||||
|
export let checked = false; |
||||
|
export let centered = false; |
||||
|
export let disabled = false; |
||||
|
export let noLabel = false; |
||||
|
export let noBorder = false; |
||||
|
export let inline = false; |
||||
|
export let icon: IconName = "check"; |
||||
|
export let label = "(Missing label property)"; |
||||
|
|
||||
|
const dispatch = createEventDispatcher(); |
||||
|
|
||||
|
function handleClick() { |
||||
|
if (!disabled) { |
||||
|
checked = !checked; |
||||
|
dispatch('check', { checked }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handlePress(e: KeyboardEvent) { |
||||
|
if (!disabled && ["enter", " ", " ", "c"].includes(e.key?.toLowerCase())) { |
||||
|
checked = !checked; |
||||
|
dispatch('check', { checked }); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div |
||||
|
class="checkbox" |
||||
|
class:centered |
||||
|
class:noLabel |
||||
|
class:inline |
||||
|
role="checkbox" |
||||
|
aria-checked={checked} |
||||
|
aria-disabled={disabled} |
||||
|
tabindex={tabIndex} |
||||
|
on:keypress={handlePress} |
||||
|
on:click={handleClick} |
||||
|
> |
||||
|
<div class="box" class:noBorder class:disabled class:checked class:unchecked={!checked}> |
||||
|
<Icon name={icon} /> |
||||
|
</div> |
||||
|
{#if !noLabel} |
||||
|
<div class="label">{label}</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
@import "$lib/css/colors" |
||||
|
|
||||
|
div.checkbox |
||||
|
display: flex |
||||
|
flex-direction: row |
||||
|
flex-shrink: 0 |
||||
|
margin-top: 0.5em |
||||
|
margin-bottom: 1em |
||||
|
-webkit-user-select: none |
||||
|
-moz-user-select: none |
||||
|
user-select: none |
||||
|
transition: 250ms |
||||
|
padding: 0.1em |
||||
|
font-size: 0.9em |
||||
|
|
||||
|
&.noLabel |
||||
|
display: block |
||||
|
margin-top: 0.35em |
||||
|
margin-bottom: 0 |
||||
|
font-size: 0.75em |
||||
|
|
||||
|
div.checkbox.centered |
||||
|
margin: auto |
||||
|
|
||||
|
:global(div.checkbox + div.checkbox) |
||||
|
margin-top: -0.75em |
||||
|
|
||||
|
div.checkbox.inline |
||||
|
margin-top: 0.25rem |
||||
|
margin-bottom: 0.25rem |
||||
|
margin-right: 1rch |
||||
|
|
||||
|
div.box |
||||
|
cursor: pointer |
||||
|
border: 0.5px solid |
||||
|
padding: 0.05em 0.3ch |
||||
|
padding-top: 0.2em |
||||
|
line-height: 1 |
||||
|
background-color: $color-mainhalf |
||||
|
color: $color-mainhalf |
||||
|
border-color: $color-main5 |
||||
|
font-size: 0.9em |
||||
|
&.noBorder |
||||
|
color: $color-main2 |
||||
|
&.checked |
||||
|
color: $color-main9 |
||||
|
background-color: $color-main4 |
||||
|
&.disabled |
||||
|
color: $color-main1 |
||||
|
&.checked |
||||
|
color: $color-main5 |
||||
|
background-color: $color-main1 |
||||
|
&.noBorder |
||||
|
border: none |
||||
|
background: none |
||||
|
|
||||
|
div.label |
||||
|
margin: auto 0 |
||||
|
margin-left: 1ch |
||||
|
|
||||
|
-webkit-user-select: none |
||||
|
-moz-user-select: none |
||||
|
user-select: none |
||||
|
</style> |
@ -0,0 +1,43 @@ |
|||||
|
<script lang="ts"> |
||||
|
import { rgb, type ColorRGB } from "$lib/models/color"; |
||||
|
import DeviceIcon, { deviceIconList, type DeviceIconName } from "./DeviceIcon.svelte"; |
||||
|
import { DARK_COLOR, DARK_COLOR_SELECTED } from "./Lamp.svelte"; |
||||
|
|
||||
|
export let value: DeviceIconName; |
||||
|
</script> |
||||
|
|
||||
|
<div class="device-icon-selector"> |
||||
|
{#each deviceIconList as deviceIconName (deviceIconName)} |
||||
|
<!-- svelte-ignore a11y-click-events-have-key-events --> |
||||
|
<div class="item" class:selected={value === deviceIconName} on:click={() => { value = deviceIconName }}> |
||||
|
<DeviceIcon |
||||
|
darkColor={ value === deviceIconName ? DARK_COLOR_SELECTED : DARK_COLOR } |
||||
|
brightColor={ value === deviceIconName ? rgb(0.517, 0.537, 1.000) : DARK_COLOR } |
||||
|
name={deviceIconName} |
||||
|
/> |
||||
|
</div> |
||||
|
{/each} |
||||
|
</div> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
@import "$lib/css/colors.sass" |
||||
|
|
||||
|
div.device-icon-selector |
||||
|
display: flex |
||||
|
flex-direction: row |
||||
|
flex-wrap: wrap |
||||
|
|
||||
|
> div.item |
||||
|
font-size: 2em |
||||
|
padding: 0.3rem |
||||
|
padding-bottom: 0 |
||||
|
padding-top: 0.5rem |
||||
|
box-sizing: border-box |
||||
|
border: 2px solid $color-mainhalf |
||||
|
|
||||
|
&.selected |
||||
|
border: 2px solid $color-main1 |
||||
|
|
||||
|
&:hover |
||||
|
background-color: $color-mainquarter |
||||
|
</style> |
@ -0,0 +1,18 @@ |
|||||
|
<script lang="ts"> |
||||
|
export let reverse: boolean = false |
||||
|
</script> |
||||
|
|
||||
|
<div class="hsplit" class:reverse><slot></slot></div> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
div.hsplit |
||||
|
display: flex |
||||
|
flex-direction: row |
||||
|
flex-basis: 10 |
||||
|
|
||||
|
&.reverse |
||||
|
flex-direction: row-reverse |
||||
|
|
||||
|
@media screen and (max-width: 500px) |
||||
|
display: block |
||||
|
</style> |
@ -0,0 +1,18 @@ |
|||||
|
<script lang="ts"> |
||||
|
export let weight = 1; |
||||
|
export let left = false; |
||||
|
export let right = false; |
||||
|
</script> |
||||
|
|
||||
|
<div class="hsplit-part" class:right class:left style="flex: {weight}"><slot></slot></div> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
div.hsplit-part |
||||
|
box-sizing: border-box |
||||
|
padding: 0 0.5ch |
||||
|
|
||||
|
&.left |
||||
|
padding-left: 0 |
||||
|
&.right |
||||
|
padding-right: 0 |
||||
|
</style> |
@ -0,0 +1,97 @@ |
|||||
|
<script lang="ts"> |
||||
|
import Icon from "fa-svelte"; |
||||
|
|
||||
|
export let name: IconName = "question"; |
||||
|
export let block: boolean = false; |
||||
|
export let marginAutio: boolean = false; |
||||
|
</script> |
||||
|
|
||||
|
<!-- svelte-ignore a11y-click-events-have-key-events --> |
||||
|
{#if block} |
||||
|
<div on:click class:marginAutio> |
||||
|
<Icon class="icon" icon={icons[name] || icons.question} /> |
||||
|
</div> |
||||
|
{:else} |
||||
|
<Icon on:click class="icon" icon={icons[name] || icons.question} /> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
div.marginAutio { |
||||
|
margin: auto; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<script lang="ts" context="module"> |
||||
|
import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion"; |
||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus"; |
||||
|
import { faPen } from "@fortawesome/free-solid-svg-icons/faPen"; |
||||
|
import { faArchive } from "@fortawesome/free-solid-svg-icons/faArchive"; |
||||
|
import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck"; |
||||
|
import { faCog } from "@fortawesome/free-solid-svg-icons/faCog"; |
||||
|
import { faLink } from "@fortawesome/free-solid-svg-icons/faLink"; |
||||
|
import { faStar } from "@fortawesome/free-solid-svg-icons/faStar"; |
||||
|
import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes"; |
||||
|
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner"; |
||||
|
import { faHourglass } from "@fortawesome/free-solid-svg-icons/faHourglass"; |
||||
|
import { faCalendar } from "@fortawesome/free-solid-svg-icons/faCalendar"; |
||||
|
import { faExpand } from "@fortawesome/free-solid-svg-icons/faExpand"; |
||||
|
import { faSearch } from "@fortawesome/free-solid-svg-icons/faSearch"; |
||||
|
import { faClock } from "@fortawesome/free-solid-svg-icons/faClock"; |
||||
|
import { faThumbtack } from "@fortawesome/free-solid-svg-icons/faThumbtack"; |
||||
|
import { faHistory } from "@fortawesome/free-solid-svg-icons/faHistory"; |
||||
|
import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb"; |
||||
|
import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight"; |
||||
|
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; |
||||
|
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash"; |
||||
|
import { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot"; |
||||
|
import { faEye } from "@fortawesome/free-solid-svg-icons/faEye"; |
||||
|
import { faList } from "@fortawesome/free-solid-svg-icons/faList"; |
||||
|
import { faPowerOff } from "@fortawesome/free-solid-svg-icons/faPowerOff"; |
||||
|
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch"; |
||||
|
import { faTemperatureHalf } from "@fortawesome/free-solid-svg-icons/faTemperatureHalf"; |
||||
|
import { faPalette } from "@fortawesome/free-solid-svg-icons/faPalette"; |
||||
|
import { faSwatchbook } from "@fortawesome/free-solid-svg-icons/faSwatchbook"; |
||||
|
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"; |
||||
|
|
||||
|
const icons = { |
||||
|
"clock": faClock, |
||||
|
"thumbtack": faThumbtack, |
||||
|
"history": faHistory, |
||||
|
"question": faQuestion, |
||||
|
"plus": faPlus, |
||||
|
"pen": faPen, |
||||
|
"archive": faArchive, |
||||
|
"check": faCheck, |
||||
|
"cog": faCog, |
||||
|
"link": faLink, |
||||
|
"star": faStar, |
||||
|
"times": faTimes, |
||||
|
"lightbulb": faLightbulb, |
||||
|
"spinner": faSpinner, |
||||
|
"hourglass": faHourglass, |
||||
|
"calendar": faCalendar, |
||||
|
"expand": faExpand, |
||||
|
"search": faSearch, |
||||
|
"chevron_right": faChevronRight, |
||||
|
"chevron_down": faChevronDown, |
||||
|
"trash": faTrash, |
||||
|
"check_slot": faCheckToSlot, |
||||
|
"eye": faEye, |
||||
|
"list": faList, |
||||
|
"power": faPowerOff, |
||||
|
"cirlce_notch": faCircleNotch, |
||||
|
"temperature_half": faTemperatureHalf, |
||||
|
"palette": faPalette, |
||||
|
"swatch_book": faSwatchbook, |
||||
|
"circle_dot": faCircleDot, |
||||
|
"masks_theater": faMasksTheater, |
||||
|
"tag": faTag, |
||||
|
}; |
||||
|
|
||||
|
export type IconName = keyof typeof icons; |
||||
|
|
||||
|
export const iconNames = Object.keys(icons).sort() as IconName[]; |
||||
|
export const DEFAULT_ICON: IconName = "question"; |
||||
|
</script> |
@ -0,0 +1,15 @@ |
|||||
|
<div class="modal-body"><slot></slot></div> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
div.modal-body |
||||
|
padding: 1em |
||||
|
padding-left: 0 |
||||
|
flex: 1 |
||||
|
|
||||
|
@media screen and (max-width: 749px) |
||||
|
padding-left: 1em |
||||
|
padding: 0 1em |
||||
|
|
||||
|
&:first-of-type |
||||
|
padding-left: 1em |
||||
|
</style> |
@ -0,0 +1,59 @@ |
|||||
|
<script lang="ts"> |
||||
|
import Icon from "./Icon.svelte"; |
||||
|
|
||||
|
export let expanded: boolean; |
||||
|
export let title: string; |
||||
|
|
||||
|
function toggleExpand() { |
||||
|
expanded = !expanded; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="modal-section" class:expanded> |
||||
|
<!-- svelte-ignore a11y-click-events-have-key-events --> |
||||
|
<div class="modal-section-header" on:click={toggleExpand}> |
||||
|
<div class="title">{title}</div> |
||||
|
<Icon block name="{expanded ? "chevron_down" : "chevron_right"}" /> |
||||
|
</div> |
||||
|
<div class="modal-section-body"> |
||||
|
<slot></slot> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
@import "$lib/css/colors.sass" |
||||
|
|
||||
|
div.modal-section |
||||
|
margin-left: -1.8ch |
||||
|
margin-right: -1.8ch |
||||
|
margin-bottom: 0.5em |
||||
|
|
||||
|
> div.modal-section-body |
||||
|
display: none |
||||
|
padding: 0.25em 2.3ch |
||||
|
|
||||
|
> div.modal-section-header |
||||
|
display: flex |
||||
|
flex-direction: row |
||||
|
user-select: none |
||||
|
padding: 0.5ch 1.6ch |
||||
|
padding-right: 1.8ch |
||||
|
color: $color-main4 |
||||
|
|
||||
|
> div.title |
||||
|
margin-left: 0.5ch |
||||
|
font-size: 0.9em |
||||
|
color: $color-main5 |
||||
|
margin-right: auto |
||||
|
|
||||
|
&.expanded |
||||
|
background-color: $color-mainhalf |
||||
|
|
||||
|
> div.modal-section-body |
||||
|
display: block |
||||
|
|
||||
|
> div.modal-section-header |
||||
|
color: $color-main5 |
||||
|
> div.title |
||||
|
color: $color-main9 |
||||
|
</style> |
@ -0,0 +1,92 @@ |
|||||
|
<script lang="ts"> |
||||
|
import Icon from "./Icon.svelte"; |
||||
|
|
||||
|
export let value: string[]; |
||||
|
export let exclaimMode = false; |
||||
|
|
||||
|
let nextTag = ""; |
||||
|
|
||||
|
function onKey(ev: KeyboardEvent) { |
||||
|
if ((ev.metaKey||ev.ctrlKey) && nextTag === "" && ev.key === "Backspace") { |
||||
|
value = value.slice(0, -1); |
||||
|
} |
||||
|
|
||||
|
if ((ev.metaKey||ev.ctrlKey) && nextTag !== "" && ev.key === "Enter") { |
||||
|
value = [...value, nextTag]; |
||||
|
nextTag = ""; |
||||
|
ev.preventDefault(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$: while (nextTag.includes(",")) { |
||||
|
const newTags = nextTag.split(",").map(t => t.trim()); |
||||
|
value = [...value, ...newTags.slice(0, -1)]; |
||||
|
nextTag = newTags[newTags.length - 1]; |
||||
|
} |
||||
|
|
||||
|
$: value = (value||[]).filter((e, i) => !value.slice(0, i).includes(e)); |
||||
|
|
||||
|
$: if (exclaimMode) { |
||||
|
value = (value||[]).filter((e, i) => ( |
||||
|
e.startsWith("!") |
||||
|
? !value.slice(i+1).includes(e.slice(1)) |
||||
|
: !value.slice(i+1).includes("!"+e) |
||||
|
)); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="tag-input"> |
||||
|
{#each value as tag (tag)} |
||||
|
<div class="tag"> |
||||
|
<span>{tag}</span> |
||||
|
<span class="comma">,</span> |
||||
|
<!-- svelte-ignore a11y-click-events-have-key-events --> |
||||
|
<span class="x" on:click={() => value = value.filter(v => v !== tag)}> |
||||
|
<Icon name="times" /> |
||||
|
</span> |
||||
|
</div> |
||||
|
{/each} |
||||
|
<input placeholder="(type , or press Ctrl+Enter to add)" on:keyup={onKey} bind:value={nextTag} /> |
||||
|
</div> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
@import "$lib/css/colors.sass" |
||||
|
|
||||
|
div.tag-input |
||||
|
width: calc(100% - 2ch) |
||||
|
margin-bottom: 1em |
||||
|
margin-top: 0.20em |
||||
|
min-height: 2em |
||||
|
|
||||
|
background: $color-mainhalf |
||||
|
color: $color-main8 |
||||
|
border: none |
||||
|
outline: none |
||||
|
resize: vertical |
||||
|
padding: 0.5em 1ch |
||||
|
|
||||
|
display: flex |
||||
|
flex-direction: row |
||||
|
flex-wrap: wrap |
||||
|
|
||||
|
> div.tag |
||||
|
margin: 0.25em 0.5ch |
||||
|
background-color: $color-main1 |
||||
|
padding: 0.25em 1ch |
||||
|
border-radius: 0.25em |
||||
|
|
||||
|
span.x |
||||
|
font-size: 0.75em |
||||
|
line-height: 1em |
||||
|
user-select: none |
||||
|
cursor: pointer |
||||
|
|
||||
|
&:hover |
||||
|
color: $color-main12 |
||||
|
|
||||
|
span.comma |
||||
|
font-size: 0 |
||||
|
|
||||
|
> input |
||||
|
margin-bottom: 0.125em |
||||
|
</style> |
@ -0,0 +1,345 @@ |
|||||
|
<script lang="ts"> |
||||
|
import { runCommand } from "$lib/client/lucifer"; |
||||
|
import AssignmentState from "$lib/components/AssignmentState.svelte"; |
||||
|
import Button from "$lib/components/Button.svelte"; |
||||
|
import Checkbox from "$lib/components/Checkbox.svelte"; |
||||
|
import type { DeviceIconName } from "$lib/components/DeviceIcon.svelte"; |
||||
|
import DeviceIconSelector from "$lib/components/DeviceIconSelector.svelte"; |
||||
|
import HSplit from "$lib/components/HSplit.svelte"; |
||||
|
import HSplitPart from "$lib/components/HSplitPart.svelte"; |
||||
|
import Icon from "$lib/components/Icon.svelte"; |
||||
|
import Modal from "$lib/components/Modal.svelte"; |
||||
|
import ModalBody from "$lib/components/ModalBody.svelte"; |
||||
|
import ModalSection from "$lib/components/ModalSection.svelte"; |
||||
|
import TagInput from "$lib/components/TagInput.svelte"; |
||||
|
import { getModalContext } from "$lib/contexts/ModalContext.svelte"; |
||||
|
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte"; |
||||
|
import { getStateContext } from "$lib/contexts/StateContext.svelte"; |
||||
|
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"; |
||||
|
|
||||
|
const { modal } = getModalContext(); |
||||
|
const { selectedMasks, selectedMap, selectedList } = getSelectedContext(); |
||||
|
const { deviceList, assignmentList } = getStateContext(); |
||||
|
|
||||
|
let show: boolean = false; |
||||
|
let match: string = ""; |
||||
|
let disabled: boolean = false; |
||||
|
|
||||
|
let enableRename: boolean = false; |
||||
|
let newName: string = ""; |
||||
|
|
||||
|
let enableRoom: boolean = false; |
||||
|
let newRoom: string = ""; |
||||
|
let customRoom: string = ""; |
||||
|
|
||||
|
let enableGroup: boolean = false; |
||||
|
let newGroup: string = ""; |
||||
|
let customGroup: string = ""; |
||||
|
|
||||
|
let enableAssign: boolean = false; |
||||
|
let newEffect: EffectRaw = toEffectRaw(undefined); |
||||
|
|
||||
|
let enableIcon: boolean = false; |
||||
|
let newIcon: DeviceIconName = "generic_ball"; |
||||
|
|
||||
|
let enableTag: boolean = false; |
||||
|
let newTags: string[] = []; |
||||
|
let oldTags: string[] = []; |
||||
|
|
||||
|
let enableRole: boolean = false; |
||||
|
let newRoles: string[] = []; |
||||
|
let oldRoles: string[] = []; |
||||
|
|
||||
|
function setupModal(op: DeviceEditOp) { |
||||
|
show = true; |
||||
|
|
||||
|
enableRename = (op === "rename"); |
||||
|
enableAssign = (op === "assign"); |
||||
|
enableIcon = (op === "change_icon"); |
||||
|
enableRoom = (op === "move_room"); |
||||
|
enableGroup = (op === "move_group"); |
||||
|
enableTag = (op === "change_tags"); |
||||
|
enableRole = (op === "change_roles"); |
||||
|
|
||||
|
const firstDevice = $deviceList.find(d => $selectedMap[d.id]); |
||||
|
|
||||
|
newName = firstDevice?.name || ""; |
||||
|
newIcon = firstDevice?.icon || "generic_ball"; |
||||
|
newEffect = toEffectRaw(undefined); |
||||
|
newRoom = ""; |
||||
|
newGroup = ""; |
||||
|
|
||||
|
reloadTagsAndRoles(); |
||||
|
|
||||
|
for (const device of $deviceList) { |
||||
|
if (!$selectedMap[device.id]) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (newRoom == "") { |
||||
|
const roomAlias = device.aliases.find(a => a.startsWith("lucifer:room:"))?.slice("lucifer:room:".length); |
||||
|
if (roomAlias != null) { |
||||
|
newRoom = roomAlias; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (newGroup == "") { |
||||
|
const groupAlias = device.aliases.find(a => a.startsWith("lucifer:group:"))?.slice("lucifer:group:".length); |
||||
|
if (groupAlias != null) { |
||||
|
newGroup = groupAlias; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let mostPopularEffect = 0; |
||||
|
for (const assignment of $assignmentList) { |
||||
|
const selectedCount = assignment.deviceIds?.filter(id => $selectedMap[id]).length || 0; |
||||
|
if (selectedCount > mostPopularEffect) { |
||||
|
newEffect = toEffectRaw(assignment.effect); |
||||
|
mostPopularEffect = selectedCount; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function closeModal() { |
||||
|
show = false; |
||||
|
match = ""; |
||||
|
} |
||||
|
|
||||
|
function addEffectState() { |
||||
|
if (newEffect.states.length > 0) { |
||||
|
newEffect.states = [...newEffect.states, {...newEffect.states[newEffect.states.length - 1]}]; |
||||
|
} else { |
||||
|
newEffect.states = [...newEffect.states, {color: null, intensity: null, power: null, temperature: null}] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function removeEffectState(i: number) { |
||||
|
newEffect.states = [...newEffect.states.slice(0, i), ...newEffect.states.slice(i+1)]; |
||||
|
} |
||||
|
|
||||
|
function reloadTagsAndRoles() { |
||||
|
const firstDevice = $deviceList.find(d => $selectedMap[d.id]); |
||||
|
|
||||
|
newTags = firstDevice?.aliases |
||||
|
.filter(a => a.startsWith("lucifer:tag:")) |
||||
|
.filter(a => !$deviceList.filter(d => $selectedMap[d.id]).find(d => !d.aliases.includes(a))) |
||||
|
.map(a => a.slice("lucifer:tag:".length)) || []; |
||||
|
newRoles = firstDevice?.aliases |
||||
|
.filter(a => a.startsWith("lucifer:role:")) |
||||
|
.filter(a => !$deviceList.filter(d => $selectedMap[d.id]).find(d => !d.aliases.includes(a))) |
||||
|
.map(a => a.slice("lucifer:role:".length)) || []; |
||||
|
|
||||
|
oldTags = [...newTags]; |
||||
|
oldRoles = [...newRoles]; |
||||
|
} |
||||
|
|
||||
|
async function onSubmit() { |
||||
|
disabled = true; |
||||
|
let shouldWait = false; |
||||
|
|
||||
|
console.log("SOOBMIEET") |
||||
|
|
||||
|
try { |
||||
|
if (enableRename && newName !== "") { |
||||
|
await runCommand({addAlias: { match, alias: `lucifer:name:${newName}` }}); |
||||
|
enableRename = false; |
||||
|
shouldWait = match.startsWith("lucifer:name:"); |
||||
|
} |
||||
|
if (enableAssign) { |
||||
|
await runCommand({assign: { match, effect: fromEffectRaw(newEffect) }}); |
||||
|
} |
||||
|
if (enableRoom) { |
||||
|
await runCommand({addAlias: { match, alias: `lucifer:room:${newRoom || customRoom}` }}); |
||||
|
enableRoom = false; |
||||
|
shouldWait = match.startsWith("lucifer:room:"); |
||||
|
newRoom = newRoom || customRoom; |
||||
|
} |
||||
|
if (enableGroup) { |
||||
|
await runCommand({addAlias: { match, alias: `lucifer:group:${newGroup || customGroup}` }}); |
||||
|
enableGroup = false; |
||||
|
shouldWait = match.startsWith("lucifer:group:"); |
||||
|
newGroup = newGroup || customGroup; |
||||
|
} |
||||
|
if (enableIcon) { |
||||
|
await runCommand({addAlias: { match, alias: `lucifer:icon:${newIcon}` }}); |
||||
|
enableIcon = false; |
||||
|
shouldWait = match.startsWith("lucifer:icon:"); |
||||
|
} |
||||
|
if (enableTag) { |
||||
|
const removeTags = oldTags.filter(ot => !newTags.includes(ot)); |
||||
|
for (const removeTag of removeTags) { |
||||
|
await runCommand({removeAlias: { match, alias: `lucifer:tag:${removeTag}` }}); |
||||
|
} |
||||
|
|
||||
|
const addTags = newTags.filter(nt => !oldTags.includes(nt)); |
||||
|
for (const addTag of addTags) { |
||||
|
await runCommand({addAlias: { match, alias: `lucifer:tag:${addTag}` }}); |
||||
|
} |
||||
|
|
||||
|
shouldWait = removeTags.length > 0 || addTags.length > 0; |
||||
|
} |
||||
|
if (enableRole) { |
||||
|
const removeRoles = oldRoles.filter(or => !newRoles.includes(or)); |
||||
|
for (const removeRole of removeRoles) { |
||||
|
await runCommand({removeAlias: { match, alias: `lucifer:role:${removeRole}` }}); |
||||
|
} |
||||
|
|
||||
|
const addRoles = newRoles.filter(nr => !oldRoles.includes(nr)); |
||||
|
for (const addRole of addRoles) { |
||||
|
await runCommand({addAlias: { match, alias: `lucifer:role:${addRole}` }}); |
||||
|
} |
||||
|
|
||||
|
shouldWait = removeRoles.length > 0 || addRoles.length > 0; |
||||
|
} |
||||
|
|
||||
|
if (shouldWait) { |
||||
|
await new Promise(resolve => setTimeout(resolve, 1000)) |
||||
|
} |
||||
|
} catch (err) {} |
||||
|
|
||||
|
reloadTagsAndRoles(); |
||||
|
|
||||
|
disabled = false; |
||||
|
} |
||||
|
|
||||
|
let roomOptions: string[] = []; |
||||
|
$: roomOptions = $deviceList.flatMap(d => d.aliases) |
||||
|
.filter(k => k.startsWith("lucifer:room:")) |
||||
|
.sort() |
||||
|
.filter((v, i, a) => v !== a[i-1]) |
||||
|
.map(r => r.slice("lucifer:room:".length)); |
||||
|
|
||||
|
let groupOptions: string[] = []; |
||||
|
$: groupOptions = $deviceList.flatMap(d => d.aliases) |
||||
|
.filter(k => k.startsWith("lucifer:group:")) |
||||
|
.sort() |
||||
|
.filter((v, i, a) => v !== a[i-1]) |
||||
|
.map(r => r.slice("lucifer:group:".length)); |
||||
|
|
||||
|
$: { |
||||
|
if ($modal.kind === "device.edit") { |
||||
|
setupModal($modal.op); |
||||
|
} else { |
||||
|
closeModal(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$: if (!$selectedMasks.includes(match)) { |
||||
|
match = $selectedMasks[0]; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<form novalidate on:submit|preventDefault={onSubmit}> |
||||
|
<Modal wide disabled={disabled} closable show={show} titleText="Device Editor" submitText="Save Changes"> |
||||
|
<ModalBody> |
||||
|
<label for="mask">Selection</label> |
||||
|
<select bind:value={match}> |
||||
|
{#each $selectedMasks as option (option)} |
||||
|
<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> |
||||
|
<label for="states">Effect</label> |
||||
|
<select bind:value={newEffect.kind}> |
||||
|
<option value="gradient">Gradient</option> |
||||
|
<option value="pattern">Pattern</option> |
||||
|
<option value="random">Random</option> |
||||
|
<option value="solid">Solid</option> |
||||
|
<option value="vrange">Variable Range</option> |
||||
|
</select> |
||||
|
{#if newEffect.kind !== "manual" && newEffect.kind !== "vrange"} |
||||
|
<label for="animationMs">Interval (ms)</label> |
||||
|
<input type="number" name="animationMs" min=0 max=10000 step=100 bind:value={newEffect.animationMs} /> |
||||
|
{/if} |
||||
|
{#if newEffect.kind === "solid"} |
||||
|
<label for="interleave">Interleave</label> |
||||
|
<input type="number" name="interleave" min=0 step=1 bind:value={newEffect.interleave} /> |
||||
|
{/if} |
||||
|
{#if newEffect.kind === "vrange"} |
||||
|
<label for="states">Variable</label> |
||||
|
<select bind:value={newEffect.variable}> |
||||
|
<option value="motion.min">Motion Min (Seconds)</option> |
||||
|
<option value="motion.avg">Motion Avg (Seconds)</option> |
||||
|
<option value="motion.max">Motion Max (Seconds)</option> |
||||
|
<option value="temperature.min">Temperature Min (Celcius)</option> |
||||
|
<option value="temperature.avg">Temperature Avg (Celcius)</option> |
||||
|
<option value="temperature.max">Temperature Max (Celcius)</option> |
||||
|
</select> |
||||
|
<HSplit> |
||||
|
<HSplitPart left> |
||||
|
<label for="min">Min</label> |
||||
|
<input type="number" name="min" min=0 step=1 bind:value={newEffect.min} /> |
||||
|
</HSplitPart> |
||||
|
<HSplitPart right> |
||||
|
<label for="max">Max</label> |
||||
|
<input type="number" name="max" min=0 step=1 bind:value={newEffect.max} /> |
||||
|
</HSplitPart> |
||||
|
</HSplit> |
||||
|
{/if} |
||||
|
{#if ["gradient", "random", "vrange"].includes(newEffect.kind)} |
||||
|
<label for="states">Options</label> |
||||
|
<Checkbox bind:checked={newEffect.interpolate} label="Interpolate" /> |
||||
|
{#if (newEffect.kind === "gradient")} |
||||
|
<Checkbox bind:checked={newEffect.reverse} label="Reverse" /> |
||||
|
{/if} |
||||
|
{/if} |
||||
|
</HSplitPart> |
||||
|
<HSplitPart weight={1.0}> |
||||
|
<label for="states">States</label> |
||||
|
{#each newEffect.states as state, i } |
||||
|
<AssignmentState deletable bind:value={state} on:delete={() => removeEffectState(i)} /> |
||||
|
{/each} |
||||
|
<Button on:click={addEffectState} icon><Icon name="plus" /></Button> |
||||
|
</HSplitPart> |
||||
|
</HSplit> |
||||
|
</ModalSection> |
||||
|
</ModalBody> |
||||
|
</Modal> |
||||
|
</form> |
@ -0,0 +1,17 @@ |
|||||
|
import type { AssignmentInput } from "./assignment" |
||||
|
|
||||
|
export default interface CommandInput { |
||||
|
addAlias?: AddAliasCommand |
||||
|
removeAlias?: RemoveAliasComamnd |
||||
|
assign?: AssignmentInput |
||||
|
} |
||||
|
|
||||
|
export interface AddAliasCommand { |
||||
|
match: string |
||||
|
alias: string |
||||
|
} |
||||
|
|
||||
|
export interface RemoveAliasComamnd { |
||||
|
match: string |
||||
|
alias: string |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
export const prerender = true; |
@ -0,0 +1,9 @@ |
|||||
|
-- +goose Up |
||||
|
-- +goose StatementBegin |
||||
|
ALTER TABLE script_trigger ADD COLUMN name VARCHAR(255) NOT NULL DEFAULT ''; |
||||
|
-- +goose StatementEnd |
||||
|
|
||||
|
-- +goose Down |
||||
|
-- +goose StatementBegin |
||||
|
ALTER TABLE script_trigger DROP COLUMN IF EXISTS name; |
||||
|
-- +goose StatementEnd |
Write
Preview
Loading…
Cancel
Save
Reference in new issue