Gisle Aune
1 year ago
15 changed files with 410 additions and 45 deletions
-
2frontend/src/lib/components/HSplit.svelte
-
4frontend/src/lib/components/Modal.svelte
-
1frontend/src/lib/contexts/ModalContext.svelte
-
32frontend/src/lib/contexts/StateContext.svelte
-
181frontend/src/lib/modals/TriggerModal.svelte
-
10frontend/src/lib/models/command.ts
-
81frontend/src/lib/models/script.ts
-
12frontend/src/lib/models/uistate.ts
-
6frontend/src/routes/+page.svelte
-
29services/httpapiv1/service.go
-
21services/script/service.go
-
42services/script/variables.go
-
16services/uistate/data.go
-
6services/uistate/patch.go
-
12services/uistate/service.go
@ -0,0 +1,181 @@ |
|||||
|
<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 { getStateContext } from "$lib/contexts/StateContext.svelte"; |
||||
|
import { fromEditableTrigger, newEditableScriptLine, newEditableTrigger, toEditableScriptLine, toEditableTrigger, type TriggerEditable } from "$lib/models/script"; |
||||
|
|
||||
|
const { state, deviceList, maskList } = getStateContext(); |
||||
|
const { modal } = getModalContext(); |
||||
|
|
||||
|
let show = false; |
||||
|
let disabled = false; |
||||
|
let selectedTrigger = ""; |
||||
|
let deleteCount = 3; |
||||
|
|
||||
|
let current: TriggerEditable = newEditableTrigger("*", ""); |
||||
|
|
||||
|
async function submitForm() { |
||||
|
disabled = true; |
||||
|
|
||||
|
try { |
||||
|
const before = Object.keys($state.triggers); |
||||
|
await runCommand({updateTrigger: fromEditableTrigger(current)}); |
||||
|
|
||||
|
if (current.id === null) { |
||||
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
||||
|
const after = Object.keys($state.triggers); |
||||
|
selectedTrigger = after.find(id => !before.includes(id)) || triggerList[0].id; |
||||
|
} |
||||
|
} catch(_) {} |
||||
|
|
||||
|
deleteCount = 3; |
||||
|
disabled = false; |
||||
|
} |
||||
|
|
||||
|
async function deleteTrigger() { |
||||
|
deleteCount -= 1; |
||||
|
if (deleteCount <= 0) { |
||||
|
try { |
||||
|
await runCommand({deleteTrigger: {id: selectedTrigger}}); |
||||
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
||||
|
selectedTrigger = triggerList[0]?.id || ""; |
||||
|
} catch(_) {} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function loadTrigger(id: string) { |
||||
|
deleteCount = 3; |
||||
|
|
||||
|
if (id !== "") { |
||||
|
current = toEditableTrigger($state.triggers[id]); |
||||
|
} else { |
||||
|
current = newEditableTrigger($maskList[0], scriptList?.[0] || ""); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function addPreLine() { |
||||
|
current.scriptPre = [...current.scriptPre, newEditableScriptLine()] |
||||
|
} |
||||
|
|
||||
|
function addPostLine() { |
||||
|
current.scriptPost = [...current.scriptPost, newEditableScriptLine()] |
||||
|
} |
||||
|
|
||||
|
function ensureButton(buttonList: string[]) { |
||||
|
if (!buttonList.includes(current.button)) { |
||||
|
current.button = buttonList[0]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$: loadTrigger(selectedTrigger); |
||||
|
$: triggerList = Object.keys($state.triggers).map(k => $state.triggers[k]).sort((a, b) => ( |
||||
|
a.event.localeCompare(b.event) |
||||
|
|| a.deviceMatch.localeCompare(b.deviceMatch) |
||||
|
|| a.parameter.localeCompare(b.parameter) |
||||
|
)); |
||||
|
$: buttonList = $deviceList.filter(d => d.aliases.includes(current.deviceMatch)) |
||||
|
.flatMap(d => d.hwState?.buttons||[]) |
||||
|
.sort() |
||||
|
.filter((e,i,a) => a[i-1] !== e); |
||||
|
$: scriptList = Object.keys($state.scripts).sort(); |
||||
|
$: maskGroups = $maskList.map(s => s.split(":")[1]).filter((e, i, a) => a[i-1] !== e).sort().reverse(); |
||||
|
$: show = ($modal.kind === "trigger.edit"); |
||||
|
$: if (scriptList.length > 0 && current.scriptName === "") { current.scriptName = scriptList[0]; }; |
||||
|
$: ensureButton(buttonList); |
||||
|
</script> |
||||
|
|
||||
|
<form novalidate on:submit|preventDefault={submitForm}> |
||||
|
<Modal |
||||
|
ultrawide closable show={show} |
||||
|
titleText="Trigger Editor" |
||||
|
disabled={disabled} |
||||
|
submitText="Save Trigger" |
||||
|
> |
||||
|
<ModalBody> |
||||
|
<HSplit> |
||||
|
<HSplitPart weight={1}> |
||||
|
<label for="mask">Trigger</label> |
||||
|
<select bind:value={selectedTrigger}> |
||||
|
{#each triggerList as trigger (trigger.id)} |
||||
|
<option value={trigger.id}>{trigger.name || `${trigger.deviceMatch} – ${trigger.event} ${trigger.parameter}`}</option> |
||||
|
{/each} |
||||
|
<option value="">New Trigger</option> |
||||
|
</select> |
||||
|
</HSplitPart> |
||||
|
<HSplitPart weight={2}> |
||||
|
</HSplitPart> |
||||
|
</HSplit> |
||||
|
<HSplit> |
||||
|
<HSplitPart weight={1}> |
||||
|
<label for="mask">Event</label> |
||||
|
<select bind:value={current.event}> |
||||
|
<option value="Button">Button Pressed</option> |
||||
|
<option value="Time">Time Changed</option> |
||||
|
</select> |
||||
|
</HSplitPart> |
||||
|
<HSplitPart weight={1}> |
||||
|
{#if current.event === "Button"} |
||||
|
<label for="mask">Button</label> |
||||
|
<select bind:value={current.button}> |
||||
|
{#each buttonList as buttonOption} |
||||
|
<option value={buttonOption}>{buttonOption}</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
{:else} |
||||
|
<label for="mask">Time</label> |
||||
|
<input type="time" bind:value={current.time} /> |
||||
|
{/if} |
||||
|
</HSplitPart> |
||||
|
<HSplitPart weight={1}> |
||||
|
{#if current.event === "Button"} |
||||
|
<label for="mask">Device</label> |
||||
|
<select bind:value={current.deviceMatch}> |
||||
|
{#each maskGroups as g} |
||||
|
<optgroup label="{g.slice(0, 1).toUpperCase() + g.slice(1)}"> |
||||
|
{#each $maskList.filter(m => m.split(":")[1] === g) as mask} |
||||
|
<option value={mask}>{mask}</option> |
||||
|
{/each} |
||||
|
</optgroup> |
||||
|
{/each} |
||||
|
</select> |
||||
|
{/if} |
||||
|
</HSplitPart> |
||||
|
</HSplit> |
||||
|
<HSplit> |
||||
|
<HSplitPart weight={1}> |
||||
|
<label for="mask">Run Script</label> |
||||
|
<select bind:value={current.scriptName}> |
||||
|
{#each scriptList as script} |
||||
|
<option value={script}>{script}</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
</HSplitPart> |
||||
|
<HSplitPart weight={1}> |
||||
|
<label for="mask">Script Target</label> |
||||
|
<select bind:value={current.scriptTarget}> |
||||
|
{#each maskGroups as g} |
||||
|
<optgroup label="{g.slice(0, 1).toUpperCase() + g.slice(1)}"> |
||||
|
{#each $maskList.filter(m => m.split(":")[1] === g) as mask} |
||||
|
<option value={mask}>{mask}</option> |
||||
|
{/each} |
||||
|
</optgroup> |
||||
|
{/each} |
||||
|
</select> |
||||
|
</HSplitPart> |
||||
|
<HSplitPart weight={1}> |
||||
|
</HSplitPart> |
||||
|
</HSplit> |
||||
|
<ModalSection expanded disableExpandToggle title="Trigger-Specific Scripting"> |
||||
|
<ScriptLineBlock add on:add={addPreLine} label="Before Script" bind:value={current.scriptPre} /> |
||||
|
<ScriptLineBlock add on:add={addPostLine} label="After Script" bind:value={current.scriptPost} /> |
||||
|
</ModalSection> |
||||
|
</ModalBody> |
||||
|
<button slot="secondary-button-1" disabled={disabled || selectedTrigger === ""} on:click|preventDefault={deleteTrigger}>Delete ({deleteCount})</button> |
||||
|
</Modal> |
||||
|
</form> |
@ -1,20 +1,26 @@ |
|||||
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 { ScriptLine, Trigger } 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]: ScriptLine[]} |
scripts: {[id: string]: ScriptLine[]} |
||||
|
triggers: {[id: string]: Trigger} |
||||
} |
} |
||||
|
|
||||
export interface UIStatePatch { |
export interface UIStatePatch { |
||||
device: Partial<Device> & { id: string, delete?: boolean, addAlias?: string, removeAlias?: string } |
device: Partial<Device> & { id: string, delete?: boolean, addAlias?: string, removeAlias?: string } |
||||
assignment: Partial<Assignment> & { id: string, delete?: boolean, addDeviceId?: string, removeDeviceId?: string } |
assignment: Partial<Assignment> & { id: string, delete?: boolean, addDeviceId?: string, removeDeviceId?: string } |
||||
script: Partial<Script> & { id: string, delete?: boolean } |
script: Partial<Script> & { id: string, delete?: boolean } |
||||
|
trigger: Partial<Trigger> & { delete?: boolean } |
||||
} |
} |
||||
|
|
||||
export interface UIStatePatch2 extends UIState { |
|
||||
|
export interface UIStatePatch2 { |
||||
full?: UIState |
full?: UIState |
||||
} |
|
||||
|
devices: {[id: string]: Device | null} |
||||
|
assignments: {[id: string]: Assignment | null} |
||||
|
scripts: {[id: string]: ScriptLine[] | null} |
||||
|
triggers: {[id: string]: Trigger | null} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue