@@ -11,7 +11,7 @@ ifeq ($(ARCH), lin) | |||||
SOURCES += ext/noc/noc_file_dialog.c | SOURCES += ext/noc/noc_file_dialog.c | ||||
CFLAGS += -DNOC_FILE_DIALOG_GTK $(shell pkg-config --cflags gtk+-2.0) | CFLAGS += -DNOC_FILE_DIALOG_GTK $(shell pkg-config --cflags gtk+-2.0) | ||||
LDFLAGS += -rdynamic \ | LDFLAGS += -rdynamic \ | ||||
-lpthread -lGL -lGLEW -lglfw -ldl -ljansson -lportaudio -lportmidi -lsamplerate \ | |||||
-lpthread -lGL -lGLEW -lglfw -ldl -ljansson -lportaudio -lportmidi -lsamplerate -lcurl -lzip \ | |||||
$(shell pkg-config --libs gtk+-2.0) | $(shell pkg-config --libs gtk+-2.0) | ||||
TARGET = Rack | TARGET = Rack | ||||
endif | endif | ||||
@@ -20,7 +20,7 @@ ifeq ($(ARCH), mac) | |||||
SOURCES += ext/noc/noc_file_dialog.m | SOURCES += ext/noc/noc_file_dialog.m | ||||
CFLAGS += -DNOC_FILE_DIALOG_OSX | CFLAGS += -DNOC_FILE_DIALOG_OSX | ||||
CXXFLAGS += -DAPPLE -stdlib=libc++ -I$(HOME)/local/include | CXXFLAGS += -DAPPLE -stdlib=libc++ -I$(HOME)/local/include | ||||
LDFLAGS += -stdlib=libc++ -L$(HOME)/local/lib -lpthread -lglew -lglfw3 -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo -ldl -ljansson -lportaudio -lportmidi -lsamplerate | |||||
LDFLAGS += -stdlib=libc++ -L$(HOME)/local/lib -lpthread -lglew -lglfw3 -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo -ldl -ljansson -lportaudio -lportmidi -lsamplerate -lcurl -lzip | |||||
TARGET = Rack | TARGET = Rack | ||||
endif | endif | ||||
@@ -31,7 +31,7 @@ CXXFLAGS += -DGLEW_STATIC \ | |||||
-I$(HOME)/pkg/portaudio-r1891-build/include | -I$(HOME)/pkg/portaudio-r1891-build/include | ||||
LDFLAGS += \ | LDFLAGS += \ | ||||
-Wl,-Bstatic,--whole-archive \ | -Wl,-Bstatic,--whole-archive \ | ||||
-lglfw3 -lgdi32 -lglew32 -ljansson -lsamplerate \ | |||||
-lglfw3 -lgdi32 -lglew32 -ljansson -lsamplerate -lcurl -lzip \ | |||||
-Wl,-Bdynamic,--no-whole-archive \ | -Wl,-Bdynamic,--no-whole-archive \ | ||||
-lpthread -lopengl32 -lcomdlg32 -lole32 \ | -lpthread -lopengl32 -lcomdlg32 -lole32 \ | ||||
-lportmidi \ | -lportmidi \ | ||||
@@ -24,6 +24,6 @@ If the build breaks because you think I've missed a step, feel free to post an i | |||||
## License | ## License | ||||
Rack source code by Andrew Belt: BSD-3-Clause | |||||
Rack source code by [Andrew Belt](https://andrewbelt.name/): [BSD-3-Clause](LICENSE.txt) | |||||
Component Library graphics by [Grayscale](http://grayscale.info/): [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/) | Component Library graphics by [Grayscale](http://grayscale.info/): [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/) |
@@ -234,6 +234,14 @@ struct Toolbar : OpaqueWidget { | |||||
void draw(NVGcontext *vg); | void draw(NVGcontext *vg); | ||||
}; | }; | ||||
struct PluginManagerWidget : Widget { | |||||
Widget *loginWidget; | |||||
Widget *manageWidget; | |||||
Widget *downloadWidget; | |||||
PluginManagerWidget(); | |||||
void step(); | |||||
}; | |||||
struct RackScene : Scene { | struct RackScene : Scene { | ||||
Toolbar *toolbar; | Toolbar *toolbar; | ||||
ScrollWidget *scrollWidget; | ScrollWidget *scrollWidget; | ||||
@@ -37,6 +37,16 @@ extern std::list<Plugin*> gPlugins; | |||||
void pluginInit(); | void pluginInit(); | ||||
void pluginDestroy(); | void pluginDestroy(); | ||||
void pluginOpenBrowser(std::string url); | |||||
void pluginLogIn(std::string email, std::string password); | |||||
void pluginLogOut(); | |||||
void pluginRefresh(); | |||||
void pluginCancelDownload(); | |||||
bool pluginIsLoggedIn(); | |||||
bool pluginIsDownloading(); | |||||
float pluginGetDownloadProgress(); | |||||
std::string pluginGetDownloadName(); | |||||
} // namespace rack | } // namespace rack | ||||
@@ -52,6 +52,7 @@ struct Widget { | |||||
Rect box = Rect(Vec(), Vec(INFINITY, INFINITY)); | Rect box = Rect(Vec(), Vec(INFINITY, INFINITY)); | ||||
Widget *parent = NULL; | Widget *parent = NULL; | ||||
std::list<Widget*> children; | std::list<Widget*> children; | ||||
bool visible = true; | |||||
virtual ~Widget(); | virtual ~Widget(); | ||||
@@ -219,10 +220,10 @@ struct QuantityWidget : virtual Widget { | |||||
std::string label; | std::string label; | ||||
/** Include a space character if you want a space after the number, e.g. " Hz" */ | /** Include a space character if you want a space after the number, e.g. " Hz" */ | ||||
std::string unit; | std::string unit; | ||||
/** The digit place to round for displaying values. | |||||
A precision of -2 will display as "1.00" for example. | |||||
/** The decimal place to round for displaying values. | |||||
A precision of 2 will display as "1.00" for example. | |||||
*/ | */ | ||||
int precision = -2; | |||||
int precision = 2; | |||||
QuantityWidget(); | QuantityWidget(); | ||||
void setValue(float value); | void setValue(float value); | ||||
@@ -357,6 +358,7 @@ struct ScrollWidget : OpaqueWidget { | |||||
struct TextField : OpaqueWidget { | struct TextField : OpaqueWidget { | ||||
std::string text; | std::string text; | ||||
std::string placeholder; | |||||
int begin = 0; | int begin = 0; | ||||
int end = 0; | int end = 0; | ||||
@@ -374,6 +376,13 @@ struct PasswordField : TextField { | |||||
void draw(NVGcontext *vg); | void draw(NVGcontext *vg); | ||||
}; | }; | ||||
struct ProgressBar : TransparentWidget, QuantityWidget { | |||||
ProgressBar() { | |||||
box.size.y = BND_WIDGET_HEIGHT; | |||||
} | |||||
void draw(NVGcontext *vg); | |||||
}; | |||||
struct Tooltip : Widget { | struct Tooltip : Widget { | ||||
void step(); | void step(); | ||||
void draw(NVGcontext *vg); | void draw(NVGcontext *vg); | ||||
@@ -0,0 +1,166 @@ | |||||
#include <thread> | |||||
#include "app.hpp" | |||||
#include "plugin.hpp" | |||||
namespace rack { | |||||
PluginManagerWidget::PluginManagerWidget() { | |||||
box.size.y = BND_WIDGET_HEIGHT; | |||||
float margin = 5; | |||||
{ | |||||
loginWidget = new Widget(); | |||||
Vec pos = Vec(0, 0); | |||||
struct RegisterButton : Button { | |||||
void onAction() { | |||||
std::thread t(pluginOpenBrowser, "http://vcvrack.com/"); | |||||
t.detach(); | |||||
} | |||||
}; | |||||
Button *registerButton = new RegisterButton(); | |||||
registerButton->box.pos = pos; | |||||
registerButton->box.size.x = 75; | |||||
registerButton->text = "Register"; | |||||
loginWidget->addChild(registerButton); | |||||
pos.x += registerButton->box.size.x; | |||||
pos.x += margin; | |||||
TextField *emailField = new TextField(); | |||||
emailField->box.pos = pos; | |||||
emailField->box.size.x = 175; | |||||
emailField->placeholder = "Email"; | |||||
loginWidget->addChild(emailField); | |||||
pos.x += emailField->box.size.x; | |||||
pos.x += margin; | |||||
PasswordField *passwordField = new PasswordField(); | |||||
passwordField->box.pos = pos; | |||||
passwordField->box.size.x = 175; | |||||
passwordField->placeholder = "Password"; | |||||
loginWidget->addChild(passwordField); | |||||
pos.x += passwordField->box.size.x; | |||||
struct LogInButton : Button { | |||||
TextField *emailField; | |||||
TextField *passwordField; | |||||
void onAction() { | |||||
std::thread t(pluginLogIn, emailField->text, passwordField->text); | |||||
t.detach(); | |||||
passwordField->text = ""; | |||||
} | |||||
}; | |||||
pos.x += margin; | |||||
LogInButton *logInButton = new LogInButton(); | |||||
logInButton->box.pos = pos; | |||||
logInButton->box.size.x = 100; | |||||
logInButton->text = "Log in"; | |||||
logInButton->emailField = emailField; | |||||
logInButton->passwordField = passwordField; | |||||
loginWidget->addChild(logInButton); | |||||
addChild(loginWidget); | |||||
} | |||||
{ | |||||
manageWidget = new Widget(); | |||||
Vec pos = Vec(0, 0); | |||||
struct ManageButton : Button { | |||||
void onAction() { | |||||
std::thread t(pluginOpenBrowser, "http://vcvrack.com/"); | |||||
t.detach(); | |||||
} | |||||
}; | |||||
Button *manageButton = new ManageButton(); | |||||
manageButton->box.pos = pos; | |||||
manageButton->box.size.x = 125; | |||||
manageButton->text = "Manage plugins"; | |||||
manageWidget->addChild(manageButton); | |||||
pos.x += manageButton->box.size.x; | |||||
struct RefreshButton : Button { | |||||
void onAction() { | |||||
std::thread t(pluginRefresh); | |||||
t.detach(); | |||||
} | |||||
}; | |||||
pos.x += margin; | |||||
Button *refreshButton = new RefreshButton(); | |||||
refreshButton->box.pos = pos; | |||||
refreshButton->box.size.x = 125; | |||||
refreshButton->text = "Refresh plugins"; | |||||
manageWidget->addChild(refreshButton); | |||||
pos.x += refreshButton->box.size.x; | |||||
struct LogOutButton : Button { | |||||
void onAction() { | |||||
pluginLogOut(); | |||||
} | |||||
}; | |||||
pos.x += margin; | |||||
Button *logOutButton = new LogOutButton(); | |||||
logOutButton->box.pos = pos; | |||||
logOutButton->box.size.x = 100; | |||||
logOutButton->text = "Log out"; | |||||
manageWidget->addChild(logOutButton); | |||||
addChild(manageWidget); | |||||
} | |||||
{ | |||||
downloadWidget = new Widget(); | |||||
Vec pos = Vec(0, 0); | |||||
struct DownloadProgressBar : ProgressBar { | |||||
void step() { | |||||
label = "Downloading"; | |||||
std::string name = pluginGetDownloadName(); | |||||
if (name != "") | |||||
label += " " + name; | |||||
setValue(100.0 * pluginGetDownloadProgress()); | |||||
} | |||||
}; | |||||
ProgressBar *downloadProgress = new DownloadProgressBar(); | |||||
downloadProgress->box.pos = pos; | |||||
downloadProgress->box.size.x = 300; | |||||
downloadProgress->setLimits(0, 100); | |||||
downloadProgress->unit = "%"; | |||||
downloadWidget->addChild(downloadProgress); | |||||
pos.x += downloadProgress->box.size.x; | |||||
// struct CancelButton : Button { | |||||
// void onAction() { | |||||
// pluginCancelDownload(); | |||||
// } | |||||
// }; | |||||
// pos.x += margin; | |||||
// Button *logOutButton = new CancelButton(); | |||||
// logOutButton->box.pos = pos; | |||||
// logOutButton->box.size.x = 100; | |||||
// logOutButton->text = "Cancel"; | |||||
// downloadWidget->addChild(logOutButton); | |||||
addChild(downloadWidget); | |||||
} | |||||
} | |||||
void PluginManagerWidget::step() { | |||||
loginWidget->visible = false; | |||||
manageWidget->visible = false; | |||||
downloadWidget->visible = false; | |||||
if (pluginIsDownloading()) | |||||
downloadWidget->visible = true; | |||||
else if (pluginIsLoggedIn()) | |||||
manageWidget->visible = true; | |||||
else | |||||
loginWidget->visible = true; | |||||
Widget::step(); | |||||
} | |||||
} // namespace rack |
@@ -96,15 +96,7 @@ struct SampleRateChoice : ChoiceButton { | |||||
Toolbar::Toolbar() { | Toolbar::Toolbar() { | ||||
float margin = 5; | float margin = 5; | ||||
box.size.y = BND_WIDGET_HEIGHT + 2*margin; | box.size.y = BND_WIDGET_HEIGHT + 2*margin; | ||||
float xPos = margin; | |||||
{ | |||||
Label *label = new Label(); | |||||
label->box.pos = Vec(xPos, margin); | |||||
label->text = gApplicationVersion; | |||||
addChild(label); | |||||
xPos += 100; | |||||
} | |||||
float xPos = 0; | |||||
xPos += margin; | xPos += margin; | ||||
{ | { | ||||
@@ -163,6 +155,14 @@ Toolbar::Toolbar() { | |||||
addChild(cpuUsageButton); | addChild(cpuUsageButton); | ||||
xPos += cpuUsageButton->box.size.x; | xPos += cpuUsageButton->box.size.x; | ||||
} | } | ||||
xPos += margin; | |||||
{ | |||||
Widget *pluginManager = new PluginManagerWidget(); | |||||
pluginManager->box.pos = Vec(xPos, margin); | |||||
addChild(pluginManager); | |||||
xPos += pluginManager->box.size.x; | |||||
} | |||||
} | } | ||||
void Toolbar::draw(NVGcontext *vg) { | void Toolbar::draw(NVGcontext *vg) { | ||||
@@ -195,7 +195,8 @@ void guiInit() { | |||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); | ||||
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); | ||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); | ||||
window = glfwCreateWindow(1000, 750, gApplicationName.c_str(), NULL, NULL); | |||||
std::string title = gApplicationName + " " + gApplicationVersion; | |||||
window = glfwCreateWindow(1000, 750, title.c_str(), NULL, NULL); | |||||
assert(window); | assert(window); | ||||
glfwMakeContextCurrent(window); | glfwMakeContextCurrent(window); | ||||
@@ -27,10 +27,10 @@ int main() { | |||||
} | } | ||||
#endif | #endif | ||||
pluginInit(); | |||||
engineInit(); | engineInit(); | ||||
guiInit(); | guiInit(); | ||||
sceneInit(); | sceneInit(); | ||||
pluginInit(); | |||||
gRackWidget->loadPatch("autosave.json"); | gRackWidget->loadPatch("autosave.json"); | ||||
engineStart(); | engineStart(); | ||||
@@ -1,5 +1,12 @@ | |||||
#include <stdio.h> | #include <stdio.h> | ||||
#include <assert.h> | |||||
#include <string.h> | |||||
#include <unistd.h> | |||||
#include <sys/types.h> | |||||
#include <sys/stat.h> | |||||
#include <fcntl.h> | |||||
#if defined(WINDOWS) | #if defined(WINDOWS) | ||||
#include <windows.h> | #include <windows.h> | ||||
#elif defined(LINUX) || defined(APPLE) | #elif defined(LINUX) || defined(APPLE) | ||||
@@ -7,6 +14,10 @@ | |||||
#include <glob.h> | #include <glob.h> | ||||
#endif | #endif | ||||
#include <curl/curl.h> | |||||
#include <zip.h> | |||||
#include <jansson.h> | |||||
#include "plugin.hpp" | #include "plugin.hpp" | ||||
@@ -14,23 +25,36 @@ namespace rack { | |||||
std::list<Plugin*> gPlugins; | std::list<Plugin*> gPlugins; | ||||
static | |||||
int loadPlugin(const char *path) { | |||||
static const std::string apiUrl = "http://localhost:8081"; | |||||
static std::string token; | |||||
static bool isDownloading = false; | |||||
static float downloadProgress = 0.0; | |||||
static std::string downloadName; | |||||
Plugin::~Plugin() { | |||||
for (Model *model : models) { | |||||
delete model; | |||||
} | |||||
} | |||||
static int loadPlugin(const char *path) { | |||||
// Load dynamic/shared library | // Load dynamic/shared library | ||||
#if defined(WINDOWS) | #if defined(WINDOWS) | ||||
HINSTANCE handle = LoadLibrary(path); | HINSTANCE handle = LoadLibrary(path); | ||||
if (!handle) { | |||||
fprintf(stderr, "Failed to load library %s\n", path); | |||||
return -1; | |||||
} | |||||
if (!handle) { | |||||
fprintf(stderr, "Failed to load library %s\n", path); | |||||
return -1; | |||||
} | |||||
#elif defined(LINUX) || defined(APPLE) | #elif defined(LINUX) || defined(APPLE) | ||||
char ppath[1024]; | char ppath[1024]; | ||||
snprintf(ppath, sizeof(ppath), "./%s", path); | snprintf(ppath, sizeof(ppath), "./%s", path); | ||||
void *handle = dlopen(ppath, RTLD_NOW | RTLD_GLOBAL); | void *handle = dlopen(ppath, RTLD_NOW | RTLD_GLOBAL); | ||||
if (!handle) { | |||||
fprintf(stderr, "Failed to load library %s: %s\n", path, dlerror()); | |||||
return -1; | |||||
} | |||||
if (!handle) { | |||||
fprintf(stderr, "Failed to load library %s: %s\n", path, dlerror()); | |||||
return -1; | |||||
} | |||||
#endif | #endif | ||||
// Call plugin init() function | // Call plugin init() function | ||||
@@ -58,6 +82,8 @@ int loadPlugin(const char *path) { | |||||
} | } | ||||
void pluginInit() { | void pluginInit() { | ||||
curl_global_init(CURL_GLOBAL_ALL); | |||||
// Load core | // Load core | ||||
// This function is defined in core.cpp | // This function is defined in core.cpp | ||||
Plugin *corePlugin = init(); | Plugin *corePlugin = init(); | ||||
@@ -95,18 +121,249 @@ void pluginInit() { | |||||
void pluginDestroy() { | void pluginDestroy() { | ||||
for (Plugin *plugin : gPlugins) { | for (Plugin *plugin : gPlugins) { | ||||
// TODO unload plugin with `dlclose` or `FreeLibrary` | |||||
// TODO free shared library handle with `dlclose` or `FreeLibrary` | |||||
delete plugin; | delete plugin; | ||||
} | } | ||||
gPlugins.clear(); | gPlugins.clear(); | ||||
curl_global_cleanup(); | |||||
} | } | ||||
//////////////////// | |||||
// CURL and libzip helpers | |||||
//////////////////// | |||||
Plugin::~Plugin() { | |||||
for (Model *model : models) { | |||||
delete model; | |||||
static size_t write_file_callback(void *data, size_t size, size_t nmemb, void *p) { | |||||
int fd = *((int*)p); | |||||
ssize_t len = write(fd, data, size*nmemb); | |||||
return len; | |||||
} | |||||
static int progress_callback(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow) { | |||||
if (dltotal == 0.0) | |||||
return 0; | |||||
float progress = dlnow / dltotal; | |||||
downloadProgress = progress; | |||||
return 0; | |||||
} | |||||
static CURLcode download_file(int fd, const char *url) { | |||||
CURL *curl = curl_easy_init(); | |||||
curl_easy_setopt(curl, CURLOPT_URL, url); | |||||
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); | |||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); | |||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file_callback); | |||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &fd); | |||||
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progress_callback); | |||||
curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, NULL); | |||||
CURLcode res = curl_easy_perform(curl); | |||||
curl_easy_cleanup(curl); | |||||
return res; | |||||
} | |||||
static size_t write_string_callback(void *data, size_t size, size_t nmemb, void *p) { | |||||
std::string &text = *((std::string*)p); | |||||
char *dataStr = (char*) data; | |||||
size_t len = size * nmemb; | |||||
text.append(dataStr, len); | |||||
return len; | |||||
} | |||||
static void extract_zip(int zipfd, int dirfd) { | |||||
int err = 0; | |||||
zip_t *za = zip_fdopen(zipfd, 0, &err); | |||||
if (!za) return; | |||||
if (err) goto cleanup; | |||||
for (int i = 0; i < zip_get_num_entries(za, 0); i++) { | |||||
zip_stat_t zs; | |||||
err = zip_stat_index(za, i, 0, &zs); | |||||
if (err) goto cleanup; | |||||
int nameLen = strlen(zs.name); | |||||
if (zs.name[nameLen - 1] == '/') { | |||||
err = mkdirat(dirfd, zs.name, 0755); | |||||
if (err) goto cleanup; | |||||
} | |||||
else { | |||||
zip_file_t *zf = zip_fopen_index(za, i, 0); | |||||
if (!zf) goto cleanup; | |||||
int out = openat(dirfd, zs.name, O_RDWR | O_TRUNC | O_CREAT, 0644); | |||||
assert(out != -1); | |||||
while (1) { | |||||
char buffer[4096]; | |||||
int len = zip_fread(zf, buffer, sizeof(buffer)); | |||||
if (len <= 0) | |||||
break; | |||||
write(out, buffer, len); | |||||
} | |||||
err = zip_fclose(zf); | |||||
assert(!err); | |||||
close(out); | |||||
} | |||||
} | |||||
cleanup: | |||||
zip_close(za); | |||||
} | |||||
//////////////////// | |||||
// plugin manager | |||||
//////////////////// | |||||
void pluginOpenBrowser(std::string url) { | |||||
// shell injection is possible, so make sure the URL is trusted | |||||
#if defined(LINUX) | |||||
std::string command = "xdg-open " + url; | |||||
system(command.c_str()); | |||||
#endif | |||||
#if defined(APPLE) | |||||
std::string command = "open " + url; | |||||
system(command.c_str()); | |||||
#endif | |||||
#if defined(WINDOWS) | |||||
ShellExecute(NULL, "open", url.c_str(), NULL, NULL, SW_SHOWNORMAL); | |||||
#endif | |||||
} | |||||
void pluginLogIn(std::string email, std::string password) { | |||||
CURL *curl = curl_easy_init(); | |||||
assert(curl); | |||||
std::string postFields = "email=" + email + "&password=" + password; | |||||
std::string url = apiUrl + "/token"; | |||||
std::string resText; | |||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); | |||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_string_callback); | |||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resText); | |||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields.c_str()); | |||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, postFields.size()); | |||||
CURLcode res = curl_easy_perform(curl); | |||||
curl_easy_cleanup(curl); | |||||
if (res == CURLE_OK) { | |||||
// Parse JSON response | |||||
json_error_t error; | |||||
json_t *root = json_loads(resText.c_str(), 0, &error); | |||||
if (root) { | |||||
json_t *tokenJ = json_object_get(root, "token"); | |||||
if (tokenJ) { | |||||
// Set the token, which logs the user in | |||||
token = json_string_value(tokenJ); | |||||
} | |||||
json_decref(root); | |||||
} | |||||
} | } | ||||
} | } | ||||
void pluginLogOut() { | |||||
token = ""; | |||||
} | |||||
static void pluginRefreshPlugin(json_t *pluginJ) { | |||||
json_t *slugJ = json_object_get(pluginJ, "slug"); | |||||
if (!slugJ) return; | |||||
std::string slug = json_string_value(slugJ); | |||||
json_t *nameJ = json_object_get(pluginJ, "name"); | |||||
if (!nameJ) return; | |||||
std::string name = json_string_value(nameJ); | |||||
json_t *urlJ = json_object_get(pluginJ, "download"); | |||||
if (!urlJ) return; | |||||
std::string url = json_string_value(urlJ); | |||||
// Find slug in plugins list | |||||
for (Plugin *p : gPlugins) { | |||||
if (p->slug == slug) { | |||||
return; | |||||
} | |||||
} | |||||
// If plugin is not loaded, download the zip file to /plugins | |||||
fprintf(stderr, "Downloading %s from %s\n", name.c_str(), url.c_str()); | |||||
downloadName = name; | |||||
downloadProgress = 0.0; | |||||
std::string filename = slug + ".zip"; | |||||
int dir = open("plugins", O_RDONLY | O_DIRECTORY); | |||||
int zip = openat(dir, filename.c_str(), O_RDWR | O_TRUNC | O_CREAT, 0644); | |||||
// Download zip | |||||
download_file(zip, url.c_str()); | |||||
// Unzip file | |||||
lseek(zip, 0, SEEK_SET); | |||||
extract_zip(zip, dir); | |||||
// Close files | |||||
close(zip); | |||||
close(dir); | |||||
downloadName = ""; | |||||
} | |||||
void pluginRefresh() { | |||||
if (token.empty()) | |||||
return; | |||||
isDownloading = true; | |||||
downloadProgress = 0.0; | |||||
downloadName = ""; | |||||
// Get plugin list from /plugin | |||||
CURL *curl = curl_easy_init(); | |||||
assert(curl); | |||||
std::string url = apiUrl + "/plugins?token=" + token; | |||||
std::string resText; | |||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); | |||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_string_callback); | |||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resText); | |||||
CURLcode res = curl_easy_perform(curl); | |||||
curl_easy_cleanup(curl); | |||||
if (res == CURLE_OK) { | |||||
// Parse JSON response | |||||
json_error_t error; | |||||
json_t *root = json_loads(resText.c_str(), 0, &error); | |||||
if (root) { | |||||
json_t *pluginsJ = json_object_get(root, "plugins"); | |||||
if (pluginsJ) { | |||||
// Iterate through each plugin object | |||||
size_t index; | |||||
json_t *pluginJ; | |||||
json_array_foreach(pluginsJ, index, pluginJ) { | |||||
pluginRefreshPlugin(pluginJ); | |||||
} | |||||
} | |||||
json_decref(root); | |||||
} | |||||
} | |||||
isDownloading = false; | |||||
} | |||||
void pluginCancelDownload() { | |||||
// TODO | |||||
} | |||||
bool pluginIsLoggedIn() { | |||||
return token != ""; | |||||
} | |||||
bool pluginIsDownloading() { | |||||
return isDownloading; | |||||
} | |||||
float pluginGetDownloadProgress() { | |||||
return downloadProgress; | |||||
} | |||||
std::string pluginGetDownloadName() { | |||||
return downloadName; | |||||
} | |||||
} // namespace rack | } // namespace rack |
@@ -34,11 +34,10 @@ std::string stringf(const char *format, ...) { | |||||
va_end(args); | va_end(args); | ||||
if (size < 0) | if (size < 0) | ||||
return ""; | return ""; | ||||
size++; | |||||
std::string s; | std::string s; | ||||
s.resize(size); | s.resize(size); | ||||
va_start(args, format); | va_start(args, format); | ||||
vsnprintf(&s[0], size, format, args); | |||||
vsnprintf(&s[0], size+1, format, args); | |||||
va_end(args); | va_end(args); | ||||
return s; | return s; | ||||
} | } | ||||
@@ -0,0 +1,12 @@ | |||||
#include "widgets.hpp" | |||||
namespace rack { | |||||
void ProgressBar::draw(NVGcontext *vg) { | |||||
float progress = mapf(value, minValue, maxValue, 0.0, 1.0); | |||||
bndSlider(vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_ALL, BND_DEFAULT, progress, getText().c_str(), NULL); | |||||
} | |||||
} // namespace rack |
@@ -25,16 +25,7 @@ void QuantityWidget::setDefaultValue(float defaultValue) { | |||||
std::string QuantityWidget::getText() { | std::string QuantityWidget::getText() { | ||||
std::string text = label; | std::string text = label; | ||||
text += ": "; | text += ": "; | ||||
char valueStr[128]; | |||||
if (precision >= 0) { | |||||
float factor = powf(10.0, precision); | |||||
float v = roundf(value / factor) * factor; | |||||
snprintf(valueStr, sizeof(valueStr), "%.0f", v); | |||||
} | |||||
else { | |||||
snprintf(valueStr, sizeof(valueStr), "%.*f", -precision, value); | |||||
} | |||||
text += valueStr; | |||||
text += stringf("%.*f", precision, value); | |||||
text += unit; | text += unit; | ||||
return text; | return text; | ||||
} | } | ||||
@@ -18,6 +18,9 @@ void TextField::draw(NVGcontext *vg) { | |||||
state = BND_DEFAULT; | state = BND_DEFAULT; | ||||
bndTextField(vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str(), begin, end); | bndTextField(vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str(), begin, end); | ||||
if (text.empty() && state != BND_ACTIVE) { | |||||
bndIconLabelCaret(vg, 0.0, 0.0, box.size.x, box.size.y, -1, bndGetTheme()->textFieldTheme.itemColor, 13, placeholder.c_str(), bndGetTheme()->textFieldTheme.itemColor, 0, -1); | |||||
} | |||||
} | } | ||||
Widget *TextField::onMouseDown(Vec pos, int button) { | Widget *TextField::onMouseDown(Vec pos, int button) { | ||||
@@ -70,6 +70,8 @@ void Widget::step() { | |||||
void Widget::draw(NVGcontext *vg) { | void Widget::draw(NVGcontext *vg) { | ||||
for (Widget *child : children) { | for (Widget *child : children) { | ||||
if (!child->visible) | |||||
continue; | |||||
nvgSave(vg); | nvgSave(vg); | ||||
nvgTranslate(vg, child->box.pos.x, child->box.pos.y); | nvgTranslate(vg, child->box.pos.x, child->box.pos.y); | ||||
child->draw(vg); | child->draw(vg); | ||||
@@ -80,6 +82,8 @@ void Widget::draw(NVGcontext *vg) { | |||||
Widget *Widget::onMouseDown(Vec pos, int button) { | Widget *Widget::onMouseDown(Vec pos, int button) { | ||||
for (auto it = children.rbegin(); it != children.rend(); it++) { | for (auto it = children.rbegin(); it != children.rend(); it++) { | ||||
Widget *child = *it; | Widget *child = *it; | ||||
if (!child->visible) | |||||
continue; | |||||
if (child->box.contains(pos)) { | if (child->box.contains(pos)) { | ||||
Widget *w = child->onMouseDown(pos.minus(child->box.pos), button); | Widget *w = child->onMouseDown(pos.minus(child->box.pos), button); | ||||
if (w) | if (w) | ||||
@@ -92,6 +96,8 @@ Widget *Widget::onMouseDown(Vec pos, int button) { | |||||
Widget *Widget::onMouseUp(Vec pos, int button) { | Widget *Widget::onMouseUp(Vec pos, int button) { | ||||
for (auto it = children.rbegin(); it != children.rend(); it++) { | for (auto it = children.rbegin(); it != children.rend(); it++) { | ||||
Widget *child = *it; | Widget *child = *it; | ||||
if (!child->visible) | |||||
continue; | |||||
if (child->box.contains(pos)) { | if (child->box.contains(pos)) { | ||||
Widget *w = child->onMouseUp(pos.minus(child->box.pos), button); | Widget *w = child->onMouseUp(pos.minus(child->box.pos), button); | ||||
if (w) | if (w) | ||||
@@ -104,6 +110,8 @@ Widget *Widget::onMouseUp(Vec pos, int button) { | |||||
Widget *Widget::onMouseMove(Vec pos, Vec mouseRel) { | Widget *Widget::onMouseMove(Vec pos, Vec mouseRel) { | ||||
for (auto it = children.rbegin(); it != children.rend(); it++) { | for (auto it = children.rbegin(); it != children.rend(); it++) { | ||||
Widget *child = *it; | Widget *child = *it; | ||||
if (!child->visible) | |||||
continue; | |||||
if (child->box.contains(pos)) { | if (child->box.contains(pos)) { | ||||
Widget *w = child->onMouseMove(pos.minus(child->box.pos), mouseRel); | Widget *w = child->onMouseMove(pos.minus(child->box.pos), mouseRel); | ||||
if (w) | if (w) | ||||
@@ -116,6 +124,8 @@ Widget *Widget::onMouseMove(Vec pos, Vec mouseRel) { | |||||
Widget *Widget::onScroll(Vec pos, Vec scrollRel) { | Widget *Widget::onScroll(Vec pos, Vec scrollRel) { | ||||
for (auto it = children.rbegin(); it != children.rend(); it++) { | for (auto it = children.rbegin(); it != children.rend(); it++) { | ||||
Widget *child = *it; | Widget *child = *it; | ||||
if (!child->visible) | |||||
continue; | |||||
if (child->box.contains(pos)) { | if (child->box.contains(pos)) { | ||||
Widget *w = child->onScroll(pos.minus(child->box.pos), scrollRel); | Widget *w = child->onScroll(pos.minus(child->box.pos), scrollRel); | ||||
if (w) | if (w) | ||||