From e95ad50e4168f69ada4a50d6092e1c410cd34947 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 6 May 2026 03:42:05 -0500 Subject: [PATCH] feat(lite): add ObsidianDragonLite build mode and gate full-node features Add --lite build flow and ObsidianDragonLite target naming, hide full-node pages/features in lite mode, enforce pool-only mining in lite, and include chat port feasibility audit documentation. --- CMakeLists.txt | 16 +++- build.sh | 100 +++++++++++++---------- docs/chat-port-feasibility-2026-05-06.md | 63 ++++++++++++++ src/app.h | 4 +- src/app_network.cpp | 28 +++++++ src/config/version.h | 2 +- src/config/version.h.in | 2 +- src/ui/pages/settings_page.cpp | 38 +++++---- src/ui/sidebar.h | 28 +++++-- src/ui/windows/mining_tab.cpp | 66 +++++++++------ 10 files changed, 255 insertions(+), 92 deletions(-) create mode 100644 docs/chat-port-feasibility-2026-05-06.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cb43c7..2f64850 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,16 @@ endif() # Options option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON) option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON) +option(DRAGONX_BUILD_LITE "Build ObsidianDragonLite variant without full-node features" OFF) + +if(DRAGONX_BUILD_LITE) + set(DRAGONX_APP_NAME "ObsidianDragonLite") + set(DRAGONX_BINARY_NAME "ObsidianDragonLite") + set(DRAGONX_ENABLE_EMBEDDED_DAEMON OFF CACHE BOOL "Enable embedded dragonxd support" FORCE) +else() + set(DRAGONX_APP_NAME "ObsidianDragon") + set(DRAGONX_BINARY_NAME "ObsidianDragon") +endif() include(CTest) @@ -518,6 +528,8 @@ add_executable(ObsidianDragon ${WIN_RC_FILE} ) +set_target_properties(ObsidianDragon PROPERTIES OUTPUT_NAME "${DRAGONX_BINARY_NAME}") + target_include_directories(ObsidianDragon PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src/embedded @@ -570,6 +582,8 @@ endif() # Compile definitions target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_DEBUG + DRAGONX_LITE_BUILD=$ + DRAGONX_ENABLE_EMBEDDED_DAEMON=$ ) if(WIN32) target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11) @@ -727,7 +741,7 @@ install(TARGETS ObsidianDragon ) install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res - DESTINATION share/ObsidianDragon + DESTINATION share/${DRAGONX_BINARY_NAME} OPTIONAL ) diff --git a/build.sh b/build.sh index 6c3891c..c695613 100755 --- a/build.sh +++ b/build.sh @@ -41,6 +41,7 @@ DO_DEV=false DO_LINUX=false DO_WIN=false DO_MAC=false +DO_LITE=false CLEAN=false BUILD_TYPE="Release" @@ -54,6 +55,7 @@ Targets (at least one required, or none for dev build): --linux-release Linux release (zip + AppImage) -> release/linux/ --win-release Windows cross-compile (mingw-w64) -> release/windows/ --mac-release macOS .app bundle + DMG -> release/mac/ + --lite Build ObsidianDragonLite variant (no embedded daemon/full-node features) Build trees are stored under build/{linux,windows,mac}/ @@ -85,6 +87,7 @@ while [[ $# -gt 0 ]]; do --linux-release) DO_LINUX=true; shift ;; --win-release) DO_WIN=true; shift ;; --mac-release) DO_MAC=true; shift ;; + --lite) DO_LITE=true; shift ;; -c|--clean) CLEAN=true; shift ;; -d|--debug) BUILD_TYPE="Debug"; shift ;; -j) JOBS="$2"; shift 2 ;; @@ -98,6 +101,14 @@ if ! $DO_LINUX && ! $DO_WIN && ! $DO_MAC; then DO_DEV=true fi +APP_BASENAME="ObsidianDragon" +CMAKE_LITE_ARGS=() +if $DO_LITE; then + APP_BASENAME="ObsidianDragonLite" + CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=ON") + info "Lite mode enabled: building ${APP_BASENAME}" +fi + # ── Helper: find resource files ────────────────────────────────────────────── find_sapling_params() { local dirs=( @@ -215,13 +226,14 @@ build_dev() { cmake "$SCRIPT_DIR" \ -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ -DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \ - -DDRAGONX_USE_SYSTEM_SDL3=ON + -DDRAGONX_USE_SYSTEM_SDL3=ON \ + "${CMAKE_LITE_ARGS[@]}" info "Building with $JOBS jobs ..." cmake --build . -j "$JOBS" - [[ -f "bin/ObsidianDragon" ]] || { err "Build failed"; exit 1; } - info "Dev binary: $bd/bin/ObsidianDragon ($(du -h bin/ObsidianDragon | cut -f1))" + [[ -f "bin/${APP_BASENAME}" ]] || { err "Build failed"; exit 1; } + info "Dev binary: $bd/bin/${APP_BASENAME} ($(du -h "bin/${APP_BASENAME}" | cut -f1))" } # ═══════════════════════════════════════════════════════════════════════════════ @@ -242,16 +254,17 @@ build_release_linux() { cmake "$SCRIPT_DIR" \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \ - -DDRAGONX_USE_SYSTEM_SDL3=ON + -DDRAGONX_USE_SYSTEM_SDL3=ON \ + "${CMAKE_LITE_ARGS[@]}" info "Building with $JOBS jobs ..." cmake --build . -j "$JOBS" - [[ -f "bin/ObsidianDragon" ]] || { err "Linux build failed"; exit 1; } + [[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; } info "Stripping ..." - strip bin/ObsidianDragon - info "Binary: $(du -h bin/ObsidianDragon | cut -f1)" + strip "bin/${APP_BASENAME}" + info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)" # ── Bundle daemon ──────────────────────────────────────────────────────── bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build" @@ -268,11 +281,11 @@ build_release_linux() { rm -rf "$out" mkdir -p "$out" - local DIST="ObsidianDragon-${VERSION}-Linux-x64" + local DIST="${APP_BASENAME}-${VERSION}-Linux-x64" local dist_dir="$out/$DIST" mkdir -p "$dist_dir" - cp bin/ObsidianDragon "$dist_dir/" + cp "bin/${APP_BASENAME}" "$dist_dir/" [[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/" [[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/" [[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/" @@ -299,7 +312,7 @@ build_release_linux() { "$APPDIR/usr/share/icons/hicolor/256x256/apps" \ "$APPDIR/usr/share/ObsidianDragon/res" - cp bin/ObsidianDragon "$APPDIR/usr/bin/" + cp "bin/${APP_BASENAME}" "$APPDIR/usr/bin/" cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true [[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/" @@ -314,12 +327,12 @@ build_release_linux() { [[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; } # Desktop entry - cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK' + cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" </dev/null || true # AppRun - cat > "$APPDIR/AppRun" <<'APPRUN' + cat > "$APPDIR/AppRun" </dev/null && { - cp "ObsidianDragon-${VERSION}-${ARCH}.AppImage" "$out/ObsidianDragon-${VERSION}.AppImage" - info "AppImage: $out/ObsidianDragon-${VERSION}.AppImage ($(du -h "$out/ObsidianDragon-${VERSION}.AppImage" | cut -f1))" + ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" 2>/dev/null && { + cp "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" "$out/${APP_BASENAME}-${VERSION}.AppImage" + info "AppImage: $out/${APP_BASENAME}-${VERSION}.AppImage ($(du -h "$out/${APP_BASENAME}-${VERSION}.AppImage" | cut -f1))" } || warn "AppImage creation failed — binaries zip still in release/linux/" info "Linux release artifacts: $out/" @@ -599,22 +612,23 @@ HDR cmake "$SCRIPT_DIR" \ -DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \ -DCMAKE_BUILD_TYPE=Release \ - -DDRAGONX_USE_SYSTEM_SDL3=OFF + -DDRAGONX_USE_SYSTEM_SDL3=OFF \ + "${CMAKE_LITE_ARGS[@]}" info "Building with $JOBS jobs ..." cmake --build . -j "$JOBS" - [[ -f "bin/ObsidianDragon.exe" ]] || { err "Windows build failed"; exit 1; } - info "Binary: $(du -h bin/ObsidianDragon.exe | cut -f1)" + [[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; } + info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)" # ── Package: release/windows/ ──────────────────────────────────────────── rm -rf "$out" mkdir -p "$out" - local DIST="ObsidianDragon-${VERSION}-Windows-x64" + local DIST="${APP_BASENAME}-${VERSION}-Windows-x64" local dist_dir="$out/$DIST" mkdir -p "$dist_dir" - cp bin/ObsidianDragon.exe "$dist_dir/" + cp "bin/${APP_BASENAME}.exe" "$dist_dir/" local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win" for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do @@ -635,8 +649,8 @@ HDR cp -r bin/res "$dist_dir/" 2>/dev/null || true # ── Single-file exe (all resources embedded) ──────────────────────────── - cp bin/ObsidianDragon.exe "$out/ObsidianDragon-${VERSION}.exe" - info "Single-file exe: $out/ObsidianDragon-${VERSION}.exe ($(du -h "$out/ObsidianDragon-${VERSION}.exe" | cut -f1))" + cp "bin/${APP_BASENAME}.exe" "$out/${APP_BASENAME}-${VERSION}.exe" + info "Single-file exe: $out/${APP_BASENAME}-${VERSION}.exe ($(du -h "$out/${APP_BASENAME}-${VERSION}.exe" | cut -f1))" # ── Zip ────────────────────────────────────────────────────────────────── if command -v zip &>/dev/null; then @@ -818,7 +832,8 @@ TOOLCHAIN -DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \ -DDRAGONX_USE_SYSTEM_SDL3=OFF \ -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \ - ${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} + ${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \ + "${CMAKE_LITE_ARGS[@]}" else # Build libsodium as universal if needed local need_sodium=false @@ -844,39 +859,40 @@ TOOLCHAIN -DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \ -DDRAGONX_USE_SYSTEM_SDL3=OFF \ -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \ - -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" + -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \ + "${CMAKE_LITE_ARGS[@]}" fi info "Building with $JOBS jobs ..." cmake --build . -j "$JOBS" - [[ -f "bin/ObsidianDragon" ]] || { err "macOS build failed"; exit 1; } + [[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; } # Strip — use osxcross strip for cross-builds if $IS_CROSS; then local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip" if [[ -x "$STRIP_CMD" ]]; then info "Stripping (osxcross) ..." - "$STRIP_CMD" bin/ObsidianDragon + "$STRIP_CMD" "bin/${APP_BASENAME}" else warn "osxcross strip not found at $STRIP_CMD — skipping" fi else info "Stripping ..." - strip bin/ObsidianDragon + strip "bin/${APP_BASENAME}" # Verify universal binary if command -v lipo &>/dev/null; then info "Architecture info:" - lipo -info bin/ObsidianDragon + lipo -info "bin/${APP_BASENAME}" fi fi - info "Binary: $(du -h bin/ObsidianDragon | cut -f1)" + info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)" # ── Create .app bundle ─────────────────────────────────────────────────── rm -rf "$out" mkdir -p "$out" - local APP="$out/ObsidianDragon.app" + local APP="$out/${APP_BASENAME}.app" local CONTENTS="$APP/Contents" local MACOS="$CONTENTS/MacOS" local RESOURCES="$CONTENTS/Resources" @@ -885,8 +901,8 @@ TOOLCHAIN mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS" # Main binary - cp bin/ObsidianDragon "$MACOS/" - chmod +x "$MACOS/ObsidianDragon" + cp "bin/${APP_BASENAME}" "$MACOS/" + chmod +x "$MACOS/${APP_BASENAME}" # Resources cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true @@ -1053,9 +1069,9 @@ PLIST info ".app bundle created: $APP" # ── Zip the .app bundle ────────────────────────────────────────────────── - local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip" + local APP_ZIP="${APP_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.app.zip" if command -v zip &>/dev/null; then - (cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app") + (cd "$out" && zip -r "$APP_ZIP" "${APP_BASENAME}.app") info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))" fi @@ -1071,7 +1087,7 @@ PLIST --window-pos 200 120 \ --window-size 600 400 \ --icon-size 100 \ - --icon "ObsidianDragon.app" 150 190 \ + --icon "${APP_BASENAME}.app" 150 190 \ --app-drop-link 450 190 \ --no-internet-enable \ "$out/$DMG_NAME" \ diff --git a/docs/chat-port-feasibility-2026-05-06.md b/docs/chat-port-feasibility-2026-05-06.md new file mode 100644 index 0000000..e584840 --- /dev/null +++ b/docs/chat-port-feasibility-2026-05-06.md @@ -0,0 +1,63 @@ +# Chat Port Feasibility Audit (2026-05-06) + +Scope: +- Evaluate whether SilentDragonXLite chat can be ported to the current full-node ImGui wallet. +- If feasible, define a safe path with feature flagging. + +## Source Audit (SilentDragonXLite) + +Primary chat surfaces in the reference codebase: +- `external/SilentDragonXLite/src/chatmodel.h` +- `external/SilentDragonXLite/src/chatmodel.cpp` +- `external/SilentDragonXLite/src/Chat/Chat.h` +- `external/SilentDragonXLite/src/Chat/Chat.cpp` +- `external/SilentDragonXLite/src/controller.cpp` (transaction scan + memo decrypt/encrypt) +- `external/SilentDragonXLite/src/Model/ChatItem.*` +- `external/SilentDragonXLite/src/Model/ContactRequest*` +- `external/SilentDragonXLite/src/DataStore/ChatDataStore.*` +- `external/SilentDragonXLite/src/mainwindow.ui` and related Qt UI widgets. + +Observed characteristics: +- Chat is tightly coupled to Qt widget/UI classes (`QListView`, `QStandardItemModel`, `.ui` forms). +- Message transport relies on memo payload parsing/decryption embedded directly in transaction refresh/controller flow. +- Contact requests, chat IDs, and message rendering are mixed with Qt view models and DataStore semantics. +- Crypto flow depends on shared secret derivation and memo header conventions implemented inline in controller/chat code. + +## Current Wallet Architecture Gap + +Current app (`src/`) is Dear ImGui + SDL3 with different state/render model: +- No existing chat domain model/service in ImGui code path. +- No Qt runtime/view-model infrastructure to reuse reference chat modules. +- Transaction refresh path is centralized in `NetworkRefreshService` and app refresh orchestration, not Qt controller callbacks. + +## Feasibility Decision + +Decision: **Not safe to directly port in this batch**. + +Reason: +- A direct transplant would require substantial re-architecture, not incremental copy-over. +- High risk of regressions in transaction refresh correctness, memo handling, and wallet UX stability. +- Crypto/message parsing code requires dedicated security review before enabling in production. + +## Estimated Effort (for a proper staged port) + +1. Domain extraction (chat/contact/message models, storage interfaces): 3-5 days +2. Service integration with current refresh pipeline (`NetworkRefreshService` + app): 4-7 days +3. ImGui chat UI (list, composer, contact requests, actions): 4-6 days +4. Security hardening + memo/crypto validation tests: 3-5 days +5. End-to-end QA/regression/perf pass: 2-4 days + +Total estimate: **16-27 engineer-days**. + +## Risk Profile + +- Security risk: high (memo crypto path and malformed payload handling). +- Stability risk: high (transaction refresh path is core-wallet critical). +- UX risk: medium (new asynchronous chat states and contact flows). + +## Recommended Safe Plan + +1. Add compile/runtime feature flag (`DRAGONX_ENABLE_CHAT`) default OFF. +2. Implement read-only parser + storage first (no send), fully test malformed memos. +3. Add send path behind same flag with strict size/format guards. +4. Ship disabled by default until security and regression acceptance criteria are met. diff --git a/src/app.h b/src/app.h index a3444e0..673d1c4 100644 --- a/src/app.h +++ b/src/app.h @@ -130,6 +130,8 @@ public: * @brief Whether we are in the shutdown phase */ bool isShuttingDown() const { return shutting_down_; } + bool isLiteBuild() const { return DRAGONX_LITE_BUILD != 0; } + bool supportsEmbeddedDaemon() const { return DRAGONX_ENABLE_EMBEDDED_DAEMON != 0; } /** * @brief Render the shutdown overlay (called instead of normal UI during shutdown) @@ -439,7 +441,7 @@ private: bool show_address_book_ = false; // Embedded daemon state - bool use_embedded_daemon_ = true; + bool use_embedded_daemon_ = (DRAGONX_ENABLE_EMBEDDED_DAEMON != 0); std::string daemon_status_; mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection) diff --git a/src/app_network.cpp b/src/app_network.cpp index ea27c42..ee39232 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -60,6 +60,23 @@ using NetworkRefreshService = services::NetworkRefreshService; namespace { +bool isPageEnabledForBuild(ui::NavPage page) +{ +#if DRAGONX_LITE_BUILD + switch (page) { + case ui::NavPage::Console: + case ui::NavPage::Peers: + case ui::NavPage::Explorer: + return false; + default: + return true; + } +#else + (void)page; + return true; +#endif +} + std::string unencryptedTransactionHistoryCacheKey(const std::string& walletIdentity) { return std::string("obsidian-dragon-unencrypted-tx-cache-v1:") + @@ -545,6 +562,9 @@ bool App::shouldRunWalletTransactionRefresh() const void App::setCurrentPage(ui::NavPage page) { + if (!isPageEnabledForBuild(page)) { + page = ui::NavPage::Overview; + } if (page == current_page_) return; current_page_ = page; applyRefreshPolicy(page); @@ -1372,6 +1392,11 @@ void App::refreshMarketData() void App::startMining(int threads) { +#if DRAGONX_LITE_BUILD + (void)threads; + ui::Notifications::instance().warning("Solo mining is unavailable in lite build"); + return; +#endif if (!state_.connected || !rpc_ || !worker_) return; if (mining_toggle_in_progress_.exchange(true)) return; // already in progress @@ -1401,6 +1426,9 @@ void App::startMining(int threads) void App::stopMining() { +#if DRAGONX_LITE_BUILD + return; +#endif if (!state_.connected || !rpc_ || !worker_) return; if (mining_toggle_in_progress_.exchange(true)) return; // already in progress diff --git a/src/config/version.h b/src/config/version.h index baf77b0..c1d5512 100644 --- a/src/config/version.h +++ b/src/config/version.h @@ -12,7 +12,7 @@ #define DRAGONX_VERSION_MINOR 2 #define DRAGONX_VERSION_PATCH 0 -#define DRAGONX_APP_NAME "ObsidianDragon" +#define DRAGONX_APP_NAME "ObsidianDragonLite" #define DRAGONX_ORG_NAME "Hush" // Default RPC settings diff --git a/src/config/version.h.in b/src/config/version.h.in index 48ec09e..c6a2097 100644 --- a/src/config/version.h.in +++ b/src/config/version.h.in @@ -12,7 +12,7 @@ #define DRAGONX_VERSION_MINOR @PROJECT_VERSION_MINOR@ #define DRAGONX_VERSION_PATCH @PROJECT_VERSION_PATCH@ -#define DRAGONX_APP_NAME "ObsidianDragon" +#define DRAGONX_APP_NAME "@DRAGONX_APP_NAME@" #define DRAGONX_ORG_NAME "Hush" // Default RPC settings diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 3ce7c83..f0e66b7 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1073,6 +1073,7 @@ void RenderSettingsPage(App* app) { // Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit { + const bool showDaemonOptions = !app->isLiteBuild(); float cbSpacing = Layout::spacingMd(); float fh = ImGui::GetFrameHeight(); float inner = ImGui::GetStyle().ItemInnerSpacing.x; @@ -1080,9 +1081,12 @@ void RenderSettingsPage(App* app) { float totalW = cbW(TR("save_z_transactions")) + cbSpacing + cbW(TR("auto_shield")) + cbSpacing - + cbW(TR("use_tor")) + cbSpacing - + cbW(TR("keep_daemon")) + cbSpacing - + cbW(TR("stop_external")); + + cbW(TR("use_tor")) + cbSpacing; + if (showDaemonOptions) { + totalW += cbW(TR("keep_daemon")) + cbSpacing + + cbW(TR("stop_external")) + cbSpacing; + } + totalW += cbW(TR("verbose_logging")); float scale = (totalW > contentW) ? contentW / totalW : 1.0f; if (scale < 1.0f) ImGui::SetWindowFontScale(scale); float sp = cbSpacing * scale; @@ -1095,18 +1099,20 @@ void RenderSettingsPage(App* app) { ImGui::SameLine(0, sp); ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tor")); - ImGui::SameLine(0, sp); - if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) { - saveSettingsPageState(app->settings()); + if (showDaemonOptions) { + ImGui::SameLine(0, sp); + if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) { + saveSettingsPageState(app->settings()); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", TR("tt_keep_daemon")); + ImGui::SameLine(0, sp); + if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &s_settingsState.stop_external_daemon)) { + saveSettingsPageState(app->settings()); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", TR("tt_stop_external")); } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", TR("tt_keep_daemon")); - ImGui::SameLine(0, sp); - if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &s_settingsState.stop_external_daemon)) { - saveSettingsPageState(app->settings()); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", TR("tt_stop_external")); ImGui::SameLine(0, sp); if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &s_settingsState.verbose_logging)) { dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging); @@ -1469,8 +1475,8 @@ void RenderSettingsPage(App* app) { } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - // Node maintenance buttons - { + // Node maintenance buttons (full-node build only) + if (!app->isLiteBuild()) { ImFont* btnFont = S.resolveFont("button"); float nodeBtnW; { diff --git a/src/ui/sidebar.h b/src/ui/sidebar.h index 7400069..f9a988e 100644 --- a/src/ui/sidebar.h +++ b/src/ui/sidebar.h @@ -66,6 +66,16 @@ inline const char* NavSectionLabel(const NavItem& item) { return item.section_tr_key ? TR(item.section_tr_key) : item.section_label; } +inline bool IsNavPageVisible(NavPage page) +{ +#if DRAGONX_LITE_BUILD + return page != NavPage::Console && page != NavPage::Peers && page != NavPage::Explorer; +#else + (void)page; + return true; +#endif +} + // Get the Material Design icon string for a navigation page. inline const char* GetNavIconMD(NavPage page) { @@ -446,12 +456,13 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei // Separate fixed parts (don't scale) from flex parts (scale when tight). float fixedH = stripH; // collapse strip for (int i = 0; i < (int)NavPage::Count_; ++i) - if (kNavItems[i].section_label && showLabels) + if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels) fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below fixedH += bottomPadding + stripH; // exit area float baseFlexH = baseNavGap; for (int i = 0; i < (int)NavPage::Count_; ++i) { + if (!IsNavPageVisible(kNavItems[i].page)) continue; if (kNavItems[i].section_label) { if (showLabels) baseFlexH += baseSectionGap; else baseFlexH += baseSectionGap * 0.8f; @@ -479,6 +490,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei float curY = stripH + navGap; // after collapse strip + gap for (int i = 0; i < (int)NavPage::Count_; ++i) { + if (!IsNavPageVisible(kNavItems[i].page)) continue; if (kNavItems[i].section_label) { if (showLabels) { curY += sectionGap; @@ -560,6 +572,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei float maxSectionW = panelRight - wp.x - sbSectionLabelPadLeft; int si = 0; for (int i = 0; i < (int)NavPage::Count_; ++i) { + if (!IsNavPageVisible(kNavItems[i].page)) continue; if (kNavItems[i].section_label && si < nSectionLabels) { float ly = panelTopY + sectionLabelY[si++]; const char* sLabel = NavSectionLabel(kNavItems[i]); @@ -587,6 +600,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei float btnPadX = collapsed ? btnPadCollapsed : btnPadExpanded; for (int i = 0; i < (int)NavPage::Count_; ++i) { const NavItem& item = kNavItems[i]; + if (!IsNavPageVisible(item.page)) continue; bool selected = (current == item.page); float btnY = panelTopY + itemY[i]; float btnRnd = itemH * 0.22f; @@ -695,10 +709,14 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) { int idx = static_cast(current); bool nav = false; - if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) - if (idx > 0) { idx--; nav = true; } - if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) - if (idx < (int)NavPage::Count_ - 1) { idx++; nav = true; } + if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) { + do { idx--; } while (idx >= 0 && !IsNavPageVisible(static_cast(idx))); + if (idx >= 0) nav = true; + } + if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) { + do { idx++; } while (idx < (int)NavPage::Count_ && !IsNavPageVisible(static_cast(idx))); + if (idx < (int)NavPage::Count_) nav = true; + } if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) collapsed = !collapsed; if (nav) { current = static_cast(idx); changed = true; } diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index a1ceaa1..32fd637 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -151,6 +151,13 @@ static void RenderMiningTabContent(App* app) strncpy(s_pool_worker, app->settings()->getPoolWorker().c_str(), sizeof(s_pool_worker) - 1); s_pool_state_loaded = true; } +#if DRAGONX_LITE_BUILD + if (!s_pool_mode) { + s_pool_mode = true; + app->settings()->setPoolMode(true); + app->settings()->save(); + } +#endif // Default pool worker to user's first shielded (z) address once available. // For new wallets without a z-address, leave the field blank so the user @@ -235,10 +242,11 @@ static void RenderMiningTabContent(App* app) // MODE TOGGLE — SOLO | POOL segmented control // ================================================================ { + const bool liteBuild = app->isLiteBuild(); float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs; float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size; float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size; - float totalW = toggleW * 2; + float totalW = liteBuild ? toggleW : (toggleW * 2); ImVec2 tMin = ImGui::GetCursorScreenPos(); ImVec2 tMax(tMin.x + totalW, tMin.y + toggleH); @@ -247,16 +255,16 @@ static void RenderMiningTabContent(App* app) dl->AddRectFilled(tMin, tMax, WithAlpha(OnSurface(), 15), toggleRnd); dl->AddRect(tMin, tMax, WithAlpha(OnSurface(), 40), toggleRnd); - // SOLO button (left half) ImVec2 soloMin = tMin; ImVec2 soloMax(tMin.x + toggleW, tMax.y); bool soloHov = material::IsRectHovered(soloMin, soloMax); - if (!s_pool_mode) { - dl->AddRectFilled(soloMin, soloMax, WithAlpha(Primary(), 180), toggleRnd); - } else if (soloHov) { - dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd); - } - { + if (!liteBuild) { + // SOLO button (left half) + if (!s_pool_mode) { + dl->AddRectFilled(soloMin, soloMax, WithAlpha(Primary(), 180), toggleRnd); + } else if (soloHov) { + dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd); + } const char* label = TR("mining_solo"); ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label); float lx = soloMin.x + (toggleW - sz.x) * 0.5f; @@ -267,7 +275,7 @@ static void RenderMiningTabContent(App* app) // POOL button (right half) — disabled when solo mining is active bool soloMiningActive = mining.generate; - ImVec2 poolMin(tMin.x + toggleW, tMin.y); + ImVec2 poolMin(tMin.x + (liteBuild ? 0.0f : toggleW), tMin.y); ImVec2 poolMax = tMax; bool poolHov = material::IsRectHovered(poolMin, poolMax); if (s_pool_mode) { @@ -288,19 +296,21 @@ static void RenderMiningTabContent(App* app) } // Invisible buttons for click targets - ImGui::SetCursorScreenPos(soloMin); - ImGui::InvisibleButton("##SoloMode", ImVec2(toggleW, toggleH)); - if (ImGui::IsItemClicked() && s_pool_mode) { - s_pool_mode = false; - app->settings()->setPoolMode(false); - app->settings()->save(); - app->stopPoolMining(); + if (!liteBuild) { + ImGui::SetCursorScreenPos(soloMin); + ImGui::InvisibleButton("##SoloMode", ImVec2(toggleW, toggleH)); + if (ImGui::IsItemClicked() && s_pool_mode) { + s_pool_mode = false; + app->settings()->setPoolMode(false); + app->settings()->save(); + app->stopPoolMining(); + } + if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } - if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetCursorScreenPos(poolMin); ImGui::InvisibleButton("##PoolMode", ImVec2(toggleW, toggleH)); - if (ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) { + if (!liteBuild && ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) { s_pool_mode = true; app->settings()->setPoolMode(true); app->settings()->save(); @@ -308,7 +318,7 @@ static void RenderMiningTabContent(App* app) // so no need to call stopMining() — it would just set the // toggle-in-progress flag and make the button show "STARTING". } - if (poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (!liteBuild && poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (poolHov && soloMiningActive && !s_pool_mode) { ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool")); } @@ -1925,8 +1935,12 @@ static void RenderMiningTabContent(App* app) && tx.memo.find("Mining Pool payout") != std::string::npos); if (isSoloMined || isPoolPayout) { // Apply earnings filter - if (s_earnings_filter == 1 && !isSoloMined) continue; - if (s_earnings_filter == 2 && !isPoolPayout) continue; + if (app->isLiteBuild()) { + if (s_earnings_filter == 1 && !isPoolPayout) continue; + } else { + if (s_earnings_filter == 1 && !isSoloMined) continue; + if (s_earnings_filter == 2 && !isPoolPayout) continue; + } double amt = std::abs(tx.amount); minedAllTime += amt; @@ -1970,8 +1984,10 @@ static void RenderMiningTabContent(App* app) // === Earnings filter toggle button (top-right of card) === { - const char* filterLabels[] = { TR("mining_filter_all"), TR("mining_solo"), TR("mining_pool") }; - const char* filterIcons[] = { ICON_MD_FUNCTIONS, ICON_MD_MEMORY, ICON_MD_CLOUD }; + const bool liteBuild = app->isLiteBuild(); + const char* filterLabels[] = { TR("mining_filter_all"), liteBuild ? TR("mining_pool") : TR("mining_solo"), TR("mining_pool") }; + const char* filterIcons[] = { ICON_MD_FUNCTIONS, liteBuild ? ICON_MD_CLOUD : ICON_MD_MEMORY, ICON_MD_CLOUD }; + const int filterCount = liteBuild ? 2 : 3; const char* curIcon = filterIcons[s_earnings_filter]; const char* curLabel = filterLabels[s_earnings_filter]; @@ -2011,13 +2027,13 @@ static void RenderMiningTabContent(App* app) ImGui::SetCursorScreenPos(bMin); ImGui::InvisibleButton("##EarningsFilter", ImVec2(btnW, btnH)); if (ImGui::IsItemClicked()) { - s_earnings_filter = (s_earnings_filter + 1) % 3; + s_earnings_filter = (s_earnings_filter + 1) % filterCount; } if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); const char* tips[] = { TR("mining_filter_tip_all"), - TR("mining_filter_tip_solo"), + liteBuild ? TR("mining_filter_tip_pool") : TR("mining_filter_tip_solo"), TR("mining_filter_tip_pool") }; ImGui::SetTooltip("%s", tips[s_earnings_filter]);