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
-
12frontend/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