| @@ -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) | ||||