feat: implement real-time LED strip preview
- Add LED strip visualization around display previews - Show real-time color status for each LED pixel - Support multi-display LED strip configurations - Use elegant 16px thin LED strip design - Real-time LED color sync via WebSocket - Responsive layout with display scaling support
This commit is contained in:
@ -2,7 +2,7 @@ import { Routes, Route } from '@solidjs/router';
|
||||
import { LedStripConfiguration } from './components/led-strip-configuration/led-strip-configuration';
|
||||
import { WhiteBalance } from './components/white-balance/white-balance';
|
||||
import { createEffect } from 'solid-js';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { setLedStripStore } from './stores/led-strip.store';
|
||||
import { LedStripConfigContainer } from './models/led-strip-config';
|
||||
import { InfoIndex } from './components/info/info-index';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, For, createEffect, createSignal } from 'solid-js';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import debug from 'debug';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { DisplayState, RawDisplayState } from '../../models/display-state.model';
|
||||
import { DisplayStateCard } from './display-state-card';
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { Component, For, createEffect, createSignal } from 'solid-js';
|
||||
import { BoardInfo } from '../../models/board-info.model';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import debug from 'debug';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { BoardInfoPanel } from './board-info-panel';
|
||||
|
||||
const logger = debug('app:components:info:board-index');
|
||||
|
@ -23,7 +23,6 @@ export const DisplayView: Component<DisplayViewProps> = (props) => {
|
||||
}));
|
||||
|
||||
const ledStripConfigs = createMemo(() => {
|
||||
console.log('ledStripConfigs', ledStripStore.strips);
|
||||
return ledStripStore.strips.filter((c) => c.display_id === props.display.id);
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createEffect, onCleanup } from 'solid-js';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { DisplayView } from './display-view';
|
||||
import { DisplayListContainer } from './display-list-container';
|
||||
import { displayStore, setDisplayStore } from '../../stores/display.store';
|
||||
@ -22,7 +22,6 @@ export const LedStripConfiguration = () => {
|
||||
});
|
||||
});
|
||||
invoke<LedStripConfigContainer>('read_led_strip_configs').then((configs) => {
|
||||
console.log(configs);
|
||||
setLedStripStore(configs);
|
||||
});
|
||||
});
|
||||
@ -31,7 +30,6 @@ export const LedStripConfiguration = () => {
|
||||
createEffect(() => {
|
||||
const unlisten = listen('config_changed', (event) => {
|
||||
const { strips, mappers } = event.payload as LedStripConfigContainer;
|
||||
console.log(event.payload);
|
||||
setLedStripStore({
|
||||
strips,
|
||||
mappers,
|
||||
@ -46,17 +44,11 @@ export const LedStripConfiguration = () => {
|
||||
// listen to led_colors_changed event
|
||||
createEffect(() => {
|
||||
const unlisten = listen<Uint8ClampedArray>('led_colors_changed', (event) => {
|
||||
console.log('Received led_colors_changed event:', {
|
||||
hidden: window.document.hidden,
|
||||
colorsLength: event.payload.length,
|
||||
firstFewColors: Array.from(event.payload.slice(0, 12))
|
||||
});
|
||||
if (!window.document.hidden) {
|
||||
const colors = event.payload;
|
||||
setLedStripStore({
|
||||
colors,
|
||||
});
|
||||
console.log('Updated ledStripStore.colors with length:', colors.length);
|
||||
}
|
||||
});
|
||||
|
||||
@ -68,17 +60,11 @@ export const LedStripConfiguration = () => {
|
||||
// listen to led_sorted_colors_changed event
|
||||
createEffect(() => {
|
||||
const unlisten = listen<Uint8ClampedArray>('led_sorted_colors_changed', (event) => {
|
||||
console.log('Received led_sorted_colors_changed event:', {
|
||||
hidden: window.document.hidden,
|
||||
sortedColorsLength: event.payload.length,
|
||||
firstFewSortedColors: Array.from(event.payload.slice(0, 12))
|
||||
});
|
||||
if (!window.document.hidden) {
|
||||
const sortedColors = event.payload;
|
||||
setLedStripStore({
|
||||
sortedColors,
|
||||
});
|
||||
console.log('Updated ledStripStore.sortedColors with length:', sortedColors.length);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
@ -34,7 +34,7 @@ export const Pixel: Component<PixelProps> = (props) => {
|
||||
title={props.color}
|
||||
>
|
||||
<div
|
||||
class="absolute top-1/2 -translate-y-1/2 h-2.5 w-2.5 rounded-full ring-1 ring-stone-300"
|
||||
class="absolute top-1/2 -translate-y-1/2 h-2 w-2 rounded-full ring-1 ring-stone-300/30"
|
||||
style={style()}
|
||||
/>
|
||||
</div>
|
||||
@ -60,27 +60,46 @@ export const LedStripPart: Component<LedStripPartProps> = (props) => {
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
console.log(`LED strip not found for display ${localProps.config.display_id}, border ${localProps.config.border}`);
|
||||
console.log('🔍 LED: Strip config not found', {
|
||||
displayId: localProps.config.display_id,
|
||||
border: localProps.config.border,
|
||||
availableStrips: ledStripStore.strips.length
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const mapper = ledStripStore.mappers[index];
|
||||
if (!mapper) {
|
||||
console.log(`Mapper not found for index ${index}`);
|
||||
console.log('🔍 LED: Mapper not found', { index, mappersCount: ledStripStore.mappers.length });
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = mapper.pos * 3;
|
||||
console.log(`Updating LED strip colors for ${localProps.config.border}, offset: ${offset}, colors length: ${ledStripStore.colors.length}`);
|
||||
const offset = mapper.start * 3;
|
||||
|
||||
console.log('🎨 LED: Updating colors', {
|
||||
displayId: localProps.config.display_id,
|
||||
border: localProps.config.border,
|
||||
stripLength: localProps.config.len,
|
||||
mapperPos: mapper.pos,
|
||||
offset,
|
||||
colorsArrayLength: ledStripStore.colors.length,
|
||||
firstFewColors: Array.from(ledStripStore.colors.slice(offset, offset + 9))
|
||||
});
|
||||
|
||||
const colors = new Array(localProps.config.len).fill(null).map((_, i) => {
|
||||
const index = offset + i * 3;
|
||||
return `rgb(${ledStripStore.colors[index]}, ${ledStripStore.colors[index + 1]}, ${
|
||||
ledStripStore.colors[index + 2]
|
||||
})`;
|
||||
const r = ledStripStore.colors[index] || 0;
|
||||
const g = ledStripStore.colors[index + 1] || 0;
|
||||
const b = ledStripStore.colors[index + 2] || 0;
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
});
|
||||
|
||||
console.log('🎨 LED: Generated colors', {
|
||||
border: localProps.config.border,
|
||||
colorsCount: colors.length,
|
||||
sampleColors: colors.slice(0, 3)
|
||||
});
|
||||
|
||||
console.log(`Generated ${colors.length} colors for ${localProps.config.border}:`, colors.slice(0, 3));
|
||||
setColors(colors);
|
||||
});
|
||||
|
||||
@ -124,7 +143,7 @@ export const LedStripPart: Component<LedStripPartProps> = (props) => {
|
||||
{...rootProps}
|
||||
ref={setAnchor}
|
||||
class={
|
||||
'flex rounded-full flex-nowrap justify-around items-center overflow-hidden ' +
|
||||
'flex rounded-full flex-nowrap justify-around items-center overflow-hidden bg-gray-800/20 border border-gray-600/30 min-h-[16px] min-w-[16px] ' +
|
||||
rootProps.class
|
||||
}
|
||||
classList={{
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
} from 'solid-js';
|
||||
import { LedStripConfig, LedStripPixelMapper } from '../../models/led-strip-config';
|
||||
import { ledStripStore } from '../../stores/led-strip.store';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { LedStripConfigurationContext } from '../../contexts/led-strip-configuration.context';
|
||||
import background from '../../assets/transparent-grid-background.svg?url';
|
||||
|
||||
|
@ -17,6 +17,10 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||
let canvas: HTMLCanvasElement;
|
||||
let root: HTMLDivElement;
|
||||
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
|
||||
|
||||
// Cache temporary canvas for scaling
|
||||
let tempCanvas: HTMLCanvasElement | null = null;
|
||||
let tempCtx: CanvasRenderingContext2D | null = null;
|
||||
const [drawInfo, setDrawInfo] = createSignal({
|
||||
drawX: 0,
|
||||
drawY: 0,
|
||||
@ -29,9 +33,134 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||
height: number;
|
||||
} | null>(null);
|
||||
const [hidden, setHidden] = createSignal(false);
|
||||
const [isLoading, setIsLoading] = createSignal(false);
|
||||
let isMounted = true;
|
||||
|
||||
// Fetch screenshot data from backend
|
||||
const fetchScreenshot = async () => {
|
||||
console.log('📸 FETCH: Starting screenshot fetch', {
|
||||
isLoading: isLoading(),
|
||||
isMounted,
|
||||
hidden: hidden(),
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
if (isLoading()) {
|
||||
console.log('⏳ FETCH: Already loading, skipping');
|
||||
return; // Skip if already loading
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const timestamp = Date.now();
|
||||
const response = await fetch(`ambient-light://displays/${localProps.displayId}?width=400&height=225&t=${timestamp}`);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('❌ FETCH: Screenshot fetch failed', response.status, response.statusText);
|
||||
const errorText = await response.text();
|
||||
console.error('❌ FETCH: Error response body:', errorText);
|
||||
return;
|
||||
}
|
||||
|
||||
const width = parseInt(response.headers.get('X-Image-Width') || '400');
|
||||
const height = parseInt(response.headers.get('X-Image-Height') || '225');
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = new Uint8ClampedArray(arrayBuffer);
|
||||
const expectedSize = width * height * 4;
|
||||
|
||||
|
||||
|
||||
// Validate buffer size
|
||||
if (buffer.length !== expectedSize) {
|
||||
console.error('❌ FETCH: Invalid buffer size!', {
|
||||
received: buffer.length,
|
||||
expected: expectedSize,
|
||||
ratio: buffer.length / expectedSize
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📊 FETCH: Setting image data', { width, height, bufferSize: buffer.length });
|
||||
|
||||
setImageData({
|
||||
buffer,
|
||||
width,
|
||||
height
|
||||
});
|
||||
|
||||
// Use setTimeout to ensure the signal update has been processed
|
||||
setTimeout(() => {
|
||||
console.log('🖼️ FETCH: Triggering draw after data set');
|
||||
draw(false);
|
||||
}, 0);
|
||||
|
||||
// Schedule next frame after rendering is complete
|
||||
const shouldContinue = !hidden() && isMounted;
|
||||
console.log('🔄 FETCH: Scheduling next frame', {
|
||||
hidden: hidden(),
|
||||
isMounted,
|
||||
shouldContinue,
|
||||
nextFrameDelay: '1000ms'
|
||||
});
|
||||
|
||||
if (shouldContinue) {
|
||||
setTimeout(() => {
|
||||
if (isMounted) {
|
||||
console.log('🔄 FETCH: Starting next frame');
|
||||
fetchScreenshot();
|
||||
} else {
|
||||
console.log('❌ FETCH: Component unmounted, stopping loop');
|
||||
}
|
||||
}, 1000); // Wait 1 second before next frame
|
||||
} else {
|
||||
console.log('❌ FETCH: Loop stopped - component hidden or unmounted');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ FETCH: Error fetching screenshot:', error);
|
||||
// Even on error, schedule next frame
|
||||
const shouldContinueOnError = !hidden() && isMounted;
|
||||
console.log('🔄 FETCH: Error recovery - scheduling next frame', {
|
||||
error: error.message,
|
||||
shouldContinue: shouldContinueOnError,
|
||||
nextFrameDelay: '2000ms'
|
||||
});
|
||||
|
||||
if (shouldContinueOnError) {
|
||||
setTimeout(() => {
|
||||
if (isMounted) {
|
||||
console.log('🔄 FETCH: Retrying after error');
|
||||
fetchScreenshot();
|
||||
}
|
||||
}, 2000); // Wait longer on error
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetSize = () => {
|
||||
const aspectRatio = canvas.width / canvas.height;
|
||||
console.log('📏 CANVAS: Resizing', {
|
||||
rootClientWidth: root.clientWidth,
|
||||
rootClientHeight: root.clientHeight,
|
||||
oldCanvasWidth: canvas.width,
|
||||
oldCanvasHeight: canvas.height
|
||||
});
|
||||
|
||||
// Set canvas size first
|
||||
canvas.width = root.clientWidth;
|
||||
canvas.height = root.clientHeight;
|
||||
|
||||
console.log('📏 CANVAS: Size set', {
|
||||
newCanvasWidth: canvas.width,
|
||||
newCanvasHeight: canvas.height
|
||||
});
|
||||
|
||||
// Use a default aspect ratio if canvas dimensions are invalid
|
||||
const aspectRatio = (canvas.width > 0 && canvas.height > 0)
|
||||
? canvas.width / canvas.height
|
||||
: 16 / 9; // Default 16:9 aspect ratio
|
||||
|
||||
const drawWidth = Math.round(
|
||||
Math.min(root.clientWidth, root.clientHeight * aspectRatio),
|
||||
@ -50,132 +179,114 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||
drawHeight,
|
||||
});
|
||||
|
||||
canvas.width = root.clientWidth;
|
||||
canvas.height = root.clientHeight;
|
||||
|
||||
|
||||
draw(true);
|
||||
};
|
||||
|
||||
const draw = (cached: boolean = false) => {
|
||||
const { drawX, drawY } = drawInfo();
|
||||
const { drawX, drawY, drawWidth, drawHeight } = drawInfo();
|
||||
|
||||
let _ctx = ctx();
|
||||
let raw = imageData();
|
||||
|
||||
console.log('🖼️ DRAW: Called with', {
|
||||
cached,
|
||||
hasContext: !!_ctx,
|
||||
hasImageData: !!raw,
|
||||
imageDataSize: raw ? `${raw.width}x${raw.height}` : 'none',
|
||||
drawInfo: { drawX, drawY, drawWidth, drawHeight },
|
||||
canvasSize: `${canvas.width}x${canvas.height}`,
|
||||
contextType: _ctx ? 'valid' : 'null',
|
||||
rawBufferSize: raw ? raw.buffer.length : 0
|
||||
});
|
||||
|
||||
if (_ctx && raw) {
|
||||
console.log('🖼️ DRAW: Starting to draw image');
|
||||
_ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Apply transparency effect for cached images if needed
|
||||
let buffer = raw.buffer;
|
||||
if (cached) {
|
||||
for (let i = 3; i < raw.buffer.length; i += 8) {
|
||||
raw.buffer[i] = Math.floor(raw.buffer[i] * 0.7);
|
||||
buffer = new Uint8ClampedArray(raw.buffer);
|
||||
for (let i = 3; i < buffer.length; i += 4) {
|
||||
buffer[i] = Math.floor(buffer[i] * 0.7);
|
||||
}
|
||||
}
|
||||
const img = new ImageData(raw.buffer, raw.width, raw.height);
|
||||
_ctx.putImageData(img, drawX, drawY);
|
||||
|
||||
try {
|
||||
// Create ImageData and draw directly
|
||||
const img = new ImageData(buffer, raw.width, raw.height);
|
||||
|
||||
// If the image size matches the draw size, use putImageData directly
|
||||
if (raw.width === drawWidth && raw.height === drawHeight) {
|
||||
console.log('🖼️ DRAW: Using putImageData directly');
|
||||
_ctx.putImageData(img, drawX, drawY);
|
||||
console.log('✅ DRAW: putImageData completed');
|
||||
} else {
|
||||
console.log('🖼️ DRAW: Using scaling with temp canvas');
|
||||
// Otherwise, use cached temporary canvas for scaling
|
||||
if (!tempCanvas || tempCanvas.width !== raw.width || tempCanvas.height !== raw.height) {
|
||||
tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = raw.width;
|
||||
tempCanvas.height = raw.height;
|
||||
tempCtx = tempCanvas.getContext('2d');
|
||||
console.log('🖼️ DRAW: Created new temp canvas');
|
||||
}
|
||||
|
||||
if (tempCtx) {
|
||||
tempCtx.putImageData(img, 0, 0);
|
||||
_ctx.drawImage(tempCanvas, drawX, drawY, drawWidth, drawHeight);
|
||||
console.log('✅ DRAW: Scaled drawing completed');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ DRAW: Error in draw():', error);
|
||||
}
|
||||
} else {
|
||||
console.log('❌ DRAW: Cannot draw - missing context or image data', {
|
||||
hasContext: !!_ctx,
|
||||
hasImageData: !!raw
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// get screenshot
|
||||
createEffect(() => {
|
||||
let stopped = false;
|
||||
const frame = async () => {
|
||||
const { drawWidth, drawHeight } = drawInfo();
|
||||
|
||||
// Skip if dimensions are not ready
|
||||
if (drawWidth <= 0 || drawHeight <= 0) {
|
||||
console.log('Skipping frame: invalid dimensions', { drawWidth, drawHeight });
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `ambient-light://displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`;
|
||||
// Initialize canvas and resize observer
|
||||
onMount(() => {
|
||||
console.log('🚀 CANVAS: Component mounted');
|
||||
const context = canvas.getContext('2d');
|
||||
console.log('🚀 CANVAS: Context obtained', !!context);
|
||||
setCtx(context);
|
||||
console.log('🚀 CANVAS: Context signal set');
|
||||
|
||||
console.log('Fetching screenshot:', url);
|
||||
// Initial size setup
|
||||
resetSize();
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
mode: 'cors',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Screenshot fetch failed:', response.status, response.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = await response.body?.getReader().read();
|
||||
if (buffer?.value) {
|
||||
console.log('Screenshot received, size:', buffer.value.length);
|
||||
setImageData({
|
||||
buffer: new Uint8ClampedArray(buffer?.value),
|
||||
width: drawWidth,
|
||||
height: drawHeight,
|
||||
});
|
||||
} else {
|
||||
console.log('No screenshot data received');
|
||||
setImageData(null);
|
||||
}
|
||||
draw();
|
||||
} catch (error) {
|
||||
console.error('Screenshot fetch error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
while (!stopped) {
|
||||
if (hidden()) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
await frame();
|
||||
|
||||
// Add a small delay to prevent overwhelming the backend
|
||||
await new Promise((resolve) => setTimeout(resolve, 33)); // ~30 FPS
|
||||
}
|
||||
})();
|
||||
|
||||
onCleanup(() => {
|
||||
stopped = true;
|
||||
});
|
||||
});
|
||||
|
||||
// resize
|
||||
createEffect(() => {
|
||||
let resizeObserver: ResizeObserver;
|
||||
|
||||
onMount(() => {
|
||||
setCtx(canvas.getContext('2d'));
|
||||
|
||||
// Initial size setup
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
resetSize();
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
resetSize();
|
||||
});
|
||||
resizeObserver.observe(root);
|
||||
});
|
||||
resizeObserver.observe(root);
|
||||
|
||||
// Start screenshot fetching after context is ready
|
||||
console.log('🚀 SCREENSHOT: Starting screenshot fetching');
|
||||
setTimeout(() => {
|
||||
console.log('🚀 SCREENSHOT: Context ready, starting fetch');
|
||||
fetchScreenshot(); // Initial fetch - will self-schedule subsequent frames
|
||||
}, 100); // Small delay to ensure context is ready
|
||||
|
||||
onCleanup(() => {
|
||||
isMounted = false; // Stop scheduling new frames
|
||||
resizeObserver?.unobserve(root);
|
||||
console.log('🧹 CLEANUP: Component unmounted');
|
||||
});
|
||||
});
|
||||
|
||||
// update hidden
|
||||
createEffect(() => {
|
||||
const hide = () => {
|
||||
setHidden(true);
|
||||
console.log('hide');
|
||||
};
|
||||
const show = () => {
|
||||
setHidden(false);
|
||||
console.log('show');
|
||||
};
|
||||
|
||||
window.addEventListener('focus', show);
|
||||
window.addEventListener('blur', hide);
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener('focus', show);
|
||||
window.removeEventListener('blur', hide);
|
||||
});
|
||||
});
|
||||
// Note: Removed window focus/blur logic as it was causing screenshot loop to stop
|
||||
// when user interacted with dev tools or other windows
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -183,7 +294,15 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||
{...rootProps}
|
||||
class={'overflow-hidden h-full w-full ' + rootProps.class}
|
||||
>
|
||||
<canvas ref={canvas!} />
|
||||
<canvas
|
||||
ref={canvas!}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'background-color': '#f0f0f0'
|
||||
}}
|
||||
/>
|
||||
{rootProps.children}
|
||||
</div>
|
||||
);
|
||||
|
@ -4,7 +4,7 @@ import { ColorCalibration, LedStripConfigContainer } from '../../models/led-stri
|
||||
import { ledStripStore, setLedStripStore } from '../../stores/led-strip.store';
|
||||
import { ColorSlider } from './color-slider';
|
||||
import { TestColorsBg } from './test-colors-bg';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { VsClose } from 'solid-icons/vs';
|
||||
import { BiRegularReset } from 'solid-icons/bi';
|
||||
import transparentBg from '../../assets/transparent-grid-background.svg?url';
|
||||
|
Reference in New Issue
Block a user