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.

363 lines
14 KiB

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