Compare commits
7 Commits
30-fronten
...
more-front
| Author | SHA1 | Date | |
|---|---|---|---|
| e5a2f7bc78 | |||
| 735167db22 | |||
| 11e31aabc2 | |||
| 32bf6a8d68 | |||
| 49197c8ca4 | |||
| e2f9287ebd | |||
| 19dff763bd |
@@ -45,6 +45,71 @@
|
|||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tree view */
|
||||||
|
.tree ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree li {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caret {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caret::before {
|
||||||
|
content: "▶";
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.expanded > .caret::before {
|
||||||
|
content: "▼";
|
||||||
|
}
|
||||||
|
|
||||||
|
li:not(:has(ul)) > .caret::before {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.node {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node:hover {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node.selected {
|
||||||
|
background: #0078d7;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul ul {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.expanded > ul {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-tree-container {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -54,6 +119,7 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<button onclick="showScreen('menu')">🏠 Main Menu</button>
|
<button onclick="showScreen('menu')">🏠 Main Menu</button>
|
||||||
<button onclick="showScreen('config')">⚙️ Config Editor</button>
|
<button onclick="showScreen('config')">⚙️ Config Editor</button>
|
||||||
|
<button onclick="showScreen('playlist')">🖹 Playlist Editor</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- MAIN MENU -->
|
<!-- MAIN MENU -->
|
||||||
@@ -63,6 +129,7 @@
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><button onclick="showScreen('config')">Open Config Editor</button></li>
|
<li><button onclick="showScreen('config')">Open Config Editor</button></li>
|
||||||
|
<li><button onclick="showScreen('playlist')">Open Playlist Editor</button></li>
|
||||||
<!-- More screens can be added later -->
|
<!-- More screens can be added later -->
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,10 +138,53 @@
|
|||||||
<div id="screen-config" class="screen">
|
<div id="screen-config" class="screen">
|
||||||
<h2>Configuration Editor</h2>
|
<h2>Configuration Editor</h2>
|
||||||
<div id="config-container">Loading…</div>
|
<div id="config-container">Loading…</div>
|
||||||
<button id="save-btn" disabled>Save</button>
|
<button id="config-save-btn" disabled>Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PLAYLIST EDITOR SCREEN -->
|
||||||
|
<div id="screen-playlist" class="screen">
|
||||||
|
<h2>Playlist Editor</h2>
|
||||||
|
<div id="playlist-container">Loading…</div>
|
||||||
|
<div class="scroll-tree-container">
|
||||||
|
<div class="tree" id="tree">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="caret"></span>
|
||||||
|
<span class="node">Fruits</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="caret"></span>
|
||||||
|
<span class="node">Apple</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="caret"></span>
|
||||||
|
<span class="node">Citrus</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="caret"></span>
|
||||||
|
<span class="node">Orange</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="caret"></span>
|
||||||
|
<span class="node">Lemon</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="caret"></span>
|
||||||
|
<span class="node">Strawberry</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="playlist-save-btn" disabled>Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const Screens = {};
|
||||||
|
let activeScreen = null;
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
Screen switching
|
Screen switching
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
@@ -82,33 +192,60 @@ function showScreen(name) {
|
|||||||
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||||||
document.getElementById('screen-' + name).classList.add('active');
|
document.getElementById('screen-' + name).classList.add('active');
|
||||||
|
|
||||||
if (name === "config") {
|
activeScreen = name;
|
||||||
loadConfig(); // refresh most up-to-date config
|
Screens[name]?.onShow?.();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
CONFIG EDITOR LOGIC
|
CONFIG EDITOR LOGIC
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
async function loadConfig() {
|
Screens.config = (() => {
|
||||||
|
let screenroot = null;
|
||||||
|
function init() {
|
||||||
|
screenroot = document.getElementById('screen-config');
|
||||||
|
document.getElementById('config-save-btn').addEventListener('click', async () => {
|
||||||
|
const res = await fetch('/api/v1/config');
|
||||||
|
const original = await res.json();
|
||||||
|
const updated = serialize(original);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saveRes = await fetch('/api/v1/config', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(updated, null, 2)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!saveRes.ok) {
|
||||||
|
alert("Failed to save config: " + await saveRes.text());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("Configuration saved successfully!");
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error saving configuration: " + err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onShow() {
|
||||||
const container = document.getElementById('config-container');
|
const container = document.getElementById('config-container');
|
||||||
container.innerHTML = "Loading…";
|
container.innerHTML = "Loading…";
|
||||||
document.getElementById('save-btn').disabled = true;
|
document.getElementById('config-save-btn').disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/config');
|
const res = await fetch('/api/v1/config');
|
||||||
const config = await res.json();
|
const config = await res.json();
|
||||||
renderConfigForm(config);
|
render(config);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
container.innerHTML = "Failed to load config: " + err;
|
container.innerHTML = "Failed to load config: " + err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderConfigForm(config) {
|
function render(config) {
|
||||||
const container = document.getElementById('config-container');
|
const container = document.getElementById('config-container');
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
container.appendChild(renderObject(config, "root"));
|
container.appendChild(renderObject(config, "root"));
|
||||||
document.getElementById('save-btn').disabled = false;
|
document.getElementById('config-save-btn').disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config_names = {
|
const config_names = {
|
||||||
@@ -197,8 +334,8 @@ function renderObject(obj, path) {
|
|||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeConfig(rootObj) {
|
function serialize(rootObj) {
|
||||||
const inputs = document.querySelectorAll("input[data-path], select[data-path]");
|
const inputs = screenroot.querySelectorAll("input[data-path], select[data-path]");
|
||||||
|
|
||||||
inputs.forEach(input => {
|
inputs.forEach(input => {
|
||||||
const path = input.dataset.path.split('.').slice(1); // remove "root"
|
const path = input.dataset.path.split('.').slice(1); // remove "root"
|
||||||
@@ -218,31 +355,95 @@ function serializeConfig(rootObj) {
|
|||||||
return rootObj;
|
return rootObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('save-btn').addEventListener('click', async () => {
|
return { init, onShow };
|
||||||
const res = await fetch('/api/v1/config');
|
})();
|
||||||
const original = await res.json();
|
|
||||||
const updated = serializeConfig(original);
|
/* -----------------------------
|
||||||
|
PLAYLIST EDITOR LOGIC
|
||||||
|
----------------------------- */
|
||||||
|
Screens.playlist = (() => {
|
||||||
|
let lastSelected = null;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
document.getElementById("tree").addEventListener("click", (e) => {
|
||||||
|
// CARET CLICK → expand/collapse
|
||||||
|
const caret = e.target.closest(".caret");
|
||||||
|
if (caret) {
|
||||||
|
const li = caret.parentElement;
|
||||||
|
if (li.querySelector("ul")) {
|
||||||
|
li.classList.toggle("expanded");
|
||||||
|
}
|
||||||
|
return; // IMPORTANT: don't affect selection
|
||||||
|
}
|
||||||
|
|
||||||
|
// NODE LABEL CLICK → selection only
|
||||||
|
const node = e.target.closest(".node");
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
if (e.shiftKey && lastSelected) {
|
||||||
|
selectRange(lastSelected, node);
|
||||||
|
} else if (e.ctrlKey || e.metaKey) {
|
||||||
|
node.classList.toggle("selected");
|
||||||
|
} else {
|
||||||
|
clearSelection();
|
||||||
|
node.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSelected = node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onShow() {
|
||||||
|
const container = document.getElementById('playlist-container');
|
||||||
|
container.innerHTML = "Loading…";
|
||||||
|
document.getElementById('playlist-save-btn').disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const saveRes = await fetch('/api/v1/config', {
|
const res = await fetch('/api/v1/playlist');
|
||||||
method: 'PUT',
|
const config = await res.json();
|
||||||
headers: { 'Content-Type': 'application/json' },
|
renderPlaylistForm(config);
|
||||||
body: JSON.stringify(updated, null, 2)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!saveRes.ok) {
|
|
||||||
alert("Failed to save config: " + await saveRes.text());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
alert("Configuration saved successfully!");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Error saving configuration: " + err);
|
container.innerHTML = "Failed to load config: " + err;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPlaylistForm(config) {
|
||||||
|
const container = document.getElementById('playlist-container');
|
||||||
|
container.innerHTML = "";
|
||||||
|
container.appendChild(renderPlaylistObject(config, "root"));
|
||||||
|
document.getElementById('playlist-save-btn').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
tree.querySelectorAll(".selected").forEach(n =>
|
||||||
|
n.classList.remove("selected")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRange(start, end) {
|
||||||
|
const nodes = [...tree.querySelectorAll(".node")];
|
||||||
|
const startIndex = nodes.indexOf(start);
|
||||||
|
const endIndex = nodes.indexOf(end);
|
||||||
|
|
||||||
|
const [from, to] = startIndex < endIndex
|
||||||
|
? [startIndex, endIndex]
|
||||||
|
: [endIndex, startIndex];
|
||||||
|
|
||||||
|
clearSelection();
|
||||||
|
nodes.slice(from, to + 1).forEach(n =>
|
||||||
|
n.classList.add("selected")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { init, onShow };
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
Object.values(Screens).forEach(screen => {
|
||||||
|
screen.init?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load main menu by default
|
showScreen("menu");
|
||||||
showScreen('menu');
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -190,3 +190,6 @@ class PlayerApp:
|
|||||||
self.timer_manager.cancel(self.onIdleTimeout)
|
self.timer_manager.cancel(self.onIdleTimeout)
|
||||||
self.leds.set_state(self.leds.PLAYING)
|
self.leds.set_state(self.leds.PLAYING)
|
||||||
self.playing = True
|
self.playing = True
|
||||||
|
|
||||||
|
def get_nfc(self):
|
||||||
|
return self.nfc
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ webapp = Microdot()
|
|||||||
server = None
|
server = None
|
||||||
config = None
|
config = None
|
||||||
app = None
|
app = None
|
||||||
|
nfc = None
|
||||||
|
|
||||||
|
|
||||||
def start_webserver(config_, app_):
|
def start_webserver(config_, app_):
|
||||||
global server, config, app
|
global server, config, app, nfc
|
||||||
server = asyncio.create_task(webapp.start_server(port=80))
|
server = asyncio.create_task(webapp.start_server(port=80))
|
||||||
config = config_
|
config = config_
|
||||||
app = app_
|
app = app_
|
||||||
|
nfc = app.get_nfc()
|
||||||
|
|
||||||
|
|
||||||
@webapp.before_request
|
@webapp.before_request
|
||||||
@@ -66,6 +68,12 @@ async def config_put(request):
|
|||||||
return '', 204
|
return '', 204
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/api/v1/last_tag_uid', methods=['GET'])
|
||||||
|
async def last_tag_uid_get(request):
|
||||||
|
tag, _ = nfc.get_last_uid()
|
||||||
|
return {'tag': tag}
|
||||||
|
|
||||||
|
|
||||||
@webapp.route('/', methods=['GET'])
|
@webapp.route('/', methods=['GET'])
|
||||||
async def root_get(request):
|
async def root_get(request):
|
||||||
return redirect('/index.html')
|
return redirect('/index.html')
|
||||||
|
|||||||
Reference in New Issue
Block a user