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