moar stuff.
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
+2
-2
@@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
type Gradient struct {
|
type Gradient struct {
|
||||||
States []device.State `json:"states,omitempty"`
|
States []device.State `json:"states,omitempty"`
|
||||||
AnimationMS int64 `json:"AnimationMs,omitempty"`
|
AnimationMS int64 `json:"animationMs,omitempty"`
|
||||||
Reverse bool `json:"backward,omitempty"`
|
Reverse bool `json:"reverse,omitempty"`
|
||||||
Interpolate bool `json:"interpolate,omitempty"`
|
Interpolate bool `json:"interpolate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
type serializedEffect struct {
|
type serializedEffect struct {
|
||||||
Manual *Manual `json:"manual,omitempty"`
|
Manual *Manual `json:"manual,omitempty"`
|
||||||
|
Solid *Solid `json:"solid,omitempty"`
|
||||||
Gradient *Gradient `json:"gradient,omitempty"`
|
Gradient *Gradient `json:"gradient,omitempty"`
|
||||||
Pattern *Pattern `json:"pattern,omitempty"`
|
Pattern *Pattern `json:"pattern,omitempty"`
|
||||||
Random *Random `json:"random,omitempty"`
|
Random *Random `json:"random,omitempty"`
|
||||||
@@ -28,6 +29,8 @@ func (s *Serializable) UnmarshalJSON(raw []byte) error {
|
|||||||
switch {
|
switch {
|
||||||
case value.Manual != nil:
|
case value.Manual != nil:
|
||||||
s.Effect = *value.Manual
|
s.Effect = *value.Manual
|
||||||
|
case value.Solid != nil:
|
||||||
|
s.Effect = *value.Solid
|
||||||
case value.Gradient != nil:
|
case value.Gradient != nil:
|
||||||
s.Effect = *value.Gradient
|
s.Effect = *value.Gradient
|
||||||
case value.Pattern != nil:
|
case value.Pattern != nil:
|
||||||
@@ -47,6 +50,8 @@ func (s *Serializable) MarshalJSON() ([]byte, error) {
|
|||||||
switch effect := s.Effect.(type) {
|
switch effect := s.Effect.(type) {
|
||||||
case Manual:
|
case Manual:
|
||||||
return json.Marshal(serializedEffect{Manual: &effect})
|
return json.Marshal(serializedEffect{Manual: &effect})
|
||||||
|
case Solid:
|
||||||
|
return json.Marshal(serializedEffect{Solid: &effect})
|
||||||
case Gradient:
|
case Gradient:
|
||||||
return json.Marshal(serializedEffect{Gradient: &effect})
|
return json.Marshal(serializedEffect{Gradient: &effect})
|
||||||
case Pattern:
|
case Pattern:
|
||||||
|
|||||||
+2
-1
@@ -11,6 +11,7 @@ type VRange struct {
|
|||||||
Variable string `json:"variable"`
|
Variable string `json:"variable"`
|
||||||
Min float64 `json:"min"`
|
Min float64 `json:"min"`
|
||||||
Max float64 `json:"max"`
|
Max float64 `json:"max"`
|
||||||
|
Interpolate bool `json:"interpolate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e VRange) VariableName() string {
|
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 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 {
|
func (e VRange) State(_, _, _ int) device.State {
|
||||||
|
|||||||
Generated
+101
@@ -8,19 +8,24 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@sveltejs/adapter-auto": "^1.0.0",
|
"@sveltejs/adapter-auto": "^1.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^2.0.3",
|
||||||
"@sveltejs/kit": "^1.0.0",
|
"@sveltejs/kit": "^1.0.0",
|
||||||
|
"@types/svelte-range-slider-pips": "^2.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
|
"fa-svelte": "^3.1.0",
|
||||||
"node-sass": "^8.0.0",
|
"node-sass": "^8.0.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"prettier-plugin-svelte": "^2.8.1",
|
"prettier-plugin-svelte": "^2.8.1",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"svelte": "^3.54.0",
|
"svelte": "^3.54.0",
|
||||||
"svelte-check": "^2.9.2",
|
"svelte-check": "^2.9.2",
|
||||||
|
"svelte-range-slider-pips": "^2.2.2",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.3",
|
||||||
"vite": "^4.0.0"
|
"vite": "^4.0.0"
|
||||||
@@ -507,6 +512,29 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"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": {
|
"node_modules/@gar/promisify": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||||
@@ -663,6 +691,15 @@
|
|||||||
"@sveltejs/kit": "^1.0.0"
|
"@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": {
|
"node_modules/@sveltejs/kit": {
|
||||||
"version": "1.24.0",
|
"version": "1.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.24.0.tgz",
|
||||||
@@ -794,6 +831,15 @@
|
|||||||
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.48.0",
|
"version": "5.48.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz",
|
||||||
@@ -1946,6 +1992,12 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -4482,6 +4534,12 @@
|
|||||||
"sourcemap-codec": "^1.4.8"
|
"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": {
|
"node_modules/tar": {
|
||||||
"version": "6.1.13",
|
"version": "6.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
|
||||||
@@ -5145,6 +5203,21 @@
|
|||||||
"strip-json-comments": "^3.1.1"
|
"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": {
|
"@gar/promisify": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||||
@@ -5265,6 +5338,13 @@
|
|||||||
"import-meta-resolve": "^2.2.0"
|
"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": {
|
"@sveltejs/kit": {
|
||||||
"version": "1.24.0",
|
"version": "1.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.24.0.tgz",
|
||||||
@@ -5367,6 +5447,15 @@
|
|||||||
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
||||||
"dev": true
|
"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": {
|
"@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.48.0",
|
"version": "5.48.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz",
|
"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==",
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
"dev": true
|
"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": {
|
"fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"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": {
|
"tar": {
|
||||||
"version": "6.1.13",
|
"version": "6.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
|
||||||
|
|||||||
@@ -12,19 +12,24 @@
|
|||||||
"format": "prettier --plugin-search-dir . --write ."
|
"format": "prettier --plugin-search-dir . --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@sveltejs/adapter-auto": "^1.0.0",
|
"@sveltejs/adapter-auto": "^1.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^2.0.3",
|
||||||
"@sveltejs/kit": "^1.0.0",
|
"@sveltejs/kit": "^1.0.0",
|
||||||
|
"@types/svelte-range-slider-pips": "^2.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
|
"fa-svelte": "^3.1.0",
|
||||||
"node-sass": "^8.0.0",
|
"node-sass": "^8.0.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"prettier-plugin-svelte": "^2.8.1",
|
"prettier-plugin-svelte": "^2.8.1",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"svelte": "^3.54.0",
|
"svelte": "^3.54.0",
|
||||||
"svelte-check": "^2.9.2",
|
"svelte-check": "^2.9.2",
|
||||||
|
"svelte-range-slider-pips": "^2.2.2",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.3",
|
||||||
"vite": "^4.0.0"
|
"vite": "^4.0.0"
|
||||||
|
|||||||
@@ -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";
|
import type UIState from "$lib/models/uistate";
|
||||||
|
|
||||||
export default async function fetchLucifer<T>(path: string, init?: RequestInit): Promise<T> {
|
export default async function fetchLucifer<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
@@ -21,3 +23,15 @@ export async function fetchUIState(): Promise<UIState> {
|
|||||||
return fetchLucifer("state");
|
return fetchLucifer("state");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchColor(color: string): Promise<Color> {
|
||||||
|
return fetchLucifer("color/"+color);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCommand(command: CommandInput): Promise<CommandInput> {
|
||||||
|
return fetchLucifer("command", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(command),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
const cache: Record<string, ColorRGB> = {};
|
||||||
|
|
||||||
|
function getColor(color: string) {
|
||||||
|
if (cache.hasOwnProperty(color)) {
|
||||||
|
return Promise.resolve(cache[color]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchColor(color).then(r => {
|
||||||
|
cache[color] = r.rgb || {red: 255, green: 255, blue: 255};
|
||||||
|
return cache[color];
|
||||||
|
}).catch(err => {
|
||||||
|
delete cache[color];
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { State } from "$lib/models/device";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
import type { ColorRGB } from '$lib/models/color';
|
||||||
|
import { fetchColor } from '$lib/client/lucifer';
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
export let value: State;
|
||||||
|
export let deletable: boolean = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
function togglePower() {
|
||||||
|
switch (value.power) {
|
||||||
|
case null: value.power = true; break;
|
||||||
|
case true: value.power = false; break;
|
||||||
|
case false: value.power = null; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleIntensity() {
|
||||||
|
if (value.intensity === null) {
|
||||||
|
value.intensity = 0.5;
|
||||||
|
} else {
|
||||||
|
value.intensity = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTemperature() {
|
||||||
|
if (value.temperature === null) {
|
||||||
|
value.temperature = 20;
|
||||||
|
} else {
|
||||||
|
value.temperature = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleColor() {
|
||||||
|
switch (value.color?.split(":")[0]) {
|
||||||
|
case undefined: value.color = "k:2750"; break;
|
||||||
|
case "k": value.color = "hs:180,0.5"; break;
|
||||||
|
case "hs": value.color = "rgb:1.000,0.800,0.066"; break;
|
||||||
|
case "rgb": value.color = "xy:0.2000,0.2000"; break;
|
||||||
|
case "xy": value.color = null; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeColorInputs(color: string | null) {
|
||||||
|
const kind = color?.split(":")[0];
|
||||||
|
const values = color?.split(":")[1]?.split(",").map(v => parseFloat(v)) || [0,0];
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
case undefined: colorKind = "null"; break;
|
||||||
|
case "k": colorKind = "k"; colorX = values[0]; colorY = values[1]||0; break;
|
||||||
|
case "hs": colorKind = "hs"; colorX = values[0]; colorY = values[1]||0; break;
|
||||||
|
case "xy": colorKind = "xy"; colorX = values[0]; colorY = values[1]||0; break;
|
||||||
|
case "rgb": colorKind = "rgb"; colorX = values[0]; colorY = values[1]||0, colorZ = values[2]||0; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
computedColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateColor(kind: string, x: number, y: number, z: number) {
|
||||||
|
x = x || 0;
|
||||||
|
y = y || 0;
|
||||||
|
z = z || 0;
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
case "k": value.color = `k:${x.toFixed(0)}`; break;
|
||||||
|
case "xy": value.color = `xy:${x.toFixed(4)},${y.toFixed(4)}`; break;
|
||||||
|
case "hs": value.color = `hs:${x.toFixed(0)},${y.toFixed(3)}`; break;
|
||||||
|
case "rgb": value.color = `rgb:${x.toFixed(3)},${y.toFixed(3)},${z.toFixed(3)}`; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
computedColor = value.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
let intensityColor = "none";
|
||||||
|
$: intensityColor = value.intensity !== null ? "off" : "none";
|
||||||
|
|
||||||
|
let temperatureColor = "none";
|
||||||
|
$: temperatureColor = value.temperature !== null ? "off" : "none";
|
||||||
|
|
||||||
|
let powerColor = "";
|
||||||
|
$: if (value.power != null) {
|
||||||
|
powerColor = value.power ? "on" : "off"
|
||||||
|
} else {
|
||||||
|
powerColor = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
let computedColor: string | null = "";
|
||||||
|
let colorButton = "none";
|
||||||
|
let colorKind = "null";
|
||||||
|
let colorX = 0;
|
||||||
|
let colorY = 0;
|
||||||
|
let colorZ = 0;
|
||||||
|
|
||||||
|
$: if (value.color !== computedColor) {
|
||||||
|
computeColorInputs(value.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: updateColor(colorKind, colorX, colorY, colorZ);
|
||||||
|
|
||||||
|
let timeout: NodeJS.Timeout | null = null;
|
||||||
|
let rgb = "";
|
||||||
|
$: if (value.color !== null) {
|
||||||
|
let before = value.color;
|
||||||
|
|
||||||
|
if (timeout !== null) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
if (value.color !== null) {
|
||||||
|
getColor(value.color).then(v => {
|
||||||
|
if (value.color === before) {
|
||||||
|
rgb = `rgb(${v.red*255}, ${v.green*255}, ${v.blue*255})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
} else {
|
||||||
|
rgb = "hsl(240, 8%, 21%)";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="assignment-state">
|
||||||
|
<div class="option {powerColor}"><Icon on:click={togglePower} block name="power" /></div>
|
||||||
|
<div class="option {intensityColor}">
|
||||||
|
<Icon on:click={toggleIntensity} block name="cirlce_notch" />
|
||||||
|
{#if value.intensity != null}
|
||||||
|
<input class="custom" type="number" min={0} max={1} step={0.01} bind:value={value.intensity} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="option {temperatureColor}">
|
||||||
|
<Icon on:click={toggleTemperature} block name="temperature_half" />
|
||||||
|
{#if value.temperature != null}
|
||||||
|
<input class="custom" type="number" min={10} max={40} step={0.5} bind:value={value.temperature} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="option {colorButton}" style="--color: {rgb}">
|
||||||
|
<Icon on:click={toggleColor} block name="palette" />
|
||||||
|
{#if colorKind !== "null"}
|
||||||
|
{#if colorKind === "k"}
|
||||||
|
<div class="color-input">
|
||||||
|
<label for="color_x">Kelvin</label>
|
||||||
|
<input class="custom" name="color_x" type="number" min={1000} max={12000} step={10} bind:value={colorX} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if colorKind === "hs"}
|
||||||
|
<div class="color-input">
|
||||||
|
<label for="color_x">Hue</label>
|
||||||
|
<input class="custom" name="color_x" type="number" bind:value={colorX} />
|
||||||
|
</div>
|
||||||
|
<div class="color-input">
|
||||||
|
<label for="color_y">Sat</label>
|
||||||
|
<input class="custom" name="color_y" type="number" min={0} max={1} step={0.01} bind:value={colorY} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if colorKind === "xy"}
|
||||||
|
<div class="color-input">
|
||||||
|
<label for="color_x">X</label>
|
||||||
|
<input class="custom" name="color_x" type="number" min={0} max={1} step={0.0001} bind:value={colorX} />
|
||||||
|
</div>
|
||||||
|
<div class="color-input">
|
||||||
|
<label for="color_y">Y</label>
|
||||||
|
<input class="custom" name="color_y" type="number" min={0} max={1} step={0.0001} bind:value={colorY} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if colorKind === "rgb"}
|
||||||
|
<div class="color-input">
|
||||||
|
<label class="short" for="color_x">Red</label>
|
||||||
|
<input class="custom short" name="color_x" type="number" min={0} max={1} step={0.001} bind:value={colorX} />
|
||||||
|
</div>
|
||||||
|
<div class="color-input">
|
||||||
|
<label class="short" for="color_y">Green</label>
|
||||||
|
<input class="custom short" name="color_y" type="number" min={0} max={1} step={0.001} bind:value={colorY} />
|
||||||
|
</div>
|
||||||
|
<div class="color-input">
|
||||||
|
<label class="short" for="color_z">Blue</label>
|
||||||
|
<input class="custom short" name="color_z" type="number" min={0} max={1} step={0.001} bind:value={colorZ} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if deletable}
|
||||||
|
<div class="option red">
|
||||||
|
<Icon on:click={() => dispatch("delete")} block name="trash" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@import "$lib/css/colors.sass"
|
||||||
|
|
||||||
|
div.assignment-state
|
||||||
|
display: flex
|
||||||
|
user-select: none
|
||||||
|
font-size: 1rem
|
||||||
|
width: 100%
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
|
@media screen and (max-width: 749px)
|
||||||
|
font-size: 0.66rem
|
||||||
|
|
||||||
|
> div.option
|
||||||
|
box-shadow: 1px 1px 1px #000
|
||||||
|
display: flex
|
||||||
|
margin: 0.25em 0.25ch
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
:global(.icon)
|
||||||
|
padding: 0.1em 0.5ch
|
||||||
|
padding-top: 0.35em
|
||||||
|
color: var(--color)
|
||||||
|
|
||||||
|
input, :global(div.rangeSlider)
|
||||||
|
width: 4rem
|
||||||
|
font-size: 0.9rem
|
||||||
|
padding-left: 0
|
||||||
|
background: $color-main1
|
||||||
|
outline: none
|
||||||
|
border: none
|
||||||
|
text-align: center
|
||||||
|
font-size: 1rem
|
||||||
|
color: $color-main9
|
||||||
|
&::-webkit-inner-spin-button
|
||||||
|
-webkit-appearance: none
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
moz-appearance: textfield
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
color: $color-main13
|
||||||
|
|
||||||
|
> div.color-input
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
> label
|
||||||
|
font-size: 0.5em
|
||||||
|
color: $color-main5
|
||||||
|
text-align: center
|
||||||
|
margin: 0
|
||||||
|
line-height: 1em
|
||||||
|
margin-top: 0.15rem !important
|
||||||
|
padding-top: 0
|
||||||
|
margin-bottom: -0.2em
|
||||||
|
width: 2.5rem
|
||||||
|
|
||||||
|
&.short
|
||||||
|
width: 2.2rem
|
||||||
|
|
||||||
|
|
||||||
|
> input
|
||||||
|
width: 2.5rem
|
||||||
|
font-size: 0.8em
|
||||||
|
background: none
|
||||||
|
|
||||||
|
&.short
|
||||||
|
width: 2.2rem
|
||||||
|
|
||||||
|
|
||||||
|
&.none
|
||||||
|
background: $color-main1
|
||||||
|
color: $color-main2
|
||||||
|
&.off
|
||||||
|
background-color: $color-main2
|
||||||
|
color: $color-main6
|
||||||
|
&.on
|
||||||
|
background-color: $color-main2
|
||||||
|
color: #00FF00
|
||||||
|
&.red
|
||||||
|
background-color: $color-main2-red
|
||||||
|
color: $color-main6-redder
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let red: boolean = false;
|
||||||
|
export let icon: boolean = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class:red class:icon class="button" on:click><slot></slot></div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@import "$lib/css/colors.sass"
|
||||||
|
|
||||||
|
div.button
|
||||||
|
box-shadow: 1px 1px 1px #000
|
||||||
|
margin: 0.25em 0.25ch
|
||||||
|
cursor: pointer
|
||||||
|
user-select: none
|
||||||
|
font-size: 1rem
|
||||||
|
background-color: $color-main2
|
||||||
|
color: $color-main6
|
||||||
|
|
||||||
|
> :global(.icon)
|
||||||
|
padding: 0.1em 0.5ch
|
||||||
|
padding-top: 0.35em
|
||||||
|
|
||||||
|
&.icon
|
||||||
|
width: 2.8ch
|
||||||
|
|
||||||
|
&.red
|
||||||
|
background-color: $color-main2-red
|
||||||
|
color: $color-main6-redder
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import Icon, { type IconName } from "./Icon.svelte";
|
||||||
|
|
||||||
|
export let tabIndex: number | undefined | null = void(0);
|
||||||
|
export let checked = false;
|
||||||
|
export let centered = false;
|
||||||
|
export let disabled = false;
|
||||||
|
export let noLabel = false;
|
||||||
|
export let noBorder = false;
|
||||||
|
export let inline = false;
|
||||||
|
export let icon: IconName = "check";
|
||||||
|
export let label = "(Missing label property)";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (!disabled) {
|
||||||
|
checked = !checked;
|
||||||
|
dispatch('check', { checked });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePress(e: KeyboardEvent) {
|
||||||
|
if (!disabled && ["enter", " ", " ", "c"].includes(e.key?.toLowerCase())) {
|
||||||
|
checked = !checked;
|
||||||
|
dispatch('check', { checked });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="checkbox"
|
||||||
|
class:centered
|
||||||
|
class:noLabel
|
||||||
|
class:inline
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={checked}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
tabindex={tabIndex}
|
||||||
|
on:keypress={handlePress}
|
||||||
|
on:click={handleClick}
|
||||||
|
>
|
||||||
|
<div class="box" class:noBorder class:disabled class:checked class:unchecked={!checked}>
|
||||||
|
<Icon name={icon} />
|
||||||
|
</div>
|
||||||
|
{#if !noLabel}
|
||||||
|
<div class="label">{label}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@import "$lib/css/colors"
|
||||||
|
|
||||||
|
div.checkbox
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
flex-shrink: 0
|
||||||
|
margin-top: 0.5em
|
||||||
|
margin-bottom: 1em
|
||||||
|
-webkit-user-select: none
|
||||||
|
-moz-user-select: none
|
||||||
|
user-select: none
|
||||||
|
transition: 250ms
|
||||||
|
padding: 0.1em
|
||||||
|
font-size: 0.9em
|
||||||
|
|
||||||
|
&.noLabel
|
||||||
|
display: block
|
||||||
|
margin-top: 0.35em
|
||||||
|
margin-bottom: 0
|
||||||
|
font-size: 0.75em
|
||||||
|
|
||||||
|
div.checkbox.centered
|
||||||
|
margin: auto
|
||||||
|
|
||||||
|
:global(div.checkbox + div.checkbox)
|
||||||
|
margin-top: -0.75em
|
||||||
|
|
||||||
|
div.checkbox.inline
|
||||||
|
margin-top: 0.25rem
|
||||||
|
margin-bottom: 0.25rem
|
||||||
|
margin-right: 1rch
|
||||||
|
|
||||||
|
div.box
|
||||||
|
cursor: pointer
|
||||||
|
border: 0.5px solid
|
||||||
|
padding: 0.05em 0.3ch
|
||||||
|
padding-top: 0.2em
|
||||||
|
line-height: 1
|
||||||
|
background-color: $color-mainhalf
|
||||||
|
color: $color-mainhalf
|
||||||
|
border-color: $color-main5
|
||||||
|
font-size: 0.9em
|
||||||
|
&.noBorder
|
||||||
|
color: $color-main2
|
||||||
|
&.checked
|
||||||
|
color: $color-main9
|
||||||
|
background-color: $color-main4
|
||||||
|
&.disabled
|
||||||
|
color: $color-main1
|
||||||
|
&.checked
|
||||||
|
color: $color-main5
|
||||||
|
background-color: $color-main1
|
||||||
|
&.noBorder
|
||||||
|
border: none
|
||||||
|
background: none
|
||||||
|
|
||||||
|
div.label
|
||||||
|
margin: auto 0
|
||||||
|
margin-left: 1ch
|
||||||
|
|
||||||
|
-webkit-user-select: none
|
||||||
|
-moz-user-select: none
|
||||||
|
user-select: none
|
||||||
|
</style>
|
||||||
@@ -12,11 +12,11 @@
|
|||||||
import hue_adore_tube from "./icons/hue_adore_tube.svg?raw";
|
import hue_adore_tube from "./icons/hue_adore_tube.svg?raw";
|
||||||
import hue_playbar from "./icons/hue_playbar.svg?raw";
|
import hue_playbar from "./icons/hue_playbar.svg?raw";
|
||||||
import hue_go from "./icons/hue_go.svg?raw";
|
import hue_go from "./icons/hue_go.svg?raw";
|
||||||
import square from "./icons/square.svg?raw";
|
import shape_square from "./icons/shape_square.svg?raw";
|
||||||
import hexagon from "./icons/hexagon.svg?raw";
|
import shape_hexagon from "./icons/shape_hexagon.svg?raw";
|
||||||
import triangle from "./icons/triangle.svg?raw";
|
import shape_triangle from "./icons/shape_triangle.svg?raw";
|
||||||
|
|
||||||
export const iconMap = Object.seal({
|
export const deviceIconMap = Object.seal({
|
||||||
generic_lamp,
|
generic_lamp,
|
||||||
generic_boob,
|
generic_boob,
|
||||||
generic_ball,
|
generic_ball,
|
||||||
@@ -29,13 +29,15 @@
|
|||||||
hue_lightbulb_gu10,
|
hue_lightbulb_gu10,
|
||||||
hue_adore_tube,
|
hue_adore_tube,
|
||||||
hue_playbar,
|
hue_playbar,
|
||||||
square,
|
shape_square,
|
||||||
hexagon,
|
shape_hexagon,
|
||||||
triangle,
|
shape_triangle,
|
||||||
hue_go,
|
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;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -44,7 +46,7 @@
|
|||||||
|
|
||||||
export let brightColor: ColorRGB;
|
export let brightColor: ColorRGB;
|
||||||
export let darkColor: ColorRGB;
|
export let darkColor: ColorRGB;
|
||||||
export let name: IconName;
|
export let name: DeviceIconName;
|
||||||
|
|
||||||
let gandalf: string;
|
let gandalf: string;
|
||||||
let voldemort: string;
|
let voldemort: string;
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
$: voldemort = rgbToHex(darkColor);
|
$: voldemort = rgbToHex(darkColor);
|
||||||
|
|
||||||
let icon: string;
|
let icon: string;
|
||||||
$: icon = iconMap[name] || iconMap.generic_lamp;
|
$: icon = deviceIconMap[name] || deviceIconMap.generic_lamp;
|
||||||
|
|
||||||
let dataUrl: string;
|
let dataUrl: string;
|
||||||
$: {
|
$: {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { rgb, type ColorRGB } from "$lib/models/color";
|
||||||
|
import DeviceIcon, { deviceIconList, type DeviceIconName } from "./DeviceIcon.svelte";
|
||||||
|
import { DARK_COLOR, DARK_COLOR_SELECTED } from "./Lamp.svelte";
|
||||||
|
|
||||||
|
export let value: DeviceIconName;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="device-icon-selector">
|
||||||
|
{#each deviceIconList as deviceIconName (deviceIconName)}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="item" class:selected={value === deviceIconName} on:click={() => { value = deviceIconName }}>
|
||||||
|
<DeviceIcon
|
||||||
|
darkColor={ value === deviceIconName ? DARK_COLOR_SELECTED : DARK_COLOR }
|
||||||
|
brightColor={ value === deviceIconName ? rgb(0.517, 0.537, 1.000) : DARK_COLOR }
|
||||||
|
name={deviceIconName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@import "$lib/css/colors.sass"
|
||||||
|
|
||||||
|
div.device-icon-selector
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
|
> div.item
|
||||||
|
font-size: 2em
|
||||||
|
padding: 0.3rem
|
||||||
|
padding-bottom: 0
|
||||||
|
padding-top: 0.5rem
|
||||||
|
box-sizing: border-box
|
||||||
|
border: 2px solid $color-mainhalf
|
||||||
|
|
||||||
|
&.selected
|
||||||
|
border: 2px solid $color-main1
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: $color-mainquarter
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let reverse: boolean = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="hsplit" class:reverse><slot></slot></div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
div.hsplit
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
flex-basis: 10
|
||||||
|
|
||||||
|
&.reverse
|
||||||
|
flex-direction: row-reverse
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px)
|
||||||
|
display: block
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let weight = 1;
|
||||||
|
export let left = false;
|
||||||
|
export let right = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="hsplit-part" class:right class:left style="flex: {weight}"><slot></slot></div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
div.hsplit-part
|
||||||
|
box-sizing: border-box
|
||||||
|
padding: 0 0.5ch
|
||||||
|
|
||||||
|
&.left
|
||||||
|
padding-left: 0
|
||||||
|
&.right
|
||||||
|
padding-right: 0
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "fa-svelte";
|
||||||
|
|
||||||
|
export let name: IconName = "question";
|
||||||
|
export let block: boolean = false;
|
||||||
|
export let marginAutio: boolean = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
{#if block}
|
||||||
|
<div on:click class:marginAutio>
|
||||||
|
<Icon class="icon" icon={icons[name] || icons.question} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Icon on:click class="icon" icon={icons[name] || icons.question} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.marginAutio {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus";
|
||||||
|
import { faPen } from "@fortawesome/free-solid-svg-icons/faPen";
|
||||||
|
import { faArchive } from "@fortawesome/free-solid-svg-icons/faArchive";
|
||||||
|
import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck";
|
||||||
|
import { faCog } from "@fortawesome/free-solid-svg-icons/faCog";
|
||||||
|
import { faLink } from "@fortawesome/free-solid-svg-icons/faLink";
|
||||||
|
import { faStar } from "@fortawesome/free-solid-svg-icons/faStar";
|
||||||
|
import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes";
|
||||||
|
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
|
||||||
|
import { faHourglass } from "@fortawesome/free-solid-svg-icons/faHourglass";
|
||||||
|
import { faCalendar } from "@fortawesome/free-solid-svg-icons/faCalendar";
|
||||||
|
import { faExpand } from "@fortawesome/free-solid-svg-icons/faExpand";
|
||||||
|
import { faSearch } from "@fortawesome/free-solid-svg-icons/faSearch";
|
||||||
|
import { faClock } from "@fortawesome/free-solid-svg-icons/faClock";
|
||||||
|
import { faThumbtack } from "@fortawesome/free-solid-svg-icons/faThumbtack";
|
||||||
|
import { faHistory } from "@fortawesome/free-solid-svg-icons/faHistory";
|
||||||
|
import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb";
|
||||||
|
import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight";
|
||||||
|
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
|
||||||
|
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||||
|
import { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot";
|
||||||
|
import { faEye } from "@fortawesome/free-solid-svg-icons/faEye";
|
||||||
|
import { faList } from "@fortawesome/free-solid-svg-icons/faList";
|
||||||
|
import { faPowerOff } from "@fortawesome/free-solid-svg-icons/faPowerOff";
|
||||||
|
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
|
||||||
|
import { faTemperatureHalf } from "@fortawesome/free-solid-svg-icons/faTemperatureHalf";
|
||||||
|
import { faPalette } from "@fortawesome/free-solid-svg-icons/faPalette";
|
||||||
|
import { faSwatchbook } from "@fortawesome/free-solid-svg-icons/faSwatchbook";
|
||||||
|
import { faCircleDot } from "@fortawesome/free-solid-svg-icons/faCircleDot";
|
||||||
|
import { faMasksTheater } from "@fortawesome/free-solid-svg-icons/faMasksTheater";
|
||||||
|
import { faTag } from "@fortawesome/free-solid-svg-icons/faTag";
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
"clock": faClock,
|
||||||
|
"thumbtack": faThumbtack,
|
||||||
|
"history": faHistory,
|
||||||
|
"question": faQuestion,
|
||||||
|
"plus": faPlus,
|
||||||
|
"pen": faPen,
|
||||||
|
"archive": faArchive,
|
||||||
|
"check": faCheck,
|
||||||
|
"cog": faCog,
|
||||||
|
"link": faLink,
|
||||||
|
"star": faStar,
|
||||||
|
"times": faTimes,
|
||||||
|
"lightbulb": faLightbulb,
|
||||||
|
"spinner": faSpinner,
|
||||||
|
"hourglass": faHourglass,
|
||||||
|
"calendar": faCalendar,
|
||||||
|
"expand": faExpand,
|
||||||
|
"search": faSearch,
|
||||||
|
"chevron_right": faChevronRight,
|
||||||
|
"chevron_down": faChevronDown,
|
||||||
|
"trash": faTrash,
|
||||||
|
"check_slot": faCheckToSlot,
|
||||||
|
"eye": faEye,
|
||||||
|
"list": faList,
|
||||||
|
"power": faPowerOff,
|
||||||
|
"cirlce_notch": faCircleNotch,
|
||||||
|
"temperature_half": faTemperatureHalf,
|
||||||
|
"palette": faPalette,
|
||||||
|
"swatch_book": faSwatchbook,
|
||||||
|
"circle_dot": faCircleDot,
|
||||||
|
"masks_theater": faMasksTheater,
|
||||||
|
"tag": faTag,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IconName = keyof typeof icons;
|
||||||
|
|
||||||
|
export const iconNames = Object.keys(icons).sort() as IconName[];
|
||||||
|
export const DEFAULT_ICON: IconName = "question";
|
||||||
|
</script>
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import { SupportFlags } from "$lib/models/device";
|
import { SupportFlags } from "$lib/models/device";
|
||||||
import { rgb, type ColorRGB } from "../models/color";
|
import { rgb, type ColorRGB } from "../models/color";
|
||||||
import DeviceIcon from "./DeviceIcon.svelte";
|
import DeviceIcon from "./DeviceIcon.svelte";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
|
||||||
export let device: Device;
|
export let device: Device;
|
||||||
export let compact: boolean = false;
|
export let compact: boolean = false;
|
||||||
@@ -25,6 +26,9 @@
|
|||||||
let barColor: ColorRGB | null;
|
let barColor: ColorRGB | null;
|
||||||
let barFraction: number | null;
|
let barFraction: number | null;
|
||||||
let roundboiText: string | null;
|
let roundboiText: string | null;
|
||||||
|
let roles: string[];
|
||||||
|
let tags: string[];
|
||||||
|
let hasRoleOrTag: boolean;
|
||||||
$: {
|
$: {
|
||||||
// TODO: Fix device.name on the backend
|
// TODO: Fix device.name on the backend
|
||||||
const nameAlias = device.aliases?.find(a => a.startsWith("lucifer:name:"));
|
const nameAlias = device.aliases?.find(a => a.startsWith("lucifer:name:"));
|
||||||
@@ -34,6 +38,10 @@
|
|||||||
deviceTitle = "";
|
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;
|
barFraction = null;
|
||||||
barColor = null;
|
barColor = null;
|
||||||
iconColor = null;
|
iconColor = null;
|
||||||
@@ -114,7 +122,25 @@
|
|||||||
</DeviceIcon>
|
</DeviceIcon>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="title">{displayTitle}</div>
|
<div class="title">
|
||||||
|
<div class="name" class:hasRoleOrTag>{displayTitle}</div>
|
||||||
|
{#if hasRoleOrTag}
|
||||||
|
<div class="tag-list">
|
||||||
|
{#each roles as role}
|
||||||
|
<div class="tag">
|
||||||
|
<Icon block name="masks_theater" />
|
||||||
|
<div class="tag-name">{role}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#each tags as tag}
|
||||||
|
<div class="tag">
|
||||||
|
<Icon block name="tag" />
|
||||||
|
<div class="tag-name">{tag}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flatboi">
|
<div class="flatboi">
|
||||||
{#if barColor != null && barFraction != null}
|
{#if barColor != null && barFraction != null}
|
||||||
@@ -183,6 +209,23 @@
|
|||||||
margin-right: 1ch
|
margin-right: 1ch
|
||||||
height: 1.6em
|
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
|
> div.flatboi
|
||||||
height: 0.2em
|
height: 0.2em
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import { getModalContext, type ModalSelection } from "$lib/contexts/ModalContext.svelte";
|
import { getModalContext, type ModalSelection } from "$lib/contexts/ModalContext.svelte";
|
||||||
|
|
||||||
export let show: boolean = false;
|
export let show: boolean = false;
|
||||||
export let verb: string = "Submit";
|
export let titleText: string = "Edit Form";
|
||||||
export let noun: string = "Form";
|
export let submitText: string = "Submit";
|
||||||
export let cancelLabel: string = "Cancel";
|
export let cancelLabel: string = "Cancel";
|
||||||
export let wide: boolean = false;
|
export let wide: boolean = false;
|
||||||
export let error: string | null = null;
|
export let error: string | null = null;
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<div role="dialog" class="modal-background" on:keypress={onKeyPress}>
|
<div role="dialog" class="modal-background" on:keypress={onKeyPress}>
|
||||||
<div class="modal" class:wide class:nobody>
|
<div class="modal" class:wide class:nobody>
|
||||||
<div class="header" class:nobody>
|
<div class="header" class:nobody>
|
||||||
<div class="title" class:noclose={!closable}>{verb} {noun}</div>
|
<div class="title" class:noclose={!closable}>{titleText}</div>
|
||||||
{#if (closable)}
|
{#if (closable)}
|
||||||
<div class="x">
|
<div class="x">
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row" class:nobody>
|
<div class="button-row" class:nobody>
|
||||||
<button disabled={disabled} type="submit">{verb} {noun}</button>
|
<button disabled={disabled} type="submit">{submitText}</button>
|
||||||
<slot name="secondary-button-1"></slot>
|
<slot name="secondary-button-1"></slot>
|
||||||
<slot name="secondary-button-2"></slot>
|
<slot name="secondary-button-2"></slot>
|
||||||
<button disabled={!closable} on:click|preventDefault={onClose}>{cancelLabel}</button>
|
<button disabled={!closable} on:click|preventDefault={onClose}>{cancelLabel}</button>
|
||||||
@@ -84,6 +84,10 @@
|
|||||||
&.nobody {
|
&.nobody {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.fullheight {
|
||||||
|
min-height: 100%
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div.modal.wide {
|
div.modal.wide {
|
||||||
max-width: 80ch;
|
max-width: 80ch;
|
||||||
@@ -182,7 +186,7 @@
|
|||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.modal :global(input), div.modal :global(select), div.modal :global(textarea) {
|
div.modal :global(input:not(.custom)), div.modal :global(select), div.modal :global(textarea) {
|
||||||
width: calc(100% - 2ch);
|
width: calc(100% - 2ch);
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
@@ -206,7 +210,7 @@
|
|||||||
padding: 0.5em 0;
|
padding: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.modal :global(input)::placeholder {
|
div.modal :global(input:not(.custom))::placeholder {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
div.modal :global(select:disabled) {
|
div.modal :global(select:disabled) {
|
||||||
@@ -214,7 +218,7 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: #789;
|
color: #789;
|
||||||
}
|
}
|
||||||
div.modal :global(input:disabled) {
|
div.modal :global(input:not(.custom):disabled) {
|
||||||
background: #1a1c1f;
|
background: #1a1c1f;
|
||||||
color: #789;
|
color: #789;
|
||||||
}
|
}
|
||||||
@@ -229,22 +233,22 @@
|
|||||||
color: #aaa;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.modal :global(input.nolast) {
|
div.modal :global(input:not(.custom).nolast) {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.modal :global(input[type="checkbox"]) {
|
div.modal :global(input:not(.custom)[type="checkbox"]) {
|
||||||
width: initial;
|
width: initial;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
div.modal :global(input[type="checkbox"] + label) {
|
div.modal :global(input:not(.custom)[type="checkbox"] + label) {
|
||||||
width: initial;
|
width: initial;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.modal :global(input:focus), div.modal :global(select:focus), div.modal :global(textarea:focus) {
|
div.modal :global(input:not(.custom):focus), div.modal :global(select:focus), div.modal :global(textarea:focus) {
|
||||||
background: #121418;
|
background: #121418;
|
||||||
color: $color-main9;
|
color: $color-main9;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -256,13 +260,13 @@
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.modal :global(input::-webkit-outer-spin-button),
|
div.modal :global(input:not(.custom)::-webkit-outer-spin-button),
|
||||||
div.modal :global(input::-webkit-inner-spin-button) {
|
div.modal :global(input:not(.custom)::-webkit-inner-spin-button) {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.modal :global(input[type=number]) {
|
div.modal :global(input:not(.custom)[type=number]) {
|
||||||
appearance: textfield;
|
appearance: textfield;
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="modal-body"><slot></slot></div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
div.modal-body
|
||||||
|
padding: 1em
|
||||||
|
padding-left: 0
|
||||||
|
flex: 1
|
||||||
|
|
||||||
|
@media screen and (max-width: 749px)
|
||||||
|
padding-left: 1em
|
||||||
|
padding: 0 1em
|
||||||
|
|
||||||
|
&:first-of-type
|
||||||
|
padding-left: 1em
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
|
||||||
|
export let expanded: boolean;
|
||||||
|
export let title: string;
|
||||||
|
|
||||||
|
function toggleExpand() {
|
||||||
|
expanded = !expanded;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="modal-section" class:expanded>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="modal-section-header" on:click={toggleExpand}>
|
||||||
|
<div class="title">{title}</div>
|
||||||
|
<Icon block name="{expanded ? "chevron_down" : "chevron_right"}" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-section-body">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@import "$lib/css/colors.sass"
|
||||||
|
|
||||||
|
div.modal-section
|
||||||
|
margin-left: -1.8ch
|
||||||
|
margin-right: -1.8ch
|
||||||
|
margin-bottom: 0.5em
|
||||||
|
|
||||||
|
> div.modal-section-body
|
||||||
|
display: none
|
||||||
|
padding: 0.25em 2.3ch
|
||||||
|
|
||||||
|
> div.modal-section-header
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
user-select: none
|
||||||
|
padding: 0.5ch 1.6ch
|
||||||
|
padding-right: 1.8ch
|
||||||
|
color: $color-main4
|
||||||
|
|
||||||
|
> div.title
|
||||||
|
margin-left: 0.5ch
|
||||||
|
font-size: 0.9em
|
||||||
|
color: $color-main5
|
||||||
|
margin-right: auto
|
||||||
|
|
||||||
|
&.expanded
|
||||||
|
background-color: $color-mainhalf
|
||||||
|
|
||||||
|
> div.modal-section-body
|
||||||
|
display: block
|
||||||
|
|
||||||
|
> div.modal-section-header
|
||||||
|
color: $color-main5
|
||||||
|
> div.title
|
||||||
|
color: $color-main9
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
|
||||||
|
export let value: string[];
|
||||||
|
export let exclaimMode = false;
|
||||||
|
|
||||||
|
let nextTag = "";
|
||||||
|
|
||||||
|
function onKey(ev: KeyboardEvent) {
|
||||||
|
if ((ev.metaKey||ev.ctrlKey) && nextTag === "" && ev.key === "Backspace") {
|
||||||
|
value = value.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ev.metaKey||ev.ctrlKey) && nextTag !== "" && ev.key === "Enter") {
|
||||||
|
value = [...value, nextTag];
|
||||||
|
nextTag = "";
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: while (nextTag.includes(",")) {
|
||||||
|
const newTags = nextTag.split(",").map(t => t.trim());
|
||||||
|
value = [...value, ...newTags.slice(0, -1)];
|
||||||
|
nextTag = newTags[newTags.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$: value = (value||[]).filter((e, i) => !value.slice(0, i).includes(e));
|
||||||
|
|
||||||
|
$: if (exclaimMode) {
|
||||||
|
value = (value||[]).filter((e, i) => (
|
||||||
|
e.startsWith("!")
|
||||||
|
? !value.slice(i+1).includes(e.slice(1))
|
||||||
|
: !value.slice(i+1).includes("!"+e)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="tag-input">
|
||||||
|
{#each value as tag (tag)}
|
||||||
|
<div class="tag">
|
||||||
|
<span>{tag}</span>
|
||||||
|
<span class="comma">,</span>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<span class="x" on:click={() => value = value.filter(v => v !== tag)}>
|
||||||
|
<Icon name="times" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<input placeholder="(type , or press Ctrl+Enter to add)" on:keyup={onKey} bind:value={nextTag} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@import "$lib/css/colors.sass"
|
||||||
|
|
||||||
|
div.tag-input
|
||||||
|
width: calc(100% - 2ch)
|
||||||
|
margin-bottom: 1em
|
||||||
|
margin-top: 0.20em
|
||||||
|
min-height: 2em
|
||||||
|
|
||||||
|
background: $color-mainhalf
|
||||||
|
color: $color-main8
|
||||||
|
border: none
|
||||||
|
outline: none
|
||||||
|
resize: vertical
|
||||||
|
padding: 0.5em 1ch
|
||||||
|
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
|
> div.tag
|
||||||
|
margin: 0.25em 0.5ch
|
||||||
|
background-color: $color-main1
|
||||||
|
padding: 0.25em 1ch
|
||||||
|
border-radius: 0.25em
|
||||||
|
|
||||||
|
span.x
|
||||||
|
font-size: 0.75em
|
||||||
|
line-height: 1em
|
||||||
|
user-select: none
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: $color-main12
|
||||||
|
|
||||||
|
span.comma
|
||||||
|
font-size: 0
|
||||||
|
|
||||||
|
> input
|
||||||
|
margin-bottom: 0.125em
|
||||||
|
</style>
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import type { DeviceEditOp } from "$lib/models/device";
|
import type { DeviceEditOp } from "$lib/models/device";
|
||||||
import type Device from "$lib/models/device";
|
|
||||||
import type Script from "$lib/models/script";
|
import type Script from "$lib/models/script";
|
||||||
import type { ScriptLine } from "$lib/models/script";
|
import type { ScriptLine } from "$lib/models/script";
|
||||||
import { getContext, setContext } from "svelte";
|
import { getContext, setContext } from "svelte";
|
||||||
@@ -10,7 +9,7 @@
|
|||||||
|
|
||||||
export type ModalSelection =
|
export type ModalSelection =
|
||||||
| { kind: "closed" }
|
| { kind: "closed" }
|
||||||
| { kind: "device.edit", images: Device[], op: DeviceEditOp }
|
| { kind: "device.edit", op: DeviceEditOp }
|
||||||
| { kind: "script.edit", id: string | null, lines: ScriptLine[] }
|
| { kind: "script.edit", id: string | null, lines: ScriptLine[] }
|
||||||
| { kind: "script.execute", script: Script }
|
| { kind: "script.execute", script: Script }
|
||||||
|
|
||||||
|
|||||||
@@ -61,10 +61,6 @@
|
|||||||
const firstDevice = $deviceList.find(d => $selectedMap[d.id]);
|
const firstDevice = $deviceList.find(d => $selectedMap[d.id]);
|
||||||
const nextMasks: string[] = [];
|
const nextMasks: string[] = [];
|
||||||
|
|
||||||
if (!$deviceList.find(d => !$selectedMap[d.id])) {
|
|
||||||
nextMasks.push("*");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstDevice != null) {
|
if (firstDevice != null) {
|
||||||
// Common aliases first
|
// Common aliases first
|
||||||
for (const alias of firstDevice.aliases) {
|
for (const alias of firstDevice.aliases) {
|
||||||
@@ -78,10 +74,6 @@
|
|||||||
const isSelected = $selectedMap[device.id] || false;
|
const isSelected = $selectedMap[device.id] || false;
|
||||||
const hasAlias = device.aliases.includes(alias);
|
const hasAlias = device.aliases.includes(alias);
|
||||||
|
|
||||||
if (alias === "lucifer:group:Squares") {
|
|
||||||
console.log(device.name, isSelected, hasAlias);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSelected !== hasAlias) {
|
if (isSelected !== hasAlias) {
|
||||||
qualified = false;
|
qualified = false;
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import { fetchUIState } from "$lib/client/lucifer";
|
import { fetchUIState } from "$lib/client/lucifer";
|
||||||
import type { IconName } from "$lib/components/DeviceIcon.svelte";
|
import type { DeviceIconName } from "$lib/components/DeviceIcon.svelte";
|
||||||
|
import type Assignment from "$lib/models/assignment";
|
||||||
import type Device from "$lib/models/device";
|
import type Device from "$lib/models/device";
|
||||||
import type { UIStatePatch } from "$lib/models/uistate";
|
import type { UIStatePatch } from "$lib/models/uistate";
|
||||||
import type UIState from "$lib/models/uistate";
|
import type UIState from "$lib/models/uistate";
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
state: Readable<UIState>
|
state: Readable<UIState>
|
||||||
error: Readable<string | null>
|
error: Readable<string | null>
|
||||||
deviceList: Readable<Device[]>
|
deviceList: Readable<Device[]>
|
||||||
|
assignmentList: Readable<Assignment[]>
|
||||||
roomList: Readable<{name: string, groups: {name: string, devices: Device[]}[], devices: Device[]}[]>
|
roomList: Readable<{name: string, groups: {name: string, devices: Device[]}[], devices: Device[]}[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +44,17 @@
|
|||||||
.sort((a,b) => a.name.localeCompare(b.name));
|
.sort((a,b) => a.name.localeCompare(b.name));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const assignmentList = derived(state, state => {
|
||||||
|
if (state == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(state.assignments)
|
||||||
|
.map(k => state.assignments[k])
|
||||||
|
.sort((a,b) => a.id.localeCompare(b.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const roomList = derived(state, state => {
|
const roomList = derived(state, state => {
|
||||||
const roomMap: Record<string, Device[]> = {};
|
const roomMap: Record<string, Device[]> = {};
|
||||||
const groupMap: Record<string, string> = {};
|
const groupMap: Record<string, string> = {};
|
||||||
@@ -129,7 +142,7 @@
|
|||||||
patch.name = patch.addAlias.slice("lucifer:name:".length)
|
patch.name = patch.addAlias.slice("lucifer:name:".length)
|
||||||
}
|
}
|
||||||
if (patch.addAlias.startsWith("lucifer:icon:")) {
|
if (patch.addAlias.startsWith("lucifer:icon:")) {
|
||||||
patch.icon = patch.addAlias.slice("lucifer:icon:".length) as IconName
|
patch.icon = patch.addAlias.slice("lucifer:icon:".length) as DeviceIconName
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exclExisting) {
|
if (exclExisting) {
|
||||||
@@ -170,6 +183,28 @@
|
|||||||
const assignments = {...s.assignments};
|
const assignments = {...s.assignments};
|
||||||
delete assignments[patch.assignment.id];
|
delete assignments[patch.assignment.id];
|
||||||
s = {...s, assignments};
|
s = {...s, assignments};
|
||||||
|
} else if (patch.assignment.addDeviceId) {
|
||||||
|
s = {
|
||||||
|
...s,
|
||||||
|
assignments: {
|
||||||
|
...s.assignments,
|
||||||
|
[patch.assignment.id]: {
|
||||||
|
...s.assignments[patch.assignment.id],
|
||||||
|
deviceIds: [...s.assignments[patch.assignment.id].deviceIds || [], patch.assignment.addDeviceId],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (patch.assignment.removeDeviceId) {
|
||||||
|
s = {
|
||||||
|
...s,
|
||||||
|
assignments: {
|
||||||
|
...s.assignments,
|
||||||
|
[patch.assignment.id]: {
|
||||||
|
...s.assignments[patch.assignment.id],
|
||||||
|
deviceIds: s.assignments[patch.assignment.id].deviceIds.map(id => id === patch.assignment.removeDeviceId ? "" : id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
s = {
|
s = {
|
||||||
...s,
|
...s,
|
||||||
@@ -257,6 +292,7 @@
|
|||||||
error: { subscribe: error.subscribe },
|
error: { subscribe: error.subscribe },
|
||||||
state: { subscribe: state.subscribe },
|
state: { subscribe: state.subscribe },
|
||||||
deviceList,
|
deviceList,
|
||||||
|
assignmentList,
|
||||||
roomList,
|
roomList,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
$color-maindark: hsl(240, 8%, 3.5%)
|
$color-maindark: hsl(240, 8%, 3.5%)
|
||||||
$color-main0: hsl(240, 8%, 7%)
|
$color-main0: hsl(240, 8%, 7%)
|
||||||
$color-main0-transparent: hsla(240, 8%, 10%, 0.7)
|
$color-main0-transparent: hsla(240, 8%, 10%, 0.7)
|
||||||
|
$color-mainquarter: hsl(240, 8%, 8.75%)
|
||||||
$color-mainhalf: hsl(240, 8%, 10.5%)
|
$color-mainhalf: hsl(240, 8%, 10.5%)
|
||||||
$color-main1: hsl(240, 8%, 14%)
|
$color-main1: hsl(240, 8%, 14%)
|
||||||
$color-main1-transparent: hsla(240, 8%, 17%, 0.7)
|
$color-main1-transparent: hsla(240, 8%, 17%, 0.7)
|
||||||
$color-main2: hsl(240, 8%, 21%)
|
$color-main2: hsl(240, 8%, 21%)
|
||||||
$color-main2-transparent: hsla(240, 8%, 24%, 0.7)
|
$color-main2-transparent: hsla(240, 8%, 24%, 0.7)
|
||||||
|
$color-main2-red: hsl(0, 16%, 21%)
|
||||||
$color-main3: hsl(240, 8%, 28%)
|
$color-main3: hsl(240, 8%, 28%)
|
||||||
$color-main4: hsl(240, 8%, 35%)
|
$color-main4: hsl(240, 8%, 35%)
|
||||||
$color-main5: hsl(240, 8%, 42%)
|
$color-main5: hsl(240, 8%, 42%)
|
||||||
$color-main6: hsl(240, 8%, 49%)
|
$color-main6: hsl(240, 8%, 49%)
|
||||||
|
$color-main6-red: hsl(0, 16%, 49%)
|
||||||
|
$color-main6-redder: hsl(0, 64%, 49%)
|
||||||
$color-main7: hsl(240, 8%, 56%) // Default
|
$color-main7: hsl(240, 8%, 56%) // Default
|
||||||
$color-main8: hsl(240, 8%, 63%)
|
$color-main8: hsl(240, 8%, 63%)
|
||||||
$color-main9: hsl(240, 8%, 70%)
|
$color-main9: hsl(240, 8%, 70%)
|
||||||
|
|||||||
@@ -0,0 +1,345 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { runCommand } from "$lib/client/lucifer";
|
||||||
|
import AssignmentState from "$lib/components/AssignmentState.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";
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
console.log("SOOBMIEET")
|
||||||
|
|
||||||
|
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={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>
|
||||||
|
<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 }
|
||||||
|
<AssignmentState deletable bind:value={state} on:delete={() => removeEffectState(i)} />
|
||||||
|
{/each}
|
||||||
|
<Button on:click={addEffectState} icon><Icon name="plus" /></Button>
|
||||||
|
</HSplitPart>
|
||||||
|
</HSplit>
|
||||||
|
</ModalSection>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
</form>
|
||||||
@@ -7,9 +7,63 @@ export default interface Assignment {
|
|||||||
variables: Record<string, number>
|
variables: Record<string, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssignmentInput {
|
||||||
|
match: string
|
||||||
|
effect: Effect
|
||||||
|
}
|
||||||
|
|
||||||
export type Effect =
|
export type Effect =
|
||||||
| { manual: State }
|
| { manual: State }
|
||||||
|
| { solid: { states: State[], animationMs: number, interleave: number } }
|
||||||
| { gradient: { states: State[], animationMs: number, reverse: boolean, interpolate: boolean } }
|
| { gradient: { states: State[], animationMs: number, reverse: boolean, interpolate: boolean } }
|
||||||
| { pattern: { states: State[], animationMs: number } }
|
| { pattern: { states: State[], animationMs: number } }
|
||||||
| { random: { states: State[], animationMs: number, interpolate: boolean } }
|
| { random: { states: State[], animationMs: number, interpolate: boolean } }
|
||||||
| { vrange: { states: State[], variable: string, min: number, max: number } }
|
| { vrange: { states: State[], variable: string, min: number, max: number } }
|
||||||
|
|
||||||
|
export function toEffectRaw(effect?: Effect): EffectRaw {
|
||||||
|
if (effect != null) {
|
||||||
|
if ("manual" in effect) {
|
||||||
|
return { kind: "manual", states: [effect.manual], animationMs: 0, reverse: false, interpolate: false, min: 0, max: 0, variable: "", interleave: 0 }
|
||||||
|
}
|
||||||
|
if ("solid" in effect) {
|
||||||
|
return { kind: "solid", ...effect.solid, min: 0, max: 0, variable: "", interpolate: false, reverse: false}
|
||||||
|
}
|
||||||
|
if ("gradient" in effect) {
|
||||||
|
return { kind: "gradient", ...effect.gradient, min: 0, max: 0, variable: "", interleave: 0 }
|
||||||
|
}
|
||||||
|
if ("pattern" in effect) {
|
||||||
|
return { kind: "pattern", ...effect.pattern, reverse: false, interpolate: false, min: 0, max: 0, variable: "", interleave: 0 }
|
||||||
|
}
|
||||||
|
if ("random" in effect) {
|
||||||
|
return { kind: "random", ...effect.random, reverse: false, min: 0, max: 0, variable: "", interleave: 0 }
|
||||||
|
}
|
||||||
|
if ("vrange" in effect) {
|
||||||
|
return { kind: "vrange", ...effect.vrange, animationMs: 0, interpolate: false, reverse: false, interleave: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kind: "manual", states: [{color: null, intensity: null, power: null, temperature: null}], animationMs: 0, reverse: false, interpolate: false, min: 0, max: 0, variable: "", interleave: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromEffectRaw(raw: EffectRaw): Effect {
|
||||||
|
switch (raw.kind) {
|
||||||
|
case "manual": return { manual: raw.states[0] };
|
||||||
|
case "solid": return { solid: {...raw} };
|
||||||
|
case "gradient": return { gradient: {...raw} };
|
||||||
|
case "pattern": return { pattern: {...raw} };
|
||||||
|
case "random": return { random: {...raw} };
|
||||||
|
case "vrange": return { vrange: {...raw} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EffectRaw {
|
||||||
|
kind: "manual" | "solid" | "gradient" | "pattern" | "random" | "vrange"
|
||||||
|
states: State[]
|
||||||
|
animationMs: number
|
||||||
|
reverse: boolean
|
||||||
|
interpolate: boolean
|
||||||
|
interleave: number
|
||||||
|
variable: string
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ export interface ColorRGB {
|
|||||||
blue: number
|
blue: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ColorXY {
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColorHS {
|
||||||
|
hue: number,
|
||||||
|
sat: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Color {
|
||||||
|
rgb?: ColorRGB
|
||||||
|
xy?: ColorXY
|
||||||
|
hs?: ColorHS
|
||||||
|
k?: number
|
||||||
|
}
|
||||||
|
|
||||||
export enum ColorFlags {
|
export enum ColorFlags {
|
||||||
CFlagXY = 1 << 0,
|
CFlagXY = 1 << 0,
|
||||||
CFlagRGB = 1 << 1,
|
CFlagRGB = 1 << 1,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { AssignmentInput } from "./assignment"
|
||||||
|
|
||||||
|
export default interface CommandInput {
|
||||||
|
addAlias?: AddAliasCommand
|
||||||
|
removeAlias?: RemoveAliasComamnd
|
||||||
|
assign?: AssignmentInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddAliasCommand {
|
||||||
|
match: string
|
||||||
|
alias: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoveAliasComamnd {
|
||||||
|
match: string
|
||||||
|
alias: string
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { IconName } from "$lib/components/DeviceIcon.svelte"
|
import type { DeviceIconName } from "$lib/components/DeviceIcon.svelte"
|
||||||
import type { ColorFlags, ColorRGB } from "./color"
|
import type { ColorFlags, ColorRGB } from "./color"
|
||||||
|
|
||||||
export default interface Device {
|
export default interface Device {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
icon: IconName
|
icon: DeviceIconName
|
||||||
hwMetadata: HardwareMetadata | null
|
hwMetadata: HardwareMetadata | null
|
||||||
hwState: HardwareState | null
|
hwState: HardwareState | null
|
||||||
desiredColorRgb: ColorRGB | null
|
desiredColorRgb: ColorRGB | null
|
||||||
@@ -33,7 +33,7 @@ export interface HardwareState {
|
|||||||
|
|
||||||
export interface HardwareMetadata {
|
export interface HardwareMetadata {
|
||||||
firmwareVersion?: string
|
firmwareVersion?: string
|
||||||
icon?: IconName
|
icon?: DeviceIconName
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
@@ -62,4 +62,6 @@ export const BLANK_STATE: State = (Object.seal||(v=>v))({
|
|||||||
color: null,
|
color: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type DeviceEditOp = "assign" | "rename" | "change_icon" | "move_group"
|
export type DeviceEditOp = "none" | "assign" | "rename" | "change_icon" | "move_group" | "move_room" | "change_tags" | "change_roles";
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ export default interface UIState {
|
|||||||
|
|
||||||
export interface UIStatePatch {
|
export interface UIStatePatch {
|
||||||
device: Partial<Device> & { id: string, delete?: boolean, addAlias?: string, removeAlias?: string }
|
device: Partial<Device> & { id: string, delete?: boolean, addAlias?: string, removeAlias?: string }
|
||||||
assignment: Partial<Assignment> & { id: string, delete?: boolean }
|
assignment: Partial<Assignment> & { id: string, delete?: boolean, addDeviceId?: string, removeDeviceId?: string }
|
||||||
script: Partial<Script> & { id: string, delete?: boolean }
|
script: Partial<Script> & { id: string, delete?: boolean }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const prerender = true;
|
||||||
@@ -1,17 +1,49 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import AssignmentState from "$lib/components/AssignmentState.svelte";
|
||||||
import Lamp from "$lib/components/Lamp.svelte";
|
import Lamp from "$lib/components/Lamp.svelte";
|
||||||
import MetaLamp from "$lib/components/MetaLamp.svelte";
|
import MetaLamp from "$lib/components/MetaLamp.svelte";
|
||||||
import RoomHeader from "$lib/components/RoomHeader.svelte";
|
import RoomHeader from "$lib/components/RoomHeader.svelte";
|
||||||
|
import { getModalContext } from "$lib/contexts/ModalContext.svelte";
|
||||||
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
|
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
|
||||||
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
||||||
|
import DeviceModal from "$lib/modals/DeviceModal.svelte";
|
||||||
|
|
||||||
const {roomList} = getStateContext();
|
const {roomList} = getStateContext();
|
||||||
const {selectedMasks} = getSelectedContext();
|
const {modal} = getModalContext();
|
||||||
|
|
||||||
|
function handleKeyPress(e: KeyboardEvent) {
|
||||||
|
if ($modal.kind === "closed" && e.shiftKey) {
|
||||||
|
switch (e.key.toLocaleLowerCase()) {
|
||||||
|
case 'a':
|
||||||
|
modal.set({kind: "device.edit", op: "assign"});
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
modal.set({kind: "device.edit", op: "rename"});
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
case 'i':
|
||||||
|
modal.set({kind: "device.edit", op: "change_icon"});
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
case 'e':
|
||||||
|
modal.set({kind: "device.edit", op: "none"});
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:body on:keypress={handleKeyPress} />
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
{#each $roomList as room (room.name)}
|
{#each $roomList as room (room.name)}
|
||||||
<RoomHeader devices={room.devices} name={room.name} />
|
<div class="room">
|
||||||
|
<RoomHeader
|
||||||
|
devices={[...room.devices, ...room.groups.flatMap(g => g.devices)]}
|
||||||
|
name={room.name}
|
||||||
|
/>
|
||||||
<div class="devices">
|
<div class="devices">
|
||||||
{#each room.devices as device (device.id) }
|
{#each room.devices as device (device.id) }
|
||||||
<Lamp device={device} />
|
<Lamp device={device} />
|
||||||
@@ -22,9 +54,11 @@
|
|||||||
<MetaLamp name={group.name} devices={group.devices} />
|
<MetaLamp name={group.name} devices={group.devices} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<pre>{JSON.stringify($selectedMasks, null, 4)}</pre>
|
|
||||||
|
<DeviceModal />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div.page {
|
div.page {
|
||||||
@@ -35,6 +69,10 @@
|
|||||||
font-size: 1.33rem;
|
font-size: 1.33rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.room {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
div.devices {
|
div.devices {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from '@sveltejs/adapter-static';
|
||||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func NewService(resolver device.Resolver) lucifer3.ActiveService {
|
|||||||
|
|
||||||
supportFlags: make(map[string]device.SupportFlags),
|
supportFlags: make(map[string]device.SupportFlags),
|
||||||
colorFlags: make(map[string]device.ColorFlags),
|
colorFlags: make(map[string]device.ColorFlags),
|
||||||
|
ctRanges: make(map[string]*[2]int),
|
||||||
temperatures: make(map[string]float64),
|
temperatures: make(map[string]float64),
|
||||||
motions: make(map[string]float64),
|
motions: make(map[string]float64),
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ type effectEnforcer struct {
|
|||||||
|
|
||||||
list []*effectEnforcerRun
|
list []*effectEnforcerRun
|
||||||
index map[string]*effectEnforcerRun
|
index map[string]*effectEnforcerRun
|
||||||
|
ctRanges map[string]*[2]int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *effectEnforcer) Active() bool {
|
func (s *effectEnforcer) Active() bool {
|
||||||
@@ -53,6 +55,7 @@ func (s *effectEnforcer) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Even
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
colorFlags := s.colorFlags
|
colorFlags := s.colorFlags
|
||||||
supportFlags := s.supportFlags
|
supportFlags := s.supportFlags
|
||||||
|
ctRanges := s.ctRanges
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
colorFlags = gentools.CopyMap(colorFlags)
|
colorFlags = gentools.CopyMap(colorFlags)
|
||||||
@@ -60,9 +63,15 @@ func (s *effectEnforcer) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Even
|
|||||||
colorFlags[event.ID] = event.ColorFlags
|
colorFlags[event.ID] = event.ColorFlags
|
||||||
supportFlags[event.ID] = event.SupportFlags
|
supportFlags[event.ID] = event.SupportFlags
|
||||||
|
|
||||||
|
if event.ColorKelvinRange != nil {
|
||||||
|
ctRanges = gentools.CopyMap(ctRanges)
|
||||||
|
ctRanges[event.ID] = event.ColorKelvinRange
|
||||||
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.colorFlags = colorFlags
|
s.colorFlags = colorFlags
|
||||||
s.supportFlags = supportFlags
|
s.supportFlags = supportFlags
|
||||||
|
s.ctRanges = ctRanges
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
case events.DeviceReady:
|
case events.DeviceReady:
|
||||||
@@ -237,6 +246,7 @@ func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
colorFlags := s.colorFlags
|
colorFlags := s.colorFlags
|
||||||
supportFlags := s.supportFlags
|
supportFlags := s.supportFlags
|
||||||
|
ctRanges := s.ctRanges
|
||||||
|
|
||||||
for i, run := range s.list {
|
for i, run := range s.list {
|
||||||
if run.dead {
|
if run.dead {
|
||||||
@@ -307,11 +317,18 @@ func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
|
|||||||
state.Color = nil
|
state.Color = nil
|
||||||
} else if state.Color != nil {
|
} else if state.Color != nil {
|
||||||
cf := colorFlags[id]
|
cf := colorFlags[id]
|
||||||
|
|
||||||
invalid := (state.Color.K != nil && !cf.HasAll(device.CFlagKelvin)) ||
|
invalid := (state.Color.K != nil && !cf.HasAll(device.CFlagKelvin)) ||
|
||||||
(state.Color.XY != nil && !cf.HasAll(device.CFlagXY)) ||
|
(state.Color.XY != nil && !cf.HasAll(device.CFlagXY)) ||
|
||||||
(state.Color.RGB != nil && !cf.HasAll(device.CFlagRGB)) ||
|
(state.Color.RGB != nil && !cf.HasAll(device.CFlagRGB)) ||
|
||||||
(state.Color.HS != nil && !cf.HasAll(device.CFlagHS))
|
(state.Color.HS != nil && !cf.HasAll(device.CFlagHS))
|
||||||
|
|
||||||
|
if state.Color.K != nil && cf.IsColor() && ctRanges[id] != nil {
|
||||||
|
if *state.Color.K < ctRanges[id][0] || *state.Color.K >= ctRanges[id][1] {
|
||||||
|
invalid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if invalid {
|
if invalid {
|
||||||
var converted color.Color
|
var converted color.Color
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"git.aiterp.net/lucifer3/server/commands"
|
"git.aiterp.net/lucifer3/server/commands"
|
||||||
"git.aiterp.net/lucifer3/server/effects"
|
"git.aiterp.net/lucifer3/server/effects"
|
||||||
"git.aiterp.net/lucifer3/server/events"
|
"git.aiterp.net/lucifer3/server/events"
|
||||||
|
"git.aiterp.net/lucifer3/server/internal/color"
|
||||||
"git.aiterp.net/lucifer3/server/services/script"
|
"git.aiterp.net/lucifer3/server/services/script"
|
||||||
"git.aiterp.net/lucifer3/server/services/uistate"
|
"git.aiterp.net/lucifer3/server/services/uistate"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -38,6 +39,24 @@ func New(addr string) (lucifer3.Service, error) {
|
|||||||
e.HidePort = true
|
e.HidePort = true
|
||||||
e.Use(middleware.CORS())
|
e.Use(middleware.CORS())
|
||||||
|
|
||||||
|
e.GET("/color/:value", func(c echo.Context) error {
|
||||||
|
col, err := color.Parse(c.Param("value"))
|
||||||
|
if err != nil {
|
||||||
|
return c.String(400, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
rgb, _ := col.ToRGB()
|
||||||
|
xy, _ := col.ToXY()
|
||||||
|
hs, _ := col.ToHS()
|
||||||
|
|
||||||
|
return c.JSON(200, color.Color{
|
||||||
|
K: col.K,
|
||||||
|
RGB: rgb.RGB,
|
||||||
|
HS: hs.HS,
|
||||||
|
XY: xy.XY,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
e.GET("/state", func(c echo.Context) error {
|
e.GET("/state", func(c echo.Context) error {
|
||||||
svc.mu.Lock()
|
svc.mu.Lock()
|
||||||
data := svc.data
|
data := svc.data
|
||||||
|
|||||||
+29
-3
@@ -18,6 +18,7 @@ import (
|
|||||||
|
|
||||||
func NewBridge(host string, client *Client) *Bridge {
|
func NewBridge(host string, client *Client) *Bridge {
|
||||||
return &Bridge{
|
return &Bridge{
|
||||||
|
mu: sync.Mutex{},
|
||||||
client: client,
|
client: client,
|
||||||
host: host,
|
host: host,
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@@ -26,6 +27,7 @@ func NewBridge(host string, client *Client) *Bridge {
|
|||||||
activeStates: map[string]device.State{},
|
activeStates: map[string]device.State{},
|
||||||
desiredStates: map[string]device.State{},
|
desiredStates: map[string]device.State{},
|
||||||
colorFlags: map[string]device.ColorFlags{},
|
colorFlags: map[string]device.ColorFlags{},
|
||||||
|
reachable: nil,
|
||||||
hasSeen: map[string]bool{},
|
hasSeen: map[string]bool{},
|
||||||
triggerCongruenceCheckCh: make(chan struct{}, 2),
|
triggerCongruenceCheckCh: make(chan struct{}, 2),
|
||||||
}
|
}
|
||||||
@@ -46,6 +48,7 @@ type Bridge struct {
|
|||||||
reachable map[string]bool
|
reachable map[string]bool
|
||||||
hasSeen map[string]bool
|
hasSeen map[string]bool
|
||||||
lastMotion map[string]time.Time
|
lastMotion map[string]time.Time
|
||||||
|
lastPresence map[string]bool
|
||||||
lastButton map[string]time.Time
|
lastButton map[string]time.Time
|
||||||
|
|
||||||
triggerCongruenceCheckCh chan struct{}
|
triggerCongruenceCheckCh chan struct{}
|
||||||
@@ -115,12 +118,14 @@ func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
|
|||||||
hasSeen := b.hasSeen
|
hasSeen := b.hasSeen
|
||||||
reachable := b.reachable
|
reachable := b.reachable
|
||||||
lastMotion := b.lastMotion
|
lastMotion := b.lastMotion
|
||||||
|
lastPresence := b.lastPresence
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
oldHasSeen := hasSeen
|
oldHasSeen := hasSeen
|
||||||
hasSeen = gentools.CopyMap(hasSeen)
|
hasSeen = gentools.CopyMap(hasSeen)
|
||||||
reachable = gentools.CopyMap(reachable)
|
reachable = gentools.CopyMap(reachable)
|
||||||
lastMotion = gentools.CopyMap(lastMotion)
|
lastMotion = gentools.CopyMap(lastMotion)
|
||||||
|
lastPresence = gentools.CopyMap(lastPresence)
|
||||||
|
|
||||||
colorFlags := make(map[string]device.ColorFlags)
|
colorFlags := make(map[string]device.ColorFlags)
|
||||||
activeStates := make(map[string]device.State)
|
activeStates := make(map[string]device.State)
|
||||||
@@ -144,7 +149,9 @@ func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
lastMotion[b.fullId(*res)] = time.Now()
|
lastMotion[b.fullId(*res)] = time.Now()
|
||||||
|
lastPresence[b.fullId(*res)] = true
|
||||||
} else {
|
} else {
|
||||||
|
if lastMotion[b.fullId(*res)].IsZero() {
|
||||||
extraEvents = append(extraEvents, events.MotionSensed{
|
extraEvents = append(extraEvents, events.MotionSensed{
|
||||||
ID: b.fullId(*res),
|
ID: b.fullId(*res),
|
||||||
SecondsSince: 301,
|
SecondsSince: 301,
|
||||||
@@ -152,6 +159,8 @@ func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
|
|||||||
|
|
||||||
lastMotion[b.fullId(*res)] = time.Now().Add(-time.Millisecond * 301)
|
lastMotion[b.fullId(*res)] = time.Now().Add(-time.Millisecond * 301)
|
||||||
}
|
}
|
||||||
|
lastPresence[b.fullId(*res)] = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +189,7 @@ func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
|
|||||||
b.activeStates = activeStates
|
b.activeStates = activeStates
|
||||||
b.reachable = reachable
|
b.reachable = reachable
|
||||||
b.lastMotion = lastMotion
|
b.lastMotion = lastMotion
|
||||||
|
b.lastPresence = lastPresence
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
return append(newEvents, extraEvents...), nil
|
return append(newEvents, extraEvents...), nil
|
||||||
@@ -193,6 +203,7 @@ func (b *Bridge) ApplyPatches(date time.Time, resources []ResourceData) (eventLi
|
|||||||
colorFlags := b.colorFlags
|
colorFlags := b.colorFlags
|
||||||
lastMotion := b.lastMotion
|
lastMotion := b.lastMotion
|
||||||
lastButton := b.lastButton
|
lastButton := b.lastButton
|
||||||
|
lastPresence := b.lastPresence
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
mapCopy := gentools.CopyMap(resourceMap)
|
mapCopy := gentools.CopyMap(resourceMap)
|
||||||
@@ -201,6 +212,7 @@ func (b *Bridge) ApplyPatches(date time.Time, resources []ResourceData) (eventLi
|
|||||||
colorFlagsCopy := gentools.CopyMap(colorFlags)
|
colorFlagsCopy := gentools.CopyMap(colorFlags)
|
||||||
lastMotionCopy := gentools.CopyMap(lastMotion)
|
lastMotionCopy := gentools.CopyMap(lastMotion)
|
||||||
lastButtonCopy := gentools.CopyMap(lastButton)
|
lastButtonCopy := gentools.CopyMap(lastButton)
|
||||||
|
lastPresenceCopy := gentools.CopyMap(lastPresence)
|
||||||
|
|
||||||
for _, resource := range resources {
|
for _, resource := range resources {
|
||||||
if mapCopy[resource.ID] != nil {
|
if mapCopy[resource.ID] != nil {
|
||||||
@@ -218,6 +230,8 @@ func (b *Bridge) ApplyPatches(date time.Time, resources []ResourceData) (eventLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, resource := range resources {
|
for _, resource := range resources {
|
||||||
|
fullID := b.fullId(resource)
|
||||||
|
|
||||||
if resource.Owner != nil && resource.Owner.Kind == "device" {
|
if resource.Owner != nil && resource.Owner.Kind == "device" {
|
||||||
if parent, ok := mapCopy[resource.Owner.ID]; ok && !strings.HasPrefix(parent.Metadata.Archetype, "bridge") {
|
if parent, ok := mapCopy[resource.Owner.ID]; ok && !strings.HasPrefix(parent.Metadata.Archetype, "bridge") {
|
||||||
hwState, _ := parent.GenerateEvent(b.host, mapCopy)
|
hwState, _ := parent.GenerateEvent(b.host, mapCopy)
|
||||||
@@ -231,18 +245,24 @@ func (b *Bridge) ApplyPatches(date time.Time, resources []ResourceData) (eventLi
|
|||||||
|
|
||||||
if resource.Temperature != nil {
|
if resource.Temperature != nil {
|
||||||
eventList = append(eventList, events.TemperatureChanged{
|
eventList = append(eventList, events.TemperatureChanged{
|
||||||
ID: b.fullId(resource),
|
ID: fullID,
|
||||||
Temperature: resource.Temperature.Temperature,
|
Temperature: resource.Temperature.Temperature,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if resource.Motion != nil {
|
if resource.Motion != nil {
|
||||||
|
lastMotionCopy[fullID] = time.Now()
|
||||||
|
|
||||||
if resource.Motion.Motion {
|
if resource.Motion.Motion {
|
||||||
eventList = append(eventList, events.MotionSensed{
|
eventList = append(eventList, events.MotionSensed{
|
||||||
ID: b.fullId(resource),
|
ID: fullID,
|
||||||
SecondsSince: 0,
|
SecondsSince: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
lastMotionCopy[b.fullId(resource)] = time.Now()
|
lastMotion[fullID] = time.Now()
|
||||||
|
lastPresenceCopy[fullID] = true
|
||||||
|
} else if lastPresenceCopy[fullID] {
|
||||||
|
lastMotion[fullID] = time.Now()
|
||||||
|
lastPresenceCopy[fullID] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if resource.Button != nil {
|
if resource.Button != nil {
|
||||||
@@ -285,6 +305,7 @@ func (b *Bridge) ApplyPatches(date time.Time, resources []ResourceData) (eventLi
|
|||||||
b.colorFlags = colorFlagsCopy
|
b.colorFlags = colorFlagsCopy
|
||||||
b.lastMotion = lastMotionCopy
|
b.lastMotion = lastMotionCopy
|
||||||
b.lastButton = lastButtonCopy
|
b.lastButton = lastButtonCopy
|
||||||
|
b.lastPresence = lastPresenceCopy
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -382,9 +403,14 @@ func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
|
|||||||
case <-quickStep.C:
|
case <-quickStep.C:
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
lastMotion := b.lastMotion
|
lastMotion := b.lastMotion
|
||||||
|
lastPresence := b.lastPresence
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
|
||||||
for id, value := range lastMotion {
|
for id, value := range lastMotion {
|
||||||
|
if lastPresence[id] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
since := time.Since(value)
|
since := time.Since(value)
|
||||||
sinceMod := since % (time.Second * 30)
|
sinceMod := since % (time.Second * 30)
|
||||||
if (since > time.Second*20) && (sinceMod >= time.Second*27 || sinceMod <= time.Second*3) {
|
if (since > time.Second*20) && (sinceMod >= time.Second*27 || sinceMod <= time.Second*3) {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE script_trigger ADD COLUMN name VARCHAR(255) NOT NULL DEFAULT '';
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE script_trigger DROP COLUMN IF EXISTS name;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -49,6 +49,7 @@ type ScriptTrigger struct {
|
|||||||
ScriptName string
|
ScriptName string
|
||||||
ScriptPre json.RawMessage
|
ScriptPre json.RawMessage
|
||||||
ScriptPost json.RawMessage
|
ScriptPost json.RawMessage
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScriptVariable struct {
|
type ScriptVariable struct {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func (q *Queries) DeleteScriptTrigger(ctx context.Context, id uuid.UUID) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listScriptTriggers = `-- name: ListScriptTriggers :many
|
const listScriptTriggers = `-- name: ListScriptTriggers :many
|
||||||
SELECT id, event, device_match, parameter, script_target, script_name, script_pre, script_post FROM script_trigger
|
SELECT id, event, device_match, parameter, script_target, script_name, script_pre, script_post, name FROM script_trigger
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListScriptTriggers(ctx context.Context) ([]ScriptTrigger, error) {
|
func (q *Queries) ListScriptTriggers(ctx context.Context) ([]ScriptTrigger, error) {
|
||||||
@@ -43,6 +43,7 @@ func (q *Queries) ListScriptTriggers(ctx context.Context) ([]ScriptTrigger, erro
|
|||||||
&i.ScriptName,
|
&i.ScriptName,
|
||||||
&i.ScriptPre,
|
&i.ScriptPre,
|
||||||
&i.ScriptPost,
|
&i.ScriptPost,
|
||||||
|
&i.Name,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -112,12 +113,13 @@ func (q *Queries) ListScripts(ctx context.Context) ([]Script, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const replaceScriptTrigger = `-- name: ReplaceScriptTrigger :exec
|
const replaceScriptTrigger = `-- name: ReplaceScriptTrigger :exec
|
||||||
REPLACE INTO script_trigger (id, event, device_match, parameter, script_target, script_name, script_pre, script_post)
|
REPLACE INTO script_trigger (id, name, event, device_match, parameter, script_target, script_name, script_pre, script_post)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`
|
`
|
||||||
|
|
||||||
type ReplaceScriptTriggerParams struct {
|
type ReplaceScriptTriggerParams struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
|
Name string
|
||||||
Event string
|
Event string
|
||||||
DeviceMatch string
|
DeviceMatch string
|
||||||
Parameter string
|
Parameter string
|
||||||
@@ -130,6 +132,7 @@ type ReplaceScriptTriggerParams struct {
|
|||||||
func (q *Queries) ReplaceScriptTrigger(ctx context.Context, arg ReplaceScriptTriggerParams) error {
|
func (q *Queries) ReplaceScriptTrigger(ctx context.Context, arg ReplaceScriptTriggerParams) error {
|
||||||
_, err := q.exec(ctx, q.replaceScriptTriggerStmt, replaceScriptTrigger,
|
_, err := q.exec(ctx, q.replaceScriptTriggerStmt, replaceScriptTrigger,
|
||||||
arg.ID,
|
arg.ID,
|
||||||
|
arg.Name,
|
||||||
arg.Event,
|
arg.Event,
|
||||||
arg.DeviceMatch,
|
arg.DeviceMatch,
|
||||||
arg.Parameter,
|
arg.Parameter,
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ REPLACE INTO script_variable (scope, name, value) VALUES (?, ?, ?);
|
|||||||
SELECT * FROM script_trigger;
|
SELECT * FROM script_trigger;
|
||||||
|
|
||||||
-- name: ReplaceScriptTrigger :exec
|
-- name: ReplaceScriptTrigger :exec
|
||||||
REPLACE INTO script_trigger (id, event, device_match, parameter, script_target, script_name, script_pre, script_post)
|
REPLACE INTO script_trigger (id, name, event, device_match, parameter, script_target, script_name, script_pre, script_post)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
-- name: DeleteScriptTrigger :exec
|
-- name: DeleteScriptTrigger :exec
|
||||||
DELETE FROM script_trigger WHERE id = ?;
|
DELETE FROM script_trigger WHERE id = ?;
|
||||||
@@ -34,7 +34,7 @@ func (d *database) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
|||||||
|
|
||||||
switch event := event.(type) {
|
switch event := event.(type) {
|
||||||
case events.Started:
|
case events.Started:
|
||||||
timeout, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
timeout, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Fetch all aliases first
|
// Fetch all aliases first
|
||||||
@@ -185,6 +185,7 @@ func (d *database) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
|||||||
for _, trig := range scriptTriggers {
|
for _, trig := range scriptTriggers {
|
||||||
bus.RunCommand(script.UpdateTrigger{
|
bus.RunCommand(script.UpdateTrigger{
|
||||||
ID: trig.ID,
|
ID: trig.ID,
|
||||||
|
Name: trig.Name,
|
||||||
Event: script.TriggerEvent(trig.Event),
|
Event: script.TriggerEvent(trig.Event),
|
||||||
DeviceMatch: trig.DeviceMatch,
|
DeviceMatch: trig.DeviceMatch,
|
||||||
Parameter: trig.Parameter,
|
Parameter: trig.Parameter,
|
||||||
@@ -389,6 +390,7 @@ func (d *database) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Comman
|
|||||||
ScriptName: command.ScriptName,
|
ScriptName: command.ScriptName,
|
||||||
ScriptPre: toJSON(command.ScriptPre),
|
ScriptPre: toJSON(command.ScriptPre),
|
||||||
ScriptPost: toJSON(command.ScriptPost),
|
ScriptPost: toJSON(command.ScriptPost),
|
||||||
|
Name: command.Name,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bus.RunEvent(events.Log{
|
bus.RunEvent(events.Log{
|
||||||
|
|||||||
@@ -141,15 +141,15 @@ var shapeTypeMap = map[int]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var shapeIconMap = map[int]string{
|
var shapeIconMap = map[int]string{
|
||||||
0: "triangle",
|
0: "shape_triangle",
|
||||||
1: "triangle",
|
1: "shape_triangle",
|
||||||
2: "square",
|
2: "shape_square",
|
||||||
3: "square",
|
3: "shape_square",
|
||||||
4: "square",
|
4: "shape_square",
|
||||||
7: "hexagon",
|
7: "shape_hexagon",
|
||||||
8: "triangle",
|
8: "shape_triangle",
|
||||||
9: "triangle",
|
9: "shape_triangle",
|
||||||
12: "hexagon",
|
12: "shape_hexagon",
|
||||||
}
|
}
|
||||||
|
|
||||||
var shapeWidthMap = map[int]int{
|
var shapeWidthMap = map[int]int{
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const (
|
|||||||
|
|
||||||
type Trigger struct {
|
type Trigger struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
Event TriggerEvent `json:"event"`
|
Event TriggerEvent `json:"event"`
|
||||||
DeviceMatch string `json:"deviceMatch"`
|
DeviceMatch string `json:"deviceMatch"`
|
||||||
Parameter string `json:"parameter"`
|
Parameter string `json:"parameter"`
|
||||||
|
|||||||
@@ -53,7 +53,15 @@ func (d *Data) WithPatch(patches ...Patch) Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if patch.Device.HWMetadata != nil {
|
if patch.Device.HWMetadata != nil {
|
||||||
if pd.Icon == "" {
|
hasIconAlias := false
|
||||||
|
for _, a := range pd.Aliases {
|
||||||
|
if strings.HasPrefix(a, "lucifer:icon") {
|
||||||
|
hasIconAlias = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasIconAlias {
|
||||||
pd.Icon = patch.Device.HWMetadata.Icon
|
pd.Icon = patch.Device.HWMetadata.Icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user