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.

343 lines
13 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
  1. <script lang="ts">
  2. import { runCommand } from "$lib/client/lucifer";
  3. import AssignmentState from "$lib/components/scripting/ScriptAssignmentState.svelte";
  4. import Button from "$lib/components/Button.svelte";
  5. import Checkbox from "$lib/components/Checkbox.svelte";
  6. import type { DeviceIconName } from "$lib/components/DeviceIcon.svelte";
  7. import DeviceIconSelector from "$lib/components/DeviceIconSelector.svelte";
  8. import HSplit from "$lib/components/HSplit.svelte";
  9. import HSplitPart from "$lib/components/HSplitPart.svelte";
  10. import Icon from "$lib/components/Icon.svelte";
  11. import Modal from "$lib/components/Modal.svelte";
  12. import ModalBody from "$lib/components/ModalBody.svelte";
  13. import ModalSection from "$lib/components/ModalSection.svelte";
  14. import TagInput from "$lib/components/TagInput.svelte";
  15. import { getModalContext } from "$lib/contexts/ModalContext.svelte";
  16. import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
  17. import { getStateContext } from "$lib/contexts/StateContext.svelte";
  18. import { toEffectRaw, type EffectRaw, fromEffectRaw } from "$lib/models/assignment";
  19. import type { DeviceEditOp } from "$lib/models/device";
  20. import { iconName } from "@fortawesome/free-solid-svg-icons/faQuestion";
  21. import ScriptAssignmentState from "$lib/components/scripting/ScriptAssignmentState.svelte";
  22. const { modal } = getModalContext();
  23. const { selectedMasks, selectedMap, selectedList } = getSelectedContext();
  24. const { deviceList, assignmentList } = getStateContext();
  25. let show: boolean = false;
  26. let match: string = "";
  27. let disabled: boolean = false;
  28. let enableRename: boolean = false;
  29. let newName: string = "";
  30. let enableRoom: boolean = false;
  31. let newRoom: string = "";
  32. let customRoom: string = "";
  33. let enableGroup: boolean = false;
  34. let newGroup: string = "";
  35. let customGroup: string = "";
  36. let enableAssign: boolean = false;
  37. let newEffect: EffectRaw = toEffectRaw(undefined);
  38. let enableIcon: boolean = false;
  39. let newIcon: DeviceIconName = "generic_ball";
  40. let enableTag: boolean = false;
  41. let newTags: string[] = [];
  42. let oldTags: string[] = [];
  43. let enableRole: boolean = false;
  44. let newRoles: string[] = [];
  45. let oldRoles: string[] = [];
  46. function setupModal(op: DeviceEditOp) {
  47. show = true;
  48. enableRename = (op === "rename");
  49. enableAssign = (op === "assign");
  50. enableIcon = (op === "change_icon");
  51. enableRoom = (op === "move_room");
  52. enableGroup = (op === "move_group");
  53. enableTag = (op === "change_tags");
  54. enableRole = (op === "change_roles");
  55. const firstDevice = $deviceList.find(d => $selectedMap[d.id]);
  56. newName = firstDevice?.name || "";
  57. newIcon = firstDevice?.icon || "generic_ball";
  58. newEffect = toEffectRaw(undefined);
  59. newRoom = "";
  60. newGroup = "";
  61. reloadTagsAndRoles();
  62. for (const device of $deviceList) {
  63. if (!$selectedMap[device.id]) {
  64. continue;
  65. }
  66. if (newRoom == "") {
  67. const roomAlias = device.aliases.find(a => a.startsWith("lucifer:room:"))?.slice("lucifer:room:".length);
  68. if (roomAlias != null) {
  69. newRoom = roomAlias;
  70. }
  71. }
  72. if (newGroup == "") {
  73. const groupAlias = device.aliases.find(a => a.startsWith("lucifer:group:"))?.slice("lucifer:group:".length);
  74. if (groupAlias != null) {
  75. newGroup = groupAlias;
  76. }
  77. }
  78. }
  79. let mostPopularEffect = 0;
  80. for (const assignment of $assignmentList) {
  81. const selectedCount = assignment.deviceIds?.filter(id => $selectedMap[id]).length || 0;
  82. if (selectedCount > mostPopularEffect) {
  83. newEffect = toEffectRaw(assignment.effect);
  84. mostPopularEffect = selectedCount;
  85. }
  86. }
  87. }
  88. function closeModal() {
  89. show = false;
  90. match = "";
  91. }
  92. function addEffectState() {
  93. if (newEffect.states.length > 0) {
  94. newEffect.states = [...newEffect.states, {...newEffect.states[newEffect.states.length - 1]}];
  95. } else {
  96. newEffect.states = [...newEffect.states, {color: null, intensity: null, power: null, temperature: null}]
  97. }
  98. }
  99. function removeEffectState(i: number) {
  100. newEffect.states = [...newEffect.states.slice(0, i), ...newEffect.states.slice(i+1)];
  101. }
  102. function reloadTagsAndRoles() {
  103. const firstDevice = $deviceList.find(d => $selectedMap[d.id]);
  104. newTags = firstDevice?.aliases
  105. .filter(a => a.startsWith("lucifer:tag:"))
  106. .filter(a => !$deviceList.filter(d => $selectedMap[d.id]).find(d => !d.aliases.includes(a)))
  107. .map(a => a.slice("lucifer:tag:".length)) || [];
  108. newRoles = firstDevice?.aliases
  109. .filter(a => a.startsWith("lucifer:role:"))
  110. .filter(a => !$deviceList.filter(d => $selectedMap[d.id]).find(d => !d.aliases.includes(a)))
  111. .map(a => a.slice("lucifer:role:".length)) || [];
  112. oldTags = [...newTags];
  113. oldRoles = [...newRoles];
  114. }
  115. async function onSubmit() {
  116. disabled = true;
  117. let shouldWait = false;
  118. try {
  119. if (enableRename && newName !== "") {
  120. await runCommand({addAlias: { match, alias: `lucifer:name:${newName}` }});
  121. enableRename = false;
  122. shouldWait = match.startsWith("lucifer:name:");
  123. }
  124. if (enableAssign) {
  125. await runCommand({assign: { match, effect: fromEffectRaw(newEffect) }});
  126. }
  127. if (enableRoom) {
  128. await runCommand({addAlias: { match, alias: `lucifer:room:${newRoom || customRoom}` }});
  129. enableRoom = false;
  130. shouldWait = match.startsWith("lucifer:room:");
  131. newRoom = newRoom || customRoom;
  132. }
  133. if (enableGroup) {
  134. await runCommand({addAlias: { match, alias: `lucifer:group:${newGroup || customGroup}` }});
  135. enableGroup = false;
  136. shouldWait = match.startsWith("lucifer:group:");
  137. newGroup = newGroup || customGroup;
  138. }
  139. if (enableIcon) {
  140. await runCommand({addAlias: { match, alias: `lucifer:icon:${newIcon}` }});
  141. enableIcon = false;
  142. shouldWait = match.startsWith("lucifer:icon:");
  143. }
  144. if (enableTag) {
  145. const removeTags = oldTags.filter(ot => !newTags.includes(ot));
  146. for (const removeTag of removeTags) {
  147. await runCommand({removeAlias: { match, alias: `lucifer:tag:${removeTag}` }});
  148. }
  149. const addTags = newTags.filter(nt => !oldTags.includes(nt));
  150. for (const addTag of addTags) {
  151. await runCommand({addAlias: { match, alias: `lucifer:tag:${addTag}` }});
  152. }
  153. shouldWait = removeTags.length > 0 || addTags.length > 0;
  154. }
  155. if (enableRole) {
  156. const removeRoles = oldRoles.filter(or => !newRoles.includes(or));
  157. for (const removeRole of removeRoles) {
  158. await runCommand({removeAlias: { match, alias: `lucifer:role:${removeRole}` }});
  159. }
  160. const addRoles = newRoles.filter(nr => !oldRoles.includes(nr));
  161. for (const addRole of addRoles) {
  162. await runCommand({addAlias: { match, alias: `lucifer:role:${addRole}` }});
  163. }
  164. shouldWait = removeRoles.length > 0 || addRoles.length > 0;
  165. }
  166. if (shouldWait) {
  167. await new Promise(resolve => setTimeout(resolve, 1000))
  168. }
  169. } catch (err) {}
  170. reloadTagsAndRoles();
  171. disabled = false;
  172. }
  173. let roomOptions: string[] = [];
  174. $: roomOptions = $deviceList.flatMap(d => d.aliases)
  175. .filter(k => k.startsWith("lucifer:room:"))
  176. .sort()
  177. .filter((v, i, a) => v !== a[i-1])
  178. .map(r => r.slice("lucifer:room:".length));
  179. let groupOptions: string[] = [];
  180. $: groupOptions = $deviceList.flatMap(d => d.aliases)
  181. .filter(k => k.startsWith("lucifer:group:"))
  182. .sort()
  183. .filter((v, i, a) => v !== a[i-1])
  184. .map(r => r.slice("lucifer:group:".length));
  185. $: {
  186. if ($modal.kind === "device.edit") {
  187. setupModal($modal.op);
  188. } else {
  189. closeModal();
  190. }
  191. }
  192. $: if (!$selectedMasks.includes(match)) {
  193. match = $selectedMasks[0];
  194. }
  195. </script>
  196. <form novalidate on:submit|preventDefault={onSubmit}>
  197. <Modal wide disabled={disabled} closable show={show} titleText="Device Editor" submitText="Save Changes">
  198. <ModalBody>
  199. <label for="mask">Selection</label>
  200. <select bind:value={match}>
  201. {#each $selectedMasks as option (option)}
  202. <option value={option}>{option}</option>
  203. {/each}
  204. </select>
  205. <ModalSection bind:expanded={enableAssign} title="Assign">
  206. <HSplit reverse>
  207. <HSplitPart>
  208. <label for="states">Effect</label>
  209. <select bind:value={newEffect.kind}>
  210. <option value="gradient">Gradient</option>
  211. <option value="pattern">Pattern</option>
  212. <option value="random">Random</option>
  213. <option value="solid">Solid</option>
  214. <option value="vrange">Variable Range</option>
  215. </select>
  216. {#if newEffect.kind !== "manual" && newEffect.kind !== "vrange"}
  217. <label for="animationMs">Interval (ms)</label>
  218. <input type="number" name="animationMs" min=0 max=10000 step=100 bind:value={newEffect.animationMs} />
  219. {/if}
  220. {#if newEffect.kind === "solid"}
  221. <label for="interleave">Interleave</label>
  222. <input type="number" name="interleave" min=0 step=1 bind:value={newEffect.interleave} />
  223. {/if}
  224. {#if newEffect.kind === "vrange"}
  225. <label for="states">Variable</label>
  226. <select bind:value={newEffect.variable}>
  227. <option value="motion.min">Motion Min (Seconds)</option>
  228. <option value="motion.avg">Motion Avg (Seconds)</option>
  229. <option value="motion.max">Motion Max (Seconds)</option>
  230. <option value="temperature.min">Temperature Min (Celcius)</option>
  231. <option value="temperature.avg">Temperature Avg (Celcius)</option>
  232. <option value="temperature.max">Temperature Max (Celcius)</option>
  233. </select>
  234. <HSplit>
  235. <HSplitPart left>
  236. <label for="min">Min</label>
  237. <input type="number" name="min" min=0 step=1 bind:value={newEffect.min} />
  238. </HSplitPart>
  239. <HSplitPart right>
  240. <label for="max">Max</label>
  241. <input type="number" name="max" min=0 step=1 bind:value={newEffect.max} />
  242. </HSplitPart>
  243. </HSplit>
  244. {/if}
  245. {#if ["gradient", "random", "vrange"].includes(newEffect.kind)}
  246. <label for="states">Options</label>
  247. <Checkbox bind:checked={newEffect.interpolate} label="Interpolate" />
  248. {#if (newEffect.kind === "gradient")}
  249. <Checkbox bind:checked={newEffect.reverse} label="Reverse" />
  250. {/if}
  251. {/if}
  252. </HSplitPart>
  253. <HSplitPart weight={1.0}>
  254. <label for="states">States</label>
  255. {#each newEffect.states as state, i }
  256. <ScriptAssignmentState deletable bind:value={state} on:delete={() => removeEffectState(i)} />
  257. {/each}
  258. <Button on:click={addEffectState} icon><Icon name="plus" /></Button>
  259. </HSplitPart>
  260. </HSplit>
  261. </ModalSection>
  262. <ModalSection bind:expanded={enableRename} title="Rename">
  263. <label for="name">New Name</label>
  264. <input type="text" name="name" bind:value={newName} />
  265. </ModalSection>
  266. <ModalSection bind:expanded={enableRoom} title="Change Room">
  267. <label for="newRoom">Select Room</label>
  268. <select bind:value={newRoom}>
  269. {#each roomOptions as roomOption}
  270. <option value={roomOption}>{roomOption}</option>
  271. {/each}
  272. <option value="">Create Room</option>
  273. </select>
  274. {#if newRoom == ""}
  275. <label for="customRoom">New Room</label>
  276. <input type="text" name="customRoom" bind:value={customRoom} />
  277. {/if}
  278. </ModalSection>
  279. <ModalSection bind:expanded={enableGroup} title="Change Group">
  280. <label for="newGroup">Select Group</label>
  281. <select bind:value={newGroup}>
  282. {#each groupOptions as groupOption}
  283. <option value={groupOption}>{groupOption}</option>
  284. {/each}
  285. <option value="">Create Group</option>
  286. </select>
  287. {#if newGroup == ""}
  288. <label for="customGroup">New Group</label>
  289. <input type="text" name="customGroup" bind:value={customGroup} />
  290. {/if}
  291. </ModalSection>
  292. <ModalSection bind:expanded={enableIcon} title="Change Icon">
  293. <label for="icon">New Icon</label>
  294. <DeviceIconSelector bind:value={newIcon} />
  295. </ModalSection>
  296. <ModalSection bind:expanded={enableTag} title="Change Tags">
  297. <label for="icon">Tags</label>
  298. <TagInput bind:value={newTags} />
  299. </ModalSection>
  300. <ModalSection bind:expanded={enableRole} title="Change Roles">
  301. <label for="icon">Roles</label>
  302. <TagInput bind:value={newRoles} />
  303. </ModalSection>
  304. </ModalBody>
  305. </Modal>
  306. </form>