diff --git a/effects/animation.go b/effects/animation.go new file mode 100644 index 0000000..7244b65 --- /dev/null +++ b/effects/animation.go @@ -0,0 +1,43 @@ +package effects + +import ( + "fmt" + "git.aiterp.net/lucifer3/server/device" + "time" +) + +type Solid struct { + States []device.State `json:"states,omitempty"` + AnimationMS int64 `json:"animationMs,omitempty"` + Interleave int `json:"interleave,omitempty"` +} + +func (e Solid) State(_, length, round int) device.State { + if len(e.States) == 0 { + return device.State{} + } + if len(e.States) == 1 { + return e.States[0] + } + + interleave := e.Interleave + 1 + if interleave < 1 { + interleave = 1 + } + + if interleave > 1 { + calcIndex := round % (len(e.States) * interleave) + return gradientState(append(e.States, e.States[0]), e.Interleave != 0, calcIndex, len(e.States)*interleave+1) + } else { + calcIndex := round % len(e.States) + return gradientState(e.States, e.Interleave != 0, calcIndex, len(e.States)*interleave) + } +} + +func (e Solid) Frequency() time.Duration { + return time.Duration(e.AnimationMS) * time.Millisecond +} + +func (e Solid) EffectDescription() string { + return fmt.Sprintf("Solid(states:%s, anim:%dms, interleave:%d)", statesDescription(e.States), e.AnimationMS, e.Interleave) +} diff --git a/effects/gradient.go b/effects/gradient.go index 2f5106e..0a83374 100644 --- a/effects/gradient.go +++ b/effects/gradient.go @@ -8,8 +8,8 @@ import ( type Gradient struct { States []device.State `json:"states,omitempty"` - AnimationMS int64 `json:"AnimationMs,omitempty"` - Reverse bool `json:"backward,omitempty"` + AnimationMS int64 `json:"animationMs,omitempty"` + Reverse bool `json:"reverse,omitempty"` Interpolate bool `json:"interpolate,omitempty"` } diff --git a/effects/serializable.go b/effects/serializable.go index b418d25..4f6c3ee 100644 --- a/effects/serializable.go +++ b/effects/serializable.go @@ -8,6 +8,7 @@ import ( type serializedEffect struct { Manual *Manual `json:"manual,omitempty"` + Solid *Solid `json:"solid,omitempty"` Gradient *Gradient `json:"gradient,omitempty"` Pattern *Pattern `json:"pattern,omitempty"` Random *Random `json:"random,omitempty"` @@ -28,6 +29,8 @@ func (s *Serializable) UnmarshalJSON(raw []byte) error { switch { case value.Manual != nil: s.Effect = *value.Manual + case value.Solid != nil: + s.Effect = *value.Solid case value.Gradient != nil: s.Effect = *value.Gradient case value.Pattern != nil: @@ -47,6 +50,8 @@ func (s *Serializable) MarshalJSON() ([]byte, error) { switch effect := s.Effect.(type) { case Manual: return json.Marshal(serializedEffect{Manual: &effect}) + case Solid: + return json.Marshal(serializedEffect{Solid: &effect}) case Gradient: return json.Marshal(serializedEffect{Gradient: &effect}) case Pattern: diff --git a/effects/vrange.go b/effects/vrange.go index f6c90f8..6fee806 100644 --- a/effects/vrange.go +++ b/effects/vrange.go @@ -7,10 +7,11 @@ import ( ) type VRange struct { - States []device.State `json:"states,omitempty"` - Variable string `json:"variable"` - Min float64 `json:"min"` - Max float64 `json:"max"` + States []device.State `json:"states,omitempty"` + Variable string `json:"variable"` + Min float64 `json:"min"` + Max float64 `json:"max"` + Interpolate bool `json:"interpolate"` } func (e VRange) VariableName() string { @@ -24,7 +25,7 @@ func (e VRange) VariableState(_, _ int, value float64) device.State { return e.States[len(e.States)-1] } - return gradientStateFactor(e.States, true, (value-e.Min)/(e.Max-e.Min)) + return gradientStateFactor(e.States, e.Interpolate, (value-e.Min)/(e.Max-e.Min)) } func (e VRange) State(_, _, _ int) device.State { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9492229..9a2d861 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,19 +8,24 @@ "name": "frontend", "version": "0.0.1", "devDependencies": { + "@fortawesome/free-solid-svg-icons": "^6.4.2", "@sveltejs/adapter-auto": "^1.0.0", + "@sveltejs/adapter-static": "^2.0.3", "@sveltejs/kit": "^1.0.0", + "@types/svelte-range-slider-pips": "^2.0.0", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte3": "^4.0.0", + "fa-svelte": "^3.1.0", "node-sass": "^8.0.0", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.8.1", "sass": "^1.57.1", "svelte": "^3.54.0", "svelte-check": "^2.9.2", + "svelte-range-slider-pips": "^2.2.2", "tslib": "^2.4.1", "typescript": "^4.9.3", "vite": "^4.0.0" @@ -507,6 +512,29 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", + "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", + "integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -663,6 +691,15 @@ "@sveltejs/kit": "^1.0.0" } }, + "node_modules/@sveltejs/adapter-static": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.3.tgz", + "integrity": "sha512-VUqTfXsxYGugCpMqQv1U0LIdbR3S5nBkMMDmpjGVJyM6Q2jHVMFtdWJCkeHMySc6mZxJ+0eZK3T7IgmUCDrcUQ==", + "dev": true, + "peerDependencies": { + "@sveltejs/kit": "^1.5.0" + } + }, "node_modules/@sveltejs/kit": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.24.0.tgz", @@ -794,6 +831,15 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, + "node_modules/@types/svelte-range-slider-pips": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/svelte-range-slider-pips/-/svelte-range-slider-pips-2.0.0.tgz", + "integrity": "sha512-+Yq3xwyI0ViGZKdrsNpM8Wqa0wCnSBKc8NcRfYGiu22kVJ3Xr0gfiZATWMzZ2mryt/tQR8QgH2B782ogrZS1aw==", + "dev": true, + "dependencies": { + "svelte": "^3.0.0" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.48.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz", @@ -1946,6 +1992,12 @@ "node": ">=0.10.0" } }, + "node_modules/fa-svelte": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fa-svelte/-/fa-svelte-3.1.0.tgz", + "integrity": "sha512-RqBOWwt7sc+ta9GFjbu5GOwKFRzn3rMPPSqvSGpIwsfVnpMjiI5ttv84lwNsCMEYI6/lu/iH21HUcE3TLz8RGQ==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4482,6 +4534,12 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/svelte-range-slider-pips": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svelte-range-slider-pips/-/svelte-range-slider-pips-2.2.2.tgz", + "integrity": "sha512-SsiIBpCkBnqvfaHLeIDolTfZlc8m7y93MhlxPKQ90YMZnE/rAxj+IoCrd7LTL/nXA2SCla4IJutSzfwdOtJddA==", + "dev": true + }, "node_modules/tar": { "version": "6.1.13", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", @@ -5145,6 +5203,21 @@ "strip-json-comments": "^3.1.1" } }, + "@fortawesome/fontawesome-common-types": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", + "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==", + "dev": true + }, + "@fortawesome/free-solid-svg-icons": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", + "integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==", + "dev": true, + "requires": { + "@fortawesome/fontawesome-common-types": "6.4.2" + } + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -5265,6 +5338,13 @@ "import-meta-resolve": "^2.2.0" } }, + "@sveltejs/adapter-static": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.3.tgz", + "integrity": "sha512-VUqTfXsxYGugCpMqQv1U0LIdbR3S5nBkMMDmpjGVJyM6Q2jHVMFtdWJCkeHMySc6mZxJ+0eZK3T7IgmUCDrcUQ==", + "dev": true, + "requires": {} + }, "@sveltejs/kit": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.24.0.tgz", @@ -5367,6 +5447,15 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, + "@types/svelte-range-slider-pips": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/svelte-range-slider-pips/-/svelte-range-slider-pips-2.0.0.tgz", + "integrity": "sha512-+Yq3xwyI0ViGZKdrsNpM8Wqa0wCnSBKc8NcRfYGiu22kVJ3Xr0gfiZATWMzZ2mryt/tQR8QgH2B782ogrZS1aw==", + "dev": true, + "requires": { + "svelte": "^3.0.0" + } + }, "@typescript-eslint/eslint-plugin": { "version": "5.48.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz", @@ -6194,6 +6283,12 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "fa-svelte": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fa-svelte/-/fa-svelte-3.1.0.tgz", + "integrity": "sha512-RqBOWwt7sc+ta9GFjbu5GOwKFRzn3rMPPSqvSGpIwsfVnpMjiI5ttv84lwNsCMEYI6/lu/iH21HUcE3TLz8RGQ==", + "dev": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8061,6 +8156,12 @@ } } }, + "svelte-range-slider-pips": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svelte-range-slider-pips/-/svelte-range-slider-pips-2.2.2.tgz", + "integrity": "sha512-SsiIBpCkBnqvfaHLeIDolTfZlc8m7y93MhlxPKQ90YMZnE/rAxj+IoCrd7LTL/nXA2SCla4IJutSzfwdOtJddA==", + "dev": true + }, "tar": { "version": "6.1.13", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index 21fb9bb..6cf575f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,19 +12,24 @@ "format": "prettier --plugin-search-dir . --write ." }, "devDependencies": { + "@fortawesome/free-solid-svg-icons": "^6.4.2", "@sveltejs/adapter-auto": "^1.0.0", + "@sveltejs/adapter-static": "^2.0.3", "@sveltejs/kit": "^1.0.0", + "@types/svelte-range-slider-pips": "^2.0.0", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte3": "^4.0.0", + "fa-svelte": "^3.1.0", "node-sass": "^8.0.0", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.8.1", "sass": "^1.57.1", "svelte": "^3.54.0", "svelte-check": "^2.9.2", + "svelte-range-slider-pips": "^2.2.2", "tslib": "^2.4.1", "typescript": "^4.9.3", "vite": "^4.0.0" diff --git a/frontend/src/lib/client/lucifer.ts b/frontend/src/lib/client/lucifer.ts index 1017ca4..a636403 100644 --- a/frontend/src/lib/client/lucifer.ts +++ b/frontend/src/lib/client/lucifer.ts @@ -1,3 +1,5 @@ +import type { Color } from "$lib/models/color"; +import type CommandInput from "$lib/models/command"; import type UIState from "$lib/models/uistate"; export default async function fetchLucifer(path: string, init?: RequestInit): Promise { @@ -21,3 +23,15 @@ export async function fetchUIState(): Promise { return fetchLucifer("state"); } +export async function fetchColor(color: string): Promise { + return fetchLucifer("color/"+color); +} + +export async function runCommand(command: CommandInput): Promise { + return fetchLucifer("command", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(command), + }) +} + diff --git a/frontend/src/lib/components/AssignmentState.svelte b/frontend/src/lib/components/AssignmentState.svelte new file mode 100644 index 0000000..0d7689d --- /dev/null +++ b/frontend/src/lib/components/AssignmentState.svelte @@ -0,0 +1,294 @@ + + + + + +
+
+
+ + {#if value.intensity != null} + + {/if} +
+
+ + {#if value.temperature != null} + + {/if} +
+
+ + {#if colorKind !== "null"} + {#if colorKind === "k"} +
+ + +
+ {/if} + {#if colorKind === "hs"} +
+ + +
+
+ + +
+ {/if} + {#if colorKind === "xy"} +
+ + +
+
+ + +
+ {/if} + {#if colorKind === "rgb"} +
+ + +
+
+ + +
+
+ + +
+ {/if} + {/if} +
+ {#if deletable} +
+ dispatch("delete")} block name="trash" /> +
+ {/if} +
+ + \ No newline at end of file diff --git a/frontend/src/lib/components/Button.svelte b/frontend/src/lib/components/Button.svelte new file mode 100644 index 0000000..bb0ab14 --- /dev/null +++ b/frontend/src/lib/components/Button.svelte @@ -0,0 +1,31 @@ + + + +
+ + \ No newline at end of file diff --git a/frontend/src/lib/components/Checkbox.svelte b/frontend/src/lib/components/Checkbox.svelte new file mode 100644 index 0000000..db6c19e --- /dev/null +++ b/frontend/src/lib/components/Checkbox.svelte @@ -0,0 +1,117 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/lib/components/DeviceIcon.svelte b/frontend/src/lib/components/DeviceIcon.svelte index a1ba951..b519968 100644 --- a/frontend/src/lib/components/DeviceIcon.svelte +++ b/frontend/src/lib/components/DeviceIcon.svelte @@ -12,11 +12,11 @@ import hue_adore_tube from "./icons/hue_adore_tube.svg?raw"; import hue_playbar from "./icons/hue_playbar.svg?raw"; import hue_go from "./icons/hue_go.svg?raw"; - import square from "./icons/square.svg?raw"; - import hexagon from "./icons/hexagon.svg?raw"; - import triangle from "./icons/triangle.svg?raw"; + import shape_square from "./icons/shape_square.svg?raw"; + import shape_hexagon from "./icons/shape_hexagon.svg?raw"; + import shape_triangle from "./icons/shape_triangle.svg?raw"; - export const iconMap = Object.seal({ + export const deviceIconMap = Object.seal({ generic_lamp, generic_boob, generic_ball, @@ -29,13 +29,15 @@ hue_lightbulb_gu10, hue_adore_tube, hue_playbar, - square, - hexagon, - triangle, + shape_square, + shape_hexagon, + shape_triangle, hue_go, }); - export type IconName = keyof typeof iconMap; + export const deviceIconList = Object.seal(Object.keys(deviceIconMap).sort()) as DeviceIconName[]; + + export type DeviceIconName = keyof typeof deviceIconMap; + +
+ {#each deviceIconList as deviceIconName (deviceIconName)} + +
{ value = deviceIconName }}> + +
+ {/each} +
+ + \ No newline at end of file diff --git a/frontend/src/lib/components/HSplit.svelte b/frontend/src/lib/components/HSplit.svelte new file mode 100644 index 0000000..7a85d28 --- /dev/null +++ b/frontend/src/lib/components/HSplit.svelte @@ -0,0 +1,18 @@ + + +
+ + \ No newline at end of file diff --git a/frontend/src/lib/components/HSplitPart.svelte b/frontend/src/lib/components/HSplitPart.svelte new file mode 100644 index 0000000..00d025c --- /dev/null +++ b/frontend/src/lib/components/HSplitPart.svelte @@ -0,0 +1,18 @@ + + +
+ + \ No newline at end of file diff --git a/frontend/src/lib/components/Icon.svelte b/frontend/src/lib/components/Icon.svelte new file mode 100644 index 0000000..a8340c7 --- /dev/null +++ b/frontend/src/lib/components/Icon.svelte @@ -0,0 +1,97 @@ + + + +{#if block} +
+ +
+{:else} + +{/if} + + + + \ No newline at end of file diff --git a/frontend/src/lib/components/Lamp.svelte b/frontend/src/lib/components/Lamp.svelte index 54c77d6..68c7b26 100644 --- a/frontend/src/lib/components/Lamp.svelte +++ b/frontend/src/lib/components/Lamp.svelte @@ -9,6 +9,7 @@ import { SupportFlags } from "$lib/models/device"; import { rgb, type ColorRGB } from "../models/color"; import DeviceIcon from "./DeviceIcon.svelte"; + import Icon from "./Icon.svelte"; export let device: Device; export let compact: boolean = false; @@ -25,6 +26,9 @@ let barColor: ColorRGB | null; let barFraction: number | null; let roundboiText: string | null; + let roles: string[]; + let tags: string[]; + let hasRoleOrTag: boolean; $: { // TODO: Fix device.name on the backend const nameAlias = device.aliases?.find(a => a.startsWith("lucifer:name:")); @@ -34,6 +38,10 @@ deviceTitle = ""; } + roles = device.aliases?.filter(a => a.startsWith("lucifer:role:")).map(a => a.slice("lucifer:role:".length)); + tags = device.aliases?.filter(a => a.startsWith("lucifer:tag:")).map(a => a.slice("lucifer:tag:".length)); + hasRoleOrTag = !!(roles.length || tags.length) + barFraction = null; barColor = null; iconColor = null; @@ -114,7 +122,25 @@ {/if} -
{displayTitle}
+
+
{displayTitle}
+ {#if hasRoleOrTag} +
+ {#each roles as role} +
+ +
{role}
+
+ {/each} + {#each tags as tag} +
+ +
{tag}
+
+ {/each} +
+ {/if} +
{#if barColor != null && barFraction != null} @@ -182,7 +208,24 @@ margin-left: 2em margin-right: 1ch height: 1.6em - + + > div.name.hasRoleOrTag + margin-top: -0.275em + + > div.tag-list + display: flex + flex-direction: row + font-size: 0.5em + + > div.tag + display: flex + flex-direction: row + padding: 0 0.5ch + padding-right: 1ch + + div.tag-name + padding-left: 0.5ch + > div.flatboi height: 0.2em diff --git a/frontend/src/lib/components/Modal.svelte b/frontend/src/lib/components/Modal.svelte index 29d7b56..4504b7b 100644 --- a/frontend/src/lib/components/Modal.svelte +++ b/frontend/src/lib/components/Modal.svelte @@ -2,8 +2,8 @@ import { getModalContext, type ModalSelection } from "$lib/contexts/ModalContext.svelte"; export let show: boolean = false; - export let verb: string = "Submit"; - export let noun: string = "Form"; + export let titleText: string = "Edit Form"; + export let submitText: string = "Submit"; export let cancelLabel: string = "Cancel"; export let wide: boolean = false; export let error: string | null = null; @@ -30,7 +30,7 @@