mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
Projucer: Updated the autoupdater
This commit is contained in:
parent
0831c718a2
commit
fadd578b60
14 changed files with 399 additions and 213 deletions
|
|
@ -113,6 +113,7 @@ OBJECTS_APP := \
|
|||
$(JUCE_OBJDIR)/jucer_CodeHelpers_1e797672.o \
|
||||
$(JUCE_OBJDIR)/jucer_FileHelpers_54f12f83.o \
|
||||
$(JUCE_OBJDIR)/jucer_MiscUtilities_31fc8dd8.o \
|
||||
$(JUCE_OBJDIR)/jucer_VersionInfo_46f3ed40.o \
|
||||
$(JUCE_OBJDIR)/jucer_PIPGenerator_fd3402c7.o \
|
||||
$(JUCE_OBJDIR)/jucer_Icons_d02d18f1.o \
|
||||
$(JUCE_OBJDIR)/jucer_JucerTreeViewBase_9b9f2ff0.o \
|
||||
|
|
@ -368,6 +369,11 @@ $(JUCE_OBJDIR)/jucer_MiscUtilities_31fc8dd8.o: ../../Source/Utility/Helpers/juce
|
|||
@echo "Compiling jucer_MiscUtilities.cpp"
|
||||
$(V_AT)$(CXX) $(JUCE_CXXFLAGS) $(JUCE_CPPFLAGS_APP) $(JUCE_CFLAGS_APP) -o "$@" -c "$<"
|
||||
|
||||
$(JUCE_OBJDIR)/jucer_VersionInfo_46f3ed40.o: ../../Source/Utility/Helpers/jucer_VersionInfo.cpp
|
||||
-$(V_AT)mkdir -p $(JUCE_OBJDIR)
|
||||
@echo "Compiling jucer_VersionInfo.cpp"
|
||||
$(V_AT)$(CXX) $(JUCE_CXXFLAGS) $(JUCE_CPPFLAGS_APP) $(JUCE_CFLAGS_APP) -o "$@" -c "$<"
|
||||
|
||||
$(JUCE_OBJDIR)/jucer_PIPGenerator_fd3402c7.o: ../../Source/Utility/PIPs/jucer_PIPGenerator.cpp
|
||||
-$(V_AT)mkdir -p $(JUCE_OBJDIR)
|
||||
@echo "Compiling jucer_PIPGenerator.cpp"
|
||||
|
|
|
|||
|
|
@ -257,6 +257,10 @@
|
|||
isa = PBXBuildFile;
|
||||
fileRef = 486E8D02DAD2A0BF54A901C0;
|
||||
};
|
||||
44AD0D81A65C5EAE3BE588FD = {
|
||||
isa = PBXBuildFile;
|
||||
fileRef = FF3A6A384D536E1AEF47CD54;
|
||||
};
|
||||
638C7247B6DBA67EFE46E124 = {
|
||||
isa = PBXBuildFile;
|
||||
fileRef = 191330B20DAC08B890656EA0;
|
||||
|
|
@ -2092,6 +2096,13 @@
|
|||
path = "../../Source/Wizards/jucer_TemplateThumbnailsComponent.h";
|
||||
sourceTree = "SOURCE_ROOT";
|
||||
};
|
||||
C16F9F479A3A5F6DAD7647A2 = {
|
||||
isa = PBXFileReference;
|
||||
lastKnownFileType = sourcecode.c.h;
|
||||
name = "jucer_VersionInfo.h";
|
||||
path = "../../Source/Utility/Helpers/jucer_VersionInfo.h";
|
||||
sourceTree = "SOURCE_ROOT";
|
||||
};
|
||||
C187718F7B9EBA88584B43F3 = {
|
||||
isa = PBXFileReference;
|
||||
lastKnownFileType = sourcecode.cpp.cpp;
|
||||
|
|
@ -2582,6 +2593,13 @@
|
|||
path = "../../Source/Utility/UI/jucer_ProjucerLookAndFeel.h";
|
||||
sourceTree = "SOURCE_ROOT";
|
||||
};
|
||||
FF3A6A384D536E1AEF47CD54 = {
|
||||
isa = PBXFileReference;
|
||||
lastKnownFileType = sourcecode.cpp.cpp;
|
||||
name = "jucer_VersionInfo.cpp";
|
||||
path = "../../Source/Utility/Helpers/jucer_VersionInfo.cpp";
|
||||
sourceTree = "SOURCE_ROOT";
|
||||
};
|
||||
FF68231DE2B395461009116C = {
|
||||
isa = PBXFileReference;
|
||||
lastKnownFileType = sourcecode.c.h;
|
||||
|
|
@ -3011,6 +3029,8 @@
|
|||
A6C4AE13FB409DE414094CFA,
|
||||
6FD8DBC0FF42C87D8BEE2452,
|
||||
00515BA4EC5A7D4DC078ED37,
|
||||
FF3A6A384D536E1AEF47CD54,
|
||||
C16F9F479A3A5F6DAD7647A2,
|
||||
);
|
||||
name = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -3455,6 +3475,7 @@
|
|||
8BE478303CDF061B72F219E2,
|
||||
BF913199032B4CE970E82AA3,
|
||||
25EF9B3FECB4C9F0F522DCAA,
|
||||
44AD0D81A65C5EAE3BE588FD,
|
||||
638C7247B6DBA67EFE46E124,
|
||||
D0E26EB54B0087C8BE3D541E,
|
||||
468548FB21D264DC12321327,
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@
|
|||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_CodeHelpers.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_FileHelpers.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_MiscUtilities.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\UI\jucer_Icons.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\UI\jucer_JucerTreeViewBase.cpp"/>
|
||||
|
|
@ -1624,6 +1625,7 @@
|
|||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_RelativePath.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_TranslationHelpers.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_ValueSourceHelpers.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\UI\PropertyComponents\jucer_ColourPropertyComponent.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\UI\PropertyComponents\jucer_FilePathPropertyComponent.h"/>
|
||||
|
|
|
|||
|
|
@ -496,6 +496,9 @@
|
|||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_MiscUtilities.cpp">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.cpp">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.cpp">
|
||||
<Filter>Projucer\Utility\PIPs</Filter>
|
||||
</ClCompile>
|
||||
|
|
@ -2325,6 +2328,9 @@
|
|||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_ValueSourceHelpers.h">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.h">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.h">
|
||||
<Filter>Projucer\Utility\PIPs</Filter>
|
||||
</ClInclude>
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@
|
|||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_CodeHelpers.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_FileHelpers.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_MiscUtilities.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\UI\jucer_Icons.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\UI\jucer_JucerTreeViewBase.cpp"/>
|
||||
|
|
@ -1624,6 +1625,7 @@
|
|||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_RelativePath.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_TranslationHelpers.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_ValueSourceHelpers.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\UI\PropertyComponents\jucer_ColourPropertyComponent.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\UI\PropertyComponents\jucer_FilePathPropertyComponent.h"/>
|
||||
|
|
|
|||
|
|
@ -496,6 +496,9 @@
|
|||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_MiscUtilities.cpp">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.cpp">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.cpp">
|
||||
<Filter>Projucer\Utility\PIPs</Filter>
|
||||
</ClCompile>
|
||||
|
|
@ -2325,6 +2328,9 @@
|
|||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_ValueSourceHelpers.h">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.h">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.h">
|
||||
<Filter>Projucer\Utility\PIPs</Filter>
|
||||
</ClInclude>
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@
|
|||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_CodeHelpers.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_FileHelpers.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_MiscUtilities.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\UI\jucer_Icons.cpp"/>
|
||||
<ClCompile Include="..\..\Source\Utility\UI\jucer_JucerTreeViewBase.cpp"/>
|
||||
|
|
@ -1624,6 +1625,7 @@
|
|||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_RelativePath.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_TranslationHelpers.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_ValueSourceHelpers.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\UI\PropertyComponents\jucer_ColourPropertyComponent.h"/>
|
||||
<ClInclude Include="..\..\Source\Utility\UI\PropertyComponents\jucer_FilePathPropertyComponent.h"/>
|
||||
|
|
|
|||
|
|
@ -496,6 +496,9 @@
|
|||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_MiscUtilities.cpp">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.cpp">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.cpp">
|
||||
<Filter>Projucer\Utility\PIPs</Filter>
|
||||
</ClCompile>
|
||||
|
|
@ -2325,6 +2328,9 @@
|
|||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_ValueSourceHelpers.h">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\Source\Utility\Helpers\jucer_VersionInfo.h">
|
||||
<Filter>Projucer\Utility\Helpers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\Source\Utility\PIPs\jucer_PIPGenerator.h">
|
||||
<Filter>Projucer\Utility\PIPs</Filter>
|
||||
</ClInclude>
|
||||
|
|
|
|||
|
|
@ -636,6 +636,10 @@
|
|||
file="Source/Utility/Helpers/jucer_TranslationHelpers.h"/>
|
||||
<FILE id="EuC4K4" name="jucer_ValueSourceHelpers.h" compile="0" resource="0"
|
||||
file="Source/Utility/Helpers/jucer_ValueSourceHelpers.h"/>
|
||||
<FILE id="BPCoKV" name="jucer_VersionInfo.cpp" compile="1" resource="0"
|
||||
file="Source/Utility/Helpers/jucer_VersionInfo.cpp"/>
|
||||
<FILE id="TnBQtv" name="jucer_VersionInfo.h" compile="0" resource="0"
|
||||
file="Source/Utility/Helpers/jucer_VersionInfo.h"/>
|
||||
</GROUP>
|
||||
<GROUP id="{A07C4A97-0855-5346-CAF2-A005580B6773}" name="PIPs">
|
||||
<FILE id="joAnDa" name="jucer_PIPGenerator.cpp" compile="1" resource="0"
|
||||
|
|
|
|||
|
|
@ -52,119 +52,65 @@ void LatestVersionCheckerAndUpdater::checkForNewVersion (bool showAlerts)
|
|||
//==============================================================================
|
||||
void LatestVersionCheckerAndUpdater::run()
|
||||
{
|
||||
queryUpdateServer();
|
||||
auto info = VersionInfo::fetchLatestFromUpdateServer();
|
||||
|
||||
if (! threadShouldExit())
|
||||
MessageManager::callAsync ([this] { processResult(); });
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
String getOSString()
|
||||
{
|
||||
#if JUCE_MAC
|
||||
return "OSX";
|
||||
#elif JUCE_WINDOWS
|
||||
return "Windows";
|
||||
#elif JUCE_LINUX
|
||||
return "Linux";
|
||||
#else
|
||||
jassertfalse;
|
||||
return "Unknown";
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace VersionHelpers
|
||||
{
|
||||
String formatProductVersion (int versionNum)
|
||||
if (info == nullptr)
|
||||
{
|
||||
int major = (versionNum & 0xff0000) >> 16;
|
||||
int minor = (versionNum & 0x00ff00) >> 8;
|
||||
int build = (versionNum & 0x0000ff) >> 0;
|
||||
if (showAlertWindows)
|
||||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
|
||||
"Update Server Communication Error",
|
||||
"Failed to communicate with the JUCE update server.\n"
|
||||
"Please try again in a few minutes.\n\n"
|
||||
"If this problem persists you can download the latest version of JUCE from juce.com");
|
||||
|
||||
return String (major) + '.' + String (minor) + '.' + String (build);
|
||||
}
|
||||
|
||||
String getProductVersionString()
|
||||
{
|
||||
return formatProductVersion (ProjectInfo::versionNumber);
|
||||
}
|
||||
|
||||
bool isNewVersion (const String& current, const String& other)
|
||||
{
|
||||
auto currentTokens = StringArray::fromTokens (current, ".", {});
|
||||
auto otherTokens = StringArray::fromTokens (other, ".", {});
|
||||
|
||||
jassert (currentTokens.size() == 3 && otherTokens.size() == 3);
|
||||
|
||||
if (currentTokens[0].getIntValue() == otherTokens[0].getIntValue())
|
||||
{
|
||||
if (currentTokens[1].getIntValue() == otherTokens[1].getIntValue())
|
||||
return currentTokens[2].getIntValue() < otherTokens[2].getIntValue();
|
||||
|
||||
return currentTokens[1].getIntValue() < otherTokens[1].getIntValue();
|
||||
}
|
||||
|
||||
return currentTokens[0].getIntValue() < otherTokens[0].getIntValue();
|
||||
}
|
||||
}
|
||||
|
||||
void LatestVersionCheckerAndUpdater::queryUpdateServer()
|
||||
{
|
||||
StringPairArray responseHeaders;
|
||||
|
||||
URL latestVersionURL ("https://my.roli.com/software_versions/update_to/Projucer/"
|
||||
+ VersionHelpers::getProductVersionString() + '/' + getOSString()
|
||||
+ "?language=" + SystemStats::getUserLanguage());
|
||||
|
||||
std::unique_ptr<InputStream> inStream (latestVersionURL.createInputStream (false, nullptr, nullptr,
|
||||
"X-API-Key: 265441b-343403c-20f6932-76361d\nContent-Type: "
|
||||
"application/json\nAccept: application/json; version=1",
|
||||
0, &responseHeaders, &statusCode, 0));
|
||||
|
||||
if (threadShouldExit())
|
||||
return;
|
||||
|
||||
if (inStream.get() != nullptr && (statusCode == 303 || statusCode == 400))
|
||||
{
|
||||
if (statusCode == 303)
|
||||
relativeDownloadPath = responseHeaders["Location"];
|
||||
|
||||
jassert (relativeDownloadPath.isNotEmpty());
|
||||
|
||||
jsonReply = JSON::parse (inStream->readEntireStreamAsString());
|
||||
}
|
||||
else if (showAlertWindows)
|
||||
{
|
||||
if (statusCode == 204)
|
||||
AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, "No New Version Available", "Your JUCE version is up to date.");
|
||||
else
|
||||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Network Error", "Could not connect to the web server.\n"
|
||||
"Please check your internet connection and try again.");
|
||||
}
|
||||
}
|
||||
|
||||
void LatestVersionCheckerAndUpdater::processResult()
|
||||
{
|
||||
if (! jsonReply.isObject())
|
||||
if (! info->isNewerVersionThanCurrent())
|
||||
{
|
||||
if (showAlertWindows)
|
||||
AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon,
|
||||
"No New Version Available",
|
||||
"Your JUCE version is up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode == 400)
|
||||
auto osString = []
|
||||
{
|
||||
auto errorObject = jsonReply.getDynamicObject()->getProperty ("error");
|
||||
#if JUCE_MAC
|
||||
return "osx";
|
||||
#elif JUCE_WINDOWS
|
||||
return "windows";
|
||||
#elif JUCE_LINUX
|
||||
return "linux";
|
||||
#else
|
||||
jassertfalse;
|
||||
return "Unknown";
|
||||
#endif
|
||||
}();
|
||||
|
||||
if (errorObject.isObject())
|
||||
String requiredFilename ("juce-" + info->versionString + "-" + osString + ".zip");
|
||||
|
||||
for (auto& asset : info->assets)
|
||||
{
|
||||
if (asset.name == requiredFilename)
|
||||
{
|
||||
auto message = errorObject.getProperty ("message", {}).toString();
|
||||
auto versionString = info->versionString;
|
||||
auto releaseNotes = info->releaseNotes;
|
||||
|
||||
if (message.isNotEmpty())
|
||||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "JUCE Updater", message);
|
||||
MessageManager::callAsync ([this, versionString, releaseNotes, asset]
|
||||
{
|
||||
askUserAboutNewVersion (versionString, releaseNotes, asset);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (statusCode == 303)
|
||||
{
|
||||
askUserAboutNewVersion (jsonReply.getProperty ("version", {}).toString(),
|
||||
jsonReply.getProperty ("notes", {}).toString());
|
||||
}
|
||||
|
||||
if (showAlertWindows)
|
||||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
|
||||
"Failed to find any new downloads",
|
||||
"Please try again in a few minutes.");
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -246,14 +192,15 @@ public:
|
|||
RectanglePlacement::stretchToFit, 1.0f);
|
||||
}
|
||||
|
||||
static std::unique_ptr<DialogWindow> launchDialog (const String& newVersion, const String& releaseNotes)
|
||||
static std::unique_ptr<DialogWindow> launchDialog (const String& newVersionString,
|
||||
const String& releaseNotes)
|
||||
{
|
||||
DialogWindow::LaunchOptions options;
|
||||
|
||||
options.dialogTitle = "Download JUCE version " + newVersion + "?";
|
||||
options.dialogTitle = "Download JUCE version " + newVersionString + "?";
|
||||
options.resizable = false;
|
||||
|
||||
auto* content = new UpdateDialog (newVersion, releaseNotes);
|
||||
auto* content = new UpdateDialog (newVersionString, releaseNotes);
|
||||
options.content.set (content, true);
|
||||
|
||||
std::unique_ptr<DialogWindow> dialog (options.create());
|
||||
|
|
@ -292,66 +239,83 @@ private:
|
|||
DialogWindow* parentWindow = nullptr;
|
||||
};
|
||||
|
||||
void LatestVersionCheckerAndUpdater::askUserForLocationToDownload()
|
||||
void LatestVersionCheckerAndUpdater::askUserForLocationToDownload (const VersionInfo::Asset& asset)
|
||||
{
|
||||
FileChooser chooser ("Please select the location into which you'd like to install the new version",
|
||||
FileChooser chooser ("Please select the location into which you would like to install the new version",
|
||||
{ getAppSettings().getStoredPath (Ids::jucePath, TargetOS::getThisOS()).get() });
|
||||
|
||||
if (chooser.browseForDirectory())
|
||||
{
|
||||
auto targetFolder = chooser.getResult();
|
||||
|
||||
if (isJUCEFolder (targetFolder))
|
||||
// By default we will install into 'targetFolder/JUCE', but we should install into
|
||||
// 'targetFolder' if that is an existing JUCE directory.
|
||||
bool willOverwriteJuceFolder = [&targetFolder]
|
||||
{
|
||||
if (isJUCEFolder (targetFolder))
|
||||
return true;
|
||||
|
||||
targetFolder = targetFolder.getChildFile ("JUCE");
|
||||
|
||||
return isJUCEFolder (targetFolder);
|
||||
}();
|
||||
|
||||
auto targetFolderPath = targetFolder.getFullPathName();
|
||||
|
||||
if (willOverwriteJuceFolder)
|
||||
{
|
||||
if (targetFolder.getChildFile (".git").isDirectory())
|
||||
{
|
||||
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Downloading New JUCE Version",
|
||||
"This folder is a GIT repository!\n\nYou should use a \"git pull\" to update it to the latest version.");
|
||||
targetFolderPath + "\n\nis a GIT repository!\n\nYou should use a \"git pull\" to update it to the latest version.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, "Overwrite Existing JUCE Folder?",
|
||||
String ("Do you want to overwrite the folder:\n\n" + targetFolder.getFullPathName() + "\n\n..with the latest version from juce.com?\n\n"
|
||||
"This will move the existing folder to " + targetFolder.getFullPathName() + "_old.")))
|
||||
"Do you want to replace the folder\n\n" + targetFolderPath + "\n\nwith the latest version from juce.com?\n\n"
|
||||
"This will move the existing folder to " + targetFolderPath + "_old."))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (targetFolder.exists())
|
||||
{
|
||||
targetFolder = targetFolder.getChildFile ("JUCE").getNonexistentSibling();
|
||||
if (! AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, "Existing File Or Directory",
|
||||
"Do you want to move\n\n" + targetFolderPath + "\n\nto\n\n" + targetFolderPath + "_old?"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
downloadAndInstall (targetFolder);
|
||||
downloadAndInstall (asset, targetFolder);
|
||||
}
|
||||
}
|
||||
|
||||
void LatestVersionCheckerAndUpdater::askUserAboutNewVersion (const String& newVersion, const String& releaseNotes)
|
||||
void LatestVersionCheckerAndUpdater::askUserAboutNewVersion (const String& newVersionString,
|
||||
const String& releaseNotes,
|
||||
const VersionInfo::Asset& asset)
|
||||
{
|
||||
if (newVersion.isNotEmpty() && releaseNotes.isNotEmpty()
|
||||
&& VersionHelpers::isNewVersion (VersionHelpers::getProductVersionString(), newVersion))
|
||||
{
|
||||
dialogWindow = UpdateDialog::launchDialog (newVersion, releaseNotes);
|
||||
dialogWindow = UpdateDialog::launchDialog (newVersionString, releaseNotes);
|
||||
|
||||
if (auto* mm = ModalComponentManager::getInstance())
|
||||
mm->attachCallback (dialogWindow.get(), ModalCallbackFunction::create ([this] (int result)
|
||||
{
|
||||
if (result == 1)
|
||||
askUserForLocationToDownload();
|
||||
if (auto* mm = ModalComponentManager::getInstance())
|
||||
mm->attachCallback (dialogWindow.get(),
|
||||
ModalCallbackFunction::create ([this, asset] (int result)
|
||||
{
|
||||
if (result == 1)
|
||||
askUserForLocationToDownload (asset);
|
||||
|
||||
dialogWindow.reset();
|
||||
}));
|
||||
}
|
||||
dialogWindow.reset();
|
||||
}));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
class DownloadAndInstallThread : private ThreadWithProgressWindow
|
||||
{
|
||||
public:
|
||||
DownloadAndInstallThread (const URL& u, const File& t, std::function<void()>&& cb)
|
||||
DownloadAndInstallThread (const VersionInfo::Asset& a, const File& t, std::function<void()>&& cb)
|
||||
: ThreadWithProgressWindow ("Downloading New Version", true, true),
|
||||
downloadURL (u), targetFolder (t), completionCallback (std::move (cb))
|
||||
asset (a), targetFolder (t), completionCallback (std::move (cb))
|
||||
{
|
||||
launchThread (3);
|
||||
}
|
||||
|
|
@ -368,7 +332,9 @@ private:
|
|||
result = install (zipData);
|
||||
|
||||
if (result.failed())
|
||||
MessageManager::callAsync ([result] () { AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Installation Failed", result.getErrorMessage()); });
|
||||
MessageManager::callAsync ([result] { AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
|
||||
"Installation Failed",
|
||||
result.getErrorMessage()); });
|
||||
else
|
||||
MessageManager::callAsync (completionCallback);
|
||||
}
|
||||
|
|
@ -378,10 +344,7 @@ private:
|
|||
setStatusMessage ("Downloading...");
|
||||
|
||||
int statusCode = 0;
|
||||
StringPairArray responseHeaders;
|
||||
|
||||
std::unique_ptr<InputStream> inStream (downloadURL.createInputStream (false, nullptr, nullptr, {}, 0,
|
||||
&responseHeaders, &statusCode, 0));
|
||||
auto inStream = VersionInfo::createInputStreamForAsset (asset, statusCode);
|
||||
|
||||
if (inStream != nullptr && statusCode == 200)
|
||||
{
|
||||
|
|
@ -406,53 +369,53 @@ private:
|
|||
return Result::ok();
|
||||
}
|
||||
|
||||
return Result::fail ("Failed to download from: " + downloadURL.toString (false));
|
||||
return Result::fail ("Failed to download from: " + asset.url);
|
||||
}
|
||||
|
||||
Result install (MemoryBlock& data)
|
||||
Result install (const MemoryBlock& data)
|
||||
{
|
||||
setStatusMessage ("Installing...");
|
||||
|
||||
auto result = unzipDownload (data);
|
||||
|
||||
if (threadShouldExit())
|
||||
result = Result::fail ("Cancelled");
|
||||
|
||||
if (result.failed())
|
||||
return result;
|
||||
|
||||
return Result::ok();
|
||||
}
|
||||
|
||||
Result unzipDownload (const MemoryBlock& data)
|
||||
{
|
||||
MemoryInputStream input (data, false);
|
||||
ZipFile zip (input);
|
||||
|
||||
if (zip.getNumEntries() == 0)
|
||||
return Result::fail ("The downloaded file was not a valid JUCE file!");
|
||||
|
||||
auto unzipTarget = File::createTempFile ({});
|
||||
struct ScopedDownloadFolder
|
||||
{
|
||||
ScopedDownloadFolder (const File& installTargetFolder)
|
||||
{
|
||||
folder = installTargetFolder.getSiblingFile (installTargetFolder.getFileNameWithoutExtension() + "_download").getNonexistentSibling();
|
||||
jassert (folder.createDirectory());
|
||||
}
|
||||
|
||||
if (! unzipTarget.createDirectory())
|
||||
~ScopedDownloadFolder() { folder.deleteRecursively(); }
|
||||
|
||||
File folder;
|
||||
};
|
||||
|
||||
ScopedDownloadFolder unzipTarget (targetFolder);
|
||||
|
||||
if (! unzipTarget.folder.isDirectory())
|
||||
return Result::fail ("Couldn't create a temporary folder to unzip the new version!");
|
||||
|
||||
auto r = zip.uncompressTo (unzipTarget);
|
||||
auto r = zip.uncompressTo (unzipTarget.folder);
|
||||
|
||||
if (r.failed())
|
||||
{
|
||||
unzipTarget.deleteRecursively();
|
||||
return r;
|
||||
}
|
||||
|
||||
if (threadShouldExit())
|
||||
return Result::fail ("Cancelled");
|
||||
|
||||
#if JUCE_LINUX || JUCE_MAC
|
||||
r = setFilePermissions (unzipTarget, zip);
|
||||
r = setFilePermissions (unzipTarget.folder, zip);
|
||||
|
||||
if (r.failed())
|
||||
{
|
||||
unzipTarget.deleteRecursively();
|
||||
return r;
|
||||
}
|
||||
|
||||
if (threadShouldExit())
|
||||
return Result::fail ("Cancelled");
|
||||
#endif
|
||||
|
||||
if (targetFolder.exists())
|
||||
|
|
@ -460,21 +423,15 @@ private:
|
|||
auto oldFolder = targetFolder.getSiblingFile (targetFolder.getFileNameWithoutExtension() + "_old").getNonexistentSibling();
|
||||
|
||||
if (! targetFolder.moveFileTo (oldFolder))
|
||||
{
|
||||
unzipTarget.deleteRecursively();
|
||||
return Result::fail ("Could not remove the existing folder!\n\n"
|
||||
"This may happen if you are trying to download into a directory that requires administrator privileges to modify.\n"
|
||||
"Please select a folder that is writable by the current user.");
|
||||
}
|
||||
}
|
||||
|
||||
if (! unzipTarget.moveFileTo (targetFolder))
|
||||
{
|
||||
unzipTarget.deleteRecursively();
|
||||
if (! unzipTarget.folder.getChildFile ("JUCE").moveFileTo (targetFolder))
|
||||
return Result::fail ("Could not overwrite the existing folder!\n\n"
|
||||
"This may happen if you are trying to download into a directory that requires administrator privileges to modify.\n"
|
||||
"Please select a folder that is writable by the current user.");
|
||||
}
|
||||
|
||||
return Result::ok();
|
||||
}
|
||||
|
|
@ -502,7 +459,7 @@ private:
|
|||
return Result::ok();
|
||||
}
|
||||
|
||||
URL downloadURL;
|
||||
VersionInfo::Asset asset;
|
||||
File targetFolder;
|
||||
std::function<void()> completionCallback;
|
||||
};
|
||||
|
|
@ -533,9 +490,9 @@ void restartProcess (const File& targetFolder)
|
|||
}
|
||||
}
|
||||
|
||||
void LatestVersionCheckerAndUpdater::downloadAndInstall (const File& targetFolder)
|
||||
void LatestVersionCheckerAndUpdater::downloadAndInstall (const VersionInfo::Asset& asset, const File& targetFolder)
|
||||
{
|
||||
installer.reset (new DownloadAndInstallThread ({ relativeDownloadPath }, targetFolder,
|
||||
installer.reset (new DownloadAndInstallThread (asset, targetFolder,
|
||||
[this, targetFolder]
|
||||
{
|
||||
installer.reset();
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "../Utility/Helpers/jucer_VersionInfo.h"
|
||||
|
||||
class DownloadAndInstallThread;
|
||||
|
||||
class LatestVersionCheckerAndUpdater : public DeletedAtShutdown,
|
||||
|
|
@ -43,17 +45,12 @@ public:
|
|||
private:
|
||||
//==============================================================================
|
||||
void run() override;
|
||||
void queryUpdateServer();
|
||||
void processResult();
|
||||
void askUserAboutNewVersion (const String&, const String&);
|
||||
void askUserForLocationToDownload();
|
||||
void downloadAndInstall (const File&);
|
||||
void askUserAboutNewVersion (const String&, const String&, const VersionInfo::Asset&);
|
||||
void askUserForLocationToDownload (const VersionInfo::Asset&);
|
||||
void downloadAndInstall (const VersionInfo::Asset&, const File&);
|
||||
|
||||
//==============================================================================
|
||||
bool showAlertWindows = false;
|
||||
int statusCode = 0;
|
||||
String relativeDownloadPath;
|
||||
var jsonReply;
|
||||
|
||||
std::unique_ptr<DownloadAndInstallThread> installer;
|
||||
std::unique_ptr<Component> dialogWindow;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include "../Application/jucer_Headers.h"
|
||||
#include "jucer_DownloadCompileEngineThread.h"
|
||||
#include "../LiveBuildEngine/jucer_CompileEngineDLL.h"
|
||||
#include "../Utility/Helpers/jucer_VersionInfo.h"
|
||||
|
||||
//==============================================================================
|
||||
bool DownloadCompileEngineThread::downloadAndInstall()
|
||||
|
|
@ -83,38 +84,60 @@ void DownloadCompileEngineThread::run()
|
|||
|
||||
Result DownloadCompileEngineThread::download (MemoryBlock& dest)
|
||||
{
|
||||
int statusCode = 302;
|
||||
const int timeoutMs = 10000;
|
||||
StringPairArray responseHeaders;
|
||||
auto info = VersionInfo::fetchFromUpdateServer (ProjectInfo::versionString);
|
||||
|
||||
URL url = getDownloadUrl();
|
||||
std::unique_ptr<InputStream> in (url.createInputStream (false, nullptr, nullptr,
|
||||
String(), timeoutMs, &responseHeaders,
|
||||
&statusCode, 0));
|
||||
if (info == nullptr)
|
||||
return Result::fail ("Download error: cannot communicate with server");
|
||||
|
||||
if (in == nullptr || statusCode != 200)
|
||||
return Result::fail ("Download error: cannot establish connection");
|
||||
|
||||
MemoryOutputStream mo (dest, true);
|
||||
|
||||
int64 size = in->getTotalLength();
|
||||
int64 bytesReceived = -1;
|
||||
String msg("Downloading... (123)");
|
||||
|
||||
for (int64 pos = 0; pos < size; pos += bytesReceived)
|
||||
auto requiredAssetName = []
|
||||
{
|
||||
setStatusMessage (msg.replace ("123", File::descriptionOfSizeInBytes (pos)));
|
||||
String name ("JUCECompileEngine_");
|
||||
|
||||
if (threadShouldExit())
|
||||
return Result::fail ("Download error: operation interrupted");
|
||||
#if JUCE_MAC
|
||||
name << "osx_";
|
||||
#elif JUCE_WINDOWS
|
||||
name << "windows_";
|
||||
#else
|
||||
jassertfalse;
|
||||
#endif
|
||||
|
||||
bytesReceived = mo.writeFromInputStream (*in, 8192);
|
||||
return name + ProjectInfo::versionString + ".zip";
|
||||
}();
|
||||
|
||||
if (bytesReceived == 0)
|
||||
return Result::fail ("Download error: lost connection");
|
||||
for (auto& asset : info->assets)
|
||||
{
|
||||
if (asset.name == requiredAssetName)
|
||||
{
|
||||
int statusCode = 0;
|
||||
auto in = VersionInfo::createInputStreamForAsset (asset, statusCode);
|
||||
|
||||
if (in == nullptr || statusCode != 200)
|
||||
return Result::fail ("Download error: cannot establish connection");
|
||||
|
||||
MemoryOutputStream mo (dest, true);
|
||||
|
||||
int64 size = in->getTotalLength();
|
||||
int64 bytesReceived = -1;
|
||||
String msg("Downloading... (123)");
|
||||
|
||||
for (int64 pos = 0; pos < size; pos += bytesReceived)
|
||||
{
|
||||
setStatusMessage (msg.replace ("123", File::descriptionOfSizeInBytes (pos)));
|
||||
|
||||
if (threadShouldExit())
|
||||
return Result::fail ("Download error: operation interrupted");
|
||||
|
||||
bytesReceived = mo.writeFromInputStream (*in, 8192);
|
||||
|
||||
if (bytesReceived == 0)
|
||||
return Result::fail ("Download error: lost connection");
|
||||
}
|
||||
|
||||
return Result::ok();
|
||||
}
|
||||
}
|
||||
|
||||
return Result::ok();
|
||||
return Result::fail ("Download error: no downloads available");
|
||||
}
|
||||
|
||||
Result DownloadCompileEngineThread::install (const MemoryBlock& data, File& targetFolder)
|
||||
|
|
@ -131,21 +154,6 @@ Result DownloadCompileEngineThread::install (const MemoryBlock& data, File& targ
|
|||
return zip.uncompressTo (targetFolder);
|
||||
}
|
||||
|
||||
URL DownloadCompileEngineThread::getDownloadUrl()
|
||||
{
|
||||
String urlStub ("http://assets.roli.com/juce/JUCECompileEngine_");
|
||||
|
||||
#if JUCE_MAC
|
||||
urlStub << "osx_";
|
||||
#elif JUCE_WINDOWS
|
||||
urlStub << "windows_";
|
||||
#else
|
||||
jassertfalse;
|
||||
#endif
|
||||
|
||||
return urlStub + ProjectInfo::versionString + ".zip";
|
||||
}
|
||||
|
||||
File DownloadCompileEngineThread::getInstallFolder()
|
||||
{
|
||||
return CompileEngineDLL::getVersionedUserAppSupportFolder();
|
||||
|
|
|
|||
115
extras/Projucer/Source/Utility/Helpers/jucer_VersionInfo.cpp
Normal file
115
extras/Projucer/Source/Utility/Helpers/jucer_VersionInfo.cpp
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#include "../../Application/jucer_Headers.h"
|
||||
#include "jucer_VersionInfo.h"
|
||||
|
||||
std::unique_ptr<VersionInfo> VersionInfo::fetchFromUpdateServer (const String& versionString)
|
||||
{
|
||||
return fetch ("tags/" + versionString);
|
||||
}
|
||||
|
||||
std::unique_ptr<VersionInfo> VersionInfo::fetchLatestFromUpdateServer()
|
||||
{
|
||||
return fetch ("latest");
|
||||
}
|
||||
|
||||
std::unique_ptr<InputStream> VersionInfo::createInputStreamForAsset (const Asset& asset, int& statusCode)
|
||||
{
|
||||
URL downloadUrl (asset.url);
|
||||
StringPairArray responseHeaders;
|
||||
|
||||
return std::unique_ptr<InputStream> (downloadUrl.createInputStream (false, nullptr, nullptr,
|
||||
"Accept: application/octet-stream",
|
||||
0, &responseHeaders, &statusCode, 1));
|
||||
}
|
||||
|
||||
bool VersionInfo::isNewerVersionThanCurrent()
|
||||
{
|
||||
jassert (versionString.isNotEmpty());
|
||||
|
||||
auto currentTokens = StringArray::fromTokens (ProjectInfo::versionString, ".", {});
|
||||
auto thisTokens = StringArray::fromTokens (versionString, ".", {});
|
||||
|
||||
jassert (thisTokens.size() == 3 && thisTokens.size() == 3);
|
||||
|
||||
if (currentTokens[0].getIntValue() == thisTokens[0].getIntValue())
|
||||
{
|
||||
if (currentTokens[1].getIntValue() == thisTokens[1].getIntValue())
|
||||
return currentTokens[2].getIntValue() < thisTokens[2].getIntValue();
|
||||
|
||||
return currentTokens[1].getIntValue() < thisTokens[1].getIntValue();
|
||||
}
|
||||
|
||||
return currentTokens[0].getIntValue() < thisTokens[0].getIntValue();
|
||||
}
|
||||
|
||||
std::unique_ptr<VersionInfo> VersionInfo::fetch (const String& endpoint)
|
||||
{
|
||||
URL latestVersionURL ("https://api.github.com/repos/WeAreROLI/JUCE/releases/" + endpoint);
|
||||
std::unique_ptr<InputStream> inStream (latestVersionURL.createInputStream (false));
|
||||
|
||||
if (inStream == nullptr)
|
||||
return nullptr;
|
||||
|
||||
auto content = inStream->readEntireStreamAsString();
|
||||
auto latestReleaseDetails = JSON::parse (content);
|
||||
|
||||
auto* json = latestReleaseDetails.getDynamicObject();
|
||||
|
||||
if (json == nullptr)
|
||||
return nullptr;
|
||||
|
||||
auto versionString = json->getProperty ("tag_name").toString();
|
||||
|
||||
if (versionString.isEmpty())
|
||||
return nullptr;
|
||||
|
||||
auto* assets = json->getProperty ("assets").getArray();
|
||||
|
||||
if (assets == nullptr)
|
||||
return nullptr;
|
||||
|
||||
auto releaseNotes = json->getProperty ("body").toString();
|
||||
std::vector<VersionInfo::Asset> parsedAssets;
|
||||
|
||||
for (auto& asset : *assets)
|
||||
{
|
||||
if (auto* assetJson = asset.getDynamicObject())
|
||||
{
|
||||
parsedAssets.push_back ({ assetJson->getProperty ("name").toString(),
|
||||
assetJson->getProperty ("url").toString() });
|
||||
jassert (parsedAssets.back().name.isNotEmpty());
|
||||
jassert (parsedAssets.back().url.isNotEmpty());
|
||||
}
|
||||
else
|
||||
{
|
||||
jassertfalse;
|
||||
}
|
||||
}
|
||||
|
||||
return std::unique_ptr<VersionInfo> (new VersionInfo ({ versionString, releaseNotes, std::move (parsedAssets) }));
|
||||
}
|
||||
54
extras/Projucer/Source/Utility/Helpers/jucer_VersionInfo.h
Normal file
54
extras/Projucer/Source/Utility/Helpers/jucer_VersionInfo.h
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
struct Asset
|
||||
{
|
||||
const String name;
|
||||
const String url;
|
||||
};
|
||||
|
||||
static std::unique_ptr<VersionInfo> fetchFromUpdateServer (const String& versionString);
|
||||
static std::unique_ptr<VersionInfo> fetchLatestFromUpdateServer();
|
||||
static std::unique_ptr<InputStream> createInputStreamForAsset (const Asset& asset, int& statusCode);
|
||||
|
||||
bool isNewerVersionThanCurrent();
|
||||
|
||||
const String versionString;
|
||||
const String releaseNotes;
|
||||
const std::vector<Asset> assets;
|
||||
|
||||
private:
|
||||
VersionInfo() = default;
|
||||
|
||||
static std::unique_ptr<VersionInfo> fetch (const String&);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue