Checkpoint
@@ -103,7 +103,7 @@
|
||||
"request": "launch",
|
||||
// Resolved by CMake Tools:
|
||||
"program": "${command:cmake.launchTargetPath}",
|
||||
"args": [ "/etc/pipedal/config", "/etc/pipedal/react", "-port", "0.0.0.0:8080", "-log-level","debug" ],
|
||||
"args": [ "/etc/pipedal/config", "${workspaceFolder}/vite/dist", "-port", "0.0.0.0:8080", "-log-level","debug" ],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"environment": [
|
||||
@@ -142,7 +142,8 @@
|
||||
//"[pipedal_alsa_test]"
|
||||
// "[wifi_channels_test]"
|
||||
// "[locale]"
|
||||
"[pipewire_input_stream]"
|
||||
//"[mod_gui_init]"
|
||||
"[mod_gui_templates]"
|
||||
],
|
||||
|
||||
"stopAtEntry": false,
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
"cSpell.ignoreWords": [
|
||||
"nammodel"
|
||||
],
|
||||
"cSpell.enabled": true,
|
||||
"cSpell.enabled": false,
|
||||
|
||||
// Disable all automatic completion suggestions - only show when Ctrl+Space is pressed
|
||||
"editor.quickSuggestions": {
|
||||
|
||||
@@ -73,6 +73,7 @@ DBusDispatcher::PostHandle DBusDispatcher::PostDelayed(const clock::duration &de
|
||||
void DBusDispatcher::Run()
|
||||
{
|
||||
this->eventFd = eventfd(0, 0);
|
||||
threadStarted = true;
|
||||
this->serviceThread = std::thread(
|
||||
[this]()
|
||||
{ this->ThreadProc(); });
|
||||
@@ -169,7 +170,7 @@ void DBusDispatcher::ThreadProc()
|
||||
int eventTimeout = GetEventTimeoutMs();
|
||||
if (eventTimeout != -1 && eventTimeout < pollTimeout)
|
||||
{
|
||||
pollTimeout = eventTimeout;/*+-*/
|
||||
pollTimeout = eventTimeout; /*+-*/
|
||||
}
|
||||
if (pollTimeout > 250 || pollTimeout == -1)
|
||||
{
|
||||
@@ -197,13 +198,13 @@ void DBusDispatcher::ThreadProc()
|
||||
LogTrace("DBusDispatcher", "ThreadProc", "Service thread terminated.");
|
||||
}
|
||||
|
||||
void DBusDispatcher::Wait()
|
||||
void DBusDispatcher::WaitForClose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!threadJoined)
|
||||
if (threadStarted)
|
||||
{
|
||||
threadJoined = true;
|
||||
threadStarted = false;
|
||||
serviceThread.join();
|
||||
}
|
||||
}
|
||||
@@ -229,7 +230,7 @@ void DBusDispatcher::Stop()
|
||||
|
||||
this->stopping = true;
|
||||
WakeThread();
|
||||
Wait();
|
||||
WaitForClose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public:
|
||||
void Run();
|
||||
// Stop everything, shutting down gracefully.
|
||||
void Stop();
|
||||
void Wait();
|
||||
void WaitForClose();
|
||||
bool IsFinished();
|
||||
|
||||
// Stop, but don't wait. Suitable for use in a signal handler.
|
||||
@@ -78,7 +78,7 @@ private:
|
||||
std::atomic<bool> stopping;
|
||||
|
||||
std::vector<CallbackEntry> postedEvents;
|
||||
bool threadJoined;
|
||||
bool threadStarted;
|
||||
std::thread serviceThread;
|
||||
std::mutex postMutex;
|
||||
};
|
||||
@@ -35,6 +35,9 @@ public:
|
||||
|
||||
|
||||
static std::string encode_url_segment(const char*pStart, const char*pEnd, bool isQuerySegment = false);
|
||||
static std::string encode_url_segment(const std::string &segment, bool isQuerySegment = false) {
|
||||
return encode_url_segment(segment.c_str(), segment.c_str() + segment.length(), isQuerySegment);
|
||||
}
|
||||
static void encode_url_segment(std::ostream&os, const char*pStart, const char *pEnd, bool isQuerySegment = false);
|
||||
static void encode_url_segment(std::ostream&os, const std::string&segment, bool isQuerySegment = false)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg132"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
sodipodi:docname="red-light-on.png"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs136"><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient1239"><stop
|
||||
style="stop-color:#ffffff;stop-opacity:0.84431136;"
|
||||
offset="0"
|
||||
id="stop1235" /><stop
|
||||
style="stop-color:#ffffff;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop1237" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient1051"><stop
|
||||
style="stop-color:#580b0b;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop1047" /><stop
|
||||
style="stop-color:#252525;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop1049" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1051"
|
||||
id="linearGradient1053"
|
||||
x1="16"
|
||||
y1="24"
|
||||
x2="16"
|
||||
y2="8"
|
||||
gradientUnits="userSpaceOnUse" /><radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1239"
|
||||
id="radialGradient1241"
|
||||
cx="15.964222"
|
||||
cy="13.292918"
|
||||
fx="15.964222"
|
||||
fy="13.292918"
|
||||
r="4.8461695"
|
||||
gradientTransform="matrix(1,0,0,0.74626396,0,3.3728925)"
|
||||
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
|
||||
id="namedview134"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="23.583986"
|
||||
inkscape:cx="16.536645"
|
||||
inkscape:cy="17.066666"
|
||||
inkscape:window-width="1867"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g138" /><g
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="Image"
|
||||
id="g138"><image
|
||||
width="32"
|
||||
height="32"
|
||||
preserveAspectRatio="none"
|
||||
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
|
||||
AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAQSSURB
|
||||
VFiF7VZNaFxVFP7O/Xvv3Zk3P8/JhEnsKCNp0xJokFilSJCC0GUo6NKNSxeuBBcKwZWLUsRNUSJU
|
||||
ihQyuAmYjYsORRChwUKpENJYaUpqtbGxfTN5/9dFJyo0b5pUXZkPDlw495zz3XPuuecC+9jH/x30
|
||||
BPs927bLQghHSsmVUpmUMtRa94jo/vLysg/A/BcEvEKhMCqlFEqpjIgyy7IyIsps284YY5nWOhNC
|
||||
hEmS/La0tNT7twiQ4zijlmUNKaUyR6kWB04x4CUDjPad3CLOv7G0vlBvNH6QUmaWZfkLCwsP/jGB
|
||||
Wq02whirOY4jRJK8w4153QoCpuIYPE0BABnniKREYNspSfl58+DB9yuVSgBgq91ubz0xgVarVU6S
|
||||
5GnOuRBJ8okVhi9Wt7bw7NgYRlstFMplAIC/uYn1Gzfw08oK7jkOQsu6NDE1dapYLAZBEATtdjvN
|
||||
i8EHkRsaGhpRSsGE4Xs6DE82whBT09NoNJuQUgJpCqQplJR4ql5HfXgYvdVVhETP/Hz3rjt94sRi
|
||||
vV5Hp9PJ9pyBQ4cOucVisR74/ljy4MFXzc1NNjYxgaEDBwYlDb/cvInr167hZqWSJpxPrq6tXeur
|
||||
duwMludIa207jhNHQfCaFwTMNgalchlJrzdQypUKbGPgBQEn4M35+Xk2Pz+fG0fkKTzPY0KI2KTp
|
||||
y9U4hgKQ9XogkWsCAMiSBAqAF8e4C7yKv8q84z3I9aaUyoQQKYDRYpqCAfDv3EGxWh1IwL93DxIA
|
||||
S1MYY5rdbpcVCoXcUucSGB4ejrvdrgFgZP8Y3du3oQBI297RJg6CP/ckAGAMOOe83O+WPRFQSkVK
|
||||
KcCYtYTzw3aSAFkGf30dqlCAtCxwKQEAaRwjDkNE3S6EeXjXQs7BOF+r1WpcKZXbBbkERkZGovX1
|
||||
dTJEX/8u5WEvSR4qjIHxfUS+/4iN/Nt6U0pIIS42Gg3GGMt9B3IJzM7OpgDMeKs1dyvL3nouCPj2
|
||||
6R6HmAi3HCc9eOTIF1EU8Y2NjdzXcNBDhNnZWTZx9OjG91eu1Hucv3AgisD7RnnCAFxxXYSl0tyn
|
||||
5861lVI4fvx47kzI7c9teJ7HT87MvHvHcS4uuS6ICArYUUCEJdfFr8XipY/Onv1ASsnHx8cfrdVu
|
||||
M9DpdMzMzAybnJykkWbzy2+vXq2uSjkJImYDsIwBEcEXAj9aFr5z3TSqVj+bO3/+7WazmZVKpZ7W
|
||||
euBY3tV/YHFx0QJgSSnp4zNnDt+4fv2NOIpeybKsCQCMsTWtdef5Y8cufHj69IrWmrTWPhHdf5zv
|
||||
XX9ILl++LAHoNE0F55zK5TIREbmuS9voB04B3CeigWN4zwS2sbKyYnmeZ4VhqEqlEu/7MIVCIQaw
|
||||
BSAgol1/yfaxj338AXYNhqZlqq9VAAAAAElFTkSuQmCC
|
||||
"
|
||||
id="image412"
|
||||
x="0"
|
||||
y="0" /><circle
|
||||
style="fill:url(#linearGradient1053);fill-opacity:1;stroke:#000000;stroke-width:1;stop-color:#000000;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path468"
|
||||
cx="16"
|
||||
cy="16"
|
||||
r="7.9999995" /><ellipse
|
||||
style="fill:url(#radialGradient1241);fill-opacity:1;stroke:#000000;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;stop-color:#000000"
|
||||
id="path1231"
|
||||
cx="15.964222"
|
||||
cy="13.292918"
|
||||
rx="4.3461695"
|
||||
ry="3.1165216" /></g></svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -206,6 +206,9 @@ else()
|
||||
endif()
|
||||
|
||||
set (PIPEDAL_SOURCES
|
||||
ModTemplateGenerator.cpp ModTemplateGenerator.hpp
|
||||
WebServerMod.cpp WebServerMod.hpp
|
||||
ModGui.cpp ModGui.hpp
|
||||
PipewireInputStream.cpp PipewireInputStream.hpp
|
||||
AudioFiles.cpp AudioFiles.hpp
|
||||
AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp
|
||||
@@ -414,6 +417,8 @@ add_executable(AuxInTest
|
||||
|
||||
add_executable(pipedaltest
|
||||
testMain.cpp
|
||||
|
||||
ModGuiTest.cpp
|
||||
PipewireInputStreamTest.cpp
|
||||
|
||||
AudioFilesTest.cpp
|
||||
|
||||
@@ -36,7 +36,7 @@ TEST_CASE( "PluginHost memory leak", "[lv2host_leak][Build][Dev]" ) {
|
||||
{
|
||||
PluginHost host;
|
||||
|
||||
host.Load("/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2");
|
||||
host.LoadLilv("/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2");
|
||||
}
|
||||
MemStats finalMemory = GetMemStats();
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "ss.hpp"
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include "util.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
@@ -30,7 +31,16 @@ static std::string empty;
|
||||
const std::string& MimeTypes::MimeTypeFromExtension(const std::string &extension) const
|
||||
{
|
||||
auto iter = extensionToMimeType.find(extension);
|
||||
if (iter == extensionToMimeType.end()) return empty;
|
||||
if (iter == extensionToMimeType.end())
|
||||
{
|
||||
std::string lowerExt = ToLower(extension);
|
||||
auto iter2 = extensionToMimeType.find(lowerExt);
|
||||
if (iter2 != extensionToMimeType.end())
|
||||
{
|
||||
return iter2->second;
|
||||
}
|
||||
return empty;
|
||||
}
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ ModFileTypes::ModFileTypes(const std::string &fileTypes)
|
||||
if (wellKnownType)
|
||||
{
|
||||
rootDirectories_.push_back(type);
|
||||
modFileTypes_.push_back(type);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -32,12 +32,14 @@ namespace pipedal
|
||||
ModFileTypes(const std::string &fileTypes);
|
||||
const std::vector<std::string> &rootDirectories() const { return rootDirectories_; }
|
||||
std::vector<std::string> &rootDirectories() { return rootDirectories_; }
|
||||
const std::vector<std::string> &modFileTypes() const { return modFileTypes_; }
|
||||
const std::vector<std::string> &fileTypes() const { return fileTypes_; }
|
||||
|
||||
static constexpr const char *DEFAULT_FILE_TYPES = "audio,nammodel,mlmodel,sf2,sfz,midisong,midiclip,*"; // (everything)
|
||||
|
||||
private:
|
||||
std::vector<std::string> rootDirectories_;
|
||||
std::vector<std::string> modFileTypes_; // e.g. "audio", "nammodel", etc.
|
||||
std::vector<std::string> fileTypes_;
|
||||
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
#include "ModGui.hpp"
|
||||
#include "MapFeature.hpp"
|
||||
#include "PluginHost.hpp"
|
||||
#include "Finally.hpp"
|
||||
#include "lv2/atom/atom.h"
|
||||
|
||||
|
||||
#define MOD_GUI_PREFIX "http://moddevices.com/ns/modgui#"
|
||||
#define MOD_GUI__gui (MOD_GUI_PREFIX "gui")
|
||||
#define MOD_GUI__modgui (MOD_GUI_PREFIX "modgui")
|
||||
#define MOD_GUI__resourcesDirectory (MOD_GUI_PREFIX "resourcesDirectory")
|
||||
#define MOD_GUI__iconTemplate (MOD_GUI_PREFIX "iconTemplate")
|
||||
#define MOD_GUI__settingsTemplate (MOD_GUI_PREFIX "settingsTemplate")
|
||||
#define MOD_GUI__javascript (MOD_GUI_PREFIX "javascript")
|
||||
#define MOD_GUI__stylesheet (MOD_GUI_PREFIX "stylesheet")
|
||||
#define MOD_GUI__screenshot (MOD_GUI_PREFIX "screenshot")
|
||||
#define MOD_GUI__thumbnail (MOD_GUI_PREFIX "thumbnail")
|
||||
#define MOD_GUI__discussionURL (MOD_GUI_PREFIX "discussionURL")
|
||||
#define MOD_GUI__documentation (MOD_GUI_PREFIX "documentation")
|
||||
#define MOD_GUI__brand (MOD_GUI_PREFIX "brand")
|
||||
#define MOD_GUI__label (MOD_GUI_PREFIX "label")
|
||||
#define MOD_GUI__model (MOD_GUI_PREFIX "model")
|
||||
#define MOD_GUI__panel (MOD_GUI_PREFIX "panel")
|
||||
#define MOD_GUI__color (MOD_GUI_PREFIX "color")
|
||||
#define MOD_GUI__knob (MOD_GUI_PREFIX "knob")
|
||||
#define MOD_GUI__port (MOD_GUI_PREFIX "port")
|
||||
#define MOD_GUI__monitoredOutputs (MOD_GUI_PREFIX "monitoredOutputs")
|
||||
|
||||
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
ModGuiUris::ModGuiUris(LilvWorld *pWorld, MapFeature &mapFeature)
|
||||
{
|
||||
mod_gui__gui = lilv_new_uri(pWorld, MOD_GUI__gui);
|
||||
mod_gui__modgui = lilv_new_uri(pWorld, MOD_GUI__modgui);
|
||||
mod_gui__resourceDirectory = lilv_new_uri(pWorld, MOD_GUI__resourcesDirectory);
|
||||
mod_gui__iconTemplate = lilv_new_uri(pWorld, MOD_GUI__iconTemplate);
|
||||
mod_gui__settingsTemplate = lilv_new_uri(pWorld, MOD_GUI__settingsTemplate);
|
||||
mod_gui__javascript = lilv_new_uri(pWorld, MOD_GUI__javascript);
|
||||
mod_gui__stylesheet = lilv_new_uri(pWorld, MOD_GUI__stylesheet);
|
||||
mod_gui__screenshot = lilv_new_uri(pWorld, MOD_GUI__screenshot);
|
||||
mod_gui__thumbnail = lilv_new_uri(pWorld, MOD_GUI__thumbnail);
|
||||
mod_gui__discussionURL = lilv_new_uri(pWorld, MOD_GUI__discussionURL);
|
||||
mod_gui__documentation = lilv_new_uri(pWorld, MOD_GUI__documentation);
|
||||
mod_gui__brand = lilv_new_uri(pWorld, MOD_GUI__brand);
|
||||
mod_gui__label = lilv_new_uri(pWorld, MOD_GUI__label);
|
||||
mod_gui__model = lilv_new_uri(pWorld, MOD_GUI__model);
|
||||
mod_gui__panel = lilv_new_uri(pWorld, MOD_GUI__panel);
|
||||
mod_gui__color = lilv_new_uri(pWorld, MOD_GUI__color);
|
||||
mod_gui__knob = lilv_new_uri(pWorld, MOD_GUI__knob);
|
||||
mod_gui__port = lilv_new_uri(pWorld, MOD_GUI__port);
|
||||
mod_gui__monitoredOutputs = lilv_new_uri(pWorld, MOD_GUI__monitoredOutputs);
|
||||
}
|
||||
|
||||
static const char*NonNull(const char*string) {
|
||||
return string ? string : "";
|
||||
}
|
||||
|
||||
|
||||
ModGui::ModGui(
|
||||
PluginHost *lv2Host,
|
||||
const LilvPlugin *lilvPlugin,
|
||||
const std::string &resourceDirectory,
|
||||
const LilvNode * modGuiUrl)
|
||||
: pluginUri_(lilv_node_as_uri(lilv_plugin_get_uri(lilvPlugin))),
|
||||
resourceDirectory_(resourceDirectory)
|
||||
{
|
||||
LilvWorld *pWorld = lv2Host->getWorld();
|
||||
ModGuiUris &modGuiUrids = *lv2Host->mod_gui_uris;
|
||||
PluginHost::LilvUris &lilvUris = *lv2Host->lilvUris;
|
||||
|
||||
AutoLilvNode modguiIcon = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__iconTemplate, nullptr);
|
||||
if (modguiIcon) {
|
||||
this->iconTemplate_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiIcon), nullptr));
|
||||
}
|
||||
|
||||
AutoLilvNode modguiSettingsTemplate = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__settingsTemplate, nullptr);
|
||||
if (modguiSettingsTemplate) {
|
||||
this->settingsTemplate_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiSettingsTemplate), nullptr));
|
||||
}
|
||||
|
||||
AutoLilvNode modguiJavascript = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__javascript, nullptr);
|
||||
if (modguiJavascript) {
|
||||
this->javascript_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiJavascript), nullptr));
|
||||
}
|
||||
|
||||
AutoLilvNode modguiStylesheet = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__stylesheet, nullptr);
|
||||
if (modguiStylesheet) {
|
||||
this->stylesheet_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiStylesheet), nullptr));
|
||||
}
|
||||
|
||||
AutoLilvNode modguiScreenshot = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__screenshot, nullptr);
|
||||
if (modguiScreenshot) {
|
||||
this->screenshot_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiScreenshot), nullptr));
|
||||
}
|
||||
|
||||
AutoLilvNode modguiThumbnail = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__thumbnail, nullptr);
|
||||
if (modguiThumbnail) {
|
||||
this->thumbnail_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiThumbnail), nullptr));
|
||||
}
|
||||
AutoLilvNode discussionUrl = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__discussionURL, nullptr);
|
||||
if (discussionUrl) {
|
||||
this->discussionUrl_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(discussionUrl), nullptr));
|
||||
}
|
||||
|
||||
AutoLilvNode documentationUrl = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__documentation, nullptr);
|
||||
if (documentationUrl) {
|
||||
this->documentationUrl_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(documentationUrl), nullptr));
|
||||
}
|
||||
|
||||
AutoLilvNode brand = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__brand, nullptr);
|
||||
if (brand) {
|
||||
this->brand_ = NonNull(lilv_node_as_string(brand));
|
||||
}
|
||||
|
||||
AutoLilvNode label = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__label, nullptr);
|
||||
if (label) {
|
||||
this->label_ = NonNull(lilv_node_as_string(label));
|
||||
}
|
||||
|
||||
AutoLilvNode model = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__model, nullptr);
|
||||
if (model) {
|
||||
this->model_ = NonNull(lilv_node_as_string(model));
|
||||
}
|
||||
|
||||
AutoLilvNode panel = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__panel, nullptr);
|
||||
if (panel) {
|
||||
this->panel_ = NonNull(lilv_node_as_string(panel));
|
||||
}
|
||||
|
||||
AutoLilvNode color = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__color, nullptr);
|
||||
if (color) {
|
||||
this->color_ = NonNull(lilv_node_as_string(color));
|
||||
}
|
||||
|
||||
AutoLilvNode knob = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__knob, nullptr);
|
||||
if (knob) {
|
||||
this->knob_ = NonNull(lilv_node_as_string(knob));
|
||||
}
|
||||
|
||||
AutoLilvNodes ports = lilv_world_find_nodes(pWorld, modGuiUrl, modGuiUrids.mod_gui__port, nullptr);
|
||||
if (ports) {
|
||||
LILV_FOREACH(nodes, it, ports.Get()) {
|
||||
AutoLilvNode port_node = lilv_nodes_get(ports, it);
|
||||
this->ports_.push_back(ModGuiPort(lv2Host,port_node));
|
||||
}
|
||||
}
|
||||
std::sort(this->ports_.begin(), this->ports_.end(),
|
||||
[](const ModGuiPort &p1, const ModGuiPort &p2) {
|
||||
return p1.index() < p2.index();
|
||||
});
|
||||
AutoLilvNodes monitoredOutputs = lilv_world_find_nodes(pWorld, modGuiUrl, modGuiUrids.mod_gui__monitoredOutputs, nullptr);
|
||||
if (monitoredOutputs) {
|
||||
LILV_FOREACH(nodes, it, monitoredOutputs.Get()) {
|
||||
AutoLilvNode monitoredOutput = lilv_nodes_get(monitoredOutputs.Get(), it);
|
||||
AutoLilvNode symbol = lilv_world_get(pWorld, monitoredOutput, lilvUris.lv2core__symbol, nullptr);
|
||||
if (symbol) {
|
||||
this->monitoredOutputs_.push_back(NonNull(lilv_node_as_string(symbol)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ModGui::ptr ModGui::Create(PluginHost *lv2Host, const LilvPlugin *lilvPlugin)
|
||||
{
|
||||
|
||||
LilvWorld *pWorld = lv2Host->getWorld();
|
||||
ModGuiUris &modGuiUrids = *(lv2Host->mod_gui_uris);
|
||||
|
||||
AutoLilvNode modGuiUri;
|
||||
std::string resourceDirectory;
|
||||
|
||||
AutoLilvNodes modGuiNodes = lilv_plugin_get_value(lilvPlugin,modGuiUrids.mod_gui__gui);
|
||||
|
||||
if (!modGuiNodes) {
|
||||
return nullptr; // no mod gui found.
|
||||
}
|
||||
LILV_FOREACH(nodes, it, modGuiNodes.Get()) {
|
||||
AutoLilvNode node = lilv_nodes_get(modGuiNodes.Get(), it);
|
||||
|
||||
AutoLilvNode resourceDir = lilv_world_get(pWorld, node, modGuiUrids.mod_gui__resourceDirectory,nullptr);
|
||||
if (resourceDir) {
|
||||
resourceDirectory = lilv_file_uri_parse(lilv_node_as_string(resourceDir),nullptr);
|
||||
modGuiUri = lilv_node_duplicate(node);
|
||||
} else {
|
||||
resourceDirectory.clear();
|
||||
modGuiUri.Free();
|
||||
}
|
||||
}
|
||||
if (!modGuiUri) {
|
||||
return nullptr; // no mod gui found.
|
||||
}
|
||||
|
||||
|
||||
return std::shared_ptr<ModGui>(new ModGui(lv2Host, lilvPlugin, resourceDirectory, modGuiUri.Get()));
|
||||
}
|
||||
|
||||
|
||||
ModGuiPort::ModGuiPort(PluginHost *lv2Host, const LilvNode *portNode)
|
||||
{
|
||||
LilvWorld *pWorld = lv2Host->getWorld();
|
||||
PluginHost::LilvUris &lilvUris = *lv2Host->lilvUris;
|
||||
|
||||
this->index_ = lilv_node_as_int(lilv_world_get(pWorld, portNode, lilvUris.lv2core__index, nullptr));
|
||||
this->symbol_ = NonNull(lilv_node_as_string(lilv_world_get(pWorld, portNode, lilvUris.lv2core__symbol, nullptr)));
|
||||
this->name_ = NonNull(lilv_node_as_string(lilv_world_get(pWorld, portNode, lilvUris.lv2core__name, nullptr)));
|
||||
}
|
||||
|
||||
static json_variant UnitsObj(const std::string &label, const std::string &render, const std::string &symbol)
|
||||
{
|
||||
json_variant obj = json_variant::make_object();
|
||||
obj["label"] = label;
|
||||
obj["render"] = render;
|
||||
obj["symbol"] = symbol;
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
static std::map<Units,json_variant> unitsMap
|
||||
{
|
||||
{Units::none, UnitsObj("","%f","")},
|
||||
{Units::unknown, UnitsObj("","%f","")},
|
||||
|
||||
{Units::s, UnitsObj("seconds", "%f s", "s")},
|
||||
{Units::ms, UnitsObj("milliseconds", "%f ms", "ms")},
|
||||
{Units::min, UnitsObj("minutes", "%f mins", "min")},
|
||||
{Units::bar, UnitsObj("bars", "%f bars", "bars")},
|
||||
{Units::beat, UnitsObj("beats", "%f beats", "beats")},
|
||||
{Units::frame, UnitsObj("audio frames", "%f frames", "frames")},
|
||||
{Units::m, UnitsObj("metres", "%f m", "m")},
|
||||
{Units::cm, UnitsObj("centimetres", "%f cm", "cm")},
|
||||
{Units::mm, UnitsObj("millimetres", "%f mm", "mm")},
|
||||
{Units::km, UnitsObj("kilometres", "%f km", "km")},
|
||||
{Units::inch, UnitsObj("inches", "\"%f\"", "in")},
|
||||
{Units::mile, UnitsObj("miles", "%f mi", "mi")},
|
||||
{Units::db, UnitsObj("decibels", "%f dB", "dB")},
|
||||
{Units::pc, UnitsObj("percent", "%f%%", "%")},
|
||||
{Units::coef, UnitsObj("coefficient", "* %f", "*")},
|
||||
{Units::hz, UnitsObj("hertz", "%f Hz", "Hz")},
|
||||
{Units::khz, UnitsObj("kilohertz", "%f kHz", "kHz")},
|
||||
{Units::mhz, UnitsObj("megahertz", "%f MHz", "MHz")},
|
||||
{Units::bpm, UnitsObj("beats per minute", "%f BPM", "BPM")},
|
||||
{Units::oct, UnitsObj("octaves", "%f octaves", "oct")},
|
||||
{Units::cent, UnitsObj("cents", "%f ct", "ct")},
|
||||
{Units::semitone12TET, UnitsObj("semitones", "%f semi", "semi")},
|
||||
{Units::degree, UnitsObj("degrees", "%f°", "°")},
|
||||
{Units::midiNote, UnitsObj("MIDI note", "MIDI note %d", "note")},
|
||||
};
|
||||
|
||||
static json_variant UnitsToJsonVariant(Units units)
|
||||
{
|
||||
if (unitsMap.find(units) != unitsMap.end())
|
||||
{
|
||||
return unitsMap[units];
|
||||
}
|
||||
return unitsMap[Units::none];
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline json_variant MakeJsonArray(const std::vector<T> &vec)
|
||||
{
|
||||
json_variant array = json_variant::make_array();
|
||||
auto &arrayRef = *array.as_array();
|
||||
for (const auto &item : vec)
|
||||
{
|
||||
json_variant itemVariant = json_variant(item);
|
||||
arrayRef.push_back(itemVariant);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static std::string AtomTypeToRangesType(const std::string& atomType)
|
||||
{
|
||||
if (atomType == LV2_ATOM__Path ||
|
||||
atomType == LV2_ATOM__String ||
|
||||
atomType == LV2_ATOM__URI ||
|
||||
atomType == LV2_ATOM__URID)
|
||||
{
|
||||
return "s";
|
||||
}
|
||||
if (atomType == LV2_ATOM__Float)
|
||||
{
|
||||
return "f";
|
||||
}
|
||||
if (atomType == LV2_ATOM__Double)
|
||||
{
|
||||
return "d";
|
||||
}
|
||||
if (atomType == LV2_ATOM__Long)
|
||||
{
|
||||
return "l";
|
||||
}
|
||||
if (atomType == LV2_ATOM__Vector)
|
||||
{
|
||||
return "v";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
json_variant pipedal::MakeModGuiTemplateData(
|
||||
std::shared_ptr<Lv2PluginInfo> pluginInfo)
|
||||
{
|
||||
json_variant context = json_variant::make_object();
|
||||
auto &contextObj = *(context.as_object());
|
||||
|
||||
auto modGui = pluginInfo->modGui();
|
||||
|
||||
contextObj["brand"] = modGui->brand();
|
||||
contextObj["label"] = modGui->label();
|
||||
contextObj["model"] = modGui->model();
|
||||
contextObj["panel"] = modGui->panel();
|
||||
contextObj["color"] = modGui->color();
|
||||
contextObj["knob"] = modGui->knob();
|
||||
|
||||
json_variant controls = json_variant::make_array();
|
||||
auto &controlArray = *controls.as_array();
|
||||
size_t ix = 0;
|
||||
for (const auto& modGuiPort : modGui->ports())
|
||||
{
|
||||
const Lv2PortInfo &pluginPort = pluginInfo->getPort(modGuiPort.symbol());
|
||||
if (!pluginPort.is_control_port() || !pluginPort.is_input() || pluginPort.not_on_gui())
|
||||
{
|
||||
continue; // only include control ports
|
||||
}
|
||||
json_variant control = json_variant::make_object();
|
||||
auto &portObj = *control.as_object();
|
||||
portObj["index"] = double(pluginPort.index());
|
||||
portObj["symbol"] = pluginPort.symbol();
|
||||
portObj["name"] = modGuiPort.name();
|
||||
portObj["comment"] = pluginPort.comment();
|
||||
|
||||
portObj["units"] = UnitsToJsonVariant(pluginPort.units());
|
||||
|
||||
{
|
||||
json_variant ranges = json_variant::make_object();
|
||||
ranges["minimum"] = SS(pluginPort.min_value());
|
||||
ranges["maximum"] = SS(pluginPort.max_value());
|
||||
ranges["default"] = SS(pluginPort.default_value());
|
||||
portObj["ranges"] = json_variant::make_array();
|
||||
}
|
||||
json_variant scalePoints = json_variant::make_array();
|
||||
auto &scalePointsArray = *scalePoints.as_array();
|
||||
for (const auto &scalePoint: pluginPort.scale_points())
|
||||
{
|
||||
json_variant scalePointObj = json_variant::make_object();
|
||||
scalePointObj["valid"] = true;
|
||||
scalePointObj["label"] = scalePoint.label();
|
||||
scalePointObj["value"] = SS(scalePoint.value());
|
||||
scalePointsArray.push_back(scalePointObj);
|
||||
}
|
||||
portObj["scalePoints"] = scalePoints;
|
||||
|
||||
controlArray.push_back(control);
|
||||
}
|
||||
contextObj["controls"] = controls;
|
||||
|
||||
json_variant effect = json_variant::make_object();
|
||||
contextObj["effect"] = effect;
|
||||
|
||||
json_variant ports = json_variant::make_object();
|
||||
(*effect.as_object())["ports"] = ports;
|
||||
|
||||
json_variant audio = json_variant::make_object();
|
||||
(*ports.as_object())["audio"] = audio;
|
||||
|
||||
json_variant audio_input = json_variant::make_array();
|
||||
(*audio.as_object())["input"] = audio_input;
|
||||
|
||||
json_variant audio_output = json_variant::make_array();
|
||||
(*audio.as_object())["output"] = audio_output;
|
||||
json_variant midi = json_variant::make_object();
|
||||
(*ports.as_object())["midi"] = midi;
|
||||
|
||||
json_variant midi_input = json_variant::make_array();
|
||||
(*midi.as_object())["input"] = midi_input;
|
||||
json_variant midi_output = json_variant::make_array();
|
||||
(*midi.as_object())["output"] = midi_output;
|
||||
|
||||
json_variant cv = json_variant::make_object();
|
||||
(*ports.as_object())["cv"] = cv;
|
||||
|
||||
json_variant cv_input = json_variant::make_array();
|
||||
(*cv.as_object())["input"] = cv_input;
|
||||
json_variant cv_output = json_variant::make_array();
|
||||
(*cv.as_object())["output"] = cv_output;
|
||||
|
||||
{
|
||||
json_variant parametersObj = json_variant::make_object();
|
||||
json_variant pathObj = json_variant::make_array();
|
||||
parametersObj["path"] = pathObj;
|
||||
|
||||
for (const auto &patchProperty: pluginInfo->patchProperties())
|
||||
{
|
||||
json_variant parameterObj = json_variant::make_object();
|
||||
parameterObj["valid"] = true;
|
||||
parameterObj["readable"] = patchProperty.readable();
|
||||
parameterObj["writable"] = patchProperty.writable();
|
||||
parameterObj["uri"] = patchProperty.uri();
|
||||
parameterObj["label"] = patchProperty.label();
|
||||
parameterObj["type"] = patchProperty.type();
|
||||
|
||||
json_variant rangesObj = json_variant::make_object();
|
||||
rangesObj["type"] = AtomTypeToRangesType(patchProperty.type());
|
||||
rangesObj["atomType"] = patchProperty.type();
|
||||
parameterObj["ranges"] = rangesObj;
|
||||
|
||||
parameterObj["comment"] = patchProperty.comment();
|
||||
parameterObj["shortName"] = patchProperty.shortName();
|
||||
|
||||
parameterObj["fileTypes"] = MakeJsonArray(patchProperty.fileTypes());
|
||||
parameterObj["supportedExtensions"] = MakeJsonArray(patchProperty.supportedExtensions());
|
||||
|
||||
if (patchProperty.type() == LV2_ATOM__Path && patchProperty.writable()) {
|
||||
// Feed them no files; they will be added later.
|
||||
json_variant filesObj = json_variant::make_array();
|
||||
json_variant fileObj = json_variant::make_object();
|
||||
fileObj["fileType"] = "{{filetype}}"; // magic tags that we will use to generate files at runtime.
|
||||
fileObj["fullname"] = "{{fullname}}";
|
||||
fileObj["basename"] = "{{basename}}";
|
||||
filesObj.as_array()->push_back(fileObj);
|
||||
parameterObj["files"] = filesObj;
|
||||
|
||||
pathObj.as_array()->push_back(parameterObj);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
effect["parameters"] = parametersObj;
|
||||
|
||||
|
||||
}
|
||||
|
||||
for (const auto &pluginPort : pluginInfo->ports())
|
||||
{
|
||||
if (pluginPort->is_control_port())
|
||||
{
|
||||
continue; // only include control ports
|
||||
}
|
||||
if (pluginPort->is_audio_port())
|
||||
{
|
||||
json_variant audioPort = json_variant::make_object();
|
||||
auto &audioPortObj = *audioPort.as_object();
|
||||
audioPortObj["index"] = double(pluginPort->index());
|
||||
audioPortObj["symbol"] = pluginPort->symbol();
|
||||
audioPortObj["name"] = pluginPort->name();
|
||||
if (pluginPort->is_input())
|
||||
{
|
||||
audio_input.as_array()->push_back(audioPort);
|
||||
}
|
||||
else
|
||||
{
|
||||
audio_output.as_array()->push_back(audioPort);
|
||||
}
|
||||
}
|
||||
else if (pluginPort->is_atom_port())
|
||||
{
|
||||
if (pluginPort->supports_midi())
|
||||
{
|
||||
json_variant midiPort = json_variant::make_object();
|
||||
auto &midiPortObj = *midiPort.as_object();
|
||||
midiPortObj["index"] = double(pluginPort->index());
|
||||
midiPortObj["symbol"] = pluginPort->symbol();
|
||||
midiPortObj["name"] = pluginPort->name();
|
||||
if (pluginPort->is_input())
|
||||
{
|
||||
midi_input.as_array()->push_back(midiPort);
|
||||
}
|
||||
else
|
||||
{
|
||||
midi_output.as_array()->push_back(midiPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* else if plugin_port->is_cv_port()... */
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
JSON_MAP_BEGIN(ModGuiPort)
|
||||
JSON_MAP_REFERENCE(ModGuiPort,index)
|
||||
JSON_MAP_REFERENCE(ModGuiPort,symbol)
|
||||
JSON_MAP_REFERENCE(ModGuiPort,name)
|
||||
JSON_MAP_END()
|
||||
|
||||
JSON_MAP_BEGIN(ModGui)
|
||||
JSON_MAP_REFERENCE(ModGui,pluginUri)
|
||||
JSON_MAP_REFERENCE(ModGui,resourceDirectory)
|
||||
JSON_MAP_REFERENCE(ModGui,iconTemplate)
|
||||
JSON_MAP_REFERENCE(ModGui,settingsTemplate)
|
||||
JSON_MAP_REFERENCE(ModGui,screenshot)
|
||||
JSON_MAP_REFERENCE(ModGui,javascript)
|
||||
JSON_MAP_REFERENCE(ModGui,stylesheet)
|
||||
JSON_MAP_REFERENCE(ModGui,thumbnail)
|
||||
JSON_MAP_REFERENCE(ModGui,discussionUrl)
|
||||
JSON_MAP_REFERENCE(ModGui,documentationUrl)
|
||||
JSON_MAP_REFERENCE(ModGui,brand)
|
||||
JSON_MAP_REFERENCE(ModGui,label)
|
||||
JSON_MAP_REFERENCE(ModGui,model)
|
||||
JSON_MAP_REFERENCE(ModGui,panel)
|
||||
JSON_MAP_REFERENCE(ModGui,color)
|
||||
JSON_MAP_REFERENCE(ModGui,knob)
|
||||
JSON_MAP_REFERENCE(ModGui,ports)
|
||||
JSON_MAP_REFERENCE(ModGui,monitoredOutputs)
|
||||
JSON_MAP_END()
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "lv2/urid/urid.h"
|
||||
#include <string>
|
||||
#include <lilv/lilv.h>
|
||||
#include <memory>
|
||||
#include "AutoLilvNode.hpp"
|
||||
#include "json.hpp"
|
||||
#include "json_variant.hpp"
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
|
||||
class MapFeature;
|
||||
class Lv2PluginInfo;
|
||||
|
||||
class ModGuiUris
|
||||
{
|
||||
public:
|
||||
ModGuiUris(LilvWorld *pWorld, MapFeature &mapFeature);
|
||||
|
||||
AutoLilvNode mod_gui__gui;
|
||||
AutoLilvNode mod_gui__modgui;
|
||||
AutoLilvNode mod_gui__resourceDirectory;
|
||||
AutoLilvNode mod_gui__iconTemplate;
|
||||
AutoLilvNode mod_gui__settingsTemplate;
|
||||
AutoLilvNode mod_gui__javascript;
|
||||
AutoLilvNode mod_gui__stylesheet;
|
||||
AutoLilvNode mod_gui__screenshot;
|
||||
AutoLilvNode mod_gui__thumbnail;
|
||||
AutoLilvNode mod_gui__discussionURL;
|
||||
AutoLilvNode mod_gui__documentation;
|
||||
AutoLilvNode mod_gui__brand;
|
||||
AutoLilvNode mod_gui__label;
|
||||
AutoLilvNode mod_gui__model;
|
||||
AutoLilvNode mod_gui__panel;
|
||||
AutoLilvNode mod_gui__color;
|
||||
AutoLilvNode mod_gui__knob;
|
||||
AutoLilvNode mod_gui__port;
|
||||
AutoLilvNode mod_gui__monitoredOutputs;
|
||||
};
|
||||
|
||||
class PluginHost;
|
||||
|
||||
class ModGuiPort {
|
||||
public:
|
||||
ModGuiPort() = default;
|
||||
ModGuiPort(PluginHost *lv2Host, const LilvNode *portNode);
|
||||
private:
|
||||
uint32_t index_ = 0;
|
||||
std::string symbol_;
|
||||
std::string name_;
|
||||
public:
|
||||
uint32_t index() const { return index_; }
|
||||
void index(uint32_t value) { index_ = value; }
|
||||
const std::string &symbol() const { return symbol_; }
|
||||
void symbol(const std::string &value) { symbol_ = value; }
|
||||
const std::string &name() const { return name_; }
|
||||
void name(const std::string &value) { name_ = value; }
|
||||
|
||||
DECLARE_JSON_MAP(ModGuiPort);
|
||||
};
|
||||
|
||||
class ModGui
|
||||
{
|
||||
private:
|
||||
ModGui(PluginHost *lv2Host, const LilvPlugin *lilvPlugin, const std::string &resourceDirectory, const LilvNode *modGuiNode);
|
||||
|
||||
public:
|
||||
using self = ModGui;
|
||||
using ptr = std::shared_ptr<self>;
|
||||
static ptr Create(PluginHost *lv2Host, const LilvPlugin *lilvPlugin);
|
||||
|
||||
ModGui() = default;
|
||||
virtual ~ModGui() = default;
|
||||
|
||||
ModGui(const ModGui &) = default;
|
||||
ModGui &operator=(const ModGui &) = default;
|
||||
ModGui(ModGui &&) = default;
|
||||
ModGui &operator=(ModGui &&) = default;
|
||||
|
||||
private:
|
||||
std::string pluginUri_;
|
||||
|
||||
std::string resourceDirectory_;
|
||||
std::string iconTemplate_;
|
||||
std::string settingsTemplate_;
|
||||
std::string javascript_;
|
||||
std::string stylesheet_;
|
||||
std::string screenshot_;
|
||||
std::string thumbnail_;
|
||||
std::string discussionUrl_;
|
||||
std::string documentationUrl_;
|
||||
std::string brand_;
|
||||
std::string label_;
|
||||
std::string model_;
|
||||
std::string panel_;
|
||||
std::string color_;
|
||||
std::string knob_;
|
||||
std::vector<ModGuiPort> ports_;
|
||||
|
||||
std::vector<std::string> monitoredOutputs_;
|
||||
|
||||
public:
|
||||
const std::string &pluginUri() const { return pluginUri_; }
|
||||
void pluginUri(const std::string &value) { pluginUri_ = value; }
|
||||
|
||||
const std::string &resourceDirectory() const { return resourceDirectory_; }
|
||||
void resourceDirectory(const std::string &value) { resourceDirectory_ = value; }
|
||||
const std::string &iconTemplate() const { return iconTemplate_; }
|
||||
void iconTemplate(const std::string &value) { iconTemplate_ = value; }
|
||||
const std::string &settingsTemplate() const { return settingsTemplate_; }
|
||||
void settingsTemplate(const std::string &value) { settingsTemplate_ = value; }
|
||||
const std::string &screenshot() const { return screenshot_; }
|
||||
const std::string &javascript() const { return javascript_; }
|
||||
void javascript(const std::string &value) { javascript_ = value; }
|
||||
const std::string &stylesheet() const { return stylesheet_; }
|
||||
void stylesheet(const std::string &value) { stylesheet_ = value; }
|
||||
void screenshot(const std::string &value) { screenshot_ = value; }
|
||||
const std::string &thumbnail() const { return thumbnail_; }
|
||||
void thumbnail(const std::string &value) { thumbnail_ = value; }
|
||||
const std::string &discussionUrl() const { return discussionUrl_; }
|
||||
void discussionUrl(const std::string &value) { discussionUrl_ = value; }
|
||||
const std::string &documentationUrl() const { return documentationUrl_; }
|
||||
void documentationUrl(const std::string &value) { documentationUrl_ = value; }
|
||||
const std::string &brand() const { return brand_; }
|
||||
void brand(const std::string &value) { brand_ = value; }
|
||||
const std::string &label() const { return label_; }
|
||||
void label(const std::string &value) { label_ = value; }
|
||||
const std::string &model() const { return model_; }
|
||||
void model(const std::string &value) { model_ = value; }
|
||||
const std::string &panel() const { return panel_; }
|
||||
void panel(const std::string &value) { panel_ = value; }
|
||||
|
||||
const std::string &color() const { return color_; }
|
||||
void color(const std::string &value) { color_ = value; }
|
||||
const std::string &knob() const { return knob_; }
|
||||
void knob(const std::string &value) { knob_ = value; }
|
||||
|
||||
const std::vector<ModGuiPort> &ports() const { return ports_; }
|
||||
std::vector<ModGuiPort> &ports() { return ports_; }
|
||||
void ports(const std::vector<ModGuiPort> &value) { ports_ = value; }
|
||||
|
||||
|
||||
const std::vector<std::string> &monitoredOutputs() const { return monitoredOutputs_; }
|
||||
std::vector<std::string> &monitoredOutputs() { return monitoredOutputs_; }
|
||||
void monitoredOutputs(const std::vector<std::string> &value) { monitoredOutputs_ = value; }
|
||||
|
||||
DECLARE_JSON_MAP(ModGui);
|
||||
|
||||
};
|
||||
|
||||
json_variant MakeModGuiTemplateData(
|
||||
std::shared_ptr<Lv2PluginInfo> pluginInfo
|
||||
);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2025 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#include "pch.h"
|
||||
#include "catch.hpp"
|
||||
#include <sstream>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include "ModTemplateGenerator.hpp"
|
||||
|
||||
#include "PiPedalModel.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
using namespace std;
|
||||
|
||||
class PluginHostTest {
|
||||
|
||||
public:
|
||||
static void TestInit()
|
||||
{
|
||||
PiPedalModel model;
|
||||
model.GetPluginHost().LoadLilv();
|
||||
const auto&plugins = model.GetPluginHost().GetPlugins();
|
||||
REQUIRE(!plugins.empty());
|
||||
size_t modGuicount = 0;
|
||||
for (const auto&plugin : plugins)
|
||||
{
|
||||
REQUIRE(plugin != nullptr);
|
||||
|
||||
if (plugin->modGui())
|
||||
{
|
||||
modGuicount++;
|
||||
const auto&modGui = plugin->modGui();
|
||||
REQUIRE(!modGui->iconTemplate().empty());
|
||||
REQUIRE(modGui->javascript().empty());
|
||||
REQUIRE(!modGui->stylesheet().empty());
|
||||
REQUIRE(!modGui->screenshot().empty());
|
||||
REQUIRE(!modGui->thumbnail().empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CASE("ModGui Init Test", "[mod_gui_init]")
|
||||
{
|
||||
PluginHostTest::TestInit();
|
||||
}
|
||||
|
||||
TEST_CASE("ModGui Templates", "[mod_gui_templates]")
|
||||
{
|
||||
|
||||
json_variant data = json_variant::make_object();
|
||||
data["name"] = "Test Plugin";
|
||||
data["uri"] = "http://example.com/test-plugin";
|
||||
data["version"] = "1.0.0";
|
||||
data["author"] = "Test Author";
|
||||
data["cn"] = "14323";
|
||||
json_variant items = json_variant::make_array();
|
||||
for (size_t i = 0; i < 3; ++i)
|
||||
{
|
||||
json_variant item = json_variant::make_object();
|
||||
item["label"] = "Item " + std::to_string(i + 1);
|
||||
items.as_array()->push_back(item);
|
||||
|
||||
}
|
||||
data["items"] = items;
|
||||
|
||||
data["inputs"] = json_variant::make_object();
|
||||
data["inputs"]["input1"] = "Input 1";
|
||||
data["inputs"]["input2"] = "Input 2";
|
||||
|
||||
data["_cn"] = "?cn=1234";
|
||||
data["_cns"] = "_toobamp.lv2_ToobAmp__1234";
|
||||
|
||||
std::string templateString = R"(
|
||||
<div>
|
||||
<h1>{{name}}</h1>
|
||||
<p>URI: {{uri}}</p>
|
||||
<p>Version: {{version}}</p>
|
||||
<p>Author: {{author}}</p>
|
||||
<ul>
|
||||
{{#items}}
|
||||
<li id="item-{{cn}}">{{label}}</li>
|
||||
{{/items}}
|
||||
</ul>
|
||||
<h2>{{inputs.input1}}</h2>
|
||||
<h2>{{inputs.input2}}</h2>
|
||||
<h2>{{inputs.input3}}</h2>
|
||||
<img src="img/helpIcon.png{{{cn}}}" class="{{{cns}}}_help_icon" alt="Help Icon" />
|
||||
</div>
|
||||
)";
|
||||
auto result = GenerateFromTemplateString(templateString, data);
|
||||
|
||||
cout << result;
|
||||
cout << endl;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "ModTemplateGenerator.hpp"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include "util.hpp"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
namespace impl
|
||||
{
|
||||
|
||||
static std::string trim(const std::string &str)
|
||||
{
|
||||
size_t first = str.find_first_not_of(" \t\n\r");
|
||||
if (first == std::string::npos)
|
||||
return "";
|
||||
size_t last = str.find_last_not_of(" \t\n\r");
|
||||
return str.substr(first, (last - first + 1));
|
||||
}
|
||||
|
||||
static bool variableEndsWithNumber(const std::string &variableName)
|
||||
{
|
||||
size_t pos = variableName.find_last_of(".");
|
||||
if (pos == std::string::npos)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (size_t i = pos + 1; i < variableName.length(); ++i)
|
||||
{
|
||||
if (!std::isdigit(variableName[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Check if the last character is a digit
|
||||
return true;
|
||||
}
|
||||
void splitIndexedArrayVariable(
|
||||
const std::string &variableName,
|
||||
std::string &arrayName,
|
||||
int64_t &arrayIndex)
|
||||
{
|
||||
size_t pos = variableName.find_last_of(".");
|
||||
if (pos == std::string::npos)
|
||||
{
|
||||
throw std::logic_error("Variable name does not contain an index: " + variableName);
|
||||
}
|
||||
else
|
||||
{
|
||||
arrayName = variableName.substr(0, pos);
|
||||
arrayIndex = std::stoll(variableName.substr(pos + 1));
|
||||
}
|
||||
}
|
||||
|
||||
class VariableContext
|
||||
{
|
||||
public:
|
||||
VariableContext(
|
||||
const json_variant &context,
|
||||
VariableContext *parent = nullptr)
|
||||
: context(context), parent(parent)
|
||||
{
|
||||
}
|
||||
json_variant getVariable(const std::string &name)
|
||||
{
|
||||
std::vector<std::string> parts = split(trim(name), '.');
|
||||
json_variant *current = &context;
|
||||
for (const auto &part : parts)
|
||||
{
|
||||
if (current->is_object())
|
||||
{
|
||||
auto ff = current->as_object()->find(part);
|
||||
if (ff != current->as_object()->end())
|
||||
{
|
||||
current = &ff->second;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (parent)
|
||||
{
|
||||
return parent->getVariable(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
return json_variant(json_null());
|
||||
}
|
||||
}
|
||||
return *current;
|
||||
}
|
||||
|
||||
private:
|
||||
json_variant context;
|
||||
VariableContext *parent;
|
||||
};
|
||||
|
||||
static std::string GenerateTemplateFromString(
|
||||
const std::string &content,
|
||||
VariableContext &context)
|
||||
{
|
||||
|
||||
std::stringstream ss;
|
||||
|
||||
size_t ix = 0;
|
||||
size_t end = content.length();
|
||||
while (ix < end)
|
||||
{
|
||||
// copy content until we find a '{{'
|
||||
char c = content[ix++];
|
||||
if (c != '{')
|
||||
{
|
||||
ss << c;
|
||||
continue;
|
||||
}
|
||||
if (ix >= end)
|
||||
{
|
||||
break;
|
||||
}
|
||||
c = content[ix++];
|
||||
if (c != '{')
|
||||
{
|
||||
ss << '{';
|
||||
ss << c;
|
||||
continue;
|
||||
}
|
||||
if (ix < end && content[ix] == '{')
|
||||
{
|
||||
// this is a '{{{', so we just copy it.
|
||||
++ix; // skip the third '{'
|
||||
size_t endTagPos = content.find("}}}", ix);
|
||||
if (endTagPos == std::string::npos)
|
||||
{
|
||||
throw std::runtime_error("Unmatched opening tag '{{{' in template.");
|
||||
}
|
||||
std::string variableString = content.substr(ix, endTagPos - ix);
|
||||
ix = endTagPos + 3; // Move past the closing '}}}'
|
||||
auto result = context.getVariable("_" + variableString);
|
||||
if (result.is_null())
|
||||
{
|
||||
result = ""; // Default to empty string if variable not found
|
||||
}
|
||||
if (!result.is_string())
|
||||
{
|
||||
throw std::runtime_error("Variable '" + variableString + "' is not a string.");
|
||||
}
|
||||
ss << result.as_string();
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Now we have a '{{' at position ix-2.
|
||||
size_t endTagPos = content.find("}}", ix);
|
||||
if (endTagPos == std::string::npos)
|
||||
{
|
||||
throw std::runtime_error("Unmatched opening tag '{{' in template.");
|
||||
}
|
||||
std::string variableString = content.substr(ix, endTagPos - ix);
|
||||
ix = endTagPos + 2; // Move past the closing '}}'
|
||||
if (!variableString.starts_with("#"))
|
||||
{
|
||||
// a straightforward variable substitution.
|
||||
json_variant value = context.getVariable(variableString);
|
||||
if (value.is_null())
|
||||
{
|
||||
value = ""; // Default to empty string if variable not found
|
||||
}
|
||||
if (!value.is_string())
|
||||
{
|
||||
throw std::runtime_error("Variable '" + variableString + "' is not a string.");
|
||||
}
|
||||
ss << value.as_string();
|
||||
}
|
||||
else
|
||||
{
|
||||
// this is a looping construct.
|
||||
std::string variableName = variableString.substr(1);
|
||||
std::string endTag = SS("{{/" << variableName << "}}");
|
||||
size_t endTagPos = content.find(endTag, ix);
|
||||
if (endTagPos == std::string::npos)
|
||||
{
|
||||
throw std::runtime_error("Unmatched opening tag for '" + variableString + "' in template.");
|
||||
}
|
||||
|
||||
std::string arrayContent = content.substr(ix, endTagPos - ix);
|
||||
|
||||
if (variableEndsWithNumber(variableName))
|
||||
{
|
||||
std::string arrayName;
|
||||
int64_t arrayIndex = 0;
|
||||
splitIndexedArrayVariable(variableName, arrayName, arrayIndex);
|
||||
|
||||
json_variant array = context.getVariable(arrayName);
|
||||
auto &arrayValue = *array.as_array();
|
||||
if (arrayIndex >= 0 && arrayIndex < static_cast<int64_t>(arrayValue.size()))
|
||||
{
|
||||
json_variant contextValue = arrayValue[(size_t)arrayIndex];
|
||||
// variable frame.
|
||||
VariableContext itemContext{contextValue, &context};
|
||||
|
||||
ss << GenerateTemplateFromString(
|
||||
arrayContent,
|
||||
itemContext);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
json_variant variable = context.getVariable(variableName);
|
||||
if (variable.is_array())
|
||||
{
|
||||
auto &arrayValue = *variable.as_array();
|
||||
|
||||
for (auto iter = arrayValue.begin(); iter != arrayValue.end(); ++iter)
|
||||
{
|
||||
// variable frame.
|
||||
VariableContext itemContext{*iter, &context};
|
||||
|
||||
ss << GenerateTemplateFromString(
|
||||
arrayContent,
|
||||
itemContext);
|
||||
}
|
||||
} else if (variable.is_object())
|
||||
{
|
||||
VariableContext itemContext(variable,&context);
|
||||
ss << GenerateTemplateFromString(
|
||||
arrayContent,
|
||||
itemContext);
|
||||
}
|
||||
}
|
||||
|
||||
ix = endTagPos + endTag.length(); // Move past the closing tag
|
||||
}
|
||||
}
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
}
|
||||
using namespace impl;
|
||||
|
||||
std::string GenerateFromTemplateString(
|
||||
const std::string &templateString,
|
||||
const json_variant &data)
|
||||
{
|
||||
VariableContext context{data, nullptr};
|
||||
return impl::GenerateTemplateFromString(
|
||||
templateString,
|
||||
context);
|
||||
}
|
||||
|
||||
std::string GenerateFromTemplateFile(
|
||||
const std::filesystem::path &templateFilePath,
|
||||
const json_variant &data)
|
||||
{
|
||||
std::string templateContent;
|
||||
if (!fs::exists(templateFilePath))
|
||||
{
|
||||
throw std::runtime_error("Template file not found: " + templateFilePath.string());
|
||||
}
|
||||
std::ifstream file(templateFilePath);
|
||||
if (!file.is_open())
|
||||
{
|
||||
throw std::runtime_error("Failed to open template file: " + templateFilePath.string());
|
||||
}
|
||||
templateContent.assign((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
return GenerateFromTemplateString(
|
||||
templateContent,
|
||||
data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include "json_variant.hpp"
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
std::string GenerateFromTemplateString(
|
||||
const std::string& templateString,
|
||||
const json_variant& data
|
||||
);
|
||||
|
||||
std::string GenerateFromTemplateFile(
|
||||
const std::filesystem::path& templateFilePath,
|
||||
const json_variant& data
|
||||
);
|
||||
}
|
||||
@@ -242,7 +242,7 @@ void PiPedalModel::LoadLv2PluginInfo()
|
||||
}
|
||||
|
||||
pluginChangeMonitor = std::make_unique<Lv2PluginChangeMonitor>(*this);
|
||||
pluginHost.Load(configuration.GetLv2Path().c_str());
|
||||
pluginHost.LoadLilv(configuration.GetLv2Path().c_str());
|
||||
|
||||
// Copy all presets out of Lilv data to json files
|
||||
// so that we can close lilv while we're actually
|
||||
|
||||
@@ -331,7 +331,9 @@ namespace pipedal
|
||||
void LoadLv2PluginInfo();
|
||||
void Load();
|
||||
|
||||
const PluginHost &GetLv2Host() const { return pluginHost; }
|
||||
const PluginHost &GetPluginHost() const { return pluginHost; }
|
||||
PluginHost &GetPluginHost() { return pluginHost; }
|
||||
|
||||
Pedalboard GetCurrentPedalboardCopy()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex);
|
||||
|
||||
@@ -1331,12 +1331,12 @@ public:
|
||||
}
|
||||
else if (message == "plugins")
|
||||
{
|
||||
auto ui_plugins = model.GetLv2Host().GetUiPlugins();
|
||||
auto ui_plugins = model.GetPluginHost().GetUiPlugins();
|
||||
Reply(replyTo, "plugins", ui_plugins);
|
||||
}
|
||||
else if (message == "pluginClasses")
|
||||
{
|
||||
auto classes = model.GetLv2Host().GetLv2PluginClass();
|
||||
auto classes = model.GetPluginHost().GetLv2PluginClass();
|
||||
Reply(replyTo, "pluginClasses", classes);
|
||||
}
|
||||
else if (message == "hello")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
// Copyright (c) 2025 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
@@ -136,8 +136,11 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
|
||||
|
||||
lv2core__symbol = lilv_new_uri(pWorld, LV2_CORE__symbol);
|
||||
lv2core__name = lilv_new_uri(pWorld, LV2_CORE__name);
|
||||
lv2core__shortName = lilv_new_uri(pWorld, LV2_CORE_PREFIX "shortName"); // ?? from mod sources
|
||||
lv2core__index = lilv_new_uri(pWorld, LV2_CORE__index);
|
||||
lv2core__Parameter = lilv_new_uri(pWorld, LV2_CORE_PREFIX "Parameter");
|
||||
lv2core__minorVersion = lilv_new_uri(pWorld, LV2_CORE__minorVersion);
|
||||
lv2core__microVersion = lilv_new_uri(pWorld, LV2_CORE__microVersion);
|
||||
|
||||
pipedalUI__ui = lilv_new_uri(pWorld, PIPEDAL_UI__ui);
|
||||
pipedalUI__fileProperties = lilv_new_uri(pWorld, PIPEDAL_UI__fileProperties);
|
||||
@@ -150,7 +153,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
|
||||
pipedalUI__mimeType = lilv_new_uri(pWorld, PIPEDAL_UI__mimeType);
|
||||
pipedalUI__outputPorts = lilv_new_uri(pWorld, PIPEDAL_UI__outputPorts);
|
||||
pipedalUI__text = lilv_new_uri(pWorld, PIPEDAL_UI__text);
|
||||
pipedalUI__ledColor = lilv_new_uri(pWorld,PIPEDAL_UI__ledColor);
|
||||
pipedalUI__ledColor = lilv_new_uri(pWorld, PIPEDAL_UI__ledColor);
|
||||
|
||||
pipedalUI__frequencyPlot = lilv_new_uri(pWorld, PIPEDAL_UI__frequencyPlot);
|
||||
pipedalUI__xLeft = lilv_new_uri(pWorld, PIPEDAL_UI__xLeft);
|
||||
@@ -159,7 +162,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
|
||||
pipedalUI__yBottom = lilv_new_uri(pWorld, PIPEDAL_UI__yBottom);
|
||||
pipedalUI__xLog = lilv_new_uri(pWorld, PIPEDAL_UI__xLog);
|
||||
pipedalUI__width = lilv_new_uri(pWorld, PIPEDAL_UI__width);
|
||||
pipedalUI__graphicEq = lilv_new_uri(pWorld,PIPEDAL_UI__graphicEq);
|
||||
pipedalUI__graphicEq = lilv_new_uri(pWorld, PIPEDAL_UI__graphicEq);
|
||||
|
||||
ui__portNotification = lilv_new_uri(pWorld, LV2_UI__portNotification);
|
||||
ui__plugin = lilv_new_uri(pWorld, LV2_UI__plugin);
|
||||
@@ -171,6 +174,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
|
||||
lv2__port = lilv_new_uri(pWorld, LV2_CORE__port);
|
||||
|
||||
#define MOD_PREFIX "http://moddevices.com/ns/mod#"
|
||||
|
||||
mod__label = lilv_new_uri(pWorld, MOD_PREFIX "label");
|
||||
mod__brand = lilv_new_uri(pWorld, MOD_PREFIX "brand");
|
||||
mod__preferMomentaryOffByDefault = lilv_new_uri(pWorld, MOD_PREFIX "preferMomentaryOffByDefault");
|
||||
@@ -194,16 +198,39 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
|
||||
|
||||
patch__writable = lilv_new_uri(pWorld, LV2_PATCH__writable);
|
||||
patch__readable = lilv_new_uri(pWorld, LV2_PATCH__readable);
|
||||
pipedal_patch__readable = lilv_new_uri(pWorld, PIPEDAL_PATCH__readable);
|
||||
pipedal_patch__readable = lilv_new_uri(pWorld, PIPEDAL_PATCH__readable);
|
||||
|
||||
dc__format = lilv_new_uri(pWorld, "http://purl.org/dc/terms/format");
|
||||
|
||||
mod__fileTypes = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#fileTypes");
|
||||
mod__supportedExtensions = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#supportedExtensions");
|
||||
}
|
||||
|
||||
void PluginHost::LilvUris::Free()
|
||||
{
|
||||
}
|
||||
static int32_t nodesAsInt(const LilvNodes *nodes)
|
||||
{
|
||||
if (!nodes)
|
||||
return 0;
|
||||
|
||||
LILV_FOREACH(nodes, iNode, nodes)
|
||||
{
|
||||
const LilvNode *node = lilv_nodes_get(nodes, iNode);
|
||||
if (!node)
|
||||
return 0;
|
||||
if (lilv_node_is_int(node))
|
||||
{
|
||||
return lilv_node_as_int(node);
|
||||
}
|
||||
if (lilv_node_is_float(node))
|
||||
{
|
||||
return static_cast<int32_t>(lilv_node_as_float(node));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static std::string nodeAsString(const LilvNode *node)
|
||||
{
|
||||
@@ -270,7 +297,6 @@ PluginHost::PluginHost()
|
||||
fileMetadataFeature.Prepare(mapFeature);
|
||||
lv2Features.push_back(fileMetadataFeature.GetFeature());
|
||||
|
||||
|
||||
lv2Features.push_back(nullptr);
|
||||
|
||||
this->urids = new Urids(mapFeature);
|
||||
@@ -294,8 +320,11 @@ PluginHost::~PluginHost()
|
||||
{
|
||||
delete lilvUris;
|
||||
lilvUris = nullptr;
|
||||
|
||||
delete urids;
|
||||
urids = nullptr;
|
||||
delete mod_gui_uris;
|
||||
mod_gui_uris = nullptr;
|
||||
free_world();
|
||||
}
|
||||
|
||||
@@ -397,7 +426,7 @@ void PluginHost::LoadPluginClassesFromLilv()
|
||||
}
|
||||
}
|
||||
|
||||
void PluginHost::Load(const char *lv2Path)
|
||||
void PluginHost::LoadLilv(const char *lv2Path)
|
||||
{
|
||||
|
||||
this->plugins_.clear();
|
||||
@@ -418,6 +447,9 @@ void PluginHost::Load(const char *lv2Path)
|
||||
|
||||
lilv_world_load_all(pWorld);
|
||||
}
|
||||
// Not a safe pointer,because auto-deleting after freeWorld() would be death.
|
||||
this->mod_gui_uris = new ModGuiUris(pWorld, mapFeature);
|
||||
|
||||
// LilvNode*lv2_path = lilv_new_file_uri(pWorld,NULL,lv2Path);
|
||||
// lilv_world_set_option(world,LILV_OPTION_LV2_PATH,lv)
|
||||
|
||||
@@ -439,7 +471,8 @@ void PluginHost::Load(const char *lv2Path)
|
||||
if (pluginInfo->hasCvPorts())
|
||||
{
|
||||
Lv2Log::debug("Plugin %s (%s) skipped. (Has CV ports).", pluginInfo->name().c_str(), pluginInfo->uri().c_str());
|
||||
} else if (pluginInfo->hasUnsupportedPatchProperties())
|
||||
}
|
||||
else if (pluginInfo->hasUnsupportedPatchProperties())
|
||||
{
|
||||
Lv2Log::debug("Plugin %s (%s) skipped. (Has unsupported patch parameters).", pluginInfo->name().c_str(), pluginInfo->uri().c_str());
|
||||
}
|
||||
@@ -497,20 +530,20 @@ void PluginHost::Load(const char *lv2Path)
|
||||
if (plugin->is_valid())
|
||||
{
|
||||
#if 1
|
||||
// no plugins with more than 2 inputs or outputs.
|
||||
// no zero-input or zero-output plugins (temporarily disables midi plugins)
|
||||
// no zero output devices (permanent, I think)
|
||||
if (info.audio_inputs() > 2 || info.audio_outputs() > 2) {
|
||||
// no plugins with more than 2 inputs or outputs.
|
||||
// no zero-input or zero-output plugins (temporarily disables midi plugins)
|
||||
// no zero output devices (permanent, I think)
|
||||
if (info.audio_inputs() > 2 || info.audio_outputs() > 2)
|
||||
{
|
||||
Lv2Log::debug(
|
||||
"Plugin %s (%s) skipped. %d inputs, %d outputs.", plugin->name().c_str(), plugin->uri().c_str(),
|
||||
(int)info.audio_inputs(),(int)info.audio_outputs());
|
||||
|
||||
(int)info.audio_inputs(), (int)info.audio_outputs());
|
||||
}
|
||||
else if (info.audio_inputs() == 0 && info.audio_outputs() == 0 )
|
||||
else if (info.audio_inputs() == 0 && info.audio_outputs() == 0)
|
||||
{
|
||||
Lv2Log::debug("Plugin %s (%s) skipped. No audio i/o.", plugin->name().c_str(), plugin->uri().c_str());
|
||||
|
||||
} else if (info.audio_inputs() == 0)
|
||||
}
|
||||
else if (info.audio_inputs() == 0)
|
||||
{
|
||||
// temporarily disable this feature.
|
||||
Lv2Log::debug("Plugin %s (%s) skipped. No inputs.", plugin->name().c_str(), plugin->uri().c_str());
|
||||
@@ -536,7 +569,8 @@ void PluginHost::Load(const char *lv2Path)
|
||||
#endif
|
||||
else
|
||||
{
|
||||
if (info.audio_inputs() == 0) {
|
||||
if (info.audio_inputs() == 0)
|
||||
{
|
||||
Lv2Log::debug("************* ZERO INPUTS: %s (%s) skipped. No audio outputs.", plugin->name().c_str(), plugin->uri().c_str());
|
||||
}
|
||||
ui_plugins_.push_back(std::move(info));
|
||||
@@ -619,7 +653,7 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
||||
// example:
|
||||
|
||||
// <http://github.com/mikeoliphant/neural-amp-modeler-lv2#model>
|
||||
// a lv2:Parameter;
|
||||
// a lv2:Parameter;
|
||||
// rdfs:label "Model";
|
||||
// rdfs:range atom:Path.
|
||||
// ...
|
||||
@@ -644,7 +678,6 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
||||
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->rdfs__range, lv2Host->lilvUris->atom__Path))
|
||||
{
|
||||
|
||||
|
||||
AutoLilvNode label = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->rdfs__label, nullptr);
|
||||
std::string strLabel = label.AsString();
|
||||
|
||||
@@ -662,19 +695,16 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
||||
std::make_shared<UiFileProperty>(
|
||||
strLabel, propertyUri.AsUri(), lv2DirectoryName);
|
||||
|
||||
|
||||
AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->lv2core__index, nullptr);
|
||||
int32_t index = indexNode.AsInt(-1);
|
||||
fileProperty->index(index);
|
||||
|
||||
|
||||
// if there's a pipedalui_fileTypes node, use that instead.
|
||||
|
||||
|
||||
AutoLilvNode mod__fileTypes = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr);
|
||||
|
||||
// default: everything.
|
||||
std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES;
|
||||
std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES;
|
||||
if (mod__fileTypes)
|
||||
{
|
||||
// "nam,nammodel"
|
||||
@@ -682,8 +712,6 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
||||
}
|
||||
ModFileTypes modFileTypes(fileTypes);
|
||||
|
||||
|
||||
|
||||
// Legacy case: audio_uploads/<plugin_directory> exists.
|
||||
|
||||
std::filesystem::path bundleDirectoryName = std::filesystem::path(bundle_path()).parent_path().filename();
|
||||
@@ -693,13 +721,14 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
||||
|
||||
fileProperty->directory(bundleDirectoryName);
|
||||
|
||||
if (std::filesystem::exists(legacyUploadPath)
|
||||
&& !std::filesystem::exists(legacyUploadPath / ".migrated"))
|
||||
if (std::filesystem::exists(legacyUploadPath) && !std::filesystem::exists(legacyUploadPath / ".migrated"))
|
||||
{
|
||||
fileProperty->useLegacyModDirectory(true);
|
||||
fileProperty->directory(bundleDirectoryName);
|
||||
modFileTypes.rootDirectories().push_back(bundleDirectoryName.filename()); // push the private directory!
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
if (modFileTypes.rootDirectories().size() == 1)
|
||||
{
|
||||
std::string modName = modFileTypes.rootDirectories()[0];
|
||||
@@ -708,9 +737,10 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileProperties.push_back(fileProperty);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
unsupportedPatchProperty = true;
|
||||
}
|
||||
}
|
||||
@@ -718,24 +748,25 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
||||
}
|
||||
FindWritablePathPropertiesResult result;
|
||||
|
||||
|
||||
|
||||
if (fileProperties.size() != 0)
|
||||
{
|
||||
std::sort(fileProperties.begin(),fileProperties.end(),[](const UiFileProperty::ptr& left,const UiFileProperty::ptr&right) {
|
||||
// properies with indexes first.
|
||||
int32_t indexL = left->index();
|
||||
if (indexL < 0) indexL = std::numeric_limits<int32_t>::max();
|
||||
int32_t indexR = right->index();
|
||||
if (indexR < 0) indexR = std::numeric_limits<int32_t>::max();
|
||||
if (indexL < indexR) return true;
|
||||
if (indexL > indexR) return false;
|
||||
std::sort(fileProperties.begin(), fileProperties.end(), [](const UiFileProperty::ptr &left, const UiFileProperty::ptr &right)
|
||||
{
|
||||
// properies with indexes first.
|
||||
int32_t indexL = left->index();
|
||||
if (indexL < 0)
|
||||
indexL = std::numeric_limits<int32_t>::max();
|
||||
int32_t indexR = right->index();
|
||||
if (indexR < 0)
|
||||
indexR = std::numeric_limits<int32_t>::max();
|
||||
if (indexL < indexR)
|
||||
return true;
|
||||
if (indexL > indexR)
|
||||
return false;
|
||||
|
||||
// there is no natural order. TTL indexing means that the order we see them in is random.
|
||||
// We can't order them sensibly. Let's at least order them consistently.
|
||||
return left->label() < right->label();
|
||||
|
||||
});
|
||||
// there is no natural order. TTL indexing means that the order we see them in is random.
|
||||
// We can't order them sensibly. Let's at least order them consistently.
|
||||
return left->label() < right->label(); });
|
||||
result.pipedalUi = std::make_shared<PiPedalUI>(std::move(fileProperties));
|
||||
}
|
||||
result.hasUnsupportedPatchProperties = unsupportedPatchProperty;
|
||||
@@ -764,6 +795,12 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
||||
AutoLilvNode name = (lilv_plugin_get_name(pPlugin));
|
||||
this->name_ = nodeAsString(name);
|
||||
|
||||
AutoLilvNodes minorVersion = lilv_plugin_get_value(pPlugin, lv2Host->lilvUris->lv2core__minorVersion);
|
||||
this->minorVersion_ = nodesAsInt(minorVersion);
|
||||
|
||||
AutoLilvNodes microVersion = lilv_plugin_get_value(pPlugin, lv2Host->lilvUris->lv2core__microVersion);
|
||||
this->microVersion_ = nodesAsInt(microVersion);
|
||||
|
||||
AutoLilvNode brand = lilv_world_get(pWorld, plugUri, lv2Host->lilvUris->mod__brand, nullptr);
|
||||
this->brand_ = nodeAsString(brand);
|
||||
|
||||
@@ -838,6 +875,59 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
||||
}
|
||||
|
||||
std::sort(ports_.begin(), ports_.end(), ports_sort_compare);
|
||||
// Fetch patch properties.
|
||||
{
|
||||
AutoLilvNodes patchWritables = lilv_world_find_nodes(pWorld, plugUri, lv2Host->lilvUris->patch__writable, nullptr);
|
||||
|
||||
bool unsupportedPatchProperty = false;
|
||||
LILV_FOREACH(nodes, iNode, patchWritables)
|
||||
{
|
||||
AutoLilvNode propertyUri = lilv_nodes_get(patchWritables, iNode);
|
||||
if (propertyUri)
|
||||
{
|
||||
// a lv2:Parameter?
|
||||
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter))
|
||||
{
|
||||
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
|
||||
propertyInfo.writable(true);
|
||||
patchProperties_.push_back(std::move(propertyInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
AutoLilvNodes patchReadables = lilv_world_find_nodes(pWorld, plugUri, lv2Host->lilvUris->patch__readable, nullptr);
|
||||
|
||||
bool unsupportedPatchProperty = false;
|
||||
LILV_FOREACH(nodes, iNode, patchReadables)
|
||||
{
|
||||
AutoLilvNode propertyUri = lilv_nodes_get(patchReadables, iNode);
|
||||
if (propertyUri)
|
||||
{
|
||||
std::string uri = propertyUri.AsUri();
|
||||
bool found = false;
|
||||
for (auto &property : patchProperties_)
|
||||
{
|
||||
if (property.uri() == uri)
|
||||
{
|
||||
property.readable(true);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter))
|
||||
{
|
||||
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
|
||||
propertyInfo.readable(true);
|
||||
patchProperties_.push_back(std::move(propertyInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fetch pipedal plugin UI
|
||||
|
||||
@@ -886,7 +976,7 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
||||
++nOutputs;
|
||||
}
|
||||
}
|
||||
|
||||
this->modGui_ = ModGui::Create(lv2Host, pPlugin);
|
||||
this->is_valid_ = isValid;
|
||||
}
|
||||
|
||||
@@ -1012,22 +1102,21 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP
|
||||
this->integer_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->integer_property_uri);
|
||||
this->mod_momentaryOffByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOffByDefault);
|
||||
this->mod_momentaryOnByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOnByDefault);
|
||||
this->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq);
|
||||
|
||||
this->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq);
|
||||
|
||||
this->enumeration_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->enumeration_property_uri);
|
||||
|
||||
|
||||
this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__toggled);
|
||||
this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__not_on_gui_property_uri);
|
||||
this->connection_optional_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__connectionOptional);
|
||||
this->trigger_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__trigger);
|
||||
|
||||
AutoLilvNode port_ledColor = lilv_port_get(plugin,pPort,host->lilvUris->pipedalUI__ledColor);
|
||||
AutoLilvNode port_ledColor = lilv_port_get(plugin, pPort, host->lilvUris->pipedalUI__ledColor);
|
||||
if (port_ledColor)
|
||||
{
|
||||
auto value = lilv_node_as_string(port_ledColor);
|
||||
if (value) {
|
||||
if (value)
|
||||
{
|
||||
this->pipedal_ledColor_ = value;
|
||||
}
|
||||
}
|
||||
@@ -1164,6 +1253,8 @@ bool PluginHost::is_a(const std::string &class_, const std::string &target_class
|
||||
Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin)
|
||||
: uri_(plugin->uri()),
|
||||
name_(plugin->name()),
|
||||
minorVersion_(plugin->minorVersion()),
|
||||
microVersion_(plugin->microVersion()),
|
||||
brand_(plugin->brand()),
|
||||
label_(plugin->label()),
|
||||
author_name_(plugin->author_name()),
|
||||
@@ -1173,7 +1264,9 @@ Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin)
|
||||
audio_outputs_(0),
|
||||
description_(plugin->comment()),
|
||||
has_midi_input_(false),
|
||||
has_midi_output_(false)
|
||||
has_midi_output_(false),
|
||||
modGui_(plugin->modGui()),
|
||||
patchProperties_(plugin->patchProperties())
|
||||
{
|
||||
PLUGIN_MAP_CHECK();
|
||||
auto pluginClass = pHost->GetPluginClass(plugin->plugin_class());
|
||||
@@ -1253,7 +1346,7 @@ std::shared_ptr<Lv2PluginInfo> PluginHost::GetPluginInfo(const std::string &uri)
|
||||
return ff->second;
|
||||
}
|
||||
|
||||
Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,Lv2Pedalboard *existingPedalboard,Lv2PedalboardErrorList &errorList)
|
||||
Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard, Lv2Pedalboard *existingPedalboard, Lv2PedalboardErrorList &errorList)
|
||||
{
|
||||
ExistingEffectMap existingEffects;
|
||||
|
||||
@@ -1261,7 +1354,8 @@ Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,L
|
||||
{
|
||||
for (auto &effect : existingPedalboard->GetSharedEffectList())
|
||||
{
|
||||
if (effect->IsLv2Effect()) {
|
||||
if (effect->IsLv2Effect())
|
||||
{
|
||||
existingEffects[effect->GetInstanceId()] = effect;
|
||||
}
|
||||
}
|
||||
@@ -1269,7 +1363,7 @@ Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,L
|
||||
Lv2Pedalboard *pPedalboard = new Lv2Pedalboard();
|
||||
try
|
||||
{
|
||||
pPedalboard->Prepare(this, pedalboard, errorList,&existingEffects);
|
||||
pPedalboard->Prepare(this, pedalboard, errorList, &existingEffects);
|
||||
return pPedalboard;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
@@ -1589,17 +1683,16 @@ static void createTargetLinks(const std::filesystem::path &sourceDirectory, cons
|
||||
}
|
||||
}
|
||||
|
||||
std::string PluginHost::MapResourcePath(const std::string&pluginUri, const std::string&filePath)
|
||||
std::string PluginHost::MapResourcePath(const std::string &pluginUri, const std::string &filePath)
|
||||
{
|
||||
auto plugin = GetPluginInfo(pluginUri);
|
||||
if (plugin) {
|
||||
|
||||
if (plugin)
|
||||
{
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
|
||||
return filePath;
|
||||
|
||||
}
|
||||
|
||||
void PluginHost::CheckForResourceInitialization(const std::string &pluginUri, const std::filesystem::path &pluginUploadDirectory)
|
||||
@@ -1679,6 +1772,43 @@ std::string PluginHost::AbstractPath(const std::string &path)
|
||||
}
|
||||
return path;
|
||||
}
|
||||
Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNode *propertyUri)
|
||||
{
|
||||
LilvWorld *pWorld = pluginHost->getWorld();
|
||||
this->uri_ = nodeAsString(propertyUri);
|
||||
|
||||
AutoLilvNode range = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__range, nullptr);
|
||||
if (range)
|
||||
{
|
||||
this->type_ = range.AsUri();
|
||||
}
|
||||
AutoLilvNode comment = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__Comment, nullptr);
|
||||
if (comment)
|
||||
{
|
||||
this->comment_ = comment.AsString();
|
||||
}
|
||||
AutoLilvNode shortName = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->lv2core__shortName, nullptr);
|
||||
if (shortName)
|
||||
{
|
||||
this->shortName_ = shortName.AsString();
|
||||
} else {
|
||||
this->shortName_ = this->label_;
|
||||
}
|
||||
|
||||
AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->lv2core__index, nullptr);
|
||||
this->index_ = indexNode.AsInt(-1);
|
||||
|
||||
AutoLilvNode modFileTypesNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__fileTypes, nullptr);
|
||||
if (modFileTypesNode) {
|
||||
std::string strFileTypes = modFileTypesNode.AsString();
|
||||
this->fileTypes_ = split(strFileTypes,',');
|
||||
}
|
||||
AutoLilvNode modSupportedExtensionsNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__supportedExtensions, nullptr);
|
||||
if (modSupportedExtensionsNode) {
|
||||
std::string strSupportedExtensions = modSupportedExtensionsNode.AsString();
|
||||
this->supportedExtensions_ = split(strSupportedExtensions,',');
|
||||
}
|
||||
}
|
||||
|
||||
// void PiPedalHostLogError(const std::string &error)
|
||||
// {
|
||||
@@ -1757,6 +1887,8 @@ json_map::storage_type<Lv2PortGroup> Lv2PortGroup::jmap{{
|
||||
json_map::storage_type<Lv2PluginInfo> Lv2PluginInfo::jmap{{
|
||||
json_map::reference("bundle_path", &Lv2PluginInfo::bundle_path_),
|
||||
json_map::reference("uri", &Lv2PluginInfo::uri_),
|
||||
json_map::reference("minorVersion", &Lv2PluginInfo::minorVersion_),
|
||||
json_map::reference("microVersion", &Lv2PluginInfo::microVersion_),
|
||||
json_map::reference("name", &Lv2PluginInfo::name_),
|
||||
json_map::reference("brand", &Lv2PluginInfo::brand_),
|
||||
json_map::reference("label", &Lv2PluginInfo::label_),
|
||||
@@ -1774,6 +1906,8 @@ json_map::storage_type<Lv2PluginInfo> Lv2PluginInfo::jmap{{
|
||||
json_map::reference("port_groups", &Lv2PluginInfo::port_groups_),
|
||||
json_map::reference("is_valid", &Lv2PluginInfo::is_valid_),
|
||||
json_map::reference("has_factory_presets", &Lv2PluginInfo::has_factory_presets_),
|
||||
json_map::reference("modGui", &Lv2PluginInfo::modGui_),
|
||||
json_map::reference("patchProperties", &Lv2PluginInfo::patchProperties_),
|
||||
}};
|
||||
|
||||
json_map::storage_type<Lv2PluginClass> Lv2PluginClass::jmap{{
|
||||
@@ -1831,6 +1965,8 @@ json_map::storage_type<Lv2PluginUiInfo>
|
||||
{
|
||||
json_map::reference("uri", &Lv2PluginUiInfo::uri_),
|
||||
json_map::reference("name", &Lv2PluginUiInfo::name_),
|
||||
json_map::reference("minorVersion", &Lv2PluginUiInfo::minorVersion_),
|
||||
json_map::reference("microVersion", &Lv2PluginUiInfo::microVersion_),
|
||||
json_map::reference("brand", &Lv2PluginUiInfo::brand_),
|
||||
json_map::reference("label", &Lv2PluginUiInfo::label_),
|
||||
json_map::enum_reference("plugin_type", &Lv2PluginUiInfo::plugin_type_, get_plugin_type_enum_converter()),
|
||||
@@ -1848,4 +1984,16 @@ json_map::storage_type<Lv2PluginUiInfo>
|
||||
json_map::reference("fileProperties", &Lv2PluginUiInfo::fileProperties_),
|
||||
json_map::reference("frequencyPlots", &Lv2PluginUiInfo::frequencyPlots_),
|
||||
json_map::reference("uiPortNotifications", &Lv2PluginUiInfo::uiPortNotifications_),
|
||||
json_map::reference("modGui", &Lv2PluginUiInfo::modGui_),
|
||||
json_map::reference("patchProperties", &Lv2PluginUiInfo::patchProperties_),
|
||||
}};
|
||||
JSON_MAP_BEGIN(Lv2PatchPropertyInfo)
|
||||
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, uri)
|
||||
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, label)
|
||||
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, type)
|
||||
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, comment)
|
||||
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, shortName)
|
||||
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, fileTypes)
|
||||
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, supportedExtensions)
|
||||
|
||||
JSON_MAP_END()
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <string>
|
||||
#include "IHost.hpp"
|
||||
#include <set>
|
||||
#include "ModGui.hpp"
|
||||
|
||||
//#include "lv2.h"
|
||||
#include "Units.hpp"
|
||||
@@ -42,6 +43,7 @@
|
||||
#include "AutoLilvNode.hpp"
|
||||
#include "PiPedalUI.hpp"
|
||||
#include "MapPathFeature.hpp"
|
||||
#include "ModGui.hpp"
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
@@ -325,6 +327,38 @@ namespace pipedal
|
||||
static json_map::storage_type<Lv2PortInfo> jmap;
|
||||
};
|
||||
|
||||
class Lv2PatchPropertyInfo {
|
||||
|
||||
private:
|
||||
std::string uri_;
|
||||
bool writable_ = false;
|
||||
bool readable_ = false;
|
||||
std::string label_;
|
||||
uint32_t index_ = -1;
|
||||
std::string type_;
|
||||
std::string comment_;
|
||||
std::string shortName_;
|
||||
std::vector<std::string> fileTypes_;
|
||||
std::vector<std::string> supportedExtensions_;
|
||||
public:
|
||||
Lv2PatchPropertyInfo() {}
|
||||
Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNode *propertyUri);
|
||||
|
||||
LV2_PROPERTY_GETSET(uri);
|
||||
LV2_PROPERTY_GETSET(writable);
|
||||
LV2_PROPERTY_GETSET(readable);
|
||||
LV2_PROPERTY_GETSET(label);
|
||||
LV2_PROPERTY_GETSET_SCALAR(index);
|
||||
LV2_PROPERTY_GETSET(type);
|
||||
LV2_PROPERTY_GETSET(comment);
|
||||
LV2_PROPERTY_GETSET(shortName);
|
||||
LV2_PROPERTY_GETSET(fileTypes);
|
||||
LV2_PROPERTY_GETSET(supportedExtensions);
|
||||
|
||||
DECLARE_JSON_MAP(Lv2PatchPropertyInfo);
|
||||
|
||||
};
|
||||
|
||||
class Lv2PortGroup
|
||||
{
|
||||
private:
|
||||
@@ -364,6 +398,8 @@ namespace pipedal
|
||||
bool HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plugin);
|
||||
std::string bundle_path_;
|
||||
std::string uri_;
|
||||
uint32_t minorVersion_ = 0;
|
||||
uint32_t microVersion_ = 0;
|
||||
std::string name_;
|
||||
std::string plugin_class_;
|
||||
std::string brand_;
|
||||
@@ -380,8 +416,12 @@ namespace pipedal
|
||||
std::string comment_;
|
||||
std::vector<std::shared_ptr<Lv2PortInfo>> ports_;
|
||||
std::vector<std::shared_ptr<Lv2PortGroup>> port_groups_;
|
||||
std::vector<Lv2PatchPropertyInfo> patchProperties_;
|
||||
|
||||
bool is_valid_ = false;
|
||||
PiPedalUI::ptr piPedalUI_;
|
||||
ModGui::ptr modGui_;
|
||||
|
||||
bool hasUnsupportedPatchProperties_ = false;
|
||||
|
||||
bool IsSupportedFeature(const std::string &feature) const;
|
||||
@@ -390,6 +430,8 @@ namespace pipedal
|
||||
LV2_PROPERTY_GETSET(bundle_path)
|
||||
LV2_PROPERTY_GETSET(uri)
|
||||
LV2_PROPERTY_GETSET(name)
|
||||
LV2_PROPERTY_GETSET(minorVersion)
|
||||
LV2_PROPERTY_GETSET(microVersion)
|
||||
LV2_PROPERTY_GETSET(brand)
|
||||
LV2_PROPERTY_GETSET(label)
|
||||
LV2_PROPERTY_GETSET(plugin_class)
|
||||
@@ -406,6 +448,8 @@ namespace pipedal
|
||||
LV2_PROPERTY_GETSET(has_factory_presets)
|
||||
LV2_PROPERTY_GETSET(piPedalUI)
|
||||
LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties)
|
||||
LV2_PROPERTY_GETSET(modGui)
|
||||
LV2_PROPERTY_GETSET(patchProperties)
|
||||
|
||||
const Lv2PortInfo &getPort(const std::string &symbol)
|
||||
{
|
||||
@@ -618,6 +662,8 @@ namespace pipedal
|
||||
private:
|
||||
std::string uri_;
|
||||
std::string name_;
|
||||
uint32_t minorVersion_ = 0;
|
||||
uint32_t microVersion_ = 0;;
|
||||
std::string brand_;
|
||||
std::string label_;
|
||||
std::string author_name_;
|
||||
@@ -636,10 +682,15 @@ namespace pipedal
|
||||
std::vector<UiFileProperty::ptr> fileProperties_;
|
||||
std::vector<UiFrequencyPlot::ptr> frequencyPlots_;
|
||||
std::vector<UiPortNotification::ptr> uiPortNotifications_;
|
||||
ModGui::ptr modGui_;
|
||||
std::vector<Lv2PatchPropertyInfo> patchProperties_;
|
||||
|
||||
|
||||
public:
|
||||
LV2_PROPERTY_GETSET(uri)
|
||||
LV2_PROPERTY_GETSET(name)
|
||||
LV2_PROPERTY_GETSET(minorVersion)
|
||||
LV2_PROPERTY_GETSET(microVersion)
|
||||
LV2_PROPERTY_GETSET(brand)
|
||||
LV2_PROPERTY_GETSET(label)
|
||||
LV2_PROPERTY_GETSET(author_name)
|
||||
@@ -657,6 +708,8 @@ namespace pipedal
|
||||
LV2_PROPERTY_GETSET(fileProperties)
|
||||
LV2_PROPERTY_GETSET(frequencyPlots)
|
||||
LV2_PROPERTY_GETSET(uiPortNotifications)
|
||||
LV2_PROPERTY_GETSET(modGui)
|
||||
LV2_PROPERTY_GETSET(patchProperties)
|
||||
|
||||
static json_map::storage_type<Lv2PluginUiInfo> jmap;
|
||||
};
|
||||
@@ -679,6 +732,8 @@ namespace pipedal
|
||||
#endif
|
||||
friend class pipedal::AutoLilvNode;
|
||||
friend class pipedal::PiPedalUI;
|
||||
friend class PluginHostTest;
|
||||
|
||||
static const char *RDFS__comment;
|
||||
static const char *RDFS__range;
|
||||
|
||||
@@ -707,6 +762,7 @@ namespace pipedal
|
||||
AutoLilvNode invada_units__unit; // typo in invada plugins.
|
||||
AutoLilvNode invada_portprops__logarithmic; // typo in invada plugins.
|
||||
|
||||
|
||||
AutoLilvNode atom__bufferType;
|
||||
AutoLilvNode atom__Path;
|
||||
AutoLilvNode presets__preset;
|
||||
@@ -714,8 +770,11 @@ namespace pipedal
|
||||
AutoLilvNode rdfs__label;
|
||||
AutoLilvNode lv2core__symbol;
|
||||
AutoLilvNode lv2core__name;
|
||||
AutoLilvNode lv2core__shortName;
|
||||
AutoLilvNode lv2core__index;
|
||||
AutoLilvNode lv2core__Parameter;
|
||||
AutoLilvNode lv2core__minorVersion;
|
||||
AutoLilvNode lv2core__microVersion;
|
||||
AutoLilvNode pipedalUI__ui;
|
||||
AutoLilvNode pipedalUI__fileProperties;
|
||||
AutoLilvNode pipedalUI__directory;
|
||||
@@ -766,6 +825,7 @@ namespace pipedal
|
||||
AutoLilvNode patch__readable;
|
||||
AutoLilvNode pipedal_patch__readable;
|
||||
|
||||
|
||||
AutoLilvNode mod__brand;
|
||||
AutoLilvNode mod__label;
|
||||
AutoLilvNode mod__preferMomentaryOffByDefault;
|
||||
@@ -773,6 +833,7 @@ namespace pipedal
|
||||
AutoLilvNode dc__format;
|
||||
|
||||
AutoLilvNode mod__fileTypes;
|
||||
AutoLilvNode mod__supportedExtensions;
|
||||
AutoLilvNode pipedalui__fileTypes;
|
||||
|
||||
|
||||
@@ -817,6 +878,7 @@ namespace pipedal
|
||||
friend class Lv2PluginInfo;
|
||||
friend class Lv2PortInfo;
|
||||
friend class Lv2PortGroup;
|
||||
friend class ModGui;
|
||||
|
||||
std::shared_ptr<Lv2PluginClass> GetPluginClass(const LilvPluginClass *pClass);
|
||||
std::shared_ptr<Lv2PluginClass> MakePluginClass(const LilvPluginClass *pClass);
|
||||
@@ -924,7 +986,9 @@ namespace pipedal
|
||||
|
||||
class Urids;
|
||||
|
||||
Urids *urids;
|
||||
Urids *urids = nullptr;
|
||||
ModGuiUris* mod_gui_uris = nullptr;
|
||||
|
||||
|
||||
void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings);
|
||||
|
||||
@@ -933,7 +997,7 @@ namespace pipedal
|
||||
|
||||
std::shared_ptr<Lv2PluginClass> GetLv2PluginClass() const;
|
||||
|
||||
std::vector<std::shared_ptr<Lv2PluginInfo>> GetPlugins() const { return plugins_; }
|
||||
const std::vector<std::shared_ptr<Lv2PluginInfo>>& GetPlugins() const { return plugins_; }
|
||||
const std::vector<Lv2PluginUiInfo> &GetUiPlugins() const { return ui_plugins_; }
|
||||
|
||||
virtual std::shared_ptr<Lv2PluginInfo> GetPluginInfo(const std::string &uri) const;
|
||||
@@ -941,7 +1005,7 @@ namespace pipedal
|
||||
static constexpr const char *DEFAULT_LV2_PATH = "/usr/lib/lv2";
|
||||
|
||||
void LoadPluginClassesFromJson(std::filesystem::path jsonFile);
|
||||
void Load(const char *lv2Path = PluginHost::DEFAULT_LV2_PATH);
|
||||
void LoadLilv(const char *lv2Path = PluginHost::DEFAULT_LV2_PATH);
|
||||
|
||||
virtual LV2_URID GetLv2Urid(const char *uri)
|
||||
{
|
||||
|
||||
@@ -68,3 +68,4 @@ json_enum_converter<Units> *get_units_enum_converter();
|
||||
|
||||
} // namespace.
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "Ipv6Helpers.hpp"
|
||||
#include "util.hpp"
|
||||
#include "ofstream_synced.hpp"
|
||||
#include "ModTemplateGenerator.hpp"
|
||||
|
||||
#include <mutex>
|
||||
#include "WebServer.hpp"
|
||||
@@ -741,6 +742,14 @@ namespace pipedal
|
||||
request.set_body_file(bodyFile->Path(),bodyFile->DeleteFile());
|
||||
bodyFile->Detach();
|
||||
}
|
||||
|
||||
|
||||
virtual void clearBody() override {
|
||||
// clear the body, but leave the file size intact (e.g for a HEAD request).
|
||||
request.set_body("");
|
||||
|
||||
// STUB: Don't know how to clear the body file!!
|
||||
}
|
||||
virtual void setBodyFile(std::filesystem::path&path, bool deleteWhenDone) override {
|
||||
// cast away const to do what ther request would do if it had that method.
|
||||
request.set_body_file(path,deleteWhenDone);
|
||||
|
||||
@@ -43,6 +43,7 @@ public:
|
||||
virtual void setBody(const std::string&body) = 0;
|
||||
virtual void setBodyFile(std::shared_ptr<TemporaryFile>&temporaryFile) = 0;
|
||||
virtual void setBodyFile(std::filesystem::path&path, bool deleteWhenDone) = 0;
|
||||
virtual void clearBody() = 0; // but leave the file size intact (e.g for a HEAD request).
|
||||
|
||||
virtual void keepAlive(bool value) = 0;
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "AudioFiles.hpp"
|
||||
#include "util.hpp"
|
||||
#include "HtmlHelper.hpp"
|
||||
#include "WebServerMod.hpp"
|
||||
|
||||
#define OLD_PRESET_EXTENSION ".piPreset"
|
||||
#define PRESET_EXTENSION ".piPreset"
|
||||
@@ -249,7 +250,7 @@ public:
|
||||
void GetPluginPresets(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string pluginUri = request_uri.query("id");
|
||||
auto plugin = model->GetLv2Host().GetPluginInfo(pluginUri);
|
||||
auto plugin = model->GetPluginHost().GetPluginInfo(pluginUri);
|
||||
*pName = plugin->name();
|
||||
|
||||
PluginPresets pluginPresets = model->GetPluginPresets(pluginUri);
|
||||
@@ -1102,4 +1103,7 @@ void pipedal::ConfigureWebServer(
|
||||
|
||||
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
|
||||
server.AddRequestHandler(downloadIntercept);
|
||||
|
||||
std::shared_ptr<ModWebIntercept> modWebIntercept = ModWebIntercept::Create(&model);
|
||||
server.AddRequestHandler(modWebIntercept);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "WebServerMod.hpp"
|
||||
#include <filesystem>
|
||||
#include "json_variant.hpp"
|
||||
#include "ss.hpp"
|
||||
#include "ModTemplateGenerator.hpp"
|
||||
#include "HtmlHelper.hpp"
|
||||
#include "MimeTypes.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace pipedal::impl
|
||||
{
|
||||
|
||||
class ModWebInterceptImpl : public ModWebIntercept
|
||||
{
|
||||
private:
|
||||
using super = ModWebIntercept;
|
||||
using self = ModWebInterceptImpl;
|
||||
|
||||
PiPedalModel *model;
|
||||
|
||||
public:
|
||||
ModWebInterceptImpl(PiPedalModel *model)
|
||||
: super("/resources"),
|
||||
model(model)
|
||||
{
|
||||
}
|
||||
virtual bool wants(const std::string &method, const uri &request_uri) const override;
|
||||
|
||||
virtual void head_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override;
|
||||
|
||||
virtual void get_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override;
|
||||
|
||||
private:
|
||||
std::string GenerateTemplate(
|
||||
const fs::path &templateFile,
|
||||
std::shared_ptr<Lv2PluginInfo> pluginInfo);
|
||||
};
|
||||
}
|
||||
|
||||
using namespace pipedal::impl;
|
||||
|
||||
ModWebIntercept::ptr ModWebIntercept::Create(PiPedalModel *model)
|
||||
{
|
||||
return std::make_shared<impl::ModWebInterceptImpl>(model);
|
||||
}
|
||||
|
||||
bool ModWebInterceptImpl::wants(const std::string &method, const uri &request_uri) const
|
||||
{
|
||||
if (request_uri.segment_count() < 2 || request_uri.segment(0) != "resources")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModWebInterceptImpl::head_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
get_response(request_uri, req, res, ec);
|
||||
res.clearBody();
|
||||
}
|
||||
|
||||
static void setCacheControl(HttpResponse &res, const fs::path &path)
|
||||
{
|
||||
if (fs::exists(path))
|
||||
{
|
||||
res.set("Cache-Control", "public, max-age=31536000"); // 1 month
|
||||
auto lastModified = std::filesystem::last_write_time(path);
|
||||
res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified));
|
||||
}
|
||||
else
|
||||
{
|
||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
}
|
||||
}
|
||||
void ModWebInterceptImpl::get_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string ns = request_uri.query("ns");
|
||||
std::string strVersion = request_uri.query("v");
|
||||
(void)strVersion; // this is just cache-busting, so we don't actually use it.
|
||||
|
||||
std::shared_ptr<Lv2PluginInfo> pluginInfo;
|
||||
if (!ns.empty())
|
||||
{
|
||||
pluginInfo = model->GetPluginInfo(ns);
|
||||
if (!pluginInfo)
|
||||
{
|
||||
throw std::runtime_error("Plugin not found.");
|
||||
}
|
||||
}
|
||||
if (!pluginInfo->modGui())
|
||||
{
|
||||
throw std::runtime_error("Plugin does not have a ModGui.");
|
||||
}
|
||||
if (request_uri.segment_count() < 2)
|
||||
{
|
||||
throw std::runtime_error("Invalid request URI.");
|
||||
}
|
||||
if (request_uri.segment(0) != "resources")
|
||||
{
|
||||
throw std::runtime_error("Invalid request URI: expected 'resources'.");
|
||||
}
|
||||
std::string segment = request_uri.segment(1);
|
||||
if (segment == "_")
|
||||
{
|
||||
segment = request_uri.segment(2);
|
||||
if (segment == "iconTemplate")
|
||||
{
|
||||
res.set("Content-Type", "text/html");
|
||||
res.setBody(GenerateTemplate(
|
||||
pluginInfo->modGui()->iconTemplate(),
|
||||
pluginInfo));
|
||||
setCacheControl(res, pluginInfo->modGui()->iconTemplate());
|
||||
}
|
||||
else if (segment == "stylesheet")
|
||||
{
|
||||
res.set("Content-Type", "text/css");
|
||||
res.setBody(GenerateTemplate(
|
||||
pluginInfo->modGui()->stylesheet(),
|
||||
pluginInfo));
|
||||
setCacheControl(res, pluginInfo->modGui()->stylesheet());
|
||||
|
||||
}
|
||||
|
||||
else if (segment == "screenshot")
|
||||
{
|
||||
std::filesystem::path path = pluginInfo->modGui()->screenshot();
|
||||
if (!fs::exists(path))
|
||||
{
|
||||
ec = std::make_error_code(std::errc::no_such_file_or_directory);
|
||||
return;
|
||||
}
|
||||
if (!fs::is_regular_file(path))
|
||||
{
|
||||
throw std::runtime_error("Screenshot is not a regular file: " + path.string());
|
||||
}
|
||||
|
||||
size_t size = fs::file_size(path);
|
||||
std::string mimeType = MimeTypes::instance().MimeTypeFromExtension(path.extension().string());
|
||||
if (mimeType.empty())
|
||||
{
|
||||
throw std::runtime_error("Unknown file type for screenshot: " + path.string());
|
||||
}
|
||||
res.setBodyFile(
|
||||
path,
|
||||
false); // delete when done
|
||||
res.set("Content-Type", mimeType);
|
||||
res.setContentLength(size);
|
||||
setCacheControl(res, path);
|
||||
|
||||
}
|
||||
else if (segment == "thumbnail")
|
||||
{
|
||||
std::filesystem::path path = pluginInfo->modGui()->thumbnail();
|
||||
if (!fs::exists(path))
|
||||
{
|
||||
throw std::runtime_error("Thumbnail not found: " + path.string());
|
||||
}
|
||||
if (!fs::is_regular_file(path))
|
||||
{
|
||||
throw std::runtime_error("Thumbnail is not a regular file: " + path.string());
|
||||
}
|
||||
|
||||
size_t size = fs::file_size(path);
|
||||
std::string mimeType = MimeTypes::instance().MimeTypeFromExtension(path.extension().string());
|
||||
if (mimeType.empty())
|
||||
{
|
||||
throw std::runtime_error("Unknown file type for thumbnail: " + path.string());
|
||||
}
|
||||
res.setBodyFile(
|
||||
path,
|
||||
false); // delete when done
|
||||
res.set("Content-Type", mimeType);
|
||||
setCacheControl(res, path);
|
||||
res.setContentLength(size);
|
||||
} else {
|
||||
throw std::runtime_error("Unknown resource: _/" + segment);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// a request for a plugin resource file.
|
||||
fs::path resourcefile = pluginInfo->modGui()->resourceDirectory();
|
||||
for (size_t i = 1; i < request_uri.segment_count(); ++i)
|
||||
{
|
||||
resourcefile /= request_uri.segment(i);
|
||||
}
|
||||
if (!fs::exists(resourcefile) && !fs::is_regular_file(resourcefile))
|
||||
{
|
||||
ec = std::make_error_code(std::errc::no_such_file_or_directory);
|
||||
return;
|
||||
}
|
||||
size_t size = fs::file_size(resourcefile);
|
||||
std::string mimeType = MimeTypes::instance().MimeTypeFromExtension(resourcefile.extension().string());
|
||||
if (mimeType.empty())
|
||||
{
|
||||
throw std::runtime_error("Unknown file type for resource: " + resourcefile.string());
|
||||
}
|
||||
res.setBodyFile(
|
||||
resourcefile,
|
||||
false); // delete when done
|
||||
res.set("Content-Type", mimeType);
|
||||
Lv2Log::debug(SS("ModWebIntercept: Serving resource file: " << resourcefile.string() << " (" << size << " bytes)"));
|
||||
Lv2Log::debug(SS(" url: " << request_uri.str()));
|
||||
setCacheControl(res, resourcefile);
|
||||
res.setContentLength(size);
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
res.setBody("Error: " + std::string(e.what()));
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string makeCns(const std::string &encodedUri, int64_t instanceId)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "_";
|
||||
for (char c : encodedUri)
|
||||
{
|
||||
if (
|
||||
(c >= 'a' && c <= 'z') || (c >= 'A' & c <= 'Z') || (c >= '0' && c <= '9'))
|
||||
{
|
||||
ss << c;
|
||||
}
|
||||
else
|
||||
{
|
||||
ss << '_'; // replace non-alphanumeric characters with '_'
|
||||
}
|
||||
}
|
||||
ss << "_";
|
||||
if (instanceId < 0)
|
||||
{
|
||||
ss << "_";
|
||||
instanceId = -instanceId;
|
||||
}
|
||||
ss << instanceId;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string ModWebInterceptImpl::GenerateTemplate(
|
||||
const fs::path &templateFile,
|
||||
std::shared_ptr<Lv2PluginInfo> pluginInfo)
|
||||
{
|
||||
if (!fs::exists(templateFile))
|
||||
{
|
||||
throw std::runtime_error("File not found. " + templateFile.string());
|
||||
}
|
||||
|
||||
json_variant context = MakeModGuiTemplateData(pluginInfo);
|
||||
int64_t version = pluginInfo->minorVersion() * 1000 +
|
||||
pluginInfo->microVersion();
|
||||
|
||||
std::string encodedUri = HtmlHelper::encode_url_segment(pluginInfo->uri(), true);
|
||||
std::string ns = SS("?ns=" << encodedUri << "&v=" << version);
|
||||
context["_ns"] = ns;
|
||||
|
||||
std::string cns = makeCns(encodedUri, version);
|
||||
context["_cns"] = cns;
|
||||
|
||||
return GenerateFromTemplateFile(
|
||||
templateFile,
|
||||
context);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "WebServer.hpp"
|
||||
|
||||
#include "PiPedalModel.hpp"
|
||||
#include <memory>
|
||||
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
|
||||
class ModWebIntercept : public RequestHandler
|
||||
{
|
||||
|
||||
PiPedalModel *model;
|
||||
protected:
|
||||
ModWebIntercept(const std::string &target_url)
|
||||
: RequestHandler(target_url.c_str())
|
||||
{
|
||||
}
|
||||
public:
|
||||
using self = ModWebIntercept;
|
||||
using super = RequestHandler;
|
||||
using ptr = std::shared_ptr<self>;
|
||||
|
||||
static ptr Create(PiPedalModel *model);
|
||||
|
||||
ModWebIntercept(PiPedalModel *model)
|
||||
: RequestHandler("/var"),
|
||||
model(model)
|
||||
{
|
||||
}
|
||||
virtual ~ModWebIntercept() = default;
|
||||
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
Exctly one underrun per seek in Toob Player
|
||||
|
||||
check reload after change of LV2 plugins.
|
||||
|
||||
Vu Meters, move rendering to background images, in order to reduce layout overhead.
|
||||
|
||||
Remove FullScreenIME in PluginControlView.tsx
|
||||
|
||||
Exactly one underrun per seek in Toob Player
|
||||
|
||||
Test autohotspot detection.
|
||||
|
||||
|
||||
@@ -108,7 +108,6 @@ add_custom_command(
|
||||
public/iso_codes.json
|
||||
public/logo512.png
|
||||
public/sample_lv2_plugins.json
|
||||
public/vite.svg
|
||||
src/pipedal/SplitUiControls.tsx
|
||||
src/pipedal/JackHostStatus.tsx
|
||||
src/pipedal/VirtualKeyboardHandler.tsx
|
||||
@@ -291,8 +290,6 @@ add_custom_command(
|
||||
src/pipedal/MidiChannelBinding.tsx
|
||||
src/pipedal/ModFileTypes.tsx
|
||||
src/assets/react.svg
|
||||
src/App.css
|
||||
src/App.tsx
|
||||
src/vite-env.d.ts
|
||||
src/index.css
|
||||
src/main.tsx
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<link rel="stylesheet" href="/css/roboto.css" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="stylesheet" href="/css/modGui.css" />
|
||||
<style>
|
||||
BODY {
|
||||
background: #D0D0D0;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
.mod-pedal {
|
||||
}
|
||||
|
||||
.mod-pedal .mod-light {
|
||||
}
|
||||
.mod-pedal .mod-light.off { background-image:url(../img/red-light-off.png); }
|
||||
.mod-pedal .mod-light.on { background-image:url(../img/red-light-on.png); }
|
||||
|
||||
.mod-pedal .mod-pedal-input {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left: -24px;
|
||||
width: 24px;
|
||||
|
||||
}
|
||||
|
||||
.mod-pedal .mod-pedal-output {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: -25px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.mod-pedal .mod-pedal-input .mod-audio-input,
|
||||
.mod-pedal .mod-pedal-input .mod-midi-input,
|
||||
.mod-pedal .mod-pedal-input .mod-cv-input {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.mod-audio-input .mod-pedal-input-image {
|
||||
background-image:url(../img/audio-input.png) ;
|
||||
width: 24px;
|
||||
height: 56px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.mod-audio-output .mod-pedal-output-image {
|
||||
background-image:url(../img/audio-output.png);
|
||||
width: 24px;
|
||||
height: 56px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ppmod-dial-control-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: fill;
|
||||
|
||||
}
|
||||
.ppmod-dial-control-frame
|
||||
{
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mod-knob-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mod-slider-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
src: url(/fonts/Roboto-Light.woff2) format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
src: url(/fonts/Roboto-Regular.woff2) format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
src: url(/fonts/Roboto-Medium.woff2) format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
src: url(/fonts/Roboto-Bold.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'EnglandHand';
|
||||
src: url('england-webfont.woff');
|
||||
font-weight: normal;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@font-face {
|
||||
font-family: 'epflul';
|
||||
src: url('epf_lul-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Web Fonts from fontspring.com
|
||||
*
|
||||
* All OpenType features and all extended glyphs have been removed.
|
||||
* Fully installable fonts can be purchased at http://www.fontspring.com
|
||||
*
|
||||
* The fonts included in this stylesheet are subject to the End User License you purchased
|
||||
* from Fontspring. The fonts are protected under domestic and international trademark and
|
||||
* copyright law. You are prohibited from modifying, reverse engineering, duplicating, or
|
||||
* distributing this font software.
|
||||
*
|
||||
* (c) 2010-2012 Fontspring
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* The fonts included are copyrighted by the vendor listed below.
|
||||
*
|
||||
* Vendor: Fontfabric
|
||||
* License URL: http://www.fontspring.com/fflicense/fontfabric
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nexa';
|
||||
src: url('Nexa_Free_Bold-webfont.woff') format('woff');
|
||||
font-display: block;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on February 25, 2013 */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pirulen';
|
||||
src: url('pirulen_rg-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on February 27, 2013 */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Questrial';
|
||||
src: url('questrial-regular-webfont.woff') format('woff');
|
||||
font-display: block;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 766 B |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,36 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import ReactLogo from './assets/react.svg?react'
|
||||
//import ViteLogo from '/vite.svg?react'
|
||||
import './App.css'
|
||||
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
{/* <ViteLogo/> */}
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<ReactLogo style={{background: "blue", padding: 96}} />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -152,7 +152,7 @@ const AboutDialog = class extends Component<AboutDialogProps, AboutDialogState>
|
||||
<ArrowBackIcon />
|
||||
</IconButtonEx>
|
||||
<Typography noWrap variant="h6"
|
||||
sx={{ maringLeft: 2, flex: 1 }}
|
||||
sx={{ marginLeft: 2, flex: 1 }}
|
||||
>
|
||||
About
|
||||
</Typography>
|
||||
|
||||
@@ -18,13 +18,15 @@
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
|
||||
import VirtualKeyboardHandler from './VirtualKeyboardHandler';
|
||||
import AppThemed from "./AppThemed";
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import Tone3000AuthComplete from './Tone3000AuthComplete';
|
||||
import FontTest from './FontTest';
|
||||
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
@@ -225,8 +227,13 @@ type AppThemeProps = {
|
||||
|
||||
function isTone3000Auth() {
|
||||
let url = new URL(window.location.href);
|
||||
let param = url.searchParams.get("api_key");
|
||||
return (param !== null && param !== "")
|
||||
let param = url.searchParams.get("api_key");
|
||||
return (param !== null && param !== "")
|
||||
}
|
||||
function isFontTest() {
|
||||
let url = new URL(window.location.href);
|
||||
let param = url.searchParams.get("fontTest");
|
||||
return (param !== null)
|
||||
}
|
||||
|
||||
const App = (class extends React.Component {
|
||||
@@ -248,11 +255,11 @@ const App = (class extends React.Component {
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{isTone3000Auth() ? (
|
||||
<Tone3000AuthComplete />
|
||||
) : (
|
||||
<AppThemed />
|
||||
)}
|
||||
{
|
||||
isTone3000Auth() && (<Tone3000AuthComplete />)
|
||||
|| isFontTest() && (<FontTest />)
|
||||
|| (<AppThemed />)
|
||||
}
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
|
||||
import './AppThemed.css';
|
||||
|
||||
//import {alpha} from '@mui/material/styles';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
|
||||
@@ -74,6 +75,7 @@ import HelpOutlineIcon from './svg/ic_help_outline.svg?react';
|
||||
import FxAmplifierIcon from './svg/fx_amplifier.svg?react';
|
||||
import { PerformanceView } from './PerformanceView';
|
||||
|
||||
|
||||
import DialogEx from './DialogEx';
|
||||
|
||||
import { IDialogStackable, popDialogStack, pushDialogStack } from './DialogStack';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
@@ -42,7 +42,11 @@ let pluginFactories: IControlViewFactory[] = [
|
||||
];
|
||||
|
||||
|
||||
export function GetControlView(pedalboardItem?: PedalboardItem | null): React.ReactNode {
|
||||
export function GetControlView(
|
||||
pedalboardItem: PedalboardItem | null,
|
||||
showModUi: boolean,
|
||||
onSetShowModGui: (instanceId: number, showModGui: boolean) => void
|
||||
): React.ReactNode {
|
||||
let model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
|
||||
if (!pedalboardItem) {
|
||||
@@ -50,7 +54,13 @@ export function GetControlView(pedalboardItem?: PedalboardItem | null): React.Re
|
||||
}
|
||||
if (pedalboardItem.isStart() || pedalboardItem.isEnd()) {
|
||||
return (
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId}
|
||||
item={pedalboardItem}
|
||||
showModGui={showModUi}
|
||||
onSetShowModGui={(instanceId, showModGui) => {
|
||||
onSetShowModGui?.(instanceId, showModGui);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (pedalboardItem.isSplit()) {
|
||||
@@ -73,7 +83,11 @@ export function GetControlView(pedalboardItem?: PedalboardItem | null): React.Re
|
||||
</div>
|
||||
} else {
|
||||
return (
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} showModGui={showModUi}
|
||||
onSetShowModGui={(instanceId, showModGui) => {
|
||||
onSetShowModGui?.(instanceId, showModGui);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Divider from '@mui/material/Divider';
|
||||
|
||||
function TypeSample(props: { fontFamily: string, cssRef: string, fontWeights: number[] }) {
|
||||
let { fontFamily, cssRef, fontWeights } = props;
|
||||
React.useEffect(() => {
|
||||
let link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = cssRef;
|
||||
document.head.appendChild(link);
|
||||
return () => {
|
||||
document.head.removeChild(link);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fontWeights.map((weight) => (
|
||||
<div key={`${fontFamily}-${weight}`} style={{ marginBottom: 24 }}>
|
||||
<Divider />
|
||||
<div style={{ marginLeft: 24, marginRight: 24 }}>
|
||||
<h4 style={{marginBottom: 0}}>{fontFamily} {weight}</h4>
|
||||
<div style={{ fontFamily: fontFamily, fontSize: 20, fontWeight: weight, marginLeft: 24 }}>
|
||||
<div style={{ fontFamily: fontFamily }}>The quick brown fox jumped over the lazy dog.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FontTest() {
|
||||
return (
|
||||
<div>
|
||||
<h4 style={{ marginLeft: 24, marginRight: 24 }}>Font Test</h4>
|
||||
{TypeSample({ fontFamily: "EnglandHand", cssRef: "/fonts/england-hand/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "epflul", cssRef: "/fonts/epf/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "Nexa", cssRef: "/fonts/nexa/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "Questrial", cssRef: "/fonts/questrial/stylesheet.css", fontWeights: [400] })}
|
||||
|
||||
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FontTest;
|
||||
@@ -21,12 +21,13 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import IconButton, {IconButtonProps} from '@mui/material/IconButton';
|
||||
import ToolTipEx from './ToolTipEx';
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
interface IconButtonExProps extends IconButtonProps {
|
||||
tooltip: string;
|
||||
tooltip: React.ReactElement | string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
@@ -35,9 +36,10 @@ function IconButtonEx(props: IconButtonExProps) {
|
||||
|
||||
return (
|
||||
<ToolTipEx title={
|
||||
(typeof props.tooltip === 'string') ?
|
||||
(
|
||||
<Typography variant="caption">{tooltip || extra['aria-label'] }</Typography>
|
||||
)
|
||||
): (props.tooltip as React.ReactElement)
|
||||
}
|
||||
>
|
||||
<IconButton {...extra} style={style} />
|
||||
|
||||
@@ -164,7 +164,8 @@ interface PluginGridProps extends WithStyles<typeof pluginGridStyles> {
|
||||
uri?: string;
|
||||
minimumItemWidth?: number;
|
||||
theme: Theme;
|
||||
open: boolean
|
||||
open: boolean;
|
||||
modGuiOnly?: boolean;
|
||||
};
|
||||
|
||||
type PluginGridState = {
|
||||
@@ -526,6 +527,8 @@ export const LoadPluginDialog =
|
||||
|
||||
for (let i = 0; i < plugins.length; ++i) {
|
||||
let plugin = plugins[i];
|
||||
if (this.props.modGuiOnly == true && !plugin.modGui)
|
||||
continue;
|
||||
try {
|
||||
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) {
|
||||
let score: number = 0;
|
||||
@@ -720,9 +723,15 @@ export const LoadPluginDialog =
|
||||
style={{ overflowX: "hidden", overflowY: "hidden", display: "flex", flexDirection: "column", flexWrap: "nowrap" }}
|
||||
aria-labelledby="select-plugin-dialog-title">
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", height: "100%" }}>
|
||||
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto", padding: "0px", height: 54 }}>
|
||||
<div style={{ display: "flex", flexDirection: "row", paddingTop: 3, paddingBottom: 3, flexWrap: "nowrap", width: "100%", alignItems: "center" }}>
|
||||
<IconButtonEx tooltip="Back" onClick={() => { this.cancel(); }} style={{ flex: "0 0 auto" }} >
|
||||
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto", padding: "0px", height: 54,
|
||||
marginRight: 16
|
||||
}}>
|
||||
<div style={{ display: "flex", flexDirection: "row", paddingTop: 3, paddingBottom: 3, flexWrap: "nowrap",
|
||||
height: 64,
|
||||
width: "100%", alignItems: "center"
|
||||
}}>
|
||||
<IconButtonEx tooltip="Back" onClick={() => { this.cancel(); }}
|
||||
style={{ flex: "0 0 auto", marginRight: 16 }} >
|
||||
<ArrowBackIcon />
|
||||
</IconButtonEx>
|
||||
|
||||
@@ -731,9 +740,9 @@ export const LoadPluginDialog =
|
||||
visibility: (this.state.search_collapsed ? "visible" : "collapse"),
|
||||
width: (this.state.search_collapsed ? undefined : "0px")
|
||||
}}>
|
||||
{this.state.client_width > 520 ? "Select Plugin" : ""}
|
||||
{this.state.client_width > 645 ? "Select Plugin" : ""}
|
||||
</Typography>
|
||||
<div style={{ flex: "1 1 auto" }} >
|
||||
<div style={{ flex: "2 2 auto" }} >
|
||||
<SearchControl collapsed={this.state.search_collapsed} searchText={this.state.search_string}
|
||||
inputRef={this.searchInputRef}
|
||||
showSearchIcon={showSearchIcon}
|
||||
@@ -784,13 +793,13 @@ export const LoadPluginDialog =
|
||||
)}
|
||||
</IconButtonEx>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 160px", marginRight: 24 }} >
|
||||
<div style={{ flex: "0 1 160px", position: "relative", marginRight: 24 }} >
|
||||
|
||||
<TextField select variant="standard"
|
||||
defaultValue={this.state.filterType}
|
||||
key={this.state.filterType}
|
||||
onChange={(e) => { this.onFilterChange(e); }}
|
||||
sx={{ minWidth: 160 }}
|
||||
sx={{ width: "100%",minWidth: "100%" }}
|
||||
>
|
||||
{this.createFilterOptions()}
|
||||
</TextField>
|
||||
|
||||
@@ -26,10 +26,10 @@ interface Deserializable<T> {
|
||||
}
|
||||
|
||||
const noteNames: string[] = [
|
||||
"C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B","C"
|
||||
"C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", "C"
|
||||
];
|
||||
|
||||
function semitone12TETValue(value: number) : string {
|
||||
function semitone12TETValue(value: number): string {
|
||||
let iValue = Math.round(value) % 12;
|
||||
return noteNames[iValue];
|
||||
|
||||
@@ -313,10 +313,85 @@ export class UiFileProperty {
|
||||
useLegacyModDirectory: boolean = false;
|
||||
|
||||
};
|
||||
|
||||
export class ModGuiPort {
|
||||
deserialize(input: any): ModGuiPort {
|
||||
this.index = input.index;
|
||||
this.symbol = input.symbol;
|
||||
this.name = input.name;
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): ModGuiPort[] {
|
||||
let result: ModGuiPort[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
result[i] = new ModGuiPort().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
index: number = -1;
|
||||
symbol: string = "";
|
||||
name: string = "";
|
||||
};
|
||||
export class ModGui {
|
||||
deserialize(input: any): ModGui {
|
||||
this.pluginUri = input.pluginUri;
|
||||
this.resourceDirectory = input.resourceDirectory;
|
||||
this.iconTemplate = input.iconTemplate ?? "";
|
||||
this.settingsTemplate = input.settingsTemplate;
|
||||
this.javascript = input.javascript;
|
||||
this.stylesheet = input.stylesheet ?? "";
|
||||
this.screenshot = input.screenshot;
|
||||
this.thumbnail = input.thumbnail;
|
||||
this.discussionUrl = input.discussionUrl;
|
||||
this.documentationUrl = input.documentationUrl;
|
||||
this.brand = input.brand;
|
||||
this.label = input.label;
|
||||
this.model = input.model;
|
||||
this.panel = input.panel;
|
||||
this.color = input.color;
|
||||
this.knob = input.knob;
|
||||
|
||||
if (input.ports) {
|
||||
this.ports = ModGuiPort.deserialize_array(input.ports);
|
||||
} else {
|
||||
this.ports = [];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
static serialize_array(input: any): ModGui[] {
|
||||
let result: ModGui[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
result[i] = new ModGui().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
pluginUri = "";
|
||||
resourceDirectory = "";
|
||||
iconTemplate = "";
|
||||
settingsTemplate = "";
|
||||
javascript = "";
|
||||
stylesheet = "";
|
||||
screenshot = "";
|
||||
thumbnail = "";
|
||||
discussionUrl = "";
|
||||
documentationUrl = "";
|
||||
brand = "";
|
||||
label = "";
|
||||
model = "";
|
||||
panel = "";
|
||||
color = "";
|
||||
knob = "";
|
||||
ports: ModGuiPort[] = [];
|
||||
|
||||
|
||||
};
|
||||
|
||||
export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
||||
deserialize(input: any): Lv2Plugin {
|
||||
this.uri = input.uri;
|
||||
this.name = input.name;
|
||||
this.minorVersion = input.minorVersion ? input.minorVersion : 0;
|
||||
this.microVersion = input.microVersion ? input.microVersion : 0;
|
||||
this.brand = input.name ? input.name : "";
|
||||
this.label = input.label ? input.label : this.name;
|
||||
this.plugin_class = input.plugin_class;
|
||||
@@ -344,12 +419,18 @@ export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
||||
} else {
|
||||
this.uiPortNotifications = [];
|
||||
}
|
||||
this.modGui = null;
|
||||
if (input.modGui) {
|
||||
this.modGui = new ModGui().deserialize(input.modGui);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
static EmptyFeatures: string[] = [];
|
||||
|
||||
uri: string = "";
|
||||
name: string = "";
|
||||
minorVersion: number = 0;
|
||||
microVersion: number = 0;
|
||||
brand: string = "";
|
||||
label: string = "";
|
||||
plugin_class: string = "";
|
||||
@@ -364,6 +445,7 @@ export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
||||
fileProperties: UiFileProperty[] = [];
|
||||
frequencyPlots: UiFrequencyPlot[] = [];
|
||||
uiPortNotifications: UiPropertyNotification[] = [];
|
||||
modGui: ModGui | null = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -459,10 +541,12 @@ export enum ControlType {
|
||||
Vu,
|
||||
Progress,
|
||||
DbVu,
|
||||
OutputText
|
||||
OutputText,
|
||||
|
||||
BypassLight
|
||||
}
|
||||
|
||||
const textUnits : Set<Units> = new Set<Units>([
|
||||
const textUnits: Set<Units> = new Set<Units>([
|
||||
Units.bar,
|
||||
Units.beat,
|
||||
Units.bpm,
|
||||
@@ -478,13 +562,12 @@ const textUnits : Set<Units> = new Set<Units>([
|
||||
Units.semitone12TET
|
||||
]);
|
||||
|
||||
function displayUnitAsText(unit: Units): boolean
|
||||
{
|
||||
function displayUnitAsText(unit: Units): boolean {
|
||||
return textUnits.has(unit);
|
||||
}
|
||||
|
||||
export class UiControl implements Deserializable<UiControl> {
|
||||
deserialize(input: any): UiControl {
|
||||
deserialize(input: any): UiControl {
|
||||
this.symbol = input.symbol;
|
||||
this.name = input.name;
|
||||
this.index = input.index;
|
||||
@@ -531,12 +614,11 @@ deserialize(input: any): UiControl {
|
||||
this.controlType = ControlType.Progress;
|
||||
} else if (this.enumeration_property) {
|
||||
this.controlType = ControlType.OutputText;
|
||||
} else if (displayUnitAsText(this.units))
|
||||
{
|
||||
} else if (displayUnitAsText(this.units)) {
|
||||
this.controlType = ControlType.OutputText
|
||||
} else {
|
||||
this.controlType = ControlType.Vu;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.isValidEnumeration()) {
|
||||
this.controlType = ControlType.Select;
|
||||
@@ -548,10 +630,8 @@ deserialize(input: any): UiControl {
|
||||
this.controlType = ControlType.OnOffSwitch;
|
||||
}
|
||||
}
|
||||
if (this.is_input)
|
||||
{
|
||||
if (this.pipedal_graphicEq)
|
||||
{
|
||||
if (this.is_input) {
|
||||
if (this.pipedal_graphicEq) {
|
||||
this.controlType = ControlType.GraphicEq;
|
||||
}
|
||||
else if (this.mod_momentaryOnByDefault) {
|
||||
@@ -560,7 +640,7 @@ deserialize(input: any): UiControl {
|
||||
this.controlType = ControlType.Momentary;
|
||||
} else if (this.trigger_property) {
|
||||
this.controlType = ControlType.Trigger;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -654,7 +734,7 @@ deserialize(input: any): UiControl {
|
||||
isAbToggle(): boolean {
|
||||
return this.controlType === ControlType.ABSwitch;
|
||||
}
|
||||
isGraphicEq() : boolean {
|
||||
isGraphicEq(): boolean {
|
||||
return this.controlType === ControlType.GraphicEq;
|
||||
}
|
||||
isSelect(): boolean {
|
||||
@@ -739,15 +819,12 @@ deserialize(input: any): UiControl {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.units === Units.s)
|
||||
{
|
||||
if (this.units === Units.s) {
|
||||
let iValue = Math.round(value);
|
||||
if (iValue >= 60)
|
||||
{
|
||||
let minutes = Math.floor(iValue/60);
|
||||
if (iValue >= 60) {
|
||||
let minutes = Math.floor(iValue / 60);
|
||||
let seconds = iValue % 60;
|
||||
if (seconds < 10)
|
||||
{
|
||||
if (seconds < 10) {
|
||||
return minutes + ":0" + seconds;
|
||||
|
||||
}
|
||||
@@ -756,11 +833,10 @@ deserialize(input: any): UiControl {
|
||||
return this.formatShortValue(value);
|
||||
}
|
||||
}
|
||||
if (this.units === Units.semitone12TET)
|
||||
{
|
||||
if (this.units === Units.semitone12TET) {
|
||||
return semitone12TETValue(value);
|
||||
}
|
||||
|
||||
|
||||
let text = this.formatShortValue(value);
|
||||
|
||||
switch (this.units) {
|
||||
@@ -831,11 +907,45 @@ deserialize(input: any): UiControl {
|
||||
|
||||
}
|
||||
|
||||
export class Lv2PatchPropertyInfo {
|
||||
deserialize(input: any): Lv2PatchPropertyInfo {
|
||||
this.uri = input.uri;
|
||||
this.writable = input.writable;
|
||||
this.readable = input.readable;
|
||||
this.label = input.label;
|
||||
this.index = input.index;
|
||||
this.type = input.type;
|
||||
this.comment = input.comment;
|
||||
this.shortName = input.shortName;
|
||||
this.fileTypes = input.fileTypes;
|
||||
this.supportedExtensions = input.supportedExtensions;
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): Lv2PatchPropertyInfo[] {
|
||||
let result: Lv2PatchPropertyInfo[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
result[i] = new Lv2PatchPropertyInfo().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
uri: string = "";
|
||||
writable : boolean = false;
|
||||
readable: boolean = false;
|
||||
label: string = "";
|
||||
index: number = -1;
|
||||
type: string = "";
|
||||
comment: string = "";
|
||||
shortName: string = "";
|
||||
fileTypes: string[] = [];
|
||||
supportedExtensions: string[] = [];
|
||||
};
|
||||
|
||||
export class UiPlugin implements Deserializable<UiPlugin> {
|
||||
deserialize(input: any): UiPlugin {
|
||||
this.uri = input.uri;
|
||||
this.name = input.name;
|
||||
this.minorVersion = input.minorVersion ? input.minorVersion : 0;
|
||||
this.microVersion = input.microVersion ? input.microVersion : 0;
|
||||
this.brand = input.brand ? input.brand : "";
|
||||
this.label = input.label ? input.label : this.name;
|
||||
this.plugin_type = input.plugin_type as PluginType;
|
||||
@@ -861,6 +971,11 @@ export class UiPlugin implements Deserializable<UiPlugin> {
|
||||
}
|
||||
|
||||
this.is_vst3 = input.is_vst3;
|
||||
this.modGui = null;
|
||||
if (input.modGui) {
|
||||
this.modGui = new ModGui().deserialize(input.modGui);
|
||||
}
|
||||
this.patchProperties = Lv2PatchPropertyInfo.deserialize_array(input.patchProperties ?? []);
|
||||
return this;
|
||||
|
||||
}
|
||||
@@ -908,6 +1023,8 @@ export class UiPlugin implements Deserializable<UiPlugin> {
|
||||
|
||||
uri: string = "";
|
||||
name: string = "";
|
||||
minorVersion: number = 0;
|
||||
microVersion: number = 0;
|
||||
brand: string = "";
|
||||
label: string = "";
|
||||
plugin_type: PluginType = PluginType.InvalidPlugin;
|
||||
@@ -924,6 +1041,8 @@ export class UiPlugin implements Deserializable<UiPlugin> {
|
||||
fileProperties: UiFileProperty[] = [];
|
||||
frequencyPlots: UiFrequencyPlot[] = [];
|
||||
is_vst3: boolean = false;
|
||||
modGui: ModGui | null = null; // null if no mod gui.
|
||||
patchProperties: Lv2PatchPropertyInfo[] = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ import Snapshot3Icon from "./svg/snapshot_3.svg?react";
|
||||
import Snapshot4Icon from "./svg/snapshot_4.svg?react";
|
||||
import Snapshot5Icon from "./svg/snapshot_5.svg?react";
|
||||
import Snapshot6Icon from "./svg/snapshot_6.svg?react";
|
||||
import ModUiIcon from './svg/mod_ui.svg?react';
|
||||
import PipedalUiIcon from './svg/pp_ui.svg?react';
|
||||
|
||||
import SnapshotDialog from './SnapshotDialog';
|
||||
import { css } from '@emotion/react';
|
||||
@@ -128,6 +130,8 @@ interface MainState {
|
||||
showMidiBindingsDialog: boolean;
|
||||
screenHeight: number;
|
||||
displayNameDialogOpen: boolean;
|
||||
showModUi: boolean;
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -167,7 +171,8 @@ export const MainPage =
|
||||
displayAuthor: this.getDisplayAuthor(),
|
||||
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
|
||||
showMidiBindingsDialog: false,
|
||||
screenHeight: this.windowSize.height
|
||||
screenHeight: this.windowSize.height,
|
||||
showModUi: false
|
||||
|
||||
|
||||
};
|
||||
@@ -375,7 +380,7 @@ export const MainPage =
|
||||
if (pedalboardItem === null) return "";
|
||||
return pedalboardItem.uri;
|
||||
}
|
||||
titleBar(pedalboardItem: PedalboardItem | null): React.ReactNode {
|
||||
titleBar(pedalboardItem: PedalboardItem | null,canShowModUi: boolean): React.ReactNode {
|
||||
let title = "";
|
||||
let author = "";
|
||||
let infoPluginUri = "";
|
||||
@@ -441,7 +446,7 @@ export const MainPage =
|
||||
{canEditTitle ? (
|
||||
<ButtonBase
|
||||
style={{ borderRadius: "6px", width: "100%", paddingLeft: 8, paddingRight: 8, height: 48, textTransform: "none" }}
|
||||
onClick={()=> {
|
||||
onClick={() => {
|
||||
this.handleEditPluginDisplayName();
|
||||
}}
|
||||
>
|
||||
@@ -453,7 +458,7 @@ export const MainPage =
|
||||
</span>
|
||||
</ButtonBase>)
|
||||
: (
|
||||
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8,minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
{this.state.displayAuthor && (
|
||||
<span className={classes.author}>{author}</span>
|
||||
@@ -469,6 +474,32 @@ export const MainPage =
|
||||
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
|
||||
/>
|
||||
</div>
|
||||
{canShowModUi && (
|
||||
<div style={{ flex: "0 0 auot" }}>
|
||||
<IconButtonEx tooltip={(
|
||||
<div>
|
||||
<Typography variant="body2" >
|
||||
{this.state.showModUi ? "PiPedal UI" : "MOD UI"}</Typography>
|
||||
<Divider />
|
||||
<Typography variant="caption">Use MOD UI or PiPedal UI for plugin</Typography>
|
||||
</div>
|
||||
|
||||
)}
|
||||
onClick={() => {
|
||||
this.setState({ showModUi: !this.state.showModUi });
|
||||
}}
|
||||
size="large">
|
||||
{!this.state.showModUi ?
|
||||
(
|
||||
<ModUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||
) : (
|
||||
<PipedalUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||
)
|
||||
}
|
||||
</IconButtonEx>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -494,6 +525,15 @@ export const MainPage =
|
||||
}
|
||||
}
|
||||
|
||||
canShowModUi(pedalboardItem: PedalboardItem): boolean {
|
||||
let pluginInfo = this.model.getUiPlugin(pedalboardItem.uri);
|
||||
if (pluginInfo === null) {
|
||||
return false;
|
||||
}
|
||||
return pluginInfo.modGui !== null;
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
@@ -508,8 +548,9 @@ export const MainPage =
|
||||
let instanceId = -1;
|
||||
let missing = false;
|
||||
let pluginUri = "#error";
|
||||
|
||||
let canShowModUi = false;
|
||||
if (pedalboardItem) {
|
||||
canShowModUi = this.canShowModUi(pedalboardItem);
|
||||
canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId);
|
||||
instanceId = pedalboardItem.instanceId;
|
||||
if (pedalboardItem.isEmpty()) {
|
||||
@@ -570,7 +611,7 @@ export const MainPage =
|
||||
</div>
|
||||
{
|
||||
(!this.state.splitControlBar || !this.props.enableStructureEditing)
|
||||
&& this.titleBar(pedalboardItem)
|
||||
&& this.titleBar(pedalboardItem, canShowModUi)
|
||||
}
|
||||
<div style={{ flex: "1 1 1px" }}>
|
||||
|
||||
@@ -642,7 +683,7 @@ export const MainPage =
|
||||
this.state.splitControlBar && this.props.enableStructureEditing && (
|
||||
<div className={classes.splitControlBar}>
|
||||
{
|
||||
this.titleBar(pedalboardItem)
|
||||
this.titleBar(pedalboardItem, canShowModUi)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
@@ -657,7 +698,11 @@ export const MainPage =
|
||||
|
||||
) :
|
||||
(
|
||||
GetControlView(pedalboardItem)
|
||||
GetControlView(pedalboardItem,this.state.showModUi && canShowModUi,
|
||||
(instanceId: number, showModGui: boolean) => {
|
||||
this.setState({ showModUi: showModGui });
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
@@ -689,7 +734,7 @@ export const MainPage =
|
||||
this.setState({ displayNameDialogOpen: false });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { Component, ErrorInfo, ReactNode } from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
import { UiPlugin } from "./Lv2Plugin";
|
||||
|
||||
interface Props {
|
||||
plugin: UiPlugin | null;
|
||||
children?: ReactNode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
class ModGuiErrorBoundary extends Component<Props, State> {
|
||||
public state: State = {
|
||||
hasError: false,
|
||||
message: "",
|
||||
};
|
||||
|
||||
public static getDerivedStateFromError(error: Error): State {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return { hasError: true, message: error.message };
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error("ModGUI rendering error: ", error);
|
||||
console.error("Stack trace: " , errorInfo.componentStack);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{
|
||||
position: "absolute",left: 0, right: 0, top: 0, bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "stretch",
|
||||
}}>
|
||||
<div style={{ flex: "1 1 auto" }} />
|
||||
<div style={{display: "flex", flexDirection: "row", alignItems: "stretch", justifyContent: "stretch"}}>
|
||||
<div style={{ flex: "1 1 auto" }} />
|
||||
<div style={{maxWidth: 600, margin: 24}}>
|
||||
<Typography variant="h6" style={{ marginBottom: 8 }} >
|
||||
{ this.props.plugin ?
|
||||
`Error Loading '${this.props.plugin.name}'` :
|
||||
`Error Loading UI`
|
||||
}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" style={{marginBottom:16}} >
|
||||
{this.state.message}
|
||||
</Typography>
|
||||
<div style={{alignSelf: "flex-end", marginBottom: 16, marginRight: 24, display: "flex", flexFlow: "row nowrap", justifyContent: "flex-end" }}>
|
||||
<Button
|
||||
style={{alignSelf: "flex-end"}}
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
this.props.onClose();
|
||||
|
||||
}}
|
||||
>Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: "1 1 auto" }} />
|
||||
</div>
|
||||
<div style={{ flex: "2 2 auto" }} />
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ModGuiErrorBoundary;
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State, ListenHandle, PatchPropertyListener } from './PiPedalModel';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import LoadPluginDialog from './LoadPluginDialog';
|
||||
import ButtonEx from './ButtonEx';
|
||||
import Typography from '@mui/material/Typography/Typography';
|
||||
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
|
||||
import { UiPlugin } from './Lv2Plugin';
|
||||
import ModGuiErrorBoundary from './ModGuiErrorBoundary';
|
||||
|
||||
class MyPortHandle implements MonitorPortHandle {
|
||||
private static nextHandle: number = 1;
|
||||
constructor() {
|
||||
this.handle = MyPortHandle.nextHandle++;
|
||||
}
|
||||
handle: number;
|
||||
};
|
||||
|
||||
type MonitorCallback = (value: number) => void;
|
||||
|
||||
class ValueEntry {
|
||||
constructor(defaultValue: number) {
|
||||
this.value = defaultValue;
|
||||
}
|
||||
private listeners: { [handle: number]: MonitorCallback } = {};
|
||||
|
||||
monitor(interval: number, callback: MonitorCallback): MyPortHandle {
|
||||
let handle = new MyPortHandle();
|
||||
this.listeners[handle.handle] = callback;
|
||||
return handle;
|
||||
}
|
||||
unmonitor(handle: MonitorPortHandle) {
|
||||
let h = (handle as MyPortHandle).handle;
|
||||
// remove entry h from listeners
|
||||
if (!(h in this.listeners)) {
|
||||
throw new Error("Invalid handle: " + h);
|
||||
}
|
||||
// remove the callback from the listeners
|
||||
delete this.listeners[h];
|
||||
}
|
||||
setValue(value: number) {
|
||||
if (value !== this.value) {
|
||||
this.value = value;
|
||||
// notify all listeners
|
||||
for (let handle in this.listeners) {
|
||||
this.listeners[handle](value);
|
||||
}
|
||||
}
|
||||
}
|
||||
value: number;
|
||||
};
|
||||
|
||||
class PatchListenerEntry {
|
||||
constructor(instanceId: number, propertyUri: string) {
|
||||
this.instanceId = instanceId;
|
||||
this.propertyUri = propertyUri;
|
||||
}
|
||||
instanceId: number;
|
||||
propertyUri: string;
|
||||
value: any = "";
|
||||
listeners: { handle: number, listener: PatchPropertyListener}[] = [];;
|
||||
};
|
||||
|
||||
|
||||
class ValueHandler {
|
||||
|
||||
private valueDictionary: { [symbol: string]: ValueEntry } = {};
|
||||
private handleToValueEnry: { [handle: number]: ValueEntry } = {};
|
||||
|
||||
constructor(instanceId: number, plugin: UiPlugin) {
|
||||
for (let control of plugin.controls) {
|
||||
this.valueDictionary[control.symbol] = new ValueEntry(control.default_value);
|
||||
}
|
||||
this.valueDictionary["_bypass"] = new ValueEntry(0);
|
||||
}
|
||||
monitorPort(instanceId: number, symbol: string, interval: number, callback: MonitorCallback): MonitorPortHandle {
|
||||
let valueEntry = this.valueDictionary[symbol];
|
||||
|
||||
let portHandle = valueEntry.monitor(interval, callback);
|
||||
this.handleToValueEnry[(portHandle as MyPortHandle).handle] = valueEntry;
|
||||
callback(valueEntry.value);
|
||||
return portHandle;
|
||||
}
|
||||
unmonitorPort(handle: MonitorPortHandle): void {
|
||||
let h = (handle as MyPortHandle).handle;
|
||||
if (!(h in this.handleToValueEnry)) {
|
||||
throw new Error("Invalid handle: " + h);
|
||||
}
|
||||
let valueEntry = this.handleToValueEnry[h];
|
||||
valueEntry.unmonitor(handle);
|
||||
delete this.handleToValueEnry[h];
|
||||
}
|
||||
setValue(instanceId: number, symbol: string, value: number) {
|
||||
// add a delay just for funsies.
|
||||
window.setTimeout(() => {
|
||||
if (!(symbol in this.valueDictionary)) {
|
||||
throw new Error("Invalid symbol: " + symbol);
|
||||
}
|
||||
let valueEntry = this.valueDictionary[symbol];
|
||||
valueEntry.setValue(value);
|
||||
}, 100);
|
||||
}
|
||||
getValue(instanceId: number, symbol: string): number {
|
||||
if (!(symbol in this.valueDictionary)) {
|
||||
throw new Error("Invalid symbol: " + symbol);
|
||||
}
|
||||
let valueEntry = this.valueDictionary[symbol];
|
||||
return valueEntry.value;
|
||||
}
|
||||
private nextListenHandle: number = 1;
|
||||
|
||||
|
||||
private patchListeners: PatchListenerEntry[] = [];
|
||||
|
||||
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
|
||||
let h = this.nextListenHandle++;
|
||||
for (let entry of this.patchListeners) {
|
||||
if (entry.instanceId === instanceId && entry.propertyUri === propertyUri) {
|
||||
// already listening to this property
|
||||
entry.listeners.push({ handle: h, listener: onReceived });
|
||||
onReceived(instanceId,propertyUri,entry.value);
|
||||
return { _handle: h };
|
||||
}
|
||||
}
|
||||
let entry = new PatchListenerEntry(instanceId, propertyUri);
|
||||
this.patchListeners.push(entry);
|
||||
entry.listeners.push({ handle: h, listener: onReceived });
|
||||
onReceived(instanceId, propertyUri, entry.value);
|
||||
return {_handle: h};
|
||||
}
|
||||
cancelMonitorPatchProperty(listenHandle: ListenHandle): void {
|
||||
for (let i = 0; i < this.patchListeners.length; ++i) {
|
||||
let entry = this.patchListeners[i];
|
||||
for (let j = 0; j < entry.listeners.length; ++j) {
|
||||
if (entry.listeners[j].handle === listenHandle._handle) {
|
||||
// found the listener, remove it
|
||||
entry.listeners.splice(j, 1);
|
||||
if (entry.listeners.length === 0) {
|
||||
// no more listeners, remove the entry
|
||||
this.patchListeners.splice(i, 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getPatchProperty(instanceId: number, uri: string): Promise<any> {
|
||||
let promise = new Promise<any>((resolve, reject) => {
|
||||
window.setTimeout(() => {
|
||||
for (let entry of this.patchListeners) {
|
||||
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
|
||||
window.setTimeout(() => {
|
||||
// simulate a delay for getting the property
|
||||
entry.listeners.forEach(l => l.listener(instanceId, uri, entry.value));
|
||||
}, 100);
|
||||
return resolve(entry.value);
|
||||
}
|
||||
}
|
||||
reject(new Error(`Patch property not found: ${uri} for instance ${instanceId}`));
|
||||
},50);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
|
||||
// Implementation here
|
||||
for (let entry of this.patchListeners) {
|
||||
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
|
||||
entry.value = value;
|
||||
// notify all listeners
|
||||
entry.listeners.forEach(l => l.listener(instanceId, uri, value));
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
// if we reach here, the property was not found
|
||||
return Promise.reject("Not found.");
|
||||
}
|
||||
};
|
||||
|
||||
function ModGuiTest() {
|
||||
const [pluginUrl, setPluginUrl] = React.useState("");
|
||||
const [uiPlugin, setUiPlugin] = React.useState<UiPlugin | null>(null);
|
||||
const [pluginDialogOpen, setPluginDialogOpen] = React.useState(false);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [valueHandler, setValueHandler] = React.useState<ValueHandler | null>(null);
|
||||
|
||||
const [modGuiHostSite] = React.useState<IModGuiHostSite>({
|
||||
monitorPort: (instanceId: number, symbol: string, interval: number, callback: (value: number) => void) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
return valueHandler.monitorPort(instanceId, symbol, interval, callback);
|
||||
},
|
||||
unmonitorPort: (handle) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
valueHandler.unmonitorPort(handle);
|
||||
},
|
||||
setPedalboardControl: (instanceId, symbol, value) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
valueHandler.setValue(instanceId, symbol, value);
|
||||
},
|
||||
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
|
||||
return valueHandler.monitorPatchProperty(instanceId, propertyUri, onReceived);
|
||||
},
|
||||
cancelMonitorPatchProperty: (listenHandle) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
valueHandler.cancelMonitorPatchProperty(listenHandle);
|
||||
},
|
||||
getPatchProperty: async (instanceId, uri) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
return valueHandler.getPatchProperty(instanceId, uri) as Promise<any>;
|
||||
},
|
||||
setPatchProperty: async (instanceId, uri, value) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
return valueHandler.setPatchProperty(instanceId, uri, value) as Promise<boolean>;
|
||||
}
|
||||
});
|
||||
|
||||
let model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
const onStateChange = (state: State) => {
|
||||
if (state == State.Ready) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
model.state.addOnChangedHandler(onStateChange);
|
||||
|
||||
return () => {
|
||||
model.state.removeOnChangedHandler(onStateChange);
|
||||
};
|
||||
}, [])
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex", flexFlow: "column nowrap", alignItems: "stretch", justifyContent: "stretch",
|
||||
position: "absolute", top: 0, left: 0, width: "100%", height: "100%", backgroundColor: (theme.palette as any).mainBackground
|
||||
}}>
|
||||
|
||||
<div style={{
|
||||
position: "absolute", top: 0, left: 0, right: 0,
|
||||
bottom: 0, zIndex: 1000,
|
||||
display: loading ? "flex" : "none",
|
||||
alignItems: "center", justifyContent: "center",
|
||||
background: "rgba(0.5,0.5,0.5, 0.9)"
|
||||
}}>
|
||||
<CircularProgress style={{ color: theme.palette.text.secondary }} />
|
||||
</div>
|
||||
|
||||
|
||||
<div style={{
|
||||
flex: "0 0 auto", display: "flex", flexFlow: "row nowrap",
|
||||
alignItems: "center", gap: 16, paddingLeft: 32, paddingRight: 32, paddingTop: 16, paddingBottom: 16,
|
||||
}}>
|
||||
<Typography noWrap variant="h6" style={{ flex: "1 0 auto" }}>ModGUI Test</Typography>
|
||||
<Typography noWrap variant="body2" style={{ flex: "0 1 auto", }}>
|
||||
{pluginUrl ? pluginUrl : "No plugin loaded"}
|
||||
</Typography>
|
||||
<ButtonEx style={{ flex: "0 0 auto" }} variant="contained" tooltip="Select plugin" onClick={() => setPluginDialogOpen(true)} >
|
||||
Load
|
||||
</ButtonEx>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{uiPlugin && (
|
||||
<div style={{ flex: "1 1 auto", position: "relative" }}>
|
||||
<ModGuiErrorBoundary plugin={uiPlugin} onClose={() => {
|
||||
setPluginUrl("");
|
||||
setUiPlugin(null);
|
||||
setValueHandler(null);
|
||||
}} >
|
||||
|
||||
<ModGuiHost instanceId={-1} plugin={uiPlugin}
|
||||
onClose={() => {
|
||||
setPluginUrl("");
|
||||
setUiPlugin(null);
|
||||
}}
|
||||
hostSite={modGuiHostSite}
|
||||
/>
|
||||
</ModGuiErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
{pluginDialogOpen &&
|
||||
<LoadPluginDialog
|
||||
open={pluginDialogOpen}
|
||||
onOk={(url) => {
|
||||
setPluginUrl(url as string);
|
||||
let uiPlugin = model.getUiPlugin(url);
|
||||
setUiPlugin(uiPlugin);
|
||||
if (uiPlugin) {
|
||||
setValueHandler(new ValueHandler(-1, uiPlugin));
|
||||
}
|
||||
setPluginDialogOpen(false);
|
||||
}}
|
||||
onCancel={() => setPluginDialogOpen(false)}
|
||||
uri={pluginUrl}
|
||||
modGuiOnly={true}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModGuiTest;
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import IconButtonEx from './IconButtonEx';
|
||||
import Button from '@mui/material/Button';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
|
||||
import ReportGmailerrorredIcon from '@mui/icons-material/ReportGmailerrorred';
|
||||
|
||||
export interface OkDialogProps {
|
||||
open: boolean,
|
||||
title?: string,
|
||||
text: string,
|
||||
okButtonText?: string,
|
||||
onClose: () => void
|
||||
};
|
||||
|
||||
export interface OkDialogState {
|
||||
};
|
||||
|
||||
export default class OkDialog extends React.Component<OkDialogProps, OkDialogState> {
|
||||
|
||||
|
||||
constructor(props: OkDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let { open, okButtonText, text, onClose } = props;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
onClose();
|
||||
}
|
||||
return (
|
||||
<DialogEx tag="alert" open={open} onClose={handleClose}
|
||||
onEnterKey={handleOk}
|
||||
maxWidth="sm"
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
{props.title && (
|
||||
<DialogTitle id="alert-dialog-title" style={{ userSelect: "none", padding: 0, margin: 0 }}>
|
||||
<Toolbar style={{paddingLeft: 24, paddingRight: 24,margin: 0}} >
|
||||
<IconButtonEx
|
||||
tooltip="Back" edge="start" color="inherit" onClick={()=>{this.props.onClose();}} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButtonEx>
|
||||
|
||||
<Typography noWrap variant="body1" style={{ flex: "1 1 auto" }}>{props.title}</Typography>
|
||||
</Toolbar>
|
||||
</DialogTitle>
|
||||
)}
|
||||
|
||||
<DialogContent >
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", gap: 16, marginLeft: 32, marginRight: 32, alignItems: "start" }}>
|
||||
<ReportGmailerrorredIcon style={{ color: "var(--color-error)", fontSize: 48, flexShrink: 0, opacity: 0.8 }} />
|
||||
<Typography variant="body2" style={{ marginTop:16}}
|
||||
>{text}</Typography>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
<Button variant="dialogPrimary" onClick={handleOk} style={{marginRight: 24}} >
|
||||
{okButtonText ? okButtonText : "OK"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -416,6 +416,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
socketServerUrl: string = "";
|
||||
varServerUrl: string = "";
|
||||
modResourcesUrl: string = "";
|
||||
lv2Path: string = "";
|
||||
webSocket?: PiPedalSocket;
|
||||
|
||||
@@ -972,13 +973,17 @@ export class PiPedalModel //implements PiPedalModel
|
||||
// anything could have changed while we were disconnected.
|
||||
await this.loadServerState();
|
||||
}
|
||||
makeSocketServerUrl(hostName: string, port: number): string {
|
||||
private makeSocketServerUrl(hostName: string, port: number): string {
|
||||
return "ws://" + hostName + ":" + port + "/pipedal";
|
||||
|
||||
}
|
||||
makeVarServerUrl(protocol: string, hostName: string, port: number): string {
|
||||
private makeVarServerUrl(protocol: string, hostName: string, port: number): string {
|
||||
return protocol + "://" + hostName + ":" + port + "/var/";
|
||||
|
||||
}
|
||||
private makeModResourceUrl(protocol: string, hostName: string, port: number): string {
|
||||
return protocol + "://" + hostName + ":" + port + "/resources/";
|
||||
|
||||
}
|
||||
|
||||
async getNextAudioFile(filePath: string): Promise<string> {
|
||||
@@ -1053,6 +1058,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
if (!socket_server_port) socket_server_port = 8080;
|
||||
let socket_server = this.makeSocketServerUrl(socket_server_address, socket_server_port);
|
||||
let var_server_url = this.makeVarServerUrl("http", socket_server_address, socket_server_port);
|
||||
this.modResourcesUrl = this.makeModResourceUrl("http", socket_server_address, socket_server_port);
|
||||
|
||||
this.socketServerUrl = socket_server;
|
||||
this.varServerUrl = var_server_url;
|
||||
|
||||
@@ -23,6 +23,8 @@ import WithStyles, { withTheme } from './WithStyles';
|
||||
import { createStyles } from './WithStyles';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { UiPlugin, UiControl, UiFileProperty, UiFrequencyPlot, ScalePoint } from './Lv2Plugin';
|
||||
@@ -340,6 +342,8 @@ export interface PluginControlViewProps extends WithStyles<typeof styles> {
|
||||
item: PedalboardItem;
|
||||
customization?: ControlViewCustomization;
|
||||
customizationId?: number;
|
||||
showModGui: boolean;
|
||||
onSetShowModGui: (instanceId: number, showModGui: boolean) => void;
|
||||
}
|
||||
type PluginControlViewState = {
|
||||
landscapeGrid: boolean;
|
||||
@@ -360,7 +364,6 @@ const PluginControlView =
|
||||
constructor(props: PluginControlViewProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.state = {
|
||||
landscapeGrid: false,
|
||||
imeUiControl: undefined,
|
||||
@@ -376,6 +379,19 @@ const PluginControlView =
|
||||
this.onControlValueChanged = this.onControlValueChanged.bind(this);
|
||||
this.onPreviewChange = this.onPreviewChange.bind(this);
|
||||
}
|
||||
// unmonitorPort: (handle: MonitorPortHandle) => void;
|
||||
// onValueChanged: (instanceId: number, symbol: string, value: number) => void;
|
||||
|
||||
// monitorPatchProperty(
|
||||
// instanceId: number,
|
||||
// propertyUri: string,
|
||||
// onReceived: PatchPropertyListener
|
||||
// ): ListenHandle;
|
||||
|
||||
// cancelMonitorPatchProperty(listenHandle: ListenHandle): void;
|
||||
|
||||
// getPatchProperty(instanceId: number, uri: string): Promise<any>;
|
||||
// setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean>
|
||||
|
||||
|
||||
onPreviewChange(key: string, value: number): void {
|
||||
@@ -758,19 +774,45 @@ const PluginControlView =
|
||||
}
|
||||
return false;
|
||||
}
|
||||
makeHostSite(): IModGuiHostSite {
|
||||
// Yuck. :-( This worked out badly.
|
||||
let model = this.model;
|
||||
return {
|
||||
monitorPort: this.model.monitorPort.bind(model),
|
||||
unmonitorPort: this.model.unmonitorPort.bind(model),
|
||||
setPedalboardControl: this.model.setPedalboardControl.bind(model),
|
||||
monitorPatchProperty: this.model.monitorPatchProperty.bind(model),
|
||||
cancelMonitorPatchProperty: this.model.cancelMonitorPatchProperty.bind(model),
|
||||
getPatchProperty: this.model.getPatchProperty.bind(model),
|
||||
setPatchProperty: this.model.setPatchProperty.bind(model),
|
||||
};
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
this.controlKeyIndex = 0;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let pedalboardItem: PedalboardItem;
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
if (this.props.instanceId === Pedalboard.START_CONTROL) {
|
||||
pedalboardItem = pedalboard.makeStartItem();
|
||||
} else if (this.props.instanceId === Pedalboard.END_CONTROL) {
|
||||
pedalboardItem = pedalboard.makeEndItem();
|
||||
} else {
|
||||
pedalboardItem = pedalboard.getItem(this.props.instanceId);
|
||||
renderModGui(): ReactNode {
|
||||
let uiPlugin = this.model.getUiPlugin(this.props.item.uri);
|
||||
if (!uiPlugin) {
|
||||
return (<div />);
|
||||
}
|
||||
return (
|
||||
<ModGuiHost
|
||||
instanceId={this.props.instanceId}
|
||||
plugin={uiPlugin}
|
||||
onClose={() => {
|
||||
if (this.props.onSetShowModGui) {
|
||||
this.props.onSetShowModGui(this.props.instanceId, false);
|
||||
}
|
||||
}}
|
||||
hostSite={this.makeHostSite()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
renderPiPedalControl(pedalboardItem?: PedalboardItem): ReactNode {
|
||||
this.controlKeyIndex = 0;
|
||||
|
||||
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
|
||||
if (!pedalboardItem)
|
||||
return (<div className={classes.frame} ></div>);
|
||||
@@ -792,19 +834,13 @@ const PluginControlView =
|
||||
|
||||
controlValues = this.filterNotOnGui(controlValues, plugin);
|
||||
|
||||
|
||||
|
||||
let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid;
|
||||
let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait;
|
||||
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
||||
let controlNodes: ControlNodes;
|
||||
let frameClass = classes.frame;
|
||||
|
||||
if (this.fullScreen()) {
|
||||
gridClass = classes.noScrollGrid;
|
||||
scrollClass = classes.frameScrollNone;
|
||||
frameClass = classes.noScrollFrame;
|
||||
}
|
||||
let controlNodes: ControlNodes;
|
||||
|
||||
controlNodes = this.getStandardControlNodes(plugin, controlValues);
|
||||
|
||||
@@ -822,16 +858,7 @@ const PluginControlView =
|
||||
if (pedalboardItem.midiChannelBinding) {
|
||||
nodes.push(this.midiBindingControl(pedalboardItem));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={frameClass}>
|
||||
<div className={classes.vuMeterL}>
|
||||
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
<div className={vuMeterRClass}>
|
||||
<VuMeter displayText={true} display="output" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
<div className={scrollClass}>
|
||||
<div className={gridClass} >
|
||||
{
|
||||
@@ -848,6 +875,44 @@ const PluginControlView =
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
render(): ReactNode {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
|
||||
let pedalboardItem: PedalboardItem;
|
||||
if (this.props.instanceId === Pedalboard.START_CONTROL) {
|
||||
pedalboardItem = pedalboard.makeStartItem();
|
||||
} else if (this.props.instanceId === Pedalboard.END_CONTROL) {
|
||||
pedalboardItem = pedalboard.makeEndItem();
|
||||
} else {
|
||||
pedalboardItem = pedalboard.getItem(this.props.instanceId);
|
||||
}
|
||||
|
||||
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
||||
let frameClass = classes.frame;
|
||||
|
||||
if (this.fullScreen()) {
|
||||
frameClass = classes.noScrollFrame;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={frameClass}>
|
||||
<div className={classes.vuMeterL}>
|
||||
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
<div className={vuMeterRClass}>
|
||||
<VuMeter displayText={true} display="output" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
{
|
||||
this.props.showModGui && this.renderModGui()
|
||||
|| this.renderPiPedalControl(pedalboardItem)
|
||||
}
|
||||
|
||||
{this.state.showFileDialog && (
|
||||
|
||||
<FilePropertyDialog open={this.state.showFileDialog}
|
||||
@@ -889,7 +954,7 @@ const PluginControlView =
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* xxx: I don't think we need this anyore. */}
|
||||
<FullScreenIME uiControl={this.state.imeUiControl} value={this.state.imeValue}
|
||||
|
||||
onChange={(key, value) => this.onImeValueChange(key, value)}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
pgraph: {
|
||||
paddingBottom: 16
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export interface ScratchClassProps extends WithStyles<typeof styles> {
|
||||
}
|
||||
|
||||
export interface ScratchClassState {
|
||||
screenWidth: number,
|
||||
screenHeight: number
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ScratchClass = withStyles(
|
||||
class extends ResizeResponsiveComponent<ScratchClassProps, ScratchClassState> {
|
||||
private model: PiPedalModel;
|
||||
constructor(props: ScratchClassProps) {
|
||||
super(props);
|
||||
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
void this.model; // suppress unused variable warning
|
||||
|
||||
this.state = {
|
||||
screenWidth: this.windowSize.width,
|
||||
screenHeight: this.windowSize.height
|
||||
};
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ screenWidth: width, screenHeight: height });
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
super.componentWillUnmount();
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate(prevProps: ScratchClassProps) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
void classes; // suppress unused variable warning
|
||||
return (<div/>);
|
||||
}
|
||||
},
|
||||
styles);
|
||||
|
||||
export default ScratchClass;
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 6.3499999 6.35"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill-rule:evenodd;-inkscape-stroke:none"
|
||||
d="m 2.2089844,0.26367188 c -0.564979,0 -1.0234375,0.46978477 -1.0234375,1.03710932 v 3.7578125 c 0,0.5673246 0.4584585,1.0371094 1.0234375,1.0371094 H 4.140625 c 0.564979,0 1.0234375,-0.4697848 1.0234375,-1.0371094 V 1.3007812 c 0,-0.56732455 -0.4584585,-1.03710932 -1.0234375,-1.03710932 z m 0,0.52929687 H 4.140625 c 0.276179,0 0.4941406,0.21977775 0.4941406,0.50781245 v 3.7578125 c 0,0.2880348 -0.2179616,0.5078125 -0.4941406,0.5078125 H 2.2089844 c -0.276179,0 -0.4941406,-0.2197777 -0.4941407,-0.5078125 V 1.3007812 c 0,-0.2880347 0.2179617,-0.50781245 0.4941407,-0.50781245 z"
|
||||
id="rect234" />
|
||||
<path
|
||||
style="-inkscape-stroke:none"
|
||||
d="m 3.1757813,3.0429688 c -0.4352418,0 -0.7949219,0.357727 -0.7949219,0.7929687 0,0.4352417 0.3596801,0.7949219 0.7949219,0.7949219 0.4352417,0 0.7929687,-0.3596802 0.7929687,-0.7949219 0,-0.4352417 -0.357727,-0.7929687 -0.7929687,-0.7929687 z m 0,0.5292968 c 0.1492592,0 0.2636718,0.1144129 0.2636718,0.2636719 0,0.149259 -0.1144126,0.265625 -0.2636718,0.265625 -0.1492593,0 -0.2656251,-0.116366 -0.265625,-0.265625 0,-0.149259 0.1163657,-0.2636719 0.265625,-0.2636719 z"
|
||||
id="path2914" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 6.3499999 6.35"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
id="layer1">
|
||||
<path
|
||||
id="path3692"
|
||||
d="M 1.7218342,0.53148711 C 1.03731,0.54023357 0.48004958,1.102787 0.48004958,1.7892914 c 0,0.69191 0.56589442,1.2578044 1.25780432,1.2578044 0.69191,0 1.2598715,-0.5658944 1.2598715,-1.2578044 0,-0.5024751 -0.2996529,-0.9383787 -0.7286377,-1.13946526 L 2.1460978,1.1831269 c 0.1950392,0.1300488 0.3224609,0.3518001 0.3224609,0.6061645 0,0.4059278 -0.324777,0.7286377 -0.7307048,0.7286377 -0.4059278,0 -0.7286376,-0.3227099 -0.7286376,-0.7286377 0,-0.3691252 0.2670204,-0.669466 0.6206339,-0.7208862 L 1.4810221,1.7143606 1.7388875,1.7737885 1.8990844,1.0782237 2.0174234,0.56300973 c -0.0053,-0.001214 -0.010694,-0.002471 -0.01602,-0.003617 -0.00604,-0.001301 -0.012021,-0.002404 -0.018087,-0.003617 -0.00465,-9.2974e-4 -0.00928,-0.001706 -0.013953,-0.002584 -0.0066,-0.001242 -0.013523,-0.002479 -0.020154,-0.003617 -0.00411,-7.0267e-4 -0.00828,-0.001405 -0.012402,-0.002067 -0.00736,-0.001187 -0.01482,-0.00256 -0.022221,-0.003617 -8.711e-4,-1.2398e-4 -0.00171,-3.9459e-4 -0.00258,-5.1676e-4 -0.010646,-0.001498 -0.021313,-0.002391 -0.032039,-0.003617 -0.00292,-3.3196e-4 -0.00586,-7.218e-4 -0.00879,-0.001034 -0.00879,-9.4149e-4 -0.017517,-0.001827 -0.026355,-0.002584 -0.00221,-1.8873e-4 -0.0045,-3.3967e-4 -0.00672,-5.1677e-4 -0.00954,-7.6485e-4 -0.019336,-0.00152 -0.028939,-0.002067 -0.00717,-4.0808e-4 -0.014505,-7.4831e-4 -0.021704,-0.001034 -0.00474,-1.8774e-4 -0.0092,-3.827e-4 -0.013953,-5.1677e-4 -0.00657,-1.8642e-4 -0.013046,-4.3271e-4 -0.019637,-5.1676e-4 -0.00537,-6.855e-5 -0.010629,0 -0.01602,0 -0.00541,0 -0.01063,-6.887e-5 -0.01602,0 z" />
|
||||
<path
|
||||
id="path3861"
|
||||
d="m 4.23951,2.741472 c -0.6845242,0.00875 -1.2417846,0.5712999 -1.2417846,1.2578043 0,0.69191 0.5658944,1.2578044 1.2578043,1.2578044 0.69191,0 1.2598715,-0.5658944 1.2598715,-1.2578044 0,-0.5024751 -0.2996529,-0.9383787 -0.7286377,-1.1394653 L 4.6637736,3.3931118 c 0.1950392,0.1300488 0.3224609,0.3518001 0.3224609,0.6061645 0,0.4059278 -0.324777,0.7286377 -0.7307048,0.7286377 -0.4059278,0 -0.7286377,-0.3227099 -0.7286377,-0.7286377 0,-0.3691252 0.2670204,-0.669466 0.620634,-0.7208862 l -0.1488281,0.6459554 0.2578654,0.059428 0.1601969,-0.6955648 0.118339,-0.515214 c -0.0053,-0.00121 -0.010694,-0.00247 -0.01602,-0.00362 -0.00604,-0.0013 -0.012021,-0.0024 -0.018087,-0.00362 -0.00465,-9.297e-4 -0.00928,-0.00171 -0.013953,-0.00258 -0.0066,-0.00124 -0.013523,-0.00248 -0.020154,-0.00362 -0.00411,-7.027e-4 -0.00828,-0.0014 -0.012402,-0.00207 -0.00736,-0.00119 -0.01482,-0.00256 -0.022221,-0.00362 -8.711e-4,-1.24e-4 -0.00171,-3.946e-4 -0.00258,-5.168e-4 -0.010646,-0.0015 -0.021313,-0.00239 -0.032039,-0.00362 -0.00292,-3.319e-4 -0.00586,-7.218e-4 -0.00878,-0.00103 -0.00879,-9.415e-4 -0.017517,-0.00183 -0.026355,-0.00258 -0.00221,-1.887e-4 -0.0045,-3.397e-4 -0.00672,-5.168e-4 -0.00954,-7.648e-4 -0.019336,-0.00152 -0.028939,-0.00207 -0.00717,-4.081e-4 -0.014505,-7.483e-4 -0.021704,-0.00103 -0.00474,-1.877e-4 -0.0092,-3.827e-4 -0.013953,-5.167e-4 -0.00657,-1.864e-4 -0.013046,-4.327e-4 -0.019637,-5.168e-4 -0.00537,-6.85e-5 -0.010629,0 -0.01602,0 -0.00541,0 -0.01063,-6.89e-5 -0.01602,0 z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -8,4 +8,12 @@ export default defineConfig({
|
||||
chunkSizeWarningLimit: 2000
|
||||
},
|
||||
plugins: [react(),svgr()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/resources': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||