31 Commits
Author SHA1 Message Date
gisle f00575fd7f fix hue, some ui tweaks.
continuous-integration/drone/push Build is passing
2023-10-15 21:23:11 +02:00
gisle 054e881f76 fix bug
continuous-integration/drone/push Build is passing
2023-10-13 20:37:21 +02:00
gisle 39d803c19b droning on
continuous-integration/drone/push Build is passing
2023-10-13 20:28:20 +02:00
gisle c4e74968f3 droning on
continuous-integration/drone/push Build is passing
2023-10-13 20:20:20 +02:00
gisle 33d9db778c droning on
continuous-integration/drone/push Build is passing
2023-10-13 20:18:48 +02:00
gisle 65fb6997b2 gitignore is bliss
continuous-integration/drone/push Build is passing
2023-10-13 20:13:00 +02:00
gisle 91f8ffe23b remove embed in test.
continuous-integration/drone/push Build is failing
2023-10-13 20:07:27 +02:00
gisle 9a8e8a96b5 embed and build.
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is failing
2023-10-13 19:39:13 +02:00
gisle 16e5726cbc trigger editor 2023-10-12 23:27:04 +02:00
gisle e06cf1ae34 script editor 2023-10-06 13:15:19 +02:00
gisle 13da56009e refactorings + basis of script editor. 2023-09-26 22:43:27 +02:00
gisle 2188d489ea moar stuff. 2023-09-23 14:04:13 +02:00
gisle c7d3a3200e stuff 2023-09-09 17:39:13 +02:00
gisle 4fc7d2d475 room groups and grouping. 2023-09-06 20:25:57 +02:00
gisle 07c9398414 some more stuffs. 2023-09-06 18:00:18 +02:00
gisle 64bb737569 websocket backend + frontend patching. 2023-08-30 22:56:58 +02:00
gisle e8b77ae69c add script triggers. 2023-08-29 21:00:29 +02:00
gisle 8776ed6df3 scripting system, hue fixes and such 2023-08-29 00:06:57 +02:00
gisle da8fd03e8a ui stuff from laptop 2023-02-04 11:15:22 +01:00
gisle d2b2244e2f refactor variable system to be the responsibility of effect enforcer. 2023-01-15 22:56:37 +01:00
gisle 4ec8918308 go mod shenanigans 2023-01-14 14:24:37 +01:00
gisle e1b536e128 add cors 2023-01-14 14:22:59 +01:00
gisle e831c0f342 very early frontend wip + small fixes. 2023-01-13 17:24:13 +01:00
gisle 842f96d8e3 add variable system + fix bugs here and there. 2023-01-13 01:19:34 +01:00
gisle 88036cfcae add state filtering to effect enforcer. 2023-01-08 17:07:53 +01:00
gisle 956bd970a0 add sensor handling. 2023-01-08 16:43:08 +01:00
gisle 8c10b15dd8 fix color in tradfri 2023-01-01 22:22:23 +01:00
gisle cfb1727a89 add color conversion in parse and rgb to ui state. 2023-01-01 19:19:02 +01:00
gisle fe5bfc1efc basic HTTP api and uistate. 2023-01-01 16:30:40 +01:00
Stian Fredrik Aune 865dd1994a Mill: maybe wifi? 2023-01-01 01:03:13 +01:00
Stian Fredrik Aune d8454dcee2 Mill: Online + icon in tradfri 2022-12-31 18:35:53 +01:00
167 changed files with 69491 additions and 448 deletions
+26
View File
@@ -0,0 +1,26 @@
name: lucifer4-build
kind: pipeline
type: docker
steps:
- name: go-test
image: golang:1.20
commands:
- go test -v ./...
when:
event:
exclude:
- tag
- name: docker-backend-tag
image: plugins/docker
settings:
auto_tag: true
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: r.vmaple.dev/lucifer4/backend
registry: r.vmaple.dev
dockerfile: Dockerfile
when:
event:
- tag
+1
View File
@@ -1,2 +1,3 @@
.idea
.vscode
.env
+23
View File
@@ -0,0 +1,23 @@
## 1. Build Frontend
FROM node:20-bookworm AS build-frontend
WORKDIR /project
COPY . .
ENV CGO_ENABLED 0
RUN cd frontend && npm install && rm -rf ./build && npm run build
## 2. Build server
FROM golang:1.20 AS build-server
WORKDIR /project
COPY . .
COPY --from=build-frontend /project/frontend/build /project/frontend/build
ENV CGO_ENABLED 0
RUN go mod download
RUN go build -ldflags "-w -s" -o /build/lucifer4 cmd/lucifer4-server/main.go
## 3. Run server
FROM alpine:3.18.4
# Add results from previous builds
COPY --from=build-server /build/lucifer4 /usr/local/bin/lucifer4
RUN apk add --no-cache tzdata
# Run server
CMD lucifer4
+58 -12
View File
@@ -2,13 +2,21 @@ package lucifer3
import (
"fmt"
"log"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
)
type ServiceKey struct{}
var showVerboseEvents bool
type verboseEventOrCommand interface {
VerboseKey() string
}
type Event interface {
EventDescription() string
}
@@ -31,6 +39,8 @@ type EventBus struct {
privilegedList []ActiveService
signal chan struct{}
setStates int32
verboseCount int
verboseCounts map[string]int
}
// JoinCallback joins the event bus for a moment.
@@ -65,25 +75,27 @@ func (b *EventBus) JoinPrivileged(service ActiveService) {
}
func (b *EventBus) RunCommand(command Command) {
if cd := command.CommandDescription(); !strings.HasPrefix(cd, "SetState") {
if setStates := atomic.LoadInt32(&b.setStates); setStates > 0 {
fmt.Println("[INFO]", setStates, "SetStates commands hidden.")
atomic.AddInt32(&b.setStates, -setStates)
}
if v, ok := command.(verboseEventOrCommand); ok && !showVerboseEvents {
b.mu.Lock()
b.countVerbose(v.VerboseKey())
b.mu.Unlock()
fmt.Println("[COMMAND]", cd)
} else {
if atomic.AddInt32(&b.setStates, 1) >= 1000 {
fmt.Println("[INFO] 1000 SetStates commands hidden.")
atomic.AddInt32(&b.setStates, -1000)
}
fmt.Println("[COMMAND]", command.CommandDescription())
}
b.send(serviceMessage{command: command})
}
func (b *EventBus) RunEvent(event Event) {
fmt.Println("[EVENT]", event.EventDescription())
if v, ok := event.(verboseEventOrCommand); ok && !showVerboseEvents {
b.mu.Lock()
b.countVerbose(v.VerboseKey())
b.mu.Unlock()
} else {
fmt.Println("[EVENT]", event.EventDescription())
}
b.send(serviceMessage{event: event})
}
@@ -194,7 +206,41 @@ func (l *serviceListener) run(signal <-chan struct{}) {
}
}
func (b *EventBus) countVerbose(key string) {
if b.verboseCounts == nil {
b.verboseCounts = make(map[string]int, 8)
}
b.verboseCounts[key] = b.verboseCounts[key] + 1
b.verboseCount += 1
if b.verboseCount >= 1000 {
first := true
s := strings.Builder{}
s.WriteString("[EVENT] 1000 verbose events hidden (")
for key, value := range b.verboseCounts {
if !first {
s.WriteString(", ")
}
first = false
s.WriteString(key)
s.WriteRune(':')
s.WriteString(strconv.Itoa(value))
}
s.WriteRune(')')
log.Println(s.String())
b.verboseCounts = make(map[string]int, 8)
b.verboseCount = 0
}
}
type serviceMessage struct {
event Event
command Command
}
func init() {
showVerboseEvents = os.Getenv("LUCIFER4_VERBOSE") == "true"
}
-26
View File
@@ -1,26 +0,0 @@
package main
import (
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/services"
"git.aiterp.net/lucifer3/server/services/hue"
"git.aiterp.net/lucifer3/server/services/nanoleaf"
"git.aiterp.net/lucifer3/server/services/tradfri"
"time"
)
func main() {
bus := lucifer3.EventBus{}
resolver := services.NewResolver()
sceneMap := services.NewSceneMap(resolver)
bus.JoinPrivileged(resolver)
bus.JoinPrivileged(sceneMap)
bus.Join(services.NewEffectEnforcer(resolver, sceneMap))
bus.Join(nanoleaf.NewService())
bus.Join(hue.NewService())
bus.Join(tradfri.NewService())
time.Sleep(time.Hour)
}
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"git.aiterp.net/lucifer3/server/internal/color"
"image"
color2 "image/color"
"image/png"
"math"
"os"
)
func main() {
img := image.NewRGBA(image.Rect(0, 0, 1000, 1000))
for y := 0; y < 1000; y += 1 {
for x := 0; x < 1000; x += 1 {
vecX := float64(x-500) / 500.0
vecY := float64(y-500) / 500.0
dist := math.Sqrt(vecX*vecX + vecY*vecY)
angle := math.Mod((math.Atan2(vecY/dist, vecX/dist)*(180/math.Pi))+360+90, 360)
if dist >= 0.9 && dist < 1 && angle > 2.5 && angle < 357.5 {
k := 1000 + int(11000*((angle-2.5)/355))
c := color.Color{K: &k}
rgb, _ := c.ToRGB()
img.Set(x, y, color2.RGBA{
R: uint8(rgb.RGB.Red * 255.0),
G: uint8(rgb.RGB.Green * 255.0),
B: uint8(rgb.RGB.Blue * 255.0),
A: 255,
})
} else if dist < 0.85 {
rgb := (color.HueSat{Hue: math.Mod(angle+180, 360), Sat: dist / 0.85}).ToRGB()
img.Set(x, y, color2.RGBA{
R: uint8(rgb.Red * 255.0),
G: uint8(rgb.Green * 255.0),
B: uint8(rgb.Blue * 255.0),
A: 255,
})
} else {
img.Set(x, y, color2.RGBA{R: 0, G: 0, B: 0, A: 0})
}
}
}
_ = png.Encode(os.Stdout, img)
}
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"git.aiterp.net/lucifer3/server/internal/color"
"image"
color2 "image/color"
"image/png"
"log"
"os"
)
func main() {
img := image.NewRGBA(image.Rect(0, 0, 500, 500))
for y := 0; y < 500; y += 1 {
for x := 0; x < 500; x += 1 {
rgb := (color.XY{X: float64(x) / 499, Y: float64(y) / 499}).ToRGB()
if y == 300 {
log.Println(rgb.Red, rgb.Green, rgb.Blue)
}
img.Set(x, y, color2.RGBA{
R: uint8(rgb.Red * 255.0),
G: uint8(rgb.Green * 255.0),
B: uint8(rgb.Blue * 255.0),
A: 255,
})
}
}
_ = png.Encode(os.Stdout, img)
}
+80
View File
@@ -0,0 +1,80 @@
package main
import (
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/services"
"git.aiterp.net/lucifer3/server/services/effectenforcer"
"git.aiterp.net/lucifer3/server/services/httpapiv1"
"git.aiterp.net/lucifer3/server/services/hue"
"git.aiterp.net/lucifer3/server/services/mysqldb"
"git.aiterp.net/lucifer3/server/services/nanoleaf"
"git.aiterp.net/lucifer3/server/services/script"
"git.aiterp.net/lucifer3/server/services/tradfri"
"git.aiterp.net/lucifer3/server/services/uistate"
"log"
"os"
"os/signal"
"strconv"
"syscall"
)
func main() {
bus := lucifer3.EventBus{}
resolver := services.NewResolver()
variables := script.NewVariables()
database, err := mysqldb.Connect(
env("LUCIFER4_DB_HOST"),
envInt("LUCIFER4_DB_PORT"),
env("LUCIFER4_DB_USER"),
env("LUCIFER4_DB_PASSWORD"),
env("LUCIFER4_DB_SCHEMA"),
)
if err != nil {
log.Fatalln("Database failed", err)
}
httpAPI, err := httpapiv1.New(env("LUCIFER4_HTTP_LISTEN"))
if err != nil {
log.Fatalln("HTTP Listen failed", err)
}
bus.JoinPrivileged(resolver)
bus.Join(services.NewTicker())
bus.Join(effectenforcer.NewService(resolver))
bus.Join(nanoleaf.NewService())
bus.Join(hue.NewService())
bus.Join(tradfri.NewService())
bus.Join(uistate.NewService())
bus.Join(script.NewService(resolver, variables))
bus.Join(database)
bus.Join(httpAPI)
bus.Join(variables)
bus.RunEvent(events.Started{})
exitSignal := make(chan os.Signal)
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)
sig := <-exitSignal
log.Println("Received signal", sig)
}
func env(key string) string {
value := os.Getenv(key)
if value == "" {
log.Fatalln("Expected env:", key)
}
return value
}
func envInt(key string) int {
value, err := strconv.Atoi(os.Getenv(key))
if err != nil {
log.Fatalln("Expected numeric env:", key)
}
return value
}
+4 -4
View File
@@ -5,8 +5,8 @@ import (
)
type AddAlias struct {
Match string
Alias string
Match string `json:"match"`
Alias string `json:"alias"`
}
func (c AddAlias) CommandDescription() string {
@@ -14,8 +14,8 @@ func (c AddAlias) CommandDescription() string {
}
type RemoveAlias struct {
Match string
Alias string
Match string `json:"match"`
Alias string `json:"alias"`
}
func (c RemoveAlias) CommandDescription() string {
+19 -1
View File
@@ -23,8 +23,26 @@ func (c SetState) CommandDescription() string {
return fmt.Sprintf("SetState(%s, %s)", c.ID, c.State)
}
func (c SetState) VerboseKey() string {
return "SetState"
}
type SetStateBatch map[string]device.State
func (c SetStateBatch) CommandDescription() string {
return fmt.Sprintf("SetStateBatch(%d devices)", len(c))
b := strings.Builder{}
b.WriteByte('\n')
for key, value := range c {
b.WriteString(" ")
b.WriteString(key)
b.WriteString(": ")
b.WriteString(value.String())
b.WriteRune('\n')
}
return fmt.Sprintf("SetStateBatch(%s)", b.String())
}
func (c SetStateBatch) VerboseKey() string {
return "SetStateBatch"
}
+1
View File
@@ -2,6 +2,7 @@ package device
type Resolver interface {
Resolve(pattern string) []Pointer
CompileMatcher(pattern string) PointerMatcher
}
type SceneMap interface {
+28 -6
View File
@@ -2,9 +2,18 @@ package device
import (
"git.aiterp.net/lucifer3/server/internal/gentools"
"log"
"sort"
"strings"
)
var exclusivePrefixes = []string{
"lucifer:name:",
"lucifer:icon:",
"lucifer:room:",
"lucifer:group:",
}
type PointerMatcher interface {
Match(str string) bool
}
@@ -25,18 +34,24 @@ func (p *Pointer) AddAlias(alias string) (added *string, removed *string) {
}
}
if strings.HasPrefix(alias, "lucifer:name:") {
for i, alias2 := range p.Aliases {
if strings.HasPrefix(alias2, "lucifer:name:") {
p.Aliases = append(p.Aliases[:i], p.Aliases[i+1:]...)
removed = gentools.ShallowCopy(&alias2)
break
for _, prefix := range exclusivePrefixes {
if strings.HasPrefix(alias, prefix) {
for i, alias2 := range p.Aliases {
if strings.HasPrefix(alias2, prefix) {
p.Aliases = append(p.Aliases[:0:0], p.Aliases...)
p.Aliases = append(p.Aliases[:i], p.Aliases[i+1:]...)
removed = gentools.ShallowCopy(&alias2)
break
}
}
break
}
}
added = gentools.ShallowCopy(&alias)
p.Aliases = append(p.Aliases, alias)
sort.Strings(p.Aliases)
return
}
@@ -48,6 +63,7 @@ func (p *Pointer) RemoveAlias(alias string) bool {
for i, alias2 := range p.Aliases {
if alias2 == alias {
p.Aliases = append(p.Aliases[:0:0], p.Aliases...)
p.Aliases = append(p.Aliases[:i], p.Aliases[i+1:]...)
return true
}
@@ -67,6 +83,12 @@ func (p *Pointer) Name() string {
}
func (p *Pointer) Matches(matcher PointerMatcher) bool {
defer func() {
if err := recover(); err != nil {
log.Println("[PANIC]", err)
}
}()
if matcher.Match(p.ID) {
return true
}
+5 -3
View File
@@ -70,10 +70,12 @@ func (s State) Interpolate(s2 State, f float64) State {
}
if s.Power != nil && s2.Power != nil {
if f < 0.5 {
newState.Power = gentools.ShallowCopy(s.Power)
if *s.Power == *s2.Power {
newState.Power = s.Power
} else if *s.Power {
newState.Power = gentools.Ptr(f < 0.95)
} else {
newState.Power = gentools.ShallowCopy(s2.Power)
newState.Power = gentools.Ptr(f > 0.05)
}
} else if s.Power != nil {
newState.Power = gentools.ShallowCopy(s.Power)
+43
View File
@@ -0,0 +1,43 @@
package effects
import (
"fmt"
"git.aiterp.net/lucifer3/server/device"
"time"
)
type Solid struct {
States []device.State `json:"states"`
AnimationMS int64 `json:"animationMs"`
Interleave int `json:"interleave"`
}
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)
}
+4 -4
View File
@@ -7,10 +7,10 @@ import (
)
type Gradient struct {
States []device.State `json:"states,omitempty"`
AnimationMS int64 `json:"AnimationMs,omitempty"`
Reverse bool `json:"backward,omitempty"`
Interpolate bool `json:"interpolate,omitempty"`
States []device.State `json:"states"`
AnimationMS int64 `json:"animationMs"`
Reverse bool `json:"reverse"`
Interpolate bool `json:"interpolate"`
}
func (e Gradient) State(index, length, round int) device.State {
+4 -4
View File
@@ -8,10 +8,10 @@ import (
)
type Manual struct {
Power *bool `json:"power,omitempty"`
Color *color.Color `json:"color,omitempty"`
Intensity *float64 `json:"intensity,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
Power *bool `json:"power"`
Color *color.Color `json:"color"`
Intensity *float64 `json:"intensity"`
Temperature *float64 `json:"temperature"`
}
func (e Manual) EffectDescription() string {
+2 -2
View File
@@ -7,8 +7,8 @@ import (
)
type Pattern struct {
States []device.State `json:"states,omitempty"`
AnimationMS int64 `json:"animationMs,omitempty"`
States []device.State `json:"states"`
AnimationMS int64 `json:"animationMs"`
}
func (e Pattern) State(index, _, round int) device.State {
+3 -3
View File
@@ -8,9 +8,9 @@ import (
)
type Random struct {
States []device.State `json:"states,omitempty"`
Interpolate bool `json:"interpolate,omitempty"`
AnimationMS int64 `json:"animationMs,omitempty"`
States []device.State `json:"states"`
Interpolate bool `json:"interpolate"`
AnimationMS int64 `json:"animationMs"`
}
func (e Random) State(_, _, _ int) device.State {
+10
View File
@@ -8,9 +8,11 @@ import (
type serializedEffect struct {
Manual *Manual `json:"manual,omitempty"`
Solid *Solid `json:"solid,omitempty"`
Gradient *Gradient `json:"gradient,omitempty"`
Pattern *Pattern `json:"pattern,omitempty"`
Random *Random `json:"random,omitempty"`
VRange *VRange `json:"vrange,omitempty"`
}
type Serializable struct {
@@ -27,12 +29,16 @@ func (s *Serializable) UnmarshalJSON(raw []byte) error {
switch {
case value.Manual != nil:
s.Effect = *value.Manual
case value.Solid != nil:
s.Effect = *value.Solid
case value.Gradient != nil:
s.Effect = *value.Gradient
case value.Pattern != nil:
s.Effect = *value.Pattern
case value.Random != nil:
s.Effect = *value.Random
case value.VRange != nil:
s.Effect = *value.VRange
default:
return errors.New("unsupported effect")
}
@@ -44,12 +50,16 @@ func (s *Serializable) MarshalJSON() ([]byte, error) {
switch effect := s.Effect.(type) {
case Manual:
return json.Marshal(serializedEffect{Manual: &effect})
case Solid:
return json.Marshal(serializedEffect{Solid: &effect})
case Gradient:
return json.Marshal(serializedEffect{Gradient: &effect})
case Pattern:
return json.Marshal(serializedEffect{Pattern: &effect})
case Random:
return json.Marshal(serializedEffect{Random: &effect})
case VRange:
return json.Marshal(serializedEffect{VRange: &effect})
default:
panic(s.Effect.EffectDescription() + "is not understood by serializer")
}
+47
View File
@@ -0,0 +1,47 @@
package effects
import (
"fmt"
"git.aiterp.net/lucifer3/server/device"
"time"
)
type VRange struct {
States []device.State `json:"states"`
Variable string `json:"variable"`
Min float64 `json:"min"`
Max float64 `json:"max"`
Interpolate bool `json:"interpolate"`
}
func (e VRange) VariableName() string {
return e.Variable
}
func (e VRange) VariableState(_, _ int, value float64) device.State {
if value <= e.Min {
return e.States[0]
} else if value >= e.Max {
return e.States[len(e.States)-1]
}
return gradientStateFactor(e.States, e.Interpolate, (value-e.Min)/(e.Max-e.Min))
}
func (e VRange) State(_, _, _ int) device.State {
if len(e.States) == 0 {
return device.State{}
}
return e.States[0]
}
func (e VRange) Frequency() time.Duration {
return 0
}
func (e VRange) EffectDescription() string {
return fmt.Sprintf(
"VRange(states:%s, name:%s, range:%.1f..%.1f)",
statesDescription(e.States), e.Variable, e.Min, e.Max,
)
}
+6
View File
@@ -0,0 +1,6 @@
package lucifer3
import "embed"
//go:embed frontend/build/*
var FrontendFS embed.FS
+17
View File
@@ -3,7 +3,9 @@ package events
import (
"fmt"
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/internal/gentools"
"github.com/google/uuid"
"strings"
)
type AssignmentCreated struct {
@@ -32,3 +34,18 @@ type DeviceAssigned struct {
func (e DeviceAssigned) EventDescription() string {
return fmt.Sprintf("DeviceAssigned(device=%s, %s)", e.DeviceID, e.AssignmentID)
}
type AssignmentVariables struct {
ID uuid.UUID
Map map[string]float64
}
func (e AssignmentVariables) VerboseKey() string {
return "AssignmentVariables"
}
func (e AssignmentVariables) EventDescription() string {
return fmt.Sprintf("AssignmentVariables(id=%s, keys=[%s])",
e.ID, strings.Join(gentools.MapKeys(e.Map), ","),
)
}
-38
View File
@@ -1,38 +0,0 @@
package events
import "fmt"
type ButtonPressed struct {
ID string `json:"id"`
SwipedID string `json:"swipedId,omitempty"`
Name string `json:"name"`
}
func (e ButtonPressed) EventDescription() string {
if e.SwipedID != "" {
return fmt.Sprintf("ButtonPressed(name:%s, swipe:%s->%s)", e.Name, e.SwipedID, e.ID)
} else {
return fmt.Sprintf("ButtonPressed(name:%s, id:%s)", e.Name, e.ID)
}
}
func (e ButtonPressed) TriggerKind() string {
return "ButtonPressed:" + e.Name
}
func (e ButtonPressed) TriggerValue(key string) (string, bool) {
switch key {
case "name":
return e.Name, true
case "id":
return e.ID, true
case "swipedFromId", "swipedId":
if e.SwipedID != "" {
return e.SwipedID, true
} else {
return "", false
}
default:
return "", false
}
}
+8
View File
@@ -45,6 +45,10 @@ func (e HardwareState) EventDescription() string {
)
}
func (e HardwareState) VerboseKey() string {
return "HardwareState"
}
// HardwareMetadata contains things that has no bearing on the functionality of
// lucifer, but may be interesting to have in the GUI.
type HardwareMetadata struct {
@@ -63,6 +67,10 @@ func (e HardwareMetadata) EventDescription() string {
return fmt.Sprintf("HardwareMetadata(id:%s, icon:%s, ...)", e.ID, e.Icon)
}
func (e HardwareMetadata) VerboseKey() string {
return "HardwareMetadata"
}
// DeviceReady is triggered to indicate that set-state commands will be heeded for the device
// by this ID. It may be unavailable, however.
type DeviceReady struct {
+1 -1
View File
@@ -10,5 +10,5 @@ type Log struct {
}
func (l Log) EventDescription() string {
return fmt.Sprintf("LOG(id:%s level:%s code:%s :: %s)", l.ID, l.Level, l.Code, l.Message)
return fmt.Sprintf("Log(id:%s level:%s code:%s :: %s)", l.ID, l.Level, l.Code, l.Message)
}
+86
View File
@@ -0,0 +1,86 @@
package events
import "fmt"
type TemperatureChanged struct {
ID string
Temperature float64
}
func (e TemperatureChanged) TriggerKind() string {
return "TemperatureChanged"
}
func (e TemperatureChanged) TriggerValue(key string) (string, bool) {
switch key {
case "temperature":
return fmt.Sprintf("%.2f", e.Temperature), true
case "id":
return e.ID, true
default:
return "", false
}
}
func (e TemperatureChanged) EventDescription() string {
return fmt.Sprintf("TemperatureChanged(id=%s, temperature=%.2f)", e.ID, e.Temperature)
}
type MotionSensed struct {
ID string
SecondsSince float64
}
func (e MotionSensed) TriggerKind() string {
return "MotionSensed"
}
func (e MotionSensed) TriggerValue(key string) (string, bool) {
switch key {
case "secondsSince":
return fmt.Sprintf("%.1f", e.SecondsSince), true
case "id":
return e.ID, true
default:
return "", false
}
}
func (e MotionSensed) EventDescription() string {
return fmt.Sprintf("MotionSensed(id=%s, secondsSince=%.2f)", e.ID, e.SecondsSince)
}
type ButtonPressed struct {
ID string `json:"id"`
SwipedID string `json:"swipedId,omitempty"`
Name string `json:"name"`
}
func (e ButtonPressed) EventDescription() string {
if e.SwipedID != "" {
return fmt.Sprintf("ButtonPressed(name=%s, swipe=%s->%s)", e.Name, e.SwipedID, e.ID)
} else {
return fmt.Sprintf("ButtonPressed(name=%s, id=%s)", e.Name, e.ID)
}
}
func (e ButtonPressed) TriggerKind() string {
return "ButtonPressed:" + e.Name
}
func (e ButtonPressed) TriggerValue(key string) (string, bool) {
switch key {
case "name":
return e.Name, true
case "id":
return e.ID, true
case "swipedFromId", "swipedId":
if e.SwipedID != "" {
return e.SwipedID, true
} else {
return "", false
}
default:
return "", false
}
}
+16
View File
@@ -0,0 +1,16 @@
package events
import "fmt"
type Time struct {
Hour int
Minute int
}
func (t Time) EventDescription() string {
return fmt.Sprintf("Time(%02d:%02d)", t.Hour, t.Minute)
}
func (t Time) VerboseKey() string {
return "time"
}
+13
View File
@@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
+20
View File
@@ -0,0 +1,20 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};
+10
View File
@@ -0,0 +1,10 @@
.DS_Store
node_modules
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/*.blend1
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+13
View File
@@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
+9
View File
@@ -0,0 +1,9 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
+38
View File
@@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
+3
View File
@@ -0,0 +1,3 @@
/_app
*.png
*.html
Binary file not shown.
+8443
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
"name": "frontend",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.0.0",
"@types/svelte-range-slider-pips": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"fa-svelte": "^3.1.0",
"node-sass": "^8.0.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"sass": "^1.57.1",
"svelte": "^3.54.0",
"svelte-check": "^2.9.2",
"svelte-range-slider-pips": "^2.2.2",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.0"
},
"type": "module"
}
+9
View File
@@ -0,0 +1,9 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
<meta name="darkreader" content="we've already gone dark" />
<style>
body, html {
background: #111114;
color: #ccc;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
</style>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
import type { Color } from "$lib/models/color";
import type CommandInput from "$lib/models/command";
import type UIState from "$lib/models/uistate";
export default async function fetchLucifer<T>(path: string, init?: RequestInit): Promise<T> {
const url = (import.meta.env.VITE_LUCIFER4_BACKEND_URL||"") + "/" + path;
const res = await fetch(url, init);
if (res.status !== 200) {
if (res.headers.get("Content-Type")?.includes("application/json")) {
throw await res.json();
} else {
throw await res.text();
}
}
const json = await res.json();
return json as T;
}
export async function fetchUIState(): Promise<UIState> {
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),
})
}
+31
View File
@@ -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>
+123
View File
@@ -0,0 +1,123 @@
<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: boolean | string | number;
export let centered = false;
export let disabled = false;
export let noLabel = false;
export let noBorder = false;
export let inline = false;
export let small = 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
class:small
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
&.small
padding-top: 0
font-size: 0.65em
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>
@@ -0,0 +1,23 @@
<script lang="ts">
import { rgbString } from "$lib/models/color";
import type { Palette } from "$lib/models/palette";
export let palette: Palette
</script>
<div class="palette">
{#each palette as p (p.value)}
<div title={p.label} class="color" style="background-color: {rgbString(p.rgb)}"></div>
{/each}
</div>
<style lang="sass">
div.palette
display: flex
flex-direction: row
div.color
margin: 0.05em
width: 1ch
height: 0.5em
</style>
@@ -0,0 +1,108 @@
<script lang="ts" context="module">
export const selectedColorPicker = writable<any>(null);</script>
<script lang="ts">
import type { Color } from "$lib/models/color";
import { rgb2hsv } from "$lib/utils/color";
import { tick } from "svelte";
import { writable } from "svelte/store";
export let color: Color;
export let id: any = null;
let xy: boolean;
let x: number;
let y: number;
$: {
if (color.xy) {
xy = true;
x = color.xy.x;
y = color.xy.y;
} else if (color.hs) {
xy = false;
x = Math.sin((360-color.hs.hue) * (Math.PI / 180)) * (color.hs.sat * 0.85);
y = Math.cos((360-color.hs.hue) * (Math.PI / 180)) * (color.hs.sat * 0.85);
} else if (color.k) {
const angle = 2.5 + ((color.k - 1000) / 11000) * 355;
x = Math.sin((180-angle) * (Math.PI / 180)) * 0.95;
y = Math.cos((180-angle) * (Math.PI / 180)) * 0.95;
} else if (color.rgb) {
const hs = rgb2hsv(color.rgb);
xy = false;
x = Math.sin((360-hs.hue) * (Math.PI / 180)) * (hs.sat * 0.85);
y = Math.cos((360-hs.hue) * (Math.PI / 180)) * (hs.sat * 0.85);
}
}
function onClickColor(event: MouseEvent & { currentTarget: EventTarget & HTMLDivElement; }) {
if (xy) {
const x = Math.floor((event.offsetX / event.currentTarget.offsetWidth) * 1000) / 1000;
const y = Math.floor((event.offsetY / event.currentTarget.offsetHeight) * 1000) / 1000;
color = { xy: { x, y } };
} else {
const x = 2 * ((event.offsetX / event.currentTarget.offsetWidth) - 0.505);
const y = 2 * ((event.offsetY / event.currentTarget.offsetHeight) - 0.505);
const dist = Math.sqrt((x*x)+(y*y));
const angle = ((Math.atan2(y/dist, x/dist)*(180/Math.PI))+360) % 360.0
if (dist > 0.9) {
color = { k: Math.max(Math.min(Math.floor((((((angle + 90) % 360)-2.5)/355) * 11000) + 1000), 12000), 1000) }
} else {
color = { hs: { hue: Math.floor((270+angle)%360), sat: Math.floor(Math.min(dist / 0.85, 1) * 100) / 100 } };
}
}
tick().then(() => {
tick().then(() => { $selectedColorPicker = id; });
});
}
</script>
{#if $selectedColorPicker == id}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="color-picker" on:click={onClickColor}>
{#if xy}
<img draggable="false" alt="color wheel" src="/color-xy.png" />
<div class="dot" style="left: calc({x} * 10ch); top: calc({y-1} * 10ch)">
<div class="ring"></div>
</div>
{:else}
<img draggable="false" alt="color wheel" src="/color-hsk.png" />
<div class="dot" style="left: calc({((x/2)+0.5)} * 10ch + 0.5px); top: calc({((y/2)+0.5)-1} * 10ch + 0.5px)">
<div class="ring"></div>
</div>
{/if}
</div>
{/if}
<style lang="sass">
@import "$lib/css/colors.sass"
div.color-picker
position: relative
width: 10ch
height: 10ch
> img
width: 10ch
height: 10ch
div.dot
position: relative
z-index: 10
pointer-events: none
div.ring
position: relative
left: -0.525ch
top: -0.95ch
width: 1ch
height: 1ch
border: 2px solid $color-main4
box-sizing: border-box
border-radius: 1ch
</style>
@@ -0,0 +1,88 @@
<script lang="ts" context="module">
import generic_lamp from "./icons/generic_lamp.svg?raw";
import generic_boob from "./icons/generic_boob.svg?raw";
import generic_ball from "./icons/generic_ball.svg?raw";
import generic_strip from "./icons/generic_strip.svg?raw";
import generic_pendulum_01 from "./icons/generic_pendulum_01.svg?raw";
import hue_signe from "./icons/hue_signe.svg?raw";
import hue_dimmerswitch from "./icons/hue_dimmerswitch.svg?raw";
import hue_motionsensor from "./icons/hue_motionsensor.svg?raw";
import hue_lightbulb_e27 from "./icons/hue_lightbulb_e27.svg?raw";
import hue_lightbulb_e14 from "./icons/hue_lightbulb_e14.svg?raw";
import hue_lightbulb_gu10 from "./icons/hue_lightbulb_gu10.svg?raw";
import hue_adore_tube from "./icons/hue_adore_tube.svg?raw";
import hue_playbar from "./icons/hue_playbar.svg?raw";
import hue_go from "./icons/hue_go.svg?raw";
import shape_square from "./icons/shape_square.svg?raw";
import shape_hexagon from "./icons/shape_hexagon.svg?raw";
import shape_triangle from "./icons/shape_triangle.svg?raw";
import hue_ensis_up from "./icons/hue_ensis_up.svg?raw";
import hue_ensis_down from "./icons/hue_ensis_down.svg?raw";
export const deviceIconMap = Object.seal({
generic_lamp,
generic_boob,
generic_ball,
generic_strip,
hue_signe,
hue_dimmerswitch,
hue_motionsensor,
hue_lightbulb_e27,
hue_lightbulb_e14,
hue_lightbulb_gu10,
generic_pendulum_01,
hue_adore_tube,
hue_playbar,
shape_square,
shape_hexagon,
shape_triangle,
hue_go,
hue_ensis_up,
hue_ensis_down,
});
export const deviceIconList = Object.seal(Object.keys(deviceIconMap).sort()) as DeviceIconName[];
export type DeviceIconName = keyof typeof deviceIconMap;
</script>
<script lang="ts">
import type { ColorRGB } from "$lib/models/color";
import { rgbToHex } from "$lib/utils/color";
export let brightColor: ColorRGB;
export let darkColor: ColorRGB;
export let name: DeviceIconName;
let gandalf: string;
let voldemort: string;
$: gandalf = rgbToHex(brightColor);
$: voldemort = rgbToHex(darkColor);
let icon: string;
$: icon = deviceIconMap[name] || deviceIconMap.generic_lamp;
let dataUrl: string;
$: {
if (icon != null) {
const data = encodeURIComponent(
icon
.replaceAll("fill:#ff0000", `fill:${gandalf}`)
.replaceAll("fill:#0000ff", `fill:${voldemort}`)
);
dataUrl = `data:image/svg+xml;url,${data}`
}
}
</script>
<div class="icon">
<img alt="icon" src={dataUrl} />
<slot></slot>
</div>
<style>
div.icon > img {
width: 1em;
height: 1em;
}
</style>
@@ -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>
+18
View File
@@ -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: 700px)
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>
+112
View File
@@ -0,0 +1,112 @@
<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 { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
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";
import { faFilter } from "@fortawesome/free-solid-svg-icons/faFilter";
import { faSignature } from "@fortawesome/free-solid-svg-icons/faSignature";
import { faAsterisk } from "@fortawesome/free-solid-svg-icons/faAsterisk";
import { faPlay } from "@fortawesome/free-solid-svg-icons/faPlay";
import { faClockRotateLeft } from "@fortawesome/free-solid-svg-icons/faClockRotateLeft";
import { faNoteSticky } from "@fortawesome/free-solid-svg-icons/faNoteSticky";
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,
"chevron_up": faChevronUp,
"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,
"filter": faFilter,
"signature": faSignature,
"asterisk": faAsterisk,
"play": faPlay,
"clock_rotate_left": faClockRotateLeft,
"note_sticky": faNoteSticky,
};
export type IconName = keyof typeof icons;
export const iconNames = Object.keys(icons).sort() as IconName[];
export const DEFAULT_ICON: IconName = "question";
</script>
+247
View File
@@ -0,0 +1,247 @@
<script lang="ts" context="module">
export const DARK_COLOR = rgb(0.5176470588235295/2, 0.5333333333333333/2, 0.5607843137254902/1.75);
export const DARK_COLOR_SELECTED = rgb(0.5176470588235295/1.5, 0.5333333333333333/1.5, 0.5607843137254902/1.25);
</script>
<script lang="ts">
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
import type Device from "$lib/models/device";
import { SupportFlags } from "$lib/models/device";
import { rgb, type ColorRGB } from "../models/color";
import DeviceIcon from "./DeviceIcon.svelte";
import Icon from "./Icon.svelte";
export let device: Device;
export let compact: boolean = false;
const {selectedMap, toggleSelection} = getSelectedContext();
function onSelect() {
toggleSelection(device.id);
}
// Process device
let deviceTitle: string;
let iconColor: ColorRGB | null;
let barColor: ColorRGB | null;
let barFraction: number | null;
let roundboiText: string | null;
let roles: string[];
let tags: string[];
let hasRoleOrTag: boolean;
$: {
// TODO: Fix device.name on the backend
const nameAlias = device.aliases?.find(a => a.startsWith("lucifer:name:"));
if (nameAlias != null) {
deviceTitle = nameAlias.slice("lucifer:name:".length);
} else {
deviceTitle = "";
}
roles = device.aliases?.filter(a => a.startsWith("lucifer:role:")).map(a => a.slice("lucifer:role:".length));
tags = device.aliases?.filter(a => a.startsWith("lucifer:tag:")).map(a => a.slice("lucifer:tag:".length));
hasRoleOrTag = !!(roles.length || tags.length)
barFraction = null;
barColor = null;
iconColor = null;
if (device.hwState) {
const hws = device.hwState;
const sflags = hws.supportFlags;
const hasColor = !!(sflags & SupportFlags.Color);
const hasPower = !!(sflags & SupportFlags.Power)
if (deviceTitle == "") {
deviceTitle = device.hwState.internalName;
}
if (hasColor && (!hasPower || device.desiredState?.power)) {
iconColor = device.desiredColorRgb;
} else {
iconColor = null;
}
if (sflags & SupportFlags.SensorPresence) {
barColor = rgb(0.5,0.5,0.5);
barFraction = Math.max(0, (300 - (device.sensors.lastMotion!=null?device.sensors.lastMotion:300))/300);
}
if (sflags & SupportFlags.Temperature) {
barColor = rgb(1.000,0.2,0.2);
barFraction = Math.min(1, Math.max(0, (device.desiredState?.temperature||0) - 5) / 35) * 1;
}
if (sflags & SupportFlags.Intensity) {
if (sflags & SupportFlags.Color) {
barColor = device.desiredColorRgb;
} else {
barColor = rgb(1.000,0.671,0.355);
}
barFraction = device.desiredState?.intensity || 0;
}
if (sflags & SupportFlags.SensorTemperature && !!device.sensors.temperature) {
roundboiText = `${device.sensors.temperature.toFixed(0)}°`
}
}
}
// Make title visible
let displayTitle: string;
$: {
if (deviceTitle.length > 12) {
let last = deviceTitle.split(" ").pop() || deviceTitle;
if (last.length > 4) {
last = last.slice(-3);
}
displayTitle = `${(deviceTitle.split(" ").shift()||"").slice(0, 10 - last.length)}... ${last}`
} else {
displayTitle = deviceTitle;
}
}
let darkColor: ColorRGB;
$: darkColor = $selectedMap[device.id] ? DARK_COLOR_SELECTED : DARK_COLOR
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="lamp" class:compact class:selected={$selectedMap[device.id]} on:click={onSelect}>
<div class="row">
<div class="row-icon">
{#if iconColor != null}
<DeviceIcon name={device.icon||"generic_ball"} brightColor={iconColor} darkColor={darkColor} />
{:else}
<DeviceIcon name={device.icon||"generic_ball"} brightColor={darkColor} darkColor={darkColor}>
{#if !!roundboiText}
<div class="roundboi-text">{roundboiText}</div>
{/if}
</DeviceIcon>
{/if}
</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 class="flatboi">
{#if barColor != null && barFraction != null}
<div style="width: {barFraction*100}%; background-color: rgb({barColor.red*255},{barColor.green*255},{barColor.blue*255})" class="flatboi2"></div>
{/if}
</div>
</div>
<style lang="sass">
div.lamp
cursor: pointer
width: 19ch
height: 2em
margin: 0.5ch
background: #18181c
color: #84888f
border-radius: 0.5ch
border-top-right-radius: 1em
overflow: hidden
box-shadow: 1px 1px 1px #000
@media screen and (max-width: 1921px)
width: calc(20% - 1ch)
@media screen and (max-width: 1600px)
width: calc(25% - 1ch)
@media screen and (max-width: 1200px)
width: calc(33.3333333% - 1ch)
@media screen and (max-width: 700px)
width: calc(50% - 1ch)
margin-top: 0.75ch
margin-bottom: 0.75ch
@media screen and (max-width: 380px)
width: 100%
&.selected
background: #282833
color: #cde
> div.row
display: flex
> div.row-icon
font-size: 1.8em
position: absolute
width: 0
height: 0
div.roundboi-text
width: 0
height: 0
left: 1.4ch
top: 0.9em
text-align: center
position: absolute
font-size: 0.333em
> div.title
font-size: 0.9em
text-align: left
margin-top: 0.4em
margin-left: 2em
margin-right: 1ch
height: 1.6em
> div.name.hasRoleOrTag
margin-top: -0.275em
> div.tag-list
display: flex
flex-direction: row
font-size: 0.5em
> div.tag
display: flex
flex-direction: row
padding: 0 0.5ch
padding-right: 1ch
div.tag-name
padding-left: 0.5ch
> div.flatboi
height: 0.2em
> div.flatboi2
height: 0.2em
&.compact
width: 3.1ch
border-top-right-radius: 0.25em
margin: 0.25ch 0.25ch
background: #282833
&.selected
background: #3c3c50
div.row
margin-left: -0.1ch
div.title
opacity: 0
</style>
+104
View File
@@ -0,0 +1,104 @@
<script lang="ts" context="module">
export const DARK_COLOR = rgb(0.5176470588235295/2, 0.5333333333333333/2, 0.5607843137254902/1.75);
export const DARK_COLOR_SELECTED = rgb(0.5176470588235295/1.5, 0.5333333333333333/1.5, 0.5607843137254902/1.25);
</script>
<script lang="ts">
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
import type Device from "$lib/models/device";
import { SupportFlags } from "$lib/models/device";
import { rgb, type ColorRGB } from "../models/color";
import DeviceIcon from "./DeviceIcon.svelte";
import Lamp from "./Lamp.svelte";
export let name: string
export let devices: Device[];
let selected: boolean;
const {selectedMap, toggleMultiSelection} = getSelectedContext();
function onSelect() {
toggleMultiSelection(devices.map(d => d.id));
}
$: selected = !devices.find(d => !$selectedMap[d.id])
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="metalamp" class:selected>
<div class="row">
<div class="title" on:click={onSelect}>{name}</div>
</div>
<div class="row">
{#each devices as device (device.id)}
<Lamp compact device={device} />
{/each}
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
<div style="width: 3.6ch"></div>
</div>
</div>
<style lang="sass">
div.metalamp
user-select: none
cursor: pointer
width: 19ch
margin: 0.5ch
background: #18181c
color: #84888f
border-radius: 0.5ch
border-top-right-radius: 1em
overflow: hidden
box-shadow: 1px 1px 1px #000
padding-bottom: 0.5ch
@media screen and (max-width: 1921px)
width: calc(20% - 1ch)
@media screen and (max-width: 1600px)
width: calc(25% - 1ch)
@media screen and (max-width: 1200px)
width: calc(33.3333333% - 1ch)
@media screen and (max-width: 700px)
width: calc(50% - 1ch)
margin-top: 0.75ch
margin-bottom: 0.75ch
@media screen and (max-width: 380px)
width: 100%
&.selected
background: #282833
color: #cde
> div.row
width: 100%
padding: 0 0.4ch
box-sizing: border-box
width: 100%
display: flex
flex-direction: row
flex-wrap: wrap
align-content: center
justify-content: space-between
> div.title
font-size: 0.75em
text-align: left
margin-top: 0.4em
margin-right: 1ch
height: 1.6em
user-select: none
margin-left: 0.4ch
</style>
+288
View File
@@ -0,0 +1,288 @@
<script lang="ts">
import { getModalContext, type ModalSelection } from "$lib/contexts/ModalContext.svelte";
import { createEventDispatcher } from "svelte";
export let show: boolean = false;
export let titleText: string = "Edit Form";
export let submitText: string = "Submit";
export let cancelLabel: string = "Cancel";
export let wide: boolean = false;
export let ultrawide: boolean = false;
export let fullheight: boolean = false;
export let error: string | null = null;
export let closable: boolean = false;
export let disabled: boolean = false;
export let nobody: boolean = false;
export let closeInstruction: ModalSelection = { kind: "closed" };
const { modal } = getModalContext();
function onClose() {
modal.set(closeInstruction);
}
function onKeyPress(e: KeyboardEvent) {
if (e.key.toLocaleLowerCase() === "escape") {
onClose();
}
}
</script>
{#if show}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div role="dialog" class="modal-background" on:keypress={onKeyPress}>
<div class="modal" class:wide class:ultrawide class:fullheight class:nobody>
<div class="header" class:nobody>
<div class="title" class:noclose={!closable}>{titleText}</div>
{#if (closable)}
<div class="x">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="button" on:click={onClose} class:nobody>&times;</div>
</div>
{/if}
</div>
{#if (error != null)}
<div class="error">{error}</div>
{/if}
<div class="body" class:nobody>
<slot></slot>
</div>
<div class="button-row" class:nobody>
<button disabled={disabled} type="submit">{submitText}</button>
<slot name="secondary-button-1"></slot>
<slot name="secondary-button-2"></slot>
<button disabled={!closable} on:click|preventDefault={onClose}>{cancelLabel}</button>
</div>
</div>
</div>
{/if}
<style lang="scss">
@import "$lib/css/colors.sass";
div.modal-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
}
div.modal {
position: absolute;
left: 50%;
top: 50%;
width: calc(100vw - 4em);
max-width: 40ch;
max-height: calc(100vh - 4em);
overflow: auto;
transform: translate(-50%,-50%);
border-radius: 0.5em;
box-shadow: 1px 1px 1px $color-main0;
background: $color-main1;
&.nobody {
background: none;
}
&.fullheight {
min-height: 100%
}
&.wide {
max-width: 80ch;
}
&.ultrawide {
max-width: 120ch;
}
}
div.header {
padding: 1em;
padding-bottom: 0;
}
div.error {
margin: 0.5em;
padding: 0.5em;
border: 1px solid rgb(204, 65, 65);
border-radius: 0.2em;
background-color: rgb(133, 39, 39);
color: rgb(211, 141, 141);
animation: fadein 0.5s;
}
div.body {
padding: 1em 0;
margin-top: 0;
padding-top: 0;
display: flex;
flex-direction: row;
@media screen and (max-width: 749px) {
display: block;
}
}
div.title {
color: $color-main9;
line-height: 1em;
}
div.x {
position: relative;
line-height: 1em;
top: -1em;
text-align: right;
}
div.x div.button {
color: $color-main9;
display: inline-block;
padding: 0em 0.5ch 0.1em 0.5ch;
line-height: 1em;
user-select: none;
cursor: pointer;
}
div.x div.button:hover {
color: $color-main9;
}
div.button-row {
display: flex;
flex-direction: row;
border-top: 0.5px solid #000;
:global(button) {
display: inline-block;
padding: 0.25em 1ch;
font-size: 1.25em;
background: none;
border: none;
color: $color-main9;
border-left : 0.5px solid #111;
cursor: pointer;
}
:global(button:first-of-type) {
margin-left: auto;
}
:global(button:hover) {
background-color: #111;
}
:global(button:disabled) {
color: #444;
}
&.nobody {
display: none;
}
}
div.modal :global(label) {
padding: 1em 0 0.125em 0.25ch;
font-size: 0.9em;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
div.modal :global(input:not(.custom)), div.modal :global(select:not(.custom)), div.modal :global(textarea) {
width: calc(100% - 2ch);
margin-bottom: 1em;
margin-top: 0.25em;
background: #121418;
color: #789;
border: none;
outline: none;
resize: vertical;
padding: 0.5em 1ch;
}
div.modal :global(input[type="time"]:not(.custom)) {
padding: 0.333em 1ch;
}
div.modal :global(select:not(.custom)) {
padding-left: 0.5ch;
padding: 0.45em 1ch;
width: 100%;
}
div.modal :global(select:not(.custom) option), div.modal :global(select:not(.custom) optgroup) {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
padding: 0.5em 0;
}
div.modal :global(input:not(.custom))::placeholder {
opacity: 0.5;
}
div.modal :global(select:disabled) {
background: #1a1c1f;
opacity: 1;
color: #789;
}
div.modal :global(input:not(.custom):disabled) {
background: #1a1c1f;
color: #789;
}
div.modal :global(textarea) {
min-height: 6em;
height: 6em;
font-family: inherit;
resize: none0;
}
div.modal :global(textarea:disabled) {
background: #444;
color: #aaa;
}
div.modal :global(input:not(.custom).nolast) {
margin-bottom: 0.5em;
}
div.modal :global(input:not(.custom)[type="checkbox"]) {
width: initial;
display: inline-block;
}
div.modal :global(input:not(.custom)[type="checkbox"] + label) {
width: initial;
display: inline-block;
padding: 0;
margin: 0;
}
div.modal :global(input:not(.custom):focus), div.modal :global(select:focus), div.modal :global(textarea:focus) {
background: #121418;
color: $color-main9;
border: none;
outline: none;
}
div.modal :global(p) {
margin: 0.25em 1ch 1em 1ch;
font-size: 0.9em;
}
div.modal :global(input:not(.custom)::-webkit-outer-spin-button),
div.modal :global(input:not(.custom)::-webkit-inner-spin-button) {
-webkit-appearance: none;
margin: 0;
}
div.modal :global(input:not(.custom)[type=number]) {
appearance: textfield;
-moz-appearance: textfield;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
@@ -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,64 @@
<script lang="ts">
import Icon from "./Icon.svelte";
export let expanded: boolean;
export let title: string;
export let disableExpandToggle: boolean = false;
function toggleExpand() {
if (!disableExpandToggle) {
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>
{#if !disableExpandToggle}
<Icon block name="{expanded ? "chevron_down" : "chevron_right"}" />
{/if}
</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,25 @@
<script lang="ts">
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
export let name: string;
export let devices: {id: string}[];
function toggleAll() {
toggleMultiSelection(devices.map(d => d.id));
}
const {toggleMultiSelection} = getSelectedContext();
</script>
<div class="room-header">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click={toggleAll} class="name">{name}</div>
</div>
<style lang="sass">
div.room-header
user-select: none
font-size: 2rem
color: #abc
border-bottom: 1px solid #123
</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>
@@ -0,0 +1,8 @@
<script lang="ts">
import BFormOption from "./BFormOption.svelte";
import type { IconName } from "../Icon.svelte";
export let icon: IconName;
</script>
<BFormOption on:click state icon={icon}></BFormOption>
@@ -0,0 +1,111 @@
<script lang="ts" context="module">
let nextId = 0;
</script>
<script lang="ts">
import { rgbString, type Color, stringifyColor, parseColor } from "$lib/models/color";
import { hsToHsl, kToRgb, xyToRgb } from "$lib/utils/color";
import { onMount } from "svelte";
import ColorPicker, { selectedColorPicker } from "../ColorPicker.svelte";
import BFormOption from "./BFormOption.svelte";
import BFormParameter from "./BFormParameter.svelte";
export let value: string | null;
let colorPickerId = `BFormColorOption:${++nextId}`
let color: Color | null;
$: color = parseColor(value);
$: submit(color);
function toggle(event: MouseEvent & { currentTarget: EventTarget & HTMLDivElement; }) {
if (event.shiftKey) {
if (color !== null) {
if ($selectedColorPicker !== colorPickerId) {
$selectedColorPicker = colorPickerId;
} else {
$selectedColorPicker = null;
}
}
return
}
if (color === null || (color.hs == null && color.k == null)) {
color = { k: 2750 }
$selectedColorPicker = colorPickerId;
} else {
color = null;
if ($selectedColorPicker === colorPickerId) {
$selectedColorPicker = null;
}
}
}
function submit(color: Color | null) {
value = stringifyColor(color);
}
onMount(() => {
if (value !== null && $selectedColorPicker !== null) {
$selectedColorPicker = colorPickerId;
}
return () => {
if ($selectedColorPicker = colorPickerId) {
$selectedColorPicker = null;
}
}
})
let colorStr = "";
$: {
if (color !== null) {
if (color.rgb != null) {
colorStr = rgbString(color.rgb);
} else if (color.hs != null) {
colorStr = hsToHsl(color.hs);
} else if (color.xy != null) {
colorStr = rgbString(xyToRgb(color.xy));
} else {
colorStr = rgbString(kToRgb(color.k || 2750));
}
} else {
colorStr = "#000000";
}
}
</script>
<BFormOption on:click={toggle} icon="palette" state={(color !== null) || null} color={colorStr}>
{#if color != null}
<div class="picker-wrapper">
<ColorPicker bind:color={color} id={colorPickerId} />
</div>
{/if}
{#if color?.xy != null}
<BFormParameter narrower label="X" type="number" max={1} step={0.01} bind:value={color.xy.x} />
<BFormParameter narrower label="Y" type="number" max={1} step={0.01} bind:value={color.xy.y} />
{/if}
{#if color?.hs != null}
<BFormParameter narrower label="Hue" type="number" max={360} step={1} bind:value={color.hs.hue} />
<BFormParameter narrower label="Sat" type="number" max={1} step={0.01} bind:value={color.hs.sat} />
{/if}
{#if color?.rgb != null}
<BFormParameter narrowest label="R" type="number" max={1} step={0.01} bind:value={color.rgb.red} />
<BFormParameter narrowest label="G" type="number" max={1} step={0.01} bind:value={color.rgb.green} />
<BFormParameter narrowest label="B" type="number" max={1} step={0.01} bind:value={color.rgb.blue} />
{/if}
{#if color?.k != null}
<BFormParameter narrow label="Kelvin" type="number" min={1000} max={12000} step={50} bind:value={color.k} />
{/if}
</BFormOption>
<style lang="sass">
div.picker-wrapper
position: relative
left: 0.5ch
top: -11ch
width: 0
height: 0
</style>
@@ -0,0 +1,8 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import BFormOption from "./BFormOption.svelte";
const dispatch = createEventDispatcher();
</script>
<BFormOption on:click={() => dispatch("delete")} red state icon=trash></BFormOption>
@@ -0,0 +1,28 @@
<script lang="ts">
import BFormOption from "./BFormOption.svelte";
import BFormParameter from "./BFormParameter.svelte";
export let value: number | null;
function update(percentage: number) {
if (value !== null) {
value = percentage / 100;
}
}
function toggle() {
if (value === null) {
value = 0.50;
} else {
value = null;
}
}
let percentage: number
$: percentage = Math.max(Math.min((value || 0) * 100, 100), 0);
$: update(percentage);
</script>
<BFormOption on:click={toggle} icon="cirlce_notch" state={(value !== null) || null}>
<BFormParameter type="number" label="%" bind:value={percentage} min={0} max={100} step={1} narrower />
</BFormOption>
@@ -0,0 +1,12 @@
<div class="bform-body">
<slot></slot>
</div>
<style lang="sass">
div.bform-body
display: flex
user-select: none
font-size: 1rem
width: 100%
flex-wrap: wrap
</style>
@@ -0,0 +1,73 @@
<script lang="ts" context="module">
import { getContext, setContext } from "svelte";
export const ctxKey = {};
export function getBFormOptionEnabled() {
return getContext(ctxKey) as Readable<boolean>
}
</script>
<script lang="ts">
import Icon, { type IconName } from "../Icon.svelte";
import { writable, type Readable } from "svelte/store";
export let state: boolean | null = false;
export let color: string = "hsl(240, 8%, 56%)";
export let icon: IconName = "check";
export let unclickable: boolean = false;
export let red: boolean = false;
export let alwaysOpen: boolean = false;
const enabled = writable(state === true);
$: $enabled = state === true || alwaysOpen;
setContext(ctxKey, { subscribe: enabled.subscribe });
</script>
<div class="bform-option" class:red class:unclickable class:on={state === true} class:off={state === false} style="--color: {color}">
<Icon on:click block name={icon} />
<div class="bform-option-body">
<slot></slot>
</div>
</div>
<style lang="sass">
@import "$lib/css/colors.sass"
div.bform-option
box-shadow: 1px 1px 1px #000
display: flex
margin: 0.25em 0.25ch
min-height: 1.9rem
cursor: pointer
background: $color-main1
> :global(div > .icon)
padding: 0.1em 0.5ch
padding-top: 0.45em
color: $color-main2
&.off
background-color: $color-main2
> :global(div > .icon)
color: $color-main4
&.on
background-color: $color-main2
> :global(div > .icon)
color: var(--color)
&.red
background-color: $color-main2-red
> :global(div > .icon)
color: $color-main6-redder
&.unclickable
cursor: default
div.bform-option-body
display: flex
flex-direction: row
cursor: default
</style>
@@ -0,0 +1,103 @@
<script lang="ts">
import Checkbox from "../Checkbox.svelte";
import { getBFormOptionEnabled } from "./BFormOption.svelte";
export let label: string;
export let type: "number" | "text" | "select" | "checkbox";
export let value: number | string | boolean;
export let wide: boolean = false;
export let narrow: boolean = false;
export let narrower: boolean = false;
export let narrowest: boolean = false;
export let min: number = 0;
export let max: number = 100;
export let step: number = 1;
export let options: {value: string | number, label: string}[] = [];
const enabled = getBFormOptionEnabled();
</script>
{#if $enabled}
<div class="bform-parameter" class:wide class:narrow class:narrower class:narrowest>
<label for="input_{label}">{label}</label>
{#if type === "number"}
<input class="custom" on:blur on:focus type="number" bind:value={value} {min} {max} {step} />
{:else if type === "text"}
<input class="custom" on:blur on:focus type="text" bind:value={value} />
{:else if type === "checkbox"}
<Checkbox small centered noLabel bind:checked={value} />
{:else if type === "select"}
<select class="custom" bind:value={value}>
{#each options as opt (opt.value)}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
{/if}
</div>
{/if}
<style lang="sass">
@import "$lib/css/colors.sass"
div.bform-parameter
display: flex
flex-direction: column
background: $color-main1
height: 100%
> 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: -0em
width: 10rem
> input, > select
width: 10rem
font-size: 0.9rem
padding-left: 0
background: none
outline: none
border: none
text-align: center
font-size: 1rem
color: $color-main9
> option
background: $color-main1
&::-webkit-inner-spin-button
-webkit-appearance: none
margin: 0
-webkit-appearance: none
-moz-appearance: none
moz-appearance: none
appearance: none
&:focus
color: $color-main13
> select
margin-top: 0.08rem
&.wide
> input, > label, > select
width: 20rem
&.narrow
> input, > label, > select
width: calc(6rem + 0.22ch)
&.narrower
> input, > label, > select
width: 3rem
&.narrowest
> input, > label, > select
width: calc(2rem - 0.11ch)
</style>
@@ -0,0 +1,17 @@
<script lang="ts">
import BFormOption from "./BFormOption.svelte";
export let value: boolean | null;
function toggle() {
if (value === null) {
value = true;
} else if (value === true) {
value = false;
} else {
value = null;
}
}
</script>
<BFormOption icon="power" on:click={toggle} state={value} color="#00FF00"></BFormOption>
@@ -0,0 +1,20 @@
<script lang="ts">
import BFormOption from "./BFormOption.svelte";
import BFormParameter from "./BFormParameter.svelte";
export let value: number | null;
function toggle() {
if (value === null) {
value = 20;
} else {
value = null;
}
}
</script>
<BFormOption on:click={toggle} icon="temperature_half" state={(value !== null) || null}>
{#if value != null}
<BFormParameter type="number" label="Celsius" bind:value={value} min={10} max={32} step={1} narrower />
{/if}
</BFormOption>
@@ -0,0 +1,9 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 151.535, 21.348 142.812, 22.208 134.442, 23.520 126.207, 25.510 117.819, 28.055 109.866, 30.975 102.178, 34.534 94.447, 38.666 87.217, 43.082 80.371, 48.071 73.595, 53.632 67.365, 59.374 61.624, 65.603 56.063, 72.380 51.073, 79.226 46.657, 86.456 42.525, 94.187 38.967, 101.875 36.046, 109.828 33.502, 118.216 31.512, 126.451 30.199, 134.820 29.340, 143.544 28.994, 152.009 29.340, 160.474 30.199, 169.197 31.512, 177.567 33.502, 185.801 36.046, 194.190 38.967, 202.142 42.525, 209.831 47.239, 218.650 50.667, 225.063 57.011, 232.793 63.355, 240.523 67.020, 244.989 74.750, 251.333 79.739, 255.427 87.243, 260.897 95.169, 265.737 101.457, 269.098 109.884, 272.999 118.602, 276.200 125.425, 278.270 134.451, 280.451 143.625, 281.890 150.721, 282.589 160.000, 282.967 169.279, 282.589 176.375, 281.890 185.549, 280.451 194.575, 278.270 201.398, 276.200 210.116, 272.999 218.543, 269.098 224.831, 265.737 232.757, 260.897 240.261, 255.427 247.991, 249.083 252.980, 244.989 259.324, 237.259 263.937, 231.638 268.927, 224.792 273.343, 217.561 277.475, 209.831 281.033, 202.142 283.954, 194.190 286.498, 185.801 288.488, 177.567 289.801, 169.197 290.660, 160.474 291.006, 152.009 290.660, 143.544 289.801, 134.820 288.488, 126.451 286.498, 118.216 283.954, 109.828 281.033, 101.875 277.475, 94.187 273.343, 86.456 268.927, 79.226 263.937, 72.380 258.376, 65.603 252.635, 59.374 246.405, 53.632 239.629, 48.071 232.783, 43.082 225.553, 38.666 217.822, 34.534 210.134, 30.975 202.181, 28.055 193.793, 25.510 185.558, 23.520 177.188, 22.208 168.465, 21.348 160.000, 21.003 151.535, 21.348 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 240.261, 255.427 247.991, 249.083 252.980, 244.989 252.980, 252.242 252.440, 256.200 250.901, 260.442 248.598, 264.323 245.881, 267.252 240.117, 271.982 232.763, 277.342 224.996, 282.086 218.379, 285.623 210.120, 289.445 201.577, 292.582 194.397, 294.760 185.551, 296.898 176.560, 298.308 169.093, 299.044 160.000, 299.415 150.907, 299.044 143.440, 298.308 134.449, 296.898 125.603, 294.760 118.423, 292.582 109.880, 289.445 101.621, 285.623 95.004, 282.086 87.237, 277.342 79.883, 271.982 74.119, 267.252 71.402, 264.323 69.099, 260.442 67.560, 256.200 67.020, 252.242 67.020, 244.989 74.750, 251.333 79.739, 255.427 87.243, 260.897 95.169, 265.737 101.457, 269.098 109.884, 272.999 118.602, 276.200 125.425, 278.270 134.451, 280.451 143.625, 281.890 150.721, 282.589 160.000, 282.967 169.279, 282.589 176.375, 281.890 185.549, 280.451 194.575, 278.270 201.398, 276.200 210.116, 272.999 218.543, 269.098 224.831, 265.737 232.757, 260.897 240.261, 255.427 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,10 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 160.000, 62.462 150.025, 63.169 140.050, 63.876 132.687, 64.398 122.918, 66.531 113.148, 68.665 106.424, 70.133 97.091, 73.724 87.758, 77.315 82.220, 79.446 73.610, 84.532 65.000, 89.618 61.005, 91.978 53.487, 98.573 45.969, 105.167 43.594, 107.250 37.633, 115.278 31.671, 123.307 30.657, 124.673 26.773, 133.888 22.890, 143.103 22.690, 143.578 21.334, 153.485 20.000, 163.238 21.356, 173.146 22.690, 182.899 26.573, 192.114 30.457, 201.329 30.657, 201.804 36.618, 209.832 42.580, 217.861 43.594, 219.227 51.112, 225.821 58.630, 232.415 61.005, 234.498 69.615, 239.584 78.225, 244.671 82.220, 247.031 91.553, 250.622 100.886, 254.213 106.424, 256.344 116.194, 258.477 125.964, 260.610 132.687, 262.078 142.662, 262.786 152.637, 263.493 160.000, 264.015 169.975, 263.308 179.950, 262.600 187.313, 262.078 197.082, 259.945 206.852, 257.812 213.576, 256.344 222.909, 252.753 232.242, 249.162 237.780, 247.031 246.390, 241.945 255.000, 236.858 258.995, 234.498 266.513, 227.904 274.031, 221.310 276.406, 219.227 282.367, 211.198 288.329, 203.170 289.343, 201.804 293.227, 192.589 297.110, 183.373 297.310, 182.899 298.666, 172.991 300.000, 163.238 298.644, 153.331 297.310, 143.578 293.427, 134.363 289.543, 125.147 289.343, 124.673 283.382, 116.644 277.420, 108.615 276.406, 107.250 268.888, 100.656 261.370, 94.062 258.995, 91.978 250.385, 86.892 241.775, 81.806 237.780, 79.446 228.447, 75.855 219.114, 72.264 213.576, 70.133 203.806, 68.000 194.036, 65.866 187.313, 64.398 177.338, 63.691 167.363, 62.984 160.000, 62.462 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 237.780, 62.906 228.447, 59.316 219.114, 55.725 213.576, 53.594 203.806, 51.460 194.036, 49.327 187.313, 47.859 177.338, 47.152 167.363, 46.445 160.000, 45.923 150.025, 46.630 140.050, 47.337 132.687, 47.859 122.918, 49.992 113.148, 52.126 106.424, 53.594 97.091, 57.185 87.758, 60.776 82.220, 62.906 73.610, 67.993 65.000, 73.079 61.005, 75.439 53.487, 82.033 45.969, 88.628 43.594, 90.711 37.633, 98.739 31.671, 106.768 30.657, 108.134 26.773, 117.349 22.890, 126.564 22.690, 127.039 21.334, 136.946 20.000, 146.699 20.000, 156.699 20.000, 163.238 21.356, 153.331 22.690, 143.578 26.573, 134.363 30.457, 125.147 30.657, 124.673 36.618, 116.644 42.580, 108.615 43.594, 107.250 51.112, 100.656 58.630, 94.062 61.005, 91.978 69.615, 86.892 78.225, 81.806 82.220, 79.446 91.553, 75.855 100.886, 72.264 106.424, 70.133 116.194, 67.999 125.964, 65.866 132.687, 64.398 142.662, 63.691 152.637, 62.984 160.000, 62.462 169.975, 63.169 179.950, 63.876 187.313, 64.398 197.082, 66.531 206.852, 68.665 213.576, 70.133 222.909, 73.724 232.242, 77.315 237.780, 79.446 246.390, 84.532 255.000, 89.618 258.995, 91.978 266.513, 98.573 274.031, 105.167 276.406, 107.250 282.367, 115.278 288.329, 123.307 289.343, 124.673 293.227, 133.888 297.110, 143.103 297.310, 143.578 298.666, 153.485 300.000, 163.238 300.000, 153.238 300.000, 146.699 298.644, 136.792 297.310, 127.039 293.427, 117.823 289.543, 108.608 289.343, 108.134 283.382, 100.105 277.420, 92.076 276.406, 90.711 268.888, 84.117 261.370, 77.523 258.995, 75.439 250.385, 70.353 241.775, 65.267 237.780, 62.906 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 136.608, 78.586 126.838, 80.720 117.068, 82.853 114.115, 83.498 104.782, 87.089 95.449, 90.680 93.385, 91.474 84.775, 96.560 76.166, 101.646 75.216, 102.208 67.698, 108.802 60.304, 115.287 54.342, 123.315 49.224, 130.209 45.340, 139.424 42.401, 146.400 41.045, 156.308 40.097, 163.238 40.131, 166.336 40.376, 169.310 41.731, 179.218 42.674, 186.109 43.327, 188.961 44.385, 191.698 48.269, 200.913 51.093, 207.616 52.464, 210.011 58.425, 218.040 63.220, 224.497 64.777, 226.439 66.643, 228.096 74.161, 234.690 80.607, 240.343 89.217, 245.430 97.621, 250.395 99.062, 251.234 108.396, 254.824 117.729, 258.415 118.026, 258.530 119.143, 258.795 128.912, 260.928 138.682, 263.061 139.171, 263.168 149.146, 263.875 159.121, 264.582 160.000, 264.645 169.975, 263.937 179.950, 263.230 180.829, 263.168 190.599, 261.035 200.368, 258.901 200.857, 258.795 201.974, 258.530 211.307, 254.939 220.641, 251.348 220.938, 251.234 222.379, 250.395 230.989, 245.308 239.393, 240.343 246.911, 233.749 253.357, 228.096 255.223, 226.439 256.780, 224.497 262.742, 216.468 267.536, 210.011 268.907, 207.616 272.790, 198.401 275.615, 191.698 276.673, 188.961 277.326, 186.109 278.681, 176.201 279.624, 169.310 279.869, 166.336 279.903, 163.238 278.548, 153.331 277.599, 146.400 273.716, 137.185 270.776, 130.209 264.815, 122.180 259.696, 115.287 252.178, 108.693 244.784, 102.208 236.175, 97.121 227.565, 92.035 226.615, 91.474 217.282, 87.883 207.949, 84.292 205.885, 83.498 196.115, 81.365 186.345, 79.231 183.392, 78.586 173.417, 77.879 163.442, 77.172 160.000, 76.928 150.025, 77.635 140.050, 78.342 136.608, 78.586 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

@@ -0,0 +1,11 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 55.961, 155.000 65.961, 155.000 75.961, 155.000 85.961, 155.000 95.961, 155.000 105.961, 155.000 115.961, 155.000 125.961, 155.000 135.961, 155.000 145.961, 155.000 155.961, 155.000 165.961, 155.000 175.961, 155.000 185.961, 155.000 195.961, 155.000 205.961, 155.000 215.961, 155.000 225.961, 155.000 235.961, 155.000 245.961, 155.000 255.961, 155.000 264.039, 155.000 260.274, 145.736 256.926, 137.500 246.926, 137.500 236.926, 137.500 226.926, 137.500 216.926, 137.500 206.926, 137.500 196.926, 137.500 186.926, 137.500 176.926, 137.500 166.926, 137.500 156.926, 137.500 146.926, 137.500 136.926, 137.500 126.926, 137.500 116.926, 137.500 106.926, 137.500 96.926, 137.500 86.926, 137.500 76.926, 137.500 66.926, 137.500 63.074, 137.500 59.308, 146.764 55.961, 155.000 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 103.715, 37.500 113.715, 37.500 123.715, 37.500 133.715, 37.500 143.715, 37.500 153.715, 37.500 163.715, 37.500 173.715, 37.500 183.715, 37.500 193.715, 37.500 203.715, 37.500 213.715, 37.500 216.285, 37.500 212.520, 28.236 209.172, 20.000 199.172, 20.000 189.172, 20.000 179.172, 20.000 169.172, 20.000 159.172, 20.000 149.172, 20.000 139.172, 20.000 129.172, 20.000 119.172, 20.000 110.828, 20.000 107.063, 29.264 103.715, 37.500 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 168.549, 165.000 158.549, 165.000 148.549, 165.000 138.549, 165.000 131.451, 165.000 131.451, 175.000 131.451, 185.000 131.451, 195.000 131.451, 205.000 131.451, 215.000 131.451, 225.000 131.451, 235.000 131.451, 245.000 131.451, 255.000 131.451, 265.000 131.451, 275.000 131.451, 280.000 131.835, 283.902 132.973, 287.654 134.821, 291.111 137.309, 294.142 140.339, 296.629 143.797, 298.478 147.549, 299.616 151.451, 300.000 161.451, 300.000 171.451, 300.000 181.451, 300.000 188.549, 300.000 188.549, 290.000 188.549, 280.000 188.549, 270.000 188.549, 260.000 188.549, 250.000 188.549, 240.000 188.549, 230.000 188.549, 220.000 188.549, 210.000 188.549, 200.000 188.549, 190.000 188.549, 185.000 188.165, 181.098 187.027, 177.346 185.179, 173.889 182.691, 170.858 179.661, 168.371 176.203, 166.522 172.451, 165.384 168.549, 165.000 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 103.715, 37.500 99.950, 46.764 96.185, 56.028 92.420, 65.292 88.655, 74.556 84.890, 83.821 81.125, 93.085 77.360, 102.349 73.595, 111.613 69.829, 120.877 66.064, 130.141 63.074, 137.500 73.074, 137.500 83.074, 137.500 93.074, 137.500 103.074, 137.500 113.074, 137.500 123.074, 137.500 133.074, 137.500 143.074, 137.500 153.074, 137.500 163.074, 137.500 173.074, 137.500 183.074, 137.500 193.074, 137.500 203.074, 137.500 213.074, 137.500 223.074, 137.500 233.074, 137.500 243.074, 137.500 253.074, 137.500 256.926, 137.500 253.161, 128.236 249.396, 118.972 245.631, 109.708 241.866, 100.444 238.101, 91.179 234.336, 81.915 230.571, 72.651 226.806, 63.387 223.040, 54.123 219.275, 44.859 216.285, 37.500 206.285, 37.500 196.285, 37.500 186.285, 37.500 176.285, 37.500 166.285, 37.500 156.285, 37.500 146.285, 37.500 136.285, 37.500 126.285, 37.500 116.285, 37.500 106.285, 37.500 103.715, 37.500 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1,17 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 103.892, 120.570 98.382, 122.830 93.861, 125.585 90.410, 128.009 86.667, 131.511 84.205, 135.508 81.748, 139.558 80.914, 143.792 80.000, 148.766 80.000, 154.675 80.000, 164.675 80.000, 174.675 80.000, 184.675 80.000, 194.675 80.000, 204.675 80.000, 214.675 80.000, 224.675 80.000, 226.259 80.914, 231.233 81.748, 235.467 84.205, 239.516 86.667, 243.514 90.410, 247.016 93.861, 249.440 98.382, 252.195 103.892, 254.455 110.177, 256.135 116.998, 257.170 124.468, 257.538 134.468, 257.538 144.468, 257.538 154.468, 257.538 164.468, 257.538 174.468, 257.538 184.468, 257.538 194.468, 257.538 195.532, 257.538 203.002, 257.170 209.823, 256.135 216.108, 254.455 221.618, 252.195 226.139, 249.440 229.590, 247.016 233.333, 243.514 235.795, 239.516 238.252, 235.467 239.086, 231.233 240.000, 226.259 240.000, 216.259 240.000, 206.259 240.000, 196.259 240.000, 186.259 240.000, 176.259 240.000, 166.259 240.000, 156.259 240.000, 154.675 240.000, 148.766 239.086, 143.792 238.252, 139.558 235.795, 135.508 233.333, 131.511 229.590, 128.009 226.139, 125.585 221.618, 122.830 216.108, 120.570 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 99.714, 129.357 99.595, 130.436 99.595, 140.436 99.595, 141.548 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 216.108, 120.570 213.203, 120.447 209.982, 122.066 213.820, 123.641 216.970, 125.560 219.310, 127.750 220.286, 129.357 220.752, 130.125 221.329, 133.055 221.329, 138.047 220.752, 140.976 220.405, 141.548 219.310, 143.352 216.970, 145.542 213.820, 147.461 209.982, 149.036 205.603, 150.206 200.851, 150.927 194.992, 151.215 184.992, 151.215 174.992, 151.215 164.992, 151.215 154.992, 151.215 144.992, 151.215 134.992, 151.215 125.008, 151.215 119.149, 150.927 114.397, 150.206 110.018, 149.036 106.180, 147.461 103.030, 145.542 100.690, 143.352 99.595, 141.548 99.248, 140.976 98.671, 138.047 98.671, 133.055 99.248, 130.125 99.714, 129.357 100.690, 127.750 103.030, 125.560 106.180, 123.641 110.018, 122.066 106.797, 120.447 103.892, 120.570 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 220.405, 141.548 220.405, 131.548 220.405, 130.436 220.286, 129.357 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 220.752, 140.976 221.329, 138.047 221.329, 128.047 221.329, 126.608 221.329, 121.616 220.752, 118.687 219.310, 116.311 216.970, 114.121 213.820, 112.202 209.982, 110.627 205.603, 109.457 200.851, 108.736 194.992, 108.448 184.992, 108.448 174.992, 108.448 164.992, 108.448 154.992, 108.448 144.992, 108.448 134.992, 108.448 125.008, 108.448 119.149, 108.736 114.397, 109.457 110.018, 110.627 106.180, 112.202 103.030, 114.121 100.690, 116.311 99.248, 118.687 98.671, 121.616 98.671, 126.608 98.671, 136.608 98.671, 138.047 99.248, 140.976 100.690, 143.352 103.030, 145.542 106.180, 147.461 110.018, 149.036 114.397, 150.206 119.149, 150.927 125.008, 151.215 135.008, 151.215 145.008, 151.215 155.008, 151.215 165.008, 151.215 175.008, 151.215 185.008, 151.215 194.992, 151.215 200.851, 150.927 205.603, 150.206 209.982, 149.036 213.820, 147.461 216.970, 145.542 219.310, 143.352 220.752, 140.976 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 98.671, 126.608 99.248, 129.538 100.690, 131.913 103.030, 134.103 106.180, 136.022 110.018, 137.597 114.397, 138.767 119.149, 139.488 125.008, 139.777 135.008, 139.777 145.008, 139.777 155.008, 139.777 165.008, 139.777 175.008, 139.777 185.008, 139.777 194.992, 139.777 200.851, 139.488 205.603, 138.767 209.982, 137.597 213.820, 136.022 216.970, 134.103 219.310, 131.913 220.752, 129.538 221.329, 126.608 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 156.434, 128.417 158.182, 128.682 160.000, 128.772 161.818, 128.682 163.566, 128.417 165.178, 127.986 166.590, 127.407 167.749, 126.701 168.610, 125.895 169.140, 125.021 169.319, 124.112 169.319, 114.112 169.319, 104.112 169.319, 94.112 169.319, 84.112 169.319, 74.112 169.319, 66.269 169.140, 65.360 168.610, 64.486 167.749, 63.680 166.590, 62.974 165.178, 62.395 163.566, 61.964 161.818, 61.699 160.000, 61.609 158.182, 61.699 156.434, 61.964 154.822, 62.395 153.410, 62.974 152.251, 63.680 151.390, 64.486 150.860, 65.360 150.681, 66.269 150.681, 76.269 150.681, 86.269 150.681, 96.269 150.681, 106.269 150.681, 116.269 150.681, 124.112 150.860, 125.021 151.390, 125.895 152.251, 126.701 153.410, 127.407 154.822, 127.986 156.434, 128.417 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 169.319, 66.269 169.140, 67.178 168.610, 68.052 167.749, 68.858 166.590, 69.564 165.178, 70.143 163.566, 70.574 161.818, 70.839 160.000, 70.929 158.182, 70.839 156.434, 70.574 154.822, 70.143 153.410, 69.564 152.251, 68.858 151.390, 68.052 150.860, 67.178 150.681, 66.269 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -0,0 +1,9 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 19.563, 190.646 20.193, 195.399 21.151, 200.213 27.776, 198.679 34.402, 197.144 41.027, 195.610 40.320, 192.062 39.866, 188.648 39.655, 185.368 39.676, 182.220 39.919, 179.197 40.374, 176.295 41.036, 173.505 41.897, 170.821 42.955, 168.235 44.206, 165.742 45.651, 163.335 47.288, 161.013 49.117, 158.774 51.138, 156.618 53.350, 154.545 55.750, 152.559 58.336, 150.663 61.105, 148.858 64.051, 147.150 67.169, 145.542 70.455, 144.037 73.902, 142.637 77.504, 141.347 81.255, 140.169 85.146, 139.105 89.173, 138.157 93.327, 137.329 97.603, 136.620 101.992, 136.035 106.489, 135.573 111.085, 135.236 115.775, 135.026 120.551, 134.944 125.406, 134.991 130.335, 135.168 135.342, 135.478 143.028, 136.118 150.282, 136.856 157.113, 137.684 163.543, 138.593 169.596, 139.578 175.294, 140.631 180.661, 141.746 185.721, 142.916 190.498, 144.133 195.016, 145.393 199.299, 146.689 203.374, 148.015 207.264, 149.367 210.995, 150.738 214.592, 152.125 218.080, 153.522 221.483, 154.923 224.827, 156.325 228.134, 157.720 231.429, 159.102 234.734, 160.466 238.071, 161.802 241.462, 163.103 244.928, 164.360 248.488, 165.563 252.161, 166.704 255.967, 167.773 259.923, 168.760 264.048, 169.659 268.359, 170.459 272.876, 171.154 277.618, 171.738 282.605, 172.202 287.856, 172.541 293.393, 172.749 299.237, 172.820 299.237, 166.020 299.237, 159.219 299.237, 152.419 293.899, 152.354 288.896, 152.166 284.208, 151.864 279.809, 151.454 275.674, 150.946 271.774, 150.345 268.081, 149.660 264.565, 148.894 261.197, 148.053 257.946, 147.140 254.781, 146.157 251.673, 145.106 248.595, 143.989 245.517, 142.808 242.416, 141.566 239.266, 140.267 236.046, 138.915 232.734, 137.518 229.310, 136.083 225.756, 134.619 222.053, 133.137 218.184, 131.645 214.131, 130.156 209.879, 128.679 205.411, 127.224 200.709, 125.802 195.758, 124.421 190.538, 123.091 185.033, 121.819 179.223, 120.612 173.089, 119.478 166.611, 118.424 159.769, 117.456 152.541, 116.580 144.907, 115.803 136.817, 115.130 131.330, 114.791 125.872, 114.595 120.474, 114.543 115.143, 114.634 109.883, 114.870 104.700, 115.250 99.600, 115.774 94.586, 116.443 89.664, 117.259 84.840, 118.221 80.119, 119.332 75.505, 120.594 71.005, 122.009 66.623, 123.578 62.367, 125.307 58.241, 127.198 54.254, 129.255 50.414, 131.483 46.728, 133.886 43.207, 136.469 39.864, 139.237 36.711, 142.193 33.764, 145.339 31.040, 148.676 28.556, 152.201 26.330, 155.910 24.382, 159.792 22.728, 163.838 21.382, 168.031 20.357, 172.356 19.659, 176.795 19.294, 181.333 19.262, 185.954 19.563, 190.646 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 47.029, 190.950 46.634, 187.982 46.453, 185.173 46.470, 182.515 46.672, 179.998 47.047, 177.608 47.587, 175.330 48.287, 173.149 49.145, 171.050 50.165, 169.019 51.349, 167.047 52.704, 165.126 54.235, 163.253 55.947, 161.426 57.845, 159.648 59.931, 157.923 62.206, 156.255 64.668, 154.650 67.316, 153.116 70.145, 151.657 73.151, 150.280 76.329, 148.990 79.671, 147.793 83.171, 146.694 86.822, 145.695 90.617, 144.803 94.548, 144.019 98.608, 143.346 102.790, 142.788 107.085, 142.347 111.486, 142.025 115.985, 141.823 120.576, 141.744 125.251, 141.790 130.003, 141.961 134.850, 142.260 142.401, 142.890 149.529, 143.615 156.228, 144.426 162.521, 145.316 168.432, 146.278 173.985, 147.305 179.204, 148.389 184.115, 149.524 188.744, 150.704 193.118, 151.923 197.262, 153.177 201.205, 154.461 204.975, 155.771 208.599, 157.103 212.105, 158.454 215.521, 159.822 218.874, 161.203 222.191, 162.593 225.496, 163.988 228.816, 165.381 232.173, 166.766 235.589, 168.133 239.085, 169.474 242.680, 170.778 246.390, 172.032 250.233, 173.226 254.224, 174.346 258.376, 175.383 262.703, 176.325 267.221, 177.164 271.944, 177.891 276.888, 178.499 282.070, 178.981 287.509, 179.333 293.224, 179.548 299.237, 179.621 299.237, 172.820 293.393, 172.749 287.856, 172.541 282.605, 172.202 277.618, 171.738 272.876, 171.154 268.359, 170.459 264.048, 169.659 259.923, 168.760 255.967, 167.773 252.161, 166.704 248.488, 165.563 244.928, 164.360 241.462, 163.103 238.071, 161.802 234.734, 160.466 231.429, 159.102 228.134, 157.720 224.827, 156.325 221.483, 154.923 218.080, 153.522 214.592, 152.125 210.995, 150.738 207.264, 149.367 203.374, 148.015 199.299, 146.689 195.016, 145.393 190.498, 144.133 185.721, 142.916 180.661, 141.746 175.294, 140.631 169.596, 139.578 163.543, 138.593 157.113, 137.684 150.282, 136.856 143.028, 136.118 135.342, 135.478 130.335, 135.168 125.406, 134.991 120.551, 134.944 115.775, 135.026 111.085, 135.236 106.489, 135.573 101.992, 136.035 97.603, 136.620 93.327, 137.329 89.173, 138.157 85.146, 139.105 81.255, 140.169 77.504, 141.347 73.902, 142.637 70.455, 144.037 67.169, 145.542 64.051, 147.150 61.105, 148.858 58.336, 150.663 55.750, 152.559 53.350, 154.545 51.138, 156.618 49.117, 158.774 47.288, 161.013 45.651, 163.335 44.206, 165.742 42.955, 168.235 41.897, 170.821 41.036, 173.505 40.374, 176.295 39.919, 179.197 39.676, 182.220 39.655, 185.368 39.866, 188.648 40.320, 192.062 41.027, 195.610 47.652, 194.076 47.029, 190.950 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -0,0 +1,11 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 131.519, 170.000 131.519, 180.000 131.642, 181.564 132.008, 183.090 132.609, 184.540 133.429, 185.878 134.448, 187.071 135.641, 188.090 136.979, 188.910 138.429, 189.511 139.954, 189.877 141.519, 190.000 151.519, 190.000 161.519, 190.000 171.519, 190.000 181.519, 190.000 188.481, 190.000 188.481, 180.000 188.358, 178.436 187.992, 176.910 187.391, 175.460 186.571, 174.122 185.552, 172.929 184.359, 171.910 183.021, 171.090 181.571, 170.489 180.046, 170.123 178.481, 170.000 168.481, 170.000 158.481, 170.000 148.481, 170.000 138.481, 170.000 131.519, 170.000 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 290.000, 160.000 300.000, 160.000 300.000, 150.000 300.000, 140.000 300.000, 130.000 290.000, 130.000 290.000, 140.000 290.000, 150.000 290.000, 160.000 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 20.000, 160.000 30.000, 160.000 30.000, 150.000 30.000, 140.000 30.000, 130.000 20.000, 130.000 20.000, 140.000 20.000, 150.000 20.000, 160.000 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 290.000, 160.000 290.000, 150.000 290.000, 140.000 290.000, 130.000 280.000, 130.000 270.000, 130.000 260.000, 130.000 250.000, 130.000 240.000, 130.000 230.000, 130.000 220.000, 130.000 210.000, 130.000 200.000, 130.000 190.000, 130.000 180.000, 130.000 170.000, 130.000 160.000, 130.000 150.000, 130.000 140.000, 130.000 130.000, 130.000 120.000, 130.000 110.000, 130.000 100.000, 130.000 90.000, 130.000 80.000, 130.000 70.000, 130.000 60.000, 130.000 50.000, 130.000 40.000, 130.000 30.000, 130.000 30.000, 140.000 30.000, 150.000 30.000, 160.000 40.000, 160.000 50.000, 160.000 60.000, 160.000 70.000, 160.000 80.000, 160.000 90.000, 160.000 100.000, 160.000 110.000, 160.000 120.000, 160.000 130.000, 160.000 140.000, 160.000 150.000, 160.000 160.000, 160.000 170.000, 160.000 180.000, 160.000 190.000, 160.000 200.000, 160.000 210.000, 160.000 220.000, 160.000 230.000, 160.000 240.000, 160.000 250.000, 160.000 260.000, 160.000 270.000, 160.000 280.000, 160.000 290.000, 160.000 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,11 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 105.413, 186.990 100.413, 195.651 95.413, 204.311 90.413, 212.971 86.413, 219.900 85.623, 221.526 85.073, 223.247 84.773, 225.029 84.730, 226.836 84.945, 228.631 85.412, 230.376 86.124, 232.038 87.064, 233.581 88.215, 234.975 89.552, 236.190 91.049, 237.203 99.709, 242.203 108.370, 247.203 117.030, 252.203 125.690, 257.203 127.615, 258.315 129.241, 259.105 130.963, 259.655 132.745, 259.955 134.552, 259.998 136.346, 259.783 138.092, 259.315 139.753, 258.604 141.296, 257.663 142.690, 256.513 143.906, 255.175 144.919, 253.679 149.919, 245.018 154.919, 236.358 159.919, 227.698 163.919, 220.769 155.259, 215.769 146.599, 210.769 137.938, 205.769 129.278, 200.769 120.618, 195.769 111.958, 190.769 105.413, 186.990 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 175.081, 66.321 170.081, 74.982 165.081, 83.642 160.081, 92.302 156.081, 99.231 164.741, 104.231 173.401, 109.231 182.062, 114.231 190.722, 119.231 199.382, 124.231 208.042, 129.231 214.587, 133.010 219.587, 124.349 224.587, 115.689 229.587, 107.029 233.587, 100.100 234.377, 98.474 234.927, 96.753 235.227, 94.971 235.270, 93.164 235.055, 91.369 234.588, 89.624 233.876, 87.962 232.936, 86.419 231.785, 85.025 230.448, 83.810 228.951, 82.797 220.291, 77.797 211.630, 72.797 202.970, 67.797 194.310, 62.797 192.385, 61.685 190.759, 60.895 189.037, 60.345 187.255, 60.045 185.448, 60.002 183.654, 60.217 181.908, 60.685 180.247, 61.396 178.704, 62.337 177.310, 63.487 176.094, 64.825 175.081, 66.321 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 105.413, 186.990 114.073, 191.990 122.734, 196.990 131.394, 201.990 140.054, 206.990 148.714, 211.990 157.375, 216.990 163.919, 220.769 168.919, 212.109 173.919, 203.448 178.919, 194.788 183.919, 186.128 188.919, 177.468 189.253, 176.889 180.593, 171.889 171.933, 166.889 163.272, 161.889 154.612, 156.889 145.952, 151.889 137.292, 146.889 130.747, 143.111 125.747, 151.771 120.747, 160.431 115.747, 169.092 110.747, 177.752 105.747, 186.412 105.413, 186.990 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 130.747, 143.111 139.407, 148.111 148.067, 153.111 156.728, 158.111 165.388, 163.111 174.048, 168.111 182.708, 173.111 189.253, 176.889 194.253, 168.229 199.253, 159.569 204.253, 150.908 209.253, 142.248 214.253, 133.588 214.587, 133.010 205.927, 128.010 197.266, 123.010 188.606, 118.010 179.946, 113.010 171.286, 108.010 162.625, 103.010 156.081, 99.231 151.081, 107.891 146.081, 116.552 141.081, 125.212 136.081, 133.872 131.081, 142.532 130.747, 143.111 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,10 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 61.025, 138.173 60.000, 140.748 60.000, 150.748 60.000, 160.748 60.000, 170.748 60.000, 177.711 60.000, 179.252 61.025, 181.827 63.942, 184.011 68.309, 185.469 73.460, 185.982 83.460, 185.982 93.460, 185.982 103.460, 185.982 113.460, 185.982 123.460, 185.982 133.460, 185.982 143.460, 185.982 153.460, 185.982 163.460, 185.982 173.460, 185.982 183.460, 185.982 193.460, 185.982 203.460, 185.982 213.460, 185.982 223.460, 185.982 233.460, 185.982 243.460, 185.982 246.540, 185.982 251.691, 185.469 256.058, 184.011 258.975, 181.827 260.000, 179.252 260.000, 177.711 260.000, 167.711 260.000, 157.711 260.000, 147.711 260.000, 140.748 258.975, 138.173 256.058, 135.989 251.691, 134.531 246.540, 134.018 236.540, 134.018 226.540, 134.018 216.540, 134.018 206.540, 134.018 196.540, 134.018 186.540, 134.018 176.540, 134.018 166.540, 134.018 156.540, 134.018 146.540, 134.018 136.540, 134.018 126.540, 134.018 116.540, 134.018 106.540, 134.018 96.540, 134.018 86.540, 134.018 76.540, 134.018 73.460, 134.018 68.309, 134.531 63.942, 135.989 61.025, 138.173 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 60.000, 177.711 61.025, 175.136 63.942, 172.953 68.309, 171.494 73.460, 170.982 83.460, 170.982 93.460, 170.982 103.460, 170.982 113.460, 170.982 123.460, 170.982 133.460, 170.982 143.460, 170.982 153.460, 170.982 163.460, 170.982 173.460, 170.982 183.460, 170.982 193.460, 170.982 203.460, 170.982 213.460, 170.982 223.460, 170.982 233.460, 170.982 243.460, 170.982 246.540, 170.982 251.691, 171.494 256.058, 172.953 258.975, 175.136 260.000, 177.711 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 255.651, 177.926 255.651, 179.038 254.879, 180.979 252.923, 182.443 249.994, 183.421 246.112, 183.807 236.112, 183.807 226.112, 183.807 216.112, 183.807 206.112, 183.807 196.112, 183.807 186.112, 183.807 176.112, 183.807 166.112, 183.807 156.112, 183.807 146.112, 183.807 136.112, 183.807 126.112, 183.807 116.112, 183.807 106.112, 183.807 96.112, 183.807 86.112, 183.807 76.112, 183.807 73.888, 183.807 70.006, 183.421 67.077, 182.443 65.121, 180.979 64.349, 179.038 64.349, 177.926 65.121, 175.984 67.077, 174.520 70.006, 173.542 73.888, 173.156 83.888, 173.156 93.888, 173.156 103.888, 173.156 113.888, 173.156 123.888, 173.156 133.888, 173.156 143.888, 173.156 153.888, 173.156 163.888, 173.156 173.888, 173.156 183.888, 173.156 193.888, 173.156 203.888, 173.156 213.888, 173.156 223.888, 173.156 233.888, 173.156 243.888, 173.156 246.112, 173.156 249.994, 173.542 252.923, 174.520 254.879, 175.984 255.651, 177.926 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,10 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 258.975, 181.827 260.000, 179.252 260.000, 169.252 260.000, 159.252 260.000, 149.252 260.000, 142.289 260.000, 140.748 258.975, 138.173 256.058, 135.989 251.691, 134.531 246.540, 134.018 236.540, 134.018 226.540, 134.018 216.540, 134.018 206.540, 134.018 196.540, 134.018 186.540, 134.018 176.540, 134.018 166.540, 134.018 156.540, 134.018 146.540, 134.018 136.540, 134.018 126.540, 134.018 116.540, 134.018 106.540, 134.018 96.540, 134.018 86.540, 134.018 76.540, 134.018 73.460, 134.018 68.309, 134.531 63.942, 135.989 61.025, 138.173 60.000, 140.748 60.000, 142.289 60.000, 152.289 60.000, 162.289 60.000, 172.289 60.000, 177.711 60.000, 179.252 61.025, 181.827 63.942, 184.011 68.309, 185.469 73.460, 185.982 83.460, 185.982 93.460, 185.982 103.460, 185.982 113.460, 185.982 123.460, 185.982 133.460, 185.982 143.460, 185.982 153.460, 185.982 163.460, 185.982 173.460, 185.982 183.460, 185.982 193.460, 185.982 203.460, 185.982 213.460, 185.982 223.460, 185.982 233.460, 185.982 243.460, 185.982 246.540, 185.982 251.691, 185.469 256.058, 184.011 258.975, 181.827 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 260.000, 142.289 258.975, 144.864 256.058, 147.047 251.691, 148.506 246.540, 149.018 236.540, 149.018 226.540, 149.018 216.540, 149.018 206.540, 149.018 196.540, 149.018 186.540, 149.018 176.540, 149.018 166.540, 149.018 156.540, 149.018 146.540, 149.018 136.540, 149.018 126.540, 149.018 116.540, 149.018 106.540, 149.018 96.540, 149.018 86.540, 149.018 76.540, 149.018 73.460, 149.018 68.309, 148.506 63.942, 147.047 61.025, 144.864 60.000, 142.289 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 255.651, 142.074 255.651, 140.962 254.879, 139.021 252.923, 137.557 249.994, 136.579 246.112, 136.193 236.112, 136.193 226.112, 136.193 216.112, 136.193 206.112, 136.193 196.112, 136.193 186.112, 136.193 176.112, 136.193 166.112, 136.193 156.112, 136.193 146.112, 136.193 136.112, 136.193 126.112, 136.193 116.112, 136.193 106.112, 136.193 96.112, 136.193 86.112, 136.193 76.112, 136.193 73.888, 136.193 70.006, 136.579 67.077, 137.557 65.121, 139.021 64.349, 140.962 64.349, 142.074 65.121, 144.016 67.077, 145.480 70.006, 146.458 73.888, 146.844 83.888, 146.844 93.888, 146.844 103.888, 146.844 113.888, 146.844 123.888, 146.844 133.888, 146.844 143.888, 146.844 153.888, 146.844 163.888, 146.844 173.888, 146.844 183.888, 146.844 193.888, 146.844 203.888, 146.844 213.888, 146.844 223.888, 146.844 233.888, 146.844 243.888, 146.844 246.112, 146.844 249.994, 146.458 252.923, 145.480 254.879, 144.016 255.651, 142.074 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,10 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#ff0000;fill-opacity:0.666" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 102.711, 217.273 111.522, 222.002 120.333, 226.732 120.539, 226.843 130.105, 229.757 139.671, 232.671 139.883, 232.736 149.834, 233.720 159.786, 234.705 160.000, 234.726 169.951, 233.741 180.096, 232.736 189.661, 229.819 199.419, 226.843 208.228, 222.109 217.036, 217.375 217.227, 217.273 224.941, 210.908 232.654, 204.544 232.837, 204.393 239.160, 196.647 245.484, 188.900 245.647, 188.700 250.341, 179.870 255.035, 171.041 255.166, 170.796 258.054, 161.222 260.943, 151.648 261.027, 151.368 262.002, 141.416 262.977, 131.463 263.007, 131.164 262.032, 121.212 261.057, 111.259 261.027, 110.960 258.139, 101.387 255.734, 93.416 253.254, 97.907 247.342, 103.554 239.211, 109.376 237.998, 110.244 236.800, 110.960 228.090, 115.873 224.443, 117.930 215.490, 122.386 208.089, 126.070 198.961, 130.155 196.707, 131.164 189.437, 134.290 180.121, 137.925 170.806, 141.561 169.492, 142.073 160.064, 145.404 150.635, 148.735 149.474, 149.145 142.438, 151.368 132.871, 154.278 130.319, 155.054 120.651, 157.609 112.834, 159.675 103.062, 161.797 97.397, 163.027 87.516, 164.566 84.360, 165.057 74.381, 165.713 74.018, 165.737 66.624, 165.057 62.386, 163.027 64.733, 170.796 69.431, 179.623 74.129, 188.451 74.262, 188.700 80.589, 196.444 86.917, 204.187 87.086, 204.393 94.802, 210.754 102.519, 217.114 102.711, 217.273 " />
<path style="fill:#ff0000;fill-opacity:0.85" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 242.877, 87.836 233.006, 88.441 223.112, 89.898 220.663, 90.258 214.504, 91.533 205.831, 93.416 196.163, 95.971 188.841, 97.907 179.274, 100.816 170.271, 103.554 160.804, 106.776 151.338, 109.997 150.612, 110.244 148.706, 110.960 139.390, 114.596 130.847, 117.930 121.660, 121.879 112.473, 125.829 111.911, 126.070 102.882, 130.368 101.209, 131.164 94.930, 134.290 86.220, 139.202 81.130, 142.073 72.802, 147.609 70.490, 149.145 67.890, 151.368 64.031, 155.054 61.479, 159.675 62.386, 163.027 66.624, 165.057 74.018, 165.737 83.997, 165.081 84.360, 165.057 94.241, 163.519 97.397, 163.027 107.170, 160.905 112.834, 159.675 122.502, 157.120 130.319, 155.054 139.886, 152.144 142.438, 151.368 149.474, 149.145 158.903, 145.814 168.332, 142.484 169.492, 142.073 178.808, 138.438 188.124, 134.802 189.437, 134.290 196.707, 131.164 205.835, 127.079 208.089, 126.070 217.041, 121.614 224.443, 117.930 233.153, 113.017 236.800, 110.960 237.998, 110.244 246.129, 104.422 247.342, 103.554 253.254, 97.907 255.734, 93.416 255.224, 91.533 254.589, 90.258 250.124, 88.441 242.877, 87.836 " />
<path style="fill:#ff0000;fill-opacity:1.0" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 205.022, 103.230 199.585, 103.563 192.787, 104.564 189.394, 105.266 184.618, 106.303 175.260, 108.777 165.692, 111.686 165.031, 111.887 155.565, 115.109 154.203, 115.572 153.153, 115.967 143.837, 119.602 143.317, 119.806 134.130, 123.755 132.887, 124.289 126.992, 127.095 123.534, 128.816 115.933, 133.104 110.072, 136.999 108.640, 138.223 106.515, 140.253 105.109, 142.799 105.609, 144.645 107.943, 145.763 112.016, 146.137 117.712, 145.763 124.893, 144.645 133.395, 142.799 143.026, 140.253 149.701, 138.223 153.576, 136.999 163.005, 133.668 164.602, 133.104 173.918, 129.468 175.588, 128.816 179.592, 127.095 185.861, 124.289 194.869, 119.806 201.675, 115.967 202.335, 115.572 207.481, 111.887 210.738, 108.777 212.104, 106.303 211.823, 105.266 211.473, 104.564 209.014, 103.563 205.022, 103.230 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,12 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 169.178, 251.497 166.652, 253.304 166.044, 253.780 165.431, 254.331 164.979, 254.807 163.612, 256.633 163.400, 256.858 163.119, 257.060 162.797, 257.221 162.466, 257.324 162.157, 257.359 160.000, 257.359 157.843, 257.359 157.534, 257.324 157.203, 257.221 156.881, 257.060 156.600, 256.858 156.388, 256.633 155.021, 254.807 154.569, 254.331 153.956, 253.780 153.348, 253.304 150.822, 251.497 150.286, 251.020 149.872, 250.470 149.692, 249.994 149.157, 240.008 148.649, 230.521 153.262, 231.514 154.360, 231.728 155.582, 231.901 156.846, 232.037 158.057, 232.101 160.000, 232.154 161.943, 232.101 163.154, 232.037 164.418, 231.901 165.640, 231.728 166.738, 231.514 171.351, 230.521 170.816, 240.506 170.308, 249.994 170.128, 250.470 169.714, 251.020 169.178, 251.497 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 136.024, 158.261 132.797, 157.704 126.260, 155.940 127.741, 165.767 129.256, 172.976 131.568, 181.450 134.450, 190.361 137.622, 198.836 140.771, 206.044 144.402, 213.615 144.709, 214.426 144.934, 215.364 145.016, 216.176 145.016, 217.333 145.113, 218.093 145.397, 218.832 145.849, 219.500 146.438, 220.050 147.124, 220.446 147.860, 220.660 152.699, 221.422 153.775, 221.636 155.020, 221.820 156.323, 221.968 157.559, 222.041 158.630, 222.098 160.000, 222.083 161.370, 222.098 162.441, 222.041 163.677, 221.968 164.980, 221.820 166.225, 221.636 167.301, 221.422 172.140, 220.660 172.876, 220.446 173.562, 220.050 174.151, 219.500 174.603, 218.832 174.887, 218.093 174.984, 217.333 174.984, 216.176 175.066, 215.364 175.291, 214.426 175.598, 213.615 179.229, 206.044 182.378, 198.836 185.550, 190.361 188.432, 181.450 190.744, 172.976 192.259, 165.767 193.740, 155.940 187.203, 157.704 183.976, 158.261 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 183.976, 158.261 183.426, 158.356 176.872, 158.845 168.439, 159.172 160.000, 159.327 151.561, 159.172 143.128, 158.845 136.574, 158.356 136.024, 158.261 " />
<path style="fill:#ff0000;fill-opacity:0.666" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 165.524, 59.271 164.408, 58.717 163.268, 58.369 162.182, 58.250 160.000, 58.250 157.818, 58.250 156.732, 58.369 155.592, 58.717 154.476, 59.271 153.461, 59.992 152.616, 60.833 151.998, 61.735 149.009, 67.164 146.179, 72.594 143.935, 77.048 141.526, 82.302 139.319, 87.556 137.649, 92.010 136.060, 96.646 134.650, 101.100 133.185, 106.354 131.886, 111.608 130.951, 116.062 130.101, 120.698 129.357, 125.152 128.599, 130.406 127.943, 135.660 127.489, 140.114 126.715, 150.084 126.260, 155.940 132.797, 157.704 136.024, 158.260 136.024, 158.259 136.338, 148.264 136.431, 145.301 136.933, 140.114 137.364, 135.660 137.798, 131.167 137.928, 130.406 138.824, 125.152 139.583, 120.698 140.019, 118.141 140.563, 116.062 141.729, 111.608 143.008, 106.723 143.151, 106.354 145.193, 101.100 146.649, 97.353 147.071, 96.646 149.838, 92.010 150.804, 90.391 153.784, 87.556 155.312, 86.103 160.000, 84.655 164.688, 86.103 166.216, 87.556 169.196, 90.391 170.162, 92.010 172.929, 96.646 173.351, 97.353 174.807, 101.100 176.849, 106.354 176.992, 106.723 178.271, 111.608 179.437, 116.062 179.981, 118.141 180.417, 120.698 181.176, 125.152 182.072, 130.406 182.202, 131.167 182.636, 135.660 183.067, 140.114 183.569, 145.301 183.884, 155.296 183.977, 158.261 183.978, 158.260 187.203, 157.704 193.740, 155.940 192.966, 145.970 192.511, 140.114 192.057, 135.660 191.401, 130.406 190.643, 125.152 189.899, 120.698 189.049, 116.062 188.114, 111.608 186.815, 106.354 185.350, 101.100 183.940, 96.646 182.351, 92.010 180.681, 87.556 178.474, 82.302 176.065, 77.048 173.821, 72.594 170.991, 67.164 168.002, 61.735 167.384, 60.833 166.539, 59.992 165.524, 59.271 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 164.688, 86.103 160.000, 84.655 155.312, 86.103 153.784, 87.556 150.804, 90.391 149.838, 92.010 147.071, 96.646 146.649, 97.353 145.193, 101.100 143.151, 106.354 143.008, 106.723 141.729, 111.608 140.563, 116.062 140.019, 118.141 139.583, 120.698 138.824, 125.152 137.928, 130.406 137.798, 131.167 137.364, 135.660 136.933, 140.114 136.431, 145.301 136.118, 155.296 136.025, 158.261 136.026, 158.261 136.574, 158.356 143.128, 158.845 151.561, 159.172 160.000, 159.327 168.439, 159.172 176.872, 158.845 183.426, 158.356 183.976, 158.260 183.976, 158.259 183.662, 148.264 183.569, 145.301 183.067, 140.114 182.636, 135.660 182.202, 131.167 182.072, 130.406 181.176, 125.152 180.417, 120.698 179.981, 118.141 179.437, 116.062 178.271, 111.608 176.992, 106.723 176.849, 106.354 174.807, 101.100 173.351, 97.353 172.929, 96.646 170.162, 92.010 169.196, 90.391 166.216, 87.556 164.688, 86.103 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -0,0 +1,10 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 105.776, 88.534 108.781, 98.072 111.786, 107.609 114.791, 117.147 117.115, 124.523 127.092, 125.202 137.069, 125.881 139.426, 126.042 141.671, 126.174 144.307, 126.293 147.074, 126.388 149.703, 126.449 151.935, 126.469 161.935, 126.469 168.065, 126.469 170.297, 126.449 172.926, 126.388 175.693, 126.293 178.329, 126.174 180.574, 126.042 190.551, 125.363 200.527, 124.683 202.885, 124.523 205.890, 114.985 208.895, 105.447 211.900, 95.909 214.224, 88.534 214.400, 83.212 213.511, 77.890 211.327, 73.391 205.522, 68.892 196.391, 64.814 195.448, 64.393 185.689, 62.213 185.376, 62.143 175.893, 60.852 172.658, 60.485 168.835, 60.174 165.004, 59.966 161.749, 59.893 158.251, 59.893 154.996, 59.966 151.165, 60.174 147.342, 60.485 144.107, 60.852 134.624, 62.143 124.865, 64.323 124.552, 64.393 115.421, 68.471 114.478, 68.892 108.673, 73.391 106.489, 77.890 105.600, 83.212 105.776, 88.534 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 182.407, 226.829 181.225, 236.759 181.206, 236.918 180.513, 242.746 180.189, 243.597 179.436, 244.601 178.367, 245.605 177.146, 246.456 175.202, 247.603 173.855, 248.476 172.489, 249.506 171.311, 250.535 170.500, 251.408 166.395, 256.699 166.135, 256.952 165.788, 257.166 165.406, 257.309 165.047, 257.359 160.000, 257.359 154.953, 257.359 154.594, 257.309 154.212, 257.166 153.865, 256.952 153.605, 256.699 149.500, 251.408 148.689, 250.535 147.511, 249.506 146.145, 248.476 144.798, 247.603 142.854, 246.456 141.633, 245.605 140.564, 244.601 139.811, 243.597 139.487, 242.746 139.164, 240.032 137.985, 230.119 136.828, 220.396 146.248, 221.390 148.492, 221.604 150.986, 221.777 153.557, 221.891 156.042, 221.978 158.257, 222.008 159.989, 222.019 161.721, 222.029 163.958, 221.976 166.431, 221.912 169.014, 221.776 171.508, 221.603 173.752, 221.389 183.172, 220.395 182.407, 226.829 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 129.157, 183.080 129.895, 193.053 130.633, 203.026 130.839, 205.808 133.541, 210.084 143.487, 211.125 145.099, 211.294 147.296, 211.508 149.839, 211.692 152.492, 211.816 155.025, 211.910 157.196, 211.942 162.789, 211.973 164.974, 211.916 167.499, 211.843 170.160, 211.695 172.703, 211.511 174.901, 211.297 184.846, 210.255 186.458, 210.087 189.161, 205.808 189.899, 195.835 190.637, 185.862 190.843, 183.080 191.941, 173.141 192.012, 172.503 193.608, 162.632 194.038, 159.975 196.044, 150.179 196.613, 147.404 199.086, 137.715 199.344, 136.703 202.136, 127.100 202.885, 124.523 192.908, 125.202 182.931, 125.881 180.574, 126.042 178.329, 126.174 175.693, 126.293 172.926, 126.388 170.297, 126.449 168.065, 126.469 158.065, 126.469 151.935, 126.469 149.703, 126.449 147.074, 126.388 144.307, 126.293 141.671, 126.174 139.426, 126.042 129.449, 125.363 119.473, 124.683 117.115, 124.523 119.906, 134.125 120.656, 136.703 123.129, 146.392 123.387, 147.404 125.394, 157.200 125.962, 159.975 127.559, 169.847 127.988, 172.503 129.086, 182.443 129.157, 183.080 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,15 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 188.922, 250.354 188.970, 250.498 188.970, 254.785 188.300, 256.814 186.392, 258.534 183.538, 259.683 180.171, 260.087 176.803, 259.683 173.949, 258.534 172.041, 256.814 171.742, 255.906 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 148.258, 255.906 147.959, 256.814 146.051, 258.534 143.197, 259.683 139.829, 260.087 136.462, 259.683 133.608, 258.534 131.700, 256.814 131.030, 254.785 131.030, 250.498 131.078, 250.354 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 218.164, 180.475 221.584, 176.011 225.378, 170.558 228.968, 164.945 231.806, 160.026 236.526, 151.210 238.462, 147.594 228.462, 147.594 218.462, 147.594 208.462, 147.594 198.462, 147.594 188.462, 147.594 178.462, 147.594 168.462, 147.594 158.462, 147.594 148.462, 147.594 138.462, 147.594 128.462, 147.594 118.462, 147.594 108.462, 147.594 98.462, 147.594 88.462, 147.594 81.538, 147.594 86.258, 156.410 88.194, 160.026 91.032, 164.945 94.622, 170.558 98.416, 176.011 101.836, 180.475 106.401, 186.044 108.554, 189.303 110.081, 192.228 111.387, 195.891 112.318, 200.393 112.803, 205.134 112.803, 215.134 112.803, 222.979 114.007, 229.321 116.406, 235.654 118.811, 239.693 122.163, 243.560 125.848, 246.958 129.227, 249.439 131.078, 250.354 133.146, 251.377 138.664, 253.616 143.346, 255.124 147.159, 255.767 148.258, 255.906 152.905, 256.493 160.000, 257.120 167.095, 256.493 171.742, 255.906 172.841, 255.767 176.654, 255.124 181.336, 253.616 186.854, 251.377 188.922, 250.354 190.773, 249.439 194.152, 246.958 197.837, 243.560 201.189, 239.693 203.594, 235.654 205.993, 229.321 207.197, 222.979 207.197, 212.979 207.197, 205.134 207.682, 200.393 208.613, 195.891 209.919, 192.228 211.446, 189.303 213.599, 186.044 218.164, 180.475 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 72.589, 149.749 79.225, 157.230 81.332, 159.606 89.392, 165.524 93.098, 168.245 102.062, 172.678 107.436, 175.336 116.954, 178.402 123.793, 180.604 133.630, 182.403 141.542, 183.849 151.524, 184.441 160.000, 184.944 169.982, 184.352 178.458, 183.849 188.295, 182.051 196.207, 180.604 205.725, 177.538 212.564, 175.336 221.528, 170.903 226.902, 168.245 234.962, 162.327 238.668, 159.606 245.304, 152.125 247.411, 149.749 251.908, 140.817 252.795, 139.053 254.408, 129.184 254.613, 127.930 254.613, 117.930 254.613, 117.477 253.000, 107.608 252.795, 106.354 248.299, 97.422 247.411, 95.658 240.775, 88.177 238.668, 85.801 230.608, 79.883 226.902, 77.162 217.938, 72.729 212.564, 70.071 203.046, 67.006 196.207, 64.803 186.370, 63.005 178.458, 61.558 168.476, 60.966 160.000, 60.463 150.018, 61.055 141.542, 61.558 131.705, 63.356 123.793, 64.803 114.275, 67.869 107.436, 70.071 98.472, 74.504 93.098, 77.162 85.038, 83.080 81.332, 85.801 74.696, 93.283 72.589, 95.658 68.092, 104.591 67.205, 106.354 65.592, 116.223 65.387, 117.477 65.387, 127.477 65.387, 127.930 67.000, 137.799 67.205, 139.053 71.701, 147.985 72.589, 149.749 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 254.613, 117.477 253.000, 127.346 252.795, 128.600 248.299, 137.532 247.411, 139.295 240.775, 146.776 238.668, 149.152 230.608, 155.070 226.902, 157.792 217.938, 162.225 212.564, 164.882 203.046, 167.948 196.207, 170.151 186.370, 171.949 178.458, 173.395 168.476, 173.988 160.000, 174.491 150.018, 173.898 141.542, 173.395 131.705, 171.597 123.793, 170.151 114.275, 167.085 107.436, 164.882 98.472, 160.449 93.098, 157.792 85.038, 151.873 81.332, 149.152 74.696, 141.671 72.589, 139.295 68.092, 130.363 67.205, 128.600 65.592, 118.731 65.387, 117.477 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 89.099, 88.866 89.043, 88.906 82.408, 96.388 81.157, 97.797 76.661, 106.729 76.301, 107.444 74.688, 117.313 74.661, 117.477 74.780, 118.203 74.939, 119.178 76.301, 127.509 80.797, 136.441 81.157, 137.156 87.793, 144.637 89.043, 146.047 97.104, 151.965 99.656, 153.840 108.620, 158.273 112.588, 160.235 122.107, 163.301 127.342, 164.987 137.179, 166.785 143.351, 167.914 153.334, 168.506 160.000, 168.902 169.982, 168.309 176.649, 167.914 186.486, 166.115 192.658, 164.987 202.176, 161.921 207.412, 160.235 216.375, 155.802 220.344, 153.840 228.404, 147.921 230.957, 146.047 237.592, 138.566 238.843, 137.156 243.339, 128.224 243.699, 127.509 245.061, 119.178 245.220, 118.203 245.339, 117.477 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 230.901, 88.866 230.957, 88.906 237.592, 96.388 238.843, 97.797 243.339, 106.729 243.699, 107.444 245.312, 117.313 245.339, 117.477 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 245.220, 118.203 243.699, 108.897 239.203, 99.965 238.843, 99.250 237.001, 95.742 235.191, 93.702 230.901, 88.866 229.299, 87.059 221.238, 81.141 218.934, 79.449 209.970, 75.016 206.304, 73.203 196.785, 70.137 191.895, 68.561 182.058, 66.763 176.260, 65.703 166.277, 65.111 160.000, 64.738 150.018, 65.331 143.740, 65.703 133.903, 67.502 128.105, 68.561 118.587, 71.627 113.696, 73.203 104.732, 77.636 101.066, 79.449 93.006, 85.367 90.701, 87.059 89.099, 88.866 84.809, 93.702 82.999, 95.742 81.157, 99.250 76.661, 108.182 76.301, 108.897 74.780, 118.203 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,9 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 148.145, 100.400 158.097, 99.419 160.000, 99.232 169.952, 100.212 171.855, 100.400 181.425, 103.302 183.255, 103.858 192.074, 108.572 193.761, 109.473 201.491, 115.817 202.969, 117.031 209.313, 124.761 210.527, 126.239 215.241, 135.058 216.142, 136.745 219.045, 146.314 219.600, 148.145 220.581, 158.097 220.768, 160.000 219.788, 169.952 219.600, 171.855 216.698, 181.425 216.142, 183.255 211.428, 192.074 210.527, 193.761 204.183, 201.491 202.969, 202.969 195.239, 209.313 193.761, 210.527 184.942, 215.241 183.255, 216.142 173.686, 219.045 171.855, 219.600 165.000, 220.276 165.000, 230.276 165.000, 240.276 165.000, 250.276 165.000, 260.000 175.000, 260.000 185.000, 260.000 195.000, 260.000 205.000, 260.000 215.000, 260.000 220.000, 260.000 229.877, 258.436 232.361, 258.042 241.271, 253.502 243.511, 252.361 250.582, 245.290 252.361, 243.511 256.901, 234.601 258.042, 232.361 259.607, 222.484 260.000, 220.000 260.000, 210.000 260.000, 200.000 260.000, 190.000 260.000, 180.000 260.000, 170.000 260.000, 160.000 260.000, 150.000 260.000, 140.000 260.000, 130.000 260.000, 120.000 260.000, 110.000 260.000, 100.000 258.436, 90.123 258.042, 87.639 253.502, 78.729 252.361, 76.489 245.290, 69.418 243.511, 67.639 234.601, 63.099 232.361, 61.958 222.484, 60.393 220.000, 60.000 210.000, 60.000 200.000, 60.000 190.000, 60.000 180.000, 60.000 170.000, 60.000 160.000, 60.000 150.000, 60.000 140.000, 60.000 130.000, 60.000 120.000, 60.000 110.000, 60.000 100.000, 60.000 90.123, 61.564 87.639, 61.958 78.729, 66.498 76.489, 67.639 69.418, 74.710 67.639, 76.489 63.099, 85.399 61.958, 87.639 60.393, 97.516 60.000, 100.000 60.000, 110.000 60.000, 120.000 60.000, 130.000 60.000, 140.000 60.000, 150.000 60.000, 160.000 60.000, 170.000 60.000, 180.000 60.000, 190.000 60.000, 200.000 60.000, 210.000 60.000, 220.000 61.564, 229.877 61.958, 232.361 66.498, 241.271 67.639, 243.511 74.710, 250.582 76.489, 252.361 85.399, 256.901 87.639, 258.042 97.516, 259.607 100.000, 260.000 110.000, 260.000 120.000, 260.000 130.000, 260.000 140.000, 260.000 150.000, 260.000 155.000, 260.000 155.000, 250.000 155.000, 240.000 155.000, 230.000 155.000, 220.276 148.145, 219.600 138.575, 216.698 136.745, 216.142 127.926, 211.428 126.239, 210.527 118.509, 204.183 117.031, 202.969 110.687, 195.239 109.473, 193.761 104.759, 184.942 103.858, 183.255 100.955, 173.686 100.400, 171.855 99.419, 161.903 99.232, 160.000 100.212, 150.048 100.400, 148.145 103.302, 138.575 103.858, 136.745 108.572, 127.926 109.473, 126.239 115.817, 118.509 117.031, 117.031 124.761, 110.687 126.239, 109.473 135.058, 104.759 136.745, 103.858 146.314, 100.955 148.145, 100.400 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 202.172, 188.178 206.859, 179.410 209.745, 169.895 210.720, 160.000 209.745, 150.105 206.859, 140.590 202.172, 131.822 195.864, 124.136 188.178, 117.828 179.410, 113.141 169.895, 110.255 160.000, 109.280 150.105, 110.255 140.590, 113.141 131.822, 117.828 124.136, 124.136 117.828, 131.822 113.141, 140.590 110.255, 150.105 109.280, 160.000 110.255, 169.895 113.141, 179.410 117.828, 188.178 124.136, 195.864 131.822, 202.172 140.590, 206.859 150.105, 209.745 160.000, 210.720 169.895, 209.745 179.410, 206.859 188.178, 202.172 195.864, 195.864 202.172, 188.178 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,9 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 179.499, 244.450 178.019, 248.678 175.637, 252.470 172.470, 255.637 168.678, 258.019 164.450, 259.499 160.000, 260.000 170.000, 260.000 180.000, 260.000 184.450, 259.499 188.678, 258.019 192.470, 255.637 195.637, 252.470 198.019, 248.678 199.499, 244.450 200.000, 240.000 200.000, 230.000 200.000, 220.000 200.000, 210.000 200.000, 200.000 200.000, 190.000 200.000, 180.000 200.000, 170.000 200.000, 160.000 200.000, 150.000 200.000, 140.000 200.000, 130.000 200.000, 120.000 200.000, 110.000 200.000, 100.000 200.000, 90.000 200.000, 80.000 199.499, 75.550 198.019, 71.322 195.637, 67.530 192.470, 64.363 188.678, 61.981 184.450, 60.501 180.000, 60.000 170.000, 60.000 160.000, 60.000 164.450, 60.501 168.678, 61.981 172.470, 64.363 175.637, 67.530 178.019, 71.322 179.499, 75.550 180.000, 80.000 180.000, 90.000 180.000, 100.000 180.000, 110.000 180.000, 120.000 180.000, 130.000 180.000, 140.000 180.000, 150.000 180.000, 160.000 180.000, 170.000 180.000, 180.000 180.000, 190.000 180.000, 200.000 180.000, 210.000 180.000, 220.000 180.000, 230.000 180.000, 240.000 179.499, 244.450 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 140.000, 260.000 150.000, 260.000 160.000, 260.000 164.450, 259.499 168.678, 258.019 172.470, 255.637 175.637, 252.470 178.019, 248.678 179.499, 244.450 180.000, 240.000 180.000, 230.000 180.000, 220.000 180.000, 210.000 180.000, 200.000 180.000, 190.000 180.000, 180.000 180.000, 170.000 180.000, 160.000 180.000, 150.000 180.000, 140.000 180.000, 130.000 180.000, 120.000 180.000, 110.000 180.000, 100.000 180.000, 90.000 180.000, 80.000 179.499, 75.550 178.019, 71.322 175.637, 67.530 172.470, 64.363 168.678, 61.981 164.450, 60.501 160.000, 60.000 150.000, 60.000 140.000, 60.000 135.550, 60.501 131.322, 61.981 127.530, 64.363 124.363, 67.530 121.981, 71.322 120.501, 75.550 120.000, 80.000 120.000, 90.000 120.000, 100.000 120.000, 110.000 120.000, 120.000 120.000, 130.000 120.000, 140.000 120.000, 150.000 120.000, 160.000 120.000, 170.000 120.000, 180.000 120.000, 190.000 120.000, 200.000 120.000, 210.000 120.000, 220.000 120.000, 230.000 120.000, 240.000 120.501, 244.450 121.981, 248.678 124.363, 252.470 127.530, 255.637 131.322, 258.019 135.550, 259.499 140.000, 260.000 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,10 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 90.402, 270.000 90.402, 280.000 90.706, 283.473 91.608, 286.840 93.081, 290.000 95.081, 292.856 97.546, 295.321 100.402, 297.321 103.561, 298.794 106.929, 299.696 110.402, 300.000 120.402, 300.000 130.402, 300.000 140.402, 300.000 150.402, 300.000 160.402, 300.000 170.402, 300.000 180.402, 300.000 190.402, 300.000 200.402, 300.000 210.402, 300.000 220.402, 300.000 229.598, 300.000 229.598, 290.000 229.294, 286.527 228.392, 283.160 226.919, 280.000 224.919, 277.144 222.454, 274.679 219.598, 272.680 216.439, 271.206 213.071, 270.304 209.598, 270.000 199.598, 270.000 189.598, 270.000 179.598, 270.000 169.598, 270.000 159.598, 270.000 149.598, 270.000 139.598, 270.000 129.598, 270.000 119.598, 270.000 109.598, 270.000 99.598, 270.000 90.402, 270.000 " />
<path style="fill:#0000ff;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 162.071, 240.000 155.000, 232.929 155.000, 222.929 155.000, 212.929 155.000, 202.929 155.000, 192.929 155.000, 182.929 155.000, 172.929 155.000, 162.929 155.000, 152.929 155.000, 142.929 155.000, 132.929 155.000, 122.929 155.000, 112.929 155.000, 102.929 155.000, 92.929 155.000, 82.929 155.000, 72.929 155.000, 62.929 155.000, 52.929 155.000, 42.929 155.000, 32.929 155.000, 22.929 155.000, 20.000 145.000, 20.000 138.189, 20.000 138.189, 30.000 138.189, 40.000 138.189, 50.000 138.189, 60.000 138.189, 70.000 138.189, 80.000 138.189, 90.000 138.189, 100.000 138.189, 110.000 138.189, 120.000 138.189, 130.000 138.189, 140.000 138.189, 150.000 138.189, 160.000 138.189, 170.000 138.189, 180.000 138.189, 190.000 138.189, 200.000 138.189, 210.000 138.189, 220.000 138.189, 230.000 138.189, 240.000 138.189, 245.000 138.189, 250.000 138.381, 251.951 138.950, 253.827 139.874, 255.556 141.118, 257.071 142.633, 258.315 144.362, 259.239 146.238, 259.808 148.189, 260.000 150.000, 260.000 160.000, 260.000 170.000, 260.000 180.000, 260.000 181.811, 260.000 181.811, 250.000 181.811, 240.000 171.811, 240.000 162.071, 240.000 " />
<path style="fill:#ff0000;fill-opacity:1" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 155.000, 20.000 155.000, 30.000 155.000, 40.000 155.000, 50.000 155.000, 60.000 155.000, 70.000 155.000, 80.000 155.000, 90.000 155.000, 100.000 155.000, 110.000 155.000, 120.000 155.000, 130.000 155.000, 140.000 155.000, 150.000 155.000, 160.000 155.000, 170.000 155.000, 180.000 155.000, 190.000 155.000, 200.000 155.000, 210.000 155.000, 220.000 155.000, 230.000 155.000, 232.929 162.071, 240.000 172.071, 240.000 181.811, 240.000 181.811, 230.000 181.811, 220.000 181.811, 210.000 181.811, 200.000 181.811, 190.000 181.811, 180.000 181.811, 170.000 181.811, 160.000 181.811, 150.000 181.811, 140.000 181.811, 130.000 181.811, 120.000 181.811, 110.000 181.811, 100.000 181.811, 90.000 181.811, 80.000 181.811, 70.000 181.811, 60.000 181.811, 50.000 181.811, 40.000 181.811, 30.000 181.619, 28.049 181.050, 26.173 180.126, 24.444 178.882, 22.929 177.367, 21.685 175.638, 20.761 173.762, 20.192 171.811, 20.000 161.811, 20.000 155.000, 20.000 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,8 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#ff0000;fill-opacity:1" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 159.162, 60.200 150.502, 65.200 141.842, 70.200 133.181, 75.200 124.521, 80.200 115.861, 85.200 107.201, 90.200 98.540, 95.200 89.880, 100.200 81.220, 105.200 73.989, 109.374 73.844, 109.473 73.703, 109.598 73.570, 109.746 73.451, 109.912 73.347, 110.090 73.264, 110.277 73.202, 110.465 73.164, 110.650 73.151, 110.826 73.151, 120.826 73.151, 130.826 73.151, 140.826 73.151, 150.826 73.151, 160.826 73.151, 170.826 73.151, 180.826 73.151, 190.826 73.151, 200.826 73.151, 209.174 73.164, 209.350 73.202, 209.535 73.264, 209.723 73.347, 209.910 73.451, 210.088 73.570, 210.254 73.703, 210.402 73.844, 210.527 73.989, 210.626 82.650, 215.626 91.310, 220.626 99.970, 225.626 108.630, 230.626 117.291, 235.626 125.951, 240.626 134.611, 245.626 143.271, 250.626 151.932, 255.626 159.162, 259.800 159.320, 259.877 159.499, 259.937 159.693, 259.977 159.897, 259.998 160.103, 259.998 160.307, 259.977 160.501, 259.937 160.680, 259.877 160.838, 259.800 169.498, 254.800 178.158, 249.800 186.819, 244.800 195.479, 239.800 204.139, 234.800 212.799, 229.800 221.460, 224.800 230.120, 219.800 238.780, 214.800 246.011, 210.626 246.156, 210.527 246.297, 210.402 246.430, 210.254 246.549, 210.088 246.653, 209.910 246.736, 209.723 246.798, 209.535 246.836, 209.350 246.849, 209.174 246.849, 199.174 246.849, 189.174 246.849, 179.174 246.849, 169.174 246.849, 159.174 246.849, 149.174 246.849, 139.174 246.849, 129.174 246.849, 119.174 246.849, 110.826 246.836, 110.650 246.798, 110.465 246.736, 110.277 246.653, 110.090 246.549, 109.912 246.430, 109.746 246.297, 109.598 246.156, 109.473 246.011, 109.374 237.350, 104.374 228.690, 99.374 220.030, 94.374 211.370, 89.374 202.709, 84.374 194.049, 79.374 185.389, 74.374 176.729, 69.374 168.068, 64.374 160.838, 60.200 160.680, 60.123 160.501, 60.063 160.307, 60.023 160.103, 60.002 159.897, 60.002 159.693, 60.023 159.499, 60.063 159.320, 60.123 159.162, 60.200 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,8 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#ff0000;fill-opacity:1" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 60.000, 71.854 60.000, 81.854 60.000, 91.854 60.000, 101.854 60.000, 111.854 60.000, 121.854 60.000, 131.854 60.000, 141.854 60.000, 151.854 60.000, 161.854 60.000, 171.854 60.000, 181.854 60.000, 191.854 60.000, 201.854 60.000, 211.854 60.000, 221.854 60.000, 231.854 60.000, 241.854 60.000, 251.854 60.000, 258.288 60.021, 258.556 60.084, 258.817 60.187, 259.065 60.327, 259.294 60.501, 259.499 60.706, 259.673 60.935, 259.813 61.183, 259.916 61.444, 259.979 61.712, 260.000 71.712, 260.000 81.712, 260.000 91.712, 260.000 101.712, 260.000 111.712, 260.000 121.712, 260.000 131.712, 260.000 141.712, 260.000 151.712, 260.000 161.712, 260.000 171.712, 260.000 181.712, 260.000 191.712, 260.000 201.712, 260.000 211.712, 260.000 221.712, 260.000 231.712, 260.000 241.712, 260.000 248.146, 260.000 250.000, 259.854 251.809, 259.420 253.527, 258.708 255.113, 257.736 256.528, 256.528 257.736, 255.113 258.708, 253.527 259.420, 251.809 259.854, 250.000 260.000, 248.146 260.000, 238.146 260.000, 228.146 260.000, 218.146 260.000, 208.146 260.000, 198.146 260.000, 188.146 260.000, 178.146 260.000, 168.146 260.000, 158.146 260.000, 148.146 260.000, 138.146 260.000, 128.146 260.000, 118.146 260.000, 108.146 260.000, 98.146 260.000, 88.146 260.000, 78.146 260.000, 68.146 260.000, 61.712 259.979, 61.444 259.916, 61.183 259.813, 60.935 259.673, 60.706 259.499, 60.501 259.294, 60.327 259.065, 60.187 258.817, 60.084 258.556, 60.021 258.288, 60.000 248.288, 60.000 238.288, 60.000 228.288, 60.000 218.288, 60.000 208.288, 60.000 198.288, 60.000 188.288, 60.000 178.288, 60.000 168.288, 60.000 158.288, 60.000 148.288, 60.000 138.288, 60.000 128.288, 60.000 118.288, 60.000 108.288, 60.000 98.288, 60.000 88.288, 60.000 78.288, 60.000 71.854, 60.000 70.000, 60.146 68.191, 60.580 66.473, 61.292 64.887, 62.264 63.472, 63.472 62.264, 64.887 61.292, 66.473 60.580, 68.191 60.146, 70.000 60.000, 71.854 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,8 @@
<?xml version='1.0' encoding='ascii'?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="320" height="320">
<g id="View Layer_LineSet" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="lineset" inkscape:label="View Layer_LineSet">
<g xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:groupmode="layer" id="strokes" inkscape:label="strokes">
<path style="fill:#ff0000;fill-opacity:1" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 159.555, 60.312 154.555, 68.972 149.555, 77.633 144.555, 86.293 139.555, 94.953 134.555, 103.613 129.555, 112.274 124.555, 120.934 119.555, 129.594 114.555, 138.254 109.555, 146.915 104.555, 155.575 99.555, 164.235 94.555, 172.895 89.555, 181.556 84.555, 190.216 79.555, 198.876 74.555, 207.537 69.555, 216.197 64.555, 224.857 59.555, 233.517 54.555, 242.178 49.555, 250.838 44.445, 259.688 44.382, 259.822 44.347, 259.951 44.342, 260.073 44.368, 260.183 44.423, 260.278 44.505, 260.355 44.613, 260.412 44.743, 260.447 44.890, 260.459 54.890, 260.459 64.890, 260.459 74.890, 260.459 84.890, 260.459 94.890, 260.459 104.890, 260.459 114.890, 260.459 124.890, 260.459 134.890, 260.459 144.890, 260.459 154.890, 260.459 164.890, 260.459 174.890, 260.459 184.890, 260.459 194.890, 260.459 204.890, 260.459 214.890, 260.459 224.890, 260.459 234.890, 260.459 244.890, 260.459 254.890, 260.459 264.890, 260.459 275.110, 260.459 275.257, 260.447 275.387, 260.412 275.495, 260.355 275.577, 260.278 275.632, 260.183 275.658, 260.073 275.653, 259.951 275.618, 259.822 275.555, 259.688 270.555, 251.028 265.555, 242.367 260.555, 233.707 255.555, 225.047 250.555, 216.387 245.555, 207.726 240.555, 199.066 235.555, 190.406 230.555, 181.746 225.555, 173.085 220.555, 164.425 215.555, 155.765 210.555, 147.105 205.555, 138.444 200.555, 129.784 195.555, 121.124 190.555, 112.463 185.555, 103.803 180.555, 95.143 175.555, 86.483 170.555, 77.822 165.555, 69.162 160.445, 60.312 160.361, 60.190 160.266, 60.095 160.163, 60.030 160.055, 59.997 159.945, 59.997 159.837, 60.030 159.734, 60.095 159.639, 60.190 159.555, 60.312 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,22 @@
<script lang="ts">
import type { State } from "$lib/models/device";
import BFormLine from "../bforms/BFormLine.svelte";
import BFormTemperatureOption from "../bforms/BFormTemperatureOption.svelte";
import BFormColorOption from "../bforms/BFormColorOption.svelte";
import BFormIntensityOption from "../bforms/BFormIntensityOption.svelte";
import BFormPowerOption from "../bforms/BFormPowerOption.svelte";
import BFormDeleteOption from "../bforms/BFormDeleteOption.svelte";
export let value: State;
export let deletable: boolean = false;
</script>
<BFormLine>
<BFormPowerOption bind:value={value.power} />
<BFormIntensityOption bind:value={value.intensity} />
<BFormColorOption bind:value={value.color} />
<BFormTemperatureOption bind:value={value.temperature} />
{#if deletable}
<BFormDeleteOption on:delete />
{/if}
</BFormLine>
@@ -0,0 +1,58 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import Icon from "../Icon.svelte";
export let label: string;
export let add: boolean = false;
export let nopad: boolean = false;
const dispatch = createEventDispatcher();
function onAdd() {
dispatch("add");
}
</script>
<div class="block" class:nopad>
<div class="label">
<div class="text">{label}</div>
{#if add}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="add-button" on:click={onAdd}><Icon name="plus" /></div>
{/if}
</div>
<slot></slot>
</div>
<style lang="sass">
@import "$lib/css/colors.sass"
div.block
padding-left: 1ch
margin-left: 0.25ch
border-left: 2px solid $color-main2
margin-bottom: 0.5em
&.nopad
margin-left: 0
div.label
color: $color-main5
font-size: 0.666em
padding-top: 0.1em
padding-left: 0.3ch
display: flex
flex-direction: row
div.add-button
color: $color-green-dark
margin-top: 0.1em
margin-left: 0.5ch
font-size: 0.9em
line-height: 1em
cursor: pointer
&:hover
color: $color-green
</style>
@@ -0,0 +1,31 @@
<script lang="ts">
import type { ScriptCondition } from "$lib/models/script";
import BFormOption from "../bforms/BFormOption.svelte";
import BFormParameter from "../bforms/BFormParameter.svelte";
export let value: Required<ScriptCondition>
</script>
<BFormOption state unclickable icon="question">
<BFormParameter narrowest type="checkbox" bind:value={value.not} label="Not" />
<BFormParameter narrow type="select" bind:value={value.scope} label="Scope" options={[
{ label: "Match", value: "match" },
{ label: "Devices", value: "device" },
{ label: "Global", value: "global" },
]} />
<BFormParameter type="text" bind:value={value.key} label="Key" />
<BFormParameter narrow type="select" bind:value={value.op} label="Op" options={[
{ label: "==", value: "eq" },
{ label: "!==", value: "neq" },
{ label: ">", value: "gt" },
{ label: ">=", value: "gte" },
{ label: "<", value: "lt" },
{ label: "<=", value: "lte" },
{ label: "contains", value: "contains" },
{ label: "exists", value: "exists" },
{ label: "numerical", value: "numerical" },
]} />
{#if value.op !== "numerical" && value.op !== "exists"}
<BFormParameter type="text" bind:value={value.value} label="Value" />
{/if}
</BFormOption>
@@ -0,0 +1,74 @@
<script lang="ts">
import type { EffectRaw } from "$lib/models/assignment";
import BFormOption from "../bforms/BFormOption.svelte";
import BFormParameter from "../bforms/BFormParameter.svelte";
export let value: EffectRaw
</script>
<BFormOption unclickable state icon="play">
<BFormParameter
narrow label="Effect" type="select" bind:value={value.kind}
options={[
{ value:"solid", label:"Solid" },
{ value:"gradient", label:"Gradient" },
{ value:"pattern", label:"Pattern" },
{ value:"random", label:"Random" },
{ value:"vrange", label:"V. Range" },
]}
/>
</BFormOption>
{#if value.kind !== "manual" && value.kind !== "vrange"}
<BFormOption alwaysOpen unclickable state={value.animationMs > 0} icon="clock_rotate_left">
<BFormParameter
narrow label="Milliseconds" type="number"
min={0} max={10000000} step={100}
bind:value={value.animationMs}
/>
{#if value.kind === "solid"}
<BFormParameter
narrower label="Interpolate" type="number"
min={0} max={10} step={1}
bind:value={value.interleave}
/>
{/if}
{#if value.kind === "gradient"}
<BFormParameter
narrower label="Reverse" type="checkbox"
bind:value={value.reverse}
/>
{/if}
{#if value.kind === "gradient" || value.kind === "random"}
<BFormParameter
narrower label="Interpolate" type="checkbox"
bind:value={value.interpolate}
/>
{/if}
</BFormOption>
{/if}
{#if value.kind === "vrange"}
<BFormOption unclickable state icon="temperature_half">
<BFormParameter
label="Variable" type="select" bind:value={value.variable}
options={[
{ value:"motion.min", label:"Min Motion" },
{ value:"motion.max", label:"Max Motion" },
{ value:"motion.avg", label:"Avg Motion" },
{ value:"temperature.min", label:"Min Temperature" },
{ value:"temperature.max", label:"Max Temperature" },
{ value:"temperature.avg", label:"Avg Temperature" },
]}
/>
<BFormParameter
narrower label="Min" type="number"
min={0} max={1000} step={1}
bind:value={value.min}
/>
<BFormParameter
narrower label="Max" type="number"
min={0} max={1000} step={1}
bind:value={value.max}
/>
</BFormOption>
{/if}
@@ -0,0 +1,123 @@
<script lang="ts" context="module">
const KIND_ICONS: Record<ScriptLineEditable["kind"], IconName> = {
"if": "chevron_right",
"select": "filter",
"assign": "swatch_book",
"set": "pen",
}
</script>
<script lang="ts">
import { BLANK_STATE, copyState } from "$lib/models/device";
import { toEditableScriptLine, type ScriptLineEditable } from "$lib/models/script";
import { createEventDispatcher } from "svelte";
import type { IconName } from "../Icon.svelte";
import BFormButtonOption from "../bforms/BFormButtonOption.svelte";
import BFormDeleteOption from "../bforms/BFormDeleteOption.svelte";
import BFormLine from "../bforms/BFormLine.svelte";
import BFormOption from "../bforms/BFormOption.svelte";
import BFormParameter from "../bforms/BFormParameter.svelte";
import ScriptAssignmentState from "./ScriptAssignmentState.svelte";
import ScriptBlock from "./ScriptBlock.svelte";
import ScriptCondition from "./ScriptCondition.svelte";
import ScriptEffect from "./ScriptEffect.svelte";
import ScriptLineBlock from "./ScriptLineBlock.svelte";
import ScriptMatch from "./ScriptMatch.svelte";
import ScriptSet from "./ScriptSet.svelte";
export let value: ScriptLineEditable;
export let first: boolean = false;
export let last: boolean = false;
const dispatch = createEventDispatcher();
function deleteState(index: number) {
value.effect.states = [
...value.effect.states.slice(0, index),
...value.effect.states.slice(index + 1),
];
if (value.effect.states.length === 0) {
value.effect.states = [{...BLANK_STATE}];
}
}
function addState() {
value.effect.states = [
...value.effect.states,
copyState(value.effect.states[value.effect.states.length - 1]),
];
}
function addThen() {
value.then = [
...value.then,
toEditableScriptLine({if: {
condition: { scope: "global", key: "", op: "eq" },
then: [],
else: [],
}})
]
}
function addElse() {
value.else = [
...value.else,
toEditableScriptLine({if: {
condition: { scope: "global", key: "", op: "eq" },
then: [],
else: [],
}})
]
}
</script>
<BFormLine>
<BFormOption unclickable state icon={KIND_ICONS[value.kind]}>
<BFormParameter
narrow
label="Op"
type="select"
bind:value={value.kind}
options={[
{label: "If", value: "if"},
{label: "Assign", value: "assign"},
{label: "Set", value: "set"},
{label: "Select", value: "select"},
]}
/>
</BFormOption>
{#if value.kind === "assign" || value.kind === "select"}
<ScriptMatch bind:matchKind={value.matchKind} bind:matchValue={value.matchValue} />
{/if}
{#if value.kind === "assign"}
<ScriptEffect bind:value={value.effect} />
{/if}
{#if value.kind === "if"}
<ScriptCondition bind:value={value.condition} />
{/if}
{#if value.kind === "set"}
<ScriptSet bind:key={value.key} bind:scope={value.scope} bind:value={value.value} />
{/if}
<BFormDeleteOption on:delete />
{#if !first}
<BFormButtonOption icon="chevron_up" on:click={() => dispatch("up")} />
{/if}
{#if !last}
<BFormButtonOption icon="chevron_down" on:click={() => dispatch("down")} />
{/if}
</BFormLine>
{#if value.kind === "assign"}
<ScriptBlock on:add={addState} add label="States">
{#each value.effect.states as state, i}
<ScriptAssignmentState bind:value={state} deletable on:delete={() => deleteState(i)} />
{/each}
</ScriptBlock>
{/if}
{#if value.kind === "select" || value.kind === "if"}
<ScriptLineBlock on:add={addThen} add label="Then" bind:value={value.then} />
{/if}
{#if value.kind === "if"}
<ScriptLineBlock on:add={addElse} add label="Else" bind:value={value.else} />
{/if}
@@ -0,0 +1,40 @@
<script lang="ts">
import type { ScriptLineEditable } from "$lib/models/script";
import ScriptBlock from "./ScriptBlock.svelte";
import ScriptLine from "./ScriptLine.svelte";
export let label: string;
export let value: ScriptLineEditable[];
export let add: boolean = false;
function moveUp(i: number) {
value = [...value];
const temp = value[i];
value[i] = value[i - 1];
value[i - 1] = temp;
}
function moveDown(i: number) {
value = [...value];
const temp = value[i];
value[i] = value[i + 1];
value[i + 1] = temp;
}
function deleteLine(i: number) {
value = [...value.slice(0, i), ...value.slice(i + 1)];
}
</script>
<ScriptBlock on:add add={add} label={label}>
{#each value as line, i}
<ScriptLine
first={i === 0}
last={i === value.length - 1}
bind:value={line}
on:up={() => moveUp(i)}
on:down={() => moveDown(i)}
on:delete={() => deleteLine(i)}
/>
{/each}
</ScriptBlock>
@@ -0,0 +1,38 @@
<script lang="ts" context="module">
const MATCH_ICONS: Record<"all" | "raw" | "name" | "role", IconName> = {
name: "signature",
raw: "cog",
role: "masks_theater",
all: "asterisk",
}
</script>
<script lang="ts">
import type { IconName } from "../Icon.svelte";
import BFormOption from "../bforms/BFormOption.svelte";
import BFormParameter from "../bforms/BFormParameter.svelte";
export let matchKind: "all" | "name" | "role" | "raw";
export let matchValue: string;
function toggleMatchKind() {
switch (matchKind) {
case "all": matchKind = "name"; break;
case "name": matchKind = "role"; break;
case "role": matchKind = "raw"; break;
case "raw": matchKind = "all"; break;
}
}
</script>
<BFormOption on:click={toggleMatchKind} state icon={MATCH_ICONS[matchKind]}>
{#if matchKind !== "all"}
<BFormParameter
wide={matchKind === "raw"}
type="text"
label="Match ({matchKind})"
bind:value={matchValue}
/>
{/if}
</BFormOption>
@@ -0,0 +1,18 @@
<script lang="ts">
import BFormOption from "../bforms/BFormOption.svelte";
import BFormParameter from "../bforms/BFormParameter.svelte";
export let scope: string;
export let key: string;
export let value: string;
</script>
<BFormOption state unclickable icon="note_sticky">
<BFormParameter narrow type="select" bind:value={scope} label="Scope" options={[
{ label: "Match", value: "match" },
{ label: "Devices", value: "device" },
{ label: "Global", value: "global" },
]} />
<BFormParameter type="text" bind:value={key} label="Key" />
<BFormParameter type="text" bind:value={value} label="Value" />
</BFormOption>
@@ -0,0 +1,31 @@
<script lang="ts" context="module">
import type { DeviceEditOp } from "$lib/models/device";
import type Script from "$lib/models/script";
import type { ScriptLine } from "$lib/models/script";
import { getContext, setContext } from "svelte";
import { writable, type Writable } from "svelte/store";
const ctxKey = {ctx: "ModalContext"};
export type ModalSelection =
| { kind: "closed" }
| { kind: "device.edit", op: DeviceEditOp }
| { kind: "script.edit" }
| { kind: "trigger.edit" }
export interface ModalContextState {
modal: Writable<ModalSelection>
}
export function getModalContext(): ModalContextState {
return getContext(ctxKey)
}
</script>
<script lang="ts">
setContext<ModalContextState>(ctxKey, {
modal: writable({kind: "closed"}),
});
</script>
<slot></slot>
@@ -0,0 +1,129 @@
<script lang="ts" context="module">
import { getContext, setContext } from "svelte";
import { writable, type Readable } from "svelte/store";
import { getStateContext } from "./StateContext.svelte";
const ctxKey = {ctx: "SelectContext"};
export interface SelectContextData {
toggleSelection(id: string): void
toggleMultiSelection(ids: string[]): void
selectedMasks: Readable<string[]>
selectedList: Readable<string[]>
selectedMap: Readable<{[id:string]: boolean}>
}
export function getSelectedContext(): SelectContextData {
return getContext(ctxKey) as SelectContextData;
}
</script>
<script lang="ts">
const selectedList = writable<string[]>([]);
const selectedMap = writable<{[id:string]: boolean}>({});
const selectedMasks = writable<string[]>([]);
const {state, deviceList} = getStateContext();
function toggleSelection(id: string) {
selectedMap.update(m => ({...m, [id]: !m[id]}));
}
function toggleMultiSelection(ids: string[]) {
selectedMap.update(m => {
if (ids.find(i => !m[i])) {
return ids.reduce((m, i) => ({...m, [i]: true}), m)
} else {
return ids.reduce((m, i) => ({...m, [i]: false}), m)
}
});
}
// Effect: remove non-existent devices
$: {
const newMap = {...$selectedMap};
for (const id in newMap) {
if (!Object.hasOwn(newMap, id)) {
continue;
}
if (!$state?.devices[id]) {
delete newMap[id];
}
}
$selectedMap = newMap;
}
// Effect: sync list
$: $selectedList = Object.keys($selectedMap).filter(id => !!$selectedMap[id]);
$: {
const firstDevice = $deviceList.find(d => $selectedMap[d.id]);
const nextMasks: string[] = [];
if (firstDevice != null) {
// Common aliases first
for (const alias of firstDevice.aliases) {
if (alias.startsWith("lucifer:name") || alias.startsWith("lucifer:icon:")) {
continue;
}
let qualified = true;
for (const device of $deviceList) {
const isSelected = $selectedMap[device.id] || false;
const hasAlias = device.aliases.includes(alias);
if (isSelected !== hasAlias) {
qualified = false;
break
}
}
if (qualified) {
nextMasks.push(alias);
}
}
let shortestIdPrefix = firstDevice.id;
let shortestNamePrefix = firstDevice.name;
for (const device of $deviceList) {
if (!$selectedMap[device.id]) {
continue
}
let longestIdMatch = 0;
for (let i = 1; i <= shortestIdPrefix.length; ++i) {
if (device.id.startsWith(shortestIdPrefix.slice(0, i))) {
longestIdMatch = i;
}
}
let longestNameMatch = 0;
for (let i = 1; i <= shortestNamePrefix.length; ++i) {
if (device.name.startsWith(shortestNamePrefix.slice(0, i))) {
longestNameMatch = i;
}
}
shortestIdPrefix = shortestIdPrefix.substring(0, longestIdMatch);
shortestNamePrefix = shortestNamePrefix.substring(0, longestNameMatch);
}
nextMasks.push(`${shortestIdPrefix}{${$selectedList.map(s => s.substring(shortestIdPrefix.length)).sort().join(",")}}`)
nextMasks.push(`lucifer:name:${shortestNamePrefix}{${$selectedList.map(id => $state.devices[id].name).map(s => s.substring(shortestNamePrefix.length)).sort().join(",")}}`)
}
$selectedMasks = nextMasks.map(m => m.endsWith("{}") ? m.slice(0, -2) : m);
}
setContext(ctxKey, {
selectedList: {subscribe: selectedList.subscribe},
selectedMap: {subscribe: selectedMap.subscribe},
selectedMasks: {subscribe: selectedMasks.subscribe},
toggleSelection,
toggleMultiSelection,
});
</script>
<slot></slot>
@@ -0,0 +1,242 @@
<script lang="ts" context="module">
import { dev } from "$app/environment";
import { fetchUIState } from "$lib/client/lucifer";
import type { DeviceIconName } from "$lib/components/DeviceIcon.svelte";
import type Assignment from "$lib/models/assignment";
import type Device from "$lib/models/device";
import type { UIStatePatch, UIStatePatch2 } from "$lib/models/uistate";
import type UIState from "$lib/models/uistate";
import { getContext, onMount, setContext } from "svelte";
import { derived, writable, type Readable } from "svelte/store";
const ctxKey = {ctx: "StateContext"};
export interface StateContextData {
reload(): Promise<void>
state: Readable<UIState>
error: Readable<string | null>
deviceList: Readable<Device[]>
assignmentList: Readable<Assignment[]>
roomList: Readable<{name: string, groups: {name: string, devices: Device[]}[], devices: Device[]}[]>
maskList: Readable<string[]>
}
export function getStateContext(): StateContextData {
return getContext(ctxKey) as StateContextData;
}
</script>
<script lang="ts">
const state = writable<UIState>({
assignments: {},
devices: {},
scripts: {},
triggers: {},
});
const error = writable<string | null>(null);
let socket: WebSocket | null = null;
const deviceList = derived(state, state => {
if (state == null) {
return [];
}
return Object.keys(state.devices)
.map(k => state.devices[k])
.sort((a,b) => a.name.localeCompare(b.name));
});
const maskList = derived(state, state => {
return Object.keys(state.devices)
.flatMap(k => state.devices[k].aliases.filter(a => ["tag", "room", "role", "name"].find(p => a.startsWith(`lucifer:${p}:`))))
.sort()
.filter((e, i, a) => a[i - 1] !== e)
});
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 roomMap: Record<string, Device[]> = {};
const groupMap: Record<string, string> = {};
for (const id in state.devices) {
if (!state.devices.hasOwnProperty(id)) {
continue;
}
const roomAlias = state.devices[id].aliases.find(a => a.startsWith("lucifer:room:"));
const groupAlias = state.devices[id].aliases.find(a => a.startsWith("lucifer:group:"));
const roomName = roomAlias?.slice("lucifer:room:".length) || "Unroomed";
const groupName = groupAlias?.slice("lucifer:group:".length) || "";
if (!roomMap[roomName]) {
roomMap[roomName] = [];
}
if (groupAlias) {
groupMap[id] = groupName;
}
roomMap[roomName].push(state.devices[id]);
}
return Object.keys(roomMap)
.map(k => ({
name: k,
devices: roomMap[k].filter(d => !groupMap[d.id]).sort((a,b) => a.name.localeCompare(b.name)),
groups: roomMap[k].filter(d => !!groupMap[d.id]).reduce<{name: string, devices: Device[]}[]>((p, d) => {
const g = p.find(e => e.name === groupMap[d.id]);
if (g == null) {
p.push({name:groupMap[d.id], devices: [d]})
} else {
g.devices.push(d);
}
return p;
}, [])
.map(g => ({...g, devices: g.devices.sort((a,b) => a.name.localeCompare(b.name))}))
.sort((a,b) => a.name.localeCompare(b.name))
}))
.sort((a,b) => a.name.localeCompare(b.name));
});
async function reload() {
error.set(null);
try {
const newState = await fetchUIState();
state.set(newState);
} catch(err) {
error.set(err?.toString ? err.toString() : String(err))
}
}
async function connectSocket() {
let url = `ws://${window.location.host}/subscribe-simple`;
if (import.meta.env.VITE_LUCIFER4_BACKEND_URL != null) {
url = import.meta.env.VITE_LUCIFER4_BACKEND_URL.replace("http", "ws") +"/subscribe-simple";
}
const currSocket = new WebSocket(url);
currSocket.onmessage = (msg) => {
const patch: UIStatePatch2 = JSON.parse(msg.data);
state.update(state => {
if (patch.full != null) {
state = patch.full;
}
for (const deviceId in patch.devices) {
if (!patch.devices.hasOwnProperty(deviceId)) {
continue
}
state.devices = {...state.devices}
if (patch.devices[deviceId] !== null) {
state.devices[deviceId] = patch.devices[deviceId]!
} else {
delete state.devices[deviceId]
}
}
for (const assignmentId in patch.assignments) {
if (!patch.assignments.hasOwnProperty(assignmentId)) {
continue
}
state.assignments = {...state.assignments}
if (patch.assignments[assignmentId] != null) {
state.assignments[assignmentId] = patch.assignments[assignmentId]!
} else {
delete state.assignments[assignmentId]
}
}
for (const scriptName in patch.scripts) {
if (!patch.scripts.hasOwnProperty(scriptName)) {
continue
}
state.scripts = {...state.scripts}
if (patch.scripts[scriptName] != null) {
state.scripts[scriptName] = patch.scripts[scriptName]!
} else {
delete state.scripts[scriptName]
}
}
for (const triggerId in patch.triggers) {
if (!patch.triggers.hasOwnProperty(triggerId)) {
continue
}
state.triggers = {...state.triggers}
if (patch.triggers[triggerId] != null) {
state.triggers[triggerId] = patch.triggers[triggerId]!
} else {
delete state.triggers[triggerId]
}
}
return state;
})
}
currSocket.onopen = () => {
if (socket !== null) {
socket.close();
}
socket = currSocket;
}
currSocket.onerror = err => {
console.warn("Socket failed:", err);
currSocket.close();
setTimeout(() => connectSocket(), 3000);
}
currSocket.onclose = () => {
if (currSocket === socket) {
socket = null;
}
}
}
onMount(() => {
connectSocket();
window.addEventListener("visibilitychange", () => {
if (document.visibilityState == "visible") {
console.log("Reconnecting");
connectSocket();
} else {
if (socket != null) {
console.log("Disconnecting")
socket.close();
}
}
});
});
setContext<StateContextData>(ctxKey, {
reload,
error: { subscribe: error.subscribe },
state: { subscribe: state.subscribe },
deviceList,
assignmentList,
roomList,
maskList,
});
</script>
<slot></slot>
+31
View File
@@ -0,0 +1,31 @@
$color-maindark: hsl(240, 8%, 3.5%)
$color-main0: hsl(240, 8%, 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-main1: hsl(240, 8%, 14%)
$color-main1-transparent: hsla(240, 8%, 17%, 0.7)
$color-main2: hsl(240, 8%, 21%)
$color-main2-transparent: hsla(240, 8%, 24%, 0.7)
$color-main2-red: hsl(0, 16%, 21%)
$color-main3: hsl(240, 8%, 28%)
$color-main4: hsl(240, 8%, 35%)
$color-main5: hsl(240, 8%, 42%)
$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-main8: hsl(240, 8%, 63%)
$color-main9: hsl(240, 8%, 70%)
$color-main10: hsl(240, 8%, 77%)
$color-main11: hsl(240, 8%, 84%)
$color-main12: hsl(240, 8%, 91%)
$color-main13: hsl(240, 8%, 98%)
$opacity-entry3: 0.20
$opacity-entry4: 0.40
$opacity-entry5: 0.60
$opacity-entry6: 0.80
$color-green: hsl(120, 50%, 74%)
$color-green-dark: hsl(120, 50%, 35%)
+344
View File
@@ -0,0 +1,344 @@
<script lang="ts">
import { runCommand } from "$lib/client/lucifer";
import AssignmentState from "$lib/components/scripting/ScriptAssignmentState.svelte";
import Button from "$lib/components/Button.svelte";
import Checkbox from "$lib/components/Checkbox.svelte";
import type { DeviceIconName } from "$lib/components/DeviceIcon.svelte";
import DeviceIconSelector from "$lib/components/DeviceIconSelector.svelte";
import HSplit from "$lib/components/HSplit.svelte";
import HSplitPart from "$lib/components/HSplitPart.svelte";
import Icon from "$lib/components/Icon.svelte";
import Modal from "$lib/components/Modal.svelte";
import ModalBody from "$lib/components/ModalBody.svelte";
import ModalSection from "$lib/components/ModalSection.svelte";
import TagInput from "$lib/components/TagInput.svelte";
import { getModalContext } from "$lib/contexts/ModalContext.svelte";
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
import { getStateContext } from "$lib/contexts/StateContext.svelte";
import { toEffectRaw, type EffectRaw, fromEffectRaw } from "$lib/models/assignment";
import type { DeviceEditOp } from "$lib/models/device";
import { iconName } from "@fortawesome/free-solid-svg-icons/faQuestion";
import ScriptAssignmentState from "$lib/components/scripting/ScriptAssignmentState.svelte";
const { modal } = getModalContext();
const { selectedMasks, selectedMap, selectedList } = getSelectedContext();
const { deviceList, assignmentList } = getStateContext();
let show: boolean = false;
let match: string = "";
let disabled: boolean = false;
let enableRename: boolean = false;
let newName: string = "";
let enableRoom: boolean = false;
let newRoom: string = "";
let customRoom: string = "";
let enableGroup: boolean = false;
let newGroup: string = "";
let customGroup: string = "";
let enableAssign: boolean = false;
let newEffect: EffectRaw = toEffectRaw(undefined);
let enableIcon: boolean = false;
let newIcon: DeviceIconName = "generic_ball";
let enableTag: boolean = false;
let newTags: string[] = [];
let oldTags: string[] = [];
let enableRole: boolean = false;
let newRoles: string[] = [];
let oldRoles: string[] = [];
function setupModal(op: DeviceEditOp) {
show = true;
enableRename = (op === "rename");
enableAssign = (op === "assign");
enableIcon = (op === "change_icon");
enableRoom = (op === "move_room");
enableGroup = (op === "move_group");
enableTag = (op === "change_tags");
enableRole = (op === "change_roles");
const firstDevice = $deviceList.find(d => $selectedMap[d.id]);
newName = firstDevice?.name || "";
newIcon = firstDevice?.icon || "generic_ball";
newEffect = toEffectRaw(undefined);
newRoom = "";
newGroup = "";
reloadTagsAndRoles();
for (const device of $deviceList) {
if (!$selectedMap[device.id]) {
continue;
}
if (newRoom == "") {
const roomAlias = device.aliases.find(a => a.startsWith("lucifer:room:"))?.slice("lucifer:room:".length);
if (roomAlias != null) {
newRoom = roomAlias;
}
}
if (newGroup == "") {
const groupAlias = device.aliases.find(a => a.startsWith("lucifer:group:"))?.slice("lucifer:group:".length);
if (groupAlias != null) {
newGroup = groupAlias;
}
}
}
let mostPopularEffect = 0;
for (const assignment of $assignmentList) {
const selectedCount = assignment.deviceIds?.filter(id => $selectedMap[id]).length || 0;
if (selectedCount > mostPopularEffect) {
newEffect = toEffectRaw(assignment.effect);
mostPopularEffect = selectedCount;
}
}
}
function closeModal() {
show = false;
match = "";
}
function addEffectState() {
if (newEffect.states.length > 0) {
newEffect.states = [...newEffect.states, {...newEffect.states[newEffect.states.length - 1]}];
} else {
newEffect.states = [...newEffect.states, {color: null, intensity: null, power: null, temperature: null}]
}
}
function removeEffectState(i: number) {
newEffect.states = [...newEffect.states.slice(0, i), ...newEffect.states.slice(i+1)];
}
function reloadTagsAndRoles() {
const firstDevice = $deviceList.find(d => $selectedMap[d.id]);
newTags = firstDevice?.aliases
.filter(a => a.startsWith("lucifer:tag:"))
.filter(a => !$deviceList.filter(d => $selectedMap[d.id]).find(d => !d.aliases.includes(a)))
.map(a => a.slice("lucifer:tag:".length)) || [];
newRoles = firstDevice?.aliases
.filter(a => a.startsWith("lucifer:role:"))
.filter(a => !$deviceList.filter(d => $selectedMap[d.id]).find(d => !d.aliases.includes(a)))
.map(a => a.slice("lucifer:role:".length)) || [];
oldTags = [...newTags];
oldRoles = [...newRoles];
}
async function onSubmit() {
disabled = true;
let shouldWait = false;
try {
if (enableRename && newName !== "") {
await runCommand({addAlias: { match, alias: `lucifer:name:${newName}` }});
enableRename = false;
shouldWait = match.startsWith("lucifer:name:");
}
if (enableAssign) {
await runCommand({assign: { match, effect: fromEffectRaw(newEffect) }});
}
if (enableRoom) {
await runCommand({addAlias: { match, alias: `lucifer:room:${newRoom || customRoom}` }});
enableRoom = false;
shouldWait = match.startsWith("lucifer:room:");
newRoom = newRoom || customRoom;
}
if (enableGroup) {
await runCommand({addAlias: { match, alias: `lucifer:group:${newGroup || customGroup}` }});
enableGroup = false;
shouldWait = match.startsWith("lucifer:group:");
newGroup = newGroup || customGroup;
}
if (enableIcon) {
await runCommand({addAlias: { match, alias: `lucifer:icon:${newIcon}` }});
enableIcon = false;
shouldWait = match.startsWith("lucifer:icon:");
}
if (enableTag) {
const removeTags = oldTags.filter(ot => !newTags.includes(ot));
for (const removeTag of removeTags) {
await runCommand({removeAlias: { match, alias: `lucifer:tag:${removeTag}` }});
}
const addTags = newTags.filter(nt => !oldTags.includes(nt));
for (const addTag of addTags) {
await runCommand({addAlias: { match, alias: `lucifer:tag:${addTag}` }});
}
shouldWait = removeTags.length > 0 || addTags.length > 0;
}
if (enableRole) {
const removeRoles = oldRoles.filter(or => !newRoles.includes(or));
for (const removeRole of removeRoles) {
await runCommand({removeAlias: { match, alias: `lucifer:role:${removeRole}` }});
}
const addRoles = newRoles.filter(nr => !oldRoles.includes(nr));
for (const addRole of addRoles) {
await runCommand({addAlias: { match, alias: `lucifer:role:${addRole}` }});
}
shouldWait = removeRoles.length > 0 || addRoles.length > 0;
}
if (shouldWait) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
} catch (err) {}
reloadTagsAndRoles();
disabled = false;
}
let roomOptions: string[] = [];
$: roomOptions = $deviceList.flatMap(d => d.aliases)
.filter(k => k.startsWith("lucifer:room:"))
.sort()
.filter((v, i, a) => v !== a[i-1])
.map(r => r.slice("lucifer:room:".length));
let groupOptions: string[] = [];
$: groupOptions = $deviceList.flatMap(d => d.aliases)
.filter(k => k.startsWith("lucifer:group:"))
.sort()
.filter((v, i, a) => v !== a[i-1])
.map(r => r.slice("lucifer:group:".length));
$: {
if ($modal.kind === "device.edit") {
setupModal($modal.op);
} else {
closeModal();
}
}
$: if (!$selectedMasks.includes(match)) {
match = $selectedMasks[0];
}
</script>
<form novalidate on:submit|preventDefault={onSubmit}>
<Modal wide disabled={disabled} closable show={show} titleText="Device Editor" submitText="Save Changes">
<ModalBody>
<label for="mask">Selection</label>
<select bind:value={match}>
{#each $selectedMasks as option (option)}
<option value={option}>{option}</option>
{/each}
</select>
<ModalSection bind:expanded={enableAssign} title="Assign">
<HSplit reverse>
<HSplitPart>
<label for="states">Effect</label>
<select bind:value={newEffect.kind}>
<option value="gradient">Gradient</option>
<option value="pattern">Pattern</option>
<option value="random">Random</option>
<option value="solid">Solid</option>
<option value="vrange">Variable Range</option>
</select>
{#if newEffect.kind !== "manual" && newEffect.kind !== "vrange"}
<label for="animationMs">Interval (ms)</label>
<input type="number" name="animationMs" min=0 max=10000 step=100 bind:value={newEffect.animationMs} />
{/if}
{#if newEffect.kind === "solid"}
<label for="interleave">Interleave</label>
<input type="number" name="interleave" min=0 step=1 bind:value={newEffect.interleave} />
{/if}
{#if newEffect.kind === "vrange"}
<label for="states">Variable</label>
<select bind:value={newEffect.variable}>
<option value="motion.min">Motion Min (Seconds)</option>
<option value="motion.avg">Motion Avg (Seconds)</option>
<option value="motion.max">Motion Max (Seconds)</option>
<option value="temperature.min">Temperature Min (Celcius)</option>
<option value="temperature.avg">Temperature Avg (Celcius)</option>
<option value="temperature.max">Temperature Max (Celcius)</option>
</select>
<HSplit>
<HSplitPart left>
<label for="min">Min</label>
<input type="number" name="min" min=0 step=1 bind:value={newEffect.min} />
</HSplitPart>
<HSplitPart right>
<label for="max">Max</label>
<input type="number" name="max" min=0 step=1 bind:value={newEffect.max} />
</HSplitPart>
</HSplit>
{/if}
{#if ["gradient", "random", "vrange"].includes(newEffect.kind)}
<label for="states">Options</label>
<Checkbox bind:checked={newEffect.interpolate} label="Interpolate" />
{#if (newEffect.kind === "gradient")}
<Checkbox bind:checked={newEffect.reverse} label="Reverse" />
{/if}
{/if}
</HSplitPart>
<HSplitPart weight={1.0}>
<label for="states">States</label>
{#each newEffect.states as state, i }
<ScriptAssignmentState deletable bind:value={state} on:delete={() => removeEffectState(i)} />
{/each}
<Button on:click={addEffectState} icon><Icon name="plus" /></Button>
</HSplitPart>
</HSplit>
</ModalSection>
<ModalSection bind:expanded={enableRename} title="Rename">
<label for="name">New Name</label>
<input type="text" name="name" bind:value={newName} />
</ModalSection>
<ModalSection bind:expanded={enableRoom} title="Change Room">
<label for="newRoom">Select Room</label>
<select bind:value={newRoom}>
{#each roomOptions as roomOption}
<option value={roomOption}>{roomOption}</option>
{/each}
<option value="">Create Room</option>
</select>
{#if newRoom == ""}
<label for="customRoom">New Room</label>
<input type="text" name="customRoom" bind:value={customRoom} />
{/if}
</ModalSection>
<ModalSection bind:expanded={enableGroup} title="Change Group">
<label for="newGroup">Select Group</label>
<select bind:value={newGroup}>
{#each groupOptions as groupOption}
<option value={groupOption}>{groupOption}</option>
{/each}
<option value="">Create Group</option>
</select>
{#if newGroup == ""}
<label for="customGroup">New Group</label>
<input type="text" name="customGroup" bind:value={customGroup} />
{/if}
</ModalSection>
<ModalSection bind:expanded={enableIcon} title="Change Icon">
<label for="icon">New Icon</label>
<DeviceIconSelector bind:value={newIcon} />
</ModalSection>
<ModalSection bind:expanded={enableTag} title="Change Tags">
<label for="icon">Tags</label>
<TagInput bind:value={newTags} />
</ModalSection>
<ModalSection bind:expanded={enableRole} title="Change Roles">
<label for="icon">Roles</label>
<TagInput bind:value={newRoles} />
</ModalSection>
</ModalBody>
</Modal>
</form>
+120
View File
@@ -0,0 +1,120 @@
<script lang="ts">
import { runCommand } from "$lib/client/lucifer";
import HSplit from "$lib/components/HSplit.svelte";
import HSplitPart from "$lib/components/HSplitPart.svelte";
import Modal from "$lib/components/Modal.svelte";
import ModalBody from "$lib/components/ModalBody.svelte";
import ModalSection from "$lib/components/ModalSection.svelte";
import ScriptLineBlock from "$lib/components/scripting/ScriptLineBlock.svelte";
import { getModalContext } from "$lib/contexts/ModalContext.svelte";
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
import { getStateContext } from "$lib/contexts/StateContext.svelte";
import { toEditableScriptLine, type ScriptLineEditable, fromEditableScriptLine } from "$lib/models/script";
const { state } = getStateContext();
const { selectedMasks } = getSelectedContext();
const { modal } = getModalContext();
let show = false;
let disabled = false;
let selectedScript = "";
let runMatch = "";
let newScriptName = "New Script";
let current: ScriptLineEditable[] = [];
function onOpen() {
disabled = false;
current = [];
selectedScript = scriptNames?.[0] || "";
loadScript(selectedScript);
}
async function onSubmit() {
disabled = true;
try {
await runCommand({updateScript: { name: selectedScript || newScriptName, lines: current.map(fromEditableScriptLine) }});
if (runMatch !== "") {
await runCommand({executeScript: { name: selectedScript || newScriptName, match: runMatch, }});
}
await new Promise(resolve => setTimeout(resolve, 500));
selectedScript = selectedScript || newScriptName;
} catch(_) {}
disabled = false;
}
function loadScript(name: string) {
if (name === "") {
current = [];
} else {
current = $state?.scripts[name]?.map(toEditableScriptLine) || [];
}
}
function addLine() {
current = [
...current,
toEditableScriptLine({if: {
condition: { scope: "global", key: "", op: "eq" },
then: [],
else: [],
}})
]
}
$: if ($modal.kind === "script.edit") {
show = true;
onOpen();
} else {
show = false;
}
$: scriptNames = Object.keys($state?.scripts||{}).sort();
$: loadScript(selectedScript);
</script>
<form novalidate on:submit|preventDefault={onSubmit}>
<Modal
ultrawide closable show={show}
titleText="Script Editor"
disabled={disabled || (selectedScript === "" && current.length === 0)}
submitText={(current.length > 0 || selectedScript === "") ? "Save Script" : "Delete Script"}
>
<ModalBody>
<HSplit>
<HSplitPart weight={33.3}>
<label for="mask">Script</label>
<select bind:value={selectedScript}>
{#each scriptNames as scriptName}
<option value={scriptName}>{scriptName}</option>
{/each}
<option value="">New Script</option>
</select>
</HSplitPart>
<HSplitPart weight={33.3}>
{#if selectedScript === ""}
<label for="mask">New Script Name</label>
<input type="text" bind:value={newScriptName}>
{/if}
</HSplitPart>
<HSplitPart weight={33.4}>
{#if $selectedMasks.length > 0}
<label for="mask">Run on Save</label>
<select bind:value={runMatch}>
<option value="">Don't Run</option>
{#each $selectedMasks as option (option)}
<option value={option}>{option}</option>
{/each}
</select>
{/if}
</HSplitPart>
</HSplit>
<ModalSection expanded disableExpandToggle title="Edit Script">
<ScriptLineBlock add on:add={addLine} label="Script" bind:value={current} />
</ModalSection>
</ModalBody>
</Modal>
</form>
+181
View File
@@ -0,0 +1,181 @@
<script lang="ts">
import { runCommand } from "$lib/client/lucifer";
import HSplit from "$lib/components/HSplit.svelte";
import HSplitPart from "$lib/components/HSplitPart.svelte";
import Modal from "$lib/components/Modal.svelte";
import ModalBody from "$lib/components/ModalBody.svelte";
import ModalSection from "$lib/components/ModalSection.svelte";
import ScriptLineBlock from "$lib/components/scripting/ScriptLineBlock.svelte";
import { getModalContext } from "$lib/contexts/ModalContext.svelte";
import { getStateContext } from "$lib/contexts/StateContext.svelte";
import { fromEditableTrigger, newEditableScriptLine, newEditableTrigger, toEditableScriptLine, toEditableTrigger, type TriggerEditable } from "$lib/models/script";
const { state, deviceList, maskList } = getStateContext();
const { modal } = getModalContext();
let show = false;
let disabled = false;
let selectedTrigger = "";
let deleteCount = 3;
let current: TriggerEditable = newEditableTrigger("*", "");
async function submitForm() {
disabled = true;
try {
const before = Object.keys($state.triggers);
await runCommand({updateTrigger: fromEditableTrigger(current)});
if (current.id === null) {
await new Promise(resolve => setTimeout(resolve, 1000));
const after = Object.keys($state.triggers);
selectedTrigger = after.find(id => !before.includes(id)) || triggerList[0].id;
}
} catch(_) {}
deleteCount = 3;
disabled = false;
}
async function deleteTrigger() {
deleteCount -= 1;
if (deleteCount <= 0) {
try {
await runCommand({deleteTrigger: {id: selectedTrigger}});
await new Promise(resolve => setTimeout(resolve, 1000));
selectedTrigger = triggerList[0]?.id || "";
} catch(_) {}
}
}
function loadTrigger(id: string) {
deleteCount = 3;
if (id !== "") {
current = toEditableTrigger($state.triggers[id]);
} else {
current = {...current, id: null};
}
}
function addPreLine() {
current.scriptPre = [...current.scriptPre, newEditableScriptLine()]
}
function addPostLine() {
current.scriptPost = [...current.scriptPost, newEditableScriptLine()]
}
function ensureButton(buttonList: string[]) {
if (!buttonList.includes(current.button)) {
current.button = buttonList[0];
}
}
$: loadTrigger(selectedTrigger);
$: triggerList = Object.keys($state.triggers).map(k => $state.triggers[k]).sort((a, b) => (
a.event.localeCompare(b.event)
|| a.deviceMatch.localeCompare(b.deviceMatch)
|| a.parameter.localeCompare(b.parameter)
));
$: buttonList = $deviceList.filter(d => d.aliases.includes(current.deviceMatch))
.flatMap(d => d.hwState?.buttons||[])
.sort()
.filter((e,i,a) => a[i-1] !== e);
$: scriptList = Object.keys($state.scripts).sort();
$: maskGroups = $maskList.map(s => s.split(":")[1]).filter((e, i, a) => a[i-1] !== e).sort().reverse();
$: show = ($modal.kind === "trigger.edit");
$: if (scriptList.length > 0 && current.scriptName === "") { current.scriptName = scriptList[0]; };
$: ensureButton(buttonList);
</script>
<form novalidate on:submit|preventDefault={submitForm}>
<Modal
ultrawide closable show={show}
titleText="Trigger Editor"
disabled={disabled}
submitText="Save Trigger"
>
<ModalBody>
<HSplit>
<HSplitPart weight={1}>
<label for="mask">Trigger</label>
<select bind:value={selectedTrigger}>
{#each triggerList as trigger (trigger.id)}
<option value={trigger.id}>{trigger.name || `${trigger.deviceMatch} – ${trigger.event} ${trigger.parameter}`}</option>
{/each}
<option value="">New Trigger</option>
</select>
</HSplitPart>
<HSplitPart weight={2}>
</HSplitPart>
</HSplit>
<HSplit>
<HSplitPart weight={1}>
<label for="mask">Event</label>
<select bind:value={current.event}>
<option value="Button">Button Pressed</option>
<option value="Time">Time Changed</option>
</select>
</HSplitPart>
<HSplitPart weight={1}>
{#if current.event === "Button"}
<label for="mask">Button</label>
<select bind:value={current.button}>
{#each buttonList as buttonOption}
<option value={buttonOption}>{buttonOption}</option>
{/each}
</select>
{:else}
<label for="mask">Time</label>
<input type="time" bind:value={current.time} />
{/if}
</HSplitPart>
<HSplitPart weight={1}>
{#if current.event === "Button"}
<label for="mask">Device</label>
<select bind:value={current.deviceMatch}>
{#each maskGroups as g}
<optgroup label="{g.slice(0, 1).toUpperCase() + g.slice(1)}">
{#each $maskList.filter(m => m.split(":")[1] === g) as mask}
<option value={mask}>{mask}</option>
{/each}
</optgroup>
{/each}
</select>
{/if}
</HSplitPart>
</HSplit>
<HSplit>
<HSplitPart weight={1}>
<label for="mask">Run Script</label>
<select bind:value={current.scriptName}>
{#each scriptList as script}
<option value={script}>{script}</option>
{/each}
</select>
</HSplitPart>
<HSplitPart weight={1}>
<label for="mask">Script Target</label>
<select bind:value={current.scriptTarget}>
{#each maskGroups as g}
<optgroup label="{g.slice(0, 1).toUpperCase() + g.slice(1)}">
{#each $maskList.filter(m => m.split(":")[1] === g) as mask}
<option value={mask}>{mask}</option>
{/each}
</optgroup>
{/each}
</select>
</HSplitPart>
<HSplitPart weight={1}>
</HSplitPart>
</HSplit>
<ModalSection expanded disableExpandToggle title="Trigger-Specific Scripting">
<ScriptLineBlock add on:add={addPreLine} label="Before Script" bind:value={current.scriptPre} />
<ScriptLineBlock add on:add={addPostLine} label="After Script" bind:value={current.scriptPost} />
</ModalSection>
</ModalBody>
<button slot="secondary-button-1" disabled={disabled || selectedTrigger === ""} on:click|preventDefault={deleteTrigger}>Delete ({deleteCount})</button>
</Modal>
</form>

Some files were not shown because too many files have changed in this diff Show More