You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
344 lines
13 KiB
344 lines
13 KiB
<script lang="ts">
|
|
import { runCommand } from "$lib/client/lucifer";
|
|
import AssignmentState from "$lib/components/scripting/ScriptAssignmentState.svelte";
|
|
import Button from "$lib/components/Button.svelte";
|
|
import Checkbox from "$lib/components/Checkbox.svelte";
|
|
import type { DeviceIconName } from "$lib/components/DeviceIcon.svelte";
|
|
import DeviceIconSelector from "$lib/components/DeviceIconSelector.svelte";
|
|
import HSplit from "$lib/components/HSplit.svelte";
|
|
import HSplitPart from "$lib/components/HSplitPart.svelte";
|
|
import Icon from "$lib/components/Icon.svelte";
|
|
import Modal from "$lib/components/Modal.svelte";
|
|
import ModalBody from "$lib/components/ModalBody.svelte";
|
|
import ModalSection from "$lib/components/ModalSection.svelte";
|
|
import TagInput from "$lib/components/TagInput.svelte";
|
|
import { getModalContext } from "$lib/contexts/ModalContext.svelte";
|
|
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
|
|
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
|
import { toEffectRaw, type EffectRaw, fromEffectRaw } from "$lib/models/assignment";
|
|
import type { DeviceEditOp } from "$lib/models/device";
|
|
import { iconName } from "@fortawesome/free-solid-svg-icons/faQuestion";
|
|
import ScriptAssignmentState from "$lib/components/scripting/ScriptAssignmentState.svelte";
|
|
|
|
const { modal } = getModalContext();
|
|
const { selectedMasks, selectedMap, selectedList } = getSelectedContext();
|
|
const { deviceList, assignmentList } = getStateContext();
|
|
|
|
let show: boolean = false;
|
|
let match: string = "";
|
|
let disabled: boolean = false;
|
|
|
|
let enableRename: boolean = false;
|
|
let newName: string = "";
|
|
|
|
let enableRoom: boolean = false;
|
|
let newRoom: string = "";
|
|
let customRoom: string = "";
|
|
|
|
let enableGroup: boolean = false;
|
|
let newGroup: string = "";
|
|
let customGroup: string = "";
|
|
|
|
let enableAssign: boolean = false;
|
|
let newEffect: EffectRaw = toEffectRaw(undefined);
|
|
|
|
let enableIcon: boolean = false;
|
|
let newIcon: DeviceIconName = "generic_ball";
|
|
|
|
let enableTag: boolean = false;
|
|
let newTags: string[] = [];
|
|
let oldTags: string[] = [];
|
|
|
|
let enableRole: boolean = false;
|
|
let newRoles: string[] = [];
|
|
let oldRoles: string[] = [];
|
|
|
|
function setupModal(op: DeviceEditOp) {
|
|
show = true;
|
|
|
|
enableRename = (op === "rename");
|
|
enableAssign = (op === "assign");
|
|
enableIcon = (op === "change_icon");
|
|
enableRoom = (op === "move_room");
|
|
enableGroup = (op === "move_group");
|
|
enableTag = (op === "change_tags");
|
|
enableRole = (op === "change_roles");
|
|
|
|
const firstDevice = $deviceList.find(d => $selectedMap[d.id]);
|
|
|
|
newName = firstDevice?.name || "";
|
|
newIcon = firstDevice?.icon || "generic_ball";
|
|
newEffect = toEffectRaw(undefined);
|
|
newRoom = "";
|
|
newGroup = "";
|
|
|
|
reloadTagsAndRoles();
|
|
|
|
for (const device of $deviceList) {
|
|
if (!$selectedMap[device.id]) {
|
|
continue;
|
|
}
|
|
|
|
if (newRoom == "") {
|
|
const roomAlias = device.aliases.find(a => a.startsWith("lucifer:room:"))?.slice("lucifer:room:".length);
|
|
if (roomAlias != null) {
|
|
newRoom = roomAlias;
|
|
}
|
|
}
|
|
|
|
if (newGroup == "") {
|
|
const groupAlias = device.aliases.find(a => a.startsWith("lucifer:group:"))?.slice("lucifer:group:".length);
|
|
if (groupAlias != null) {
|
|
newGroup = groupAlias;
|
|
}
|
|
}
|
|
}
|
|
|
|
let mostPopularEffect = 0;
|
|
for (const assignment of $assignmentList) {
|
|
const selectedCount = assignment.deviceIds?.filter(id => $selectedMap[id]).length || 0;
|
|
if (selectedCount > mostPopularEffect) {
|
|
newEffect = toEffectRaw(assignment.effect);
|
|
mostPopularEffect = selectedCount;
|
|
}
|
|
}
|
|
}
|
|
|
|
function closeModal() {
|
|
show = false;
|
|
match = "";
|
|
}
|
|
|
|
function addEffectState() {
|
|
if (newEffect.states.length > 0) {
|
|
newEffect.states = [...newEffect.states, {...newEffect.states[newEffect.states.length - 1]}];
|
|
} else {
|
|
newEffect.states = [...newEffect.states, {color: null, intensity: null, power: null, temperature: null}]
|
|
}
|
|
}
|
|
|
|
function removeEffectState(i: number) {
|
|
newEffect.states = [...newEffect.states.slice(0, i), ...newEffect.states.slice(i+1)];
|
|
}
|
|
|
|
function reloadTagsAndRoles() {
|
|
const firstDevice = $deviceList.find(d => $selectedMap[d.id]);
|
|
|
|
newTags = firstDevice?.aliases
|
|
.filter(a => a.startsWith("lucifer:tag:"))
|
|
.filter(a => !$deviceList.filter(d => $selectedMap[d.id]).find(d => !d.aliases.includes(a)))
|
|
.map(a => a.slice("lucifer:tag:".length)) || [];
|
|
newRoles = firstDevice?.aliases
|
|
.filter(a => a.startsWith("lucifer:role:"))
|
|
.filter(a => !$deviceList.filter(d => $selectedMap[d.id]).find(d => !d.aliases.includes(a)))
|
|
.map(a => a.slice("lucifer:role:".length)) || [];
|
|
|
|
oldTags = [...newTags];
|
|
oldRoles = [...newRoles];
|
|
}
|
|
|
|
async function onSubmit() {
|
|
disabled = true;
|
|
let shouldWait = false;
|
|
|
|
try {
|
|
if (enableRename && newName !== "") {
|
|
await runCommand({addAlias: { match, alias: `lucifer:name:${newName}` }});
|
|
enableRename = false;
|
|
shouldWait = match.startsWith("lucifer:name:");
|
|
}
|
|
if (enableAssign) {
|
|
await runCommand({assign: { match, effect: fromEffectRaw(newEffect) }});
|
|
}
|
|
if (enableRoom) {
|
|
await runCommand({addAlias: { match, alias: `lucifer:room:${newRoom || customRoom}` }});
|
|
enableRoom = false;
|
|
shouldWait = match.startsWith("lucifer:room:");
|
|
newRoom = newRoom || customRoom;
|
|
}
|
|
if (enableGroup) {
|
|
await runCommand({addAlias: { match, alias: `lucifer:group:${newGroup || customGroup}` }});
|
|
enableGroup = false;
|
|
shouldWait = match.startsWith("lucifer:group:");
|
|
newGroup = newGroup || customGroup;
|
|
}
|
|
if (enableIcon) {
|
|
await runCommand({addAlias: { match, alias: `lucifer:icon:${newIcon}` }});
|
|
enableIcon = false;
|
|
shouldWait = match.startsWith("lucifer:icon:");
|
|
}
|
|
if (enableTag) {
|
|
const removeTags = oldTags.filter(ot => !newTags.includes(ot));
|
|
for (const removeTag of removeTags) {
|
|
await runCommand({removeAlias: { match, alias: `lucifer:tag:${removeTag}` }});
|
|
}
|
|
|
|
const addTags = newTags.filter(nt => !oldTags.includes(nt));
|
|
for (const addTag of addTags) {
|
|
await runCommand({addAlias: { match, alias: `lucifer:tag:${addTag}` }});
|
|
}
|
|
|
|
shouldWait = removeTags.length > 0 || addTags.length > 0;
|
|
}
|
|
if (enableRole) {
|
|
const removeRoles = oldRoles.filter(or => !newRoles.includes(or));
|
|
for (const removeRole of removeRoles) {
|
|
await runCommand({removeAlias: { match, alias: `lucifer:role:${removeRole}` }});
|
|
}
|
|
|
|
const addRoles = newRoles.filter(nr => !oldRoles.includes(nr));
|
|
for (const addRole of addRoles) {
|
|
await runCommand({addAlias: { match, alias: `lucifer:role:${addRole}` }});
|
|
}
|
|
|
|
shouldWait = removeRoles.length > 0 || addRoles.length > 0;
|
|
}
|
|
|
|
if (shouldWait) {
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
}
|
|
} catch (err) {}
|
|
|
|
reloadTagsAndRoles();
|
|
|
|
disabled = false;
|
|
}
|
|
|
|
let roomOptions: string[] = [];
|
|
$: roomOptions = $deviceList.flatMap(d => d.aliases)
|
|
.filter(k => k.startsWith("lucifer:room:"))
|
|
.sort()
|
|
.filter((v, i, a) => v !== a[i-1])
|
|
.map(r => r.slice("lucifer:room:".length));
|
|
|
|
let groupOptions: string[] = [];
|
|
$: groupOptions = $deviceList.flatMap(d => d.aliases)
|
|
.filter(k => k.startsWith("lucifer:group:"))
|
|
.sort()
|
|
.filter((v, i, a) => v !== a[i-1])
|
|
.map(r => r.slice("lucifer:group:".length));
|
|
|
|
$: {
|
|
if ($modal.kind === "device.edit") {
|
|
setupModal($modal.op);
|
|
} else {
|
|
closeModal();
|
|
}
|
|
}
|
|
|
|
$: if (!$selectedMasks.includes(match)) {
|
|
match = $selectedMasks[0];
|
|
}
|
|
</script>
|
|
|
|
<form novalidate on:submit|preventDefault={onSubmit}>
|
|
<Modal wide disabled={disabled} closable show={show} titleText="Device Editor" submitText="Save Changes">
|
|
<ModalBody>
|
|
<label for="mask">Selection</label>
|
|
<select bind:value={match}>
|
|
{#each $selectedMasks as option (option)}
|
|
<option value={option}>{option}</option>
|
|
{/each}
|
|
</select>
|
|
<ModalSection bind:expanded={enableAssign} title="Assign">
|
|
<HSplit reverse>
|
|
<HSplitPart>
|
|
<label for="states">Effect</label>
|
|
<select bind:value={newEffect.kind}>
|
|
<option value="gradient">Gradient</option>
|
|
<option value="pattern">Pattern</option>
|
|
<option value="random">Random</option>
|
|
<option value="solid">Solid</option>
|
|
<option value="vrange">Variable Range</option>
|
|
</select>
|
|
{#if newEffect.kind !== "manual" && newEffect.kind !== "vrange"}
|
|
<label for="animationMs">Interval (ms)</label>
|
|
<input type="number" name="animationMs" min=0 max=10000 step=100 bind:value={newEffect.animationMs} />
|
|
{/if}
|
|
{#if newEffect.kind === "solid"}
|
|
<label for="interleave">Interleave</label>
|
|
<input type="number" name="interleave" min=0 step=1 bind:value={newEffect.interleave} />
|
|
{/if}
|
|
{#if newEffect.kind === "vrange"}
|
|
<label for="states">Variable</label>
|
|
<select bind:value={newEffect.variable}>
|
|
<option value="motion.min">Motion Min (Seconds)</option>
|
|
<option value="motion.avg">Motion Avg (Seconds)</option>
|
|
<option value="motion.max">Motion Max (Seconds)</option>
|
|
<option value="temperature.min">Temperature Min (Celcius)</option>
|
|
<option value="temperature.avg">Temperature Avg (Celcius)</option>
|
|
<option value="temperature.max">Temperature Max (Celcius)</option>
|
|
</select>
|
|
<HSplit>
|
|
<HSplitPart left>
|
|
<label for="min">Min</label>
|
|
<input type="number" name="min" min=0 step=1 bind:value={newEffect.min} />
|
|
</HSplitPart>
|
|
<HSplitPart right>
|
|
<label for="max">Max</label>
|
|
<input type="number" name="max" min=0 step=1 bind:value={newEffect.max} />
|
|
</HSplitPart>
|
|
</HSplit>
|
|
{/if}
|
|
{#if ["gradient", "random", "vrange"].includes(newEffect.kind)}
|
|
<label for="states">Options</label>
|
|
<Checkbox bind:checked={newEffect.interpolate} label="Interpolate" />
|
|
{#if (newEffect.kind === "gradient")}
|
|
<Checkbox bind:checked={newEffect.reverse} label="Reverse" />
|
|
{/if}
|
|
{/if}
|
|
</HSplitPart>
|
|
<HSplitPart weight={1.0}>
|
|
<label for="states">States</label>
|
|
{#each newEffect.states as state, i }
|
|
<ScriptAssignmentState deletable bind:value={state} on:delete={() => removeEffectState(i)} />
|
|
{/each}
|
|
<Button on:click={addEffectState} icon><Icon name="plus" /></Button>
|
|
</HSplitPart>
|
|
</HSplit>
|
|
</ModalSection>
|
|
<ModalSection bind:expanded={enableRename} title="Rename">
|
|
<label for="name">New Name</label>
|
|
<input type="text" name="name" bind:value={newName} />
|
|
</ModalSection>
|
|
<ModalSection bind:expanded={enableRoom} title="Change Room">
|
|
<label for="newRoom">Select Room</label>
|
|
<select bind:value={newRoom}>
|
|
{#each roomOptions as roomOption}
|
|
<option value={roomOption}>{roomOption}</option>
|
|
{/each}
|
|
<option value="">Create Room</option>
|
|
</select>
|
|
{#if newRoom == ""}
|
|
<label for="customRoom">New Room</label>
|
|
<input type="text" name="customRoom" bind:value={customRoom} />
|
|
{/if}
|
|
</ModalSection>
|
|
<ModalSection bind:expanded={enableGroup} title="Change Group">
|
|
<label for="newGroup">Select Group</label>
|
|
<select bind:value={newGroup}>
|
|
{#each groupOptions as groupOption}
|
|
<option value={groupOption}>{groupOption}</option>
|
|
{/each}
|
|
<option value="">Create Group</option>
|
|
</select>
|
|
{#if newGroup == ""}
|
|
<label for="customGroup">New Group</label>
|
|
<input type="text" name="customGroup" bind:value={customGroup} />
|
|
{/if}
|
|
</ModalSection>
|
|
<ModalSection bind:expanded={enableIcon} title="Change Icon">
|
|
<label for="icon">New Icon</label>
|
|
<DeviceIconSelector bind:value={newIcon} />
|
|
</ModalSection>
|
|
<ModalSection bind:expanded={enableTag} title="Change Tags">
|
|
<label for="icon">Tags</label>
|
|
<TagInput bind:value={newTags} />
|
|
</ModalSection>
|
|
<ModalSection bind:expanded={enableRole} title="Change Roles">
|
|
<label for="icon">Roles</label>
|
|
<TagInput bind:value={newRoles} />
|
|
</ModalSection>
|
|
</ModalBody>
|
|
</Modal>
|
|
</form>
|