script editor
This commit is contained in:
@@ -7,9 +7,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Solid struct {
|
type Solid struct {
|
||||||
States []device.State `json:"states,omitempty"`
|
States []device.State `json:"states"`
|
||||||
AnimationMS int64 `json:"animationMs,omitempty"`
|
AnimationMS int64 `json:"animationMs"`
|
||||||
Interleave int `json:"interleave,omitempty"`
|
Interleave int `json:"interleave"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Solid) State(_, length, round int) device.State {
|
func (e Solid) State(_, length, round int) device.State {
|
||||||
|
|||||||
+4
-4
@@ -7,10 +7,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Gradient struct {
|
type Gradient struct {
|
||||||
States []device.State `json:"states,omitempty"`
|
States []device.State `json:"states"`
|
||||||
AnimationMS int64 `json:"animationMs,omitempty"`
|
AnimationMS int64 `json:"animationMs"`
|
||||||
Reverse bool `json:"reverse,omitempty"`
|
Reverse bool `json:"reverse"`
|
||||||
Interpolate bool `json:"interpolate,omitempty"`
|
Interpolate bool `json:"interpolate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Gradient) State(index, length, round int) device.State {
|
func (e Gradient) State(index, length, round int) device.State {
|
||||||
|
|||||||
+2
-2
@@ -7,8 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Pattern struct {
|
type Pattern struct {
|
||||||
States []device.State `json:"states,omitempty"`
|
States []device.State `json:"states"`
|
||||||
AnimationMS int64 `json:"animationMs,omitempty"`
|
AnimationMS int64 `json:"animationMs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Pattern) State(index, _, round int) device.State {
|
func (e Pattern) State(index, _, round int) device.State {
|
||||||
|
|||||||
+3
-3
@@ -8,9 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Random struct {
|
type Random struct {
|
||||||
States []device.State `json:"states,omitempty"`
|
States []device.State `json:"states"`
|
||||||
Interpolate bool `json:"interpolate,omitempty"`
|
Interpolate bool `json:"interpolate"`
|
||||||
AnimationMS int64 `json:"animationMs,omitempty"`
|
AnimationMS int64 `json:"animationMs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Random) State(_, _, _ int) device.State {
|
func (e Random) State(_, _, _ int) device.State {
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type VRange struct {
|
type VRange struct {
|
||||||
States []device.State `json:"states,omitempty"`
|
States []device.State `json:"states"`
|
||||||
Variable string `json:"variable"`
|
Variable string `json:"variable"`
|
||||||
Min float64 `json:"min"`
|
Min float64 `json:"min"`
|
||||||
Max float64 `json:"max"`
|
Max float64 `json:"max"`
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
import Icon, { type IconName } from "./Icon.svelte";
|
import Icon, { type IconName } from "./Icon.svelte";
|
||||||
|
|
||||||
export let tabIndex: number | undefined | null = void(0);
|
export let tabIndex: number | undefined | null = void(0);
|
||||||
export let checked = false;
|
export let checked: boolean | string | number;
|
||||||
export let centered = false;
|
export let centered = false;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let noLabel = false;
|
export let noLabel = false;
|
||||||
export let noBorder = false;
|
export let noBorder = false;
|
||||||
export let inline = false;
|
export let inline = false;
|
||||||
|
export let small = false;
|
||||||
export let icon: IconName = "check";
|
export let icon: IconName = "check";
|
||||||
export let label = "(Missing label property)";
|
export let label = "(Missing label property)";
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
class:centered
|
class:centered
|
||||||
class:noLabel
|
class:noLabel
|
||||||
class:inline
|
class:inline
|
||||||
|
class:small
|
||||||
role="checkbox"
|
role="checkbox"
|
||||||
aria-checked={checked}
|
aria-checked={checked}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
@@ -72,6 +74,10 @@
|
|||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
font-size: 0.75em
|
font-size: 0.75em
|
||||||
|
|
||||||
|
&.small
|
||||||
|
padding-top: 0
|
||||||
|
font-size: 0.65em
|
||||||
|
|
||||||
div.checkbox.centered
|
div.checkbox.centered
|
||||||
margin: auto
|
margin: auto
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,13 @@
|
|||||||
import { faMasksTheater } from "@fortawesome/free-solid-svg-icons/faMasksTheater";
|
import { faMasksTheater } from "@fortawesome/free-solid-svg-icons/faMasksTheater";
|
||||||
import { faTag } from "@fortawesome/free-solid-svg-icons/faTag";
|
import { faTag } from "@fortawesome/free-solid-svg-icons/faTag";
|
||||||
import { faFilter } from "@fortawesome/free-solid-svg-icons/faFilter";
|
import { faFilter } from "@fortawesome/free-solid-svg-icons/faFilter";
|
||||||
|
import { faSignature } from "@fortawesome/free-solid-svg-icons/faSignature";
|
||||||
|
import { faAsterisk } from "@fortawesome/free-solid-svg-icons/faAsterisk";
|
||||||
|
import { faPlay } from "@fortawesome/free-solid-svg-icons/faPlay";
|
||||||
|
import { faClockRotateLeft } from "@fortawesome/free-solid-svg-icons/faClockRotateLeft";
|
||||||
|
import { faNoteSticky } from "@fortawesome/free-solid-svg-icons/faNoteSticky";
|
||||||
|
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
"clock": faClock,
|
"clock": faClock,
|
||||||
"thumbtack": faThumbtack,
|
"thumbtack": faThumbtack,
|
||||||
@@ -90,6 +96,11 @@
|
|||||||
"masks_theater": faMasksTheater,
|
"masks_theater": faMasksTheater,
|
||||||
"tag": faTag,
|
"tag": faTag,
|
||||||
"filter": faFilter,
|
"filter": faFilter,
|
||||||
|
"signature": faSignature,
|
||||||
|
"asterisk": faAsterisk,
|
||||||
|
"play": faPlay,
|
||||||
|
"clock_rotate_left": faClockRotateLeft,
|
||||||
|
"note_sticky": faNoteSticky,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IconName = keyof typeof icons;
|
export type IconName = keyof typeof icons;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
export let submitText: string = "Submit";
|
export let submitText: string = "Submit";
|
||||||
export let cancelLabel: string = "Cancel";
|
export let cancelLabel: string = "Cancel";
|
||||||
export let wide: boolean = false;
|
export let wide: boolean = false;
|
||||||
|
export let ultrawide: boolean = false;
|
||||||
|
export let fullheight: boolean = false;
|
||||||
export let error: string | null = null;
|
export let error: string | null = null;
|
||||||
export let closable: boolean = false;
|
export let closable: boolean = false;
|
||||||
export let disabled: boolean = false;
|
export let disabled: boolean = false;
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
{#if show}
|
{#if show}
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
<div role="dialog" class="modal-background" on:keypress={onKeyPress}>
|
<div role="dialog" class="modal-background" on:keypress={onKeyPress}>
|
||||||
<div class="modal" class:wide class:nobody>
|
<div class="modal" class:wide class:ultrawide class:fullheight class:nobody>
|
||||||
<div class="header" class:nobody>
|
<div class="header" class:nobody>
|
||||||
<div class="title" class:noclose={!closable}>{titleText}</div>
|
<div class="title" class:noclose={!closable}>{titleText}</div>
|
||||||
{#if (closable)}
|
{#if (closable)}
|
||||||
@@ -88,9 +90,13 @@
|
|||||||
&.fullheight {
|
&.fullheight {
|
||||||
min-height: 100%
|
min-height: 100%
|
||||||
}
|
}
|
||||||
}
|
|
||||||
div.modal.wide {
|
&.wide {
|
||||||
max-width: 80ch;
|
max-width: 80ch;
|
||||||
|
}
|
||||||
|
&.ultrawide {
|
||||||
|
max-width: 120ch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.header {
|
div.header {
|
||||||
@@ -186,7 +192,7 @@
|
|||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.modal :global(input:not(.custom)), div.modal :global(select), div.modal :global(textarea) {
|
div.modal :global(input:not(.custom)), div.modal :global(select:not(.custom)), div.modal :global(textarea) {
|
||||||
width: calc(100% - 2ch);
|
width: calc(100% - 2ch);
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
@@ -198,12 +204,12 @@
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
padding: 0.5em 1ch;
|
padding: 0.5em 1ch;
|
||||||
}
|
}
|
||||||
div.modal :global(select) {
|
div.modal :global(select:not(.custom)) {
|
||||||
padding-left: 0.5ch;
|
padding-left: 0.5ch;
|
||||||
padding: 0.45em 1ch;
|
padding: 0.45em 1ch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
div.modal :global(select option), div.modal :global(select optgroup) {
|
div.modal :global(select:not(.custom) option), div.modal :global(select:not(.custom) optgroup) {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
|
|
||||||
export let expanded: boolean;
|
export let expanded: boolean;
|
||||||
export let title: string;
|
export let title: string;
|
||||||
|
export let disableExpandToggle: boolean = false;
|
||||||
|
|
||||||
function toggleExpand() {
|
function toggleExpand() {
|
||||||
expanded = !expanded;
|
if (!disableExpandToggle) {
|
||||||
|
expanded = !expanded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -13,7 +16,9 @@
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="modal-section-header" on:click={toggleExpand}>
|
<div class="modal-section-header" on:click={toggleExpand}>
|
||||||
<div class="title">{title}</div>
|
<div class="title">{title}</div>
|
||||||
<Icon block name="{expanded ? "chevron_down" : "chevron_right"}" />
|
{#if !disableExpandToggle}
|
||||||
|
<Icon block name="{expanded ? "chevron_down" : "chevron_right"}" />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-section-body">
|
<div class="modal-section-body">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { rgbString, type Color, stringifyColor, parseColor } from "$lib/models/color";
|
import { rgbString, type Color, stringifyColor, parseColor } from "$lib/models/color";
|
||||||
import { hsToHsl, kToRgb, xyToRgb } from "$lib/utils/color";
|
import { hsToHsl, kToRgb, xyToRgb } from "$lib/utils/color";
|
||||||
|
import { onMount } from "svelte";
|
||||||
import ColorPicker, { selectedColorPicker } from "../ColorPicker.svelte";
|
import ColorPicker, { selectedColorPicker } from "../ColorPicker.svelte";
|
||||||
import BFormOption from "./BFormOption.svelte";
|
import BFormOption from "./BFormOption.svelte";
|
||||||
import BFormParameter from "./BFormParameter.svelte";
|
import BFormParameter from "./BFormParameter.svelte";
|
||||||
@@ -46,6 +47,18 @@
|
|||||||
value = stringifyColor(color);
|
value = stringifyColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (value !== null && $selectedColorPicker !== null) {
|
||||||
|
$selectedColorPicker = colorPickerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if ($selectedColorPicker = colorPickerId) {
|
||||||
|
$selectedColorPicker = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
let colorStr = "";
|
let colorStr = "";
|
||||||
$: {
|
$: {
|
||||||
if (color !== null) {
|
if (color !== null) {
|
||||||
|
|||||||
@@ -17,14 +17,15 @@
|
|||||||
export let icon: IconName = "check";
|
export let icon: IconName = "check";
|
||||||
export let unclickable: boolean = false;
|
export let unclickable: boolean = false;
|
||||||
export let red: boolean = false;
|
export let red: boolean = false;
|
||||||
|
export let alwaysOpen: boolean = false;
|
||||||
|
|
||||||
const enabled = writable(state === true);
|
const enabled = writable(state === true);
|
||||||
$: $enabled = state === true;
|
$: $enabled = state === true || alwaysOpen;
|
||||||
|
|
||||||
setContext(ctxKey, { subscribe: enabled.subscribe });
|
setContext(ctxKey, { subscribe: enabled.subscribe });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bform-option" class:unclickable class:on={state === true} class:off={state === false} style="--color: {color}">
|
<div class="bform-option" class:red class:unclickable class:on={state === true} class:off={state === false} style="--color: {color}">
|
||||||
<Icon on:click block name={icon} />
|
<Icon on:click block name={icon} />
|
||||||
<div class="bform-option-body">
|
<div class="bform-option-body">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@@ -42,24 +43,24 @@
|
|||||||
cursor: pointer
|
cursor: pointer
|
||||||
background: $color-main1
|
background: $color-main1
|
||||||
|
|
||||||
:global(.icon)
|
> :global(div > .icon)
|
||||||
padding: 0.1em 0.5ch
|
padding: 0.1em 0.5ch
|
||||||
padding-top: 0.45em
|
padding-top: 0.45em
|
||||||
color: $color-main2
|
color: $color-main2
|
||||||
|
|
||||||
&.off
|
&.off
|
||||||
background-color: $color-main2
|
background-color: $color-main2
|
||||||
:global(.icon)
|
> :global(div > .icon)
|
||||||
color: $color-main4
|
color: $color-main4
|
||||||
|
|
||||||
&.on
|
&.on
|
||||||
background-color: $color-main2
|
background-color: $color-main2
|
||||||
:global(.icon)
|
> :global(div > .icon)
|
||||||
color: var(--color)
|
color: var(--color)
|
||||||
|
|
||||||
&.red
|
&.red
|
||||||
background-color: $color-main2-red
|
background-color: $color-main2-red
|
||||||
:global(.icon)
|
> :global(div > .icon)
|
||||||
color: $color-main6-redder
|
color: $color-main6-redder
|
||||||
|
|
||||||
&.unclickable
|
&.unclickable
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getBFormOptionEnabled } from "./BFormOption.svelte";
|
import Checkbox from "../Checkbox.svelte";
|
||||||
|
import { getBFormOptionEnabled } from "./BFormOption.svelte";
|
||||||
|
|
||||||
export let label: string;
|
export let label: string;
|
||||||
export let type: "number" | "text" | "select";
|
export let type: "number" | "text" | "select" | "checkbox";
|
||||||
export let value: number | string;
|
export let value: number | string | boolean;
|
||||||
export let wide: boolean = false;
|
export let wide: boolean = false;
|
||||||
export let narrow: boolean = false;
|
export let narrow: boolean = false;
|
||||||
export let narrower: boolean = false;
|
export let narrower: boolean = false;
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
<input class="custom" on:blur on:focus type="number" bind:value={value} {min} {max} {step} />
|
<input class="custom" on:blur on:focus type="number" bind:value={value} {min} {max} {step} />
|
||||||
{:else if type === "text"}
|
{:else if type === "text"}
|
||||||
<input class="custom" on:blur on:focus type="text" bind:value={value} />
|
<input class="custom" on:blur on:focus type="text" bind:value={value} />
|
||||||
|
{:else if type === "checkbox"}
|
||||||
|
<Checkbox small centered noLabel bind:checked={value} />
|
||||||
{:else if type === "select"}
|
{:else if type === "select"}
|
||||||
<select class="custom" bind:value={value}>
|
<select class="custom" bind:value={value}>
|
||||||
{#each options as opt (opt.value)}
|
{#each options as opt (opt.value)}
|
||||||
@@ -84,10 +87,7 @@
|
|||||||
|
|
||||||
&.wide
|
&.wide
|
||||||
> input, > label, > select
|
> input, > label, > select
|
||||||
text-align: left
|
|
||||||
width: 20rem
|
width: 20rem
|
||||||
> label
|
|
||||||
padding-left: 0.2rch
|
|
||||||
|
|
||||||
&.narrow
|
&.narrow
|
||||||
> input, > label, > select
|
> input, > label, > select
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { State } from "$lib/models/device";
|
import type { State } from "$lib/models/device";
|
||||||
import type { ColorRGB } from '$lib/models/color';
|
|
||||||
import BFormLine from "../bforms/BFormLine.svelte";
|
import BFormLine from "../bforms/BFormLine.svelte";
|
||||||
import BFormTemperatureOption from "../bforms/BFormTemperatureOption.svelte";
|
import BFormTemperatureOption from "../bforms/BFormTemperatureOption.svelte";
|
||||||
import BFormColorOption from "../bforms/BFormColorOption.svelte";
|
import BFormColorOption from "../bforms/BFormColorOption.svelte";
|
||||||
@@ -10,8 +9,6 @@
|
|||||||
|
|
||||||
export let value: State;
|
export let value: State;
|
||||||
export let deletable: boolean = false;
|
export let deletable: boolean = false;
|
||||||
|
|
||||||
$: console.log(value);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BFormLine>
|
<BFormLine>
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import Icon from "../Icon.svelte";
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let add: boolean = false;
|
||||||
|
export let nopad: boolean = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function onAdd() {
|
||||||
|
dispatch("add");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="block" class:nopad>
|
||||||
|
<div class="label">
|
||||||
|
<div class="text">{label}</div>
|
||||||
|
{#if add}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="add-button" on:click={onAdd}><Icon name="plus" /></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@import "$lib/css/colors.sass"
|
||||||
|
|
||||||
|
div.block
|
||||||
|
padding-left: 1ch
|
||||||
|
margin-left: 0.25ch
|
||||||
|
border-left: 2px solid $color-main2
|
||||||
|
margin-bottom: 0.5em
|
||||||
|
|
||||||
|
&.nopad
|
||||||
|
margin-left: 0
|
||||||
|
|
||||||
|
div.label
|
||||||
|
color: $color-main5
|
||||||
|
font-size: 0.666em
|
||||||
|
padding-top: 0.1em
|
||||||
|
padding-left: 0.3ch
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
|
||||||
|
|
||||||
|
div.add-button
|
||||||
|
color: $color-green-dark
|
||||||
|
margin-top: 0.1em
|
||||||
|
margin-left: 0.5ch
|
||||||
|
font-size: 0.9em
|
||||||
|
line-height: 1em
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: $color-green
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ScriptCondition } from "$lib/models/script";
|
||||||
|
import BFormOption from "../bforms/BFormOption.svelte";
|
||||||
|
import BFormParameter from "../bforms/BFormParameter.svelte";
|
||||||
|
|
||||||
|
export let value: Required<ScriptCondition>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BFormOption state unclickable icon="question">
|
||||||
|
<BFormParameter narrowest type="checkbox" bind:value={value.not} label="Not" />
|
||||||
|
<BFormParameter narrow type="select" bind:value={value.scope} label="Scope" options={[
|
||||||
|
{ label: "Match", value: "match" },
|
||||||
|
{ label: "Devices", value: "device" },
|
||||||
|
{ label: "Global", value: "global" },
|
||||||
|
]} />
|
||||||
|
<BFormParameter type="text" bind:value={value.key} label="Key" />
|
||||||
|
<BFormParameter narrow type="select" bind:value={value.op} label="Op" options={[
|
||||||
|
{ label: "==", value: "eq" },
|
||||||
|
{ label: "!==", value: "neq" },
|
||||||
|
{ label: ">", value: "gt" },
|
||||||
|
{ label: ">=", value: "gte" },
|
||||||
|
{ label: "<", value: "lt" },
|
||||||
|
{ label: "<=", value: "lte" },
|
||||||
|
{ label: "contains", value: "contains" },
|
||||||
|
{ label: "exists", value: "exists" },
|
||||||
|
{ label: "numerical", value: "numerical" },
|
||||||
|
]} />
|
||||||
|
{#if value.op !== "numerical" && value.op !== "exists"}
|
||||||
|
<BFormParameter type="text" bind:value={value.value} label="Value" />
|
||||||
|
{/if}
|
||||||
|
</BFormOption>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { EffectRaw } from "$lib/models/assignment";
|
||||||
|
import BFormOption from "../bforms/BFormOption.svelte";
|
||||||
|
import BFormParameter from "../bforms/BFormParameter.svelte";
|
||||||
|
|
||||||
|
export let value: EffectRaw
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BFormOption unclickable state icon="play">
|
||||||
|
<BFormParameter
|
||||||
|
narrow label="Effect" type="select" bind:value={value.kind}
|
||||||
|
options={[
|
||||||
|
{ value:"solid", label:"Solid" },
|
||||||
|
{ value:"gradient", label:"Gradient" },
|
||||||
|
{ value:"pattern", label:"Pattern" },
|
||||||
|
{ value:"random", label:"Random" },
|
||||||
|
{ value:"vrange", label:"V. Range" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</BFormOption>
|
||||||
|
|
||||||
|
{#if value.kind !== "manual" && value.kind !== "vrange"}
|
||||||
|
<BFormOption alwaysOpen unclickable state={value.animationMs > 0} icon="clock_rotate_left">
|
||||||
|
<BFormParameter
|
||||||
|
narrow label="Milliseconds" type="number"
|
||||||
|
min={0} max={10000000} step={100}
|
||||||
|
bind:value={value.animationMs}
|
||||||
|
/>
|
||||||
|
{#if value.kind === "solid"}
|
||||||
|
<BFormParameter
|
||||||
|
narrower label="Interpolate" type="number"
|
||||||
|
min={0} max={10} step={1}
|
||||||
|
bind:value={value.interleave}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if value.kind === "gradient"}
|
||||||
|
<BFormParameter
|
||||||
|
narrower label="Reverse" type="checkbox"
|
||||||
|
bind:value={value.reverse}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if value.kind === "gradient" || value.kind === "random"}
|
||||||
|
<BFormParameter
|
||||||
|
narrower label="Interpolate" type="checkbox"
|
||||||
|
bind:value={value.interpolate}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</BFormOption>
|
||||||
|
{/if}
|
||||||
|
{#if value.kind === "vrange"}
|
||||||
|
<BFormOption unclickable state icon="temperature_half">
|
||||||
|
<BFormParameter
|
||||||
|
label="Variable" type="select" bind:value={value.variable}
|
||||||
|
options={[
|
||||||
|
{ value:"motion.min", label:"Min Motion" },
|
||||||
|
{ value:"motion.max", label:"Max Motion" },
|
||||||
|
{ value:"motion.avg", label:"Avg Motion" },
|
||||||
|
{ value:"temperature.min", label:"Min Temperature" },
|
||||||
|
{ value:"temperature.max", label:"Max Temperature" },
|
||||||
|
{ value:"temperature.avg", label:"Avg Temperature" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<BFormParameter
|
||||||
|
narrower label="Min" type="number"
|
||||||
|
min={0} max={1000} step={1}
|
||||||
|
bind:value={value.min}
|
||||||
|
/>
|
||||||
|
<BFormParameter
|
||||||
|
narrower label="Max" type="number"
|
||||||
|
min={0} max={1000} step={1}
|
||||||
|
bind:value={value.max}
|
||||||
|
/>
|
||||||
|
</BFormOption>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
const KIND_ICONS: Record<ScriptLineEditable["kind"], IconName> = {
|
||||||
|
"if": "chevron_right",
|
||||||
|
"select": "filter",
|
||||||
|
"assign": "swatch_book",
|
||||||
|
"set": "pen",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { BLANK_STATE, copyState } from "$lib/models/device";
|
||||||
|
|
||||||
|
import { toEditableScriptLine, type ScriptLineEditable } from "$lib/models/script";
|
||||||
|
import type { IconName } from "../Icon.svelte";
|
||||||
|
import BFormDeleteOption from "../bforms/BFormDeleteOption.svelte";
|
||||||
|
import BFormLine from "../bforms/BFormLine.svelte";
|
||||||
|
import BFormOption from "../bforms/BFormOption.svelte";
|
||||||
|
import BFormParameter from "../bforms/BFormParameter.svelte";
|
||||||
|
import ScriptAssignmentState from "./ScriptAssignmentState.svelte";
|
||||||
|
import ScriptBlock from "./ScriptBlock.svelte";
|
||||||
|
import ScriptCondition from "./ScriptCondition.svelte";
|
||||||
|
import ScriptEffect from "./ScriptEffect.svelte";
|
||||||
|
import ScriptLineBlock from "./ScriptLineBlock.svelte";
|
||||||
|
import ScriptMatch from "./ScriptMatch.svelte";
|
||||||
|
import ScriptSet from "./ScriptSet.svelte";
|
||||||
|
|
||||||
|
export let value: ScriptLineEditable;
|
||||||
|
|
||||||
|
function deleteState(index: number) {
|
||||||
|
value.effect.states = [
|
||||||
|
...value.effect.states.slice(0, index),
|
||||||
|
...value.effect.states.slice(index + 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (value.effect.states.length === 0) {
|
||||||
|
value.effect.states = [{...BLANK_STATE}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addState() {
|
||||||
|
value.effect.states = [
|
||||||
|
...value.effect.states,
|
||||||
|
copyState(value.effect.states[value.effect.states.length - 1]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addThen() {
|
||||||
|
value.then = [
|
||||||
|
...value.then,
|
||||||
|
toEditableScriptLine({if: {
|
||||||
|
condition: { scope: "global", key: "", op: "eq" },
|
||||||
|
then: [],
|
||||||
|
else: [],
|
||||||
|
}})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function addElse() {
|
||||||
|
value.else = [
|
||||||
|
...value.else,
|
||||||
|
toEditableScriptLine({if: {
|
||||||
|
condition: { scope: "global", key: "", op: "eq" },
|
||||||
|
then: [],
|
||||||
|
else: [],
|
||||||
|
}})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BFormLine>
|
||||||
|
<BFormOption unclickable state icon={KIND_ICONS[value.kind]}>
|
||||||
|
<BFormParameter
|
||||||
|
narrow
|
||||||
|
label="Op"
|
||||||
|
type="select"
|
||||||
|
bind:value={value.kind}
|
||||||
|
options={[
|
||||||
|
{label: "If", value: "if"},
|
||||||
|
{label: "Assign", value: "assign"},
|
||||||
|
{label: "Set", value: "set"},
|
||||||
|
{label: "Select", value: "select"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</BFormOption>
|
||||||
|
{#if value.kind === "assign" || value.kind === "select"}
|
||||||
|
<ScriptMatch bind:matchKind={value.matchKind} bind:matchValue={value.matchValue} />
|
||||||
|
{/if}
|
||||||
|
{#if value.kind === "assign"}
|
||||||
|
<ScriptEffect bind:value={value.effect} />
|
||||||
|
{/if}
|
||||||
|
{#if value.kind === "if"}
|
||||||
|
<ScriptCondition bind:value={value.condition} />
|
||||||
|
{/if}
|
||||||
|
{#if value.kind === "set"}
|
||||||
|
<ScriptSet bind:key={value.key} bind:scope={value.scope} bind:value={value.value} />
|
||||||
|
{/if}
|
||||||
|
<BFormDeleteOption on:delete />
|
||||||
|
</BFormLine>
|
||||||
|
{#if value.kind === "assign"}
|
||||||
|
<ScriptBlock on:add={addState} add label="States">
|
||||||
|
{#each value.effect.states as state, i}
|
||||||
|
<ScriptAssignmentState bind:value={state} deletable on:delete={() => deleteState(i)} />
|
||||||
|
{/each}
|
||||||
|
</ScriptBlock>
|
||||||
|
{/if}
|
||||||
|
{#if value.kind === "select" || value.kind === "if"}
|
||||||
|
<ScriptLineBlock on:add={addThen} add label="Then" bind:value={value.then} />
|
||||||
|
{/if}
|
||||||
|
{#if value.kind === "if"}
|
||||||
|
<ScriptLineBlock on:add={addElse} add label="Else" bind:value={value.else} />
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ScriptLineEditable } from "$lib/models/script";
|
||||||
|
import ScriptBlock from "./ScriptBlock.svelte";
|
||||||
|
import ScriptLine from "./ScriptLine.svelte";
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let value: ScriptLineEditable[];
|
||||||
|
export let add: boolean = false;
|
||||||
|
|
||||||
|
function deleteLine(i: number) {
|
||||||
|
value = [...value.slice(0, i), ...value.slice(i + 1)];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ScriptBlock on:add add={add} label={label}>
|
||||||
|
{#each value as line, i}
|
||||||
|
<ScriptLine bind:value={line} on:delete={() => deleteLine(i)} />
|
||||||
|
{/each}
|
||||||
|
</ScriptBlock>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
const MATCH_ICONS: Record<"all" | "raw" | "name" | "role", IconName> = {
|
||||||
|
name: "signature",
|
||||||
|
raw: "cog",
|
||||||
|
role: "masks_theater",
|
||||||
|
all: "asterisk",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { IconName } from "../Icon.svelte";
|
||||||
|
import BFormOption from "../bforms/BFormOption.svelte";
|
||||||
|
import BFormParameter from "../bforms/BFormParameter.svelte";
|
||||||
|
|
||||||
|
export let matchKind: "all" | "name" | "role" | "raw";
|
||||||
|
export let matchValue: string;
|
||||||
|
|
||||||
|
function toggleMatchKind() {
|
||||||
|
switch (matchKind) {
|
||||||
|
case "all": matchKind = "name"; break;
|
||||||
|
case "name": matchKind = "role"; break;
|
||||||
|
case "role": matchKind = "raw"; break;
|
||||||
|
case "raw": matchKind = "all"; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BFormOption on:click={toggleMatchKind} state icon={MATCH_ICONS[matchKind]}>
|
||||||
|
{#if matchKind !== "all"}
|
||||||
|
<BFormParameter
|
||||||
|
wide={matchKind === "raw"}
|
||||||
|
type="text"
|
||||||
|
label="Match ({matchKind})"
|
||||||
|
bind:value={matchValue}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</BFormOption>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import BFormOption from "../bforms/BFormOption.svelte";
|
||||||
|
import BFormParameter from "../bforms/BFormParameter.svelte";
|
||||||
|
|
||||||
|
export let scope: string;
|
||||||
|
export let key: string;
|
||||||
|
export let value: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BFormOption state unclickable icon="note_sticky">
|
||||||
|
<BFormParameter narrow type="select" bind:value={scope} label="Scope" options={[
|
||||||
|
{ label: "Match", value: "match" },
|
||||||
|
{ label: "Devices", value: "device" },
|
||||||
|
{ label: "Global", value: "global" },
|
||||||
|
]} />
|
||||||
|
<BFormParameter type="text" bind:value={key} label="Key" />
|
||||||
|
<BFormParameter type="text" bind:value={value} label="Value" />
|
||||||
|
</BFormOption>
|
||||||
@@ -10,8 +10,7 @@
|
|||||||
export type ModalSelection =
|
export type ModalSelection =
|
||||||
| { kind: "closed" }
|
| { kind: "closed" }
|
||||||
| { kind: "device.edit", op: DeviceEditOp }
|
| { kind: "device.edit", op: DeviceEditOp }
|
||||||
| { kind: "script.edit", id: string | null, lines: ScriptLine[] }
|
| { kind: "script.edit" }
|
||||||
| { kind: "script.execute", script: Script }
|
|
||||||
|
|
||||||
export interface ModalContextState {
|
export interface ModalContextState {
|
||||||
modal: Writable<ModalSelection>
|
modal: Writable<ModalSelection>
|
||||||
|
|||||||
@@ -141,8 +141,6 @@
|
|||||||
disabled = true;
|
disabled = true;
|
||||||
let shouldWait = false;
|
let shouldWait = false;
|
||||||
|
|
||||||
console.log("SOOBMIEET")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (enableRename && newName !== "") {
|
if (enableRename && newName !== "") {
|
||||||
await runCommand({addAlias: { match, alias: `lucifer:name:${newName}` }});
|
await runCommand({addAlias: { match, alias: `lucifer:name:${newName}` }});
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { runCommand } from "$lib/client/lucifer";
|
||||||
|
import HSplit from "$lib/components/HSplit.svelte";
|
||||||
|
import HSplitPart from "$lib/components/HSplitPart.svelte";
|
||||||
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
|
import ModalBody from "$lib/components/ModalBody.svelte";
|
||||||
|
import ModalSection from "$lib/components/ModalSection.svelte";
|
||||||
|
import ScriptLineBlock from "$lib/components/scripting/ScriptLineBlock.svelte";
|
||||||
|
import { getModalContext } from "$lib/contexts/ModalContext.svelte";
|
||||||
|
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
|
||||||
|
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
||||||
|
import { toEditableScriptLine, type ScriptLineEditable, fromEditableScriptLine } from "$lib/models/script";
|
||||||
|
|
||||||
|
const { state } = getStateContext();
|
||||||
|
const { selectedMasks } = getSelectedContext();
|
||||||
|
const { modal } = getModalContext();
|
||||||
|
|
||||||
|
let show = false;
|
||||||
|
let disabled = false;
|
||||||
|
let selectedScript = "";
|
||||||
|
let runMatch = "";
|
||||||
|
let newScriptName = "New Script";
|
||||||
|
let current: ScriptLineEditable[] = [];
|
||||||
|
|
||||||
|
function onOpen() {
|
||||||
|
disabled = false;
|
||||||
|
current = [];
|
||||||
|
selectedScript = scriptNames?.[0] || "";
|
||||||
|
loadScript(selectedScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runCommand({updateScript: { name: selectedScript || newScriptName, lines: current.map(fromEditableScriptLine) }});
|
||||||
|
if (runMatch !== "") {
|
||||||
|
await runCommand({executeScript: { name: selectedScript || newScriptName, match: runMatch, }});
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
selectedScript = selectedScript || newScriptName;
|
||||||
|
} catch(_) {}
|
||||||
|
|
||||||
|
disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadScript(name: string) {
|
||||||
|
if (name === "") {
|
||||||
|
current = [];
|
||||||
|
} else {
|
||||||
|
current = $state?.scripts[name]?.map(toEditableScriptLine) || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLine() {
|
||||||
|
current = [
|
||||||
|
...current,
|
||||||
|
toEditableScriptLine({if: {
|
||||||
|
condition: { scope: "global", key: "", op: "eq" },
|
||||||
|
then: [],
|
||||||
|
else: [],
|
||||||
|
}})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($modal.kind === "script.edit") {
|
||||||
|
show = true;
|
||||||
|
onOpen();
|
||||||
|
} else {
|
||||||
|
show = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: scriptNames = Object.keys($state?.scripts||{}).sort();
|
||||||
|
$: loadScript(selectedScript);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form novalidate on:submit|preventDefault={onSubmit}>
|
||||||
|
<Modal
|
||||||
|
ultrawide closable show={show}
|
||||||
|
titleText="Script Editor"
|
||||||
|
disabled={disabled || (selectedScript === "" && current.length === 0)}
|
||||||
|
submitText={(current.length > 0 || selectedScript === "") ? "Save Script" : "Delete Script"}
|
||||||
|
>
|
||||||
|
<ModalBody>
|
||||||
|
<HSplit>
|
||||||
|
<HSplitPart weight={33.3}>
|
||||||
|
<label for="mask">Script</label>
|
||||||
|
<select bind:value={selectedScript}>
|
||||||
|
{#each scriptNames as scriptName}
|
||||||
|
<option value={scriptName}>{scriptName}</option>
|
||||||
|
{/each}
|
||||||
|
<option value="">New Script</option>
|
||||||
|
</select>
|
||||||
|
</HSplitPart>
|
||||||
|
<HSplitPart weight={33.3}>
|
||||||
|
{#if selectedScript === ""}
|
||||||
|
<label for="mask">New Script Name</label>
|
||||||
|
<input type="text" bind:value={newScriptName}>
|
||||||
|
{/if}
|
||||||
|
</HSplitPart>
|
||||||
|
<HSplitPart weight={33.4}>
|
||||||
|
{#if $selectedMasks.length > 0}
|
||||||
|
<label for="mask">Run on Save</label>
|
||||||
|
<select bind:value={runMatch}>
|
||||||
|
<option value="">Don't Run</option>
|
||||||
|
{#each $selectedMasks as option (option)}
|
||||||
|
<option value={option}>{option}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/if}
|
||||||
|
</HSplitPart>
|
||||||
|
</HSplit>
|
||||||
|
|
||||||
|
<ModalSection expanded disableExpandToggle title="Edit Script">
|
||||||
|
<ScriptLineBlock add on:add={addLine} label="Script" bind:value={current} />
|
||||||
|
</ModalSection>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
</form>
|
||||||
@@ -23,26 +23,26 @@ export type Effect =
|
|||||||
export function toEffectRaw(effect?: Effect): EffectRaw {
|
export function toEffectRaw(effect?: Effect): EffectRaw {
|
||||||
if (effect != null) {
|
if (effect != null) {
|
||||||
if ("manual" in effect) {
|
if ("manual" in effect) {
|
||||||
return { kind: "manual", states: [effect.manual], animationMs: 0, reverse: false, interpolate: false, min: 0, max: 0, variable: "", interleave: 0 }
|
return { kind: "solid", states: [effect.manual], animationMs: 0, reverse: false, interpolate: true, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||||
}
|
}
|
||||||
if ("solid" in effect) {
|
if ("solid" in effect) {
|
||||||
return { kind: "solid", ...effect.solid, min: 0, max: 0, variable: "", interpolate: false, reverse: false}
|
return { kind: "solid", ...effect.solid, min: 30, max: 300, variable: "motion.min", interpolate: true, reverse: false}
|
||||||
}
|
}
|
||||||
if ("gradient" in effect) {
|
if ("gradient" in effect) {
|
||||||
return { kind: "gradient", ...effect.gradient, min: 0, max: 0, variable: "", interleave: 0 }
|
return { kind: "gradient", ...effect.gradient, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||||
}
|
}
|
||||||
if ("pattern" in effect) {
|
if ("pattern" in effect) {
|
||||||
return { kind: "pattern", ...effect.pattern, reverse: false, interpolate: false, min: 0, max: 0, variable: "", interleave: 0 }
|
return { kind: "pattern", ...effect.pattern, reverse: false, interpolate: true, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||||
}
|
}
|
||||||
if ("random" in effect) {
|
if ("random" in effect) {
|
||||||
return { kind: "random", ...effect.random, reverse: false, min: 0, max: 0, variable: "", interleave: 0 }
|
return { kind: "random", ...effect.random, reverse: false, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||||
}
|
}
|
||||||
if ("vrange" in effect) {
|
if ("vrange" in effect) {
|
||||||
return { kind: "vrange", ...effect.vrange, animationMs: 0, interpolate: false, reverse: false, interleave: 0 }
|
return { kind: "vrange", ...effect.vrange, animationMs: 0, interpolate: true, reverse: false, interleave: 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { kind: "manual", states: [{color: null, intensity: null, power: null, temperature: null}], animationMs: 0, reverse: false, interpolate: false, min: 0, max: 0, variable: "", interleave: 0 }
|
return { kind: "manual", states: [{color: null, intensity: null, power: null, temperature: null}], animationMs: 0, reverse: false, interpolate: false, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromEffectRaw(raw: EffectRaw): Effect {
|
export function fromEffectRaw(raw: EffectRaw): Effect {
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
import type { AssignmentInput } from "./assignment"
|
import type { AssignmentInput } from "./assignment"
|
||||||
|
import type { ScriptLine } from "./script"
|
||||||
|
|
||||||
export default interface CommandInput {
|
export default interface CommandInput {
|
||||||
addAlias?: AddAliasCommand
|
addAlias?: AddAliasCommand
|
||||||
removeAlias?: RemoveAliasComamnd
|
removeAlias?: RemoveAliasComamnd
|
||||||
assign?: AssignmentInput
|
assign?: AssignmentInput
|
||||||
|
updateScript?: UpdateScriptCommand
|
||||||
|
executeScript?: ExecuteScriptCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateScriptCommand {
|
||||||
|
name: string
|
||||||
|
lines: ScriptLine[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecuteScriptCommand {
|
||||||
|
name: string
|
||||||
|
match: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddAliasCommand {
|
export interface AddAliasCommand {
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ export const BLANK_STATE: State = (Object.seal||(v=>v))({
|
|||||||
color: null,
|
color: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function copyState(state?: State | null): State {
|
||||||
|
return { ...(state || BLANK_STATE) };
|
||||||
|
}
|
||||||
|
|
||||||
export type DeviceEditOp = "none" | "assign" | "rename" | "change_icon" | "move_group" | "move_room" | "change_tags" | "change_roles";
|
export type DeviceEditOp = "none" | "assign" | "rename" | "change_icon" | "move_group" | "move_room" | "change_tags" | "change_roles";
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
import type { Effect } from "./assignment"
|
import { toEffectRaw, type Effect, type EffectRaw, fromEffectRaw } from "./assignment"
|
||||||
|
|
||||||
export default interface Script {
|
export default interface Script {
|
||||||
name: string
|
name: string
|
||||||
lines: ScriptLine[]
|
lines: ScriptLine[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptLine {
|
type ScriptLineEditableData = ScriptLineIf & ScriptLineAssign & ScriptLineSet & ScriptLineSelect
|
||||||
if: ScriptLineIf
|
|
||||||
assign: ScriptLineAssign
|
export interface ScriptLineEditable {
|
||||||
set: ScriptLineSet
|
kind: "if" | "assign" | "set" | "select"
|
||||||
|
condition: Required<ScriptCondition>
|
||||||
|
effect: EffectRaw
|
||||||
|
matchKind: "all" | "name" | "role" | "raw"
|
||||||
|
matchValue: string
|
||||||
|
scope: string
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
then: ScriptLineEditable[]
|
||||||
|
else: ScriptLineEditable[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ScriptLine =
|
||||||
|
| { if: ScriptLineIf }
|
||||||
|
| { assign: ScriptLineAssign }
|
||||||
|
| { set: ScriptLineSet }
|
||||||
|
| { select: ScriptLineSelect }
|
||||||
|
|
||||||
export interface ScriptLineIf {
|
export interface ScriptLineIf {
|
||||||
condition: ScriptCondition
|
condition: ScriptCondition
|
||||||
then: ScriptLine[]
|
then: ScriptLine[]
|
||||||
@@ -35,3 +50,124 @@ export interface ScriptLineSet {
|
|||||||
key: string
|
key: string
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScriptLineAssign {
|
||||||
|
match: string
|
||||||
|
effect: Effect
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptLineSelect {
|
||||||
|
match: string
|
||||||
|
then: ScriptLine[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromEditableScriptLine(line: ScriptLineEditable): ScriptLine {
|
||||||
|
switch (line.kind) {
|
||||||
|
case "if": return {
|
||||||
|
if: {
|
||||||
|
condition: line.condition,
|
||||||
|
then: line.then.map(fromEditableScriptLine),
|
||||||
|
else: line.else.map(fromEditableScriptLine),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case "assign": return {
|
||||||
|
assign: {
|
||||||
|
match: fromMatchKindValue(line),
|
||||||
|
effect: fromEffectRaw(line.effect),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "select": return {
|
||||||
|
select: {
|
||||||
|
match: fromMatchKindValue(line),
|
||||||
|
then: line.then.map(fromEditableScriptLine),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "set": return {
|
||||||
|
set: {
|
||||||
|
key: line.key,
|
||||||
|
scope: line.scope,
|
||||||
|
value: line.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toEditableScriptLine(line: ScriptLine): ScriptLineEditable {
|
||||||
|
const base = emptyEditable();
|
||||||
|
|
||||||
|
if ("assign" in line) {
|
||||||
|
base.kind = "assign";
|
||||||
|
base.effect = toEffectRaw(line.assign.effect);
|
||||||
|
Object.assign(base, toMatchKindValue(line.assign.match));
|
||||||
|
} else if ("if" in line) {
|
||||||
|
base.kind = "if";
|
||||||
|
base.condition = {
|
||||||
|
...line.if.condition,
|
||||||
|
not: line.if.condition.not || false,
|
||||||
|
value: line.if.condition.value || "",
|
||||||
|
};
|
||||||
|
base.then = line.if.then.map(toEditableScriptLine);
|
||||||
|
base.else = line.if.else.map(toEditableScriptLine);
|
||||||
|
} else if ("select" in line) {
|
||||||
|
base.kind = "select";
|
||||||
|
Object.assign(base, toMatchKindValue(line.select.match));
|
||||||
|
base.then = line.select.then.map(toEditableScriptLine);
|
||||||
|
} else if ("set" in line) {
|
||||||
|
base.kind = "set";
|
||||||
|
base.key = line.set.key
|
||||||
|
base.scope = line.set.scope
|
||||||
|
base.value = line.set.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMatchKindValue(match: string): { matchKind: "all" | "name" | "role" | "raw", matchValue: string } {
|
||||||
|
if (match.startsWith("lucifer:name:")) {
|
||||||
|
return { matchKind: "name", matchValue: match.slice("lucifer:name:".length) };
|
||||||
|
} else if (match.startsWith("lucifer:role:")) {
|
||||||
|
return { matchKind: "role", matchValue: match.slice("lucifer:role:".length) };
|
||||||
|
} else if (match === "*") {
|
||||||
|
return { matchKind: "all", matchValue: "*" };
|
||||||
|
} else {
|
||||||
|
return { matchKind: "raw", matchValue: match }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromMatchKindValue({matchKind, matchValue }: { matchKind: "all" | "name" | "role" | "raw", matchValue: string }): string {
|
||||||
|
switch (matchKind) {
|
||||||
|
case "name": return `lucifer:name:${matchValue}`;
|
||||||
|
case "role": return `lucifer:role:${matchValue}`;
|
||||||
|
case "all": return "*";
|
||||||
|
case "raw": return matchValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyEditable(): ScriptLineEditable {
|
||||||
|
return {
|
||||||
|
condition: {
|
||||||
|
key: "",
|
||||||
|
op: "add",
|
||||||
|
scope: "global",
|
||||||
|
not: false,
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
effect: toEffectRaw({
|
||||||
|
solid: {
|
||||||
|
states: [
|
||||||
|
{color: null, intensity: null, power: null, temperature: null}
|
||||||
|
],
|
||||||
|
interleave: 0,
|
||||||
|
animationMs: 0,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
key: "",
|
||||||
|
kind: "assign",
|
||||||
|
matchKind: "name",
|
||||||
|
matchValue: "*",
|
||||||
|
scope: "match",
|
||||||
|
value: "",
|
||||||
|
then: [],
|
||||||
|
else: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import type Assignment from "./assignment";
|
import type Assignment from "./assignment";
|
||||||
import type Device from "./device";
|
import type Device from "./device";
|
||||||
|
import type { ScriptLine } from "./script";
|
||||||
import type Script from "./script";
|
import type Script from "./script";
|
||||||
|
|
||||||
export default interface UIState {
|
export default interface UIState {
|
||||||
devices: {[id: string]: Device}
|
devices: {[id: string]: Device}
|
||||||
assignments: {[id: string]: Assignment}
|
assignments: {[id: string]: Assignment}
|
||||||
scripts: {[id: string]: Script}
|
scripts: {[id: string]: ScriptLine[]}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UIStatePatch {
|
export interface UIStatePatch {
|
||||||
|
|||||||
@@ -6,16 +6,23 @@
|
|||||||
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
|
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
|
||||||
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
||||||
import DeviceModal from "$lib/modals/DeviceModal.svelte";
|
import DeviceModal from "$lib/modals/DeviceModal.svelte";
|
||||||
|
import ScriptModal from "$lib/modals/ScriptModal.svelte";
|
||||||
|
|
||||||
const {selectedList} = getSelectedContext();
|
const {selectedList} = getSelectedContext();
|
||||||
const {roomList} = getStateContext();
|
const {roomList} = getStateContext();
|
||||||
const {modal} = getModalContext();
|
const {modal} = getModalContext();
|
||||||
|
|
||||||
function handleKeyPress(e: KeyboardEvent) {
|
function handleKeyPress(e: KeyboardEvent) {
|
||||||
if ($modal.kind === "closed" && $selectedList.length && !e.ctrlKey && !e.shiftKey) {
|
if ($modal.kind === "closed" && !e.ctrlKey && !e.shiftKey) {
|
||||||
switch (e.key.toLocaleLowerCase()) {
|
switch (e.key.toLocaleLowerCase()) {
|
||||||
case 'e':
|
case 'e':
|
||||||
modal.set({kind: "device.edit", op: "none"});
|
if ($selectedList.length) {
|
||||||
|
modal.set({kind: "device.edit", op: "none"});
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'o':
|
||||||
|
modal.set({kind: "script.edit"});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -47,6 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DeviceModal />
|
<DeviceModal />
|
||||||
|
<ScriptModal />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div.page {
|
div.page {
|
||||||
|
|||||||
@@ -177,7 +177,11 @@ func New(addr string) (lucifer3.Service, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if patch.Script != nil {
|
if patch.Script != nil {
|
||||||
statePatch.Scripts[patch.Script.Name] = svc.data.Scripts[patch.Script.Name]
|
if len(patch.Script.Lines) > 0 {
|
||||||
|
statePatch.Scripts[patch.Script.Name] = svc.data.Scripts[patch.Script.Name]
|
||||||
|
} else {
|
||||||
|
statePatch.Scripts[patch.Script.Name] = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
svc.mu.Unlock()
|
svc.mu.Unlock()
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
|||||||
if q.deleteDeviceInfoLikeStmt, err = db.PrepareContext(ctx, deleteDeviceInfoLike); err != nil {
|
if q.deleteDeviceInfoLikeStmt, err = db.PrepareContext(ctx, deleteDeviceInfoLike); err != nil {
|
||||||
return nil, fmt.Errorf("error preparing query DeleteDeviceInfoLike: %w", err)
|
return nil, fmt.Errorf("error preparing query DeleteDeviceInfoLike: %w", err)
|
||||||
}
|
}
|
||||||
|
if q.deleteScriptStmt, err = db.PrepareContext(ctx, deleteScript); err != nil {
|
||||||
|
return nil, fmt.Errorf("error preparing query DeleteScript: %w", err)
|
||||||
|
}
|
||||||
if q.deleteScriptTriggerStmt, err = db.PrepareContext(ctx, deleteScriptTrigger); err != nil {
|
if q.deleteScriptTriggerStmt, err = db.PrepareContext(ctx, deleteScriptTrigger); err != nil {
|
||||||
return nil, fmt.Errorf("error preparing query DeleteScriptTrigger: %w", err)
|
return nil, fmt.Errorf("error preparing query DeleteScriptTrigger: %w", err)
|
||||||
}
|
}
|
||||||
@@ -138,6 +141,11 @@ func (q *Queries) Close() error {
|
|||||||
err = fmt.Errorf("error closing deleteDeviceInfoLikeStmt: %w", cerr)
|
err = fmt.Errorf("error closing deleteDeviceInfoLikeStmt: %w", cerr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if q.deleteScriptStmt != nil {
|
||||||
|
if cerr := q.deleteScriptStmt.Close(); cerr != nil {
|
||||||
|
err = fmt.Errorf("error closing deleteScriptStmt: %w", cerr)
|
||||||
|
}
|
||||||
|
}
|
||||||
if q.deleteScriptTriggerStmt != nil {
|
if q.deleteScriptTriggerStmt != nil {
|
||||||
if cerr := q.deleteScriptTriggerStmt.Close(); cerr != nil {
|
if cerr := q.deleteScriptTriggerStmt.Close(); cerr != nil {
|
||||||
err = fmt.Errorf("error closing deleteScriptTriggerStmt: %w", cerr)
|
err = fmt.Errorf("error closing deleteScriptTriggerStmt: %w", cerr)
|
||||||
@@ -260,6 +268,7 @@ type Queries struct {
|
|||||||
deleteDeviceInfoStmt *sql.Stmt
|
deleteDeviceInfoStmt *sql.Stmt
|
||||||
deleteDeviceInfoByIDStmt *sql.Stmt
|
deleteDeviceInfoByIDStmt *sql.Stmt
|
||||||
deleteDeviceInfoLikeStmt *sql.Stmt
|
deleteDeviceInfoLikeStmt *sql.Stmt
|
||||||
|
deleteScriptStmt *sql.Stmt
|
||||||
deleteScriptTriggerStmt *sql.Stmt
|
deleteScriptTriggerStmt *sql.Stmt
|
||||||
insertDeviceAliasStmt *sql.Stmt
|
insertDeviceAliasStmt *sql.Stmt
|
||||||
listDeviceAliasesStmt *sql.Stmt
|
listDeviceAliasesStmt *sql.Stmt
|
||||||
@@ -289,6 +298,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
|||||||
deleteDeviceInfoStmt: q.deleteDeviceInfoStmt,
|
deleteDeviceInfoStmt: q.deleteDeviceInfoStmt,
|
||||||
deleteDeviceInfoByIDStmt: q.deleteDeviceInfoByIDStmt,
|
deleteDeviceInfoByIDStmt: q.deleteDeviceInfoByIDStmt,
|
||||||
deleteDeviceInfoLikeStmt: q.deleteDeviceInfoLikeStmt,
|
deleteDeviceInfoLikeStmt: q.deleteDeviceInfoLikeStmt,
|
||||||
|
deleteScriptStmt: q.deleteScriptStmt,
|
||||||
deleteScriptTriggerStmt: q.deleteScriptTriggerStmt,
|
deleteScriptTriggerStmt: q.deleteScriptTriggerStmt,
|
||||||
insertDeviceAliasStmt: q.insertDeviceAliasStmt,
|
insertDeviceAliasStmt: q.insertDeviceAliasStmt,
|
||||||
listDeviceAliasesStmt: q.listDeviceAliasesStmt,
|
listDeviceAliasesStmt: q.listDeviceAliasesStmt,
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const deleteScript = `-- name: DeleteScript :exec
|
||||||
|
DELETE FROM script WHERE name = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteScript(ctx context.Context, name string) error {
|
||||||
|
_, err := q.exec(ctx, q.deleteScriptStmt, deleteScript, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteScriptTrigger = `-- name: DeleteScriptTrigger :exec
|
const deleteScriptTrigger = `-- name: DeleteScriptTrigger :exec
|
||||||
DELETE FROM script_trigger WHERE id = ?
|
DELETE FROM script_trigger WHERE id = ?
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ SELECT * FROM script;
|
|||||||
-- name: SaveScript :exec
|
-- name: SaveScript :exec
|
||||||
REPLACE INTO script (name, data) VALUES (?, ?);
|
REPLACE INTO script (name, data) VALUES (?, ?);
|
||||||
|
|
||||||
|
-- name: DeleteScript :exec
|
||||||
|
DELETE FROM script WHERE name = ?;
|
||||||
|
|
||||||
-- name: ListScriptVariables :many
|
-- name: ListScriptVariables :many
|
||||||
SELECT * FROM script_variable;
|
SELECT * FROM script_variable;
|
||||||
|
|
||||||
|
|||||||
@@ -434,16 +434,27 @@ func (d *database) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Comman
|
|||||||
case script.Update:
|
case script.Update:
|
||||||
j, _ := json.Marshal(command.Lines)
|
j, _ := json.Marshal(command.Lines)
|
||||||
|
|
||||||
err := q.SaveScript(timeout, mysqlgen.SaveScriptParams{
|
if (len(command.Lines)) > 0 {
|
||||||
Name: command.Name,
|
err := q.SaveScript(timeout, mysqlgen.SaveScriptParams{
|
||||||
Data: j,
|
Name: command.Name,
|
||||||
})
|
Data: j,
|
||||||
if err != nil {
|
|
||||||
bus.RunEvent(events.Log{
|
|
||||||
Level: "error",
|
|
||||||
Code: "database_could_not_save_script",
|
|
||||||
Message: "Failed to save script: " + err.Error(),
|
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
bus.RunEvent(events.Log{
|
||||||
|
Level: "error",
|
||||||
|
Code: "database_could_not_save_script",
|
||||||
|
Message: "Failed to save script: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := q.DeleteScript(timeout, command.Name)
|
||||||
|
if err != nil {
|
||||||
|
bus.RunEvent(events.Log{
|
||||||
|
Level: "error",
|
||||||
|
Code: "database_could_not_delete_script",
|
||||||
|
Message: "Failed to delete script: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,11 @@ func (d *Data) WithPatch(patches ...Patch) Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if patch.Script != nil {
|
if patch.Script != nil {
|
||||||
newData.Scripts[patch.Script.Name] = patch.Script.Lines
|
if len(patch.Script.Lines) > 0 {
|
||||||
|
newData.Scripts[patch.Script.Name] = patch.Script.Lines
|
||||||
|
} else {
|
||||||
|
delete(newData.Scripts, patch.Script.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user