diff --git a/Core.json b/Core.json new file mode 100644 index 00000000..16335039 --- /dev/null +++ b/Core.json @@ -0,0 +1,113 @@ +{ + "slug": "Core", + "name": "Core", + "version": "1.0.0", + "license": "GPL-3.0-only", + "author": "VCV", + "brand": "VCV", + "authorEmail": "contact@vcvrack.com", + "authorUrl": "https://vcvrack.com/", + "pluginUrl": "https://vcvrack.com/", + "manualUrl": "https://vcvrack.com/manual/Core.html", + "sourceUrl": "https://github.com/VCVRack/Rack", + "donateUrl": "", + "modules": [ + { + "slug": "AudioInterface", + "name": "Audio-8", + "description": "Sends audio and CV to/from an audio device", + "tags": [ + "External" + ] + }, + { + "slug": "AudioInterface16", + "name": "Audio-16", + "description": "Sends audio and CV to/from an audio device", + "tags": [ + "External" + ] + }, + { + "slug": "MIDIToCVInterface", + "name": "MIDI-CV", + "description": "Converts MIDI from an external device to CV and gates", + "tags": [ + "External", + "MIDI", + "Polyphonic" + ] + }, + { + "slug": "MIDICCToCVInterface", + "name": "MIDI-CC", + "description": "Converts MIDI CC from an external device to CV", + "tags": [ + "External", + "MIDI" + ] + }, + { + "slug": "MIDITriggerToCVInterface", + "name": "MIDI-Gate", + "description": "Converts MIDI notes from an external device to gates", + "tags": [ + "External", + "MIDI" + ] + }, + { + "slug": "MIDI-Map", + "name": "MIDI-Map", + "description": "Controls parameters (knobs, sliders, switches) directly with MIDI CC", + "tags": [ + "External", + "MIDI" + ] + }, + { + "slug": "CV-MIDI", + "name": "CV-MIDI", + "description": "Converts CV to MIDI and sends to an external device", + "tags": [ + "External", + "MIDI", + "Polyphonic" + ] + }, + { + "slug": "CV-CC", + "name": "CV-CC", + "description": "Converts CV to MIDI CC and sends to an external device", + "tags": [ + "External", + "MIDI" + ] + }, + { + "slug": "CV-Gate", + "name": "CV-Gate", + "description": "Converts gates to MIDI notes and sends to an external device", + "tags": [ + "External", + "MIDI" + ] + }, + { + "slug": "Blank", + "name": "Blank", + "description": "A resizable blank panel", + "tags": [ + "Blank" + ] + }, + { + "slug": "Notes", + "name": "Notes", + "description": "Write text for patch notes or artist attribution", + "tags": [ + "Blank" + ] + } + ] +} \ No newline at end of file diff --git a/include/app/common.hpp b/include/app/common.hpp index 58537e34..4beabedb 100644 --- a/include/app/common.hpp +++ b/include/app/common.hpp @@ -14,8 +14,9 @@ namespace app { extern std::string APP_NAME; extern std::string APP_VERSION; -extern std::string APP_NEW_VERSION; +extern std::string APP_VERSION_UPDATE; extern std::string API_URL; +extern std::string API_VERSION; static const float SVG_DPI = 75.0; static const float MM_PER_IN = 25.4; diff --git a/include/plugin.hpp b/include/plugin.hpp index f5621949..bbc6f573 100644 --- a/include/plugin.hpp +++ b/include/plugin.hpp @@ -14,13 +14,21 @@ namespace rack { namespace plugin { +struct Update { + std::string pluginSlug; + std::string version; + std::string changelogUrl; + float progress = 0.f; +}; + + void init(); void destroy(); void logIn(const std::string &email, const std::string &password); void logOut(); void queryUpdates(); +void syncUpdate(Update *update); void syncUpdates(); -void cancelDownload(); bool isLoggedIn(); Plugin *getPlugin(const std::string &pluginSlug); Model *getModel(const std::string &pluginSlug, const std::string &modelSlug); @@ -31,20 +39,11 @@ bool isSlugValid(const std::string &slug); std::string normalizeSlug(const std::string &slug); -struct Update { - std::string pluginSlug; - std::string version; - std::string changelogUrl; -}; - - extern const std::set allowedTags; extern std::vector plugins; extern std::string loginStatus; extern std::vector updates; -extern float downloadProgress; -extern std::string downloadName; } // namespace plugin diff --git a/src/Core/plugin.cpp b/src/Core/plugin.cpp index 2723f47c..a57f1abc 100644 --- a/src/Core/plugin.cpp +++ b/src/Core/plugin.cpp @@ -2,70 +2,15 @@ void init(rack::Plugin *p) { - p->slug = "Core"; - p->version = TOSTRING(VERSION); - p->license = "BSD-3-Clause"; - p->name = "Core"; - p->brand = "Core"; - p->author = "VCV"; - p->authorEmail = "contact@vcvrack.com"; - p->authorUrl = "https://vcvrack.com/"; - p->pluginUrl = "https://vcvrack.com/"; - p->manualUrl = "https://vcvrack.com/manual/Core.html"; - p->sourceUrl = "https://github.com/VCVRack/Rack"; - - modelAudioInterface->name = "Audio-8"; - modelAudioInterface->description = "Sends audio and CV to/from an audio device"; - modelAudioInterface->tags = {"External"}; p->addModel(modelAudioInterface); - - modelAudioInterface16->name = "Audio-16"; - modelAudioInterface16->description = "Sends audio and CV to/from an audio device"; - modelAudioInterface16->tags = {"External"}; p->addModel(modelAudioInterface16); - - modelMIDI_CV->name = "MIDI-CV"; - modelMIDI_CV->description = "Converts MIDI from an external device to CV and gates"; - modelMIDI_CV->tags = {"External", "MIDI"}; p->addModel(modelMIDI_CV); - - modelMIDI_CC->name = "MIDI-CC"; - modelMIDI_CC->description = "Converts MIDI CC from an external device to CV"; - modelMIDI_CC->tags = {"External", "MIDI"}; p->addModel(modelMIDI_CC); - - modelMIDI_Gate->name = "MIDI-Gate"; - modelMIDI_Gate->description = "Converts MIDI notes from an external device to gates"; - modelMIDI_Gate->tags = {"External", "MIDI"}; p->addModel(modelMIDI_Gate); - - modelMIDI_Map->name = "MIDI-Map"; - modelMIDI_Map->description = ""; - modelMIDI_Map->tags = {"External", "MIDI"}; p->addModel(modelMIDI_Map); - - modelCV_MIDI->name = "CV-MIDI"; - modelCV_MIDI->description = "Converts CV to MIDI and sends to an external device"; - modelCV_MIDI->tags = {"External", "MIDI"}; p->addModel(modelCV_MIDI); - - modelCV_CC->name = "CV-CC"; - modelCV_CC->description = "Converts CV to MIDI CC and sends to an external device"; - modelCV_CC->tags = {"External", "MIDI"}; p->addModel(modelCV_CC); - - modelCV_Gate->name = "CV-Gate"; - modelCV_Gate->description = "Converts gates to MIDI notes and sends to an external device"; - modelCV_Gate->tags = {"External", "MIDI"}; p->addModel(modelCV_Gate); - - modelBlank->name = "Blank"; - modelBlank->description = "A resizable blank panel"; - modelBlank->tags = {"Blank"}; p->addModel(modelBlank); - - modelNotes->name = "Notes"; - modelNotes->description = "Write text for patch notes or artist attribution"; - modelNotes->tags = {"Blank"}; p->addModel(modelNotes); } diff --git a/src/app/MenuBar.cpp b/src/app/MenuBar.cpp index 9c1dc89d..ba00b127 100644 --- a/src/app/MenuBar.cpp +++ b/src/app/MenuBar.cpp @@ -188,7 +188,7 @@ struct EditButton : MenuButton { menu->addChild(redoItem); DisconnectCablesItem *disconnectCablesItem = new DisconnectCablesItem; - disconnectCablesItem->text = "Disconnect cables"; + disconnectCablesItem->text = "Clear cables"; menu->addChild(disconnectCablesItem); } }; @@ -459,6 +459,7 @@ struct AccountEmailField : ui::TextField { struct AccountPasswordField : ui::PasswordField { ui::MenuItem *logInItem; + void onSelectKey(const event::SelectKey &e) override { if (e.action == GLFW_PRESS && (e.key == GLFW_KEY_ENTER || e.key == GLFW_KEY_KP_ENTER)) { logInItem->doAction(); @@ -489,6 +490,7 @@ struct LogInItem : ui::MenuItem { disabled = isLoggingIn; text = "Log in"; rightText = plugin::loginStatus; + MenuItem::step(); } }; @@ -498,9 +500,57 @@ struct SyncItem : ui::MenuItem { plugin::syncUpdates(); }); t.detach(); + e.consume(NULL); + } +}; + +struct PluginSyncItem : ui::MenuItem { + plugin::Update *update; + + void setUpdate(plugin::Update *update) { + this->update = update; + text = update->pluginSlug; + plugin::Plugin *p = plugin::getPlugin(update->pluginSlug); + if (p) { + rightText += "v" + p->version + " → "; + } + rightText += "v" + update->version; + } + + ui::Menu *createChildMenu() override { + if (update->changelogUrl != "") { + ui::Menu *menu = new ui::Menu; + + UrlItem *changelogUrl = new UrlItem; + changelogUrl->text = "Changelog"; + changelogUrl->url = update->changelogUrl; + menu->addChild(changelogUrl); + + return menu; + } + return NULL; + } + + void step() override { + if (update->progress >= 1) { + rightText = CHECKMARK_STRING; + } + else if (update->progress > 0) { + rightText = string::f("%.0f%%", update->progress * 100.f); + } + MenuItem::step(); + } + + void onAction(const event::Action &e) override { + std::thread t([=]() { + plugin::syncUpdate(update); + }); + t.detach(); + e.consume(NULL); } }; + #if 0 struct SyncButton : ui::Button { bool checked = false; @@ -544,46 +594,48 @@ struct LogOutItem : ui::MenuItem { } }; -struct DownloadQuantity : Quantity { - float getValue() override { - return plugin::downloadProgress; - } - - float getDisplayValue() override { - return getValue() * 100.f; - } - - int getDisplayPrecision() override {return 0;} - - std::string getLabel() override { - return "Downloading " + plugin::downloadName; - } - - std::string getUnit() override {return "%";} -}; - struct PluginsMenu : ui::Menu { - int state = 0; + bool loggedIn = false; PluginsMenu() { refresh(); } void step() override { + if (!loggedIn && plugin::isLoggedIn()) + refresh(); Menu::step(); } void refresh() { clearChildren(); - { - ui::MenuLabel *disabledLable = new ui::MenuLabel; - disabledLable->text = "Server not yet available"; - addChild(disabledLable); - return; + if (!plugin::isLoggedIn()) { + UrlItem *registerItem = new UrlItem; + registerItem->text = "Register VCV account"; + registerItem->url = "https://vcvrack.com/"; + addChild(registerItem); + + AccountEmailField *emailField = new AccountEmailField; + emailField->placeholder = "Email"; + emailField->box.size.x = 240.0; + addChild(emailField); + + AccountPasswordField *passwordField = new AccountPasswordField; + passwordField->placeholder = "Password"; + passwordField->box.size.x = 240.0; + emailField->passwordField = passwordField; + addChild(passwordField); + + LogInItem *logInItem = new LogInItem; + logInItem->emailField = emailField; + logInItem->passwordField = passwordField; + passwordField->logInItem = logInItem; + addChild(logInItem); } + else { + loggedIn = true; - if (plugin::isLoggedIn()) { UrlItem *manageItem = new UrlItem; manageItem->text = "Manage"; manageItem->url = "https://vcvrack.com/plugins.html"; @@ -602,49 +654,20 @@ struct PluginsMenu : ui::Menu { addChild(new ui::MenuEntry); ui::MenuLabel *updatesLabel = new ui::MenuLabel; - updatesLabel->text = "Updates (click for changelog)"; + updatesLabel->text = "Updates"; addChild(updatesLabel); - for (const plugin::Update &update : plugin::updates) { - UrlItem *updateItem = new UrlItem; - updateItem->text = update.pluginSlug; - plugin::Plugin *p = plugin::getPlugin(update.pluginSlug); - if (p) { - updateItem->rightText += "v" + p->version + " → "; - } - updateItem->rightText += "v" + update.version; - updateItem->url = update.changelogUrl; - updateItem->disabled = update.changelogUrl.empty(); + for (plugin::Update &update : plugin::updates) { + PluginSyncItem *updateItem = new PluginSyncItem; + updateItem->setUpdate(&update); addChild(updateItem); } } } - else { - UrlItem *registerItem = new UrlItem; - registerItem->text = "Register VCV account"; - registerItem->url = "https://vcvrack.com/"; - addChild(registerItem); - - AccountEmailField *emailField = new AccountEmailField; - emailField->placeholder = "Email"; - emailField->box.size.x = 220.0; - addChild(emailField); - - AccountPasswordField *passwordField = new AccountPasswordField; - passwordField->placeholder = "Password"; - passwordField->box.size.x = 220.0; - emailField->passwordField = passwordField; - addChild(passwordField); - - LogInItem *logInItem = new LogInItem; - logInItem->emailField = emailField; - logInItem->passwordField = passwordField; - passwordField->logInItem = logInItem; - addChild(logInItem); - } } }; + struct PluginsButton : MenuButton { NotificationIcon *notification; @@ -704,7 +727,7 @@ struct HelpButton : MenuButton { if (hasUpdate()) { UrlItem *updateItem = new UrlItem; updateItem->text = "Update " + APP_NAME; - updateItem->rightText = APP_VERSION + " → " + APP_NEW_VERSION; + updateItem->rightText = APP_VERSION + " → " + APP_VERSION_UPDATE; updateItem->url = "https://vcvrack.com/"; menu->addChild(updateItem); } @@ -721,7 +744,7 @@ struct HelpButton : MenuButton { } bool hasUpdate() { - return !APP_NEW_VERSION.empty() && APP_NEW_VERSION != APP_VERSION; + return !APP_VERSION_UPDATE.empty() && APP_VERSION_UPDATE != APP_VERSION; } }; diff --git a/src/app/common.cpp b/src/app/common.cpp index afe69efd..118fcf80 100644 --- a/src/app/common.cpp +++ b/src/app/common.cpp @@ -10,8 +10,10 @@ namespace app { std::string APP_NAME = "VCV Rack"; std::string APP_VERSION = TOSTRING(VERSION); -std::string APP_NEW_VERSION; +std::string APP_VERSION_UPDATE; std::string API_URL = "https://api.vcvrack.com"; +std::string API_VERSION = "1"; + static void checkVersion() { std::string versionUrl = app::API_URL + "/version"; @@ -26,7 +28,7 @@ static void checkVersion() { json_t *versionJ = json_object_get(versionResJ, "version"); if (versionJ) - APP_NEW_VERSION = json_string_value(versionJ); + APP_VERSION_UPDATE = json_string_value(versionJ); } void init() { diff --git a/src/plugin.cpp b/src/plugin.cpp index c4e80269..f3dfca4a 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -22,11 +22,11 @@ #include #if defined ARCH_WIN - #include - #include - #define mkdir(_dir, _perms) _mkdir(_dir) +#include +#include +#define mkdir(_dir, _perms) _mkdir(_dir) #else - #include +#include #endif #include #include @@ -40,42 +40,22 @@ namespace plugin { // private API //////////////////// -static bool loadPlugin(std::string path) { - // Load plugin.json - std::string metadataFilename = path + "/plugin.json"; - FILE *file = fopen(metadataFilename.c_str(), "r"); - if (!file) { - WARN("Plugin metadata file %s does not exist", metadataFilename.c_str()); - return false; - } - DEFER({ - fclose(file); - }); - - json_error_t error; - json_t *rootJ = json_loadf(file, 0, &error); - if (!rootJ) { - WARN("JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text); - return false; - } - DEFER({ - json_decref(rootJ); - }); +typedef void (*InitCallback)(Plugin*); +static InitCallback loadLibrary(Plugin *plugin) { // Load plugin library std::string libraryFilename; #if defined ARCH_LIN - libraryFilename = path + "/" + "plugin.so"; + libraryFilename = plugin->path + "/" + "plugin.so"; #elif defined ARCH_WIN - libraryFilename = path + "/" + "plugin.dll"; + libraryFilename = plugin->path + "/" + "plugin.dll"; #elif ARCH_MAC - libraryFilename = path + "/" + "plugin.dylib"; + libraryFilename = plugin->path + "/" + "plugin.dylib"; #endif // Check file existence if (!system::isFile(libraryFilename)) { - WARN("Plugin file %s does not exist", libraryFilename.c_str()); - return false; + throw UserException(string::f("Library %s does not exist", libraryFilename.c_str())); } // Load dynamic/shared library @@ -85,19 +65,17 @@ static bool loadPlugin(std::string path) { SetErrorMode(0); if (!handle) { int error = GetLastError(); - WARN("Failed to load library %s: code %d", libraryFilename.c_str(), error); - return false; + throw UserException(string::f("Failed to load library %s: code %d", libraryFilename.c_str(), error)); } #else void *handle = dlopen(libraryFilename.c_str(), RTLD_NOW); if (!handle) { - WARN("Failed to load library %s: %s", libraryFilename.c_str(), dlerror()); - return false; + throw UserException(string::f("Failed to load library %s: %s", libraryFilename.c_str(), dlerror())); } #endif + plugin->handle = handle; - // Call plugin's init() function - typedef void (*InitCallback)(Plugin *); + // Get plugin's init() function InitCallback initCallback; #if defined ARCH_WIN initCallback = (InitCallback) GetProcAddress(handle, "init"); @@ -105,95 +83,103 @@ static bool loadPlugin(std::string path) { initCallback = (InitCallback) dlsym(handle, "init"); #endif if (!initCallback) { - WARN("Failed to read init() symbol in %s", libraryFilename.c_str()); - return false; + throw UserException(string::f("Failed to read init() symbol in %s", libraryFilename.c_str())); } - // Construct and initialize Plugin instance - Plugin *plugin = new Plugin; - plugin->path = path; - plugin->handle = handle; - initCallback(plugin); - plugin->fromJson(rootJ); - - // Check slug - if (!isSlugValid(plugin->slug)) { - WARN("Plugin slug \"%s\" is invalid", plugin->slug.c_str()); - // TODO Fix memory leak with `plugin` - return false; - } + return initCallback; +} - // Reject plugin if slug already exists - Plugin *oldPlugin = getPlugin(plugin->slug); - if (oldPlugin) { - WARN("Plugin \"%s\" is already loaded, not attempting to load it again", plugin->slug.c_str()); - // TODO Fix memory leak with `plugin` - return false; - } - // Add plugin to list - plugins.push_back(plugin); - INFO("Loaded plugin %s v%s from %s", plugin->slug.c_str(), plugin->version.c_str(), libraryFilename.c_str()); - - // Normalize tags - for (Model *model : plugin->models) { - std::vector normalizedTags; - for (const std::string &tag : model->tags) { - std::string normalizedTag = normalizeTag(tag); - if (!normalizedTag.empty()) - normalizedTags.push_back(normalizedTag); +/** If path is blank, loads Core */ +static Plugin *loadPlugin(std::string path) { + Plugin *plugin = new Plugin; + try { + plugin->path = path; + + // Load plugin.json + std::string metadataFilename; + if (path == "") + metadataFilename = asset::system("Core.json"); + else + metadataFilename = path + "/plugin.json"; + FILE *file = fopen(metadataFilename.c_str(), "r"); + if (!file) { + throw UserException(string::f("Metadata file %s does not exist", metadataFilename.c_str())); } - model->tags = normalizedTags; - } + DEFER({ + fclose(file); + }); + + json_error_t error; + json_t *rootJ = json_loadf(file, 0, &error); + if (!rootJ) { + throw UserException(string::f("JSON parsing error at %s %d:%d %s", metadataFilename.c_str(), error.line, error.column, error.text)); + } + DEFER({ + json_decref(rootJ); + }); + + // Call init callback + InitCallback initCallback; + if (path == "") { + initCallback = ::init; + } + else { + initCallback = loadLibrary(plugin); + } + initCallback(plugin); - // Search for presets - for (Model *model : plugin->models) { - std::string presetDir = asset::plugin(plugin, "presets/" + model->slug); - for (const std::string &presetPath : system::getEntries(presetDir)) { - model->presetPaths.push_back(presetPath); + // Load manifest + plugin->fromJson(rootJ); + + // Check slug + if (!isSlugValid(plugin->slug)) { + throw UserException(string::f("Plugin slug \"%s\" is invalid", plugin->slug.c_str())); } - } - return true; -} + // Reject plugin if slug already exists + Plugin *oldPlugin = getPlugin(plugin->slug); + if (oldPlugin) { + throw UserException(string::f("Plugin %s is already loaded, not attempting to load it again", plugin->slug.c_str())); + } -static bool syncUpdate(const Update &update) { -#if defined ARCH_WIN - std::string arch = "win"; -#elif ARCH_MAC - std::string arch = "mac"; -#elif defined ARCH_LIN - std::string arch = "lin"; -#endif + // Normalize tags + for (Model *model : plugin->models) { + std::vector normalizedTags; + for (const std::string &tag : model->tags) { + std::string normalizedTag = normalizeTag(tag); + if (!normalizedTag.empty()) + normalizedTags.push_back(normalizedTag); + } + model->tags = normalizedTags; + } - std::string downloadUrl = app::API_URL + "/download"; - downloadUrl += "?token=" + network::encodeUrl(settings::token); - downloadUrl += "&slug=" + network::encodeUrl(update.pluginSlug); - downloadUrl += "&version=" + network::encodeUrl(update.version); - downloadUrl += "&arch=" + network::encodeUrl(arch); + // Search for presets + for (Model *model : plugin->models) { + std::string presetDir = asset::plugin(plugin, "presets/" + model->slug); + for (const std::string &presetPath : system::getEntries(presetDir)) { + model->presetPaths.push_back(presetPath); + } + } - // downloadName = name; - downloadProgress = 0.0; - INFO("Downloading plugin %s %s %s", update.pluginSlug.c_str(), update.version.c_str(), arch.c_str()); + INFO("Loaded plugin %s v%s from %s", plugin->slug.c_str(), plugin->version.c_str(), path.c_str()); + plugins.push_back(plugin); - // Download zip - std::string pluginDest = asset::user("plugins/" + update.pluginSlug + ".zip"); - if (!network::requestDownload(downloadUrl, pluginDest, &downloadProgress)) { - WARN("Plugin %s download was unsuccessful", update.pluginSlug.c_str()); - return false; + return plugin; + } + catch (UserException &e) { + WARN("Could not load plugin %s: %s", path.c_str(), e.what()); + delete plugin; + return NULL; } - - // downloadName = ""; - return true; } static void loadPlugins(std::string path) { - std::string message; for (std::string pluginPath : system::getEntries(path)) { if (!system::isDirectory(pluginPath)) continue; if (!loadPlugin(pluginPath)) { - // Ignore bad plugins. They are reported in log.txt. + // Ignore bad plugins. They are reported in the log. } } } @@ -233,7 +219,7 @@ static int extractZipHandle(zip_t *za, const char *dir) { continue; while (1) { - char buffer[1<<15]; + char buffer[1 << 15]; int len = zip_fread(zf, buffer, sizeof(buffer)); if (len <= 0) break; @@ -267,7 +253,7 @@ static int extractZip(const char *filename, const char *path) { return err; } -static void extractPackages(const std::string &path) { +static void extractPackages(std::string path) { std::string message; for (std::string packagePath : system::getEntries(path)) { @@ -296,10 +282,7 @@ static void extractPackages(const std::string &path) { void init() { // Load Core - Plugin *corePlugin = new Plugin; - // This function is defined in Core/plugin.cpp - ::init(corePlugin); - plugins.push_back(corePlugin); + loadPlugin(""); // Get user plugins directory std::string pluginsDir = asset::user("plugins"); @@ -309,7 +292,7 @@ void init() { extractPackages(pluginsDir); loadPlugins(pluginsDir); - // Copy Fundamental package to plugins directory if Fundamental is not loaded + // If Fundamental wasn't loaded, copy the bundled Fundamental package and load it std::string fundamentalSrc = asset::system("Fundamental.zip"); std::string fundamentalDir = asset::user("plugins/Fundamental"); if (!settings::devMode && !getPlugin("Fundamental") && system::isFile(fundamentalSrc)) { @@ -318,9 +301,8 @@ void init() { loadPlugin(fundamentalDir); } - // TEMP // Sync in a detached thread - std::thread t([]{ + std::thread t([] { queryUpdates(); }); t.detach(); @@ -387,9 +369,9 @@ void queryUpdates() { updates.clear(); // Get user's plugins list + std::string pluginsUrl = app::API_URL + "/plugins"; json_t *pluginsReqJ = json_object(); json_object_set(pluginsReqJ, "token", json_string(settings::token.c_str())); - std::string pluginsUrl = app::API_URL + "/plugins"; json_t *pluginsResJ = network::requestJson(network::METHOD_GET, pluginsUrl, pluginsReqJ); json_decref(pluginsReqJ); if (!pluginsResJ) { @@ -406,11 +388,14 @@ void queryUpdates() { return; } - // Get community manifests - std::string manifestsUrl = app::API_URL + "/community/manifests"; - json_t *manifestsResJ = network::requestJson(network::METHOD_GET, manifestsUrl, NULL); + // Get library manifests + std::string manifestsUrl = app::API_URL + "/library/manifests"; + json_t *manifestsReq = json_object(); + json_object_set(manifestsReq, "version", json_string(app::API_VERSION.c_str())); + json_t *manifestsResJ = network::requestJson(network::METHOD_GET, manifestsUrl, manifestsReq); + json_decref(manifestsReq); if (!manifestsResJ) { - WARN("Request for community manifests failed"); + WARN("Request for library manifests failed"); return; } DEFER({ @@ -434,7 +419,7 @@ void queryUpdates() { // Get version // TODO Change this to "version" when API changes - json_t *versionJ = json_object_get(manifestJ, "latestVersion"); + json_t *versionJ = json_object_get(manifestJ, "version"); if (!versionJ) { WARN("Plugin %s has no version in manifest", update.pluginSlug.c_str()); continue; @@ -464,15 +449,37 @@ void queryUpdates() { } } +void syncUpdate(Update *update) { +#if defined ARCH_WIN + std::string arch = "win"; +#elif ARCH_MAC + std::string arch = "mac"; +#elif defined ARCH_LIN + std::string arch = "lin"; +#endif + + std::string downloadUrl = app::API_URL + "/download"; + downloadUrl += "?token=" + network::encodeUrl(settings::token); + downloadUrl += "&slug=" + network::encodeUrl(update->pluginSlug); + downloadUrl += "&version=" + network::encodeUrl(update->version); + downloadUrl += "&arch=" + network::encodeUrl(arch); + + INFO("Downloading plugin %s %s %s", update->pluginSlug.c_str(), update->version.c_str(), arch.c_str()); + + // Download zip + std::string pluginDest = asset::user("plugins/" + update->pluginSlug + ".zip"); + if (!network::requestDownload(downloadUrl, pluginDest, &update->progress)) { + WARN("Plugin %s download was unsuccessful", update->pluginSlug.c_str()); + return; + } +} + void syncUpdates() { if (settings::token.empty()) return; - downloadProgress = 0.0; - downloadName = "Updating plugins..."; - - for (const Update &update : updates) { - syncUpdate(update); + for (Update &update : updates) { + syncUpdate(&update); } } @@ -626,8 +633,6 @@ std::vector plugins; std::string loginStatus; std::vector updates; -float downloadProgress = 0.f; -std::string downloadName; } // namespace plugin diff --git a/src/plugin/Plugin.cpp b/src/plugin/Plugin.cpp index f5c10ca5..35541173 100644 --- a/src/plugin/Plugin.cpp +++ b/src/plugin/Plugin.cpp @@ -1,6 +1,7 @@ #include #include #include +#include namespace rack { @@ -18,8 +19,7 @@ void Plugin::addModel(Model *model) { assert(!model->plugin); // Check model slug if (!isSlugValid(model->slug)) { - WARN("Module slug \"%s\" is invalid", model->slug.c_str()); - return; + throw UserException(string::f("Module slug \"%s\" is invalid", model->slug.c_str())); } model->plugin = this; models.push_back(model); @@ -99,8 +99,7 @@ void Plugin::fromJson(json_t *rootJ) { Model *model = getModel(modelSlug); if (!model) { - WARN("plugin.json of \"%s\" contains module \"%s\" but it is not defined in the plugin", slug.c_str(), modelSlug.c_str()); - continue; + throw UserException(string::f("plugin.json of \"%s\" contains module \"%s\" but it is not defined in the plugin", slug.c_str(), modelSlug.c_str())); } model->fromJson(moduleJ); diff --git a/src/window.cpp b/src/window.cpp index 056e89c2..e2ef3f37 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -164,7 +164,7 @@ static void keyCallback(GLFWwindow *win, int key, int scancode, int action, int return; // Keyboard MIDI driver - if ((mods & RACK_MOD_MASK) == 0 && action == GLFW_PRESS) { + if (action == GLFW_PRESS && (mods & RACK_MOD_MASK) == 0) { keyboard::press(key); } if (action == GLFW_RELEASE) {