| @@ -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" | |||
| ] | |||
| } | |||
| ] | |||
| } | |||
| @@ -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; | |||
| @@ -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<std::string> allowedTags; | |||
| extern std::vector<Plugin*> plugins; | |||
| extern std::string loginStatus; | |||
| extern std::vector<Update> updates; | |||
| extern float downloadProgress; | |||
| extern std::string downloadName; | |||
| } // namespace plugin | |||
| @@ -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); | |||
| } | |||
| @@ -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; | |||
| } | |||
| }; | |||
| @@ -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() { | |||
| @@ -22,11 +22,11 @@ | |||
| #include <jansson.h> | |||
| #if defined ARCH_WIN | |||
| #include <windows.h> | |||
| #include <direct.h> | |||
| #define mkdir(_dir, _perms) _mkdir(_dir) | |||
| #include <windows.h> | |||
| #include <direct.h> | |||
| #define mkdir(_dir, _perms) _mkdir(_dir) | |||
| #else | |||
| #include <dlfcn.h> | |||
| #include <dlfcn.h> | |||
| #endif | |||
| #include <dirent.h> | |||
| #include <osdialog.h> | |||
| @@ -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<std::string> 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<std::string> 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<Plugin*> plugins; | |||
| std::string loginStatus; | |||
| std::vector<Update> updates; | |||
| float downloadProgress = 0.f; | |||
| std::string downloadName; | |||
| } // namespace plugin | |||
| @@ -1,6 +1,7 @@ | |||
| #include <plugin/Plugin.hpp> | |||
| #include <plugin/Model.hpp> | |||
| #include <plugin.hpp> | |||
| #include <string.hpp> | |||
| 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); | |||
| @@ -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) { | |||