diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..1c418b0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,68 @@ +Language: Cpp +BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignOperands: true +AlignAfterOpenBracket: BlockIndent +AlignTrailingComments: false +AlwaysBreakTemplateDeclarations: Yes +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBraces: Custom +BreakConstructorInitializers: BeforeComma +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 0 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +PackConstructorInitializers: CurrentLine +ContinuationIndentWidth: 4 +AllowShortIfStatementsOnASingleLine: AllIfsAndElse +AllowShortLoopsOnASingleLine: true +AllowShortFunctionsOnASingleLine: None +AllowAllArgumentsOnNextLine: false +BinPackArguments: false +BinPackParameters: false +PenaltyBreakAssignment: 1000000 +IncludeCategories: + - Regex: '^<.*' + Priority: 1 + - Regex: '^".*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeBlocks: Preserve +IncludeIsMainRegex: '([-_](test|unittest))?$' +SortIncludes: false +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +InsertNewlineAtEOF: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +FixNamespaceComments: false +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: false +SpaceBeforeRangeBasedForLoopColon: false +SpaceInEmptyParentheses: false +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +TabWidth: 4 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 5002fcd..854b115 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,13 @@ add_subdirectory(extern/glfw) set(REFLECTCPP_BUILD_SHARED OFF CACHE BOOL "" FORCE) set(REFLECTCPP_BUILD_TESTS OFF CACHE BOOL "" FORCE) add_subdirectory(extern/reflect-cpp) +if (MINGW) + target_compile_options(reflectcpp PRIVATE + -Wa,-mbig-obj + $<$:-O1> + $<$:-g0> + ) +endif () set(TINYFD_DIR ${CMAKE_SOURCE_DIR}/extern/tinyfiledialogs) add_library(tinyfiledialogs STATIC ${TINYFD_DIR}/tinyfiledialogs.c) @@ -164,6 +171,7 @@ add_executable(${PROJECT_NAME} src/gui/windows/avd_options.cpp src/gui/windows/delete_avd.cpp src/gui/windows/create_avd.cpp + src/gui/windows/device_profile.cpp src/gui/windows/install_image.cpp src/gui/windows/main_menu_bar.cpp src/gui/windows/onboarding.cpp @@ -230,7 +238,11 @@ endif () if (WIN32) configure_file(${CMAKE_SOURCE_DIR}/resources.rc.in ${CMAKE_BINARY_DIR}/resources.rc @ONLY) - target_link_options(${PROJECT_NAME} PRIVATE "/SUBSYSTEM:WINDOWS" "/ENTRY:mainCRTStartup") + if (MSVC) + target_link_options(${PROJECT_NAME} PRIVATE "/SUBSYSTEM:WINDOWS" "/ENTRY:mainCRTStartup") + elseif (MINGW) + target_link_options(${PROJECT_NAME} PRIVATE "-mwindows") + endif () target_sources(${PROJECT_NAME} PRIVATE ${CMAKE_BINARY_DIR}/resources.rc) elseif (APPLE) target_compile_definitions(${PROJECT_NAME} PRIVATE GL_SILENCE_DEPRECATION) diff --git a/src/core/app_settings.h b/src/core/app_settings.h index 158c7c3..f31784b 100644 --- a/src/core/app_settings.h +++ b/src/core/app_settings.h @@ -13,4 +13,4 @@ namespace CoreDeck { void SaveAppSettings(const AppSettings &settings); } -#endif //COREDECK_APP_SETTINGS_H +#endif // COREDECK_APP_SETTINGS_H diff --git a/src/core/app_settings_types.h b/src/core/app_settings_types.h index 9cd4736..eaf8e62 100644 --- a/src/core/app_settings_types.h +++ b/src/core/app_settings_types.h @@ -19,4 +19,4 @@ namespace CoreDeck { }; } -#endif //COREDECK_APP_SETTINGS_TYPES_H +#endif // COREDECK_APP_SETTINGS_TYPES_H diff --git a/src/core/avd.cpp b/src/core/avd.cpp index b842e83..dc60a21 100644 --- a/src/core/avd.cpp +++ b/src/core/avd.cpp @@ -2,14 +2,16 @@ // Created by AbdulMuaz Aqeel on 05/04/2026. // -#include -#include +#include #include +#include #include +#include #include "avd.h" #include "paths.h" #include "process.h" +#include "utilities.h" namespace CoreDeck { static std::unordered_map ParseConfigFile(const std::string &path) { @@ -39,6 +41,92 @@ namespace CoreDeck { return config; } + static std::vector SplitConfigList(const std::string &value) { + std::vector items; + std::stringstream stream(value); + std::string item; + while (std::getline(stream, item, ',')) { + while (!item.empty() && (item.back() == ' ' || item.back() == '\t')) item.pop_back(); + while (!item.empty() && (item.front() == ' ' || item.front() == '\t')) item.erase(item.begin()); + if (!item.empty()) items.push_back(item); + } + return items; + } + + static bool HasTag(const std::vector &tags, const std::string &needle) { + return std::ranges::any_of(tags, [&](const std::string &tag) { + return LowerCopy(tag) == needle; + }); + } + + static void ExtractSystemImageInfo(AvdInfo &avd, const std::unordered_map &config) { + if (const auto it = config.find("image.sysdir.1"); it != config.end()) { + avd.SystemImagePath = it->second; + + std::string sysdir = it->second; + std::ranges::replace(sysdir, '\\', '/'); + + if (auto start = sysdir.find("android-"); start != std::string::npos) { + start += 8; + if (const auto end = sysdir.find('/', start); end != std::string::npos) { + avd.ApiLevel = sysdir.substr(start, end - start); + + const auto variantStart = end + 1; + if (const auto variantEnd = sysdir.find('/', variantStart); variantEnd != std::string::npos) { + avd.SystemImageVariant = sysdir.substr(variantStart, variantEnd - variantStart); + + const auto abiStart = variantEnd + 1; + if (const auto abiEnd = sysdir.find('/', abiStart); abiEnd != std::string::npos && avd.Abi.empty()) { + avd.Abi = sysdir.substr(abiStart, abiEnd - abiStart); + } + } + } + } + } + + if (const auto it = config.find("tag.id"); it != config.end()) { + avd.SystemImageTagId = it->second; + } + if (const auto it = config.find("tag.display"); it != config.end()) { + avd.SystemImageTagDisplay = it->second; + } + if (const auto it = config.find("tag.ids"); it != config.end()) { + avd.SystemImageTagIds = SplitConfigList(it->second); + } else if (!avd.SystemImageTagId.empty()) { + avd.SystemImageTagIds = {avd.SystemImageTagId}; + } + if (const auto it = config.find("tag.displaynames"); it != config.end()) { + avd.SystemImageTagDisplayNames = SplitConfigList(it->second); + } else if (!avd.SystemImageTagDisplay.empty()) { + avd.SystemImageTagDisplayNames = {avd.SystemImageTagDisplay}; + } + + const std::string variant = LowerCopy(avd.SystemImageVariant); + const std::string tagId = LowerCopy(avd.SystemImageTagId); + const std::string tagDisplay = LowerCopy(avd.SystemImageTagDisplay); + const std::string tagDisplayNames = LowerCopy(StrConcat( + avd.SystemImageTagDisplay, + " ", + config.contains("tag.displaynames") ? config.at("tag.displaynames") : "" + )); + + avd.IsGooglePlayImage = variant.find("google_apis_playstore") != std::string::npos || + tagId == "google_apis_playstore" || + HasTag(avd.SystemImageTagIds, "google_apis_playstore") || + tagDisplay.find("play") != std::string::npos; + + avd.IsGoogleApisImage = avd.IsGooglePlayImage || + variant.find("google_apis") != std::string::npos || + tagId == "google_apis" || + HasTag(avd.SystemImageTagIds, "google_apis") || + tagDisplay.find("google apis") != std::string::npos; + + avd.Supports16KbPageSize = variant.find("ps16k") != std::string::npos || + HasTag(avd.SystemImageTagIds, "page_size_16kb") || + tagDisplayNames.find("16kb") != std::string::npos || + tagDisplayNames.find("16 kb") != std::string::npos; + } + static AvdInfo ExtractAvdInfo(const std::string &avdName) { AvdInfo avd; @@ -65,20 +153,12 @@ namespace CoreDeck { avd.DisplayName = it->second; } - if (auto it = config.find("image.sysdir.1"); it != config.end()) { - auto &sysdir = it->second; - if (auto start = sysdir.find("android-"); start != std::string::npos) { - start += 8; - if (const auto end = sysdir.find('/', start); end != std::string::npos) { - avd.ApiLevel = sysdir.substr(start, end - start); - } - } - } - if (auto it = config.find("abi.type"); it != config.end()) { avd.Abi = it->second; } + ExtractSystemImageInfo(avd, config); + if (auto it = config.find("sdcard.size"); it != config.end()) { avd.SdCard = it->second; } @@ -143,7 +223,12 @@ namespace CoreDeck { if (data.Name.empty() || data.SystemImagePackagePath.empty()) return false; std::vector args = { - "create", "avd", "-n", data.Name, "-k", data.SystemImagePackagePath + "create", + "avd", + "-n", + data.Name, + "-k", + data.SystemImagePackagePath }; if (!data.DeviceId.empty()) { args.emplace_back("-d"); diff --git a/src/core/avd.h b/src/core/avd.h index 5cffae9..30edddb 100644 --- a/src/core/avd.h +++ b/src/core/avd.h @@ -17,6 +17,15 @@ namespace CoreDeck { std::string Device; std::string ApiLevel; std::string Abi; + std::string SystemImagePath; + std::string SystemImageVariant; + std::string SystemImageTagId; + std::string SystemImageTagDisplay; + std::vector SystemImageTagIds; + std::vector SystemImageTagDisplayNames; + bool IsGoogleApisImage = false; + bool IsGooglePlayImage = false; + bool Supports16KbPageSize = false; std::string SdCard; std::string RamSize; std::string ScreenResolution; @@ -44,4 +53,4 @@ namespace CoreDeck { bool DeleteAvd(const SdkInfo &sdk, const std::string &avdName); } -#endif //EMU_LAUNCHER_AVD_INFO_H +#endif // EMU_LAUNCHER_AVD_INFO_H diff --git a/src/core/crash_reporter.cpp b/src/core/crash_reporter.cpp index a921292..a7e85ee 100644 --- a/src/core/crash_reporter.cpp +++ b/src/core/crash_reporter.cpp @@ -14,11 +14,16 @@ namespace CoreDeck::CrashReporter { static sentry_level_t ToSentryLevel(const Level level) { switch (level) { - case Level::Debug: return SENTRY_LEVEL_DEBUG; - case Level::Info: return SENTRY_LEVEL_INFO; - case Level::Warning: return SENTRY_LEVEL_WARNING; - case Level::Error: return SENTRY_LEVEL_ERROR; - case Level::Fatal: return SENTRY_LEVEL_FATAL; + case Level::Debug: + return SENTRY_LEVEL_DEBUG; + case Level::Info: + return SENTRY_LEVEL_INFO; + case Level::Warning: + return SENTRY_LEVEL_WARNING; + case Level::Error: + return SENTRY_LEVEL_ERROR; + case Level::Fatal: + return SENTRY_LEVEL_FATAL; } return SENTRY_LEVEL_INFO; } @@ -36,9 +41,9 @@ namespace CoreDeck::CrashReporter { const char *handlerName = #ifdef _WIN32 - "crashpad_handler.exe"; + "crashpad_handler.exe"; #else - "crashpad_handler"; + "crashpad_handler"; #endif const std::string handlerPath = Paths::JoinPaths( {Paths::GetExecutableDirectory(), handlerName} @@ -52,15 +57,16 @@ namespace CoreDeck::CrashReporter { sentry_close(); } - bool IsEnabled() { return true; } + bool IsEnabled() { + return true; + } void CaptureMessage(const Level level, const std::string_view message) { sentry_capture_event(sentry_value_new_message_event( - ToSentryLevel(level), - nullptr, - ToString(message).c_str() - ) - ); + ToSentryLevel(level), + nullptr, + ToString(message).c_str() + )); } void CaptureException(const std::string_view type, const std::string_view message) { @@ -87,12 +93,16 @@ namespace CoreDeck::CrashReporter { #else namespace CoreDeck::CrashReporter { - bool Init() { return false; } + bool Init() { + return false; + } void Shutdown() { } - bool IsEnabled() { return false; } + bool IsEnabled() { + return false; + } void CaptureMessage(Level, std::string_view) { } diff --git a/src/core/crash_reporter.h b/src/core/crash_reporter.h index 469e97c..0cf732c 100644 --- a/src/core/crash_reporter.h +++ b/src/core/crash_reporter.h @@ -29,4 +29,4 @@ namespace CoreDeck::CrashReporter { void AddBreadcrumb(std::string_view category, std::string_view message); } -#endif //COREDECK_CRASH_REPORTER_H \ No newline at end of file +#endif // COREDECK_CRASH_REPORTER_H \ No newline at end of file diff --git a/src/core/emulator.cpp b/src/core/emulator.cpp index ab2feff..dbb57c1 100644 --- a/src/core/emulator.cpp +++ b/src/core/emulator.cpp @@ -22,12 +22,24 @@ #endif namespace CoreDeck { + static bool WaitForConsoleUnavailable(const int port, const int timeoutMs) { + if (port <= 0) return true; + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + while (std::chrono::steady_clock::now() < deadline) { + if (!EmulatorConsole::IsAvailable(port, 200)) return true; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + return !EmulatorConsole::IsAvailable(port, 200); + } + EmulatorManager::EmulatorManager(SdkInfo sdk) : m_Sdk(std::move(sdk)) { } EmulatorManager::~EmulatorManager() { - std::vector pendingStops; { + std::vector pendingStops; + { std::lock_guard lock(m_Mutex); for (auto &instance: m_Instances | std::views::values) { if (instance.StopThread.joinable()) { @@ -59,7 +71,8 @@ namespace CoreDeck { } } - bool EmulatorManager::Launch(const std::string &avdName, const std::vector &args) { { + bool EmulatorManager::Launch(const std::string &avdName, const std::vector &args) { + { std::lock_guard lock(m_Mutex); if (const auto it = m_Instances.find(avdName); it != m_Instances.end() && it->second.IsRunning) { return false; @@ -84,7 +97,7 @@ namespace CoreDeck { { auto log = std::make_shared(); - auto stopFlag = std::make_shared >(false); + auto stopFlag = std::make_shared>(false); std::thread reader([outputFd, log, stopFlag] { std::array buf{}; std::string partial; @@ -102,9 +115,9 @@ namespace CoreDeck { while (!stopFlag->load()) { #ifdef _WIN32 - const HANDLE h = reinterpret_cast(_get_osfhandle(outputFd)); + const auto handle = reinterpret_cast(_get_osfhandle(outputFd)); DWORD nRead = 0; - if (ReadFile(h, buf.data(), static_cast(buf.size()), &nRead, nullptr)) { + if (ReadFile(handle, buf.data(), static_cast(buf.size()), &nRead, nullptr)) { if (nRead > 0) { flushLines(buf.data(), nRead); } else { @@ -144,7 +157,8 @@ namespace CoreDeck { }); std::thread oldStopThread; - std::thread oldReaderThread; { + std::thread oldReaderThread; + { std::lock_guard lock(m_Mutex); if (const auto existing = m_Instances.find(avdName); existing != m_Instances.end()) { if (existing->second.StopRequested) { @@ -177,9 +191,10 @@ namespace CoreDeck { bool EmulatorManager::Stop(const std::string &avdName) { ProcessId pid; int consolePort; - std::shared_ptr > stopFlag; + std::shared_ptr> stopFlag; std::thread readerThread; - std::thread oldStopThread; { + std::thread oldStopThread; + { std::lock_guard lock(m_Mutex); const auto it = m_Instances.find(avdName); if (it == m_Instances.end() || !it->second.IsRunning || it->second.Stopping) { @@ -195,27 +210,36 @@ namespace CoreDeck { if (oldStopThread.joinable()) oldStopThread.join(); std::thread worker( - [this, avdName, pid, consolePort, stopFlag,reader = std::move(readerThread)]() mutable { + [this, avdName, pid, consolePort, stopFlag, reader = std::move(readerThread)]() mutable { bool exited = false; if (consolePort > 0 && EmulatorConsole::SendKill(consolePort)) { - exited = WaitForProcessExit(pid, 5000); + exited = WaitForConsoleUnavailable(consolePort, 10000); } if (!exited) { KillProcess(pid); - exited = WaitForProcessExit(pid, 2000); + exited = consolePort > 0 ? WaitForConsoleUnavailable(consolePort, 3000) : WaitForProcessExit(pid, 2000); } if (!exited) { TerminateProcessTree(pid); + exited = consolePort > 0 ? WaitForConsoleUnavailable(consolePort, 3000) : WaitForProcessExit(pid, 2000); } - if (stopFlag) stopFlag->store(true); - if (reader.joinable()) reader.join(); + if (exited) { + if (stopFlag) stopFlag->store(true); + if (reader.joinable()) reader.join(); + } - std::lock_guard lock(m_Mutex); - if (const auto it = m_Instances.find(avdName); it != m_Instances.end()) { - it->second.IsRunning = false; - it->second.Stopping = false; + { + std::lock_guard lock(m_Mutex); + if (const auto it = m_Instances.find(avdName); it != m_Instances.end()) { + it->second.IsRunning = !exited; + it->second.Stopping = false; + if (!exited && reader.joinable()) { + it->second.ReaderThread = std::move(reader); + } + } } + if (!exited && reader.joinable()) reader.detach(); } ); @@ -254,7 +278,7 @@ namespace CoreDeck { for (auto &instance: m_Instances | std::views::values) { if (instance.IsRunning) { if (!IsProcessRunning(instance.Pid)) { - instance.IsRunning = false; + instance.IsRunning = instance.ConsolePort > 0 && EmulatorConsole::IsAvailable(instance.ConsolePort, 25); } } } diff --git a/src/core/emulator.h b/src/core/emulator.h index 0175583..d885554 100644 --- a/src/core/emulator.h +++ b/src/core/emulator.h @@ -25,7 +25,7 @@ namespace CoreDeck { std::shared_ptr Log; std::thread ReaderThread; std::thread StopThread; - std::shared_ptr > StopRequested; + std::shared_ptr> StopRequested; }; class EmulatorManager { @@ -55,4 +55,4 @@ namespace CoreDeck { }; } -#endif //EMU_LAUNCHER_EMULATOR_H +#endif // EMU_LAUNCHER_EMULATOR_H diff --git a/src/core/emulator_console.cpp b/src/core/emulator_console.cpp index 289ffec..9a708e1 100644 --- a/src/core/emulator_console.cpp +++ b/src/core/emulator_console.cpp @@ -29,37 +29,59 @@ namespace CoreDeck::EmulatorConsole { WSAStartup(MAKEWORD(2, 2), &d); } - ~WsaInit() { WSACleanup(); } + ~WsaInit() { + WSACleanup(); + } }; - void EnsureWsa() { static WsaInit init; } - void CloseSock(const socket_t s) { closesocket(s); } + + void EnsureWsa() { + static WsaInit init; + } + + void CloseSock(const socket_t s) { + closesocket(s); + } + void SetNonBlocking(const socket_t s) { u_long m = 1; ioctlsocket(s, FIONBIO, &m); } - int LastErr() { return WSAGetLastError(); } - bool ErrIsWouldBlock(const int e) { return e == WSAEWOULDBLOCK || e == WSAEINPROGRESS; } + + int LastErr() { + return WSAGetLastError(); + } + + bool ErrIsWouldBlock(const int e) { + return e == WSAEWOULDBLOCK || e == WSAEINPROGRESS; + } #else void EnsureWsa() { } - void CloseSock(const socket_t s) { close(s); } + void CloseSock(const socket_t s) { + close(s); + } void SetNonBlocking(const socket_t s) { const int f = fcntl(s, F_GETFL, 0); fcntl(s, F_SETFL, f | O_NONBLOCK); } - int LastErr() { return errno; } - bool ErrIsWouldBlock(const int e) { return e == EWOULDBLOCK || e == EAGAIN || e == EINPROGRESS; } + int LastErr() { + return errno; + } + + bool ErrIsWouldBlock(const int e) { + return e == EWOULDBLOCK || e == EAGAIN || e == EINPROGRESS; + } #endif std::string ReadAuthToken() { const char *home = #ifdef _WIN32 - std::getenv("USERPROFILE"); + std::getenv("USERPROFILE"); #else - std::getenv("HOME"); + std::getenv("HOME"); #endif if (!home) return ""; const std::filesystem::path p = std::filesystem::path(home) / ".emulator_console_auth_token"; @@ -68,8 +90,7 @@ namespace CoreDeck::EmulatorConsole { std::stringstream ss; ss << f.rdbuf(); std::string token = ss.str(); - while (!token.empty() && (token.back() == '\n' || token.back() == '\r' || token.back() == ' ' || token.back() == - '\t')) { + while (!token.empty() && (token.back() == '\n' || token.back() == '\r' || token.back() == ' ' || token.back() == '\t')) { token.pop_back(); } return token; @@ -83,6 +104,47 @@ namespace CoreDeck::EmulatorConsole { return select(static_cast(s) + 1, nullptr, &wf, nullptr, &tv) > 0; } + bool ConnectLocalhost(const socket_t s, const int port, const int timeoutMs) { + SetNonBlocking(s); + + sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(static_cast(port)); + + const int rc = connect(s, reinterpret_cast(&addr), sizeof(addr)); + if (rc != 0 && !ErrIsWouldBlock(LastErr())) return false; + if (rc != 0 && !WaitWritable(s, timeoutMs)) return false; + + int error = 0; +#ifdef _WIN32 + int len = sizeof(error); +#else + socklen_t len = sizeof(error); +#endif + if (getsockopt(s, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &len) != 0) return false; + return error == 0; + } + + bool SendAll(const socket_t s, const std::string &payload, const int timeoutMs) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + size_t sent = 0; + while (sent < payload.size()) { +#ifdef _WIN32 + const int n = send(s, payload.data() + sent, static_cast(payload.size() - sent), 0); +#else + const ssize_t n = send(s, payload.data() + sent, payload.size() - sent, 0); +#endif + if (n > 0) { + sent += static_cast(n); + continue; + } + if (n < 0 && !ErrIsWouldBlock(LastErr())) return false; + if (std::chrono::steady_clock::now() >= deadline) return false; + WaitWritable(s, 100); + } + return true; + } int FindFreePort(const int startPort, const int endPort) { EnsureWsa(); @@ -100,24 +162,21 @@ namespace CoreDeck::EmulatorConsole { return -1; } - bool SendKill(const int port, const int timeoutMs) { + bool IsAvailable(const int port, const int timeoutMs) { EnsureWsa(); const socket_t s = socket(AF_INET, SOCK_STREAM, 0); if (s == s_InvalidSocket) return false; + const bool connected = ConnectLocalhost(s, port, timeoutMs); + CloseSock(s); + return connected; + } - SetNonBlocking(s); - - sockaddr_in addr = {}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr.sin_port = htons(static_cast(port)); + bool SendKill(const int port, const int timeoutMs) { + EnsureWsa(); + const socket_t s = socket(AF_INET, SOCK_STREAM, 0); + if (s == s_InvalidSocket) return false; - const int rc = connect(s, reinterpret_cast(&addr), sizeof(addr)); - if (rc != 0 && !ErrIsWouldBlock(LastErr())) { - CloseSock(s); - return false; - } - if (rc != 0 && !WaitWritable(s, timeoutMs)) { + if (!ConnectLocalhost(s, port, timeoutMs)) { CloseSock(s); return false; } @@ -128,30 +187,13 @@ namespace CoreDeck::EmulatorConsole { } payload += "kill\r\n"; - const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); - size_t sent = 0; - while (sent < payload.size()) { + const bool sent = SendAll(s, payload, timeoutMs); #ifdef _WIN32 - const int n = send(s, payload.data() + sent, static_cast(payload.size() - sent), 0); + shutdown(s, SD_SEND); #else - const ssize_t n = send(s, payload.data() + sent, payload.size() - sent, 0); + shutdown(s, SHUT_WR); #endif - if (n > 0) { - sent += static_cast(n); - continue; - } - if (n < 0 && !ErrIsWouldBlock(LastErr())) { - CloseSock(s); - return false; - } - if (std::chrono::steady_clock::now() >= deadline) { - CloseSock(s); - return false; - } - if (!WaitWritable(s, 100)) continue; - } - CloseSock(s); - return true; + return sent; } } diff --git a/src/core/emulator_console.h b/src/core/emulator_console.h index 997d2fc..8e63837 100644 --- a/src/core/emulator_console.h +++ b/src/core/emulator_console.h @@ -4,6 +4,8 @@ namespace CoreDeck::EmulatorConsole { int FindFreePort(int startPort = 5554, int endPort = 5584); + bool IsAvailable(int port, int timeoutMs = 250); + bool SendKill(int port, int timeoutMs = 2000); } diff --git a/src/core/file_dialog.h b/src/core/file_dialog.h index aeba67a..147c466 100644 --- a/src/core/file_dialog.h +++ b/src/core/file_dialog.h @@ -12,4 +12,4 @@ namespace CoreDeck::FileDialog { std::optional PickFolder(const std::string &title, const std::string &defaultPath = ""); } -#endif //COREDECK_FILE_DIALOG_H +#endif // COREDECK_FILE_DIALOG_H diff --git a/src/core/log.h b/src/core/log.h index ca3aa1d..983eb67 100644 --- a/src/core/log.h +++ b/src/core/log.h @@ -19,23 +19,27 @@ namespace CoreDeck { namespace Log { inline LogLevel MinLevel = #ifdef NDEBUG - LogLevel::Warning; + LogLevel::Warning; #else - LogLevel::Debug; + LogLevel::Debug; #endif constexpr const char *LevelTag(const LogLevel level) { switch (level) { - case LogLevel::Debug: return "[DEBUG]"; - case LogLevel::Info: return "[INFO] "; - case LogLevel::Warning: return "[WARN] "; - case LogLevel::Error: return "[ERROR]"; + case LogLevel::Debug: + return "[DEBUG]"; + case LogLevel::Info: + return "[INFO] "; + case LogLevel::Warning: + return "[WARN] "; + case LogLevel::Error: + return "[ERROR]"; } return "[UNKNOWN]"; } template - void Write(const LogLevel level, Args &&... args) { + void Write(const LogLevel level, Args &&...args) { if (level < MinLevel) return; auto &out = (level >= LogLevel::Error) ? std::cerr : std::cout; out << LevelTag(level) << " "; @@ -44,17 +48,25 @@ namespace CoreDeck { } template - void Debug(Args &&... args) { Write(LogLevel::Debug, std::forward(args)...); } + void Debug(Args &&...args) { + Write(LogLevel::Debug, std::forward(args)...); + } template - void Info(Args &&... args) { Write(LogLevel::Info, std::forward(args)...); } + void Info(Args &&...args) { + Write(LogLevel::Info, std::forward(args)...); + } template - void Warn(Args &&... args) { Write(LogLevel::Warning, std::forward(args)...); } + void Warn(Args &&...args) { + Write(LogLevel::Warning, std::forward(args)...); + } template - void Error(Args &&... args) { Write(LogLevel::Error, std::forward(args)...); } + void Error(Args &&...args) { + Write(LogLevel::Error, std::forward(args)...); + } } } -#endif //COREDECK_LOG_H +#endif // COREDECK_LOG_H diff --git a/src/core/log_buffer.h b/src/core/log_buffer.h index 91fcbfb..07b7420 100644 --- a/src/core/log_buffer.h +++ b/src/core/log_buffer.h @@ -33,4 +33,4 @@ namespace CoreDeck { }; } -#endif //EMU_LAUNCHER_LOG_BUFFER_H +#endif // EMU_LAUNCHER_LOG_BUFFER_H diff --git a/src/core/options.cpp b/src/core/options.cpp index e12a525..f0b4e0d 100644 --- a/src/core/options.cpp +++ b/src/core/options.cpp @@ -10,6 +10,87 @@ #include "paths.h" namespace CoreDeck { + static const char *FindDisplayLabel(const std::vector &options, const std::string &value) { + for (const auto &[Label, RawValue]: options) { + if (value == RawValue) return Label; + } + return value.c_str(); + } + + const std::vector &GpuModeOptions() { + static const std::vector options = { + {"Automatic", "auto"}, + {"Hardware Acceleration", "host"}, + {"Software Rendering", "swiftshader_indirect"}, + {"ANGLE Rendering", "angle_indirect"}, + {"Guest Rendering", "guest"}, + }; + return options; + } + + const char *GpuModeDisplayLabel(const std::string &value) { + return FindDisplayLabel(GpuModeOptions(), value); + } + + const char *ScreenModeDisplayLabel(const std::string &value) { + static const std::vector options = { + {"Touch Screen", "touch"}, + {"Multi-Touch Screen", "multi-touch"}, + {"No Touch Input", "no-touch"}, + }; + return FindDisplayLabel(options, value); + } + + static const char *NetworkSpeedDisplayLabel(const std::string &value) { + static const std::vector options = { + {"Full Speed", "full"}, + {"LTE", "lte"}, + {"HSDPA", "hsdpa"}, + {"UMTS", "umts"}, + {"EDGE", "edge"}, + {"GPRS", "gprs"}, + {"GSM", "gsm"}, + }; + return FindDisplayLabel(options, value); + } + + static const char *NetworkDelayDisplayLabel(const std::string &value) { + static const std::vector options = { + {"No Delay", "none"}, + {"GPRS Latency", "gprs"}, + {"EDGE Latency", "edge"}, + {"UMTS Latency", "umts"}, + }; + return FindDisplayLabel(options, value); + } + + static const char *AccelerationModeDisplayLabel(const std::string &value) { + static const std::vector options = { + {"Automatic", "auto"}, + {"Disabled", "off"}, + {"Enabled", "on"}, + }; + return FindDisplayLabel(options, value); + } + + static const char *SELinuxModeDisplayLabel(const std::string &value) { + static const std::vector options = { + {"Permissive", "permissive"}, + {"Disabled", "disabled"}, + }; + return FindDisplayLabel(options, value); + } + + const char *EmulatorOptionItemDisplayLabel(const std::string &flag, const std::string &value) { + if (flag == "-gpu") return GpuModeDisplayLabel(value); + if (flag == "-screen") return ScreenModeDisplayLabel(value); + if (flag == "-netspeed") return NetworkSpeedDisplayLabel(value); + if (flag == "-netdelay") return NetworkDelayDisplayLabel(value); + if (flag == "-accel") return AccelerationModeDisplayLabel(value); + if (flag == "-selinux") return SELinuxModeDisplayLabel(value); + return value.c_str(); + } + std::vector GetEmulatorOptions() { return { // Display @@ -19,7 +100,7 @@ namespace CoreDeck { .Description = "Set hardware OpenGLES emulation mode", .Type = OptionType::Selection, .Category = OptionCategory::Display, - .Items = {"host", "swiftshader_indirect", "angle_indirect", "guest"}, + .Items = {"auto", "host", "swiftshader_indirect", "angle_indirect", "guest"}, .SelectedItem = 0, }, { @@ -263,7 +344,7 @@ namespace CoreDeck { if (json.empty()) return GetEmulatorOptions(); - auto options = rfl::json::read >(json); + auto options = rfl::json::read>(json); return options.value(); } catch (const std::exception &e) { Log::Error("Failed to load options from ", filePath, ": ", e.what()); diff --git a/src/core/options.h b/src/core/options.h index b4db746..581a071 100644 --- a/src/core/options.h +++ b/src/core/options.h @@ -43,6 +43,19 @@ namespace CoreDeck { int SelectedItem = 0; }; + struct OptionValueLabel { + const char *Label; + const char *Value; + }; + + const std::vector &GpuModeOptions(); + + const char *GpuModeDisplayLabel(const std::string &value); + + const char *ScreenModeDisplayLabel(const std::string &value); + + const char *EmulatorOptionItemDisplayLabel(const std::string &flag, const std::string &value); + std::vector GetEmulatorOptions(); std::vector BuildArgs(const std::string &avdName, const std::vector &options); @@ -56,4 +69,4 @@ namespace CoreDeck { void EnsureOptionsConfigDirectoryExists(); } -#endif //EMU_LAUNCHER_OPTIONS_H +#endif // EMU_LAUNCHER_OPTIONS_H diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 654a5ea..ababb7c 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -34,10 +34,14 @@ namespace CoreDeck::Paths { const char *GetPlatformName() { switch (GetCurrentPlatform()) { - case Platform::Windows: return "Windows"; - case Platform::macOS: return "macOS"; - case Platform::Linux: return "Linux"; - default: return "Unknown"; + case Platform::Windows: + return "Windows"; + case Platform::macOS: + return "macOS"; + case Platform::Linux: + return "Linux"; + default: + return "Unknown"; } } @@ -249,9 +253,7 @@ namespace CoreDeck::Paths { if (path.empty()) return false; if (!std::filesystem::exists(path) || !std::filesystem::is_directory(path)) return false; - const std::string emulatorBinary = Paths::JoinPaths({ - path, "emulator", "emulator" + Paths::GetExecutableExtension() - }); + const std::string emulatorBinary = Paths::JoinPaths({path, "emulator", "emulator" + Paths::GetExecutableExtension()}); return std::filesystem::exists(emulatorBinary); } diff --git a/src/core/paths.h b/src/core/paths.h index bca8f20..7dc5814 100644 --- a/src/core/paths.h +++ b/src/core/paths.h @@ -61,4 +61,4 @@ namespace CoreDeck::Paths { } } -#endif //EMU_LAUNCHER_PATHS_H +#endif // EMU_LAUNCHER_PATHS_H diff --git a/src/core/process.cpp b/src/core/process.cpp index 80deea6..2d31b1e 100644 --- a/src/core/process.cpp +++ b/src/core/process.cpp @@ -99,8 +99,7 @@ namespace CoreDeck { si.hStdError = hOutW; PROCESS_INFORMATION pi = {}; - if (!CreateProcessA(nullptr, const_cast(cmdLine.c_str()), nullptr, nullptr, - TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { + if (!CreateProcessA(nullptr, const_cast(cmdLine.c_str()), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { CloseHandle(hOutR); CloseHandle(hOutW); CloseHandle(hInR); @@ -199,9 +198,7 @@ namespace CoreDeck { #endif } - std::string RunCommandArgs(const std::string &path, - const std::vector &args, - const std::string &stdinData) { + std::string RunCommandArgs(const std::string &path, const std::vector &args, const std::string &stdinData) { std::string out; StreamCommandArgs(path, args, stdinData, [&out](const std::string &line) { out += line; @@ -237,8 +234,7 @@ namespace CoreDeck { PROCESS_INFORMATION pi = {}; - if (!CreateProcessA(nullptr, const_cast(cmdLine.c_str()), nullptr, nullptr, - TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { + if (!CreateProcessA(nullptr, const_cast(cmdLine.c_str()), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { CloseHandle(hReadPipe); CloseHandle(hWritePipe); return 0; @@ -286,7 +282,7 @@ namespace CoreDeck { } argv.push_back(nullptr); - execvp(path.c_str(), const_cast(argv.data())); + execvp(path.c_str(), const_cast(argv.data())); _exit(1); } diff --git a/src/core/process.h b/src/core/process.h index 5a0fec8..91a56a0 100644 --- a/src/core/process.h +++ b/src/core/process.h @@ -42,4 +42,4 @@ namespace CoreDeck { bool IsProcessRunning(ProcessId pid); } -#endif //EMU_LAUNCHER_PROCESS_H +#endif // EMU_LAUNCHER_PROCESS_H diff --git a/src/core/sdk.cpp b/src/core/sdk.cpp index a3e99c7..0aa127b 100644 --- a/src/core/sdk.cpp +++ b/src/core/sdk.cpp @@ -52,7 +52,9 @@ namespace CoreDeck { for (const auto &entry: std::filesystem::directory_iterator(cmdlineRoot)) { if (!entry.is_directory()) continue; const std::string candidate = FindCmdlineTool( - Paths::JoinPaths({entry.path().string(), "bin"}), "avdmanager"); + Paths::JoinPaths({entry.path().string(), "bin"}), + "avdmanager" + ); if (!candidate.empty()) { sdk.AvdManagerPath = candidate; break; diff --git a/src/core/sdk.h b/src/core/sdk.h index e18cba6..1467928 100644 --- a/src/core/sdk.h +++ b/src/core/sdk.h @@ -18,4 +18,4 @@ namespace CoreDeck { SdkInfo DetectAndroidSdk(); } -#endif //EMU_LAUNCHER_SDK_H +#endif // EMU_LAUNCHER_SDK_H diff --git a/src/core/system_image.cpp b/src/core/system_image.cpp index 86e58ad..891abb0 100644 --- a/src/core/system_image.cpp +++ b/src/core/system_image.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -142,6 +143,7 @@ namespace CoreDeck { installedSet[img.PackagePath] = true; } + std::unordered_set seenPackages; std::istringstream stream(output); std::string line; while (std::getline(stream, line)) { @@ -163,6 +165,7 @@ namespace CoreDeck { while (!packagePath.empty() && (packagePath.back() == ' ' || packagePath.back() == '\t')) { packagePath.pop_back(); } + if (!seenPackages.insert(packagePath).second) continue; std::vector parts; std::istringstream partStream(packagePath); @@ -215,7 +218,8 @@ namespace CoreDeck { StreamCommandArgs( sdk.SdkManagerPath, - {"--install", packagePath}, "", + {"--install", packagePath}, + "", [&progress](const std::string &line) { ParseProgressLine(line, progress); } diff --git a/src/core/system_image.h b/src/core/system_image.h index ee014f2..5a842ac 100644 --- a/src/core/system_image.h +++ b/src/core/system_image.h @@ -72,4 +72,4 @@ namespace CoreDeck { bool AcceptSdkLicenses(const SdkInfo &sdk); } -#endif //COREDECK_SYSTEM_IMAGE_H +#endif // COREDECK_SYSTEM_IMAGE_H diff --git a/src/core/utilities.cpp b/src/core/utilities.cpp index f92722c..6158c35 100644 --- a/src/core/utilities.cpp +++ b/src/core/utilities.cpp @@ -2,7 +2,9 @@ // Created by AbdulMuaz Aqeel on 06/04/2026. // +#include #include +#include #include #include #include @@ -30,7 +32,10 @@ namespace CoreDeck { std::uintmax_t total = 0; std::error_code ec; for (const auto &entry: std::filesystem::recursive_directory_iterator( - path, std::filesystem::directory_options::skip_permission_denied, ec)) { + path, + std::filesystem::directory_options::skip_permission_denied, + ec + )) { if (entry.is_regular_file(ec)) { total += entry.file_size(ec); } @@ -61,6 +66,18 @@ namespace CoreDeck { return buf; } + std::string LowerCopy(std::string value) { + std::ranges::transform(value, value.begin(), [](const unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; + } + + bool ContainsIgnoreCase(const std::string &text, const std::string &needle) { + if (needle.empty()) return true; + return LowerCopy(text).find(LowerCopy(needle)) != std::string::npos; + } + bool WipeAvdUserData(const std::string &avdPath) { if (!std::filesystem::exists(avdPath)) return false; diff --git a/src/core/utilities.h b/src/core/utilities.h index eb006bb..a15d15c 100644 --- a/src/core/utilities.h +++ b/src/core/utilities.h @@ -16,12 +16,16 @@ namespace CoreDeck { bool WipeAvdUserData(const std::string &avdPath); + std::string LowerCopy(std::string value); + + bool ContainsIgnoreCase(const std::string &text, const std::string &needle); + template - std::string StrConcat(Args &&... args) { + std::string StrConcat(Args &&...args) { std::string result; (result += ... += std::forward(args)); return result; } } -#endif //EMU_LAUNCHER_UTILITIES_H +#endif // EMU_LAUNCHER_UTILITIES_H diff --git a/src/core/version_check.cpp b/src/core/version_check.cpp index fb057c5..b2dd4f0 100644 --- a/src/core/version_check.cpp +++ b/src/core/version_check.cpp @@ -115,8 +115,12 @@ namespace CoreDeck { } HINTERNET request = WinHttpOpenRequest( - connect, L"GET", path, nullptr, - WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, + connect, + L"GET", + path, + nullptr, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE ); if (!request) { @@ -126,17 +130,13 @@ namespace CoreDeck { } const wchar_t *headers = L"Accept: application/vnd.github+json\r\n"; - BOOL sent = WinHttpSendRequest(request, headers, static_cast(-1L), WINHTTP_NO_REQUEST_DATA, 0, 0, 0) - && WinHttpReceiveResponse(request, nullptr); + BOOL sent = WinHttpSendRequest(request, headers, static_cast(-1L), WINHTTP_NO_REQUEST_DATA, 0, 0, 0) && WinHttpReceiveResponse(request, nullptr); std::optional result; if (sent) { DWORD status = 0; DWORD statusSize = sizeof(status); - WinHttpQueryHeaders(request, - WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, - WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusSize, - WINHTTP_NO_HEADER_INDEX); + WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusSize, WINHTTP_NO_HEADER_INDEX); if (status >= 200 && status < 300) { std::string body; DWORD available = 0; diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 55344b7..7860928 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -136,7 +136,7 @@ namespace CoreDeck { ImGui::DockBuilderSplitNode(topId, ImGuiDir_Left, 0.25, &leftId, ¢erId); ImGuiID middleId, rightId; - ImGui::DockBuilderSplitNode(centerId, ImGuiDir_Right, 0.40f, &rightId, &middleId); + ImGui::DockBuilderSplitNode(centerId, ImGuiDir_Right, 0.35f, &rightId, &middleId); ImGui::DockBuilderDockWindow("Options", leftId); ImGui::DockBuilderDockWindow("AVDs", middleId); @@ -168,7 +168,9 @@ namespace CoreDeck { BuildPreferencesWindow(m_Context); BuildUpdateNoticeWindow(m_Context); BuildCreateAvdWindow(m_Context); - BuildInstallImageWindow(m_Context); + if (!m_Context.UI.ShowCreateAvdDialog) { + BuildInstallImageWindow(m_Context); + } BuildStorageWindow(m_Context); m_Context.Host.Manager.Update(); @@ -191,7 +193,7 @@ namespace CoreDeck { } bool Application::CreateMainWindow() { - m_Window = glfwCreateWindow(1200, 800, COREDECK_TITLE, nullptr, nullptr); + m_Window = glfwCreateWindow(1200, 900, COREDECK_TITLE, nullptr, nullptr); if (!m_Window) { ShowFatalError(COREDECK_TITLE, "Failed to create window.\nYour system may not support OpenGL 3.3."); return false; @@ -235,7 +237,13 @@ namespace CoreDeck { ); if (std::filesystem::exists(textFontPath)) { - static constexpr ImWchar textRanges[] = {0x0020, 0x00FF, 0x2000, 0x206F, 0,}; + static constexpr ImWchar textRanges[] = { + 0x0020, + 0x00FF, + 0x2000, + 0x206F, + 0, + }; io.Fonts->AddFontFromFileTTF(textFontPath.c_str(), 16.0f, nullptr, textRanges); } @@ -404,8 +412,10 @@ namespace CoreDeck { for (const auto &avdName: context.Catalog.AvdNames) LoadAvdOptions(context, avdName); context.DiskUsage.PerAvdCache.clear(); - context.DiskUsage.SystemImagesSizeCached = false; - context.DiskUsage.SystemImageEntriesCached = false; + if (!context.DiskUsage.Loading.load()) { + context.DiskUsage.LastScan = {}; + context.DiskUsage.Ready = false; + } if (!context.Catalog.Avds.empty()) context.Catalog.SelectedAvd = 0; else context.Catalog.SelectedAvd = -1; diff --git a/src/gui/application.h b/src/gui/application.h index bf1665f..1b215ed 100644 --- a/src/gui/application.h +++ b/src/gui/application.h @@ -51,7 +51,7 @@ namespace CoreDeck { bool m_GlfwInitialized = false; bool m_ImGuiContextCreated = false; bool m_ImGuiBackendsInitialized = false; - std::future > m_UpdateCheckFuture; + std::future> m_UpdateCheckFuture; bool m_AutoUpdateCheckStarted = false; bool m_UpdateCheckWasManual = false; }; @@ -71,4 +71,4 @@ namespace CoreDeck { std::vector &GetDefaultAvdOptions(Context &context); } -#endif //EMU_LAUNCHER_RENDERER_H +#endif // EMU_LAUNCHER_RENDERER_H diff --git a/src/gui/context.h b/src/gui/context.h index 18d0670..feb761a 100644 --- a/src/gui/context.h +++ b/src/gui/context.h @@ -33,6 +33,33 @@ namespace CoreDeck { Device, }; + enum class ImageCategory { + All, + PhoneTablet, + Wear, + Tv, + Automotive, + Desktop, + Xr, + Other, + }; + + enum class DeviceCategory { + All, + Phone, + Tablet, + Wear, + Tv, + Automotive, + Desktop, + Other, + }; + + struct StorageScanResult { + std::uintmax_t TotalAvdSize = 0; + std::uintmax_t SystemImagesSize = 0; + }; + struct Context { struct Host { SdkInfo Sdk; @@ -51,7 +78,7 @@ namespace CoreDeck { std::vector Avds; int SelectedAvd = -1; int PreviousSelectedAvd = -1; - std::unordered_map > PerAvdOptions; + std::unordered_map> PerAvdOptions; char SearchFilter[128] = {}; AvdSortMode SortMode = AvdSortMode::Name; @@ -72,6 +99,7 @@ namespace CoreDeck { bool ShowAboutDialog = false; bool ShowDeleteAvdDialog = false; bool ShowCreateAvdDialog = false; + bool ShowDeviceProfileDialog = false; bool ShowInstallImageDialog = false; bool ReopenCreateAvdOnInstallClose = false; bool ShowPreferences = false; @@ -91,6 +119,9 @@ namespace CoreDeck { AvdCreationData CreationData; int SelectedSystemImage = 0; int SelectedDevice = 0; + int PendingSelectedDevice = 0; + DeviceCategory SelectedDeviceCategory = DeviceCategory::Phone; + char DeviceSearchFilter[128] = {}; int SelectedGpuMode = 0; bool NameAutoFilled = true; bool DisplayNameAutoFilled = true; @@ -109,7 +140,9 @@ namespace CoreDeck { struct ImageInstallationWork { std::vector RemoteImages; - int SelectedImage = 0; + int SelectedImage = -1; + ImageCategory SelectedCategory = ImageCategory::PhoneTablet; + char SearchFilter[128] = {}; struct { std::atomic Loading{false}; @@ -137,15 +170,10 @@ namespace CoreDeck { struct DiskUsage { std::unordered_map PerAvdCache; - std::uintmax_t SystemImagesSize = 0; - bool SystemImagesSizeCached = false; - - struct SystemImageEntry { - std::string Name; - std::uintmax_t Size; - }; - std::vector SystemImageEntries; - bool SystemImageEntriesCached = false; + StorageScanResult LastScan; + std::atomic Loading{false}; + bool Ready = false; + std::future Future; } DiskUsage; struct Updates { @@ -161,4 +189,4 @@ namespace CoreDeck { }; } -#endif //COREDECK_CONTEXT_H +#endif // COREDECK_CONTEXT_H diff --git a/src/gui/icons.h b/src/gui/icons.h index ac6798d..583b5a4 100644 --- a/src/gui/icons.h +++ b/src/gui/icons.h @@ -28,4 +28,4 @@ namespace CoreDeck::Icons { constexpr const char *Car = "\xef\x86\xb9"; // fa-car (f1b9) — used for Automotive } -#endif //COREDECK_ICONS_H +#endif // COREDECK_ICONS_H diff --git a/src/gui/theme.h b/src/gui/theme.h index fda8c7d..7f9e073 100644 --- a/src/gui/theme.h +++ b/src/gui/theme.h @@ -32,4 +32,4 @@ namespace CoreDeck { } } -#endif //COREDECK_THEME_H +#endif // COREDECK_THEME_H diff --git a/src/gui/widgets.cpp b/src/gui/widgets.cpp index d7dbb99..588c614 100644 --- a/src/gui/widgets.cpp +++ b/src/gui/widgets.cpp @@ -8,6 +8,24 @@ #include "theme.h" namespace CoreDeck { + PickerTableStyle::PickerTableStyle() { + Colors.push(ImGuiCol_ChildBg, HexColor("#141417")); + Colors.push(ImGuiCol_Border, HexColor("#2E2E33")); + Colors.push(ImGuiCol_TableHeaderBg, HexColor("#1A1A1C")); + Colors.push(ImGuiCol_TableRowBg, HexColor("#000000", 0.0f)); + Colors.push(ImGuiCol_TableRowBgAlt, HexColor("#1A1A1C", 0.28f)); + Colors.push(ImGuiCol_TableBorderLight, HexColor("#242428")); + Colors.push(ImGuiCol_TableBorderStrong, HexColor("#2E2E33")); + Colors.push(ImGuiCol_Header, HexColor("#29292B", 0.65f)); + Colors.push(ImGuiCol_HeaderHovered, HexColor("#333336", 0.85f)); + Colors.push(ImGuiCol_HeaderActive, HexColor("#3F3F42")); + + Vars.push(ImGuiStyleVar_ChildRounding, 6.0f); + Vars.push(ImGuiStyleVar_ChildBorderSize, 1.0f); + Vars.push(ImGuiStyleVar_WindowPadding, ImVec2(1.0f, 1.0f)); + Vars.push(ImGuiStyleVar_CellPadding, ImVec2(8.0f, 8.0f)); + } + bool PrimaryButton(const char *label, const bool isEnabled, const ImVec2 size) { if (!isEnabled) ImGui::BeginDisabled(); @@ -68,6 +86,12 @@ namespace CoreDeck { return clicked; } + bool PickerButton(const char *label, const bool isEnabled, const ImVec2 size) { + StyleVar sv; + sv.push(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); + return PrimaryButton(label, isEnabled, size); + } + void StatusBadge(const char *label, const bool isActive) { StyleColor sc; StyleVar sv; @@ -204,14 +228,45 @@ namespace CoreDeck { return clicked; } - void PropertyTextWrapped(const char *label, const char *value) { + void PropertyTextWrapped(const char *label, const char *value, const bool invertColors) { StyleColor sc; + if (invertColors) sc.push(ImGuiCol_Text, HexColor("#66666B")); ImGui::Text("%s", label); ImGui::SameLine(); - sc.push(ImGuiCol_Text, HexColor("#66666B")); + + if (invertColors) { + sc.push(ImGuiCol_Text, HexColor("#F2F2F2")); + } else { + sc.push(ImGuiCol_Text, HexColor("#66666B")); + } ImGui::TextWrapped("%s", value); } + bool CategoryChip(const char *label, const bool isSelected) { + StyleColor sc; + StyleVar sv; + + if (isSelected) { + sc.push(ImGuiCol_Button, HexColor("#26B333", 0.16f)); + sc.push(ImGuiCol_ButtonHovered, HexColor("#26B333", 0.24f)); + sc.push(ImGuiCol_ButtonActive, HexColor("#26B333", 0.32f)); + sc.push(ImGuiCol_Text, HexColor("#33CC47")); + sc.push(ImGuiCol_Border, HexColor("#33CC47")); + } else { + sc.push(ImGuiCol_Button, HexColor("#1A1A1C")); + sc.push(ImGuiCol_ButtonHovered, HexColor("#242426")); + sc.push(ImGuiCol_ButtonActive, HexColor("#2E2E30")); + sc.push(ImGuiCol_Text, HexColor("#CFCFD4")); + sc.push(ImGuiCol_Border, HexColor("#2E2E33")); + } + + sv.push(ImGuiStyleVar_FrameRounding, 999.0f); + sv.push(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 5.0f)); + sv.push(ImGuiStyleVar_FrameBorderSize, 1.0f); + + return ImGui::Button(label); + } + bool CollapsingHeader(const char *label, const ImGuiTreeNodeFlags flags) { StyleColor sc; sc.push(ImGuiCol_Header, HexColor("#000000", 0.0f)); @@ -236,10 +291,10 @@ namespace CoreDeck { ImGui::SetNextWindowSize(ImVec2(380, 0), ImGuiCond_Appearing); constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoDocking; + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoDocking; if (ImGui::BeginPopupModal(title.c_str(), data.isBusy ? nullptr : &data.isOpen, flags)) { if (!data.isOpen) { diff --git a/src/gui/widgets.h b/src/gui/widgets.h index 5012904..02236a1 100644 --- a/src/gui/widgets.h +++ b/src/gui/widgets.h @@ -42,9 +42,46 @@ namespace CoreDeck { } }; - enum class DialogResult { None, Confirmed, Cancelled }; + struct LabeledIconStyle { + const char *Icon; + const char *Label; + const char *Color; + }; + + struct PickerTableStyle { + StyleColor Colors; + StyleVar Vars; - enum class DialogType { Default, Positive, Negative }; + PickerTableStyle(); + }; + + constexpr ImGuiTableFlags PickerTableFlags = ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY | + ImGuiTableFlags_SizingStretchProp; + + constexpr ImGuiWindowFlags WindowNoResizeFlags = ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoDocking; + + constexpr ImGuiWindowFlags WindowAutoResizeFlags = ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoDocking; + + enum class DialogResult { + None, + Confirmed, + Cancelled + }; + + enum class DialogType { + Default, + Positive, + Negative + }; struct DialogData { const char *Id{}; @@ -70,6 +107,8 @@ namespace CoreDeck { bool WarningButton(const char *label, bool isEnabled = true, ImVec2 size = ImVec2(0, 0)); + bool PickerButton(const char *label, bool isEnabled = true, ImVec2 size = ImVec2(0, 0)); + void StatusBadge(const char *label, bool isActive); bool SelectableItem( @@ -83,7 +122,9 @@ namespace CoreDeck { bool PropertyText(const char *label, const char *value, bool isClickable = false, bool hasSpaceBetween = false); - void PropertyTextWrapped(const char *label, const char *value); + void PropertyTextWrapped(const char *label, const char *value, bool invertColors = false); + + bool CategoryChip(const char *label, bool isSelected); bool CollapsingHeader(const char *label, ImGuiTreeNodeFlags flags = 0); @@ -94,4 +135,4 @@ namespace CoreDeck { DialogResult SimpleDialog(const DialogData &data); } -#endif //EMU_LAUNCHER_COMPONENTS_H +#endif // EMU_LAUNCHER_COMPONENTS_H diff --git a/src/gui/windows/about.cpp b/src/gui/windows/about.cpp index 5ec970b..ad1e59d 100644 --- a/src/gui/windows/about.cpp +++ b/src/gui/windows/about.cpp @@ -19,13 +19,8 @@ namespace CoreDeck { ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_Appearing); - constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoDocking; - if (ImGui::BeginPopupModal("About CoreDeck", &context.UI.ShowAboutDialog, flags)) { + if (ImGui::BeginPopupModal("About CoreDeck", &context.UI.ShowAboutDialog, WindowNoResizeFlags)) { ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); const float titleWidth = ImGui::CalcTextSize(COREDECK_TITLE).x; ImGui::SetCursorPosX((ImGui::GetWindowWidth() - titleWidth) * 0.5f); diff --git a/src/gui/windows/about.h b/src/gui/windows/about.h index 55bc79a..15899cb 100644 --- a/src/gui/windows/about.h +++ b/src/gui/windows/about.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildAboutWindow(Context &context); } -#endif //COREDECK_ABOUT_H +#endif // COREDECK_ABOUT_H diff --git a/src/gui/windows/avd_info.cpp b/src/gui/windows/avd_info.cpp index 00bb676..44d6e17 100644 --- a/src/gui/windows/avd_info.cpp +++ b/src/gui/windows/avd_info.cpp @@ -2,51 +2,62 @@ // Created by AbdulMuaz Aqeel on 14/04/2026. // #include +#include #include "imgui.h" #include "avd_info.h" #include "../application.h" #include "../widgets.h" -#include "../theme.h" #include "../../core/utilities.h" namespace CoreDeck { + static std::string JoinAvdInfoList(const std::vector &items) { + std::stringstream stream; + for (int i = 0; i < static_cast(items.size()); i++) { + if (i > 0) stream << ", "; + stream << items[i]; + } + return stream.str(); + } + + static const char *SystemImageKindLabel(const AvdInfo &avd) { + if (avd.IsGooglePlayImage) return "Google Play"; + if (avd.IsGoogleApisImage) return "Google APIs"; + if (!avd.SystemImageTagDisplay.empty()) return avd.SystemImageTagDisplay.c_str(); + return "Default"; + } + void BuildAvdInfoWindow(Context &context) { if (!context.UI.ShowDetailsPanel) return; - constexpr ImGuiWindowFlags panelFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; + constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; if (context.Catalog.SelectedAvd < 0) { - ImGui::Begin("Details###Details", nullptr, panelFlags); + ImGui::Begin("Details###Details", nullptr, flags); ImGui::TextDisabled("Select an AVD to view details"); ImGui::End(); return; } - const auto &[ - Name, - DisplayName, - Device, - ApiLevel, - Abi, - SdCard, - RamSize, - ScreenResolution, - GpuMode, - Arch, - Path - ] = context.Catalog.Avds[context.Catalog.SelectedAvd]; + const auto &avd = context.Catalog.Avds[context.Catalog.SelectedAvd]; + const auto &Name = avd.Name; + const auto &DisplayName = avd.DisplayName; + const auto &Device = avd.Device; + const auto &ApiLevel = avd.ApiLevel; + const auto &Abi = avd.Abi; + const auto &SdCard = avd.SdCard; + const auto &RamSize = avd.RamSize; + const auto &ScreenResolution = avd.ScreenResolution; + const auto &GpuMode = avd.GpuMode; + const auto &Arch = avd.Arch; + const auto &Path = avd.Path; const auto args = BuildArgs(Name, GetDefaultAvdOptions(context)); std::string preview = context.Host.Sdk.EmulatorPath; for (const auto &arg: args) preview += " " + arg; - ImGui::Begin( - ("Details - " + DisplayName + "###Details").c_str(), - nullptr, - panelFlags - ); + ImGui::Begin(("Details - " + DisplayName + "###Details").c_str(), nullptr, flags); PropertyTextWrapped("AVD Path", Path.c_str()); ImGui::Spacing(); @@ -63,7 +74,23 @@ namespace CoreDeck { if (!RamSize.empty()) PropertyText("RAM", (RamSize + " MB").c_str(), false, true); if (!ScreenResolution.empty()) PropertyText("Resolution", ScreenResolution.c_str(), false, true); if (!SdCard.empty()) PropertyText("Storage", SdCard.c_str(), false, true); - if (!GpuMode.empty()) PropertyText("GPU Mode", GpuMode.c_str(), false, true); + if (!GpuMode.empty()) PropertyText("GPU Mode", GpuModeDisplayLabel(GpuMode), false, true); + + if (!avd.SystemImagePath.empty() || + !avd.SystemImageVariant.empty() || + !avd.SystemImageTagDisplay.empty() || + !avd.SystemImageTagDisplayNames.empty()) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + PropertyText("Type", SystemImageKindLabel(avd), false, true); + PropertyText("16 KB Page Size", avd.Supports16KbPageSize ? "Supported" : "Not supported", false, true); + if (!avd.SystemImageTagDisplayNames.empty()) { + const std::string tags = JoinAvdInfoList(avd.SystemImageTagDisplayNames); + PropertyTextWrapped("Tags", tags.c_str(), true); + } + } if (!Path.empty() && std::filesystem::exists(Path)) { ImGui::Spacing(); @@ -84,14 +111,22 @@ namespace CoreDeck { ImGui::Spacing(); const bool isRunning = context.Host.Manager.IsRunning(Name); + const float buttonSpacing = ImGui::GetStyle().ItemSpacing.x; + const float halfWidth = (ImGui::GetContentRegionAvail().x - buttonSpacing) * 0.5f; + if (isRunning) ImGui::BeginDisabled(); - if (WarningButton("Wipe User Data", !isRunning)) { + if (WarningButton("Wipe User Data", !isRunning, ImVec2(halfWidth, 0))) { context.UI.ShowWipeDataDialog = true; } if (isRunning) ImGui::EndDisabled(); if (isRunning && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { ImGui::SetTooltip("Stop the emulator before wiping data"); } + + ImGui::SameLine(); + if (PrimaryButton("Storage Overview", true, ImVec2(halfWidth, 0))) { + context.UI.ShowStorageDialog = true; + } } if (!context.Jobs.AvdWipe.Busy.load() && context.Jobs.AvdWipe.Future.valid()) { @@ -101,12 +136,11 @@ namespace CoreDeck { if (context.UI.ShowWipeDataDialog) { const bool isWiping = context.Jobs.AvdWipe.Busy.load(); - DialogData wipeDialog{ + const DialogData wipeDialog{ .Id = "WipeUserData", .isOpen = context.UI.ShowWipeDataDialog, .title = "Wipe User Data", - .message = - "This will delete userdata, cache, SD card images, and snapshots for this AVD. This cannot be undone.\n\nContinue?", + .message = "This will delete userdata, cache, SD card images, and snapshots for this AVD. This cannot be undone.\n\nContinue?", .confirmButtonTitle = "Wipe", .cancelButtonTitle = "Cancel", .busyButtonTitle = "Wiping...", diff --git a/src/gui/windows/avd_info.h b/src/gui/windows/avd_info.h index cf0b6e4..69711d7 100644 --- a/src/gui/windows/avd_info.h +++ b/src/gui/windows/avd_info.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildAvdInfoWindow(Context &context); } -#endif //COREDECK_AVD_DETAILS_H +#endif // COREDECK_AVD_DETAILS_H diff --git a/src/gui/windows/avd_list.cpp b/src/gui/windows/avd_list.cpp index b845c52..711b337 100644 --- a/src/gui/windows/avd_list.cpp +++ b/src/gui/windows/avd_list.cpp @@ -30,20 +30,22 @@ namespace CoreDeck { return {Icons::Mobile, "#4FC3F7"}; } + static const char *AvdTypeLabel(const AvdInfo &avd) { + if (avd.IsGooglePlayImage) return "Google Play"; + if (avd.IsGoogleApisImage) return "Google APIs"; + if (!avd.SystemImageTagDisplay.empty()) return avd.SystemImageTagDisplay.c_str(); + return "Default"; + } + static bool ContainsCaseInsensitive(const std::string &haystack, const char *needle) { if (needle[0] == '\0') return true; const auto len = std::strlen(needle); if (len > haystack.size()) return false; - return std::search( - haystack.begin(), haystack.end(), - needle, needle + len, - [](const char a, const char b) { - return std::tolower(static_cast(a)) == - std::tolower(static_cast(b)); - } - ) != haystack.end(); + return std::search(haystack.begin(), haystack.end(), needle, needle + len, [](const char a, const char b) { + return std::tolower(static_cast(a)) == std::tolower(static_cast(b)); + }) != haystack.end(); } static void RebuildFilteredIndices(Context &context) { @@ -57,7 +59,8 @@ namespace CoreDeck { if (!ContainsCaseInsensitive(avd.DisplayName, catalog.SearchFilter) && !ContainsCaseInsensitive(avd.Name, catalog.SearchFilter) && !ContainsCaseInsensitive(avd.Device, catalog.SearchFilter) && - !ContainsCaseInsensitive(avd.ApiLevel, catalog.SearchFilter)) { + !ContainsCaseInsensitive(avd.ApiLevel, catalog.SearchFilter) && + !ContainsCaseInsensitive(AvdTypeLabel(avd), catalog.SearchFilter)) { continue; } } @@ -101,8 +104,8 @@ namespace CoreDeck { void BuildAvdListWindow(Context &context) { if (!context.UI.ShowAvdListPanel) return; - constexpr ImGuiWindowFlags panelFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; - ImGui::Begin("Available AVDs (Android Virtual Device)###AVDs", nullptr, panelFlags); + constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; + ImGui::Begin("Available AVDs (Android Virtual Device)###AVDs", nullptr, flags); auto openCreateAvdDialog = [&context] { context.AvdCreationWork.CreationData = {}; @@ -156,10 +159,8 @@ namespace CoreDeck { wipeArgs.emplace_back("-wipe-data"); context.Host.Manager.Launch(avd.Name, wipeArgs); } - ImGui::SameLine(0, 15.0f); - ImGui::Text("-"); - ImGui::SameLine(0, 15.0f); - if (PrimaryButton(Icons::Trash)) { + ImGui::SameLine(); + if (NegativeButton(Icons::Trash)) { if (context.Prefs.ConfirmBeforeDeleteAvd) { context.UI.ShowDeleteAvdDialog = true; } else { @@ -168,11 +169,6 @@ namespace CoreDeck { } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Delete currently selected AVD"); } - - if (isRunning) { - ImGui::SameLine(0, 15.0f); - ImGui::Text("-"); - } } ImGui::Separator(); @@ -183,42 +179,41 @@ namespace CoreDeck { return; } - ImGui::Spacing(); { - const char *sortDirIcon = context.Catalog.SortAscending ? Icons::SortUp : Icons::SortDown; - const char *sortDirTooltip = context.Catalog.SortAscending ? "Ascending" : "Descending"; - if (PrimaryButton(sortDirIcon)) { - context.Catalog.SortAscending = !context.Catalog.SortAscending; - PersistAppSettings(context); - } - if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", sortDirTooltip); + ImGui::Spacing(); + const char *sortDirIcon = context.Catalog.SortAscending ? Icons::SortUp : Icons::SortDown; + const char *sortDirTooltip = context.Catalog.SortAscending ? "Ascending" : "Descending"; + if (PrimaryButton(sortDirIcon)) { + context.Catalog.SortAscending = !context.Catalog.SortAscending; + PersistAppSettings(context); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", sortDirTooltip); - ImGui::SameLine(); + ImGui::SameLine(); - ImGui::SetNextItemWidth(150.0f); - const int currentSortIdx = static_cast(context.Catalog.SortMode); - if (ImGui::BeginCombo("##AvdSort", SortModeLabels[currentSortIdx])) { - for (int i = 0; i < SortModeCount; i++) { - const bool selected = currentSortIdx == i; - if (ImGui::Selectable(SortModeLabels[i], selected)) { - context.Catalog.SortMode = static_cast(i); - PersistAppSettings(context); - } - if (selected) ImGui::SetItemDefaultFocus(); + ImGui::SetNextItemWidth(150.0f); + const int currentSortIdx = static_cast(context.Catalog.SortMode); + if (ImGui::BeginCombo("##AvdSort", SortModeLabels[currentSortIdx])) { + for (int i = 0; i < SortModeCount; i++) { + const bool selected = currentSortIdx == i; + if (ImGui::Selectable(SortModeLabels[i], selected)) { + context.Catalog.SortMode = static_cast(i); + PersistAppSettings(context); } - ImGui::EndCombo(); + if (selected) ImGui::SetItemDefaultFocus(); } + ImGui::EndCombo(); + } - ImGui::SameLine(); + ImGui::SameLine(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - const auto searchHint = IconWithLabel(Icons::Search, "Search AVDs..."); - ImGui::InputTextWithHint( - "##AvdSearch", - searchHint.c_str(), - context.Catalog.SearchFilter, - IM_ARRAYSIZE(context.Catalog.SearchFilter) - ); - } + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + const auto searchHint = IconWithLabel(Icons::Search, "Search AVDs..."); + ImGui::InputTextWithHint( + "##AvdSearch", + searchHint.c_str(), + context.Catalog.SearchFilter, + IM_ARRAYSIZE(context.Catalog.SearchFilter) + ); ImGui::Spacing(); RebuildFilteredIndices(context); @@ -252,9 +247,9 @@ namespace CoreDeck { ImGui::PushID(i); const char *avdStatusText = isRunning ? "Running..." : "Ready"; const ImVec4 avdStatusColor = isRunning ? HexColor("#33CC47") : HexColor("#66666B"); - const DeviceIconStyle iconStyle = DeviceIconStyleFor(avd.Device); - if (SelectableItem(avd.DisplayName.c_str(), isSelected, avdStatusText, avdStatusColor, - iconStyle.Icon, HexColor(iconStyle.HexColor))) { + const std::string avdRightText = StrConcat(AvdTypeLabel(avd), " - ", avdStatusText); + const auto [Icon, Color] = DeviceIconStyleFor(avd.Device); + if (SelectableItem(avd.DisplayName.c_str(), isSelected, avdRightText.c_str(), avdStatusColor, Icon, HexColor(Color))) { context.Catalog.SelectedAvd = i; } ImGui::PopID(); diff --git a/src/gui/windows/avd_list.h b/src/gui/windows/avd_list.h index aac59b0..8edea95 100644 --- a/src/gui/windows/avd_list.h +++ b/src/gui/windows/avd_list.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildAvdListWindow(Context &context); } -#endif //COREDECK_AVD_LIST_H +#endif // COREDECK_AVD_LIST_H diff --git a/src/gui/windows/avd_logs.cpp b/src/gui/windows/avd_logs.cpp index 1c6cc88..9b0054c 100644 --- a/src/gui/windows/avd_logs.cpp +++ b/src/gui/windows/avd_logs.cpp @@ -70,9 +70,8 @@ namespace CoreDeck { return; } - std::string searchQuery; const std::string &avdName = context.Catalog.Avds[context.Catalog.SelectedAvd].Name; - searchQuery = context.Logs.PerAvdLogSearch[avdName]; + const std::string searchQuery = context.Logs.PerAvdLogSearch[avdName]; const bool hasSearch = !searchQuery.empty(); bool hasVisibleLines = false; diff --git a/src/gui/windows/avd_logs.h b/src/gui/windows/avd_logs.h index a79eab9..92e9d42 100644 --- a/src/gui/windows/avd_logs.h +++ b/src/gui/windows/avd_logs.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildAvdLogsWindow(Context &context); } -#endif //COREDECK_AVD_LOGS_H +#endif // COREDECK_AVD_LOGS_H diff --git a/src/gui/windows/avd_options.cpp b/src/gui/windows/avd_options.cpp index 4525902..c0c12a7 100644 --- a/src/gui/windows/avd_options.cpp +++ b/src/gui/windows/avd_options.cpp @@ -14,18 +14,14 @@ namespace CoreDeck { void BuildAvdOptionsWindow(Context &context) { if (!context.UI.ShowOptionsPanel) return; - constexpr ImGuiWindowFlags panelFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; + constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; std::string panelTitle = "Options"; if (context.Catalog.SelectedAvd >= 0 && context.Catalog.SelectedAvd < context.Catalog.Avds.size()) { panelTitle = "Options - " + context.Catalog.Avds[context.Catalog.SelectedAvd].DisplayName; } - ImGui::Begin( - (panelTitle + "###Options").c_str(), - nullptr, - panelFlags - ); + ImGui::Begin((panelTitle + "###Options").c_str(),nullptr,flags); if (context.Catalog.SelectedAvd < 0) { ImGui::TextDisabled("Select an AVD to configure options"); @@ -47,9 +43,7 @@ namespace CoreDeck { if (CollapsingHeader(category.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Indent(20.0f); - for (auto &[Flag, DisplayName, Description, Enabled, Type, Category, Hint, TextInput, Items, - SelectedItem] - : options) { + for (auto &[Flag, DisplayName, Description, Enabled, Type, Category, Hint, TextInput, Items, SelectedItem]: options) { if (category != Category) continue; ImGui::PushID(Flag.c_str()); @@ -78,10 +72,12 @@ namespace CoreDeck { case OptionType::Selection: { ImGui::SetNextItemWidth(-1.0f); - if (ImGui::BeginCombo("##selection", Items[SelectedItem].c_str())) { + const char *selectedLabel = EmulatorOptionItemDisplayLabel(Flag, Items[SelectedItem]); + if (ImGui::BeginCombo("##selection", selectedLabel)) { for (int i = 0; i < Items.size(); ++i) { const bool isSelected = SelectedItem == i; - if (ImGui::Selectable(Items[i].c_str(), isSelected)) { + const char *itemLabel = EmulatorOptionItemDisplayLabel(Flag, Items[i]); + if (ImGui::Selectable(itemLabel, isSelected)) { SelectedItem = i; optionsChanged = true; } diff --git a/src/gui/windows/avd_options.h b/src/gui/windows/avd_options.h index 18f7c1c..316712b 100644 --- a/src/gui/windows/avd_options.h +++ b/src/gui/windows/avd_options.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildAvdOptionsWindow(Context &context); } -#endif //COREDECK_AVD_OPTIONS_H +#endif // COREDECK_AVD_OPTIONS_H diff --git a/src/gui/windows/create_avd.cpp b/src/gui/windows/create_avd.cpp index a1034ce..ed0b7b0 100644 --- a/src/gui/windows/create_avd.cpp +++ b/src/gui/windows/create_avd.cpp @@ -3,15 +3,35 @@ // #include -#include #include "imgui.h" #include "create_avd.h" +#include "device_profile.h" +#include "install_image.h" #include "../application.h" #include "../widgets.h" #include "../theme.h" namespace CoreDeck { + static void OpenSystemImagePicker(Context &context) { + context.ImageInstallationWork.SelectedImage = -1; + context.ImageInstallationWork.SelectedCategory = ImageCategory::PhoneTablet; + context.ImageInstallationWork.SearchFilter[0] = '\0'; + context.ImageInstallationWork.Progress.reset(); + context.ImageInstallationWork.Prefetch.Ready = false; + context.ImageInstallationWork.Prefetch.Loading = true; + context.UI.ShowInstallImageDialog = true; + + context.ImageInstallationWork.Prefetch.Future = std::async(std::launch::async, [&context] { + const auto localImages = ListSystemImages(context.Host.Sdk); + auto remoteImages = ListRemoteSystemImages(context.Host.Sdk, localImages); + context.AvdCreationWork.SystemImages = localImages; + context.ImageInstallationWork.RemoteImages = std::move(remoteImages); + context.ImageInstallationWork.Prefetch.Loading = false; + context.ImageInstallationWork.Prefetch.Ready = true; + }); + } + // ReSharper disable once CppParameterMayBeConstPtrOrRef static int DigitsOnlyFilter(ImGuiInputTextCallbackData *data) { return data->EventChar >= '0' && data->EventChar <= '9' ? 0 : 1; @@ -27,12 +47,8 @@ namespace CoreDeck { static bool AvdNameExists(const std::vector &names, const std::string &candidate) { if (candidate.empty()) return false; - auto lower = [](std::string s) { - std::ranges::transform(s, s.begin(), [](const unsigned char ch) { return std::tolower(ch); }); - return s; - }; - const std::string needle = lower(candidate); - return std::ranges::any_of(names, [&](const std::string &n) { return lower(n) == needle; }); + const std::string needle = LowerCopy(candidate); + return std::ranges::any_of(names, [&](const std::string &n) { return LowerCopy(n) == needle; }); } void BuildCreateAvdWindow(Context &context) { @@ -44,59 +60,33 @@ namespace CoreDeck { ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(ImVec2(500, 0), ImGuiCond_Appearing); - constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoDocking; - - if (ImGui::BeginPopupModal("Create New AVD###CreateAvdDialog", &context.UI.ShowCreateAvdDialog, flags)) { - auto &[Busy, Future] = context.AvdCreationWork.SystemImageRemoval; - if (Future.valid()) { - if (Future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - if (Future.get()) { - context.AvdCreationWork.SystemImages = ListSystemImages(context.Host.Sdk); - if (context.AvdCreationWork.SystemImages.empty()) { - context.AvdCreationWork.SelectedSystemImage = 0; - } else { - int &sel = context.AvdCreationWork.SelectedSystemImage; - sel = std::clamp(sel, 0, static_cast(context.AvdCreationWork.SystemImages.size()) - 1); - } - } - } - } - + if (ImGui::BeginPopupModal("Create New AVD###CreateAvdDialog", &context.UI.ShowCreateAvdDialog, WindowAutoResizeFlags)) { const bool isLoading = context.AvdCreationWork.Prefetch.Loading.load(); const bool isCreating = context.Jobs.AvdCreation.Busy.load(); const bool formDisabled = isLoading || isCreating; - const bool systemImageRemovalBusy = Busy.load() || Future.valid(); if (formDisabled) ImGui::BeginDisabled(); auto &work = context.AvdCreationWork; - const bool hasDevice = !work.DeviceProfiles.empty() - && work.SelectedDevice >= 0 - && work.SelectedDevice < static_cast(work.DeviceProfiles.size()); - const bool hasImage = !work.SystemImages.empty() - && work.SelectedSystemImage >= 0 - && work.SelectedSystemImage < static_cast(work.SystemImages.size()); - if (hasDevice && hasImage) { - const auto &[Id, Name] = work.DeviceProfiles[work.SelectedDevice]; + const bool hasDeviceProfile = !work.DeviceProfiles.empty() && work.SelectedDevice >= 0 && work.SelectedDevice < static_cast(work.DeviceProfiles.size()); + const bool hasImage = !work.SystemImages.empty() && work.SelectedSystemImage >= 0 && work.SelectedSystemImage < static_cast(work.SystemImages.size()); + if (hasImage) { const auto &img = work.SystemImages[work.SelectedSystemImage]; + const std::string deviceId = hasDeviceProfile ? work.DeviceProfiles[work.SelectedDevice].Id : "Android"; + const std::string deviceName = hasDeviceProfile ? work.DeviceProfiles[work.SelectedDevice].Name : "Android Device"; + if (work.NameAutoFilled) { - std::string base = Id + "_API_" + img.ApiLevel; + const std::string base = deviceId + "_API_" + img.ApiLevel; std::string sanitized; sanitized.reserve(base.size()); for (const char c: base) { - const bool keep = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') - || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; + const bool keep = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; sanitized.push_back(keep ? c : '_'); } work.CreationData.Name = std::move(sanitized); } if (work.DisplayNameAutoFilled) { - work.CreationData.DisplayName = Name + " API " + img.ApiLevel; + work.CreationData.DisplayName = deviceName + " API " + img.ApiLevel; } } @@ -105,8 +95,7 @@ namespace CoreDeck { strncpy(nameBuffer, context.AvdCreationWork.CreationData.Name.c_str(), sizeof(nameBuffer) - 1); nameBuffer[sizeof(nameBuffer) - 1] = '\0'; ImGui::SetNextItemWidth(-1.0f); - if (ImGui::InputTextWithHint("##AvdName", "e.g. MyPixel7", nameBuffer, sizeof(nameBuffer), - ImGuiInputTextFlags_CallbackCharFilter, AvdNameFilter)) { + if (ImGui::InputTextWithHint("##AvdName", "e.g. MyPixel7", nameBuffer, sizeof(nameBuffer), ImGuiInputTextFlags_CallbackCharFilter, AvdNameFilter)) { context.AvdCreationWork.CreationData.Name = nameBuffer; context.AvdCreationWork.NameAutoFilled = (nameBuffer[0] == '\0'); } @@ -137,120 +126,54 @@ namespace CoreDeck { } ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - ImGui::Text("System Image"); if (context.AvdCreationWork.Prefetch.Ready && context.AvdCreationWork.SystemImages.empty()) { - ImGui::TextColored( - HexColor("#E64D40"), - "No system images installed." - ); + if (!context.Host.Sdk.SdkManagerPath.empty()) { + if (PickerButton("No system images available. Install one...", !formDisabled, ImVec2(-1.0f, 0.0f))) { + OpenSystemImagePicker(context); + } + } else { + PickerButton("No system images installed", false, ImVec2(-1.0f, 0.0f)); + ImGui::TextColored( + HexColor("#E64D40"), + "SDK Manager was not found, so CoreDeck cannot install images automatically." + ); + } } else if (!context.AvdCreationWork.SystemImages.empty()) { - ImGui::SetNextItemWidth(-1.0f); const auto &systemImages = context.AvdCreationWork.SystemImages; const auto &selectedSystemImage = context.AvdCreationWork.SelectedSystemImage; - if (ImGui::BeginCombo("##SystemImage", systemImages[selectedSystemImage].DisplayName.c_str())) { - for (int i = 0; i < static_cast(context.AvdCreationWork.SystemImages.size()); i++) { - const bool isSelected = context.AvdCreationWork.SelectedSystemImage == i; - if (ImGui::Selectable(context.AvdCreationWork.SystemImages[i].DisplayName.c_str(), - isSelected)) { - context.AvdCreationWork.SelectedSystemImage = i; - } - if (isSelected) ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - } else { - ImGui::SetNextItemWidth(-1.0f); - ImGui::BeginCombo("##SystemImage", "Loading..."); - } - - if (!context.Host.Sdk.SdkManagerPath.empty()) { - if (PrimaryButton("Install New Image...", !formDisabled)) { - context.ImageInstallationWork.SelectedImage = 0; - context.ImageInstallationWork.Progress.reset(); - context.ImageInstallationWork.Prefetch.Ready = false; - context.ImageInstallationWork.Prefetch.Loading = true; - context.UI.ShowInstallImageDialog = true; - - context.ImageInstallationWork.Prefetch.Future = std::async(std::launch::async, [&context] { - const auto localImages = ListSystemImages(context.Host.Sdk); - auto remoteImages = ListRemoteSystemImages(context.Host.Sdk, localImages); - context.ImageInstallationWork.RemoteImages = std::move(remoteImages); - context.ImageInstallationWork.Prefetch.Loading = false; - context.ImageInstallationWork.Prefetch.Ready = true; - }); + const std::string preview = SystemImagePreviewLabel(systemImages[selectedSystemImage]); - context.UI.ReopenCreateAvdOnInstallClose = true; - context.UI.ShowCreateAvdDialog = false; - ImGui::CloseCurrentPopup(); - } - if (ImGui::IsItemHovered()) ImGui::SetTooltip("Download and install a new system image from the SDK"); - - ImGui::SameLine(); - const bool canRemove = - !formDisabled && - context.AvdCreationWork.Prefetch.Ready && - !context.AvdCreationWork.SystemImages.empty() && - context.AvdCreationWork.SelectedSystemImage >= 0 && - context.AvdCreationWork.SelectedSystemImage < static_cast(context.AvdCreationWork. - SystemImages.size()); - if (systemImageRemovalBusy) { - ImGui::BeginDisabled(); - NegativeButton("Removing...", false, ImVec2(0, 0)); - ImGui::EndDisabled(); - } else { - if (NegativeButton("Remove Image...", canRemove)) { - const std::string pkg = - context.AvdCreationWork.SystemImages[context.AvdCreationWork.SelectedSystemImage]. - PackagePath; - Busy = true; - Future = std::async(std::launch::async, [&context, pkg]() { - try { - const bool ok = UninstallSystemImage(context.Host.Sdk, pkg); - context.AvdCreationWork.SystemImageRemoval.Busy = false; - return ok; - } catch (...) { - context.AvdCreationWork.SystemImageRemoval.Busy = false; - return false; - } - }); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Uninstall the selected system image via SDK manager"); + if (PickerButton(preview.c_str(), !formDisabled, ImVec2(-1.0f, 0.0f))) { + if (!context.Host.Sdk.SdkManagerPath.empty()) { + OpenSystemImagePicker(context); } } + } else { + PickerButton("Loading system images...", false, ImVec2(-1.0f, 0.0f)); } ImGui::Spacing(); ImGui::Text("Device"); if (context.AvdCreationWork.Prefetch.Ready && context.AvdCreationWork.DeviceProfiles.empty()) { - ImGui::TextColored(HexColor("#E64D40"), "No device profiles found."); + ImGui::TextDisabled("No device profiles found."); + ImGui::TextWrapped("CoreDeck will use avdmanager's default hardware profile."); } else if (!context.AvdCreationWork.DeviceProfiles.empty()) { - ImGui::SetNextItemWidth(-1.0f); - if (ImGui::BeginCombo( - "##device", - context.AvdCreationWork.DeviceProfiles[context.AvdCreationWork.SelectedDevice].Name.c_str())) { - for (int i = 0; i < static_cast(context.AvdCreationWork.DeviceProfiles.size()); i++) { - const bool isSelected = context.AvdCreationWork.SelectedDevice == i; - if (ImGui::Selectable(context.AvdCreationWork.DeviceProfiles[i].Name.c_str(), isSelected)) { - context.AvdCreationWork.SelectedDevice = i; - } - if (isSelected) ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); + const auto &selectedDevice = context.AvdCreationWork.DeviceProfiles[context.AvdCreationWork.SelectedDevice]; + const std::string devicePreview = DeviceProfilePreviewLabel(selectedDevice); + + if (PickerButton(devicePreview.c_str(), !formDisabled, ImVec2(-1.0f, 0.0f))) { + context.AvdCreationWork.PendingSelectedDevice = context.AvdCreationWork.SelectedDevice; + context.AvdCreationWork.DeviceSearchFilter[0] = '\0'; + context.AvdCreationWork.SelectedDeviceCategory = DeviceCategory::Phone; + context.UI.ShowDeviceProfileDialog = true; } } else { - ImGui::SetNextItemWidth(-1.0f); - ImGui::BeginCombo("##device", "Loading..."); + PickerButton("Loading device profiles...", false, ImVec2(-1.0f, 0.0f)); } ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - const float rowSpacing = ImGui::GetStyle().ItemSpacing.x; const float colWidth = (ImGui::GetContentRegionAvail().x - rowSpacing) * 0.5f; const float col2X = ImGui::GetCursorPosX() + colWidth + rowSpacing; @@ -264,8 +187,7 @@ namespace CoreDeck { strncpy(ramBuffer, context.AvdCreationWork.CreationData.RamSize.c_str(), sizeof(ramBuffer) - 1); ramBuffer[sizeof(ramBuffer) - 1] = '\0'; ImGui::SetNextItemWidth(colWidth); - if (ImGui::InputTextWithHint("##ram", "e.g. 2048 (MB)", ramBuffer, sizeof(ramBuffer), - ImGuiInputTextFlags_CallbackCharFilter, DigitsOnlyFilter)) { + if (ImGui::InputTextWithHint("##ram", "e.g. 2048 (MB)", ramBuffer, sizeof(ramBuffer), ImGuiInputTextFlags_CallbackCharFilter, DigitsOnlyFilter)) { context.AvdCreationWork.CreationData.RamSize = ramBuffer; } @@ -276,20 +198,19 @@ namespace CoreDeck { strncpy(sdBuffer, context.AvdCreationWork.CreationData.SdCardSize.c_str(), sizeof(sdBuffer) - 1); sdBuffer[sizeof(sdBuffer) - 1] = '\0'; ImGui::SetNextItemWidth(colWidth); - if (ImGui::InputTextWithHint("##sdcard", "e.g. 512 (MB)", sdBuffer, sizeof(sdBuffer), - ImGuiInputTextFlags_CallbackCharFilter, DigitsOnlyFilter)) { + if (ImGui::InputTextWithHint("##sdcard", "e.g. 512 (MB)", sdBuffer, sizeof(sdBuffer), ImGuiInputTextFlags_CallbackCharFilter, DigitsOnlyFilter)) { context.AvdCreationWork.CreationData.SdCardSize = sdBuffer; } ImGui::Spacing(); ImGui::Text("GPU Mode"); - static const char *gpuModes[] = {"auto", "host", "swiftshader_indirect", "guest"}; + const auto &gpuModes = GpuModeOptions(); ImGui::SetNextItemWidth(-1.0f); - if (ImGui::BeginCombo("##gpu", gpuModes[context.AvdCreationWork.SelectedGpuMode])) { - for (int i = 0; i < 4; i++) { + if (ImGui::BeginCombo("##gpu", gpuModes[context.AvdCreationWork.SelectedGpuMode].Label)) { + for (int i = 0; i < static_cast(gpuModes.size()); i++) { const bool isSelected = context.AvdCreationWork.SelectedGpuMode == i; - if (ImGui::Selectable(gpuModes[i], isSelected)) { + if (ImGui::Selectable(gpuModes[i].Label, isSelected)) { context.AvdCreationWork.SelectedGpuMode = i; } if (isSelected) ImGui::SetItemDefaultFocus(); @@ -305,11 +226,7 @@ namespace CoreDeck { const float spacing = ImGui::GetStyle().ItemSpacing.x; const float halfWidth = (ImGui::GetContentRegionAvail().x - spacing) * 0.5f; - const bool canCreate = !context.AvdCreationWork.CreationData.Name.empty() - && !context.AvdCreationWork.SystemImages.empty() - && !context.AvdCreationWork.DeviceProfiles.empty() - && !nameConflict - && !formDisabled; + const bool canCreate = !context.AvdCreationWork.CreationData.Name.empty() && hasImage && !nameConflict && !formDisabled; if (isCreating) { ImGui::BeginDisabled(); @@ -317,17 +234,13 @@ namespace CoreDeck { ImGui::EndDisabled(); } else { if (PositiveButton("Create", canCreate, ImVec2(halfWidth, 0))) { - const auto &SystemImagePackagePath = context.AvdCreationWork.SystemImages[ - context.AvdCreationWork.SelectedSystemImage - ].PackagePath; - - const auto &DeviceId = context.AvdCreationWork.DeviceProfiles[ - context.AvdCreationWork.SelectedDevice - ].Id; + const auto &SystemImagePackagePath = context.AvdCreationWork.SystemImages[context.AvdCreationWork.SelectedSystemImage].PackagePath; context.AvdCreationWork.CreationData.SystemImagePackagePath = SystemImagePackagePath; - context.AvdCreationWork.CreationData.DeviceId = DeviceId; - context.AvdCreationWork.CreationData.GpuMode = gpuModes[context.AvdCreationWork.SelectedGpuMode]; + context.AvdCreationWork.CreationData.DeviceId = hasDeviceProfile + ? context.AvdCreationWork.DeviceProfiles[context.AvdCreationWork.SelectedDevice].Id + : ""; + context.AvdCreationWork.CreationData.GpuMode = gpuModes[context.AvdCreationWork.SelectedGpuMode].Value; if (!context.AvdCreationWork.CreationData.SdCardSize.empty()) { context.AvdCreationWork.CreationData.SdCardSize += "M"; } @@ -352,7 +265,9 @@ namespace CoreDeck { RefreshAvds(context); } - ImGui::Spacing(); + BuildDeviceProfileWindow(context); + BuildInstallImageWindow(context); + ImGui::EndPopup(); } } diff --git a/src/gui/windows/create_avd.h b/src/gui/windows/create_avd.h index 2d0e771..44c0f4b 100644 --- a/src/gui/windows/create_avd.h +++ b/src/gui/windows/create_avd.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildCreateAvdWindow(Context &context); } -#endif //COREDECK_CREATE_AVD_H +#endif // COREDECK_CREATE_AVD_H diff --git a/src/gui/windows/delete_avd.cpp b/src/gui/windows/delete_avd.cpp index 1550cda..48ab5d2 100644 --- a/src/gui/windows/delete_avd.cpp +++ b/src/gui/windows/delete_avd.cpp @@ -35,17 +35,15 @@ namespace CoreDeck { const std::string title = "Delete \"" + avdName + "\"?"; const bool isDeleting = context.Jobs.AvdDeletion.Busy.load(); const DialogResult result = SimpleDialog( - { - .Id = "Delete###DeleteAvdDialog", - .isOpen = context.UI.ShowDeleteAvdDialog, - .title = title.c_str(), - .message = "This will permanently remove the AVD and all its data. This action cannot be undone.", - .confirmButtonTitle = "Delete", - .cancelButtonTitle = "Cancel", - .busyButtonTitle = "Deleting...", - .type = DialogType::Negative, - .isBusy = isDeleting - } + {.Id = "Delete###DeleteAvdDialog", + .isOpen = context.UI.ShowDeleteAvdDialog, + .title = title.c_str(), + .message = "This will permanently remove the AVD and all its data. This action cannot be undone.", + .confirmButtonTitle = "Delete", + .cancelButtonTitle = "Cancel", + .busyButtonTitle = "Deleting...", + .type = DialogType::Negative, + .isBusy = isDeleting} ); if (result == DialogResult::Confirmed) { diff --git a/src/gui/windows/delete_avd.h b/src/gui/windows/delete_avd.h index 3948d70..5b0de1c 100644 --- a/src/gui/windows/delete_avd.h +++ b/src/gui/windows/delete_avd.h @@ -13,4 +13,4 @@ namespace CoreDeck { void BuildDeleteAvdWindow(Context &context); } -#endif //COREDECK_DELETE_AVD_H +#endif // COREDECK_DELETE_AVD_H diff --git a/src/gui/windows/device_profile.cpp b/src/gui/windows/device_profile.cpp new file mode 100644 index 0000000..2b072b3 --- /dev/null +++ b/src/gui/windows/device_profile.cpp @@ -0,0 +1,193 @@ +// +// Created by AbdulMuaz Aqeel on 02/05/2026. +// + +#include + +#include "imgui.h" + +#include "device_profile.h" +#include "../icons.h" +#include "../theme.h" +#include "../widgets.h" +#include "../../core/utilities.h" + +namespace CoreDeck { + struct DeviceCategoryOption { + DeviceCategory Category; + const char *Label; + }; + + DeviceCategory DeviceCategoryForProfile(const DeviceProfile &device) { + const std::string searchable = LowerCopy(StrConcat(device.Id, " ", device.Name)); + + if (searchable.find("wear") != std::string::npos || searchable.find("watch") != std::string::npos) { + return DeviceCategory::Wear; + } + if (searchable.find("automotive") != std::string::npos || searchable.find("auto") != std::string::npos) { + return DeviceCategory::Automotive; + } + if (searchable.find("tv") != std::string::npos) return DeviceCategory::Tv; + if (searchable.find("desktop") != std::string::npos) return DeviceCategory::Desktop; + if (searchable.find("tablet") != std::string::npos || + searchable.find("fold") != std::string::npos || + searchable.find("xl") != std::string::npos) { + return DeviceCategory::Tablet; + } + if (searchable.find("phone") != std::string::npos || + searchable.find("pixel") != std::string::npos || + searchable.find("nexus") != std::string::npos) { + return DeviceCategory::Phone; + } + return DeviceCategory::Other; + } + + static LabeledIconStyle DeviceProfileStyleFor(const DeviceProfile &device) { + switch (DeviceCategoryForProfile(device)) { + case DeviceCategory::Phone: + return {Icons::Mobile, "Phone", "#4FC3F7"}; + case DeviceCategory::Tablet: + return {Icons::Tablet, "Tablet", "#33CC47"}; + case DeviceCategory::Wear: + return {Icons::Watch, "Wear OS", "#F5A623"}; + case DeviceCategory::Tv: + return {Icons::Tv, "TV", "#7E57C2"}; + case DeviceCategory::Automotive: + return {Icons::Car, "Automotive", "#E64D40"}; + case DeviceCategory::Desktop: + return {Icons::Desktop, "Desktop", "#A7A7AD"}; + case DeviceCategory::All: + case DeviceCategory::Other: + return {Icons::Gear, "Other", "#A7A7AD"}; + } + return {Icons::Gear, "Other", "#A7A7AD"}; + } + + std::string DeviceProfilePreviewLabel(const DeviceProfile &device) { + const auto [Icon, Label, Color] = DeviceProfileStyleFor(device); + return StrConcat(device.Name, " - ", Label); + } + + static bool MatchesDeviceProfileFilters(const DeviceProfile &device, const char *filter, const DeviceCategory category) { + const bool matchesCategory = category == DeviceCategory::All || DeviceCategoryForProfile(device) == category; + return matchesCategory && ContainsIgnoreCase(StrConcat(device.Id, " ", device.Name), filter ? filter : ""); + } + + void BuildDeviceProfileWindow(Context &context) { + if (!context.UI.ShowDeviceProfileDialog) return; + + constexpr auto title = "Choose Device Profile###DeviceProfileDialog"; + if (!ImGui::IsPopupOpen(title)) { + ImGui::OpenPopup(title); + } + + const ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(760, 520), ImGuiCond_Appearing); + + if (ImGui::BeginPopupModal(title, &context.UI.ShowDeviceProfileDialog, WindowAutoResizeFlags)) { + auto &work = context.AvdCreationWork; + if (!work.DeviceProfiles.empty()) { + work.PendingSelectedDevice = std::clamp(work.PendingSelectedDevice, 0, static_cast(work.DeviceProfiles.size()) - 1); + } + + ImGui::SetNextItemWidth(-1.0f); + const std::string searchHint = IconWithLabel(Icons::Search, "Search device profiles..."); + ImGui::InputTextWithHint( + "##DeviceProfileSearch", + searchHint.c_str(), + work.DeviceSearchFilter, + sizeof(work.DeviceSearchFilter) + ); + + ImGui::Spacing(); + ImGui::TextDisabled("Categories"); + + static constexpr DeviceCategoryOption categoryOptions[] = { + {DeviceCategory::All, "All"}, + {DeviceCategory::Phone, "Phone"}, + {DeviceCategory::Tablet, "Tablet"}, + {DeviceCategory::Wear, "Wear OS"}, + {DeviceCategory::Tv, "TV"}, + {DeviceCategory::Automotive, "Automotive"}, + {DeviceCategory::Desktop, "Desktop"}, + {DeviceCategory::Other, "Other"}, + }; + + bool firstCategory = true; + for (const auto &[Category, Label]: categoryOptions) { + if (!firstCategory) ImGui::SameLine(); + firstCategory = false; + if (CategoryChip(Label, work.SelectedDeviceCategory == Category)) { + work.SelectedDeviceCategory = Category; + } + } + + ImGui::Spacing(); + ImGui::Text("Device Profiles"); + ImGui::Spacing(); + + { + PickerTableStyle tableStyle; + + ImGui::BeginChild("##DeviceProfileTableFrame", ImVec2(-1.0f, 280.0f), true, ImGuiWindowFlags_NoScrollbar); + if (ImGui::BeginTable("##DeviceProfileTable", 2, PickerTableFlags, ImVec2(-1.0f, -1.0f))) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(" Name", ImGuiTableColumnFlags_WidthStretch, 2.8f); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 130.0f); + ImGui::TableHeadersRow(); + + int visibleCount = 0; + for (int i = 0; i < static_cast(work.DeviceProfiles.size()); i++) { + const auto &device = work.DeviceProfiles[i]; + if (!MatchesDeviceProfileFilters(device, work.DeviceSearchFilter, work.SelectedDeviceCategory)) { + continue; + } + + visibleCount++; + const bool isSelected = work.PendingSelectedDevice == i; + const auto [Icon, Label, Color] = DeviceProfileStyleFor(device); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + const std::string rowLabel = StrConcat(" ", Icon, " ", device.Name, "##DeviceProfile", std::to_string(i)); + if (ImGui::Selectable(rowLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns)) { + work.PendingSelectedDevice = i; + } + if (isSelected) ImGui::SetItemDefaultFocus(); + + ImGui::TableNextColumn(); + ImGui::TextColored(HexColor(Color), "%s", Label); + } + + if (visibleCount == 0) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextDisabled("No device profiles match the selected form factor and search."); + } + + ImGui::EndTable(); + } + ImGui::EndChild(); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float halfWidth = (ImGui::GetContentRegionAvail().x - spacing) * 0.5f; + if (PrimaryButton("Use Selected Device", !work.DeviceProfiles.empty(), ImVec2(halfWidth, 0))) { + work.SelectedDevice = work.PendingSelectedDevice; + context.UI.ShowDeviceProfileDialog = false; + } + ImGui::SameLine(); + if (PrimaryButton("Cancel", true, ImVec2(halfWidth, 0))) { + context.UI.ShowDeviceProfileDialog = false; + } + + ImGui::EndPopup(); + } + } +} diff --git a/src/gui/windows/device_profile.h b/src/gui/windows/device_profile.h new file mode 100644 index 0000000..e6cc879 --- /dev/null +++ b/src/gui/windows/device_profile.h @@ -0,0 +1,20 @@ +// +// Created by AbdulMuaz Aqeel on 02/05/2026. +// + +#ifndef COREDECK_DEVICE_PROFILE_H +#define COREDECK_DEVICE_PROFILE_H + +#include + +#include "../context.h" + +namespace CoreDeck { + DeviceCategory DeviceCategoryForProfile(const DeviceProfile &device); + + std::string DeviceProfilePreviewLabel(const DeviceProfile &device); + + void BuildDeviceProfileWindow(Context &context); +} + +#endif // COREDECK_DEVICE_PROFILE_H diff --git a/src/gui/windows/install_image.cpp b/src/gui/windows/install_image.cpp index 6b2887c..41b6483 100644 --- a/src/gui/windows/install_image.cpp +++ b/src/gui/windows/install_image.cpp @@ -2,16 +2,63 @@ // Created by AbdulMuaz Aqeel on 18/04/2026. // +#include #include +#include #include "imgui.h" #include "install_image.h" #include "../application.h" +#include "../icons.h" #include "../widgets.h" #include "../theme.h" #include "../../core/utilities.h" namespace CoreDeck { + struct ImageCategoryOption { + ImageCategory Category; + const char *Label; + }; + + static ImageCategory CategoryForImage(const RemoteSystemImage &img) { + const std::string searchable = LowerCopy(StrConcat(img.PackagePath, " ", img.Variant, " ", img.DisplayName)); + + if (searchable.find("wear") != std::string::npos) return ImageCategory::Wear; + if (searchable.find("automotive") != std::string::npos || searchable.find("android-auto") != std::string::npos) { + return ImageCategory::Automotive; + } + if (searchable.find("desktop") != std::string::npos) return ImageCategory::Desktop; + if (searchable.find("xr") != std::string::npos) return ImageCategory::Xr; + if (searchable.find("android-tv") != std::string::npos || + searchable.find("google-tv") != std::string::npos || + searchable.find("_tv") != std::string::npos || + searchable.find(";tv") != std::string::npos) { + return ImageCategory::Tv; + } + if (img.Variant == "default" || + img.Variant.starts_with("google_apis") || + img.Variant.starts_with("aosp_atd") || + img.Variant.starts_with("google_atd")) { + return ImageCategory::PhoneTablet; + } + return ImageCategory::Other; + } + + static bool MatchesImageCategory(const RemoteSystemImage &img, const ImageCategory category) { + return category == ImageCategory::All || CategoryForImage(img) == category; + } + + static bool MatchesImageFilter(const RemoteSystemImage &img, const char *filter) { + if (!filter || filter[0] == '\0') return true; + + const auto searchable = StrConcat(img.DisplayName, " ", img.ApiLevel, " ", img.Variant, " ", img.Abi, " ", img.PackagePath); + return ContainsIgnoreCase(searchable, filter); + } + + static bool MatchesImageFilters(const RemoteSystemImage &img, const char *filter, const ImageCategory category) { + return MatchesImageCategory(img, category) && MatchesImageFilter(img, filter); + } + static void StartInstall(Context &context, const std::string &pkgPath) { auto &work = context.ImageInstallationWork; work.Progress = std::make_shared(); @@ -27,6 +74,69 @@ namespace CoreDeck { ); } + static bool SelectInstalledSystemImage(Context &context, const std::string &packagePath) { + auto &images = context.AvdCreationWork.SystemImages; + for (int i = 0; i < static_cast(images.size()); i++) { + if (images[i].PackagePath == packagePath) { + context.AvdCreationWork.SelectedSystemImage = i; + return true; + } + } + + images = ListSystemImages(context.Host.Sdk); + for (int i = 0; i < static_cast(images.size()); i++) { + if (images[i].PackagePath == packagePath) { + context.AvdCreationWork.SelectedSystemImage = i; + return true; + } + } + + return false; + } + + static void RefreshSystemImageLists(Context &context) { + auto &images = context.AvdCreationWork.SystemImages; + images = ListSystemImages(context.Host.Sdk); + if (images.empty()) { + context.AvdCreationWork.SelectedSystemImage = 0; + } else { + context.AvdCreationWork.SelectedSystemImage = std::clamp( + context.AvdCreationWork.SelectedSystemImage, + 0, + static_cast(images.size()) - 1 + ); + } + + context.ImageInstallationWork.RemoteImages = ListRemoteSystemImages(context.Host.Sdk, images); + } + + LabeledIconStyle SystemImageTypeStyleForVariant(const std::string &variant) { + if (variant.starts_with("google_apis_playstore")) return {Icons::Play, "Google Play", "#33CC47"}; + if (variant.starts_with("google_apis")) return {Icons::Gear, "Google APIs", "#4FC3F7"}; + if (variant.starts_with("aosp_atd") || variant.starts_with("google_atd")) { + return {Icons::Mobile, "ATD", "#F5A623"}; + } + return {Icons::Mobile, "Default", "#A7A7AD"}; + } + + LabeledIconStyle SystemImageTypeStyleFor(const SystemImage &img) { + return SystemImageTypeStyleForVariant(img.Variant); + } + + LabeledIconStyle SystemImageTypeStyleFor(const RemoteSystemImage &img) { + return SystemImageTypeStyleForVariant(img.Variant); + } + + std::string SystemImageDisplayName(const std::string &apiLevel, const std::string &fallback) { + if (!apiLevel.empty()) return StrConcat("Android ", apiLevel); + return fallback; + } + + std::string SystemImagePreviewLabel(const SystemImage &img) { + const auto style = SystemImageTypeStyleFor(img); + return StrConcat(SystemImageDisplayName(img.ApiLevel, img.DisplayName), " - ", style.Label, " - ", img.Abi); + } + void BuildInstallImageWindow(Context &context) { if (context.UI.ShowInstallImageDialog) { constexpr auto title = "Install System Image###InstallImageDialog"; @@ -36,20 +146,15 @@ namespace CoreDeck { const ImVec2 center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(560, 0), ImGuiCond_Appearing); - - constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoDocking; + ImGui::SetNextWindowSize(ImVec2(820, 560), ImGuiCond_Appearing); const bool installing = context.ImageInstallationWork.Installing.load(); - bool *pOpen = installing ? nullptr : &context.UI.ShowInstallImageDialog; + const bool removalBusy = context.AvdCreationWork.SystemImageRemoval.Busy.load(); + bool *pOpen = (installing || removalBusy) ? nullptr : &context.UI.ShowInstallImageDialog; - if (ImGui::BeginPopupModal(title, pOpen, flags)) { + if (ImGui::BeginPopupModal(title, pOpen, WindowAutoResizeFlags)) { auto &work = context.ImageInstallationWork; + auto &removal = context.AvdCreationWork.SystemImageRemoval; const bool isLoading = work.Prefetch.Loading.load(); const bool isInstalling = installing; @@ -126,75 +231,143 @@ namespace CoreDeck { return; } - if (!isInstalling && work.InstallFuture.valid()) { if (work.InstallFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { if (work.InstallFuture.get()) { - context.AvdCreationWork.SystemImages = ListSystemImages(context.Host.Sdk); - work.RemoteImages = ListRemoteSystemImages( - context.Host.Sdk, - context.AvdCreationWork.SystemImages - ); + RefreshSystemImageLists(context); } } } - ImGui::Text("Available System Images"); + if (removalBusy && removal.Future.valid()) { + if (removal.Future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + const bool removed = removal.Future.get(); + removal.Busy = false; + if (removed) RefreshSystemImageLists(context); + } + } + + if (isInstalling || removalBusy) ImGui::BeginDisabled(); + + ImGui::SetNextItemWidth(-1.0f); + const std::string searchHint = IconWithLabel(Icons::Search, "Search for a system image by name"); + ImGui::InputTextWithHint("##RemoteImageSearch", searchHint.c_str(), work.SearchFilter, sizeof(work.SearchFilter)); + ImGui::Spacing(); + ImGui::TextDisabled("Categories"); + + static constexpr ImageCategoryOption categoryOptions[] = { + {ImageCategory::All, "All"}, + {ImageCategory::PhoneTablet, "Phone / Tablet"}, + {ImageCategory::Wear, "Wear OS"}, + {ImageCategory::Tv, "TV"}, + {ImageCategory::Automotive, "Automotive"}, + {ImageCategory::Desktop, "Desktop"}, + {ImageCategory::Xr, "XR"}, + {ImageCategory::Other, "Other"}, + }; + + bool firstCategory = true; + for (const auto &[Category, Label]: categoryOptions) { + if (!firstCategory) ImGui::SameLine(); + firstCategory = false; + if (CategoryChip(Label, work.SelectedCategory == Category)) { + work.SelectedCategory = Category; + work.SelectedImage = -1; + } + } + ImGui::Spacing(); + ImGui::Text("Available System Images"); if (isLoading) { + ImGui::SameLine(); ImGui::TextDisabled("Fetching available images from SDK manager..."); - } else if (work.RemoteImages.empty()) { - ImGui::TextColored( - HexColor("#E64D40"), - "No remote system images found. Check your SDK and internet connection." - ); - } else { - bool hasAvailable = false; - for (const auto &img: work.RemoteImages) { - if (!img.IsInstalled) { - hasAvailable = true; - break; - } - } - - if (!hasAvailable) { - ImGui::TextColored(HexColor("#33CC47"), "All available system images are already installed."); - } else { - if (isInstalling) ImGui::BeginDisabled(); - ImGui::SetNextItemWidth(-1.0f); - auto preview = "Select an image..."; - if (work.SelectedImage >= 0 && - work.SelectedImage < static_cast(work.RemoteImages.size()) && - !work.RemoteImages[work.SelectedImage].IsInstalled) { - preview = work.RemoteImages[work.SelectedImage].DisplayName.c_str(); - } + } + ImGui::Spacing(); - if (ImGui::BeginCombo("##RemoteImagePicker", preview)) { + { + PickerTableStyle tableStyle; + + ImGui::BeginChild("##RemoteImageTableFrame", ImVec2(-1.0f, 280.0f), true, ImGuiWindowFlags_NoScrollbar); + if (ImGui::BeginTable("##RemoteImageTable", 5, PickerTableFlags, ImVec2(-1.0f, -1.0f))) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(" Name", ImGuiTableColumnFlags_WidthStretch, 2.7f); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthStretch, 1.1f); + ImGui::TableSetupColumn("API", ImGuiTableColumnFlags_WidthFixed, 56.0f); + ImGui::TableSetupColumn("ABI", ImGuiTableColumnFlags_WidthStretch, 1.2f); + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 92.0f); + ImGui::TableHeadersRow(); + + int visibleCount = 0; + if (!isLoading && work.RemoteImages.empty()) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored( + HexColor("#E64D40"), + "No remote system images found. Check your SDK and internet connection." + ); + } else { for (int i = 0; i < static_cast(work.RemoteImages.size()); i++) { const auto &img = work.RemoteImages[i]; - if (img.IsInstalled) continue; + if (!MatchesImageFilters(img, work.SearchFilter, work.SelectedCategory)) continue; + visibleCount++; const bool isSelected = work.SelectedImage == i; - if (ImGui::Selectable(img.DisplayName.c_str(), isSelected)) { + const auto [_, Label, Color] = SystemImageTypeStyleFor(img); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + const std::string label = StrConcat( + " ", + SystemImageDisplayName(img.ApiLevel, img.DisplayName), + "##RemoteImage", + std::to_string(i) + ); + if (ImGui::Selectable(label.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns, ImVec2(0.0f, 0.0f))) { work.SelectedImage = i; } if (isSelected) ImGui::SetItemDefaultFocus(); + + ImGui::TableNextColumn(); + ImGui::TextColored(HexColor(Color), "%s", Label); + + ImGui::TableNextColumn(); + ImGui::Text("%s", img.ApiLevel.c_str()); + + ImGui::TableNextColumn(); + ImGui::Text("%s", img.Abi.c_str()); + + ImGui::TableNextColumn(); + if (img.IsInstalled) { + ImGui::TextColored(HexColor("#33CC47"), "Installed"); + } else { + ImGui::TextDisabled("Available"); + } } - ImGui::EndCombo(); } - if (isInstalling) ImGui::EndDisabled(); + if (!isLoading && !work.RemoteImages.empty() && visibleCount == 0) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextDisabled(" No system images available!"); + } + + ImGui::EndTable(); } + ImGui::EndChild(); } + if (isInstalling || removalBusy) ImGui::EndDisabled(); + if (isInstalling && work.Progress) { ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); float fraction; - std::string statusText; { + std::string statusText; + { std::lock_guard lock(work.Progress->Mutex); fraction = work.Progress->Percent; statusText = work.Progress->StatusText; @@ -211,7 +384,8 @@ namespace CoreDeck { if (!isInstalling && work.Progress) { bool finished; bool succeeded; - std::string statusText; { + std::string statusText; + { std::lock_guard lock(work.Progress->Mutex); finished = work.Progress->Finished; succeeded = work.Progress->Succeeded; @@ -222,7 +396,8 @@ namespace CoreDeck { ImGui::Spacing(); const float textWidth = ImGui::CalcTextSize(statusText.c_str()).x; ImGui::SetCursorPosX( - (ImGui::GetContentRegionAvail().x - textWidth) * 0.5f + ImGui::GetCursorStartPos().x); + (ImGui::GetContentRegionAvail().x - textWidth) * 0.5f + ImGui::GetCursorStartPos().x + ); if (succeeded) ImGui::TextColored(HexColor("#33CC47"), "%s", statusText.c_str()); else ImGui::TextColored(HexColor("#E64D40"), "%s", statusText.c_str()); } @@ -232,14 +407,19 @@ namespace CoreDeck { ImGui::Separator(); ImGui::Spacing(); - const bool canInstall = - !isLoading && !isInstalling && - work.SelectedImage >= 0 && - work.SelectedImage < static_cast(work.RemoteImages.size()) && - !work.RemoteImages[work.SelectedImage].IsInstalled; + const bool hasVisibleSelection = work.SelectedImage >= 0 && + work.SelectedImage < static_cast(work.RemoteImages.size()) && + MatchesImageFilters(work.RemoteImages[work.SelectedImage], work.SearchFilter, work.SelectedCategory); + + const bool selectedInstalled = hasVisibleSelection && work.RemoteImages[work.SelectedImage].IsInstalled; + const bool canUseSelected = !isLoading && !isInstalling && !removalBusy && selectedInstalled; + const bool canRemove = canUseSelected; + const bool canInstall = !isLoading && !isInstalling && !removalBusy && hasVisibleSelection && !selectedInstalled; const float spacing = ImGui::GetStyle().ItemSpacing.x; - const float halfWidth = (ImGui::GetContentRegionAvail().x - spacing) * 0.5f; + const float actionWidth = ImGui::GetContentRegionAvail().x; + const float halfWidth = (actionWidth - spacing) * 0.5f; + const float thirdWidth = (actionWidth - spacing * 2.0f) / 3.0f; const bool licenseBusy = work.LicenseBusy.load(); @@ -248,14 +428,59 @@ namespace CoreDeck { ImGui::Spacing(); } - if (isInstalling) { + if (selectedInstalled || removalBusy) { + if (removalBusy) { + ImGui::BeginDisabled(); + PositiveButton("Use Selected Image", false, ImVec2(thirdWidth, 0)); + ImGui::EndDisabled(); + } else if (PositiveButton("Use Selected Image", canUseSelected, ImVec2(thirdWidth, 0))) { + const auto &img = work.RemoteImages[work.SelectedImage]; + if (SelectInstalledSystemImage(context, img.PackagePath)) { + work.Progress.reset(); + context.UI.ShowInstallImageDialog = false; + } + } + + ImGui::SameLine(); + if (removalBusy) { + ImGui::BeginDisabled(); + NegativeButton("Removing...", false, ImVec2(thirdWidth, 0)); + ImGui::EndDisabled(); + } else if (NegativeButton("Remove Image", canRemove, ImVec2(thirdWidth, 0))) { + const std::string pkg = work.RemoteImages[work.SelectedImage].PackagePath; + removal.Busy = true; + removal.Future = std::async(std::launch::async, [&context, pkg] { + try { + return UninstallSystemImage(context.Host.Sdk, pkg); + } catch (...) { + return false; + } + }); + } + + ImGui::SameLine(); + if (PrimaryButton("Close", !isInstalling && !removalBusy, ImVec2(thirdWidth, 0))) { + work.Progress.reset(); + context.UI.ShowInstallImageDialog = false; + } + } else if (isInstalling) { ImGui::BeginDisabled(); PositiveButton("Installing...", false, ImVec2(halfWidth, 0)); ImGui::EndDisabled(); + ImGui::SameLine(); + if (PrimaryButton("Close", false, ImVec2(halfWidth, 0))) { + work.Progress.reset(); + context.UI.ShowInstallImageDialog = false; + } } else if (licenseBusy) { ImGui::BeginDisabled(); PositiveButton("Checking licenses...", false, ImVec2(halfWidth, 0)); ImGui::EndDisabled(); + ImGui::SameLine(); + if (PrimaryButton("Close", false, ImVec2(halfWidth, 0))) { + work.Progress.reset(); + context.UI.ShowInstallImageDialog = false; + } } else { if (PositiveButton("Install", canInstall, ImVec2(halfWidth, 0))) { const auto &img = work.RemoteImages[work.SelectedImage]; @@ -266,25 +491,16 @@ namespace CoreDeck { return CheckSdkLicenses(context.Host.Sdk); }); } - } - ImGui::SameLine(); - if (PrimaryButton("Close", !isInstalling, ImVec2(halfWidth, 0))) { - work.Progress.reset(); - context.UI.ShowInstallImageDialog = false; - if (context.UI.ReopenCreateAvdOnInstallClose) { - context.UI.ReopenCreateAvdOnInstallClose = false; - context.UI.ShowCreateAvdDialog = true; + ImGui::SameLine(); + if (PrimaryButton("Close", !isInstalling && !removalBusy, ImVec2(halfWidth, 0))) { + work.Progress.reset(); + context.UI.ShowInstallImageDialog = false; } } ImGui::EndPopup(); } } - - if (!context.UI.ShowInstallImageDialog && context.UI.ReopenCreateAvdOnInstallClose) { - context.UI.ReopenCreateAvdOnInstallClose = false; - context.UI.ShowCreateAvdDialog = true; - } } } diff --git a/src/gui/windows/install_image.h b/src/gui/windows/install_image.h index 75ecae3..656b29b 100644 --- a/src/gui/windows/install_image.h +++ b/src/gui/windows/install_image.h @@ -6,9 +6,20 @@ #define COREDECK_INSTALL_IMAGE_H #include "../context.h" +#include "gui/widgets.h" namespace CoreDeck { + LabeledIconStyle SystemImageTypeStyleForVariant(const std::string &variant); + + LabeledIconStyle SystemImageTypeStyleFor(const SystemImage &img); + + LabeledIconStyle SystemImageTypeStyleFor(const RemoteSystemImage &img); + + std::string SystemImageDisplayName(const std::string &apiLevel, const std::string &fallback); + + std::string SystemImagePreviewLabel(const SystemImage &img); + void BuildInstallImageWindow(Context &context); } -#endif //COREDECK_INSTALL_IMAGE_H +#endif // COREDECK_INSTALL_IMAGE_H diff --git a/src/gui/windows/main_menu_bar.h b/src/gui/windows/main_menu_bar.h index 061ba32..6e82806 100644 --- a/src/gui/windows/main_menu_bar.h +++ b/src/gui/windows/main_menu_bar.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildMainMenuBar(Context &context); } -#endif //COREDECK_MAIN_MENU_BAR_H +#endif // COREDECK_MAIN_MENU_BAR_H diff --git a/src/gui/windows/onboarding.cpp b/src/gui/windows/onboarding.cpp index ce67c9d..1f19b5e 100644 --- a/src/gui/windows/onboarding.cpp +++ b/src/gui/windows/onboarding.cpp @@ -99,17 +99,20 @@ namespace CoreDeck { if (isValid) { ImGui::TextColored( HexColor("#33CC47"), - "%s", "Looks good. Found the Android emulator at this location." + "%s", + "Looks good. Found the Android emulator at this location." ); } else { ImGui::TextColored( - HexColor("#E64D40"), "%s", + HexColor("#E64D40"), + "%s", "Couldn't find the Android emulator here. Make sure this is your SDK root folder." ); } } else { ImGui::TextColored( - HexColor("#66666B"), "%s", + HexColor("#66666B"), + "%s", "Choose the folder containing your Android SDK (cmdline-tools, emulator, platform-tools, etc)." ); } @@ -157,14 +160,14 @@ namespace CoreDeck { ImGui::SetNextWindowSize(viewport->WorkSize); constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoDocking | - ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoNavFocus; + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNavFocus; ImGui::Begin("##Onboarding", nullptr, flags); diff --git a/src/gui/windows/onboarding.h b/src/gui/windows/onboarding.h index 87bac8b..cee7884 100644 --- a/src/gui/windows/onboarding.h +++ b/src/gui/windows/onboarding.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildOnboardingWindow(Context &context); } -#endif //COREDECK_ONBOARDING_WINDOW_H +#endif // COREDECK_ONBOARDING_WINDOW_H diff --git a/src/gui/windows/preferences.cpp b/src/gui/windows/preferences.cpp index cd45059..41bfb2d 100644 --- a/src/gui/windows/preferences.cpp +++ b/src/gui/windows/preferences.cpp @@ -22,15 +22,9 @@ namespace CoreDeck { ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(ImVec2(520, 0), ImGuiCond_Appearing); - constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoDocking; - static char sdkPathBuffer[2048]; - if (ImGui::BeginPopupModal("Preferences###CoreDeckPrefs", &context.UI.ShowPreferences, flags)) { + if (ImGui::BeginPopupModal("Preferences###CoreDeckPrefs", &context.UI.ShowPreferences, WindowNoResizeFlags)) { if (ImGui::IsWindowAppearing()) { const std::string &p = context.Host.Sdk.SdkPath; strncpy(sdkPathBuffer, p.c_str(), sizeof(sdkPathBuffer) - 1); @@ -115,7 +109,8 @@ namespace CoreDeck { ImGui::Spacing(); ImGui::Separator(); - ImGui::Spacing(); { + ImGui::Spacing(); + { constexpr float closeW = 120.0f; ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - closeW + ImGui::GetCursorPosX()); if (PrimaryButton("Close", true, ImVec2(closeW, 0))) { diff --git a/src/gui/windows/preferences.h b/src/gui/windows/preferences.h index e1729f3..23b31a6 100644 --- a/src/gui/windows/preferences.h +++ b/src/gui/windows/preferences.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildPreferencesWindow(Context &context); } -#endif //COREDECK_PREFERENCES_H +#endif // COREDECK_PREFERENCES_H diff --git a/src/gui/windows/sdk_banner.cpp b/src/gui/windows/sdk_banner.cpp index 7272d65..27e54a4 100644 --- a/src/gui/windows/sdk_banner.cpp +++ b/src/gui/windows/sdk_banner.cpp @@ -19,14 +19,10 @@ namespace CoreDeck { ImGui::SetNextWindowPos(vp->WorkPos); ImGui::SetNextWindowSize(ImVec2(vp->WorkSize.x, 0.0f)); - constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoDocking | - ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoNavFocus | - ImGuiWindowFlags_AlwaysAutoResize; + constexpr ImGuiWindowFlags flags = WindowAutoResizeFlags | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoNavFocus; ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.32f, 0.18f, 0.10f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 8.0f)); diff --git a/src/gui/windows/sdk_banner.h b/src/gui/windows/sdk_banner.h index a7f22ae..d75fffb 100644 --- a/src/gui/windows/sdk_banner.h +++ b/src/gui/windows/sdk_banner.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildSdkMissingBanner(Context &context); } -#endif //COREDECK_SDK_BANNER_H +#endif // COREDECK_SDK_BANNER_H diff --git a/src/gui/windows/storage.cpp b/src/gui/windows/storage.cpp index 3917114..f1d31f1 100644 --- a/src/gui/windows/storage.cpp +++ b/src/gui/windows/storage.cpp @@ -2,8 +2,9 @@ // Created by AbdulMuaz Aqeel on 19/04/2026. // -#include +#include #include +#include #include "imgui.h" @@ -12,9 +13,78 @@ #include "../widgets.h" #include "../theme.h" #include "../../core/utilities.h" -#include "../../core/paths.h" namespace CoreDeck { + static StorageScanResult ScanStorageUsage(const std::vector &avds, const std::string &sdkPath) { + StorageScanResult result; + + for (const auto &avd: avds) { + if (avd.Path.empty()) continue; + if (std::filesystem::exists(avd.Path)) { + result.TotalAvdSize += GetDirectorySize(avd.Path); + } + } + + if (!sdkPath.empty()) { + const auto sysImgRoot = std::filesystem::path(sdkPath) / "system-images"; + if (std::filesystem::exists(sysImgRoot)) { + result.SystemImagesSize = GetDirectorySize(sysImgRoot.string()); + } + } + + return result; + } + + static void StartStorageScan(Context &context) { + auto &disk = context.DiskUsage; + disk.Loading = true; + disk.Ready = false; + + const auto avds = context.Catalog.Avds; + const std::string sdkPath = context.Host.Sdk.SdkPath; + disk.Future = std::async(std::launch::async, [avds, sdkPath] { + return ScanStorageUsage(avds, sdkPath); + }); + } + + static void DrawStorageSummaryCard(const char *title, const std::string &value, const char *accentColor, const float width) { + StyleColor sc; + StyleVar sv; + sc.push(ImGuiCol_ChildBg, HexColor("#141417")); + sc.push(ImGuiCol_Border, HexColor("#2E2E33")); + sv.push(ImGuiStyleVar_ChildRounding, 8.0f); + sv.push(ImGuiStyleVar_ChildBorderSize, 1.0f); + sv.push(ImGuiStyleVar_WindowPadding, ImVec2(14.0f, 12.0f)); + + ImGui::BeginChild(title, ImVec2(width, 74.0f), true, ImGuiWindowFlags_NoScrollbar); + ImGui::TextDisabled("%s", title); + ImGui::Spacing(); + ImGui::TextColored(HexColor(accentColor), "%s", value.c_str()); + ImGui::EndChild(); + } + + static void DrawStorageBreakdownBar(const std::uintmax_t avdSize, const std::uintmax_t systemImageSize) { + const std::uintmax_t total = avdSize + systemImageSize; + const float width = ImGui::GetContentRegionAvail().x; + constexpr float height = 14.0f; + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const ImVec2 end(pos.x + width, pos.y + height); + auto *drawList = ImGui::GetWindowDrawList(); + + drawList->AddRectFilled(pos, end, ImGui::ColorConvertFloat4ToU32(HexColor("#1A1A1C")), 999.0f); + if (total > 0) { + const float avdWidth = width * (static_cast(avdSize) / static_cast(total)); + if (avdSize > 0) { + drawList->AddRectFilled(pos, ImVec2(pos.x + avdWidth, end.y), ImGui::ColorConvertFloat4ToU32(HexColor("#4D9AFF")), 999.0f); + } + if (systemImageSize > 0) { + drawList->AddRectFilled(ImVec2(pos.x + avdWidth, pos.y), end, ImGui::ColorConvertFloat4ToU32(HexColor("#33CC47")), 999.0f); + } + } + + ImGui::Dummy(ImVec2(width, height)); + } + void BuildStorageWindow(Context &context) { if (context.UI.ShowStorageDialog && !ImGui::IsPopupOpen("Storage Overview###StorageDialog")) { ImGui::OpenPopup("Storage Overview###StorageDialog"); @@ -22,164 +92,66 @@ namespace CoreDeck { const ImVec2 center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(600, 350), ImGuiCond_Appearing); - - constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoDocking; - - if (ImGui::BeginPopupModal("Storage Overview###StorageDialog", &context.UI.ShowStorageDialog, flags)) { - auto &cache = context.DiskUsage.PerAvdCache; - - // --- Summary --- - std::uintmax_t totalAvdSize = 0; - for (const auto &avd : context.Catalog.Avds) { - if (avd.Path.empty() || !std::filesystem::exists(avd.Path)) continue; - auto it = cache.find(avd.Name); - if (it == cache.end()) { - const std::uintmax_t size = GetDirectorySize(avd.Path); - cache[avd.Name] = size; - it = cache.find(avd.Name); - } - totalAvdSize += it->second; - } + ImGui::SetNextWindowSize(ImVec2(650, 260), ImGuiCond_Appearing); - // System images directory size (cached) - if (!context.DiskUsage.SystemImagesSizeCached && !context.Host.Sdk.SdkPath.empty()) { - const auto sysImgDir = std::filesystem::path(context.Host.Sdk.SdkPath) / "system-images"; - if (std::filesystem::exists(sysImgDir)) { - context.DiskUsage.SystemImagesSize = GetDirectorySize(sysImgDir.string()); - } - context.DiskUsage.SystemImagesSizeCached = true; - } - const std::uintmax_t sysImgTotal = context.DiskUsage.SystemImagesSize; + if (ImGui::BeginPopupModal("Storage Overview###StorageDialog", &context.UI.ShowStorageDialog, WindowAutoResizeFlags)) { + auto &disk = context.DiskUsage; - const std::string avdSizeStr = FormatFileSize(totalAvdSize); - const std::string imgSizeStr = FormatFileSize(sysImgTotal); - const std::uintmax_t grandTotal = totalAvdSize + sysImgTotal; - const std::string totalStr = FormatFileSize(grandTotal); + if (!disk.Ready && !disk.Loading.load() && !disk.Future.valid()) { + StartStorageScan(context); + } - const float contentMax = ImGui::GetContentRegionAvail().x + ImGui::GetCursorPosX(); + if (disk.Loading.load() && disk.Future.valid() && + disk.Future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + disk.LastScan = disk.Future.get(); + disk.Ready = true; + disk.Loading = false; + } - ImGui::Text("Total AVD Size"); - ImGui::SameLine(contentMax - ImGui::CalcTextSize(avdSizeStr.c_str()).x); - ImGui::Text("%s", avdSizeStr.c_str()); + const bool isLoading = disk.Loading.load(); + const auto &[TotalAvdSize, SystemImagesSize] = disk.LastScan; + const std::uintmax_t grandTotal = TotalAvdSize + SystemImagesSize; - ImGui::Text("System Images"); - ImGui::SameLine(contentMax - ImGui::CalcTextSize(imgSizeStr.c_str()).x); - ImGui::Text("%s", imgSizeStr.c_str()); + ImGui::Text("Statistics"); + ImGui::TextDisabled("%s", isLoading ? "Calculating..." : "Calculated from local SDK and AVD folders"); - ImGui::Spacing(); - ImGui::Separator(); ImGui::Spacing(); - ImGui::TextDisabled("Total SDK Storage"); - ImGui::SameLine(contentMax - ImGui::CalcTextSize(totalStr.c_str()).x); - ImGui::Text("%s", totalStr.c_str()); + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float cardWidth = (ImGui::GetContentRegionAvail().x - spacing * 2.0f) / 3.0f; + DrawStorageSummaryCard("Total Storage", isLoading && !disk.Ready ? "Calculating..." : FormatFileSize(grandTotal), "#F2F2F2", cardWidth); + ImGui::SameLine(); + DrawStorageSummaryCard("AVDs", isLoading && !disk.Ready ? "Calculating..." : FormatFileSize(TotalAvdSize), "#4D9AFF", cardWidth); + ImGui::SameLine(); + DrawStorageSummaryCard("System Images", isLoading && !disk.Ready ? "Calculating..." : FormatFileSize(SystemImagesSize), "#33CC47", cardWidth); ImGui::Spacing(); - ImGui::Separator(); + ImGui::TextDisabled("Breakdown"); + DrawStorageBreakdownBar(TotalAvdSize, SystemImagesSize); ImGui::Spacing(); - - // --- Per-AVD breakdown --- - if (CollapsingHeader("AVDs")) { - if (context.Catalog.Avds.empty()) { - ImGui::TextDisabled("No AVDs found."); - } else { - // Sort AVDs by size descending for the overview - struct AvdSizeEntry { - std::string Name; - std::string DisplayName; - std::uintmax_t Size; - }; - std::vector entries; - entries.reserve(context.Catalog.Avds.size()); - for (const auto &avd : context.Catalog.Avds) { - std::uintmax_t size = 0; - if (auto it = cache.find(avd.Name); it != cache.end()) { - size = it->second; - } - entries.push_back({avd.Name, avd.DisplayName, size}); - } - std::ranges::sort(entries, [](const auto &a, const auto &b) { - return a.Size > b.Size; - }); - - for (const auto &[name, displayName, size] : entries) { - const std::string sizeStr = FormatFileSize(size); - const std::string label = displayName.empty() ? name : displayName; - ImGui::Text("%s", label.c_str()); - ImGui::SameLine(contentMax - ImGui::CalcTextSize(sizeStr.c_str()).x); - ImGui::TextDisabled("%s", sizeStr.c_str()); - } - } - } - - ImGui::Spacing(); - - // --- Per-system-image breakdown --- - if (CollapsingHeader("Installed System Images")) { - if (!context.Host.Sdk.SdkPath.empty()) { - // Build the cache on first access - if (!context.DiskUsage.SystemImageEntriesCached) { - context.DiskUsage.SystemImageEntries.clear(); - const auto sysImgRoot = std::filesystem::path(context.Host.Sdk.SdkPath) / "system-images"; - if (std::filesystem::exists(sysImgRoot)) { - std::error_code ec; - // system-images/// - for (const auto &apiDir : std::filesystem::directory_iterator(sysImgRoot, ec)) { - if (!apiDir.is_directory()) continue; - for (const auto &variantDir : std::filesystem::directory_iterator(apiDir.path(), ec)) { - if (!variantDir.is_directory()) continue; - for (const auto &abiDir : std::filesystem::directory_iterator(variantDir.path(), ec)) { - if (!abiDir.is_directory()) continue; - const std::string label = - apiDir.path().filename().string() + " / " + - variantDir.path().filename().string() + " / " + - abiDir.path().filename().string(); - const std::uintmax_t size = GetDirectorySize(abiDir.path().string()); - context.DiskUsage.SystemImageEntries.push_back({label, size}); - } - } - } - std::ranges::sort(context.DiskUsage.SystemImageEntries, [](const auto &a, const auto &b) { - return a.Size > b.Size; - }); - } - context.DiskUsage.SystemImageEntriesCached = true; - } - - if (context.DiskUsage.SystemImageEntries.empty()) { - ImGui::TextDisabled("No system images found."); - } else { - for (const auto &[name, size] : context.DiskUsage.SystemImageEntries) { - const std::string sizeStr = FormatFileSize(size); - ImGui::Text("%s", name.c_str()); - ImGui::SameLine(contentMax - ImGui::CalcTextSize(sizeStr.c_str()).x); - ImGui::TextDisabled("%s", sizeStr.c_str()); - } - } - } else { - ImGui::TextDisabled("SDK path not configured."); - } - } + ImGui::TextColored(HexColor("#4D9AFF"), "AVDs"); + ImGui::SameLine(); + ImGui::TextDisabled("%s", FormatFileSize(TotalAvdSize).c_str()); + ImGui::SameLine(); + ImGui::TextColored(HexColor("#33CC47"), "System Images"); + ImGui::SameLine(); + ImGui::TextDisabled("%s", FormatFileSize(SystemImagesSize).c_str()); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - // --- Close button --- - constexpr float closeW = 100.0f; - ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - closeW + ImGui::GetCursorPosX()); - if (PrimaryButton("Close", true, ImVec2(closeW, 0))) { + const float buttonSpacing = ImGui::GetStyle().ItemSpacing.x; + const float halfWidth = (ImGui::GetContentRegionAvail().x - buttonSpacing) * 0.5f; + if (PositiveButton(isLoading ? "Refreshing..." : "Refresh", !isLoading, ImVec2(halfWidth, 0))) { + StartStorageScan(context); + } + ImGui::SameLine(); + if (PrimaryButton("Close", !isLoading, ImVec2(halfWidth, 0))) { context.UI.ShowStorageDialog = false; ImGui::CloseCurrentPopup(); } - ImGui::Spacing(); ImGui::EndPopup(); } } diff --git a/src/gui/windows/storage.h b/src/gui/windows/storage.h index 2558de2..1322d10 100644 --- a/src/gui/windows/storage.h +++ b/src/gui/windows/storage.h @@ -11,4 +11,4 @@ namespace CoreDeck { void BuildStorageWindow(Context &context); } -#endif //COREDECK_STORAGE_H +#endif // COREDECK_STORAGE_H diff --git a/src/gui/windows/update.cpp b/src/gui/windows/update.cpp index 0d2b077..073ed22 100644 --- a/src/gui/windows/update.cpp +++ b/src/gui/windows/update.cpp @@ -23,13 +23,7 @@ namespace CoreDeck { ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(ImVec2(250, 0), ImGuiCond_Appearing); - constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoDocking; - - if (ImGui::BeginPopupModal("Up to date###CoreDeckUpdateOk", &context.Updates.ShowUpToDateModal, flags)) { + if (ImGui::BeginPopupModal("Up to date###CoreDeckUpdateOk", &context.Updates.ShowUpToDateModal, WindowNoResizeFlags)) { ImGui::TextWrapped("You're running the latest CoreDeck release."); ImGui::Spacing(); ImGui::Text("Current: "); @@ -61,13 +55,7 @@ namespace CoreDeck { ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(ImVec2(440, 0), ImGuiCond_Appearing); - constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoDocking; - - if (ImGui::BeginPopupModal("Update Available###CoreDeckUpdate", &context.Updates.ShowNewVersionModal, flags)) { + if (ImGui::BeginPopupModal("Update Available###CoreDeckUpdate", &context.Updates.ShowNewVersionModal, WindowNoResizeFlags)) { ImGui::TextUnformatted("A newer CoreDeck release is available."); ImGui::Spacing(); diff --git a/src/pch.h b/src/pch.h index cc564f6..9e1fd3d 100644 --- a/src/pch.h +++ b/src/pch.h @@ -17,4 +17,4 @@ #include #include -#endif //COREDECK_PCH_H +#endif // COREDECK_PCH_H