Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
511ef97616 | ||
|
|
3d2b4e7bfe | ||
|
|
c2f3d3c4d1 | ||
|
|
16c6d5316b | ||
|
|
a558594537 | ||
|
|
5fee980f79 | ||
|
|
f00575fd7f |
@@ -7,6 +7,7 @@ import (
|
||||
"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/mill"
|
||||
"git.aiterp.net/lucifer3/server/services/mysqldb"
|
||||
"git.aiterp.net/lucifer3/server/services/nanoleaf"
|
||||
"git.aiterp.net/lucifer3/server/services/script"
|
||||
@@ -47,6 +48,7 @@ func main() {
|
||||
bus.Join(nanoleaf.NewService())
|
||||
bus.Join(hue.NewService())
|
||||
bus.Join(tradfri.NewService())
|
||||
bus.Join(mill.NewService())
|
||||
bus.Join(uistate.NewService())
|
||||
bus.Join(script.NewService(resolver, variables))
|
||||
bus.Join(database)
|
||||
|
||||
+2
-1
@@ -7,4 +7,5 @@ node_modules
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
/*.blend1
|
||||
/*.blend1
|
||||
/build
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
/_app
|
||||
*.png
|
||||
*.html
|
||||
Binary file not shown.
@@ -1,14 +1,19 @@
|
||||
<script lang="ts" context="module">
|
||||
export const selectedColorPicker = writable<any>(null);</script>
|
||||
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 { createEventDispatcher, tick } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let color: Color;
|
||||
export let id: any = null;
|
||||
export let deletable: boolean = false;
|
||||
|
||||
let xy: boolean;
|
||||
let x: number;
|
||||
@@ -62,8 +67,8 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#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" />
|
||||
@@ -77,11 +82,29 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="color-picker-buttons">
|
||||
{#if deletable} <Icon block name="trash" on:click={() => dispatch("delete")} /> {/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
@import "$lib/css/colors.sass"
|
||||
|
||||
div.color-picker-buttons
|
||||
position: relative
|
||||
color: $color-main7
|
||||
top: -12.3ch
|
||||
height: 1em
|
||||
width: 10ch
|
||||
display: flex
|
||||
flex-direction: row-reverse
|
||||
|
||||
:global(div.icon-wrapper)
|
||||
padding: 0.05em
|
||||
|
||||
&:hover
|
||||
color: $color-main8
|
||||
|
||||
div.color-picker
|
||||
position: relative
|
||||
width: 10ch
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts" context="module">
|
||||
import generic_lamp from "./icons/generic_lamp.svg?raw";
|
||||
import generic_lamp2 from "./icons/generic_lamp2.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";
|
||||
@@ -14,10 +16,14 @@
|
||||
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_hexagon_corner from "./icons/shape_hexagon_corner.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_lamp2,
|
||||
generic_boob,
|
||||
generic_ball,
|
||||
generic_strip,
|
||||
@@ -27,12 +33,16 @@
|
||||
hue_lightbulb_e27,
|
||||
hue_lightbulb_e14,
|
||||
hue_lightbulb_gu10,
|
||||
generic_pendulum_01,
|
||||
hue_adore_tube,
|
||||
hue_playbar,
|
||||
shape_square,
|
||||
shape_hexagon,
|
||||
shape_hexagon_corner,
|
||||
shape_triangle,
|
||||
hue_go,
|
||||
hue_ensis_up,
|
||||
hue_ensis_down,
|
||||
});
|
||||
|
||||
export const deviceIconList = Object.seal(Object.keys(deviceIconMap).sort()) as DeviceIconName[];
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#if block}
|
||||
<div on:click class:marginAutio>
|
||||
<div class="icon-wrapper" on:click class:marginAutio>
|
||||
<Icon class="icon" icon={icons[name] || icons.question} />
|
||||
</div>
|
||||
{:else}
|
||||
@@ -42,6 +42,7 @@
|
||||
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";
|
||||
@@ -83,6 +84,7 @@
|
||||
"search": faSearch,
|
||||
"chevron_right": faChevronRight,
|
||||
"chevron_down": faChevronDown,
|
||||
"chevron_up": faChevronUp,
|
||||
"trash": faTrash,
|
||||
"check_slot": faCheckToSlot,
|
||||
"eye": faEye,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
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:"));
|
||||
@@ -57,7 +58,7 @@
|
||||
deviceTitle = device.hwState.internalName;
|
||||
}
|
||||
|
||||
if (hasColor && (!hasPower || device.desiredState.power)) {
|
||||
if (hasColor && (!hasPower || device.desiredState?.power)) {
|
||||
iconColor = device.desiredColorRgb;
|
||||
} else {
|
||||
iconColor = null;
|
||||
@@ -70,7 +71,7 @@
|
||||
|
||||
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;
|
||||
barFraction = Math.min(1, Math.max(0, (device.desiredState?.temperature||0) - 5) / 35) * 1;
|
||||
}
|
||||
|
||||
if (sflags & SupportFlags.Intensity) {
|
||||
@@ -109,7 +110,7 @@
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="lamp" class:compact class:selected={$selectedMap[device.id]} on:click={onSelect}>
|
||||
<div title={deviceTitle} class="lamp" class:compact class:selected={$selectedMap[device.id]} on:click={onSelect}>
|
||||
<div class="row">
|
||||
<div class="row-icon">
|
||||
{#if iconColor != null}
|
||||
@@ -123,7 +124,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="title">
|
||||
<div class="name" class:hasRoleOrTag>{displayTitle}</div>
|
||||
<div class="name" class:hasRoleOrTag>{deviceTitle}</div>
|
||||
{#if hasRoleOrTag}
|
||||
<div class="tag-list">
|
||||
{#each roles as role}
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
<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";
|
||||
import Lamp from "./Lamp.svelte";
|
||||
|
||||
export let name: string
|
||||
@@ -23,6 +22,11 @@
|
||||
}
|
||||
|
||||
$: selected = !devices.find(d => !$selectedMap[d.id])
|
||||
$: tiny = devices.length > 30;
|
||||
$: allAliases = devices.flatMap(d => d.aliases).sort().filter((e, i, a) => a[i-1] !== e);
|
||||
$: allTags = allAliases.filter(a => a.startsWith("lucifer:tag:")).map(a => a.split(":")[2]);
|
||||
$: allRoles = allAliases.filter(a => a.startsWith("lucifer:role:")).map(a => a.split(":")[2]);
|
||||
$: uncommon = allAliases.filter(a => devices.find(d => !d.aliases.includes(a)))
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@@ -30,20 +34,36 @@
|
||||
<div class="row">
|
||||
<div class="title" on:click={onSelect}>{name}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row" class:tiny>
|
||||
{#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>
|
||||
<!-- Skittent datasnok -->
|
||||
{#each Array.from({length: 64}) as _}
|
||||
<div style="width: 3.6ch"></div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="row">
|
||||
{#if allTags.length > 0 || allRoles.length > 0}
|
||||
<div class="tag-list">
|
||||
{#each allRoles as role}
|
||||
<div class="tag" class:uncommon={uncommon.includes(`lucifer:role:${role}`)}>
|
||||
<Icon block name="masks_theater" />
|
||||
<div class="tag-name">
|
||||
{role}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#each allTags as tag}
|
||||
<div class="tag" class:uncommon={uncommon.includes(`lucifer:tag:${tag}`)}>
|
||||
<Icon block name="tag" />
|
||||
<div class="tag-name">
|
||||
{tag}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,6 +113,28 @@
|
||||
align-content: center
|
||||
justify-content: space-between
|
||||
|
||||
&.tiny
|
||||
margin-top: 0.2em
|
||||
font-size: 0.4em
|
||||
|
||||
> div.tag-list
|
||||
display: flex
|
||||
flex-direction: row
|
||||
font-size: 0.5em
|
||||
margin-top: 0.5em
|
||||
|
||||
> div.tag
|
||||
display: flex
|
||||
flex-direction: row
|
||||
padding: 0 0.5ch
|
||||
padding-right: 1ch
|
||||
|
||||
&.uncommon
|
||||
opacity: 0.333
|
||||
|
||||
div.tag-name
|
||||
padding-left: 0.5ch
|
||||
|
||||
> div.title
|
||||
font-size: 0.75em
|
||||
text-align: left
|
||||
@@ -101,4 +143,5 @@
|
||||
height: 1.6em
|
||||
user-select: none
|
||||
margin-left: 0.4ch
|
||||
width: 100%
|
||||
</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>
|
||||
@@ -19,7 +19,10 @@
|
||||
$: submit(color);
|
||||
|
||||
function toggle(event: MouseEvent & { currentTarget: EventTarget & HTMLDivElement; }) {
|
||||
if (event.shiftKey) {
|
||||
if (color === null) {
|
||||
color = { k: 2750 }
|
||||
$selectedColorPicker = colorPickerId;
|
||||
} else {
|
||||
if (color !== null) {
|
||||
if ($selectedColorPicker !== colorPickerId) {
|
||||
$selectedColorPicker = colorPickerId;
|
||||
@@ -30,17 +33,10 @@
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (color === null || (color.hs == null && color.k == null)) {
|
||||
color = { k: 2750 }
|
||||
$selectedColorPicker = colorPickerId;
|
||||
} else {
|
||||
color = null;
|
||||
|
||||
if ($selectedColorPicker === colorPickerId) {
|
||||
$selectedColorPicker = null;
|
||||
}
|
||||
}
|
||||
function clearColor() {
|
||||
color = null;
|
||||
}
|
||||
|
||||
function submit(color: Color | null) {
|
||||
@@ -80,7 +76,7 @@
|
||||
<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} />
|
||||
<ColorPicker on:delete={clearColor} deletable bind:color={color} id={colorPickerId} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if color?.xy != null}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
let percentage: number
|
||||
$: percentage = Math.max(Math.min((value || 0) * 100, 100), 0);
|
||||
$: percentage = Math.round(Math.max(Math.min((value || 0) * 100, 100), 0));
|
||||
$: update(percentage);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
<script lang="js">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fly, fade } from "svelte/transition";
|
||||
|
||||
// Props
|
||||
export let min = 0;
|
||||
export let max = 100;
|
||||
export let initialValue = 0;
|
||||
export let id = null;
|
||||
export let value =
|
||||
typeof initialValue === "string" ? parseInt(initialValue) : initialValue;
|
||||
|
||||
// Node Bindings
|
||||
let container = null;
|
||||
let thumb = null;
|
||||
let progressBar = null;
|
||||
let element = null;
|
||||
|
||||
// Internal State
|
||||
let elementX = null;
|
||||
let currentThumb = null;
|
||||
let holding = false;
|
||||
let thumbHover = false;
|
||||
let keydownAcceleration = 0;
|
||||
let accelerationTimer = null;
|
||||
|
||||
// Dispatch 'change' events
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Mouse shield used onMouseDown to prevent any mouse events penetrating other elements,
|
||||
// ie. hover events on other elements while dragging. Especially for Safari
|
||||
const mouseEventShield = document.createElement("div");
|
||||
mouseEventShield.setAttribute("class", "mouse-over-shield");
|
||||
mouseEventShield.addEventListener("mouseover", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
function resizeWindow() {
|
||||
elementX = element.getBoundingClientRect().left;
|
||||
}
|
||||
|
||||
// Allows both bind:value and on:change for parent value retrieval
|
||||
function setValue(val) {
|
||||
value = val;
|
||||
dispatch("change", { value });
|
||||
}
|
||||
|
||||
function onTrackEvent(e) {
|
||||
// Update value immediately before beginning drag
|
||||
updateValueOnEvent(e);
|
||||
onDragStart(e);
|
||||
}
|
||||
|
||||
function onHover(e) {
|
||||
thumbHover = thumbHover ? false : true;
|
||||
}
|
||||
|
||||
function onDragStart(e) {
|
||||
// If mouse event add a pointer events shield
|
||||
if (e.type === "mousedown") document.body.append(mouseEventShield);
|
||||
currentThumb = thumb;
|
||||
}
|
||||
|
||||
function onDragEnd(e) {
|
||||
// If using mouse - remove pointer event shield
|
||||
if (e.type === "mouseup") {
|
||||
if (document.body.contains(mouseEventShield))
|
||||
document.body.removeChild(mouseEventShield);
|
||||
// Needed to check whether thumb and mouse overlap after shield removed
|
||||
if (isMouseInElement(e, thumb)) thumbHover = true;
|
||||
}
|
||||
currentThumb = null;
|
||||
}
|
||||
|
||||
// Check if mouse event cords overlay with an element's area
|
||||
function isMouseInElement(event, element) {
|
||||
let rect = element.getBoundingClientRect();
|
||||
let { clientX: x, clientY: y } = event;
|
||||
if (x < rect.left || x >= rect.right) return false;
|
||||
if (y < rect.top || y >= rect.bottom) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Accessible keypress handling
|
||||
function onKeyPress(e) {
|
||||
// Max out at +/- 10 to value per event (50 events / 5)
|
||||
// 100 below is to increase the amount of events required to reach max velocity
|
||||
if (keydownAcceleration < 50) keydownAcceleration++;
|
||||
let throttled = Math.ceil(keydownAcceleration / 5);
|
||||
|
||||
if (e.key === "ArrowUp" || e.key === "ArrowRight") {
|
||||
if (value + throttled > max || value >= max) {
|
||||
setValue(max);
|
||||
} else {
|
||||
setValue(value + throttled);
|
||||
}
|
||||
}
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowLeft") {
|
||||
if (value - throttled < min || value <= min) {
|
||||
setValue(min);
|
||||
} else {
|
||||
setValue(value - throttled);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset acceleration after 100ms of no events
|
||||
clearTimeout(accelerationTimer);
|
||||
accelerationTimer = setTimeout(() => (keydownAcceleration = 1), 100);
|
||||
}
|
||||
|
||||
function calculateNewValue(clientX) {
|
||||
// Find distance between cursor and element's left cord (20px / 2 = 10px) - Center of thumb
|
||||
let delta = clientX - (elementX + 10);
|
||||
|
||||
// Use width of the container minus (5px * 2 sides) offset for percent calc
|
||||
let percent = (delta * 100) / (container.clientWidth - 10);
|
||||
|
||||
// Limit percent 0 -> 100
|
||||
percent = percent < 0 ? 0 : percent > 100 ? 100 : percent;
|
||||
|
||||
// Limit value min -> max
|
||||
setValue(parseInt((percent * (max - min)) / 100) + min);
|
||||
}
|
||||
|
||||
// Handles both dragging of touch/mouse as well as simple one-off click/touches
|
||||
function updateValueOnEvent(e) {
|
||||
// touchstart && mousedown are one-off updates, otherwise expect a currentPointer node
|
||||
if (!currentThumb && e.type !== "touchstart" && e.type !== "mousedown")
|
||||
return false;
|
||||
|
||||
if (e.stopPropagation) e.stopPropagation();
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
|
||||
// Get client's x cord either touch or mouse
|
||||
const clientX =
|
||||
e.type === "touchmove" || e.type === "touchstart"
|
||||
? e.touches[0].clientX
|
||||
: e.clientX;
|
||||
|
||||
calculateNewValue(clientX);
|
||||
}
|
||||
|
||||
// React to left position of element relative to window
|
||||
$: if (element) elementX = element.getBoundingClientRect().left;
|
||||
|
||||
// Set a class based on if dragging
|
||||
$: holding = Boolean(currentThumb);
|
||||
|
||||
// Update progressbar and thumb styles to represent value
|
||||
$: if (progressBar && thumb) {
|
||||
// Limit value min -> max
|
||||
value = value > min ? value : min;
|
||||
value = value < max ? value : max;
|
||||
|
||||
let percent = ((value - min) * 100) / (max - min);
|
||||
let offsetLeft = (container.clientWidth - 10) * (percent / 100) + 5;
|
||||
|
||||
// Update thumb position + active range track width
|
||||
thumb.style.left = `${offsetLeft}px`;
|
||||
progressBar.style.width = `${offsetLeft}px`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:touchmove|nonpassive={updateValueOnEvent}
|
||||
on:touchcancel={onDragEnd}
|
||||
on:touchend={onDragEnd}
|
||||
on:mousemove={updateValueOnEvent}
|
||||
on:mouseup={onDragEnd}
|
||||
on:resize={resizeWindow}
|
||||
/>
|
||||
<div class="range">
|
||||
<div
|
||||
class="range__wrapper"
|
||||
tabindex="0"
|
||||
on:keydown={onKeyPress}
|
||||
bind:this={element}
|
||||
role="slider"
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
aria-valuenow={value}
|
||||
{id}
|
||||
on:mousedown={onTrackEvent}
|
||||
on:touchstart={onTrackEvent}
|
||||
>
|
||||
<div class="range__track" bind:this={container}>
|
||||
<div class="range__track--highlighted" bind:this={progressBar} />
|
||||
<div
|
||||
class="range__thumb"
|
||||
class:range__thumb--holding={holding}
|
||||
bind:this={thumb}
|
||||
on:touchstart={onDragStart}
|
||||
on:mousedown={onDragStart}
|
||||
on:mouseover={() => (thumbHover = true)}
|
||||
on:mouseout={() => (thumbHover = false)}
|
||||
>
|
||||
{#if holding || thumbHover}
|
||||
<div
|
||||
class="range__tooltip"
|
||||
in:fly={{ y: 7, duration: 200 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:head>
|
||||
<style>
|
||||
.mouse-over-shield {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(255, 0, 0, 0);
|
||||
z-index: 10000;
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
.range {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.range__wrapper {
|
||||
min-width: 100%;
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.range__wrapper:focus-visible > .range__track {
|
||||
box-shadow: 0 0 0 2px white, 0 0 0 3px var(--track-focus, #6185ff);
|
||||
}
|
||||
|
||||
.range__track {
|
||||
height: 6px;
|
||||
background-color: var(--track-bgcolor, #d0d0d0);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.range__track--highlighted {
|
||||
background-color: var(--track-highlight-bgcolor, #6185ff);
|
||||
background: var(
|
||||
--track-highlight-bg,
|
||||
linear-gradient(90deg, #6185ff, #9c65ff)
|
||||
);
|
||||
width: 0;
|
||||
height: 6px;
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.range__thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--thumb-bgcolor, white);
|
||||
cursor: pointer;
|
||||
border-radius: 999px;
|
||||
margin-top: -8px;
|
||||
transition: box-shadow 100ms;
|
||||
user-select: none;
|
||||
box-shadow: var(
|
||||
--thumb-boxshadow,
|
||||
0 1px 1px 0 rgba(0, 0, 0, 0.14),
|
||||
0 0px 2px 1px rgba(0, 0, 0, 0.2)
|
||||
);
|
||||
}
|
||||
|
||||
.range__thumb--holding {
|
||||
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 2px 1px rgba(0, 0, 0, 0.2),
|
||||
0 0 0 6px var(--thumb-holding-outline, rgba(113, 119, 250, 0.3));
|
||||
}
|
||||
|
||||
.range__tooltip {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -33px;
|
||||
color: var(--tooltip-text, white);
|
||||
width: 38px;
|
||||
padding: 4px 0;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
background-color: var(--tooltip-bgcolor, #6185ff);
|
||||
background: var(--tooltip-bg, linear-gradient(45deg, #6185ff, #9c65ff));
|
||||
}
|
||||
|
||||
.range__tooltip::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
background-color: var(--tooltip-bgcolor, #6185ff);
|
||||
bottom: -3px;
|
||||
left: calc(50% - 3px);
|
||||
clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
|
||||
transform: rotate(-45deg);
|
||||
border-radius: 0 0 0 3px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?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 200.384, 268.295 198.051, 264.451 194.264, 260.908 189.167, 257.803 182.957, 255.255 175.872, 253.361 168.184, 252.195 160.188, 251.801 152.193, 252.195 144.505, 253.361 137.419, 255.255 131.209, 257.803 126.112, 260.908 122.325, 264.451 119.993, 268.295 119.205, 272.293 119.205, 278.205 119.993, 282.203 122.325, 286.047 126.112, 289.589 131.209, 292.695 137.419, 295.243 144.505, 297.137 152.193, 298.303 160.188, 298.696 168.184, 298.303 175.872, 297.137 182.957, 295.243 189.167, 292.695 194.264, 289.589 198.051, 286.047 200.384, 282.203 201.171, 278.205 " />
|
||||
<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 119.205, 272.293 119.993, 276.291 122.325, 280.135 126.112, 283.677 131.209, 286.783 137.419, 289.331 144.505, 291.224 152.193, 292.391 160.188, 292.784 168.184, 292.391 175.872, 291.224 182.957, 289.331 189.167, 286.783 194.264, 283.677 198.051, 280.135 200.384, 276.291 200.589, 275.249 201.171, 272.293 200.384, 268.295 " />
|
||||
<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 201.171, 278.205 201.171, 272.293 " />
|
||||
<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 153.947, 179.170 153.947, 189.170 153.947, 199.170 153.947, 209.170 153.947, 219.170 153.947, 229.170 153.947, 239.170 153.947, 249.170 153.947, 252.109 153.947, 262.109 153.947, 270.606 154.309, 271.203 154.897, 271.753 155.689, 272.235 156.653, 272.631 157.753, 272.925 158.947, 273.106 160.188, 273.167 161.430, 273.106 162.623, 272.925 163.723, 272.631 164.688, 272.235 165.479, 271.753 166.067, 271.203 166.429, 270.606 166.429, 260.606 166.429, 252.109 166.429, 242.109 166.429, 232.109 166.429, 222.109 166.429, 212.109 166.429, 202.109 166.429, 192.109 166.429, 182.109 166.429, 179.170 " />
|
||||
<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 88.941, 155.674 96.067, 162.340 104.607, 167.543 105.658, 168.183 114.909, 171.979 117.344, 172.979 127.005, 175.560 130.677, 176.542 140.563, 178.041 145.143, 178.736 153.947, 179.170 160.188, 179.477 166.429, 179.170 175.233, 178.736 185.120, 177.236 189.700, 176.542 199.361, 173.960 203.032, 172.979 212.284, 169.182 214.718, 168.183 223.258, 162.980 224.309, 162.340 231.435, 155.674 235.824, 148.441 237.306, 140.918 233.154, 131.821 229.002, 122.723 224.850, 113.626 220.698, 104.529 216.546, 95.431 212.394, 86.334 208.242, 77.237 204.090, 68.139 199.938, 59.042 195.786, 49.945 191.635, 40.847 189.441, 36.041 188.879, 38.895 187.214, 41.638 184.511, 44.167 180.873, 46.384 176.440, 48.203 171.383, 49.554 165.895, 50.387 160.188, 50.668 154.481, 50.387 148.994, 49.554 143.936, 48.203 139.503, 46.384 135.865, 44.167 133.162, 41.638 131.497, 38.895 130.935, 36.041 126.783, 45.139 122.631, 54.236 118.479, 63.333 114.328, 72.431 110.176, 81.528 106.024, 90.625 101.872, 99.723 97.720, 108.820 93.568, 117.917 89.416, 127.015 85.264, 136.112 83.071, 140.918 84.552, 148.441 88.941, 155.674 " />
|
||||
<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 160.188, 21.446 154.493, 21.727 149.018, 22.557 143.971, 23.906 139.548, 25.721 135.917, 27.933 133.220, 30.456 131.559, 33.194 130.998, 36.041 131.559, 38.889 133.220, 41.626 135.917, 44.150 139.548, 46.361 143.971, 48.177 149.018, 49.525 154.493, 50.356 160.188, 50.636 165.883, 50.356 171.359, 49.525 176.405, 48.177 180.829, 46.361 184.459, 44.150 187.156, 41.626 188.817, 38.889 189.378, 36.041 188.817, 33.194 187.156, 30.456 184.459, 27.933 180.829, 25.721 176.405, 23.906 171.359, 22.557 165.883, 21.727 160.188, 21.446 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 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,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.333" fill="none" stroke-width="3.0" stroke-linecap="butt" stroke-opacity="1.0" stroke="rgb(0, 0, 0)" stroke-linejoin="miter" d=" M 201.919, 108.039 198.923, 99.664 195.119, 91.623 193.302, 88.591 184.641, 93.591 175.981, 98.591 170.168, 101.947 171.536, 104.229 174.217, 109.898 176.329, 115.802 177.853, 121.885 178.773, 128.087 179.081, 134.351 178.773, 140.614 177.853, 146.817 176.329, 152.899 174.217, 158.803 171.536, 164.472 168.312, 169.851 164.577, 174.887 160.365, 179.534 155.719, 183.745 150.682, 187.480 145.304, 190.704 139.635, 193.385 133.731, 195.498 127.648, 197.021 121.445, 197.941 115.182, 198.249 114.613, 198.221 114.613, 208.221 114.613, 218.221 114.613, 224.962 115.182, 224.990 124.067, 224.554 132.865, 223.249 141.494, 221.087 149.869, 218.091 157.910, 214.288 165.539, 209.715 172.684, 204.416 179.274, 198.443 185.248, 191.852 190.546, 184.707 195.119, 177.078 198.923, 169.037 201.919, 160.662 204.080, 152.034 205.386, 143.235 205.822, 134.351 205.386, 125.466 204.080, 116.668 201.919, 108.039 " />
|
||||
<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 139.635, 193.385 145.304, 190.704 150.682, 187.480 155.719, 183.745 160.365, 179.534 164.577, 174.887 168.312, 169.851 171.536, 164.472 174.217, 158.803 176.329, 152.899 177.853, 146.817 178.773, 140.614 179.081, 134.351 178.773, 128.087 177.853, 121.885 176.329, 115.802 174.217, 109.898 171.536, 104.229 170.168, 101.947 161.508, 106.947 152.847, 111.947 149.399, 113.938 150.363, 115.546 152.037, 119.085 153.356, 122.771 154.307, 126.568 154.881, 130.441 155.073, 134.351 154.881, 138.261 154.307, 142.133 153.356, 145.930 152.037, 149.616 150.363, 153.155 148.350, 156.513 146.018, 159.657 143.389, 162.558 140.489, 165.187 137.345, 167.519 133.987, 169.531 130.448, 171.205 126.762, 172.524 122.965, 173.475 119.092, 174.049 115.182, 174.242 114.613, 174.214 114.613, 184.214 114.613, 194.214 114.613, 198.221 115.182, 198.249 121.445, 197.941 127.648, 197.021 133.731, 195.498 139.635, 193.385 " />
|
||||
<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 114.879, 134.338 114.765, 134.591 114.681, 134.848 114.630, 135.099 114.613, 135.337 114.613, 145.337 114.613, 155.337 114.613, 165.337 114.613, 174.214 115.182, 174.242 119.092, 174.049 122.965, 173.475 126.762, 172.524 130.448, 171.205 133.987, 169.531 137.345, 167.519 140.489, 165.187 143.389, 162.558 146.018, 159.657 148.350, 156.513 150.363, 153.155 152.037, 149.616 153.356, 145.930 154.307, 142.133 154.881, 138.261 155.073, 134.351 154.881, 130.441 154.307, 126.568 153.356, 122.771 152.037, 119.085 150.363, 115.546 149.399, 113.938 140.739, 118.938 132.078, 123.938 123.418, 128.938 115.752, 133.364 115.554, 133.498 115.362, 133.668 115.182, 133.869 115.020, 134.095 114.879, 134.338 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -11,7 +11,9 @@
|
||||
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";
|
||||
@@ -25,6 +27,10 @@
|
||||
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 = [
|
||||
@@ -95,6 +101,12 @@
|
||||
<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">
|
||||
|
||||
@@ -7,6 +7,20 @@
|
||||
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)];
|
||||
}
|
||||
@@ -14,6 +28,13 @@
|
||||
|
||||
<ScriptBlock on:add add={add} label={label}>
|
||||
{#each value as line, i}
|
||||
<ScriptLine bind:value={line} on:delete={() => deleteLine(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>
|
||||
@@ -12,6 +12,7 @@
|
||||
| { kind: "device.edit", op: DeviceEditOp }
|
||||
| { kind: "script.edit" }
|
||||
| { kind: "trigger.edit" }
|
||||
| { kind: "device.quickassign" }
|
||||
|
||||
export interface ModalContextState {
|
||||
modal: Writable<ModalSelection>
|
||||
|
||||
@@ -87,11 +87,16 @@
|
||||
|
||||
let shortestIdPrefix = firstDevice.id;
|
||||
let shortestNamePrefix = firstDevice.name;
|
||||
let nonames = false;
|
||||
for (const device of $deviceList) {
|
||||
if (!$selectedMap[device.id]) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (device.name === "") {
|
||||
nonames = true;
|
||||
}
|
||||
|
||||
let longestIdMatch = 0;
|
||||
for (let i = 1; i <= shortestIdPrefix.length; ++i) {
|
||||
if (device.id.startsWith(shortestIdPrefix.slice(0, i))) {
|
||||
@@ -111,7 +116,9 @@
|
||||
}
|
||||
|
||||
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(",")}}`)
|
||||
if (!nonames) {
|
||||
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);
|
||||
|
||||
@@ -132,11 +132,19 @@
|
||||
state.update(state => {
|
||||
if (patch.full != null) {
|
||||
state = patch.full;
|
||||
for (const deviceId in state.devices) {
|
||||
if (deviceId.startsWith("nanoleaf:") && deviceId.split(":").length === 2) {
|
||||
delete state.devices[deviceId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const deviceId in patch.devices) {
|
||||
if (!patch.devices.hasOwnProperty(deviceId)) {
|
||||
continue
|
||||
continue;;
|
||||
}
|
||||
if (deviceId.startsWith("nanoleaf:") && deviceId.split(":").length === 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.devices = {...state.devices}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<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";
|
||||
@@ -17,12 +16,11 @@
|
||||
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();
|
||||
const { deviceList, state, assignmentList } = getStateContext();
|
||||
|
||||
let show: boolean = false;
|
||||
let match: string = "";
|
||||
@@ -151,13 +149,28 @@
|
||||
await runCommand({assign: { match, effect: fromEffectRaw(newEffect) }});
|
||||
}
|
||||
if (enableRoom) {
|
||||
await runCommand({addAlias: { match, alias: `lucifer:room:${newRoom || customRoom}` }});
|
||||
if (newRoom || customRoom) {
|
||||
await runCommand({addAlias: { match, alias: `lucifer:room:${newRoom || customRoom}` }});
|
||||
} else {
|
||||
const group = $selectedList.flatMap(id => $state.devices[id].aliases).find(a => a.startsWith("lucifer:room:"));
|
||||
if (group != null) {
|
||||
await runCommand({removeAlias: { match, alias: group }});
|
||||
}
|
||||
}
|
||||
|
||||
enableRoom = false;
|
||||
shouldWait = match.startsWith("lucifer:room:");
|
||||
newRoom = newRoom || customRoom;
|
||||
}
|
||||
if (enableGroup) {
|
||||
await runCommand({addAlias: { match, alias: `lucifer:group:${newGroup || customGroup}` }});
|
||||
if (newGroup || customGroup) {
|
||||
await runCommand({addAlias: { match, alias: `lucifer:group:${newGroup || customGroup}` }});
|
||||
} else {
|
||||
const group = $selectedList.flatMap(id => $state.devices[id].aliases).find(a => a.startsWith("lucifer:group:"));
|
||||
if (group != null) {
|
||||
await runCommand({removeAlias: { match, alias: group }});
|
||||
}
|
||||
}
|
||||
enableGroup = false;
|
||||
shouldWait = match.startsWith("lucifer:group:");
|
||||
newGroup = newGroup || customGroup;
|
||||
@@ -209,14 +222,16 @@
|
||||
.filter(k => k.startsWith("lucifer:room:"))
|
||||
.sort()
|
||||
.filter((v, i, a) => v !== a[i-1])
|
||||
.map(r => r.slice("lucifer:room:".length));
|
||||
.map(r => r.slice("lucifer:room:".length))
|
||||
.filter(r => r !== "");
|
||||
|
||||
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));
|
||||
.map(g => g.slice("lucifer:group:".length))
|
||||
.filter(g => g !== "");
|
||||
|
||||
$: {
|
||||
if ($modal.kind === "device.edit") {
|
||||
@@ -229,6 +244,9 @@
|
||||
$: if (!$selectedMasks.includes(match)) {
|
||||
match = $selectedMasks[0];
|
||||
}
|
||||
|
||||
$: newTags = newTags.filter(nt => nt);
|
||||
$: newRoles = newRoles.filter(nr => nr);
|
||||
</script>
|
||||
|
||||
<form novalidate on:submit|preventDefault={onSubmit}>
|
||||
@@ -297,21 +315,23 @@
|
||||
</HSplitPart>
|
||||
</HSplit>
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableRename} title="Rename">
|
||||
<label for="name">New Name</label>
|
||||
<input type="text" name="name" bind:value={newName} />
|
||||
</ModalSection>
|
||||
{#if $selectedList.length === 1}
|
||||
<ModalSection bind:expanded={enableRename} title="Rename">
|
||||
<label for="name">New Name</label>
|
||||
<input type="text" name="name" bind:value={newName} />
|
||||
</ModalSection>
|
||||
{/if}
|
||||
<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>
|
||||
<option value="">Other Room / Clear</option>
|
||||
</select>
|
||||
{#if newRoom == ""}
|
||||
<label for="customRoom">New Room</label>
|
||||
<input type="text" name="customRoom" bind:value={customRoom} />
|
||||
<input type="text" name="customRoom" placeholder="Leave blank to clear room." bind:value={customRoom} />
|
||||
{/if}
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableGroup} title="Change Group">
|
||||
@@ -320,11 +340,11 @@
|
||||
{#each groupOptions as groupOption}
|
||||
<option value={groupOption}>{groupOption}</option>
|
||||
{/each}
|
||||
<option value="">Create Group</option>
|
||||
<option value="">Other Group / Clear</option>
|
||||
</select>
|
||||
{#if newGroup == ""}
|
||||
<label for="customGroup">New Group</label>
|
||||
<input type="text" name="customGroup" bind:value={customGroup} />
|
||||
<input type="text" name="customGroup" placeholder="Leave blank to clear group." bind:value={customGroup} />
|
||||
{/if}
|
||||
</ModalSection>
|
||||
<ModalSection bind:expanded={enableIcon} title="Change Icon">
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { runCommand } from "$lib/client/lucifer";
|
||||
import ColorPicker from "$lib/components/ColorPicker.svelte";
|
||||
import Modal from "$lib/components/Modal.svelte";
|
||||
import ModalBody from "$lib/components/ModalBody.svelte";
|
||||
import Range from "$lib/components/exetrnal/Range.svelte";
|
||||
import { getModalContext } from "$lib/contexts/ModalContext.svelte";
|
||||
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
|
||||
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
||||
import { toEffectRaw } from "$lib/models/assignment";
|
||||
import { parseColor, type Color, stringifyColor } from "$lib/models/color";
|
||||
|
||||
const { modal } = getModalContext();
|
||||
const { selectedMasks, selectedMap } = getSelectedContext();
|
||||
const { assignmentList } = getStateContext();
|
||||
|
||||
let show: boolean = false;
|
||||
let color: Color = { k: 2750 };
|
||||
let intensity: number = 50;
|
||||
let match: string = "";
|
||||
let disabled: boolean = false;
|
||||
|
||||
async function submitForm() {
|
||||
disabled = true;
|
||||
|
||||
try {
|
||||
await runCommand({assign: { match, effect: { solid: {
|
||||
interleave: 0,
|
||||
animationMs: 0,
|
||||
states: [ { color: stringifyColor(color)!, intensity: (intensity / 100), power: intensity > 0, temperature: null } ]
|
||||
} } }});
|
||||
} catch (err) {}
|
||||
|
||||
disabled = false;
|
||||
}
|
||||
|
||||
function initForm() {
|
||||
show = true;
|
||||
let mostPopularEffect = 0;
|
||||
for (const assignment of $assignmentList) {
|
||||
const selectedCount = assignment.deviceIds?.filter(id => $selectedMap[id]).length || 0;
|
||||
if (selectedCount > mostPopularEffect) {
|
||||
color = parseColor(toEffectRaw(assignment.effect).states.find(s => s.color)?.color || "k:2750") || { k: 2750 };
|
||||
intensity = (toEffectRaw(assignment.effect).states.find(s => s.intensity)?.intensity || 0.5) * 100;
|
||||
|
||||
mostPopularEffect = selectedCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($modal.kind === "device.quickassign") {
|
||||
initForm()
|
||||
} else {
|
||||
show = false;
|
||||
}
|
||||
|
||||
$: if (!$selectedMasks.includes(match)) {
|
||||
match = $selectedMasks[0];
|
||||
}
|
||||
</script>
|
||||
|
||||
<form novalidate on:submit|preventDefault={submitForm}>
|
||||
<Modal
|
||||
closable {show} {disabled}
|
||||
titleText="Quick Assign"
|
||||
submitText="Apply"
|
||||
>
|
||||
<ModalBody>
|
||||
<label for="mask">Selection</label>
|
||||
<select bind:value={match}>
|
||||
{#each $selectedMasks as option (option)}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div class="color-name">{stringifyColor(color)}</div>
|
||||
<div class="color-wrapper">
|
||||
<ColorPicker bind:color={color} />
|
||||
</div>
|
||||
<Range bind:value={intensity} />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
|
||||
<style lang="sass">
|
||||
div.color-wrapper
|
||||
padding-top: 0.2em
|
||||
margin-bottom: -0.5em
|
||||
font-size: 3.6em
|
||||
overflow: hidden
|
||||
|
||||
div.color-name
|
||||
font-size: 1.5em
|
||||
text-align: center
|
||||
</style>
|
||||
@@ -73,6 +73,7 @@
|
||||
|
||||
$: scriptNames = Object.keys($state?.scripts||{}).sort();
|
||||
$: loadScript(selectedScript);
|
||||
$: if (!$selectedMasks.includes(runMatch)) { runMatch = ""; }
|
||||
</script>
|
||||
|
||||
<form novalidate on:submit|preventDefault={onSubmit}>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
if (id !== "") {
|
||||
current = toEditableTrigger($state.triggers[id]);
|
||||
} else {
|
||||
current = newEditableTrigger($maskList[0], scriptList?.[0] || "");
|
||||
current = {...current, id: null};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,31 +18,34 @@ export type Effect =
|
||||
| { gradient: { states: State[], animationMs: number, reverse: boolean, interpolate: boolean } }
|
||||
| { pattern: { states: State[], animationMs: number } }
|
||||
| { random: { states: State[], animationMs: number, interpolate: boolean } }
|
||||
| { vrange: { states: State[], variable: string, min: number, max: number } }
|
||||
| { vrange: { states: State[], variable: string, min: number, max: number, interpolate: boolean } }
|
||||
|
||||
export function toEffectRaw(effect?: Effect): EffectRaw {
|
||||
let effectRaw: EffectRaw = { kind: "manual", states: [{color: null, intensity: null, power: null, temperature: null}], animationMs: 0, reverse: false, interpolate: false, min: 30, max: 300, variable: "motion.min", interleave: 0 };
|
||||
|
||||
if (effect != null) {
|
||||
if ("manual" in effect) {
|
||||
return { kind: "solid", states: [effect.manual], animationMs: 0, reverse: false, interpolate: true, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||
}
|
||||
if ("solid" in effect) {
|
||||
return { kind: "solid", ...effect.solid, min: 30, max: 300, variable: "motion.min", interpolate: true, reverse: false}
|
||||
}
|
||||
if ("gradient" in effect) {
|
||||
return { kind: "gradient", ...effect.gradient, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||
}
|
||||
if ("pattern" in effect) {
|
||||
return { kind: "pattern", ...effect.pattern, reverse: false, interpolate: true, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||
}
|
||||
if ("random" in effect) {
|
||||
return { kind: "random", ...effect.random, reverse: false, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||
}
|
||||
if ("vrange" in effect) {
|
||||
return { kind: "vrange", ...effect.vrange, animationMs: 0, interpolate: true, reverse: false, interleave: 0 }
|
||||
effectRaw = { kind: "solid", states: [effect.manual], animationMs: 0, reverse: false, interpolate: true, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||
} else if ("solid" in effect) {
|
||||
effectRaw = { kind: "solid", ...effect.solid, min: 30, max: 300, variable: "motion.min", interpolate: true, reverse: false}
|
||||
} else if ("gradient" in effect) {
|
||||
effectRaw = { kind: "gradient", ...effect.gradient, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||
} else if ("pattern" in effect) {
|
||||
effectRaw = { kind: "pattern", ...effect.pattern, reverse: false, interpolate: true, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||
} else if ("random" in effect) {
|
||||
effectRaw = { kind: "random", ...effect.random, reverse: false, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||
} else if ("vrange" in effect) {
|
||||
effectRaw = { kind: "vrange", ...effect.vrange, animationMs: 0, reverse: false, interleave: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
return { kind: "manual", states: [{color: null, intensity: null, power: null, temperature: null}], animationMs: 0, reverse: false, interpolate: false, min: 30, max: 300, variable: "motion.min", interleave: 0 }
|
||||
effectRaw.states.forEach(state => {
|
||||
if (state.intensity !== null) {
|
||||
state.intensity = Math.floor(state.intensity * 100) / 100;
|
||||
}
|
||||
})
|
||||
|
||||
return effectRaw;
|
||||
}
|
||||
|
||||
export function fromEffectRaw(raw: EffectRaw): Effect {
|
||||
|
||||
@@ -143,12 +143,12 @@ export function toEditableScriptLine(line: ScriptLine): ScriptLineEditable {
|
||||
not: line.if.condition.not || false,
|
||||
value: line.if.condition.value || "",
|
||||
};
|
||||
base.then = line.if.then.map(toEditableScriptLine);
|
||||
base.else = line.if.else.map(toEditableScriptLine);
|
||||
base.then = (line.if.then||[]).map(toEditableScriptLine);
|
||||
base.else = (line.if.else||[]).map(toEditableScriptLine);
|
||||
} else if ("select" in line) {
|
||||
base.kind = "select";
|
||||
Object.assign(base, toMatchKindValue(line.select.match));
|
||||
base.then = line.select.then.map(toEditableScriptLine);
|
||||
base.then = (line.select.then||[]).map(toEditableScriptLine);
|
||||
} else if ("set" in line) {
|
||||
base.kind = "set";
|
||||
base.key = line.set.key
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
import { getSelectedContext } from "$lib/contexts/SelectContext.svelte";
|
||||
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
||||
import DeviceModal from "$lib/modals/DeviceModal.svelte";
|
||||
import QuickAssignModal from "$lib/modals/QuickAssignModal.svelte";
|
||||
import ScriptModal from "$lib/modals/ScriptModal.svelte";
|
||||
import TriggerModal from "$lib/modals/TriggerModal.svelte";
|
||||
|
||||
const {selectedList} = getSelectedContext();
|
||||
const {roomList} = getStateContext();
|
||||
const {selectedList, toggleMultiSelection} = getSelectedContext();
|
||||
const {roomList, deviceList} = getStateContext();
|
||||
const {modal} = getModalContext();
|
||||
|
||||
function handleKeyPress(e: KeyboardEvent) {
|
||||
@@ -30,6 +31,19 @@
|
||||
modal.set({kind: "trigger.edit"});
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'c':
|
||||
if ($selectedList.length) {
|
||||
modal.set({kind: "device.quickassign"});
|
||||
e.preventDefault();
|
||||
}
|
||||
break;
|
||||
case 'a':
|
||||
if ($selectedList.length > 0) {
|
||||
toggleMultiSelection($selectedList);
|
||||
} else {
|
||||
toggleMultiSelection($deviceList.map(d => d.id));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +75,7 @@
|
||||
<DeviceModal />
|
||||
<ScriptModal />
|
||||
<TriggerModal />
|
||||
<QuickAssignModal />
|
||||
|
||||
<style>
|
||||
div.page {
|
||||
|
||||
Executable
BIN
Binary file not shown.
+58
-41
@@ -348,7 +348,7 @@ func (b *Bridge) SetStates(patch map[string]device.State) {
|
||||
func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
|
||||
hwEvents, err := b.RefreshAll()
|
||||
if err != nil {
|
||||
return errors.New("failed to connect to bridge")
|
||||
return fmt.Errorf("failed to connect to bridge: %s", err)
|
||||
}
|
||||
|
||||
bus.RunEvent(events.Log{
|
||||
@@ -436,6 +436,8 @@ func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
|
||||
}
|
||||
|
||||
func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
hadFirst := make(map[string]bool, 16)
|
||||
|
||||
for range b.triggerCongruenceCheckCh {
|
||||
// I hate that I have to do this, but the SSE is not being reliable.
|
||||
_, _ = b.RefreshAll()
|
||||
@@ -473,52 +475,50 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
update := ResourceUpdate{}
|
||||
|
||||
// Handle power first
|
||||
if desired.Power != nil && active.Power != nil && *desired.Power != *active.Power {
|
||||
if desired.Power != nil && active.Power != nil && (!hadFirst[*lightID] || *desired.Power != *active.Power) {
|
||||
update.Power = gentools.Ptr(*desired.Power)
|
||||
updated = true
|
||||
}
|
||||
|
||||
// Only do the rest if there's power.
|
||||
if desired.Power == nil || *desired.Power {
|
||||
if active.Color != nil && desired.Color != nil {
|
||||
ac := *active.Color
|
||||
dc := *desired.Color
|
||||
if active.Color != nil && desired.Color != nil {
|
||||
ac := *active.Color
|
||||
dc := *desired.Color
|
||||
|
||||
if !dc.IsKelvin() || !colorFlags[id].IsWarmWhite() {
|
||||
dc, _ = dc.ToXY()
|
||||
dc.XY = gentools.Ptr(light.Color.Gamut.Conform(*dc.XY))
|
||||
if !dc.IsKelvin() || !colorFlags[id].IsWarmWhite() {
|
||||
dc, _ = dc.ToXY()
|
||||
dc.XY = gentools.Ptr(light.Color.Gamut.Conform(*dc.XY))
|
||||
}
|
||||
|
||||
if dc.XY != nil {
|
||||
if ac.K != nil {
|
||||
ac.K = gentools.Ptr(1000000 / (1000000 / *ac.K))
|
||||
}
|
||||
|
||||
if dc.XY != nil {
|
||||
if ac.K != nil {
|
||||
ac.K = gentools.Ptr(1000000 / (1000000 / *ac.K))
|
||||
}
|
||||
|
||||
acXY, _ := ac.ToXY()
|
||||
dist := dc.XY.DistanceTo(*acXY.XY)
|
||||
if dist > 0.0002 {
|
||||
update.ColorXY = gentools.Ptr(*dc.XY)
|
||||
updated = true
|
||||
}
|
||||
} else {
|
||||
dcMirek := 1000000 / *dc.K
|
||||
if dcMirek < light.ColorTemperature.MirekSchema.MirekMinimum {
|
||||
dcMirek = light.ColorTemperature.MirekSchema.MirekMinimum
|
||||
} else if dcMirek > light.ColorTemperature.MirekSchema.MirekMaximum {
|
||||
dcMirek = light.ColorTemperature.MirekSchema.MirekMaximum
|
||||
}
|
||||
acMirek := 0
|
||||
if ac.K != nil {
|
||||
acMirek = 1000000 / *ac.K
|
||||
}
|
||||
if acMirek != dcMirek {
|
||||
update.Mirek = &dcMirek
|
||||
updated = true
|
||||
}
|
||||
acXY, _ := ac.ToXY()
|
||||
dist := dc.XY.DistanceTo(*acXY.XY)
|
||||
if dist > 0.0002 || !hadFirst[*lightID] {
|
||||
update.ColorXY = gentools.Ptr(*dc.XY)
|
||||
updated = true
|
||||
}
|
||||
} else {
|
||||
dcMirek := 1000000 / *dc.K
|
||||
if dcMirek < light.ColorTemperature.MirekSchema.MirekMinimum {
|
||||
dcMirek = light.ColorTemperature.MirekSchema.MirekMinimum
|
||||
} else if dcMirek > light.ColorTemperature.MirekSchema.MirekMaximum {
|
||||
dcMirek = light.ColorTemperature.MirekSchema.MirekMaximum
|
||||
}
|
||||
acMirek := 0
|
||||
if ac.K != nil {
|
||||
acMirek = 1000000 / *ac.K
|
||||
}
|
||||
if acMirek != dcMirek || !hadFirst[*lightID] {
|
||||
update.Mirek = &dcMirek
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
if active.Intensity != nil && desired.Intensity != nil {
|
||||
if active.Intensity != nil && desired.Intensity != nil || !hadFirst[*lightID] {
|
||||
if math.Abs(*active.Intensity-*desired.Intensity) >= 0.01 {
|
||||
update.Brightness = gentools.Ptr(*desired.Intensity * 100)
|
||||
updated = true
|
||||
@@ -529,11 +529,13 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
if updated {
|
||||
update.TransitionDuration = gentools.Ptr(time.Millisecond * 101)
|
||||
updates["light/"+*lightID] = update
|
||||
hadFirst[*lightID] = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
timeout, cancel := context.WithTimeout(ctx, time.Millisecond*500*time.Duration(len(updates)))
|
||||
timeout, cancel := context.WithTimeout(ctx, time.Second*60)
|
||||
hadPower := false
|
||||
|
||||
eg, ctx := errgroup.WithContext(timeout)
|
||||
for key := range updates {
|
||||
@@ -541,8 +543,19 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
split := strings.SplitN(key, "/", 2)
|
||||
link := ResourceLink{Kind: split[0], ID: split[1]}
|
||||
|
||||
if update.Power != nil {
|
||||
hadPower = true
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
return b.client.UpdateResource(ctx, link, update)
|
||||
if update.Power != nil {
|
||||
return b.client.UpdateResource(ctx, link, ResourceUpdate{
|
||||
Power: update.Power,
|
||||
TransitionDuration: update.TransitionDuration,
|
||||
})
|
||||
} else {
|
||||
return b.client.UpdateResource(ctx, link, update)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -551,11 +564,15 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
log.Println("Failed to run update", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
if hadPower {
|
||||
b.triggerCongruenceCheck()
|
||||
}
|
||||
|
||||
// Wait the remaining time for the rate limit
|
||||
<-rateLimit
|
||||
cancel()
|
||||
}
|
||||
|
||||
// Wait the remaining time for the rate limit
|
||||
<-rateLimit
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ func (w *WifiBridge) refresh() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func (w *WifiBridge) getTemperature() (float64, error) {
|
||||
|
||||
req, err := http.NewRequest(
|
||||
"GET",
|
||||
"http://%s/set-temperature",
|
||||
fmt.Sprintf("http://%s/set-temperature", w.IP),
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -144,7 +144,7 @@ func (w *WifiBridge) writeTemperature() error {
|
||||
|
||||
req, err := http.NewRequest(
|
||||
"POST",
|
||||
"http://%s/set-temperature",
|
||||
fmt.Sprintf("http://%s/set-temperature", w.IP),
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -46,7 +46,7 @@ func (b *bridge) HardwareEvents() []lucifer3.Event {
|
||||
|
||||
shapeType, shapeTypeOK := shapeTypeMap[panel.ShapeType]
|
||||
if !shapeTypeOK {
|
||||
shapeType = "Unknown"
|
||||
shapeType = fmt.Sprint("Unknown:", panel.ShapeType)
|
||||
}
|
||||
|
||||
shapeIcon, shapeIconOK := shapeIconMap[panel.ShapeType]
|
||||
@@ -65,7 +65,7 @@ func (b *bridge) HardwareEvents() []lucifer3.Event {
|
||||
|
||||
results = append(results, events.HardwareState{
|
||||
ID: panel.FullID,
|
||||
InternalName: fmt.Sprintf("%s %d (%s)", shapeType, i+1, strings.SplitN(panel.FullID, ":", 3)[2]),
|
||||
InternalName: fmt.Sprintf("%s %03d", shapeType, i+1),
|
||||
SupportFlags: device.SFlagPower | device.SFlagIntensity | device.SFlagColor,
|
||||
ColorFlags: device.CFlagRGB,
|
||||
Buttons: []string{"Touch"},
|
||||
|
||||
@@ -138,6 +138,7 @@ var shapeTypeMap = map[int]string{
|
||||
8: "Triangle",
|
||||
9: "Mini Triangle",
|
||||
12: "Shapes Controller",
|
||||
15: "Element Corner",
|
||||
}
|
||||
|
||||
var shapeIconMap = map[int]string{
|
||||
@@ -150,6 +151,7 @@ var shapeIconMap = map[int]string{
|
||||
8: "shape_triangle",
|
||||
9: "shape_triangle",
|
||||
12: "shape_hexagon",
|
||||
15: "shape_hexagon_corner",
|
||||
}
|
||||
|
||||
var shapeWidthMap = map[int]int{
|
||||
|
||||
@@ -24,7 +24,6 @@ func (d *Data) WithPatch(patches ...Patch) Data {
|
||||
if patch.Device != nil {
|
||||
pd := d.ensureDevice(patch.Device.ID)
|
||||
|
||||
gentools.ApplyUpdate(&pd.Name, patch.Device.Name)
|
||||
gentools.ApplyUpdatePtr(&pd.HWState, patch.Device.HWState)
|
||||
gentools.ApplyUpdatePtr(&pd.HWMetadata, patch.Device.HWMetadata)
|
||||
gentools.ApplyUpdatePtr(&pd.DesiredState, patch.Device.DesiredState)
|
||||
@@ -66,6 +65,22 @@ func (d *Data) WithPatch(patches ...Patch) Data {
|
||||
pd.Icon = patch.Device.HWMetadata.Icon
|
||||
}
|
||||
}
|
||||
if patch.Device.HWState != nil {
|
||||
hasName := false
|
||||
for _, alias := range pd.Aliases {
|
||||
if strings.HasPrefix(alias, "lucifer:name:") {
|
||||
hasName = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasName {
|
||||
pd.Aliases = append(pd.Aliases, "lucifer:name:"+patch.Device.HWState.InternalName)
|
||||
}
|
||||
if pd.Name == "" {
|
||||
pd.Name = patch.Device.HWState.InternalName
|
||||
}
|
||||
}
|
||||
if patch.Device.ClearAssignment {
|
||||
pd.Assignment = nil
|
||||
}
|
||||
@@ -148,7 +163,7 @@ func (d *Data) ensureDevice(id string) Device {
|
||||
if device, ok := d.Devices[id]; ok {
|
||||
return device
|
||||
} else {
|
||||
return Device{ID: id}
|
||||
return Device{ID: id, Aliases: make([]string, 0)}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ func (e Patch) VerboseKey() string {
|
||||
func (e Patch) EventDescription() string {
|
||||
if e.Device != nil {
|
||||
switch {
|
||||
case e.Device.Name != nil:
|
||||
return fmt.Sprintf("uistate.Patch(device=%s, name=%s)", e.Device.ID, *e.Device.Name)
|
||||
case e.Device.DesiredState != nil:
|
||||
return fmt.Sprintf("uistate.Patch(device=%s, desiredState=%s)", e.Device.ID, e.Device.DesiredState.String())
|
||||
case e.Device.HWState != nil:
|
||||
@@ -56,7 +54,6 @@ func (e Patch) EventDescription() string {
|
||||
|
||||
type DevicePatch struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
HWMetadata *events.HardwareMetadata `json:"hwMetadata,omitempty"`
|
||||
HWState *events.HardwareState `json:"hwState,omitempty"`
|
||||
DesiredState *device.State `json:"desiredState,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user