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;
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -54,6 +119,7 @@
|
||||
<nav>
|
||||
<button onclick="showScreen('menu')">🏠 Main Menu</button>
|
||||
<button onclick="showScreen('config')">⚙️ Config Editor</button>
|
||||
<button onclick="showScreen('playlist')">🖹 Playlist Editor</button>
|
||||
</nav>
|
||||
|
||||
<!-- MAIN MENU -->
|
||||
@@ -63,6 +129,7 @@
|
||||
|
||||
<ul>
|
||||
<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 -->
|
||||
</ul>
|
||||
</div>
|
||||
@@ -71,10 +138,53 @@
|
||||
<div id="screen-config" class="screen">
|
||||
<h2>Configuration Editor</h2>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
const Screens = {};
|
||||
let activeScreen = null;
|
||||
/* -----------------------------
|
||||
Screen switching
|
||||
----------------------------- */
|
||||
@@ -82,33 +192,60 @@ function showScreen(name) {
|
||||
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||||
document.getElementById('screen-' + name).classList.add('active');
|
||||
|
||||
if (name === "config") {
|
||||
loadConfig(); // refresh most up-to-date config
|
||||
}
|
||||
activeScreen = name;
|
||||
Screens[name]?.onShow?.();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
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');
|
||||
container.innerHTML = "Loading…";
|
||||
document.getElementById('save-btn').disabled = true;
|
||||
document.getElementById('config-save-btn').disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/config');
|
||||
const config = await res.json();
|
||||
renderConfigForm(config);
|
||||
render(config);
|
||||
} catch (err) {
|
||||
container.innerHTML = "Failed to load config: " + err;
|
||||
}
|
||||
}
|
||||
|
||||
function renderConfigForm(config) {
|
||||
function render(config) {
|
||||
const container = document.getElementById('config-container');
|
||||
container.innerHTML = "";
|
||||
container.appendChild(renderObject(config, "root"));
|
||||
document.getElementById('save-btn').disabled = false;
|
||||
document.getElementById('config-save-btn').disabled = false;
|
||||
}
|
||||
|
||||
const config_names = {
|
||||
@@ -197,8 +334,8 @@ function renderObject(obj, path) {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function serializeConfig(rootObj) {
|
||||
const inputs = document.querySelectorAll("input[data-path], select[data-path]");
|
||||
function serialize(rootObj) {
|
||||
const inputs = screenroot.querySelectorAll("input[data-path], select[data-path]");
|
||||
|
||||
inputs.forEach(input => {
|
||||
const path = input.dataset.path.split('.').slice(1); // remove "root"
|
||||
@@ -218,31 +355,95 @@ function serializeConfig(rootObj) {
|
||||
return rootObj;
|
||||
}
|
||||
|
||||
document.getElementById('save-btn').addEventListener('click', async () => {
|
||||
const res = await fetch('/api/v1/config');
|
||||
const original = await res.json();
|
||||
const updated = serializeConfig(original);
|
||||
return { init, onShow };
|
||||
})();
|
||||
|
||||
/* -----------------------------
|
||||
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 {
|
||||
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!");
|
||||
const res = await fetch('/api/v1/playlist');
|
||||
const config = await res.json();
|
||||
renderPlaylistForm(config);
|
||||
} 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>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -190,3 +190,6 @@ class PlayerApp:
|
||||
self.timer_manager.cancel(self.onIdleTimeout)
|
||||
self.leds.set_state(self.leds.PLAYING)
|
||||
self.playing = True
|
||||
|
||||
def get_nfc(self):
|
||||
return self.nfc
|
||||
|
||||
@@ -11,13 +11,15 @@ webapp = Microdot()
|
||||
server = None
|
||||
config = None
|
||||
app = None
|
||||
nfc = None
|
||||
|
||||
|
||||
def start_webserver(config_, app_):
|
||||
global server, config, app
|
||||
global server, config, app, nfc
|
||||
server = asyncio.create_task(webapp.start_server(port=80))
|
||||
config = config_
|
||||
app = app_
|
||||
nfc = app.get_nfc()
|
||||
|
||||
|
||||
@webapp.before_request
|
||||
@@ -66,6 +68,12 @@ async def config_put(request):
|
||||
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'])
|
||||
async def root_get(request):
|
||||
return redirect('/index.html')
|
||||
|
||||
Reference in New Issue
Block a user