@@ -8,12 +8,53 @@ | |||||
namespace rack { | namespace rack { | ||||
struct RegisterButton : Button { | |||||
void onAction(EventAction &e) override { | |||||
std::thread t([&]() { | |||||
systemOpenBrowser("https://vcvrack.com/"); | |||||
}); | |||||
t.detach(); | |||||
} | |||||
}; | |||||
struct LogInButton : Button { | |||||
TextField *emailField; | |||||
TextField *passwordField; | |||||
void onAction(EventAction &e) override { | |||||
std::thread t(pluginLogIn, emailField->text, passwordField->text); | |||||
t.detach(); | |||||
passwordField->text = ""; | |||||
} | |||||
}; | |||||
struct StatusLabel : Label { | |||||
void step() override { | |||||
text = pluginGetLoginStatus(); | |||||
} | |||||
}; | |||||
struct ManageButton : Button { | |||||
void onAction(EventAction &e) override { | |||||
std::thread t([&]() { | |||||
systemOpenBrowser("https://vcvrack.com/"); | |||||
}); | |||||
t.detach(); | |||||
} | |||||
}; | |||||
struct SyncButton : Button { | struct SyncButton : Button { | ||||
bool checked = false; | bool checked = false; | ||||
/** Updates are available */ | |||||
bool available = false; | bool available = false; | ||||
/** Plugins have been updated */ | |||||
bool completed = false; | bool completed = false; | ||||
void step() override { | void step() override { | ||||
// Check for plugin update on first step() | |||||
if (!checked) { | if (!checked) { | ||||
std::thread t([this]() { | std::thread t([this]() { | ||||
if (pluginSync(true)) | if (pluginSync(true)) | ||||
@@ -22,6 +63,7 @@ struct SyncButton : Button { | |||||
t.detach(); | t.detach(); | ||||
checked = true; | checked = true; | ||||
} | } | ||||
// Display message if we've completed updates | |||||
if (completed) { | if (completed) { | ||||
if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "All plugins have been updated. Close Rack and re-launch it to load new updates.")) { | if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "All plugins have been updated. Close Rack and re-launch it to load new updates.")) { | ||||
windowClose(); | windowClose(); | ||||
@@ -52,112 +94,83 @@ struct SyncButton : Button { | |||||
}; | }; | ||||
struct LogOutButton : Button { | |||||
void onAction(EventAction &e) override { | |||||
pluginLogOut(); | |||||
} | |||||
}; | |||||
struct DownloadProgressBar : ProgressBar { | |||||
void step() override { | |||||
label = "Downloading"; | |||||
std::string name = pluginGetDownloadName(); | |||||
if (name != "") | |||||
label += " " + name; | |||||
setValue(100.0 * pluginGetDownloadProgress()); | |||||
} | |||||
}; | |||||
struct CancelButton : Button { | |||||
void onAction(EventAction &e) override { | |||||
pluginCancelDownload(); | |||||
} | |||||
}; | |||||
PluginManagerWidget::PluginManagerWidget() { | PluginManagerWidget::PluginManagerWidget() { | ||||
box.size.y = BND_WIDGET_HEIGHT; | box.size.y = BND_WIDGET_HEIGHT; | ||||
float margin = 5; | |||||
{ | { | ||||
loginWidget = new Widget(); | |||||
Vec pos = Vec(0, 0); | |||||
struct RegisterButton : Button { | |||||
void onAction(EventAction &e) override { | |||||
std::thread t([&]() { | |||||
systemOpenBrowser("https://vcvrack.com/"); | |||||
}); | |||||
t.detach(); | |||||
} | |||||
}; | |||||
SequentialLayout *layout = Widget::create<SequentialLayout>(Vec(0, 0)); | |||||
layout->spacing = 5; | |||||
loginWidget = layout; | |||||
Button *registerButton = new RegisterButton(); | Button *registerButton = new RegisterButton(); | ||||
registerButton->box.pos = pos; | |||||
registerButton->box.size.x = 75; | registerButton->box.size.x = 75; | ||||
registerButton->text = "Register"; | registerButton->text = "Register"; | ||||
loginWidget->addChild(registerButton); | loginWidget->addChild(registerButton); | ||||
pos.x += registerButton->box.size.x; | |||||
pos.x += margin; | |||||
TextField *emailField = new TextField(); | TextField *emailField = new TextField(); | ||||
emailField->box.pos = pos; | |||||
emailField->box.size.x = 175; | emailField->box.size.x = 175; | ||||
emailField->placeholder = "Email"; | emailField->placeholder = "Email"; | ||||
loginWidget->addChild(emailField); | loginWidget->addChild(emailField); | ||||
pos.x += emailField->box.size.x; | |||||
pos.x += margin; | |||||
PasswordField *passwordField = new PasswordField(); | PasswordField *passwordField = new PasswordField(); | ||||
passwordField->box.pos = pos; | |||||
passwordField->box.size.x = 175; | passwordField->box.size.x = 175; | ||||
passwordField->placeholder = "Password"; | passwordField->placeholder = "Password"; | ||||
loginWidget->addChild(passwordField); | loginWidget->addChild(passwordField); | ||||
pos.x += passwordField->box.size.x; | |||||
struct LogInButton : Button { | |||||
TextField *emailField; | |||||
TextField *passwordField; | |||||
void onAction(EventAction &e) override { | |||||
std::thread t(pluginLogIn, emailField->text, passwordField->text); | |||||
t.detach(); | |||||
passwordField->text = ""; | |||||
} | |||||
}; | |||||
pos.x += margin; | |||||
LogInButton *logInButton = new LogInButton(); | LogInButton *logInButton = new LogInButton(); | ||||
logInButton->box.pos = pos; | |||||
logInButton->box.size.x = 100; | logInButton->box.size.x = 100; | ||||
logInButton->text = "Log in"; | logInButton->text = "Log in"; | ||||
logInButton->emailField = emailField; | logInButton->emailField = emailField; | ||||
logInButton->passwordField = passwordField; | logInButton->passwordField = passwordField; | ||||
loginWidget->addChild(logInButton); | loginWidget->addChild(logInButton); | ||||
pos.x += logInButton->box.size.x; | |||||
struct StatusLabel : Label { | |||||
void step() override { | |||||
text = pluginGetLoginStatus(); | |||||
} | |||||
}; | |||||
Label *label = new StatusLabel(); | Label *label = new StatusLabel(); | ||||
label->box.pos = pos; | |||||
loginWidget->addChild(label); | loginWidget->addChild(label); | ||||
addChild(loginWidget); | addChild(loginWidget); | ||||
} | } | ||||
{ | { | ||||
manageWidget = new Widget(); | |||||
Vec pos = Vec(0, 0); | |||||
struct ManageButton : Button { | |||||
void onAction(EventAction &e) override { | |||||
std::thread t([&]() { | |||||
systemOpenBrowser("https://vcvrack.com/"); | |||||
}); | |||||
t.detach(); | |||||
} | |||||
}; | |||||
SequentialLayout *layout = Widget::create<SequentialLayout>(Vec(0, 0)); | |||||
layout->spacing = 5; | |||||
manageWidget = layout; | |||||
Button *manageButton = new ManageButton(); | Button *manageButton = new ManageButton(); | ||||
manageButton->box.pos = pos; | |||||
manageButton->box.size.x = 125; | manageButton->box.size.x = 125; | ||||
manageButton->text = "Manage plugins"; | manageButton->text = "Manage plugins"; | ||||
manageWidget->addChild(manageButton); | manageWidget->addChild(manageButton); | ||||
pos.x += manageButton->box.size.x; | |||||
pos.x += margin; | |||||
Button *syncButton = new SyncButton(); | Button *syncButton = new SyncButton(); | ||||
syncButton->box.pos = pos; | |||||
syncButton->box.size.x = 125; | syncButton->box.size.x = 125; | ||||
syncButton->text = "Update plugins"; | syncButton->text = "Update plugins"; | ||||
manageWidget->addChild(syncButton); | manageWidget->addChild(syncButton); | ||||
pos.x += syncButton->box.size.x; | |||||
struct LogOutButton : Button { | |||||
void onAction(EventAction &e) override { | |||||
pluginLogOut(); | |||||
} | |||||
}; | |||||
pos.x += margin; | |||||
Button *logOutButton = new LogOutButton(); | Button *logOutButton = new LogOutButton(); | ||||
logOutButton->box.pos = pos; | |||||
logOutButton->box.size.x = 100; | logOutButton->box.size.x = 100; | ||||
logOutButton->text = "Log out"; | logOutButton->text = "Log out"; | ||||
manageWidget->addChild(logOutButton); | manageWidget->addChild(logOutButton); | ||||
@@ -166,37 +179,20 @@ PluginManagerWidget::PluginManagerWidget() { | |||||
} | } | ||||
{ | { | ||||
downloadWidget = new Widget(); | |||||
Vec pos = Vec(0, 0); | |||||
struct DownloadProgressBar : ProgressBar { | |||||
void step() override { | |||||
label = "Downloading"; | |||||
std::string name = pluginGetDownloadName(); | |||||
if (name != "") | |||||
label += " " + name; | |||||
setValue(100.0 * pluginGetDownloadProgress()); | |||||
} | |||||
}; | |||||
SequentialLayout *layout = Widget::create<SequentialLayout>(Vec(0, 0)); | |||||
layout->spacing = 5; | |||||
downloadWidget = layout; | |||||
ProgressBar *downloadProgress = new DownloadProgressBar(); | ProgressBar *downloadProgress = new DownloadProgressBar(); | ||||
downloadProgress->box.pos = pos; | |||||
downloadProgress->box.size.x = 300; | downloadProgress->box.size.x = 300; | ||||
downloadProgress->setLimits(0, 100); | downloadProgress->setLimits(0, 100); | ||||
downloadProgress->unit = "%"; | downloadProgress->unit = "%"; | ||||
downloadWidget->addChild(downloadProgress); | downloadWidget->addChild(downloadProgress); | ||||
pos.x += downloadProgress->box.size.x; | |||||
// struct CancelButton : Button { | |||||
// void onAction(EventAction &e) override { | |||||
// pluginCancelDownload(); | |||||
// } | |||||
// }; | |||||
// pos.x += margin; | |||||
// Button *logOutButton = new CancelButton(); | |||||
// logOutButton->box.pos = pos; | |||||
// logOutButton->box.size.x = 100; | |||||
// logOutButton->text = "Cancel"; | |||||
// downloadWidget->addChild(logOutButton); | |||||
// Button *cancelButton = new CancelButton(); | |||||
// cancelButton->box.size.x = 100; | |||||
// cancelButton->text = "Cancel"; | |||||
// downloadWidget->addChild(cancelButton); | |||||
addChild(downloadWidget); | addChild(downloadWidget); | ||||
} | } | ||||
@@ -5,8 +5,8 @@ namespace rack { | |||||
std::string gApplicationName = "VCV Rack"; | std::string gApplicationName = "VCV Rack"; | ||||
std::string gApplicationVersion = TOSTRING(VERSION); | std::string gApplicationVersion = TOSTRING(VERSION); | ||||
std::string gApiHost = "https://api.vcvrack.com"; | |||||
// std::string gApiHost = "http://localhost:8081"; | |||||
// std::string gApiHost = "https://api.vcvrack.com"; | |||||
std::string gApiHost = "http://localhost:8081"; | |||||
RackWidget *gRackWidget = NULL; | RackWidget *gRackWidget = NULL; | ||||
Toolbar *gToolbar = NULL; | Toolbar *gToolbar = NULL; | ||||
@@ -126,98 +126,77 @@ static bool loadPlugin(std::string path) { | |||||
return true; | return true; | ||||
} | } | ||||
static bool syncPlugin(json_t *pluginJ, bool dryRun) { | |||||
json_t *slugJ = json_object_get(pluginJ, "slug"); | |||||
if (!slugJ) | |||||
static bool syncPlugin(std::string slug, json_t *manifestJ, bool dryRun) { | |||||
// Get latest version | |||||
json_t *latestVersionJ = json_object_get(manifestJ, "latestVersion"); | |||||
if (!latestVersionJ) { | |||||
warn("Could not get latest version of plugin %s", slug.c_str()); | |||||
return false; | return false; | ||||
std::string slug = json_string_value(slugJ); | |||||
// Get community version | |||||
std::string version; | |||||
json_t *versionJ = json_object_get(pluginJ, "version"); | |||||
if (versionJ) { | |||||
version = json_string_value(versionJ); | |||||
} | } | ||||
std::string latestVersion = json_string_value(latestVersionJ); | |||||
// Check whether we already have a plugin with the same slug and version | // Check whether we already have a plugin with the same slug and version | ||||
for (Plugin *plugin : gPlugins) { | |||||
if (plugin->slug == slug) { | |||||
// plugin->version might be blank, so adding a version of the manifest will update the plugin | |||||
if (plugin->version == version) | |||||
return false; | |||||
} | |||||
} | |||||
json_t *nameJ = json_object_get(pluginJ, "name"); | |||||
if (!nameJ) | |||||
Plugin *plugin = pluginGetPlugin(slug); | |||||
if (plugin && plugin->version == latestVersion) { | |||||
return false; | return false; | ||||
std::string name = json_string_value(nameJ); | |||||
} | |||||
std::string download; | |||||
std::string sha256; | |||||
json_t *nameJ = json_object_get(manifestJ, "name"); | |||||
std::string name; | |||||
if (nameJ) { | |||||
name = json_string_value(nameJ); | |||||
} | |||||
else { | |||||
name = slug; | |||||
} | |||||
json_t *downloadsJ = json_object_get(pluginJ, "downloads"); | |||||
if (downloadsJ) { | |||||
#if ARCH_WIN | #if ARCH_WIN | ||||
#define DOWNLOADS_ARCH "win" | |||||
std::string arch = "win"; | |||||
#elif ARCH_MAC | #elif ARCH_MAC | ||||
#define DOWNLOADS_ARCH "mac" | |||||
std::string arch = "mac"; | |||||
#elif ARCH_LIN | #elif ARCH_LIN | ||||
#define DOWNLOADS_ARCH "lin" | |||||
std::string arch = "lin"; | |||||
#endif | #endif | ||||
json_t *archJ = json_object_get(downloadsJ, DOWNLOADS_ARCH); | |||||
if (archJ) { | |||||
// Get download URL | |||||
json_t *downloadJ = json_object_get(archJ, "download"); | |||||
if (downloadJ) | |||||
download = json_string_value(downloadJ); | |||||
// Get SHA256 hash | |||||
json_t *sha256J = json_object_get(archJ, "sha256"); | |||||
if (sha256J) | |||||
sha256 = json_string_value(sha256J); | |||||
} | |||||
} | |||||
json_t *productIdJ = json_object_get(pluginJ, "productId"); | |||||
if (productIdJ) { | |||||
download = gApiHost; | |||||
download += "/download"; | |||||
download += "?slug="; | |||||
download += slug; | |||||
download += "&token="; | |||||
download += requestEscape(gToken); | |||||
} | |||||
if (download.empty()) { | |||||
warn("Could not get download URL for plugin %s", slug.c_str()); | |||||
return false; | |||||
std::string downloadUrl; | |||||
downloadUrl = gApiHost; | |||||
downloadUrl += "/download"; | |||||
if (dryRun) { | |||||
downloadUrl += "/available"; | |||||
} | } | ||||
// If plugin is not loaded, download the zip file to /plugins | |||||
downloadName = name; | |||||
downloadProgress = 0.0; | |||||
// Download zip | |||||
std::string pluginsDir = assetLocal("plugins"); | |||||
std::string pluginPath = pluginsDir + "/" + slug; | |||||
std::string zipPath = pluginPath + ".zip"; | |||||
bool success = requestDownload(download, zipPath, &downloadProgress); | |||||
if (!success) { | |||||
warn("Plugin %s download was unsuccessful", slug.c_str()); | |||||
return false; | |||||
downloadUrl += "?token=" + requestEscape(gToken); | |||||
downloadUrl += "&slug=" + requestEscape(slug); | |||||
downloadUrl += "&version=" + requestEscape(latestVersion); | |||||
downloadUrl += "&arch=" + requestEscape(arch); | |||||
if (dryRun) { | |||||
// Check if available | |||||
json_t *availableResJ = requestJson(METHOD_GET, downloadUrl, NULL); | |||||
if (!availableResJ) { | |||||
warn("Could not check whether download is available"); | |||||
return false; | |||||
} | |||||
defer({ | |||||
json_decref(availableResJ); | |||||
}); | |||||
json_t *successJ = json_object_get(availableResJ, "success"); | |||||
return json_boolean_value(successJ); | |||||
} | } | ||||
else { | |||||
downloadName = name; | |||||
downloadProgress = 0.0; | |||||
info("Downloading plugin %s %s %s", slug.c_str(), latestVersion.c_str(), arch.c_str()); | |||||
if (!sha256.empty()) { | |||||
// Check SHA256 hash | |||||
std::string actualSha256 = requestSHA256File(zipPath); | |||||
if (actualSha256 != sha256) { | |||||
warn("Plugin %s does not match expected SHA256 checksum", slug.c_str()); | |||||
// Download zip | |||||
std::string pluginDest = assetLocal("plugins/" + slug + ".zip"); | |||||
if (!requestDownload(downloadUrl, pluginDest, &downloadProgress)) { | |||||
warn("Plugin %s download was unsuccessful", slug.c_str()); | |||||
return false; | return false; | ||||
} | } | ||||
} | |||||
downloadName = ""; | |||||
return true; | |||||
downloadName = ""; | |||||
return true; | |||||
} | |||||
} | } | ||||
static void loadPlugins(std::string path) { | static void loadPlugins(std::string path) { | ||||
@@ -378,7 +357,6 @@ void pluginDestroy() { | |||||
} | } | ||||
bool pluginSync(bool dryRun) { | bool pluginSync(bool dryRun) { | ||||
return false; | |||||
if (gToken.empty()) | if (gToken.empty()) | ||||
return false; | return false; | ||||
@@ -389,74 +367,69 @@ bool pluginSync(bool dryRun) { | |||||
downloadProgress = 0.0; | downloadProgress = 0.0; | ||||
downloadName = "Updating plugins..."; | downloadName = "Updating plugins..."; | ||||
} | } | ||||
defer({ | |||||
isDownloading = false; | |||||
}); | |||||
json_t *resJ = NULL; | |||||
json_t *communityResJ = NULL; | |||||
// Get user's plugins list | |||||
json_t *pluginsReqJ = json_object(); | |||||
json_object_set(pluginsReqJ, "token", json_string(gToken.c_str())); | |||||
json_t *pluginsResJ = requestJson(METHOD_GET, gApiHost + "/plugins", pluginsReqJ); | |||||
json_decref(pluginsReqJ); | |||||
if (!pluginsResJ) { | |||||
warn("Request for user's plugins failed"); | |||||
return false; | |||||
} | |||||
defer({ | |||||
json_decref(pluginsResJ); | |||||
}); | |||||
try { | |||||
// Download plugin slugs | |||||
json_t *reqJ = json_object(); | |||||
json_object_set(reqJ, "version", json_string(gApplicationVersion.c_str())); | |||||
json_object_set(reqJ, "token", json_string(gToken.c_str())); | |||||
resJ = requestJson(METHOD_GET, gApiHost + "/plugins", reqJ); | |||||
json_decref(reqJ); | |||||
if (!resJ) | |||||
throw std::runtime_error("No response from server"); | |||||
json_t *errorJ = json_object_get(pluginsResJ, "error"); | |||||
if (errorJ) { | |||||
warn("Request for user's plugins returned an error: %s", json_string_value(errorJ)); | |||||
return false; | |||||
} | |||||
json_t *errorJ = json_object_get(resJ, "error"); | |||||
if (errorJ) | |||||
throw std::runtime_error(json_string_value(errorJ)); | |||||
// Download community plugins | |||||
communityResJ = requestJson(METHOD_GET, gApiHost + "/community/plugins", NULL); | |||||
if (!communityResJ) | |||||
throw std::runtime_error("No response from server"); | |||||
json_t *communityErrorJ = json_object_get(communityResJ, "error"); | |||||
if (communityErrorJ) | |||||
throw std::runtime_error(json_string_value(communityErrorJ)); | |||||
// Check each plugin in list of plugin slugs | |||||
json_t *pluginSlugsJ = json_object_get(resJ, "plugins"); | |||||
json_t *communityPluginsJ = json_object_get(communityResJ, "plugins"); | |||||
size_t index; | |||||
json_t *pluginSlugJ; | |||||
json_array_foreach(pluginSlugsJ, index, pluginSlugJ) { | |||||
std::string slug = json_string_value(pluginSlugJ); | |||||
// Search for plugin slug in community | |||||
size_t communityIndex; | |||||
json_t *communityPluginJ = NULL; | |||||
json_array_foreach(communityPluginsJ, communityIndex, communityPluginJ) { | |||||
json_t *communitySlugJ = json_object_get(communityPluginJ, "slug"); | |||||
if (!communitySlugJ) | |||||
continue; | |||||
std::string communitySlug = json_string_value(communitySlugJ); | |||||
if (slug == communitySlug) | |||||
break; | |||||
} | |||||
// Get community manifests | |||||
json_t *manifestsResJ = requestJson(METHOD_GET, gApiHost + "/community/manifests", NULL); | |||||
if (!manifestsResJ) { | |||||
warn("Request for community manifests failed"); | |||||
return false; | |||||
} | |||||
defer({ | |||||
json_decref(manifestsResJ); | |||||
}); | |||||
// Sync plugin | |||||
if (syncPlugin(communityPluginJ, dryRun)) { | |||||
available = true; | |||||
} | |||||
else { | |||||
warn("Plugin %s not found in community", slug.c_str()); | |||||
} | |||||
} | |||||
// Check each plugin in list of plugin slugs | |||||
json_t *pluginsJ = json_object_get(pluginsResJ, "plugins"); | |||||
if (!pluginsJ) { | |||||
warn("No plugins array"); | |||||
return false; | |||||
} | } | ||||
catch (std::runtime_error &e) { | |||||
warn("Plugin sync error: %s", e.what()); | |||||
json_t *manifestsJ = json_object_get(manifestsResJ, "manifests"); | |||||
if (!manifestsJ) { | |||||
warn("No manifests object"); | |||||
return false; | |||||
} | } | ||||
if (communityResJ) | |||||
json_decref(communityResJ); | |||||
size_t slugIndex; | |||||
json_t *slugJ; | |||||
json_array_foreach(pluginsJ, slugIndex, slugJ) { | |||||
std::string slug = json_string_value(slugJ); | |||||
// Search for slug in manifests | |||||
const char *manifestSlug; | |||||
json_t *manifestJ = NULL; | |||||
json_object_foreach(manifestsJ, manifestSlug, manifestJ) { | |||||
if (slug == std::string(manifestSlug)) | |||||
break; | |||||
} | |||||
if (resJ) | |||||
json_decref(resJ); | |||||
if (!manifestJ) | |||||
continue; | |||||
if (!dryRun) { | |||||
isDownloading = false; | |||||
if (syncPlugin(slug, manifestJ, dryRun)) { | |||||
available = true; | |||||
} | |||||
} | } | ||||
return available; | return available; | ||||
@@ -131,8 +131,9 @@ bool requestDownload(std::string url, std::string filename, float *progress) { | |||||
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progress); | curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progress); | ||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); | ||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); | ||||
// Fail on 4xx and 5xx HTTP codes | |||||
curl_easy_setopt(curl, CURLOPT_FAILONERROR, true); | |||||
info("Downloading %s", url.c_str()); | |||||
CURLcode res = curl_easy_perform(curl); | CURLcode res = curl_easy_perform(curl); | ||||
curl_easy_cleanup(curl); | curl_easy_cleanup(curl); | ||||