| @@ -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_NAME; | ||||
| extern std::string APP_VERSION; | 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_URL; | ||||
| extern std::string API_VERSION; | |||||
| static const float SVG_DPI = 75.0; | static const float SVG_DPI = 75.0; | ||||
| static const float MM_PER_IN = 25.4; | static const float MM_PER_IN = 25.4; | ||||
| @@ -14,13 +14,21 @@ namespace rack { | |||||
| namespace plugin { | namespace plugin { | ||||
| struct Update { | |||||
| std::string pluginSlug; | |||||
| std::string version; | |||||
| std::string changelogUrl; | |||||
| float progress = 0.f; | |||||
| }; | |||||
| void init(); | void init(); | ||||
| void destroy(); | void destroy(); | ||||
| void logIn(const std::string &email, const std::string &password); | void logIn(const std::string &email, const std::string &password); | ||||
| void logOut(); | void logOut(); | ||||
| void queryUpdates(); | void queryUpdates(); | ||||
| void syncUpdate(Update *update); | |||||
| void syncUpdates(); | void syncUpdates(); | ||||
| void cancelDownload(); | |||||
| bool isLoggedIn(); | bool isLoggedIn(); | ||||
| Plugin *getPlugin(const std::string &pluginSlug); | Plugin *getPlugin(const std::string &pluginSlug); | ||||
| Model *getModel(const std::string &pluginSlug, const std::string &modelSlug); | 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); | 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 const std::set<std::string> allowedTags; | ||||
| extern std::vector<Plugin*> plugins; | extern std::vector<Plugin*> plugins; | ||||
| extern std::string loginStatus; | extern std::string loginStatus; | ||||
| extern std::vector<Update> updates; | extern std::vector<Update> updates; | ||||
| extern float downloadProgress; | |||||
| extern std::string downloadName; | |||||
| } // namespace plugin | } // namespace plugin | ||||
| @@ -2,70 +2,15 @@ | |||||
| void init(rack::Plugin *p) { | 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); | p->addModel(modelAudioInterface); | ||||
| modelAudioInterface16->name = "Audio-16"; | |||||
| modelAudioInterface16->description = "Sends audio and CV to/from an audio device"; | |||||
| modelAudioInterface16->tags = {"External"}; | |||||
| p->addModel(modelAudioInterface16); | 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); | 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); | 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); | p->addModel(modelMIDI_Gate); | ||||
| modelMIDI_Map->name = "MIDI-Map"; | |||||
| modelMIDI_Map->description = ""; | |||||
| modelMIDI_Map->tags = {"External", "MIDI"}; | |||||
| p->addModel(modelMIDI_Map); | 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); | 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); | 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); | p->addModel(modelCV_Gate); | ||||
| modelBlank->name = "Blank"; | |||||
| modelBlank->description = "A resizable blank panel"; | |||||
| modelBlank->tags = {"Blank"}; | |||||
| p->addModel(modelBlank); | p->addModel(modelBlank); | ||||
| modelNotes->name = "Notes"; | |||||
| modelNotes->description = "Write text for patch notes or artist attribution"; | |||||
| modelNotes->tags = {"Blank"}; | |||||
| p->addModel(modelNotes); | p->addModel(modelNotes); | ||||
| } | } | ||||
| @@ -188,7 +188,7 @@ struct EditButton : MenuButton { | |||||
| menu->addChild(redoItem); | menu->addChild(redoItem); | ||||
| DisconnectCablesItem *disconnectCablesItem = new DisconnectCablesItem; | DisconnectCablesItem *disconnectCablesItem = new DisconnectCablesItem; | ||||
| disconnectCablesItem->text = "Disconnect cables"; | |||||
| disconnectCablesItem->text = "Clear cables"; | |||||
| menu->addChild(disconnectCablesItem); | menu->addChild(disconnectCablesItem); | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -459,6 +459,7 @@ struct AccountEmailField : ui::TextField { | |||||
| struct AccountPasswordField : ui::PasswordField { | struct AccountPasswordField : ui::PasswordField { | ||||
| ui::MenuItem *logInItem; | ui::MenuItem *logInItem; | ||||
| void onSelectKey(const event::SelectKey &e) override { | void onSelectKey(const event::SelectKey &e) override { | ||||
| if (e.action == GLFW_PRESS && (e.key == GLFW_KEY_ENTER || e.key == GLFW_KEY_KP_ENTER)) { | if (e.action == GLFW_PRESS && (e.key == GLFW_KEY_ENTER || e.key == GLFW_KEY_KP_ENTER)) { | ||||
| logInItem->doAction(); | logInItem->doAction(); | ||||
| @@ -489,6 +490,7 @@ struct LogInItem : ui::MenuItem { | |||||
| disabled = isLoggingIn; | disabled = isLoggingIn; | ||||
| text = "Log in"; | text = "Log in"; | ||||
| rightText = plugin::loginStatus; | rightText = plugin::loginStatus; | ||||
| MenuItem::step(); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -498,9 +500,57 @@ struct SyncItem : ui::MenuItem { | |||||
| plugin::syncUpdates(); | plugin::syncUpdates(); | ||||
| }); | }); | ||||
| t.detach(); | 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 | #if 0 | ||||
| struct SyncButton : ui::Button { | struct SyncButton : ui::Button { | ||||
| bool checked = false; | 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 { | struct PluginsMenu : ui::Menu { | ||||
| int state = 0; | |||||
| bool loggedIn = false; | |||||
| PluginsMenu() { | PluginsMenu() { | ||||
| refresh(); | refresh(); | ||||
| } | } | ||||
| void step() override { | void step() override { | ||||
| if (!loggedIn && plugin::isLoggedIn()) | |||||
| refresh(); | |||||
| Menu::step(); | Menu::step(); | ||||
| } | } | ||||
| void refresh() { | void refresh() { | ||||
| clearChildren(); | 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; | UrlItem *manageItem = new UrlItem; | ||||
| manageItem->text = "Manage"; | manageItem->text = "Manage"; | ||||
| manageItem->url = "https://vcvrack.com/plugins.html"; | manageItem->url = "https://vcvrack.com/plugins.html"; | ||||
| @@ -602,49 +654,20 @@ struct PluginsMenu : ui::Menu { | |||||
| addChild(new ui::MenuEntry); | addChild(new ui::MenuEntry); | ||||
| ui::MenuLabel *updatesLabel = new ui::MenuLabel; | ui::MenuLabel *updatesLabel = new ui::MenuLabel; | ||||
| updatesLabel->text = "Updates (click for changelog)"; | |||||
| updatesLabel->text = "Updates"; | |||||
| addChild(updatesLabel); | 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); | 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 { | struct PluginsButton : MenuButton { | ||||
| NotificationIcon *notification; | NotificationIcon *notification; | ||||
| @@ -704,7 +727,7 @@ struct HelpButton : MenuButton { | |||||
| if (hasUpdate()) { | if (hasUpdate()) { | ||||
| UrlItem *updateItem = new UrlItem; | UrlItem *updateItem = new UrlItem; | ||||
| updateItem->text = "Update " + APP_NAME; | updateItem->text = "Update " + APP_NAME; | ||||
| updateItem->rightText = APP_VERSION + " → " + APP_NEW_VERSION; | |||||
| updateItem->rightText = APP_VERSION + " → " + APP_VERSION_UPDATE; | |||||
| updateItem->url = "https://vcvrack.com/"; | updateItem->url = "https://vcvrack.com/"; | ||||
| menu->addChild(updateItem); | menu->addChild(updateItem); | ||||
| } | } | ||||
| @@ -721,7 +744,7 @@ struct HelpButton : MenuButton { | |||||
| } | } | ||||
| bool hasUpdate() { | 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_NAME = "VCV Rack"; | ||||
| std::string APP_VERSION = TOSTRING(VERSION); | 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_URL = "https://api.vcvrack.com"; | ||||
| std::string API_VERSION = "1"; | |||||
| static void checkVersion() { | static void checkVersion() { | ||||
| std::string versionUrl = app::API_URL + "/version"; | std::string versionUrl = app::API_URL + "/version"; | ||||
| @@ -26,7 +28,7 @@ static void checkVersion() { | |||||
| json_t *versionJ = json_object_get(versionResJ, "version"); | json_t *versionJ = json_object_get(versionResJ, "version"); | ||||
| if (versionJ) | if (versionJ) | ||||
| APP_NEW_VERSION = json_string_value(versionJ); | |||||
| APP_VERSION_UPDATE = json_string_value(versionJ); | |||||
| } | } | ||||
| void init() { | void init() { | ||||
| @@ -22,11 +22,11 @@ | |||||
| #include <jansson.h> | #include <jansson.h> | ||||
| #if defined ARCH_WIN | #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 | #else | ||||
| #include <dlfcn.h> | |||||
| #include <dlfcn.h> | |||||
| #endif | #endif | ||||
| #include <dirent.h> | #include <dirent.h> | ||||
| #include <osdialog.h> | #include <osdialog.h> | ||||
| @@ -40,42 +40,22 @@ namespace plugin { | |||||
| // private API | // 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 | // Load plugin library | ||||
| std::string libraryFilename; | std::string libraryFilename; | ||||
| #if defined ARCH_LIN | #if defined ARCH_LIN | ||||
| libraryFilename = path + "/" + "plugin.so"; | |||||
| libraryFilename = plugin->path + "/" + "plugin.so"; | |||||
| #elif defined ARCH_WIN | #elif defined ARCH_WIN | ||||
| libraryFilename = path + "/" + "plugin.dll"; | |||||
| libraryFilename = plugin->path + "/" + "plugin.dll"; | |||||
| #elif ARCH_MAC | #elif ARCH_MAC | ||||
| libraryFilename = path + "/" + "plugin.dylib"; | |||||
| libraryFilename = plugin->path + "/" + "plugin.dylib"; | |||||
| #endif | #endif | ||||
| // Check file existence | // Check file existence | ||||
| if (!system::isFile(libraryFilename)) { | 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 | // Load dynamic/shared library | ||||
| @@ -85,19 +65,17 @@ static bool loadPlugin(std::string path) { | |||||
| SetErrorMode(0); | SetErrorMode(0); | ||||
| if (!handle) { | if (!handle) { | ||||
| int error = GetLastError(); | 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 | #else | ||||
| void *handle = dlopen(libraryFilename.c_str(), RTLD_NOW); | void *handle = dlopen(libraryFilename.c_str(), RTLD_NOW); | ||||
| if (!handle) { | 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 | #endif | ||||
| plugin->handle = handle; | |||||
| // Call plugin's init() function | |||||
| typedef void (*InitCallback)(Plugin *); | |||||
| // Get plugin's init() function | |||||
| InitCallback initCallback; | InitCallback initCallback; | ||||
| #if defined ARCH_WIN | #if defined ARCH_WIN | ||||
| initCallback = (InitCallback) GetProcAddress(handle, "init"); | initCallback = (InitCallback) GetProcAddress(handle, "init"); | ||||
| @@ -105,95 +83,103 @@ static bool loadPlugin(std::string path) { | |||||
| initCallback = (InitCallback) dlsym(handle, "init"); | initCallback = (InitCallback) dlsym(handle, "init"); | ||||
| #endif | #endif | ||||
| if (!initCallback) { | 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) { | static void loadPlugins(std::string path) { | ||||
| std::string message; | |||||
| for (std::string pluginPath : system::getEntries(path)) { | for (std::string pluginPath : system::getEntries(path)) { | ||||
| if (!system::isDirectory(pluginPath)) | if (!system::isDirectory(pluginPath)) | ||||
| continue; | continue; | ||||
| if (!loadPlugin(pluginPath)) { | 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; | continue; | ||||
| while (1) { | while (1) { | ||||
| char buffer[1<<15]; | |||||
| char buffer[1 << 15]; | |||||
| int len = zip_fread(zf, buffer, sizeof(buffer)); | int len = zip_fread(zf, buffer, sizeof(buffer)); | ||||
| if (len <= 0) | if (len <= 0) | ||||
| break; | break; | ||||
| @@ -267,7 +253,7 @@ static int extractZip(const char *filename, const char *path) { | |||||
| return err; | return err; | ||||
| } | } | ||||
| static void extractPackages(const std::string &path) { | |||||
| static void extractPackages(std::string path) { | |||||
| std::string message; | std::string message; | ||||
| for (std::string packagePath : system::getEntries(path)) { | for (std::string packagePath : system::getEntries(path)) { | ||||
| @@ -296,10 +282,7 @@ static void extractPackages(const std::string &path) { | |||||
| void init() { | void init() { | ||||
| // Load Core | // 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 | // Get user plugins directory | ||||
| std::string pluginsDir = asset::user("plugins"); | std::string pluginsDir = asset::user("plugins"); | ||||
| @@ -309,7 +292,7 @@ void init() { | |||||
| extractPackages(pluginsDir); | extractPackages(pluginsDir); | ||||
| loadPlugins(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 fundamentalSrc = asset::system("Fundamental.zip"); | ||||
| std::string fundamentalDir = asset::user("plugins/Fundamental"); | std::string fundamentalDir = asset::user("plugins/Fundamental"); | ||||
| if (!settings::devMode && !getPlugin("Fundamental") && system::isFile(fundamentalSrc)) { | if (!settings::devMode && !getPlugin("Fundamental") && system::isFile(fundamentalSrc)) { | ||||
| @@ -318,9 +301,8 @@ void init() { | |||||
| loadPlugin(fundamentalDir); | loadPlugin(fundamentalDir); | ||||
| } | } | ||||
| // TEMP | |||||
| // Sync in a detached thread | // Sync in a detached thread | ||||
| std::thread t([]{ | |||||
| std::thread t([] { | |||||
| queryUpdates(); | queryUpdates(); | ||||
| }); | }); | ||||
| t.detach(); | t.detach(); | ||||
| @@ -387,9 +369,9 @@ void queryUpdates() { | |||||
| updates.clear(); | updates.clear(); | ||||
| // Get user's plugins list | // Get user's plugins list | ||||
| std::string pluginsUrl = app::API_URL + "/plugins"; | |||||
| json_t *pluginsReqJ = json_object(); | json_t *pluginsReqJ = json_object(); | ||||
| json_object_set(pluginsReqJ, "token", json_string(settings::token.c_str())); | 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_t *pluginsResJ = network::requestJson(network::METHOD_GET, pluginsUrl, pluginsReqJ); | ||||
| json_decref(pluginsReqJ); | json_decref(pluginsReqJ); | ||||
| if (!pluginsResJ) { | if (!pluginsResJ) { | ||||
| @@ -406,11 +388,14 @@ void queryUpdates() { | |||||
| return; | 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) { | if (!manifestsResJ) { | ||||
| WARN("Request for community manifests failed"); | |||||
| WARN("Request for library manifests failed"); | |||||
| return; | return; | ||||
| } | } | ||||
| DEFER({ | DEFER({ | ||||
| @@ -434,7 +419,7 @@ void queryUpdates() { | |||||
| // Get version | // Get version | ||||
| // TODO Change this to "version" when API changes | // 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) { | if (!versionJ) { | ||||
| WARN("Plugin %s has no version in manifest", update.pluginSlug.c_str()); | WARN("Plugin %s has no version in manifest", update.pluginSlug.c_str()); | ||||
| continue; | 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() { | void syncUpdates() { | ||||
| if (settings::token.empty()) | if (settings::token.empty()) | ||||
| return; | 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::string loginStatus; | ||||
| std::vector<Update> updates; | std::vector<Update> updates; | ||||
| float downloadProgress = 0.f; | |||||
| std::string downloadName; | |||||
| } // namespace plugin | } // namespace plugin | ||||
| @@ -1,6 +1,7 @@ | |||||
| #include <plugin/Plugin.hpp> | #include <plugin/Plugin.hpp> | ||||
| #include <plugin/Model.hpp> | #include <plugin/Model.hpp> | ||||
| #include <plugin.hpp> | #include <plugin.hpp> | ||||
| #include <string.hpp> | |||||
| namespace rack { | namespace rack { | ||||
| @@ -18,8 +19,7 @@ void Plugin::addModel(Model *model) { | |||||
| assert(!model->plugin); | assert(!model->plugin); | ||||
| // Check model slug | // Check model slug | ||||
| if (!isSlugValid(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; | model->plugin = this; | ||||
| models.push_back(model); | models.push_back(model); | ||||
| @@ -99,8 +99,7 @@ void Plugin::fromJson(json_t *rootJ) { | |||||
| Model *model = getModel(modelSlug); | Model *model = getModel(modelSlug); | ||||
| if (!model) { | 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); | model->fromJson(moduleJ); | ||||
| @@ -164,7 +164,7 @@ static void keyCallback(GLFWwindow *win, int key, int scancode, int action, int | |||||
| return; | return; | ||||
| // Keyboard MIDI driver | // Keyboard MIDI driver | ||||
| if ((mods & RACK_MOD_MASK) == 0 && action == GLFW_PRESS) { | |||||
| if (action == GLFW_PRESS && (mods & RACK_MOD_MASK) == 0) { | |||||
| keyboard::press(key); | keyboard::press(key); | ||||
| } | } | ||||
| if (action == GLFW_RELEASE) { | if (action == GLFW_RELEASE) { | ||||