script editor
This commit is contained in:
@@ -4,12 +4,13 @@
|
||||
import Icon, { type IconName } from "./Icon.svelte";
|
||||
|
||||
export let tabIndex: number | undefined | null = void(0);
|
||||
export let checked = false;
|
||||
export let checked: boolean | string | number;
|
||||
export let centered = false;
|
||||
export let disabled = false;
|
||||
export let noLabel = false;
|
||||
export let noBorder = false;
|
||||
export let inline = false;
|
||||
export let small = false;
|
||||
export let icon: IconName = "check";
|
||||
export let label = "(Missing label property)";
|
||||
|
||||
@@ -35,6 +36,7 @@
|
||||
class:centered
|
||||
class:noLabel
|
||||
class:inline
|
||||
class:small
|
||||
role="checkbox"
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
@@ -72,6 +74,10 @@
|
||||
margin-bottom: 0
|
||||
font-size: 0.75em
|
||||
|
||||
&.small
|
||||
padding-top: 0
|
||||
font-size: 0.65em
|
||||
|
||||
div.checkbox.centered
|
||||
margin: auto
|
||||
|
||||
|
||||
@@ -55,7 +55,13 @@
|
||||
import { faMasksTheater } from "@fortawesome/free-solid-svg-icons/faMasksTheater";
|
||||
import { faTag } from "@fortawesome/free-solid-svg-icons/faTag";
|
||||
import { faFilter } from "@fortawesome/free-solid-svg-icons/faFilter";
|
||||
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 = {
|
||||
"clock": faClock,
|
||||
"thumbtack": faThumbtack,
|
||||
@@ -90,6 +96,11 @@
|
||||
"masks_theater": faMasksTheater,
|
||||
"tag": faTag,
|
||||
"filter": faFilter,
|
||||
"signature": faSignature,
|
||||
"asterisk": faAsterisk,
|
||||
"play": faPlay,
|
||||
"clock_rotate_left": faClockRotateLeft,
|
||||
"note_sticky": faNoteSticky,
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
export let submitText: string = "Submit";
|
||||
export let cancelLabel: string = "Cancel";
|
||||
export let wide: boolean = false;
|
||||
export let ultrawide: boolean = false;
|
||||
export let fullheight: boolean = false;
|
||||
export let error: string | null = null;
|
||||
export let closable: boolean = false;
|
||||
export let disabled: boolean = false;
|
||||
@@ -28,7 +30,7 @@
|
||||
{#if show}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<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="title" class:noclose={!closable}>{titleText}</div>
|
||||
{#if (closable)}
|
||||
@@ -88,9 +90,13 @@
|
||||
&.fullheight {
|
||||
min-height: 100%
|
||||
}
|
||||
}
|
||||
div.modal.wide {
|
||||
max-width: 80ch;
|
||||
|
||||
&.wide {
|
||||
max-width: 80ch;
|
||||
}
|
||||
&.ultrawide {
|
||||
max-width: 120ch;
|
||||
}
|
||||
}
|
||||
|
||||
div.header {
|
||||
@@ -186,7 +192,7 @@
|
||||
-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);
|
||||
margin-bottom: 1em;
|
||||
margin-top: 0.25em;
|
||||
@@ -198,12 +204,12 @@
|
||||
resize: vertical;
|
||||
padding: 0.5em 1ch;
|
||||
}
|
||||
div.modal :global(select) {
|
||||
div.modal :global(select:not(.custom)) {
|
||||
padding-left: 0.5ch;
|
||||
padding: 0.45em 1ch;
|
||||
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;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
|
||||
export let expanded: boolean;
|
||||
export let title: string;
|
||||
export let disableExpandToggle: boolean = false;
|
||||
|
||||
function toggleExpand() {
|
||||
expanded = !expanded;
|
||||
if (!disableExpandToggle) {
|
||||
expanded = !expanded;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -13,7 +16,9 @@
|
||||
<!-- 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"}" />
|
||||
{#if !disableExpandToggle}
|
||||
<Icon block name="{expanded ? "chevron_down" : "chevron_right"}" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="modal-section-body">
|
||||
<slot></slot>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<script lang="ts">
|
||||
import { rgbString, type Color, stringifyColor, parseColor } from "$lib/models/color";
|
||||
import { hsToHsl, kToRgb, xyToRgb } from "$lib/utils/color";
|
||||
import { onMount } from "svelte";
|
||||
import ColorPicker, { selectedColorPicker } from "../ColorPicker.svelte";
|
||||
import BFormOption from "./BFormOption.svelte";
|
||||
import BFormParameter from "./BFormParameter.svelte";
|
||||
@@ -46,6 +47,18 @@
|
||||
value = stringifyColor(color);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (value !== null && $selectedColorPicker !== null) {
|
||||
$selectedColorPicker = colorPickerId;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if ($selectedColorPicker = colorPickerId) {
|
||||
$selectedColorPicker = null;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let colorStr = "";
|
||||
$: {
|
||||
if (color !== null) {
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
export let icon: IconName = "check";
|
||||
export let unclickable: boolean = false;
|
||||
export let red: boolean = false;
|
||||
export let alwaysOpen: boolean = false;
|
||||
|
||||
const enabled = writable(state === true);
|
||||
$: $enabled = state === true;
|
||||
$: $enabled = state === true || alwaysOpen;
|
||||
|
||||
setContext(ctxKey, { subscribe: enabled.subscribe });
|
||||
</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} />
|
||||
<div class="bform-option-body">
|
||||
<slot></slot>
|
||||
@@ -42,24 +43,24 @@
|
||||
cursor: pointer
|
||||
background: $color-main1
|
||||
|
||||
:global(.icon)
|
||||
> :global(div > .icon)
|
||||
padding: 0.1em 0.5ch
|
||||
padding-top: 0.45em
|
||||
color: $color-main2
|
||||
|
||||
&.off
|
||||
background-color: $color-main2
|
||||
:global(.icon)
|
||||
> :global(div > .icon)
|
||||
color: $color-main4
|
||||
|
||||
&.on
|
||||
background-color: $color-main2
|
||||
:global(.icon)
|
||||
> :global(div > .icon)
|
||||
color: var(--color)
|
||||
|
||||
&.red
|
||||
background-color: $color-main2-red
|
||||
:global(.icon)
|
||||
> :global(div > .icon)
|
||||
color: $color-main6-redder
|
||||
|
||||
&.unclickable
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { getBFormOptionEnabled } from "./BFormOption.svelte";
|
||||
import Checkbox from "../Checkbox.svelte";
|
||||
import { getBFormOptionEnabled } from "./BFormOption.svelte";
|
||||
|
||||
export let label: string;
|
||||
export let type: "number" | "text" | "select";
|
||||
export let value: number | string;
|
||||
export let type: "number" | "text" | "select" | "checkbox";
|
||||
export let value: number | string | boolean;
|
||||
export let wide: boolean = false;
|
||||
export let narrow: 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} />
|
||||
{:else if type === "text"}
|
||||
<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"}
|
||||
<select class="custom" bind:value={value}>
|
||||
{#each options as opt (opt.value)}
|
||||
@@ -84,10 +87,7 @@
|
||||
|
||||
&.wide
|
||||
> input, > label, > select
|
||||
text-align: left
|
||||
width: 20rem
|
||||
> label
|
||||
padding-left: 0.2rch
|
||||
|
||||
&.narrow
|
||||
> input, > label, > select
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { State } from "$lib/models/device";
|
||||
import type { ColorRGB } from '$lib/models/color';
|
||||
import BFormLine from "../bforms/BFormLine.svelte";
|
||||
import BFormTemperatureOption from "../bforms/BFormTemperatureOption.svelte";
|
||||
import BFormColorOption from "../bforms/BFormColorOption.svelte";
|
||||
@@ -10,8 +9,6 @@
|
||||
|
||||
export let value: State;
|
||||
export let deletable: boolean = false;
|
||||
|
||||
$: console.log(value);
|
||||
</script>
|
||||
|
||||
<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 =
|
||||
| { kind: "closed" }
|
||||
| { kind: "device.edit", op: DeviceEditOp }
|
||||
| { kind: "script.edit", id: string | null, lines: ScriptLine[] }
|
||||
| { kind: "script.execute", script: Script }
|
||||
| { kind: "script.edit" }
|
||||
|
||||
export interface ModalContextState {
|
||||
modal: Writable<ModalSelection>
|
||||
|
||||
@@ -141,8 +141,6 @@
|
||||
disabled = true;
|
||||
let shouldWait = false;
|
||||
|
||||
console.log("SOOBMIEET")
|
||||
|
||||
try {
|
||||
if (enableRename && 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 {
|
||||
if (effect != null) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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 {
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import type { AssignmentInput } from "./assignment"
|
||||
import type { ScriptLine } from "./script"
|
||||
|
||||
export default interface CommandInput {
|
||||
addAlias?: AddAliasCommand
|
||||
removeAlias?: RemoveAliasComamnd
|
||||
assign?: AssignmentInput
|
||||
updateScript?: UpdateScriptCommand
|
||||
executeScript?: ExecuteScriptCommand
|
||||
}
|
||||
|
||||
export interface UpdateScriptCommand {
|
||||
name: string
|
||||
lines: ScriptLine[]
|
||||
}
|
||||
|
||||
export interface ExecuteScriptCommand {
|
||||
name: string
|
||||
match: string
|
||||
}
|
||||
|
||||
export interface AddAliasCommand {
|
||||
|
||||
@@ -62,6 +62,10 @@ export const BLANK_STATE: State = (Object.seal||(v=>v))({
|
||||
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";
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
import type { Effect } from "./assignment"
|
||||
import { toEffectRaw, type Effect, type EffectRaw, fromEffectRaw } from "./assignment"
|
||||
|
||||
export default interface Script {
|
||||
name: string
|
||||
lines: ScriptLine[]
|
||||
}
|
||||
|
||||
export interface ScriptLine {
|
||||
if: ScriptLineIf
|
||||
assign: ScriptLineAssign
|
||||
set: ScriptLineSet
|
||||
type ScriptLineEditableData = ScriptLineIf & ScriptLineAssign & ScriptLineSet & ScriptLineSelect
|
||||
|
||||
export interface ScriptLineEditable {
|
||||
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 {
|
||||
condition: ScriptCondition
|
||||
then: ScriptLine[]
|
||||
@@ -35,3 +50,124 @@ export interface ScriptLineSet {
|
||||
key: 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 Device from "./device";
|
||||
import type { ScriptLine } from "./script";
|
||||
import type Script from "./script";
|
||||
|
||||
export default interface UIState {
|
||||
devices: {[id: string]: Device}
|
||||
assignments: {[id: string]: Assignment}
|
||||
scripts: {[id: string]: Script}
|
||||
scripts: {[id: string]: ScriptLine[]}
|
||||
}
|
||||
|
||||
export interface UIStatePatch {
|
||||
|
||||
@@ -6,16 +6,23 @@
|
||||
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
|
||||
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
||||
import DeviceModal from "$lib/modals/DeviceModal.svelte";
|
||||
import ScriptModal from "$lib/modals/ScriptModal.svelte";
|
||||
|
||||
const {selectedList} = getSelectedContext();
|
||||
const {roomList} = getStateContext();
|
||||
const {modal} = getModalContext();
|
||||
|
||||
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()) {
|
||||
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();
|
||||
break;
|
||||
}
|
||||
@@ -47,6 +54,7 @@
|
||||
</div>
|
||||
|
||||
<DeviceModal />
|
||||
<ScriptModal />
|
||||
|
||||
<style>
|
||||
div.page {
|
||||
|
||||
Reference in New Issue
Block a user