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

<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>