From ac3dbc4f78ac5dbb818f884d794a47987a1428d0 Mon Sep 17 00:00:00 2001 From: Magno Ciqueira Date: Wed, 29 Jul 2026 22:23:37 -0300 Subject: [PATCH] feat: add MCOpenNex update presenter --- ARCHITECTURE.md | 16 +++ THIRD_PARTY_NOTICES.md | 16 ++- src/MCColorEqualizer.cpp | 173 ++++++++++++------------- src/MCOpenNexPlatform.h | 12 ++ src/MCOpenNexPlatformApple.mm | 71 +++++++++++ src/MCOpenNexPlatformPosix.cpp | 35 ++++++ src/MCOpenNexPlatformWindows.cpp | 104 +++++++++++++++ src/MCOpenNexPresenter.cpp | 210 +++++++++++++++++++++++++++++++ src/MCOpenNexPresenter.h | 37 ++++++ 9 files changed, 583 insertions(+), 91 deletions(-) create mode 100644 src/MCOpenNexPlatform.h create mode 100644 src/MCOpenNexPlatformApple.mm create mode 100644 src/MCOpenNexPlatformPosix.cpp create mode 100644 src/MCOpenNexPlatformWindows.cpp create mode 100644 src/MCOpenNexPresenter.cpp create mode 100644 src/MCOpenNexPresenter.h diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 49ba76a..8a6a247 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -44,6 +44,20 @@ └───────────────────────┘ ``` +Update discovery is intentionally outside this image pipeline: + +```text +OFX instance/UI action + → shared MCOpenNex client + → background HTTPS manifest request + → immutable snapshot + → Support group status / Open MCNexus button +``` + +The first live instance starts one asynchronous check. Instances in the same +host process share the client and its check interval. UI actions only pull the +latest snapshot; render actions never start network work or mutate the host UI. + ## Main files | File | Responsibility | @@ -53,6 +67,8 @@ | `src/MetalKernel.mm` | macOS Metal pixel-processing implementation | | `src/CudaKernel.cu` | Windows/Linux CUDA pixel-processing implementation | | `src/EQParams.h` | Shared parameter layout | +| `src/MCOpenNexPresenter.*` | Pull-based update/notice presenter and shared client lifetime | +| `src/MCOpenNexPlatform*` | Allowlisted native URL opening for MCNexus deep links and HTTPS | | `../tests/` | Shared host-side OKLCH numerical and regression tests | ## Model domains diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 48afe05..5aa82d4 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -3,7 +3,21 @@ This file lists third-party material actually used by Color Equalizer. It does not license the project-owned code, which is governed by `LICENSE.md`. -Audit date: 2026-06-20. +Audit date: 2026-07-29. + +## MCOpenNex SDK + +Color Equalizer statically links the MCOpenNex SDK for anonymous update +discovery and product notices. + +- Project: +- Version: `0.1.x` +- License: Apache License 2.0 +- Used in: `src/MCOpenNexPresenter.*` and the MC OFX build wrapper. + +Binary distributions reproduce the MCOpenNex Apache license and NOTICE in the +OFX bundle. MCOpenNex does not activate licenses, authorize downloads, or +install updates. ## Oklab and OKLCH diff --git a/src/MCColorEqualizer.cpp b/src/MCColorEqualizer.cpp index c2a17f0..f339496 100644 --- a/src/MCColorEqualizer.cpp +++ b/src/MCColorEqualizer.cpp @@ -15,20 +15,14 @@ #include "MCColorEqualizer.h" #include "ColorMath.h" #include "EQParams.h" +#include "MCOpenNexPlatform.h" +#include "MCOpenNexPresenter.h" #include -#include #include #include #include -#ifdef _WIN32 -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#include -#endif #include "ofxsImageEffect.h" #include "ofxsMultiThread.h" #include "ofxsProcessing.h" @@ -62,6 +56,8 @@ #define kParamSatMaster "satMaster" #define kParamLumMaster "lumMaster" #define kParamAboutHelp "aboutHelp" +#define kParamUpdateStatus "updateStatus" +#define kParamCheckUpdates "checkUpdates" #define kParamAppMCNexus "appMCNexus" #define kAboutHelpUrl "https://github.com/ciqueira/ColorEqualizer" @@ -99,89 +95,15 @@ static int mapSpaceType(int uiIndex) { } } -static void openExternalUrl(const char *url) { -#ifdef _WIN32 - ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL); -#elif defined(__APPLE__) - std::string command = "open \""; - command += url; - command += "\" >/dev/null 2>&1"; - std::system(command.c_str()); -#else - std::string command = "xdg-open \""; - command += url; - command += "\" >/dev/null 2>&1 &"; - std::system(command.c_str()); -#endif -} - static void openMCNexusApp() { -#ifdef __APPLE__ - std::system( - "open -a MCNexus >/dev/null 2>&1 || open \"/Applications/MCNexus.app\" " - ">/dev/null 2>&1"); -#elif defined(_WIN32) - auto shellExecuteWindowsPath = [](const wchar_t *path, - const wchar_t *parameters) { - HINSTANCE result = - ShellExecuteW(nullptr, L"open", path, parameters, nullptr, SW_SHOWNORMAL); - return reinterpret_cast(result) > 32; - }; - - auto launchPowerShellHidden = [](const wchar_t *parameters) { - std::wstring commandLine = L"powershell.exe "; - commandLine += parameters; - - STARTUPINFOW startupInfo = {}; - startupInfo.cb = sizeof(startupInfo); - startupInfo.dwFlags = STARTF_USESHOWWINDOW; - startupInfo.wShowWindow = SW_HIDE; - - PROCESS_INFORMATION processInfo = {}; - const BOOL created = CreateProcessW( - nullptr, &commandLine[0], nullptr, nullptr, FALSE, CREATE_NO_WINDOW, - nullptr, nullptr, &startupInfo, &processInfo); - if (!created) { - return false; - } - CloseHandle(processInfo.hThread); - CloseHandle(processInfo.hProcess); - return true; - }; - - auto launchWindowsExecutableIfExists = [&](const wchar_t *pathWithEnvironment) { - wchar_t expanded[MAX_PATH] = {}; - const DWORD expandedLength = - ExpandEnvironmentStringsW(pathWithEnvironment, expanded, MAX_PATH); - const wchar_t *path = - (expandedLength > 0 && expandedLength < MAX_PATH) ? expanded - : pathWithEnvironment; - const DWORD attributes = GetFileAttributesW(path); - if (attributes == INVALID_FILE_ATTRIBUTES || - (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0u) { - return false; - } - return shellExecuteWindowsPath(path, nullptr); - }; - - if (launchWindowsExecutableIfExists(L"%ProgramFiles%\\MCNexus\\MCNexus.exe") || - launchWindowsExecutableIfExists( - L"%ProgramFiles(x86)%\\MCNexus\\MCNexus.exe") || - launchWindowsExecutableIfExists( - L"%LocalAppData%\\Programs\\MCNexus\\MCNexus.exe")) { + if (mcopen::openMCNexusApplication()) { return; } - - constexpr const wchar_t *kPowerShellArgs = - LR"PS(-NoProfile -WindowStyle Hidden -Command "$app = Get-StartApps | Where-Object { $_.Name -eq 'MCNexus' } | Select-Object -First 1; if ($app) { Start-Process ('shell:AppsFolder\' + $app.AppID) } else { Start-Process 'https://apps.microsoft.com/detail/9n1qqt1xc825?hl=en-US&gl=US' }")PS"; - if (launchPowerShellHidden(kPowerShellArgs)) { + if (mcopen::canOpenUrl("mcnexus://home") && + mcopen::openUrl("mcnexus://home")) { return; } - - openExternalUrl("https://apps.microsoft.com/detail/9n1qqt1xc825?hl=en-US&gl=US"); -#else - openExternalUrl("https://mcnexus.app"); -#endif + mcopen::openUrl("https://mcnexus.app"); } // ============================================================================= @@ -333,6 +255,7 @@ class MCColorEqualizerPlugin : public OFX::ImageEffect { void setupAndProcess(ColorEqualizerProcessor &p_Processor, const OFX::RenderArguments &p_Args); EQParams getActiveParams(double time); + void refreshUpdateUi(); OFX::Clip *m_SrcClip; OFX::Clip *m_DstClip; @@ -352,12 +275,18 @@ class MCColorEqualizerPlugin : public OFX::ImageEffect { // Luma OFX::DoubleParam *m_LumMaster; OFX::DoubleParam *m_Lum[10]; + + // MCOpenNex update presenter + OFX::StringParam *m_UpdateStatus; + std::unique_ptr m_UpdatePresenter; + bool m_UpdatingSupportUi; }; // ─── Constructor ────────────────────────────────────────────────────────── MCColorEqualizerPlugin::MCColorEqualizerPlugin(OfxImageEffectHandle p_Handle) - : OFX::ImageEffect(p_Handle) { + : OFX::ImageEffect(p_Handle), m_UpdateStatus(nullptr), + m_UpdatingSupportUi(false) { m_DstClip = fetchClip(kOfxImageEffectOutputClipName); m_SrcClip = fetchClip(kOfxImageEffectSimpleSourceClipName); @@ -375,19 +304,65 @@ MCColorEqualizerPlugin::MCColorEqualizerPlugin(OfxImageEffectHandle p_Handle) m_LumMaster = fetchDoubleParam(kParamLumMaster); for (int i = 0; i < 10; i++) m_Lum[i] = fetchDoubleParam(kLumNames[i]); + + m_UpdateStatus = fetchStringParam(kParamUpdateStatus); + + const OFX::ImageEffectHostDescription *host = + OFX::getImageEffectHostDescription(); + std::string hostVersion; + const char *hostName = ""; + if (host) { + hostName = host->hostName.c_str(); + if (host->versionMajor >= 0 && host->versionMinor >= 0 && + host->versionMicro >= 0) { + hostVersion = std::to_string(host->versionMajor) + "." + + std::to_string(host->versionMinor) + "." + + std::to_string(host->versionMicro); + } + } + m_UpdatePresenter.reset( + new MCOpenNexPresenter(kPluginVersion, hostName, hostVersion.c_str())); + m_UpdatePresenter->requestCheck(false); + refreshUpdateUi(); } // ─── changedParam ───────────────────────────────────────────────────────── void MCColorEqualizerPlugin::changedParam(const OFX::InstanceChangedArgs &, const std::string &p_ParamName) { + if (m_UpdatingSupportUi || p_ParamName == kParamUpdateStatus) { + return; + } + refreshUpdateUi(); + if (p_ParamName == kParamAboutHelp) { - openExternalUrl(kAboutHelpUrl); + mcopen::openUrl(kAboutHelpUrl); + } else if (p_ParamName == kParamCheckUpdates) { + // Keep the last pulled result visible while the forced check runs. + m_UpdatePresenter->requestCheck(true); } else if (p_ParamName == kParamAppMCNexus) { - openMCNexusApp(); + if (!m_UpdatePresenter->openPrimaryAction()) { + openMCNexusApp(); + } } } +void MCColorEqualizerPlugin::refreshUpdateUi() { + if (!m_UpdateStatus || !m_UpdatePresenter || m_UpdatingSupportUi) { + return; + } + m_UpdatingSupportUi = true; + const MCOpenNexPresenter::ViewState state = + m_UpdatePresenter->viewState(); + try { + m_UpdateStatus->setValue(state.status); + } catch (...) { + m_UpdatingSupportUi = false; + throw; + } + m_UpdatingSupportUi = false; +} + // ─── getActiveParams ────────────────────────────────────────────────────── EQParams MCColorEqualizerPlugin::getActiveParams(double time) { @@ -668,9 +643,27 @@ void MCColorEqualizerFactory::describeInContext( aboutHelp->setParent(*grp); page->addChild(*aboutHelp); + OFX::StringParamDescriptor *updateStatus = + p_Desc.defineStringParam(kParamUpdateStatus); + updateStatus->setLabels("Updates", "Updates", "Updates"); + updateStatus->setDefault("Update check has not run yet."); + updateStatus->setStringType(OFX::eStringTypeMultiLine); + updateStatus->setEnabled(false); + updateStatus->setIsPersistant(false); + updateStatus->setEvaluateOnChange(false); + updateStatus->setParent(*grp); + page->addChild(*updateStatus); + + OFX::PushButtonParamDescriptor *checkUpdates = + p_Desc.definePushButtonParam(kParamCheckUpdates); + checkUpdates->setLabels("Check for Updates", "Check for Updates", + "Check for Updates"); + checkUpdates->setParent(*grp); + page->addChild(*checkUpdates); + OFX::PushButtonParamDescriptor *appMCNexus = p_Desc.definePushButtonParam(kParamAppMCNexus); - appMCNexus->setLabels("App MCNexus", "App MCNexus", "App MCNexus"); + appMCNexus->setLabels("Open MCNexus", "Open MCNexus", "Open MCNexus"); #if !defined(__APPLE__) && !defined(_WIN32) appMCNexus->setEnabled(false); #endif diff --git a/src/MCOpenNexPlatform.h b/src/MCOpenNexPlatform.h new file mode 100644 index 0000000..e0a08f8 --- /dev/null +++ b/src/MCOpenNexPlatform.h @@ -0,0 +1,12 @@ +#ifndef MC_COLOR_EQUALIZER_MCOPENNEX_PLATFORM_H +#define MC_COLOR_EQUALIZER_MCOPENNEX_PLATFORM_H + +namespace mcopen { + +bool canOpenUrl(const char *url); +bool openUrl(const char *url); +bool openMCNexusApplication(); + +} // namespace mcopen + +#endif diff --git a/src/MCOpenNexPlatformApple.mm b/src/MCOpenNexPlatformApple.mm new file mode 100644 index 0000000..b1e2c5f --- /dev/null +++ b/src/MCOpenNexPlatformApple.mm @@ -0,0 +1,71 @@ +#include "MCOpenNexPlatform.h" + +#import +#import + +namespace { + +NSURL *validatedUrl(const char *value) { + if (!value || value[0] == '\0') { + return nil; + } + NSString *text = [NSString stringWithUTF8String:value]; + NSURL *url = text ? [NSURL URLWithString:text] : nil; + NSString *scheme = [[url scheme] lowercaseString]; + if (![scheme isEqualToString:@"https"] && + ![scheme isEqualToString:@"mcnexus"]) { + return nil; + } + return url; +} + +} // namespace + +namespace mcopen { + +bool canOpenUrl(const char *value) { + @autoreleasepool { + NSURL *url = validatedUrl(value); + if (!url) { + return false; + } + if ([[[url scheme] lowercaseString] isEqualToString:@"https"]) { + return true; + } + return [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:url] != + nil; + } +} + +bool openUrl(const char *value) { + @autoreleasepool { + NSURL *url = validatedUrl(value); + return url && [[NSWorkspace sharedWorkspace] openURL:url]; + } +} + +bool openMCNexusApplication() { + @autoreleasepool { + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + NSURL *applicationURL = + [workspace URLForApplicationWithBundleIdentifier:@"com.MCAppsTools"]; + if (!applicationURL) { + NSURL *fallbackURL = + [NSURL fileURLWithPath:@"/Applications/MCNexus.app"]; + if ([[NSFileManager defaultManager] + fileExistsAtPath:[fallbackURL path]]) { + applicationURL = fallbackURL; + } + } + if (!applicationURL) { + return false; + } + [workspace + openApplicationAtURL:applicationURL + configuration:[NSWorkspaceOpenConfiguration configuration] + completionHandler:nil]; + return true; + } +} + +} // namespace mcopen diff --git a/src/MCOpenNexPlatformPosix.cpp b/src/MCOpenNexPlatformPosix.cpp new file mode 100644 index 0000000..5d5b488 --- /dev/null +++ b/src/MCOpenNexPlatformPosix.cpp @@ -0,0 +1,35 @@ +#include "MCOpenNexPlatform.h" + +#include +#include + +extern char **environ; + +namespace { + +bool isSupportedUrl(const std::string &url) { + return url.find("https://") == 0 || url.find("mcnexus://") == 0; +} + +} // namespace + +namespace mcopen { + +bool canOpenUrl(const char *value) { + return value && isSupportedUrl(value); +} + +bool openUrl(const char *value) { + if (!canOpenUrl(value)) { + return false; + } + char executable[] = "xdg-open"; + char *arguments[] = {executable, const_cast(value), nullptr}; + pid_t process = 0; + return posix_spawnp(&process, executable, nullptr, nullptr, arguments, + environ) == 0; +} + +bool openMCNexusApplication() { return false; } + +} // namespace mcopen diff --git a/src/MCOpenNexPlatformWindows.cpp b/src/MCOpenNexPlatformWindows.cpp new file mode 100644 index 0000000..6759569 --- /dev/null +++ b/src/MCOpenNexPlatformWindows.cpp @@ -0,0 +1,104 @@ +#include "MCOpenNexPlatform.h" + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +#include +#include +#include + +namespace { + +std::wstring utf8ToWide(const char *value) { + if (!value || value[0] == '\0') { + return {}; + } + const int length = + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value, -1, nullptr, 0); + if (length <= 1) { + return {}; + } + std::wstring result(static_cast(length), L'\0'); + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value, -1, + &result[0], length) == 0) { + return {}; + } + result.resize(static_cast(length - 1)); + return result; +} + +bool startsWith(const std::wstring &value, const wchar_t *prefix) { + const std::wstring expected(prefix); + return value.size() >= expected.size() && + _wcsnicmp(value.c_str(), expected.c_str(), expected.size()) == 0; +} + +bool isSupportedUrl(const std::wstring &url) { + return startsWith(url, L"https://") || startsWith(url, L"mcnexus://"); +} + +bool hasMCNexusProtocolHandler() { + HKEY key = nullptr; + const LSTATUS opened = RegOpenKeyExW( + HKEY_CLASSES_ROOT, L"mcnexus\\shell\\open\\command", 0, KEY_READ, &key); + if (opened != ERROR_SUCCESS) { + return false; + } + RegCloseKey(key); + return true; +} + +} // namespace + +namespace mcopen { + +bool canOpenUrl(const char *value) { + const std::wstring url = utf8ToWide(value); + if (!isSupportedUrl(url)) { + return false; + } + return startsWith(url, L"https://") || hasMCNexusProtocolHandler(); +} + +bool openUrl(const char *value) { + const std::wstring url = utf8ToWide(value); + if (!isSupportedUrl(url)) { + return false; + } + const HINSTANCE result = + ShellExecuteW(nullptr, L"open", url.c_str(), nullptr, nullptr, + SW_SHOWNORMAL); + return reinterpret_cast(result) > 32; +} + +bool openMCNexusApplication() { + constexpr const wchar_t *candidates[] = { + L"%ProgramFiles%\\MCNexus\\MCNexus.exe", + L"%ProgramFiles(x86)%\\MCNexus\\MCNexus.exe", + L"%LocalAppData%\\Programs\\MCNexus\\MCNexus.exe"}; + for (const wchar_t *candidate : candidates) { + wchar_t expanded[MAX_PATH] = {}; + const DWORD length = + ExpandEnvironmentStringsW(candidate, expanded, MAX_PATH); + if (length == 0 || length >= MAX_PATH) { + continue; + } + const DWORD attributes = GetFileAttributesW(expanded); + if (attributes == INVALID_FILE_ATTRIBUTES || + (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0u) { + continue; + } + const HINSTANCE result = + ShellExecuteW(nullptr, L"open", expanded, nullptr, nullptr, + SW_SHOWNORMAL); + if (reinterpret_cast(result) > 32) { + return true; + } + } + return false; +} + +} // namespace mcopen diff --git a/src/MCOpenNexPresenter.cpp b/src/MCOpenNexPresenter.cpp new file mode 100644 index 0000000..059f23d --- /dev/null +++ b/src/MCOpenNexPresenter.cpp @@ -0,0 +1,210 @@ +#include "MCOpenNexPresenter.h" + +#include "MCOpenNexPlatform.h" + +#include +#include + +#ifndef MCOPENNEX_BASE_URL +#define MCOPENNEX_BASE_URL "https://sdk.mcnexus.app" +#endif + +namespace { + +const char *currentPlatform() { +#if defined(__APPLE__) + return "macos"; +#elif defined(_WIN32) + return "windows"; +#else + return "linux"; +#endif +} + +const char *currentArchitecture() { +#if defined(__aarch64__) || defined(__arm64__) + return "arm64"; +#elif defined(__x86_64__) || defined(_M_X64) + return "x86_64"; +#else + return "unknown"; +#endif +} + +std::string normalizedHostName(const char *hostName) { + const std::string value = hostName ? hostName : ""; + if (value.find("DaVinci") != std::string::npos || + value.find("Resolve") != std::string::npos) { + return "DaVinciResolve"; + } + return value; +} + +std::string statusForSnapshot(const MCOpenNexUpdateSnapshot &snapshot) { + switch (snapshot.status) { + case MCOPENNEX_UPDATE_CHECKING: + return "Checking for Color Equalizer updates..."; + case MCOPENNEX_UPDATE_UP_TO_DATE: + return "Color Equalizer is up to date."; + case MCOPENNEX_UPDATE_AVAILABLE: + return std::string("Color Equalizer ") + snapshot.latest_version + + " is available. Open MCNexus to review the update."; + case MCOPENNEX_UPDATE_OFFLINE: + return "Update check unavailable while offline."; + case MCOPENNEX_UPDATE_ERROR: + return "Could not check for updates. Try again later."; + case MCOPENNEX_UPDATE_IDLE: + default: + return "Update check has not run yet."; + } +} + +} // namespace + +struct MCOpenNexRuntime { + explicit MCOpenNexRuntime(MCOpenNexHandle *value) : handle(value) {} + ~MCOpenNexRuntime() { mcopennex_destroy(handle); } + + MCOpenNexHandle *handle; +}; + +namespace { + +std::mutex &sharedRuntimeMutex() { + static std::mutex value; + return value; +} + +std::weak_ptr &sharedRuntime() { + static std::weak_ptr value; + return value; +} + +} // namespace + +MCOpenNexPresenter::MCOpenNexPresenter(const char *currentVersion, + const char *hostName, + const char *hostVersion) { + std::lock_guard lock(sharedRuntimeMutex()); + runtime_ = sharedRuntime().lock(); + if (runtime_) { + return; + } + + const std::string normalizedHost = normalizedHostName(hostName); + MCOpenNexConfig config; + mcopennex_config_init(&config); + config.tenant_id = "colorequalizer-oss"; + config.artifact_id = "default"; + config.current_version = currentVersion; + config.channel = "stable"; + config.platform = currentPlatform(); + config.architecture = currentArchitecture(); + config.locale = "en"; + config.host_name = normalizedHost.c_str(); + config.host_version = hostVersion; + config.base_url = MCOPENNEX_BASE_URL; + config.request_timeout_ms = 5000; + config.can_open_url = canOpenUrl; + config.open_url = openUrl; + MCOpenNexHandle *handle = mcopennex_create(&config); + if (handle) { + runtime_ = std::make_shared(handle); + sharedRuntime() = runtime_; + } +} + +MCOpenNexPresenter::~MCOpenNexPresenter() = default; + +bool MCOpenNexPresenter::isAvailable() const { + return runtime_ && runtime_->handle; +} + +void MCOpenNexPresenter::requestCheck(bool force) { + if (isAvailable()) { + mcopennex_request_check(runtime_->handle, force ? 1 : 0); + } +} + +MCOpenNexPresenter::ViewState MCOpenNexPresenter::viewState() const { + ViewState result; + if (!isAvailable()) { + result.status = "Update service is unavailable."; + return result; + } + + MCOpenNexUpdateSnapshot snapshot{}; + snapshot.struct_size = sizeof(snapshot); + if (mcopennex_get_snapshot(runtime_->handle, &snapshot) != MCOPENNEX_OK) { + result.status = "Could not read update status."; + return result; + } + result.status = statusForSnapshot(snapshot); + result.hasAction = snapshot.has_update != 0 && + (snapshot.deep_link[0] != '\0' || + snapshot.fallback_url[0] != '\0'); + + MCOpenNexNotice notice{}; + notice.struct_size = sizeof(notice); + if (mcopennex_get_active_notice(runtime_->handle, ¬ice) == MCOPENNEX_OK && + notice.available != 0) { + if (!result.status.empty()) { + result.status += "\n"; + } + result.status += notice.title; + if (notice.message[0] != '\0') { + result.status += ": "; + result.status += notice.message; + } + result.hasAction = + result.hasAction || notice.deep_link[0] != '\0' || + notice.fallback_url[0] != '\0'; + } + return result; +} + +bool MCOpenNexPresenter::openPrimaryAction() { + if (!isAvailable()) { + return false; + } + + MCOpenNexUpdateSnapshot snapshot{}; + snapshot.struct_size = sizeof(snapshot); + if (mcopennex_get_snapshot(runtime_->handle, &snapshot) == MCOPENNEX_OK && + snapshot.has_update != 0) { + if (snapshot.deep_link[0] != '\0' && + mcopen::canOpenUrl(snapshot.deep_link)) { + return mcopennex_open_update_action(runtime_->handle) == MCOPENNEX_OK; + } + if (mcopen::openMCNexusApplication()) { + return true; + } + return mcopennex_open_update_action(runtime_->handle) == MCOPENNEX_OK; + } + + MCOpenNexNotice notice{}; + notice.struct_size = sizeof(notice); + if (mcopennex_get_active_notice(runtime_->handle, ¬ice) != + MCOPENNEX_OK || + notice.available == 0) { + return false; + } + if (notice.deep_link[0] != '\0' && + mcopen::canOpenUrl(notice.deep_link)) { + return mcopennex_open_notice_action(runtime_->handle, notice.id) == + MCOPENNEX_OK; + } + if (mcopen::openMCNexusApplication()) { + return true; + } + return mcopennex_open_notice_action(runtime_->handle, notice.id) == + MCOPENNEX_OK; +} + +int MCOpenNexPresenter::canOpenUrl(void *, const char *url) { + return mcopen::canOpenUrl(url) ? 1 : 0; +} + +int MCOpenNexPresenter::openUrl(void *, const char *url) { + return mcopen::openUrl(url) ? 1 : 0; +} diff --git a/src/MCOpenNexPresenter.h b/src/MCOpenNexPresenter.h new file mode 100644 index 0000000..94eb0f0 --- /dev/null +++ b/src/MCOpenNexPresenter.h @@ -0,0 +1,37 @@ +#ifndef MC_COLOR_EQUALIZER_MCOPENNEX_PRESENTER_H +#define MC_COLOR_EQUALIZER_MCOPENNEX_PRESENTER_H + +#include + +#include +#include + +struct MCOpenNexRuntime; + +class MCOpenNexPresenter { +public: + struct ViewState { + std::string status; + bool hasAction = false; + }; + + MCOpenNexPresenter(const char *currentVersion, const char *hostName, + const char *hostVersion); + ~MCOpenNexPresenter(); + + MCOpenNexPresenter(const MCOpenNexPresenter &) = delete; + MCOpenNexPresenter &operator=(const MCOpenNexPresenter &) = delete; + + bool isAvailable() const; + void requestCheck(bool force); + ViewState viewState() const; + bool openPrimaryAction(); + +private: + static int canOpenUrl(void *userData, const char *url); + static int openUrl(void *userData, const char *url); + + std::shared_ptr runtime_; +}; + +#endif