Initial working version
This commit is contained in:
83
static/ApiService.js
Normal file
83
static/ApiService.js
Normal file
@@ -0,0 +1,83 @@
|
||||
export class ApiService {
|
||||
static async loadSettings() {
|
||||
try {
|
||||
const response = await fetch("/api/settings");
|
||||
const settings = await response.json();
|
||||
console.log("Settings loaded:", settings);
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error("Failed to load settings:", error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
static async createBar(barData) {
|
||||
const response = await fetch("/api/settings/create", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(barData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
static async deleteBar(barId) {
|
||||
const response = await fetch("/api/settings/delete", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ barId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
static async saveColor(barId, color) {
|
||||
try {
|
||||
const response = await fetch("/api/settings/color", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ barId, color }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log(`Color saved for ${barId}: ${color}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save color:", error);
|
||||
}
|
||||
}
|
||||
|
||||
static async savePosition(barId, x, y) {
|
||||
try {
|
||||
const response = await fetch("/api/settings/position", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ barId, x, y }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log(`Position saved for ${barId}: ${x}, ${y}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save position:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
270
static/BarControlSystem.js
Normal file
270
static/BarControlSystem.js
Normal file
@@ -0,0 +1,270 @@
|
||||
import { ApiService } from "./ApiService.js";
|
||||
import { DialogManager } from "./DialogManager.js";
|
||||
import { throttle, createStyledElement } from "./utils.js";
|
||||
|
||||
export class BarControlSystem {
|
||||
constructor() {
|
||||
this.settings = {};
|
||||
this.websockets = {};
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadSettings();
|
||||
this.createControlPanel();
|
||||
this.renderColorPickers();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = await ApiService.loadSettings();
|
||||
}
|
||||
|
||||
createControlPanel() {
|
||||
const panel = createStyledElement("div", {
|
||||
position: "fixed",
|
||||
top: "10px",
|
||||
right: "10px",
|
||||
padding: "15px",
|
||||
border: "2px solid #333",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "#f0f0f0",
|
||||
zIndex: "1000",
|
||||
});
|
||||
panel.id = "control-panel";
|
||||
|
||||
const title = createStyledElement(
|
||||
"h3",
|
||||
{
|
||||
margin: "0 0 10px 0",
|
||||
fontFamily: "Arial, sans-serif",
|
||||
},
|
||||
{ textContent: "Control Panel" },
|
||||
);
|
||||
|
||||
const buttonStyles = {
|
||||
padding: "8px 12px",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
color: "white",
|
||||
};
|
||||
|
||||
const createButton = createStyledElement(
|
||||
"button",
|
||||
{
|
||||
...buttonStyles,
|
||||
marginRight: "10px",
|
||||
backgroundColor: "#4CAF50",
|
||||
},
|
||||
{ textContent: "Create New Bar" },
|
||||
);
|
||||
|
||||
const refreshButton = createStyledElement(
|
||||
"button",
|
||||
{
|
||||
...buttonStyles,
|
||||
backgroundColor: "#2196F3",
|
||||
},
|
||||
{ textContent: "Refresh" },
|
||||
);
|
||||
|
||||
createButton.onclick = () => this.showCreateDialog();
|
||||
refreshButton.onclick = () => this.refresh();
|
||||
|
||||
panel.append(title, createButton, refreshButton);
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
|
||||
showCreateDialog() {
|
||||
DialogManager.showCreateDialog((barData) => this.createBar(barData));
|
||||
}
|
||||
|
||||
async createBar(barData) {
|
||||
if (!barData.barId) {
|
||||
alert("Bar ID is required");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ApiService.createBar(barData);
|
||||
console.log(`Bar created: ${barData.barId}`);
|
||||
await this.refresh();
|
||||
} catch (error) {
|
||||
console.error("Failed to create bar:", error);
|
||||
alert(error.message || "Failed to create bar");
|
||||
}
|
||||
}
|
||||
|
||||
async deleteBar(barId) {
|
||||
if (!confirm(`Are you sure you want to delete bar ${barId}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ApiService.deleteBar(barId);
|
||||
console.log(`Bar deleted: ${barId}`);
|
||||
await this.refresh();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete bar:", error);
|
||||
alert(error.message || "Failed to delete bar");
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
// Close existing websockets
|
||||
Object.values(this.websockets).forEach((ws) => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Remove existing color pickers
|
||||
Object.keys(this.settings).forEach((barId) => {
|
||||
const form = document.getElementById("color_form_" + barId);
|
||||
if (form) {
|
||||
form.remove();
|
||||
}
|
||||
});
|
||||
|
||||
this.websockets = {};
|
||||
await this.loadSettings();
|
||||
this.renderColorPickers();
|
||||
}
|
||||
|
||||
renderColorPickers() {
|
||||
Object.keys(this.settings).forEach((barId) => {
|
||||
const config = this.settings[barId];
|
||||
this.createColorPicker(barId, config);
|
||||
});
|
||||
}
|
||||
|
||||
makeDraggable(element, barId) {
|
||||
let isDragging = false;
|
||||
let startX, startY, initialX, initialY;
|
||||
|
||||
element.addEventListener("mousedown", (e) => {
|
||||
if (e.target.tagName === "BUTTON") return;
|
||||
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
initialX = rect.left;
|
||||
initialY = rect.top;
|
||||
|
||||
element.style.cursor = "move";
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
|
||||
const newX = initialX + deltaX;
|
||||
const newY = initialY + deltaY;
|
||||
|
||||
element.style.left = newX + "px";
|
||||
element.style.top = newY + "px";
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", () => {
|
||||
if (isDragging) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
ApiService.savePosition(barId, rect.left, rect.top);
|
||||
element.style.cursor = "default";
|
||||
}
|
||||
isDragging = false;
|
||||
});
|
||||
}
|
||||
|
||||
createColorPicker(barId, config) {
|
||||
const ws = new WebSocket(config["url"]);
|
||||
this.websockets[barId] = ws;
|
||||
|
||||
const form = createStyledElement("form", {
|
||||
position: "absolute",
|
||||
left: config["x"] + "px",
|
||||
top: config["y"] + "px",
|
||||
padding: "10px",
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "white",
|
||||
});
|
||||
form.id = "color_form_" + barId;
|
||||
|
||||
const label = createStyledElement(
|
||||
"label",
|
||||
{
|
||||
display: "block",
|
||||
marginBottom: "5px",
|
||||
fontFamily: "Arial, sans-serif",
|
||||
fontSize: "14px",
|
||||
},
|
||||
{
|
||||
htmlFor: "color_input_" + barId,
|
||||
textContent: barId,
|
||||
},
|
||||
);
|
||||
|
||||
const colorInput = createStyledElement(
|
||||
"input",
|
||||
{},
|
||||
{
|
||||
type: "color",
|
||||
id: "color_input_" + barId,
|
||||
name: barId,
|
||||
value: config["color"],
|
||||
},
|
||||
);
|
||||
|
||||
const deleteButton = createStyledElement(
|
||||
"button",
|
||||
{
|
||||
position: "absolute",
|
||||
top: "2px",
|
||||
right: "2px",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
backgroundColor: "#f44336",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "50%",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px",
|
||||
lineHeight: "1",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
textContent: "×",
|
||||
},
|
||||
);
|
||||
|
||||
deleteButton.onclick = () => this.deleteBar(barId);
|
||||
|
||||
const throttledColorHandler = throttle((event) => {
|
||||
const color = event.target.value;
|
||||
console.log(`Color selected for ${barId}: ${color}`);
|
||||
|
||||
ApiService.saveColor(barId, color);
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
const message = { color1: color };
|
||||
ws.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.warn(
|
||||
`WebSocket not ready for ${barId}. ReadyState: ${ws.readyState}`,
|
||||
);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
colorInput.addEventListener("input", throttledColorHandler);
|
||||
|
||||
form.append(label, colorInput, deleteButton);
|
||||
document.body.appendChild(form);
|
||||
|
||||
this.makeDraggable(form, barId);
|
||||
}
|
||||
}
|
||||
113
static/DialogManager.js
Normal file
113
static/DialogManager.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import { createStyledElement } from "./utils.js";
|
||||
|
||||
export class DialogManager {
|
||||
static showCreateDialog(onCreateCallback) {
|
||||
const dialog = createStyledElement("div", {
|
||||
position: "fixed",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
padding: "20px",
|
||||
border: "2px solid #333",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "white",
|
||||
zIndex: "1001",
|
||||
boxShadow: "0 4px 8px rgba(0,0,0,0.3)",
|
||||
});
|
||||
|
||||
const title = createStyledElement(
|
||||
"h3",
|
||||
{ margin: "0 0 15px 0" },
|
||||
{ textContent: "Create New Bar" },
|
||||
);
|
||||
|
||||
const inputStyles = {
|
||||
display: "block",
|
||||
margin: "5px 0",
|
||||
padding: "8px",
|
||||
width: "200px",
|
||||
};
|
||||
|
||||
const barIdInput = createStyledElement("input", inputStyles, {
|
||||
type: "text",
|
||||
placeholder: "Bar ID",
|
||||
});
|
||||
|
||||
const urlInput = createStyledElement("input", inputStyles, {
|
||||
type: "text",
|
||||
placeholder: "WebSocket URL",
|
||||
value: "192.168.4.1",
|
||||
});
|
||||
|
||||
const colorInput = createStyledElement("input", inputStyles, {
|
||||
type: "color",
|
||||
value: "#ff0000",
|
||||
});
|
||||
|
||||
const xInput = createStyledElement("input", inputStyles, {
|
||||
type: "number",
|
||||
placeholder: "X Position",
|
||||
value: "100",
|
||||
});
|
||||
|
||||
const yInput = createStyledElement("input", inputStyles, {
|
||||
type: "number",
|
||||
placeholder: "Y Position",
|
||||
value: "100",
|
||||
});
|
||||
|
||||
const buttonStyles = {
|
||||
padding: "8px 12px",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
color: "white",
|
||||
};
|
||||
|
||||
const createBtn = createStyledElement(
|
||||
"button",
|
||||
{
|
||||
...buttonStyles,
|
||||
margin: "10px 5px 0 0",
|
||||
backgroundColor: "#4CAF50",
|
||||
},
|
||||
{ textContent: "Create" },
|
||||
);
|
||||
|
||||
const cancelBtn = createStyledElement(
|
||||
"button",
|
||||
{
|
||||
...buttonStyles,
|
||||
margin: "10px 0 0 0",
|
||||
backgroundColor: "#f44336",
|
||||
},
|
||||
{ textContent: "Cancel" },
|
||||
);
|
||||
|
||||
createBtn.onclick = () => {
|
||||
const barData = {
|
||||
barId: barIdInput.value,
|
||||
url: `ws://${urlInput.value}/ws`,
|
||||
color: colorInput.value,
|
||||
x: parseInt(xInput.value),
|
||||
y: parseInt(yInput.value),
|
||||
};
|
||||
onCreateCallback(barData);
|
||||
document.body.removeChild(dialog);
|
||||
};
|
||||
|
||||
cancelBtn.onclick = () => document.body.removeChild(dialog);
|
||||
|
||||
dialog.append(
|
||||
title,
|
||||
barIdInput,
|
||||
urlInput,
|
||||
colorInput,
|
||||
xInput,
|
||||
yInput,
|
||||
createBtn,
|
||||
cancelBtn,
|
||||
);
|
||||
document.body.appendChild(dialog);
|
||||
}
|
||||
}
|
||||
84
static/main.css
Normal file
84
static/main.css
Normal file
@@ -0,0 +1,84 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.color-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.color-picker-item {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.color-picker-item h4 {
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.color-picker-item input[type="color"] {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
5
static/main.js
Normal file
5
static/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { BarControlSystem } from "./BarControlSystem.js";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new BarControlSystem();
|
||||
});
|
||||
30
static/utils.js
Normal file
30
static/utils.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export function throttle(func, delay) {
|
||||
let timeoutId;
|
||||
let lastExecTime = 0;
|
||||
return function (...args) {
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (currentTime - lastExecTime > delay) {
|
||||
func.apply(this, args);
|
||||
lastExecTime = currentTime;
|
||||
} else {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(
|
||||
() => {
|
||||
func.apply(this, args);
|
||||
lastExecTime = Date.now();
|
||||
},
|
||||
delay - (currentTime - lastExecTime),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createStyledElement(tag, styles = {}, attributes = {}) {
|
||||
const element = document.createElement(tag);
|
||||
|
||||
Object.assign(element.style, styles);
|
||||
Object.assign(element, attributes);
|
||||
|
||||
return element;
|
||||
}
|
||||
Reference in New Issue
Block a user