diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 4d93e7ea8..65c17bc37 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -268,6 +268,8 @@ Updater.Running.Title="Program currently active" Updater.Running.Text="Outputs are currently active, please shut down any active outputs before attempting to update" Updater.NoUpdatesAvailable.Title="No updates available" Updater.NoUpdatesAvailable.Text="No updates are currently available" +Updater.BranchNotFound.Title="Update Channel Removed" +Updater.BranchNotFound.Text="Your selected update channel is no longer available, OBS has been reset to the default." Updater.RepairButUpdatesAvailable.Title="Integrity Check Unavailable" Updater.RepairButUpdatesAvailable.Text="Checking file integrity is only possible for the latest version available. Use Help → Check For Updates to verify and update your OBS installation." Updater.RepairConfirm.Title="Confirm Integrity Check" @@ -835,6 +837,10 @@ Basic.Settings.Confirm="You have unsaved changes. Save changes?" Basic.Settings.General="General" Basic.Settings.General.Theme="Theme" Basic.Settings.General.Language="Language" +Basic.Settings.General.Updater="Updates" +Basic.Settings.General.UpdateChannel="Update Channel" +Basic.Settings.General.UpdateChannelDisabled="(Disabled)" +Basic.Settings.General.UpdateChannelDefault="(Default)" Basic.Settings.General.EnableAutoUpdates="Automatically check for updates on startup" Basic.Settings.General.OpenStatsOnStartup="Open stats dialog on startup" Basic.Settings.General.HideOBSWindowsFromCapture="Hide OBS windows from screen capture" @@ -886,6 +892,12 @@ Basic.Settings.General.MultiviewLayout.9Scene="Scenes only (9 Scenes)" Basic.Settings.General.MultiviewLayout.16Scene="Scenes only (16 Scenes)" Basic.Settings.General.MultiviewLayout.25Scene="Scenes only (25 Scenes)" +# default channel name translations +Basic.Settings.General.ChannelName.stable="Stable" +Basic.Settings.General.ChannelDescription.stable="Latest stable release" +Basic.Settings.General.ChannelName.beta="Betas / Release Candidates" +Basic.Settings.General.ChannelDescription.beta="Potentially unstable pre-release versions" + # basic mode 'stream' settings Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Stream Type" diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index 792b81444..f69096443 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -256,6 +256,55 @@ + + + Basic.Settings.General.OpenStatsOnStartup + + + + + + + Basic.Settings.General.HideOBSWindowsFromCapture.Tooltip + + + Basic.Settings.General.HideOBSWindowsFromCapture + + + + + + + + + + Basic.Settings.General.Updater + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Basic.Settings.General.UpdateChannel + + + + + + + false + + + + + + + Basic.Settings.General.EnableAutoUpdates @@ -265,22 +314,21 @@ - - - - Basic.Settings.General.OpenStatsOnStartup + + + + Qt::Horizontal - - - - - - Basic.Settings.General.HideOBSWindowsFromCapture.Tooltip + + QSizePolicy::Fixed - - Basic.Settings.General.HideOBSWindowsFromCapture + + + 170 + 20 + - + @@ -7473,6 +7521,7 @@ scrollArea_2 language theme + updateChannelBox enableAutoUpdates openStatsOnStartup warnBeforeStreamStart diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 8ac3d376a..89f487e1f 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -55,6 +55,7 @@ #include #ifdef _WIN32 +#include #include #include #else @@ -1264,6 +1265,112 @@ bool OBSApp::InitTheme() return SetTheme("System"); } +#ifdef _WIN32 +void ParseBranchesJson(const std::string &jsonString, vector &out, + std::string &error) +{ + json11::Json root; + root = json11::Json::parse(jsonString, error); + if (!error.empty() || !root.is_array()) + return; + + for (const json11::Json &item : root.array_items()) { +#ifdef _WIN32 + if (!item["windows"].bool_value()) + continue; +#endif + + UpdateBranch branch = { + QString::fromStdString(item["name"].string_value()), + QString::fromStdString( + item["display_name"].string_value()), + QString::fromStdString( + item["description"].string_value()), + item["enabled"].bool_value(), + item["visible"].bool_value(), + }; + out.push_back(branch); + } +} + +bool LoadBranchesFile(vector &out) +{ + string error; + string branchesText; + + BPtr branchesFilePath = + GetConfigPathPtr("obs-studio/updates/branches.json"); + + QFile branchesFile(branchesFilePath.Get()); + if (!branchesFile.open(QIODevice::ReadOnly)) { + error = "Opening file failed."; + goto fail; + } + + branchesText = branchesFile.readAll(); + if (branchesText.empty()) { + error = "File empty."; + goto fail; + } + + ParseBranchesJson(branchesText, out, error); + if (error.empty()) + return !out.empty(); + +fail: + blog(LOG_WARNING, "Loading branches from file failed: %s", + error.c_str()); + return false; +} +#endif + +void OBSApp::SetBranchData(const string &data) +{ +#ifdef _WIN32 + string error; + vector result; + + ParseBranchesJson(data, result, error); + + if (!error.empty()) { + blog(LOG_WARNING, "Reading branches JSON response failed: %s", + error.c_str()); + return; + } + + if (!result.empty()) + updateBranches = result; + + branches_loaded = true; +#else + UNUSED_PARAMETER(data); +#endif +} + +std::vector OBSApp::GetBranches() +{ + vector out; + /* Always ensure the default branch exists */ + out.push_back(UpdateBranch{"stable", "", "", true, true}); + +#ifdef _WIN32 + if (!branches_loaded) { + vector result; + if (LoadBranchesFile(result)) + updateBranches = result; + + branches_loaded = true; + } +#endif + + /* Copy additional branches to result (if any) */ + if (!updateBranches.empty()) + out.insert(out.end(), updateBranches.begin(), + updateBranches.end()); + + return out; +} + OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store) : QApplication(argc, argv), profilerNameStore(store) { diff --git a/UI/obs-app.hpp b/UI/obs-app.hpp index 63bcc5994..904dd6c7d 100644 --- a/UI/obs-app.hpp +++ b/UI/obs-app.hpp @@ -74,6 +74,14 @@ struct OBSThemeMeta { std::string author; }; +struct UpdateBranch { + QString name; + QString display_name; + QString description; + bool is_enabled; + bool is_visible; +}; + class OBSApp : public QApplication { Q_OBJECT @@ -86,6 +94,8 @@ private: TextLookup textLookup; QPointer mainWindow; profiler_name_store_t *profilerNameStore = nullptr; + std::vector updateBranches; + bool branches_loaded = false; bool libobs_initialized = false; @@ -142,6 +152,9 @@ public: bool SetTheme(std::string name, std::string path = ""); inline bool IsThemeDark() const { return themeDarkMode; }; + void SetBranchData(const std::string &data); + std::vector GetBranches(); + inline lookup_t *GetTextLookup() const { return textLookup; } inline const char *GetString(const char *lookupVal) const diff --git a/UI/win-update/win-update.cpp b/UI/win-update/win-update.cpp index 069a3818a..b4d19081e 100644 --- a/UI/win-update/win-update.cpp +++ b/UI/win-update/win-update.cpp @@ -36,6 +36,18 @@ extern QCef *cef; #define WIN_MANIFEST_URL "https://obsproject.com/update_studio/manifest.json" #endif +#ifndef WIN_MANIFEST_BASE_URL +#define WIN_MANIFEST_BASE_URL "https://obsproject.com/update_studio/" +#endif + +#ifndef WIN_BRANCHES_URL +#define WIN_BRANCHES_URL "https://obsproject.com/update_studio/branches.json" +#endif + +#ifndef WIN_DEFAULT_BRANCH +#define WIN_DEFAULT_BRANCH "stable" +#endif + #ifndef WIN_WHATSNEW_URL #define WIN_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" #endif @@ -399,8 +411,30 @@ try { /* ------------------------------------------------------------------------ */ +#if defined(OBS_RELEASE_CANDIDATE) && OBS_RELEASE_CANDIDATE > 0 +#define CUR_VER \ + ((uint64_t)OBS_RELEASE_CANDIDATE_VER << 16ULL | OBS_RELEASE_CANDIDATE \ + << 8ULL) +#define PRE_RELEASE true +#elif OBS_BETA > 0 +#define CUR_VER ((uint64_t)OBS_BETA_VER << 16ULL | OBS_BETA) +#define PRE_RELEASE true +#elif defined(OBS_COMMIT) +#define CUR_VER 1 << 16ULL +#define CUR_COMMIT OBS_COMMIT +#define PRE_RELEASE true +#else +#define CUR_VER ((uint64_t)LIBOBS_API_VER << 16ULL) +#define PRE_RELEASE false +#endif + +#ifndef CUR_COMMIT +#define CUR_COMMIT "00000000" +#endif + static bool ParseUpdateManifest(const char *manifest, bool *updatesAvailable, - string ¬es_str, int &updateVer) + string ¬es_str, uint64_t &updateVer, + string &branch) try { string error; @@ -415,8 +449,11 @@ try { int major = root["version_major"].int_value(); int minor = root["version_minor"].int_value(); int patch = root["version_patch"].int_value(); + int rc = root["rc"].int_value(); + int beta = root["beta"].int_value(); + string commit_hash = root["commit"].string_value(); - if (major == 0) + if (major == 0 && commit_hash.empty()) throw strprintf("Invalid version number: %d.%d.%d", major, minor, patch); @@ -430,11 +467,35 @@ try { if (!packages.is_array()) throw string("'packages' value invalid"); - int cur_ver = LIBOBS_API_VER; - int new_ver = MAKE_SEMANTIC_VERSION(major, minor, patch); + uint64_t cur_ver; + uint64_t new_ver; + + if (commit_hash.empty()) { + cur_ver = CUR_VER; + new_ver = MAKE_SEMANTIC_VERSION( + (uint64_t)major, (uint64_t)minor, (uint64_t)patch); + new_ver <<= 16; + /* RC builds are shifted so that rc1 and beta1 versions do not result + * in the same new_ver. */ + if (rc > 0) + new_ver |= (uint64_t)rc << 8; + else if (beta > 0) + new_ver |= (uint64_t)beta; + } else { + /* Test or nightly builds may not have a (valid) version number, + * so compare commit hashes instead. */ + cur_ver = stoul(CUR_COMMIT, nullptr, 16); + new_ver = stoul(commit_hash.substr(0, 8), nullptr, 16); + } updateVer = new_ver; - *updatesAvailable = new_ver > cur_ver; + + /* When using a pre-release build or non-default branch we only check if + * the manifest version is different, so that it can be rolled-back. */ + if (branch != WIN_DEFAULT_BRANCH || PRE_RELEASE) + *updatesAvailable = new_ver != cur_ver; + else + *updatesAvailable = new_ver > cur_ver; return true; @@ -443,6 +504,10 @@ try { return false; } +#undef CUR_COMMIT +#undef CUR_VER +#undef PRE_RELEASE + /* ------------------------------------------------------------------------ */ void GenerateGUID(string &guid) @@ -481,6 +546,127 @@ string GetProgramGUID() return guid; } +/* ------------------------------------------------------------------------ */ + +bool GetBranchAndUrl(string &selectedBranch, string &manifestUrl) +{ + const char *config_branch = + config_get_string(GetGlobalConfig(), "General", "UpdateBranch"); + if (!config_branch) + return true; + + bool found = false; + for (const UpdateBranch &branch : App()->GetBranches()) { + if (branch.name != config_branch) + continue; + /* A branch that is found but disabled will just silently fall back to + * the default. But if the branch was removed entirely, the user should + * be warned, so leave this false *only* if the branch was removed. */ + found = true; + + if (branch.is_enabled) { + selectedBranch = branch.name.toStdString(); + if (branch.name != WIN_DEFAULT_BRANCH) { + manifestUrl = WIN_MANIFEST_BASE_URL; + manifestUrl += "manifest_" + + branch.name.toStdString() + + ".json"; + } + } + break; + } + + return found; +} + +/* ------------------------------------------------------------------------ */ + +static bool +FetchAndVerifyFile(const char *name, const char *file, const char *url, + string &text, + const vector &extraHeaders = vector()) +{ + long responseCode; + vector headers; + string error; + string signature; + BYTE fileHash[BLAKE2_HASH_LENGTH]; + bool success; + + BPtr filePath = GetConfigPathPtr(file); + + if (!extraHeaders.empty()) { + headers.insert(headers.end(), extraHeaders.begin(), + extraHeaders.end()); + } + + /* ----------------------------------- * + * avoid downloading json again */ + + if (CalculateFileHash(filePath, fileHash)) { + char hashString[BLAKE2_HASH_STR_LENGTH]; + HashToString(fileHash, hashString); + + string header = "If-None-Match: "; + header += hashString; + headers.push_back(move(header)); + } + + /* ----------------------------------- * + * get current install GUID */ + + string guid = GetProgramGUID(); + + if (!guid.empty()) { + string header = "X-OBS2-GUID: "; + header += guid; + headers.push_back(move(header)); + } + + /* ----------------------------------- * + * get json from server */ + + success = GetRemoteFile(url, text, error, &responseCode, nullptr, "", + nullptr, headers, &signature); + + if (!success || (responseCode != 200 && responseCode != 304)) { + if (responseCode == 404) + return false; + + throw strprintf("Failed to fetch %s file: %s", name, + error.c_str()); + } + + /* ----------------------------------- * + * verify file signature */ + + if (responseCode == 200) { + success = CheckDataSignature(text, name, signature.data(), + signature.size()); + if (!success) + throw strprintf("Invalid %s signature", name); + } + + /* ----------------------------------- * + * write or load json */ + + if (responseCode == 200) { + if (!QuickWriteFile(filePath, text.data(), text.size())) + throw strprintf("Could not write file '%s'", + filePath.Get()); + } else { + if (!QuickReadFile(filePath, text)) + throw strprintf("Could not read file '%s'", + filePath.Get()); + } + + /* ----------------------------------- * + * success */ + return true; +} + +/* ------------------------------------------------------------------------ */ + void AutoUpdateThread::infoMsg(const QString &title, const QString &text) { OBSMessageBox::information(App()->GetMainWindow(), title, text); @@ -532,15 +718,12 @@ bool AutoUpdateThread::queryRepair() void AutoUpdateThread::run() try { - long responseCode; - vector extraHeaders; string text; - string error; - string signature; - CryptProvider localProvider; - BYTE manifestHash[BLAKE2_HASH_LENGTH]; + string branch = WIN_DEFAULT_BRANCH; + string manifestUrl = WIN_MANIFEST_URL; + vector extraHeaders; bool updatesAvailable = false; - bool success; + CryptProvider localProvider; struct FinishedTrigger { inline ~FinishedTrigger() @@ -550,9 +733,6 @@ try { } } finishedTrigger; - BPtr manifestPath = - GetConfigPathPtr("obs-studio\\updates\\manifest.json"); - /* ----------------------------------- * * create signature provider */ @@ -564,25 +744,20 @@ try { provider = localProvider; /* ----------------------------------- * - * avoid downloading manifest again */ + * get branches from server */ - if (CalculateFileHash(manifestPath, manifestHash)) { - char hashString[BLAKE2_HASH_STR_LENGTH]; - HashToString(manifestHash, hashString); - - string header = "If-None-Match: "; - header += hashString; - extraHeaders.push_back(move(header)); - } + if (FetchAndVerifyFile("branches", "obs-studio\\updates\\branches.json", + WIN_BRANCHES_URL, text)) + App()->SetBranchData(text); /* ----------------------------------- * - * get current install GUID */ + * get branches from server */ - string guid = GetProgramGUID(); - if (!guid.empty()) { - string header = "X-OBS2-GUID: "; - header += guid; - extraHeaders.push_back(move(header)); + if (!GetBranchAndUrl(branch, manifestUrl)) { + config_set_string(GetGlobalConfig(), "General", "UpdateBranch", + WIN_DEFAULT_BRANCH); + info(QTStr("Updater.BranchNotFound.Title"), + QTStr("Updater.BranchNotFound.Text")); } /* allow server to know if this was a manual update check in case @@ -593,50 +768,20 @@ try { /* ----------------------------------- * * get manifest from server */ - success = GetRemoteFile(WIN_MANIFEST_URL, text, error, &responseCode, - nullptr, "", nullptr, extraHeaders, &signature); - - if (!success || (responseCode != 200 && responseCode != 304)) { - if (responseCode == 404) - return; - - throw strprintf("Failed to fetch manifest file: %s", - error.c_str()); - } - - /* ----------------------------------- * - * verify file signature */ - - /* a new file must be digitally signed */ - if (responseCode == 200) { - success = CheckDataSignature(text, "manifest", signature.data(), - signature.size()); - if (!success) - throw string("Invalid manifest signature"); - } - - /* ----------------------------------- * - * write or load manifest */ - - if (responseCode == 200) { - if (!QuickWriteFile(manifestPath, text.data(), text.size())) - throw strprintf("Could not write file '%s'", - manifestPath.Get()); - } else { - if (!QuickReadFile(manifestPath, text)) - throw strprintf("Could not read file '%s'", - manifestPath.Get()); - } + text.clear(); + if (!FetchAndVerifyFile("manifest", + "obs-studio\\updates\\manifest.json", + manifestUrl.c_str(), text, extraHeaders)) + return; /* ----------------------------------- * * check manifest for update */ string notes; - int updateVer = 0; + uint64_t updateVer = 0; - success = ParseUpdateManifest(text.c_str(), &updatesAvailable, notes, - updateVer); - if (!success) + if (!ParseUpdateManifest(text.c_str(), &updatesAvailable, notes, + updateVer, branch)) throw string("Failed to parse manifest"); if (!updatesAvailable && !repairMode) { @@ -653,8 +798,8 @@ try { /* ----------------------------------- * * skip this version if set to skip */ - int skipUpdateVer = config_get_int(GetGlobalConfig(), "General", - "SkipUpdateVersion"); + uint64_t skipUpdateVer = config_get_uint(GetGlobalConfig(), "General", + "SkipUpdateVersion"); if (!manualUpdate && updateVer == skipUpdateVer && !repairMode) return; @@ -682,8 +827,8 @@ try { return; } else if (queryResult == OBSUpdate::Skip) { - config_set_int(GetGlobalConfig(), "General", - "SkipUpdateVersion", updateVer); + config_set_uint(GetGlobalConfig(), "General", + "SkipUpdateVersion", updateVer); return; } } @@ -713,16 +858,19 @@ try { execInfo.cbSize = sizeof(execInfo); execInfo.lpFile = wUpdateFilePath; -#ifndef UPDATE_CHANNEL -#define UPDATE_ARG_SUFFIX L"" -#else -#define UPDATE_ARG_SUFFIX UPDATE_CHANNEL -#endif - if (App()->IsPortableMode()) - execInfo.lpParameters = UPDATE_ARG_SUFFIX L" Portable"; - else - execInfo.lpParameters = UPDATE_ARG_SUFFIX; + string parameters = ""; + if (App()->IsPortableMode()) + parameters += "--portable"; + if (branch != WIN_DEFAULT_BRANCH) + parameters += "--branch=" + branch; + + BPtr lpParameters; + size = os_utf8_to_wcs_ptr(parameters.c_str(), 0, &lpParameters); + if (!size && !parameters.empty()) + throw string("Could not convert parameters to wide"); + + execInfo.lpParameters = lpParameters; execInfo.lpDirectory = cwd; execInfo.nShow = SW_SHOWNORMAL; @@ -737,8 +885,6 @@ try { * in case of issues with the new version */ config_set_int(GetGlobalConfig(), "General", "LastUpdateCheck", 0); config_set_int(GetGlobalConfig(), "General", "SkipUpdateVersion", 0); - config_set_string(GetGlobalConfig(), "General", "InstallGUID", - guid.c_str()); QMetaObject::invokeMethod(App()->GetMainWindow(), "close"); @@ -750,18 +896,8 @@ try { void WhatsNewInfoThread::run() try { - long responseCode; - vector extraHeaders; string text; - string error; - string signature; CryptProvider localProvider; - BYTE whatsnewHash[BLAKE2_HASH_LENGTH]; - bool success; - - BPtr whatsnewPath = - GetConfigPathPtr("obs-studio\\updates\\whatsnew.json"); - /* ----------------------------------- * * create signature provider */ @@ -772,71 +908,11 @@ try { provider = localProvider; - /* ----------------------------------- * - * avoid downloading json again */ - - if (CalculateFileHash(whatsnewPath, whatsnewHash)) { - char hashString[BLAKE2_HASH_STR_LENGTH]; - HashToString(whatsnewHash, hashString); - - string header = "If-None-Match: "; - header += hashString; - extraHeaders.push_back(move(header)); + if (FetchAndVerifyFile("whatsnew", "obs-studio\\updates\\whatsnew.json", + WIN_WHATSNEW_URL, text)) { + emit Result(QString::fromStdString(text)); } - /* ----------------------------------- * - * get current install GUID */ - - string guid = GetProgramGUID(); - - if (!guid.empty()) { - string header = "X-OBS2-GUID: "; - header += guid; - extraHeaders.push_back(move(header)); - } - - /* ----------------------------------- * - * get json from server */ - - success = GetRemoteFile(WIN_WHATSNEW_URL, text, error, &responseCode, - nullptr, "", nullptr, extraHeaders, &signature); - - if (!success || (responseCode != 200 && responseCode != 304)) { - if (responseCode == 404) - return; - - throw strprintf("Failed to fetch whatsnew file: %s", - error.c_str()); - } - - /* ----------------------------------- * - * verify file signature */ - - if (responseCode == 200) { - success = CheckDataSignature(text, "whatsnew", signature.data(), - signature.size()); - if (!success) - throw string("Invalid whatsnew signature"); - } - - /* ----------------------------------- * - * write or load json */ - - if (responseCode == 200) { - if (!QuickWriteFile(whatsnewPath, text.data(), text.size())) - throw strprintf("Could not write file '%s'", - whatsnewPath.Get()); - } else { - if (!QuickReadFile(whatsnewPath, text)) - throw strprintf("Could not read file '%s'", - whatsnewPath.Get()); - } - - /* ----------------------------------- * - * success */ - - emit Result(QString::fromUtf8(text.c_str())); - } catch (string &text) { blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); } diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index feb333b3e..cfdb76e09 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -373,6 +373,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) /* clang-format off */ HookWidget(ui->language, COMBO_CHANGED, GENERAL_CHANGED); HookWidget(ui->theme, COMBO_CHANGED, GENERAL_CHANGED); + HookWidget(ui->updateChannelBox, COMBO_CHANGED, GENERAL_CHANGED); HookWidget(ui->enableAutoUpdates, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->openStatsOnStartup, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->hideOBSFromCapture, CHECK_CHANGED, GENERAL_CHANGED); @@ -580,6 +581,17 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) #if !defined(_WIN32) && !defined(__APPLE__) delete ui->enableAutoUpdates; ui->enableAutoUpdates = nullptr; + delete ui->updateChannelBox; + ui->updateChannelBox = nullptr; + delete ui->updateSettingsGroupBox; + ui->updateSettingsGroupBox = nullptr; +#elif defined(__APPLE__) + delete ui->updateChannelBox; + ui->updateChannelBox = nullptr; +#else + // Hide update section if disabled + if (App()->IsUpdaterDisabled()) + ui->updateSettingsGroupBox->hide(); #endif // Remove the Advanced Audio section if monitoring is not supported, as the monitoring device selection is the only item in the group box. @@ -1192,6 +1204,73 @@ void OBSBasicSettings::LoadThemeList() ui->theme->setCurrentIndex(idx); } +#ifdef _WIN32 +void TranslateBranchInfo(const QString &name, QString &displayName, + QString &description) +{ + QString translatedName = + QTStr("Basic.Settings.General.ChannelName." + name.toUtf8()); + QString translatedDesc = QTStr( + "Basic.Settings.General.ChannelDescription." + name.toUtf8()); + + if (!translatedName.startsWith("Basic.Settings.")) + displayName = translatedName; + if (!translatedDesc.startsWith("Basic.Settings.")) + description = translatedDesc; +} +#endif + +void OBSBasicSettings::LoadBranchesList() +{ +#ifdef _WIN32 + bool configBranchRemoved = true; + QString configBranch = + config_get_string(GetGlobalConfig(), "General", "UpdateBranch"); + + for (const UpdateBranch &branch : App()->GetBranches()) { + if (branch.name == configBranch) + configBranchRemoved = false; + if (!branch.is_visible && branch.name != configBranch) + continue; + + QString displayName = branch.display_name; + QString description = branch.description; + + TranslateBranchInfo(branch.name, displayName, description); + QString itemDesc = displayName + " - " + description; + + if (!branch.is_enabled) { + itemDesc.prepend(" "); + itemDesc.prepend(QTStr( + "Basic.Settings.General.UpdateChannelDisabled")); + } else if (branch.name == "stable") { + itemDesc.append(" "); + itemDesc.append(QTStr( + "Basic.Settings.General.UpdateChannelDefault")); + } + + ui->updateChannelBox->addItem(itemDesc, branch.name); + + // Disable item if branch is disabled + if (!branch.is_enabled) { + QStandardItemModel *model = + dynamic_cast( + ui->updateChannelBox->model()); + QStandardItem *item = + model->item(ui->updateChannelBox->count() - 1); + item->setFlags(Qt::NoItemFlags); + } + } + + // Fall back to default if not yet set or user-selected branch has been removed + if (configBranch.isEmpty() || configBranchRemoved) + configBranch = "stable"; + + int idx = ui->updateChannelBox->findData(configBranch); + ui->updateChannelBox->setCurrentIndex(idx); +#endif +} + void OBSBasicSettings::LoadGeneralSettings() { loading = true; @@ -1203,6 +1282,10 @@ void OBSBasicSettings::LoadGeneralSettings() bool enableAutoUpdates = config_get_bool(GetGlobalConfig(), "General", "EnableAutoUpdates"); ui->enableAutoUpdates->setChecked(enableAutoUpdates); + +#ifdef _WIN32 + LoadBranchesList(); +#endif #endif bool openStatsOnStartup = config_get_bool(main->Config(), "General", "OpenStatsOnStartup"); @@ -3036,6 +3119,16 @@ void OBSBasicSettings::SaveGeneralSettings() ui->enableAutoUpdates->isChecked()); #endif #ifdef _WIN32 + int branchIdx = ui->updateChannelBox->currentIndex(); + QString branchName = + ui->updateChannelBox->itemData(branchIdx).toString(); + + if (WidgetChanged(ui->updateChannelBox)) { + config_set_string(GetGlobalConfig(), "General", "UpdateBranch", + QT_TO_UTF8(branchName)); + forceUpdateCheck = true; + } + if (ui->hideOBSFromCapture && WidgetChanged(ui->hideOBSFromCapture)) { bool hide_window = ui->hideOBSFromCapture->isChecked(); config_set_bool(GetGlobalConfig(), "BasicWindow", @@ -4142,6 +4235,11 @@ bool OBSBasicSettings::AskIfCanCloseSettings() forceAuthReload = false; } + if (forceUpdateCheck) { + main->CheckForUpdates(false); + forceUpdateCheck = false; + } + return canCloseSettings; } diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index c312eec89..80a6999cb 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -120,6 +120,7 @@ private: int pageIndex = 0; bool loading = true; bool forceAuthReload = false; + bool forceUpdateCheck = false; std::string savedTheme; int sampleRateIndex = 0; int channelIndex = 0; @@ -250,6 +251,7 @@ private: /* general */ void LoadLanguageList(); void LoadThemeList(); + void LoadBranchesList(); /* stream */ void InitStreamPage();