Initial working version

This commit is contained in:
2025-05-23 22:56:59 +12:00
commit 0b27ef2b30
16 changed files with 1078 additions and 0 deletions

83
static/ApiService.js Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
import { BarControlSystem } from "./BarControlSystem.js";
document.addEventListener("DOMContentLoaded", () => {
new BarControlSystem();
});

30
static/utils.js Normal file
View 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;
}