feat: full feature set - filters, context menu, patchbay, position persistence
Backend: - GET/PUT /api/positions - server-side node position persistence (~/.config/pwweb/) - GET/PUT /api/patchbay - save/load connection snapshots - POST /api/patchbay/apply - apply saved connections Frontend (custom SVG canvas): - Right-click wire to disconnect (context menu) - Port type filter toolbar (audio/midi/video/other toggles) - Save/Load patchbay buttons - Node positions persisted to localStorage + server - Node border color by mode (green=output, red=input) - Type indicator in node header [audio] [midi] - Selected wire highlight (white, thicker) - Select wire + Delete to disconnect - Drag output port to input port to connect - Pan (drag bg) and zoom (scroll wheel) - Filtered ports shown as dim dots when hidden
This commit is contained in:
@@ -4,9 +4,40 @@
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <condition_variable>
|
||||
#include <fstream>
|
||||
#include <sys/stat.h>
|
||||
#include <pwd.h>
|
||||
|
||||
using namespace pwgraph;
|
||||
|
||||
// ============================================================================
|
||||
// File I/O helpers
|
||||
// ============================================================================
|
||||
|
||||
std::string WebServer::dataDir() const {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) {
|
||||
auto *pw = getpwuid(getuid());
|
||||
if (pw) home = pw->pw_dir;
|
||||
}
|
||||
std::string dir = (home ? home : "/tmp");
|
||||
dir += "/.config/pwweb";
|
||||
mkdir(dir.c_str(), 0755);
|
||||
return dir;
|
||||
}
|
||||
|
||||
std::string WebServer::readFile(const std::string &path) const {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return "{}";
|
||||
return std::string((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
void WebServer::writeFile(const std::string &path, const std::string &data) const {
|
||||
std::ofstream f(path);
|
||||
if (f.is_open()) f << data;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JSON serialization helpers
|
||||
// ============================================================================
|
||||
@@ -309,6 +340,69 @@ void WebServer::setupRoutes() {
|
||||
};
|
||||
m_http.Options("/api/connect", cors_handler);
|
||||
m_http.Options("/api/disconnect", cors_handler);
|
||||
m_http.Options("/api/positions", cors_handler);
|
||||
m_http.Options("/api/patchbay", cors_handler);
|
||||
|
||||
// Positions persistence: GET /api/positions
|
||||
m_http.Get("/api/positions", [this](const httplib::Request &, httplib::Response &res) {
|
||||
std::string path = dataDir() + "/positions.json";
|
||||
res.set_content(readFile(path), "application/json");
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
|
||||
// Positions persistence: PUT /api/positions
|
||||
m_http.Put("/api/positions", [this](const httplib::Request &req, httplib::Response &res) {
|
||||
std::string path = dataDir() + "/positions.json";
|
||||
writeFile(path, req.body);
|
||||
res.set_content("{\"ok\":true}", "application/json");
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
|
||||
// Patchbay: GET /api/patchbay
|
||||
m_http.Get("/api/patchbay", [this](const httplib::Request &, httplib::Response &res) {
|
||||
std::string path = dataDir() + "/patchbay.json";
|
||||
res.set_content(readFile(path), "application/json");
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
|
||||
// Patchbay: PUT /api/patchbay (save current connections as a named snapshot)
|
||||
m_http.Put("/api/patchbay", [this](const httplib::Request &req, httplib::Response &res) {
|
||||
std::string path = dataDir() + "/patchbay.json";
|
||||
writeFile(path, req.body);
|
||||
res.set_content("{\"ok\":true}", "application/json");
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
|
||||
// Patchbay: POST /api/patchbay/apply (apply saved connections to current graph)
|
||||
m_http.Post("/api/patchbay/apply", [this](const httplib::Request &req, httplib::Response &res) {
|
||||
// req.body is a JSON array of {output_port_id, input_port_id} pairs
|
||||
// We just parse it and connect each pair
|
||||
// Simple parsing: find all pairs
|
||||
std::string body = req.body;
|
||||
size_t pos = 0;
|
||||
int connected = 0;
|
||||
while (pos < body.size()) {
|
||||
uint32_t out_id = 0, in_id = 0;
|
||||
int chars = 0;
|
||||
if (sscanf(body.c_str() + pos,
|
||||
"{\"output_port_id\":%u,\"input_port_id\":%u}%n",
|
||||
&out_id, &in_id, &chars) == 2 ||
|
||||
sscanf(body.c_str() + pos,
|
||||
"{\"output_port_id\":%u, \"input_port_id\":%u}%n",
|
||||
&out_id, &in_id, &chars) == 2)
|
||||
{
|
||||
if (m_engine.connectPorts(out_id, in_id))
|
||||
connected++;
|
||||
}
|
||||
pos = body.find('{', pos + 1);
|
||||
if (pos == std::string::npos) break;
|
||||
}
|
||||
if (connected > 0) broadcastGraph();
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "{\"ok\":true,\"connected\":%d}", connected);
|
||||
res.set_content(buf, "application/json");
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
}
|
||||
|
||||
// end of web_server.cpp
|
||||
|
||||
Reference in New Issue
Block a user