Gisle Aune
1 year ago
35 changed files with 776 additions and 68 deletions
-
6effects/animation.go
-
8effects/gradient.go
-
4effects/pattern.go
-
6effects/random.go
-
2effects/vrange.go
-
8frontend/src/lib/components/Checkbox.svelte
-
11frontend/src/lib/components/Icon.svelte
-
18frontend/src/lib/components/Modal.svelte
-
5frontend/src/lib/components/ModalSection.svelte
-
13frontend/src/lib/components/bforms/BFormColorOption.svelte
-
13frontend/src/lib/components/bforms/BFormOption.svelte
-
10frontend/src/lib/components/bforms/BFormParameter.svelte
-
3frontend/src/lib/components/scripting/ScriptAssignmentState.svelte
-
58frontend/src/lib/components/scripting/ScriptBlock.svelte
-
31frontend/src/lib/components/scripting/ScriptCondition.svelte
-
74frontend/src/lib/components/scripting/ScriptEffect.svelte
-
111frontend/src/lib/components/scripting/ScriptLine.svelte
-
19frontend/src/lib/components/scripting/ScriptLineBlock.svelte
-
38frontend/src/lib/components/scripting/ScriptMatch.svelte
-
18frontend/src/lib/components/scripting/ScriptSet.svelte
-
3frontend/src/lib/contexts/ModalContext.svelte
-
2frontend/src/lib/modals/DeviceModal.svelte
-
120frontend/src/lib/modals/ScriptModal.svelte
-
14frontend/src/lib/models/assignment.ts
-
13frontend/src/lib/models/command.ts
-
4frontend/src/lib/models/device.ts
-
146frontend/src/lib/models/script.ts
-
3frontend/src/lib/models/uistate.ts
-
10frontend/src/routes/+page.svelte
-
4services/httpapiv1/service.go
-
10services/mysqldb/mysqlgen/db.go
-
9services/mysqldb/mysqlgen/script.sql.go
-
3services/mysqldb/queries/script.sql
-
11services/mysqldb/service.go
-
4services/uistate/data.go
@ -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> |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue