Compare commits
65 Commits
64b27db2ff
...
lite-v1.0.
| Author | SHA1 | Date | |
|---|---|---|---|
| fffee9f0b5 | |||
| 00ffc959e5 | |||
| b561406c23 | |||
| 5a0743a17b | |||
| a7b0770ad0 | |||
| 7d8323a622 | |||
| 320944fd18 | |||
| 6d5e0ac614 | |||
| 02554d523d | |||
| 21da9e75fc | |||
| c53b7f771e | |||
| bcee4bfe72 | |||
| e1870c3b23 | |||
| 7cf0bb8bc7 | |||
| 541a9e1fd5 | |||
| 07b8e0b2cb | |||
| de48414c65 | |||
| f0c681b0b5 | |||
| fdf2502ca8 | |||
| e393b0d847 | |||
| 3b041f20c1 | |||
| 77a0ad64b0 | |||
| d0ca2fdf52 | |||
| 9c07b6d33d | |||
| fedfe3d60c | |||
| 95ff9ce9ae | |||
| 203967411a | |||
| 3d0305a9ca | |||
| e7f38c2a45 | |||
| 3a2be661ea | |||
| 17525003c5 | |||
| 267d839d5b | |||
| 987d9b93c7 | |||
| 4c43f2e082 | |||
| 57fa6470b7 | |||
| c68889d276 | |||
| b0a4333cdf | |||
| 4471f54842 | |||
| e08121af2b | |||
| 31ebfc782e | |||
| f3776bcbe5 | |||
| 38f2aa2f8f | |||
| 40f8425d94 | |||
| 2157a30192 | |||
| bef3c0c47d | |||
| 5c570613c8 | |||
| 567732206e | |||
| 0dcc09fc5f | |||
| 82d3178119 | |||
| 7d47c6e14e | |||
| 973bc0d338 | |||
| e4c9b98ca2 | |||
| 1c38f68781 | |||
| 818c29fca7 | |||
| 0a43742897 | |||
| defe14d913 | |||
| 8b55a90102 | |||
| 0fd3d214a4 | |||
| 996e10ed02 | |||
| 2c909d35ea | |||
| 3b5db02e09 | |||
| 8d8cd337cf | |||
| acde8c0833 | |||
| a78b44246e | |||
| 63ed31d2ec |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -11,8 +11,8 @@ prebuilt-binaries/dragonxd-win/*
|
||||
!prebuilt-binaries/dragonxd-win/.gitkeep
|
||||
prebuilt-binaries/dragonxd-mac/*
|
||||
!prebuilt-binaries/dragonxd-mac/.gitkeep
|
||||
prebuilt-binaries/xmrig-hac/*
|
||||
!prebuilt-binaries/xmrig-hac/.gitkeep
|
||||
prebuilt-binaries/drg-xmrig/*
|
||||
!prebuilt-binaries/drg-xmrig/.gitkeep
|
||||
|
||||
|
||||
# External sources / toolchains (created by scripts/setup.sh)
|
||||
@@ -33,7 +33,7 @@ imgui.ini
|
||||
*.bak*
|
||||
*.params
|
||||
asmap.dat
|
||||
/external/xmrig-hac
|
||||
/external/drg-xmrig
|
||||
/memory
|
||||
/todo.md
|
||||
/.github/
|
||||
@@ -54,3 +54,7 @@ third_party/silentdragonxlite/lib/vendor/
|
||||
|
||||
# Generated by configure_file from res/ObsidianDragon.manifest.in (do not track)
|
||||
res/ObsidianDragon.manifest
|
||||
|
||||
# Cross-built mingw FreeType (color emoji) — regenerated by scripts/build-freetype-mingw.sh
|
||||
third_party/freetype-mingw/
|
||||
third_party/.freetype-mingw-build/
|
||||
|
||||
@@ -53,7 +53,6 @@ set_property(CACHE DRAGONX_LITE_BACKEND_LINK_MODE PROPERTY STRINGS imported)
|
||||
set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version")
|
||||
set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory")
|
||||
set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest")
|
||||
option(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE "Require verified signature metadata in the lite backend artifact manifest" OFF)
|
||||
set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS
|
||||
litelib_wallet_exists
|
||||
litelib_initialize_new
|
||||
@@ -126,36 +125,24 @@ if(DRAGONX_ENABLE_LITE_BACKEND)
|
||||
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||
endif()
|
||||
if(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE)
|
||||
if(NOT DRAGONX_LITE_BACKEND_MANIFEST)
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires DRAGONX_LITE_BACKEND_MANIFEST")
|
||||
endif()
|
||||
file(READ "${DRAGONX_LITE_BACKEND_MANIFEST}" DRAGONX_LITE_BACKEND_MANIFEST_JSON)
|
||||
string(JSON DRAGONX_LITE_SIGNATURE_STATUS ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_STATUS_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_status)
|
||||
if(DRAGONX_LITE_SIGNATURE_STATUS_ERROR)
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing signature verification status")
|
||||
endif()
|
||||
if(NOT DRAGONX_LITE_SIGNATURE_STATUS STREQUAL "verified")
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verified signature metadata")
|
||||
endif()
|
||||
string(JSON DRAGONX_LITE_SIGNATURE_VERIFIED_SHA ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verified_artifact_sha256)
|
||||
string(JSON DRAGONX_LITE_ARTIFACT_SHA ERROR_VARIABLE DRAGONX_LITE_ARTIFACT_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" artifact sha256)
|
||||
if(DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR OR DRAGONX_LITE_ARTIFACT_SHA_ERROR)
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing artifact/signature SHA-256 metadata")
|
||||
endif()
|
||||
if(NOT DRAGONX_LITE_SIGNATURE_VERIFIED_SHA STREQUAL DRAGONX_LITE_ARTIFACT_SHA)
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST signature metadata does not verify the artifact SHA-256")
|
||||
endif()
|
||||
string(JSON DRAGONX_LITE_SIGNATURE_PERFORMED ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_performed)
|
||||
if(DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR OR NOT DRAGONX_LITE_SIGNATURE_PERFORMED)
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verification_performed=true")
|
||||
endif()
|
||||
endif()
|
||||
# Note (F15-1): the former signature-metadata gate was removed. It trusted a
|
||||
# "verification_status: verified" field that scripts/build-lite-backend-artifact.sh
|
||||
# self-attested with no cryptographic check (the "verified" SHA was just the artifact's
|
||||
# own SHA). The trust root is now build-from-source: that script builds the backend from
|
||||
# the vendored in-tree source and refuses prebuilt artifacts, so the library linked here
|
||||
# is the one built from reviewed source. The required-symbol inventory check above stays.
|
||||
|
||||
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
|
||||
set_target_properties(dragonx_lite_backend PROPERTIES
|
||||
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
|
||||
)
|
||||
if(APPLE)
|
||||
# The Rust backend's TLS stack (security-framework / core-foundation crates)
|
||||
# references Secure Transport (SSL*) + CoreFoundation symbols. Link the frameworks
|
||||
# that provide them, or the static lib leaves ~130 symbols undefined at link time.
|
||||
set_property(TARGET dragonx_lite_backend APPEND PROPERTY
|
||||
INTERFACE_LINK_LIBRARIES "-framework Security" "-framework CoreFoundation")
|
||||
endif()
|
||||
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
|
||||
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||
@@ -304,6 +291,15 @@ FetchContent_Declare(
|
||||
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
|
||||
GIT_TAG v1.4.0
|
||||
GIT_SHALLOW TRUE
|
||||
# libwebp's cpu.cmake applies -mno-sse2/-mno-sse4.1 to its scalar reference DSP
|
||||
# files when it can't probe SSE support. Under a macOS universal build
|
||||
# (-arch arm64;x86_64) that probe fails, so the flags land on the x86_64 slice,
|
||||
# where -mno-sse2 disables _Float16 and breaks the SDK's <math.h>. Neutralize
|
||||
# those disable flags (SSE2 is x86_64 baseline). Portable + idempotent; a no-op
|
||||
# for single-arch Linux/Windows/x86_64 builds. See cmake/patch-libwebp-simd.cmake.
|
||||
PATCH_COMMAND ${CMAKE_COMMAND}
|
||||
-DCPU_CMAKE=<SOURCE_DIR>/cmake/cpu.cmake
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/patch-libwebp-simd.cmake
|
||||
)
|
||||
set(WEBP_LINK_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE)
|
||||
@@ -406,6 +402,35 @@ else()
|
||||
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/backends/imgui_impl_opengl3.h)
|
||||
endif()
|
||||
|
||||
# Optional FreeType font loader — enables color-emoji rendering (COLR/CPAL Twemoji) when the chat
|
||||
# "color emoji" setting is on; otherwise the wallet falls back to the monochrome emoji subset.
|
||||
# - Native Linux/macOS: use the system FreeType via find_package.
|
||||
# - Windows (mingw cross): the toolchain ships no FreeType, so build.sh --win-release cross-builds a
|
||||
# static one (scripts/build-freetype-mingw.sh) and passes -DDRAGONX_MINGW_FREETYPE_PREFIX here.
|
||||
# - Other cross builds (osxcross) without FreeType: silently fall back to monochrome.
|
||||
set(DRAGONX_FREETYPE OFF)
|
||||
set(DRAGONX_FREETYPE_LIB "")
|
||||
set(DRAGONX_FREETYPE_INC "")
|
||||
if(DEFINED DRAGONX_MINGW_FREETYPE_PREFIX AND EXISTS "${DRAGONX_MINGW_FREETYPE_PREFIX}/lib/libfreetype.a")
|
||||
set(DRAGONX_FREETYPE ON)
|
||||
set(DRAGONX_FREETYPE_LIB "${DRAGONX_MINGW_FREETYPE_PREFIX}/lib/libfreetype.a")
|
||||
set(DRAGONX_FREETYPE_INC "${DRAGONX_MINGW_FREETYPE_PREFIX}/include/freetype2")
|
||||
message(STATUS "FreeType (mingw cross-built) found — chat color emoji enabled")
|
||||
elseif(NOT CMAKE_CROSSCOMPILING)
|
||||
find_package(Freetype QUIET)
|
||||
if(FREETYPE_FOUND)
|
||||
set(DRAGONX_FREETYPE ON)
|
||||
set(DRAGONX_FREETYPE_LIB Freetype::Freetype) # imported target carries include dirs
|
||||
message(STATUS "FreeType ${FREETYPE_VERSION_STRING} found — chat color emoji enabled")
|
||||
endif()
|
||||
endif()
|
||||
if(DRAGONX_FREETYPE)
|
||||
list(APPEND IMGUI_SOURCES ${IMGUI_DIR}/misc/freetype/imgui_freetype.cpp)
|
||||
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/misc/freetype/imgui_freetype.h)
|
||||
else()
|
||||
message(STATUS "FreeType not found — chat color emoji falls back to monochrome")
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# QR Code library (bundled)
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -526,6 +551,7 @@ set(APP_SOURCES
|
||||
src/util/platform.cpp
|
||||
src/util/payment_uri.cpp
|
||||
src/util/texture_loader.cpp
|
||||
src/util/svg_texture.cpp
|
||||
src/util/noise_texture.cpp
|
||||
src/daemon/embedded_daemon.cpp
|
||||
src/daemon/seed_wallet_creator.cpp
|
||||
@@ -730,7 +756,9 @@ ${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/UbuntuMono-R.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoEmoji-Subset.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/TwemojiMozilla-Color.ttf"
|
||||
)
|
||||
|
||||
add_executable(ObsidianDragon
|
||||
@@ -863,6 +891,15 @@ else()
|
||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
|
||||
endif()
|
||||
|
||||
# Color-emoji font loader (FreeType) — linked + flagged only when found (see DRAGONX_FREETYPE above).
|
||||
if(DRAGONX_FREETYPE)
|
||||
target_link_libraries(ObsidianDragon PRIVATE ${DRAGONX_FREETYPE_LIB})
|
||||
if(DRAGONX_FREETYPE_INC)
|
||||
target_include_directories(ObsidianDragon PRIVATE ${DRAGONX_FREETYPE_INC})
|
||||
endif()
|
||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAVE_FREETYPE)
|
||||
endif()
|
||||
|
||||
add_executable(HushChatFixtureCheck
|
||||
tools/hushchat_fixture_check.cpp
|
||||
src/chat/chat_protocol.cpp
|
||||
@@ -1163,5 +1200,5 @@ message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
|
||||
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
|
||||
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||
message(STATUS " Lite signature: ${DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE}")
|
||||
message(STATUS " Lite trust: built-from-source (vendored third_party/silentdragonxlite)")
|
||||
message(STATUS "")
|
||||
|
||||
@@ -81,8 +81,8 @@ Download linux and windows binaries of latest releases and place in binary direc
|
||||
- prebuilt-binaries/dragonxd-win/
|
||||
- prebuilt-binaries/dragonxd-mac/
|
||||
|
||||
**xmrig HAC fork** (https://git.dragonx.is/dragonx/xmrig-hac):
|
||||
- prebuilt-binaries/xmrig-hac/
|
||||
**DRG-XMRig fork** (https://git.dragonx.is/DragonX/drg-xmrig):
|
||||
- prebuilt-binaries/drg-xmrig/
|
||||
|
||||
|
||||
## Build Steps
|
||||
|
||||
105
build.sh
105
build.sh
@@ -131,7 +131,7 @@ fi
|
||||
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
|
||||
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
|
||||
_cml="$SCRIPT_DIR/CMakeLists.txt"
|
||||
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
|
||||
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]][[:space:]]*\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
|
||||
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||
@@ -386,7 +386,7 @@ build_release_linux() {
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
|
||||
@@ -419,7 +419,7 @@ build_release_linux() {
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
|
||||
|
||||
# Desktop entry
|
||||
@@ -478,18 +478,28 @@ APPRUN
|
||||
done
|
||||
[[ -f "$bd/_deps/sdl3-build/libSDL3.so" ]] && cp "$bd/_deps/sdl3-build/libSDL3.so"* "$APPDIR/usr/lib/" 2>/dev/null || true
|
||||
|
||||
# appimagetool
|
||||
# appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
|
||||
# The old "continuous" tag is a MOVING build fetched over the network and run on the release
|
||||
# builder; a compromised/MITM'd artifact would execute here. Verify, or refuse to package.
|
||||
local APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
|
||||
local APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
|
||||
local APPIMAGETOOL=""
|
||||
if command -v appimagetool &>/dev/null; then
|
||||
APPIMAGETOOL="appimagetool"
|
||||
elif [[ -f "$bd/appimagetool-x86_64.AppImage" ]]; then
|
||||
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
|
||||
else
|
||||
info "Downloading appimagetool ..."
|
||||
wget -q -O "$bd/appimagetool-x86_64.AppImage" \
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x "$bd/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
|
||||
local at="$bd/appimagetool-x86_64.AppImage"
|
||||
# Re-verify any cached copy too; a stale unverified download must not be trusted.
|
||||
if [[ ! -f "$at" ]] || ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
|
||||
info "Downloading appimagetool 1.9.0 (pinned) ..."
|
||||
wget -q -O "$at" "$APPIMAGETOOL_URL"
|
||||
if ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
|
||||
err "appimagetool SHA-256 verification failed — refusing to use it"
|
||||
rm -f "$at"
|
||||
return 1
|
||||
fi
|
||||
chmod +x "$at"
|
||||
fi
|
||||
APPIMAGETOOL="$at"
|
||||
fi
|
||||
|
||||
local ARCH
|
||||
@@ -628,8 +638,8 @@ HDR
|
||||
info "Lite mode: skipping embedded daemon binaries"
|
||||
fi
|
||||
|
||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
|
||||
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
|
||||
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
|
||||
@@ -725,12 +735,27 @@ HDR
|
||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh" --win
|
||||
fi
|
||||
|
||||
# ── FreeType for Windows (color-emoji rendering) ───────────────────────
|
||||
# The mingw toolchain ships no FreeType; cross-build a minimal static one (COLR/CPAL, no external
|
||||
# deps). Failure is non-fatal — the wallet just falls back to monochrome emoji.
|
||||
local FT_MINGW_PREFIX="$SCRIPT_DIR/third_party/freetype-mingw"
|
||||
if [[ ! -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
|
||||
info "Cross-building FreeType for Windows (color emoji) ..."
|
||||
"$SCRIPT_DIR/scripts/build-freetype-mingw.sh" "$FT_MINGW_PREFIX" \
|
||||
|| warn "FreeType cross-build failed — Windows build will use monochrome emoji"
|
||||
fi
|
||||
local FT_CMAKE_ARG=()
|
||||
if [[ -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
|
||||
FT_CMAKE_ARG=(-DDRAGONX_MINGW_FREETYPE_PREFIX="$FT_MINGW_PREFIX")
|
||||
fi
|
||||
|
||||
# ── CMake + build ────────────────────────────────────────────────────────
|
||||
info "Configuring (cross-compile) ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
"${FT_CMAKE_ARG[@]}" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
@@ -766,7 +791,7 @@ HDR
|
||||
fi
|
||||
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig.exe"
|
||||
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
|
||||
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
@@ -876,8 +901,26 @@ build_release_mac() {
|
||||
fi
|
||||
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
||||
else
|
||||
# Native macOS: build universal binary (arm64 + x86_64)
|
||||
MAC_ARCH="universal"
|
||||
# Native macOS: build universal (arm64 + x86_64) by default. Override with
|
||||
# DRAGONX_MAC_ARCHS (e.g. "x86_64").
|
||||
MAC_ARCHS="${DRAGONX_MAC_ARCHS:-arm64;x86_64}"
|
||||
# When linking the real lite backend, the app can only include architectures
|
||||
# the backend static library actually provides. Its pinned ring 0.16.11 has no
|
||||
# Apple-Silicon assembly, so that artifact is x86_64-only — constrain the app
|
||||
# arch to the backend's (unless the user explicitly forced DRAGONX_MAC_ARCHS),
|
||||
# otherwise the arm64 slice fails to link.
|
||||
if $DO_LITE_BACKEND && [[ -z "${DRAGONX_MAC_ARCHS:-}" && -n "${lb_lib:-}" ]] && command -v lipo &>/dev/null; then
|
||||
local _backend_archs; _backend_archs=$(lipo -archs "$lb_lib" 2>/dev/null | tr ' ' ';')
|
||||
if [[ -n "$_backend_archs" && "$_backend_archs" != "$MAC_ARCHS" ]]; then
|
||||
warn "Lite backend provides only [$_backend_archs] — building the app for that instead of universal."
|
||||
MAC_ARCHS="$_backend_archs"
|
||||
fi
|
||||
fi
|
||||
if [[ "$MAC_ARCHS" == *";"* || "$MAC_ARCHS" == *","* ]]; then
|
||||
MAC_ARCH="universal"
|
||||
else
|
||||
MAC_ARCH="$MAC_ARCHS"
|
||||
fi
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
fi
|
||||
|
||||
@@ -965,7 +1008,7 @@ TOOLCHAIN
|
||||
need_sodium=true
|
||||
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||
# Rebuild if existing lib is not universal (single-arch won't link)
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -Eq "arm64.*x86_64|x86_64.*arm64"; then
|
||||
info "Existing libsodium is not universal — rebuilding ..."
|
||||
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
||||
need_sodium=true
|
||||
@@ -976,13 +1019,13 @@ TOOLCHAIN
|
||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
||||
fi
|
||||
|
||||
info "Configuring (native universal arm64+x86_64) ..."
|
||||
info "Configuring (native macOS, arch: $MAC_ARCHS) ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-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="$MAC_ARCHS" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
fi
|
||||
|
||||
@@ -1012,8 +1055,12 @@ TOOLCHAIN
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||
|
||||
# ── Create .app bundle ───────────────────────────────────────────────────
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
# Clean only THIS variant's prior artifacts so full-node and lite releases can
|
||||
# coexist in release/mac/ (Linux/Windows scope their cleanup the same way). The
|
||||
# "ObsidianDragon-" glob never matches "ObsidianDragonLite-" (and vice versa),
|
||||
# and the ".app" names are exact.
|
||||
rm -rf "$out/${APP_BASENAME}.app" "$out/${APP_BASENAME}-"*.app.zip "$out/${APP_BASENAME}-"*.dmg
|
||||
|
||||
local APP="$out/${APP_BASENAME}.app"
|
||||
local CONTENTS="$APP/Contents"
|
||||
@@ -1063,8 +1110,8 @@ TOOLCHAIN
|
||||
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
||||
fi
|
||||
|
||||
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
# xmrig binary (from prebuilt-binaries/drg-xmrig/)
|
||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
if [[ -f "$XMRIG_MAC" ]]; then
|
||||
cp "$XMRIG_MAC" "$MACOS/xmrig"
|
||||
chmod +x "$MACOS/xmrig"
|
||||
@@ -1213,8 +1260,10 @@ PLIST
|
||||
fi
|
||||
|
||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||
local DMG_BASENAME="DragonX_Wallet"
|
||||
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
|
||||
# DMG filename matches the app bundle name (ObsidianDragon / ObsidianDragonLite).
|
||||
# The mounted volume + CFBundleName keep the "DragonX Wallet" display branding
|
||||
# (APP_DISPLAY_NAME above).
|
||||
local DMG_BASENAME="${APP_BASENAME}"
|
||||
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||
|
||||
if command -v create-dmg &>/dev/null; then
|
||||
@@ -1296,3 +1345,9 @@ if $DO_LINUX || $DO_WIN || $DO_MAC; then
|
||||
[[ -d "$SCRIPT_DIR/release/windows" ]] && echo -e " ${CYAN}windows/${NC} — .exe + .zip"
|
||||
[[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg"
|
||||
fi
|
||||
|
||||
# Reaching here means the build completed (real failures exit 1 at their point of failure).
|
||||
# Exit 0 explicitly: the final `[[ -d release/mac ]] && echo` above returns non-zero on a
|
||||
# non-mac build — and since set -e exempts the left side of an &&, that status would otherwise
|
||||
# become the script's exit code and make a successful build report failure (e.g. to CI).
|
||||
exit 0
|
||||
|
||||
31
cmake/patch-libwebp-simd.cmake
Normal file
31
cmake/patch-libwebp-simd.cmake
Normal file
@@ -0,0 +1,31 @@
|
||||
# patch-libwebp-simd.cmake — portable, idempotent FetchContent patch for libwebp.
|
||||
#
|
||||
# libwebp's cmake/cpu.cmake compiles its scalar *reference* DSP files with the
|
||||
# SSE-disable flags "-mno-sse4.1;-mno-sse2" whenever it can't positively detect
|
||||
# SSE support. Under a macOS *universal* build (-arch arm64;x86_64) the per-arch
|
||||
# SSE flag probe fails (a flag valid for x86_64 is invalid for arm64), so those
|
||||
# disable flags get applied to the x86_64 slice. clang gates the _Float16 type on
|
||||
# SSE2 for x86_64, and the macOS 15+/26 SDK's <math.h> declares _Float16 math
|
||||
# functions unconditionally — so any TU including <math.h> fails to compile with
|
||||
# "_Float16 is not supported on this target".
|
||||
#
|
||||
# SSE2 is part of the x86_64 baseline ABI, so disabling it on the reference files
|
||||
# is unnecessary on every platform we target. Blanking the SSE entries (indices
|
||||
# must stay aligned with WEBP_SIMD_FLAGS) fixes the universal build and is a no-op
|
||||
# for single-arch Linux/Windows/x86_64 builds. Idempotent: re-running is a no-op.
|
||||
if(NOT DEFINED CPU_CMAKE OR NOT EXISTS "${CPU_CMAKE}")
|
||||
message(FATAL_ERROR "patch-libwebp-simd: cpu.cmake not found at '${CPU_CMAKE}'")
|
||||
endif()
|
||||
|
||||
file(READ "${CPU_CMAKE}" _contents)
|
||||
string(REPLACE
|
||||
"set(SIMD_DISABLE_FLAGS \"-mno-sse4.1;-mno-sse2;;-mno-dspr2;;-mno-msa\")"
|
||||
"set(SIMD_DISABLE_FLAGS \";;;-mno-dspr2;;-mno-msa\")"
|
||||
_patched "${_contents}")
|
||||
|
||||
if(_patched STREQUAL _contents)
|
||||
message(STATUS "patch-libwebp-simd: no change (already patched or pattern absent)")
|
||||
else()
|
||||
file(WRITE "${CPU_CMAKE}" "${_patched}")
|
||||
message(STATUS "patch-libwebp-simd: neutralized x86 SSE-disable flags in cpu.cmake")
|
||||
endif()
|
||||
744
libs/imgui/misc/freetype/imgui_freetype.cpp
Normal file
744
libs/imgui/misc/freetype/imgui_freetype.cpp
Normal file
@@ -0,0 +1,744 @@
|
||||
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
|
||||
// (code)
|
||||
|
||||
// Get the latest version at https://github.com/ocornut/imgui/tree/master/misc/freetype
|
||||
// Original code by @vuhdo (Aleksei Skriabin) in 2017, with improvements by @mikesart.
|
||||
// Maintained since 2019 by @ocornut.
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2025/06/11: refactored for the new ImFontLoader architecture, and ImGuiBackendFlags_RendererHasTextures support.
|
||||
// 2024/10/17: added plutosvg support for SVG Fonts (seems faster/better than lunasvg). Enable by using '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG'. (#7927)
|
||||
// 2023/11/13: added support for ImFontConfig::RasterizationDensity field for scaling render density without scaling metrics.
|
||||
// 2023/08/01: added support for SVG fonts, enable by using '#define IMGUI_ENABLE_FREETYPE_LUNASVG'. (#6591)
|
||||
// 2023/01/04: fixed a packing issue which in some occurrences would prevent large amount of glyphs from being packed correctly.
|
||||
// 2021/08/23: fixed crash when FT_Render_Glyph() fails to render a glyph and returns nullptr.
|
||||
// 2021/03/05: added ImGuiFreeTypeBuilderFlags_Bitmap to load bitmap glyphs.
|
||||
// 2021/03/02: set 'atlas->TexPixelsUseColors = true' to help some backends with deciding of a preferred texture format.
|
||||
// 2021/01/28: added support for color-layered glyphs via ImGuiFreeTypeBuilderFlags_LoadColor (require Freetype 2.10+).
|
||||
// 2021/01/26: simplified integration by using '#define IMGUI_ENABLE_FREETYPE'. renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. removed ImGuiFreeType::BuildFontAtlas().
|
||||
// 2020/06/04: fix for rare case where FT_Get_Char_Index() succeed but FT_Load_Glyph() fails.
|
||||
// 2019/02/09: added RasterizerFlags::Monochrome flag to disable font anti-aliasing (combine with ::MonoHinting for best results!)
|
||||
// 2019/01/15: added support for imgui allocators + added FreeType only override function SetAllocatorFunctions().
|
||||
// 2019/01/10: re-factored to match big update in STB builder. fixed texture height waste. fixed redundant glyphs when merging. support for glyph padding.
|
||||
// 2018/06/08: added support for ImFontConfig::GlyphMinAdvanceX, GlyphMaxAdvanceX.
|
||||
// 2018/02/04: moved to main imgui repository (away from http://www.github.com/ocornut/imgui_club)
|
||||
// 2018/01/22: fix for addition of ImFontAtlas::TexUvscale member.
|
||||
// 2017/10/22: minor inconsequential change to match change in master (removed an unnecessary statement).
|
||||
// 2017/09/26: fixes for imgui internal changes.
|
||||
// 2017/08/26: cleanup, optimizations, support for ImFontConfig::RasterizerFlags, ImFontConfig::RasterizerMultiply.
|
||||
// 2017/08/16: imported from https://github.com/Vuhdo/imgui_freetype into http://www.github.com/ocornut/imgui_club, updated for latest changes in ImFontAtlas, minor tweaks.
|
||||
|
||||
// About Gamma Correct Blending:
|
||||
// - FreeType assumes blending in linear space rather than gamma space.
|
||||
// - See https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_Render_Glyph
|
||||
// - For correct results you need to be using sRGB and convert to linear space in the pixel shader output.
|
||||
// - The default dear imgui styles will be impacted by this change (alpha values will need tweaking).
|
||||
|
||||
// FIXME: cfg.OversampleH, OversampleV are not supported, but generally not necessary with this rasterizer because Hinting makes everything look better.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_freetype.h"
|
||||
#include "imgui_internal.h" // ImMin,ImMax,ImFontAtlasBuild*,
|
||||
#include <stdint.h>
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H // <freetype/freetype.h>
|
||||
#include FT_MODULE_H // <freetype/ftmodapi.h>
|
||||
#include FT_GLYPH_H // <freetype/ftglyph.h>
|
||||
#include FT_SIZES_H // <freetype/ftsizes.h>
|
||||
#include FT_SYNTHESIS_H // <freetype/ftsynth.h>
|
||||
|
||||
// Handle LunaSVG and PlutoSVG
|
||||
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) && defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||
#error "Cannot enable both IMGUI_ENABLE_FREETYPE_LUNASVG and IMGUI_ENABLE_FREETYPE_PLUTOSVG"
|
||||
#endif
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
#include FT_OTSVG_H // <freetype/otsvg.h>
|
||||
#include FT_BBOX_H // <freetype/ftbbox.h>
|
||||
#include <lunasvg.h>
|
||||
#endif
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
#include <plutosvg.h>
|
||||
#endif
|
||||
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined (IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||
#if !((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
|
||||
#error IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG requires FreeType version >= 2.12
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning (push)
|
||||
#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff)
|
||||
#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
|
||||
#endif
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
|
||||
#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used
|
||||
#ifndef __clang__
|
||||
#pragma GCC diagnostic ignored "-Wsubobject-linkage" // warning: 'xxxx' has a field 'xxxx' whose type uses the anonymous namespace
|
||||
#endif
|
||||
#endif
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Data
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
// Default memory allocators
|
||||
static void* ImGuiFreeTypeDefaultAllocFunc(size_t size, void* user_data) { IM_UNUSED(user_data); return IM_ALLOC(size); }
|
||||
static void ImGuiFreeTypeDefaultFreeFunc(void* ptr, void* user_data) { IM_UNUSED(user_data); IM_FREE(ptr); }
|
||||
|
||||
// Current memory allocators
|
||||
static void* (*GImGuiFreeTypeAllocFunc)(size_t size, void* user_data) = ImGuiFreeTypeDefaultAllocFunc;
|
||||
static void (*GImGuiFreeTypeFreeFunc)(void* ptr, void* user_data) = ImGuiFreeTypeDefaultFreeFunc;
|
||||
static void* GImGuiFreeTypeAllocatorUserData = nullptr;
|
||||
|
||||
// Lunasvg support
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* state);
|
||||
static void ImGuiLunasvgPortFree(FT_Pointer* state);
|
||||
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state);
|
||||
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state);
|
||||
#endif
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Code
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
#define FT_CEIL(X) (((X + 63) & -64) / 64) // From SDL_ttf: Handy routines for converting from fixed point
|
||||
#define FT_SCALEFACTOR 64.0f
|
||||
|
||||
// Glyph metrics:
|
||||
// --------------
|
||||
//
|
||||
// xmin xmax
|
||||
// | |
|
||||
// |<-------- width -------->|
|
||||
// | |
|
||||
// | +-------------------------+----------------- ymax
|
||||
// | | ggggggggg ggggg | ^ ^
|
||||
// | | g:::::::::ggg::::g | | |
|
||||
// | | g:::::::::::::::::g | | |
|
||||
// | | g::::::ggggg::::::gg | | |
|
||||
// | | g:::::g g:::::g | | |
|
||||
// offsetX -|-------->| g:::::g g:::::g | offsetY |
|
||||
// | | g:::::g g:::::g | | |
|
||||
// | | g::::::g g:::::g | | |
|
||||
// | | g:::::::ggggg:::::g | | |
|
||||
// | | g::::::::::::::::g | | height
|
||||
// | | gg::::::::::::::g | | |
|
||||
// baseline ---*---------|---- gggggggg::::::g-----*-------- |
|
||||
// / | | g:::::g | |
|
||||
// origin | | gggggg g:::::g | |
|
||||
// | | g:::::gg gg:::::g | |
|
||||
// | | g::::::ggg:::::::g | |
|
||||
// | | gg:::::::::::::g | |
|
||||
// | | ggg::::::ggg | |
|
||||
// | | gggggg | v
|
||||
// | +-------------------------+----------------- ymin
|
||||
// | |
|
||||
// |------------- advanceX ----------->|
|
||||
|
||||
// Stored in ImFontAtlas::FontLoaderData. ALLOCATED BY US.
|
||||
struct ImGui_ImplFreeType_Data
|
||||
{
|
||||
FT_Library Library;
|
||||
FT_MemoryRec_ MemoryManager;
|
||||
ImGui_ImplFreeType_Data() { memset((void*)this, 0, sizeof(*this)); }
|
||||
};
|
||||
|
||||
// Stored in ImFontConfig::FontLoaderData. ALLOCATED BY US.
|
||||
struct ImGui_ImplFreeType_FontSrcData
|
||||
{
|
||||
// Initialize from an external data buffer. Doesn't copy data, and you must ensure it stays valid up to this object lifetime.
|
||||
bool InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_user_flags);
|
||||
void CloseFont();
|
||||
ImGui_ImplFreeType_FontSrcData() { memset((void*)this, 0, sizeof(*this)); }
|
||||
~ImGui_ImplFreeType_FontSrcData() { CloseFont(); }
|
||||
|
||||
// Members
|
||||
FT_Face FtFace;
|
||||
ImGuiFreeTypeLoaderFlags UserFlags; // = ImFontConfig::FontLoaderFlags
|
||||
FT_Int32 LoadFlags;
|
||||
ImFontBaked* BakedLastActivated;
|
||||
};
|
||||
|
||||
// Stored in ImFontBaked::FontLoaderDatas: pointer to SourcesCount instances of this. ALLOCATED BY CORE.
|
||||
struct ImGui_ImplFreeType_FontSrcBakedData
|
||||
{
|
||||
FT_Size FtSize; // This represent a FT_Face with a given size.
|
||||
ImGui_ImplFreeType_FontSrcBakedData() { memset((void*)this, 0, sizeof(*this)); }
|
||||
};
|
||||
|
||||
bool ImGui_ImplFreeType_FontSrcData::InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_font_loader_flags)
|
||||
{
|
||||
FT_Error error = FT_New_Memory_Face(ft_library, (const FT_Byte*)src->FontData, (FT_Long)src->FontDataSize, (FT_Long)src->FontNo, &FtFace);
|
||||
if (error != 0)
|
||||
return false;
|
||||
error = FT_Select_Charmap(FtFace, FT_ENCODING_UNICODE);
|
||||
if (error != 0)
|
||||
return false;
|
||||
|
||||
// Convert to FreeType flags (NB: Bold and Oblique are processed separately)
|
||||
UserFlags = (ImGuiFreeTypeLoaderFlags)(src->FontLoaderFlags | extra_font_loader_flags);
|
||||
|
||||
LoadFlags = 0;
|
||||
if ((UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) == 0)
|
||||
LoadFlags |= FT_LOAD_NO_BITMAP;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoHinting)
|
||||
LoadFlags |= FT_LOAD_NO_HINTING;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoAutoHint)
|
||||
LoadFlags |= FT_LOAD_NO_AUTOHINT;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_ForceAutoHint)
|
||||
LoadFlags |= FT_LOAD_FORCE_AUTOHINT;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_LightHinting)
|
||||
LoadFlags |= FT_LOAD_TARGET_LIGHT;
|
||||
else if (UserFlags & ImGuiFreeTypeLoaderFlags_MonoHinting)
|
||||
LoadFlags |= FT_LOAD_TARGET_MONO;
|
||||
else
|
||||
LoadFlags |= FT_LOAD_TARGET_NORMAL;
|
||||
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_LoadColor)
|
||||
LoadFlags |= FT_LOAD_COLOR;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplFreeType_FontSrcData::CloseFont()
|
||||
{
|
||||
if (FtFace)
|
||||
{
|
||||
FT_Done_Face(FtFace);
|
||||
FtFace = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static const FT_Glyph_Metrics* ImGui_ImplFreeType_LoadGlyph(ImGui_ImplFreeType_FontSrcData* src_data, uint32_t codepoint)
|
||||
{
|
||||
uint32_t glyph_index = FT_Get_Char_Index(src_data->FtFace, codepoint);
|
||||
if (glyph_index == 0)
|
||||
return nullptr;
|
||||
|
||||
// If this crash for you: FreeType 2.11.0 has a crash bug on some bitmap/colored fonts.
|
||||
// - https://gitlab.freedesktop.org/freetype/freetype/-/issues/1076
|
||||
// - https://github.com/ocornut/imgui/issues/4567
|
||||
// - https://github.com/ocornut/imgui/issues/4566
|
||||
// You can use FreeType 2.10, or the patched version of 2.11.0 in VcPkg, or probably any upcoming FreeType version.
|
||||
FT_Error error = FT_Load_Glyph(src_data->FtFace, glyph_index, src_data->LoadFlags);
|
||||
if (error)
|
||||
return nullptr;
|
||||
|
||||
// Need an outline for this to work
|
||||
FT_GlyphSlot slot = src_data->FtFace->glyph;
|
||||
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP || slot->format == FT_GLYPH_FORMAT_SVG);
|
||||
#else
|
||||
#if ((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
|
||||
IM_ASSERT(slot->format != FT_GLYPH_FORMAT_SVG && "The font contains SVG glyphs, you'll need to enable IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG in imconfig.h and install required libraries in order to use this font");
|
||||
#endif
|
||||
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP);
|
||||
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
|
||||
// Apply convenience transform (this is not picking from real "Bold"/"Italic" fonts! Merely applying FreeType helper transform. Oblique == Slanting)
|
||||
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bold)
|
||||
FT_GlyphSlot_Embolden(slot);
|
||||
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Oblique)
|
||||
{
|
||||
FT_GlyphSlot_Oblique(slot);
|
||||
//FT_BBox bbox;
|
||||
//FT_Outline_Get_BBox(&slot->outline, &bbox);
|
||||
//slot->metrics.width = bbox.xMax - bbox.xMin;
|
||||
//slot->metrics.height = bbox.yMax - bbox.yMin;
|
||||
}
|
||||
|
||||
return &slot->metrics;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_BlitGlyph(const FT_Bitmap* ft_bitmap, uint32_t* dst, uint32_t dst_pitch)
|
||||
{
|
||||
IM_ASSERT(ft_bitmap != nullptr);
|
||||
const uint32_t w = ft_bitmap->width;
|
||||
const uint32_t h = ft_bitmap->rows;
|
||||
const uint8_t* src = ft_bitmap->buffer;
|
||||
const uint32_t src_pitch = ft_bitmap->pitch;
|
||||
|
||||
switch (ft_bitmap->pixel_mode)
|
||||
{
|
||||
case FT_PIXEL_MODE_GRAY: // Grayscale image, 1 byte per pixel.
|
||||
{
|
||||
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||
for (uint32_t x = 0; x < w; x++)
|
||||
dst[x] = IM_COL32(255, 255, 255, src[x]);
|
||||
break;
|
||||
}
|
||||
case FT_PIXEL_MODE_MONO: // Monochrome image, 1 bit per pixel. The bits in each byte are ordered from MSB to LSB.
|
||||
{
|
||||
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||
{
|
||||
uint8_t bits = 0;
|
||||
const uint8_t* bits_ptr = src;
|
||||
for (uint32_t x = 0; x < w; x++, bits <<= 1)
|
||||
{
|
||||
if ((x & 7) == 0)
|
||||
bits = *bits_ptr++;
|
||||
dst[x] = IM_COL32(255, 255, 255, (bits & 0x80) ? 255 : 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FT_PIXEL_MODE_BGRA:
|
||||
{
|
||||
// FIXME: Converting pre-multiplied alpha to straight. Doesn't smell good.
|
||||
#define DE_MULTIPLY(color, alpha) ImMin((ImU32)(255.0f * (float)color / (float)(alpha + FLT_MIN) + 0.5f), 255u)
|
||||
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||
for (uint32_t x = 0; x < w; x++)
|
||||
{
|
||||
uint8_t r = src[x * 4 + 2], g = src[x * 4 + 1], b = src[x * 4], a = src[x * 4 + 3];
|
||||
dst[x] = IM_COL32(DE_MULTIPLY(r, a), DE_MULTIPLY(g, a), DE_MULTIPLY(b, a), a);
|
||||
}
|
||||
#undef DE_MULTIPLY
|
||||
break;
|
||||
}
|
||||
default:
|
||||
IM_ASSERT(0 && "FreeTypeFont::BlitGlyph(): Unknown bitmap pixel mode!");
|
||||
}
|
||||
}
|
||||
|
||||
// FreeType memory allocation callbacks
|
||||
static void* FreeType_Alloc(FT_Memory /*memory*/, long size)
|
||||
{
|
||||
return GImGuiFreeTypeAllocFunc((size_t)size, GImGuiFreeTypeAllocatorUserData);
|
||||
}
|
||||
|
||||
static void FreeType_Free(FT_Memory /*memory*/, void* block)
|
||||
{
|
||||
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||
}
|
||||
|
||||
static void* FreeType_Realloc(FT_Memory /*memory*/, long cur_size, long new_size, void* block)
|
||||
{
|
||||
// Implement realloc() as we don't ask user to provide it.
|
||||
if (block == nullptr)
|
||||
return GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
|
||||
|
||||
if (new_size == 0)
|
||||
{
|
||||
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (new_size > cur_size)
|
||||
{
|
||||
void* new_block = GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
|
||||
memcpy(new_block, block, (size_t)cur_size);
|
||||
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||
return new_block;
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_LoaderInit(ImFontAtlas* atlas)
|
||||
{
|
||||
IM_ASSERT(atlas->FontLoaderData == nullptr);
|
||||
ImGui_ImplFreeType_Data* bd = IM_NEW(ImGui_ImplFreeType_Data)();
|
||||
|
||||
// FreeType memory management: https://www.freetype.org/freetype2/docs/design/design-4.html
|
||||
bd->MemoryManager.user = nullptr;
|
||||
bd->MemoryManager.alloc = &FreeType_Alloc;
|
||||
bd->MemoryManager.free = &FreeType_Free;
|
||||
bd->MemoryManager.realloc = &FreeType_Realloc;
|
||||
|
||||
// https://www.freetype.org/freetype2/docs/reference/ft2-module_management.html#FT_New_Library
|
||||
FT_Error error = FT_New_Library(&bd->MemoryManager, &bd->Library);
|
||||
if (error != 0)
|
||||
{
|
||||
IM_DELETE(bd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If you don't call FT_Add_Default_Modules() the rest of code may work, but FreeType won't use our custom allocator.
|
||||
FT_Add_Default_Modules(bd->Library);
|
||||
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
// Install svg hooks for FreeType
|
||||
// https://freetype.org/freetype2/docs/reference/ft2-properties.html#svg-hooks
|
||||
// https://freetype.org/freetype2/docs/reference/ft2-svg_fonts.html#svg_fonts
|
||||
SVG_RendererHooks hooks = { ImGuiLunasvgPortInit, ImGuiLunasvgPortFree, ImGuiLunasvgPortRender, ImGuiLunasvgPortPresetSlot };
|
||||
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", &hooks);
|
||||
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
// With plutosvg, use provided hooks
|
||||
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", plutosvg_ft_svg_hooks());
|
||||
#endif // IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
|
||||
// Store our data
|
||||
atlas->FontLoaderData = (void*)bd;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_LoaderShutdown(ImFontAtlas* atlas)
|
||||
{
|
||||
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
|
||||
IM_ASSERT(bd != nullptr);
|
||||
FT_Done_Library(bd->Library);
|
||||
IM_DELETE(bd);
|
||||
atlas->FontLoaderData = nullptr;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_FontSrcInit(ImFontAtlas* atlas, ImFontConfig* src)
|
||||
{
|
||||
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = IM_NEW(ImGui_ImplFreeType_FontSrcData);
|
||||
IM_ASSERT(src->FontLoaderData == nullptr);
|
||||
src->FontLoaderData = bd_font_data;
|
||||
|
||||
if (!bd_font_data->InitFont(bd->Library, src, (ImGuiFreeTypeLoaderFlags)atlas->FontLoaderFlags))
|
||||
{
|
||||
IM_DELETE(bd_font_data);
|
||||
src->FontLoaderData = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_FontSrcDestroy(ImFontAtlas* atlas, ImFontConfig* src)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
IM_DELETE(bd_font_data);
|
||||
src->FontLoaderData = nullptr;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
float size = baked->Size;
|
||||
if (src->MergeMode && src->SizePixels != 0.0f)
|
||||
size *= (src->SizePixels / baked->OwnerFont->Sources[0]->SizePixels);
|
||||
size *= src->ExtraSizeScale;
|
||||
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
bd_font_data->BakedLastActivated = baked;
|
||||
|
||||
// We use one FT_Size per (source + baked) combination.
|
||||
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||
IM_ASSERT(bd_baked_data != nullptr);
|
||||
IM_PLACEMENT_NEW(bd_baked_data) ImGui_ImplFreeType_FontSrcBakedData();
|
||||
|
||||
FT_New_Size(bd_font_data->FtFace, &bd_baked_data->FtSize);
|
||||
FT_Activate_Size(bd_baked_data->FtSize);
|
||||
|
||||
// Vuhdo 2017: "I'm not sure how to deal with font sizes properly. As far as I understand, currently ImGui assumes that the 'pixel_height'
|
||||
// is a maximum height of an any given glyph, i.e. it's the sum of font's ascender and descender. Seems strange to me.
|
||||
// FT_Set_Pixel_Sizes() doesn't seem to get us the same result."
|
||||
// (FT_Set_Pixel_Sizes() essentially calls FT_Request_Size() with FT_SIZE_REQUEST_TYPE_NOMINAL)
|
||||
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
|
||||
FT_Size_RequestRec req;
|
||||
req.type = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) ? FT_SIZE_REQUEST_TYPE_NOMINAL : FT_SIZE_REQUEST_TYPE_REAL_DIM;
|
||||
req.width = 0;
|
||||
req.height = (uint32_t)(size * 64 * rasterizer_density);
|
||||
req.horiResolution = 0;
|
||||
req.vertResolution = 0;
|
||||
FT_Request_Size(bd_font_data->FtFace, &req);
|
||||
|
||||
// Output
|
||||
if (src->MergeMode == false)
|
||||
{
|
||||
// Read metrics
|
||||
FT_Size_Metrics metrics = bd_baked_data->FtSize->metrics;
|
||||
const float scale = 1.0f / (rasterizer_density * src->ExtraSizeScale);
|
||||
baked->Ascent = (float)FT_CEIL(metrics.ascender) * scale; // The pixel extents above the baseline in pixels (typically positive).
|
||||
baked->Descent = (float)FT_CEIL(metrics.descender) * scale; // The extents below the baseline in pixels (typically negative).
|
||||
//LineSpacing = (float)FT_CEIL(metrics.height) * scale; // The baseline-to-baseline distance. Note that it usually is larger than the sum of the ascender and descender taken as absolute values. There is also no guarantee that no glyphs extend above or below subsequent baselines when using this distance. Think of it as a value the designer of the font finds appropriate.
|
||||
//LineGap = (float)FT_CEIL(metrics.height - metrics.ascender + metrics.descender) * scale; // The spacing in pixels between one row's descent and the next row's ascent.
|
||||
//MaxAdvanceWidth = (float)FT_CEIL(metrics.max_advance) * scale; // This field gives the maximum horizontal cursor advance for all glyphs in the font.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_FontBakedDestroy(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
IM_UNUSED(baked);
|
||||
IM_UNUSED(src);
|
||||
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||
IM_ASSERT(bd_baked_data != nullptr);
|
||||
FT_Done_Size(bd_baked_data->FtSize);
|
||||
bd_baked_data->~ImGui_ImplFreeType_FontSrcBakedData(); // ~IM_PLACEMENT_DELETE()
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x)
|
||||
{
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
uint32_t glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
|
||||
if (glyph_index == 0)
|
||||
return false;
|
||||
|
||||
if (bd_font_data->BakedLastActivated != baked) // <-- could use id
|
||||
{
|
||||
// Activate current size
|
||||
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||
FT_Activate_Size(bd_baked_data->FtSize);
|
||||
bd_font_data->BakedLastActivated = baked;
|
||||
}
|
||||
|
||||
const FT_Glyph_Metrics* metrics = ImGui_ImplFreeType_LoadGlyph(bd_font_data, codepoint);
|
||||
if (metrics == nullptr)
|
||||
return false;
|
||||
|
||||
FT_Face face = bd_font_data->FtFace;
|
||||
FT_GlyphSlot slot = face->glyph;
|
||||
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
|
||||
|
||||
// Load metrics only mode
|
||||
const float advance_x = (slot->advance.x / FT_SCALEFACTOR) / rasterizer_density;
|
||||
if (out_advance_x != NULL)
|
||||
{
|
||||
IM_ASSERT(out_glyph == NULL);
|
||||
*out_advance_x = advance_x;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Render glyph into a bitmap (currently held by FreeType)
|
||||
FT_Render_Mode render_mode = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Monochrome) ? FT_RENDER_MODE_MONO : FT_RENDER_MODE_NORMAL;
|
||||
FT_Error error = FT_Render_Glyph(slot, render_mode);
|
||||
const FT_Bitmap* ft_bitmap = &slot->bitmap;
|
||||
if (error != 0 || ft_bitmap == nullptr)
|
||||
return false;
|
||||
|
||||
const int w = (int)ft_bitmap->width;
|
||||
const int h = (int)ft_bitmap->rows;
|
||||
const bool is_visible = (w != 0 && h != 0);
|
||||
|
||||
// Prepare glyph
|
||||
out_glyph->Codepoint = codepoint;
|
||||
out_glyph->AdvanceX = advance_x;
|
||||
|
||||
// Pack and retrieve position inside texture atlas
|
||||
if (is_visible)
|
||||
{
|
||||
ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, w, h);
|
||||
if (pack_id == ImFontAtlasRectId_Invalid)
|
||||
{
|
||||
// Pathological out of memory case (TexMaxWidth/TexMaxHeight set too small?)
|
||||
IM_ASSERT(pack_id != ImFontAtlasRectId_Invalid && "Out of texture memory.");
|
||||
return false;
|
||||
}
|
||||
ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id);
|
||||
|
||||
// Render pixels to our temporary buffer
|
||||
atlas->Builder->TempBuffer.resize(w * h * 4);
|
||||
uint32_t* temp_buffer = (uint32_t*)atlas->Builder->TempBuffer.Data;
|
||||
ImGui_ImplFreeType_BlitGlyph(ft_bitmap, temp_buffer, w);
|
||||
|
||||
const float ref_size = baked->OwnerFont->Sources[0]->SizePixels;
|
||||
const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f;
|
||||
float font_off_x = ImFloor(src->GlyphOffset.x * offsets_scale + 0.5f); // Snap scaled offset.
|
||||
float font_off_y = ImFloor(src->GlyphOffset.y * offsets_scale + 0.5f) + baked->Ascent;
|
||||
float recip_h = 1.0f / rasterizer_density;
|
||||
float recip_v = 1.0f / rasterizer_density;
|
||||
|
||||
// Register glyph
|
||||
float glyph_off_x = (float)face->glyph->bitmap_left;
|
||||
float glyph_off_y = (float)-face->glyph->bitmap_top;
|
||||
out_glyph->X0 = glyph_off_x * recip_h + font_off_x;
|
||||
out_glyph->Y0 = glyph_off_y * recip_v + font_off_y;
|
||||
out_glyph->X1 = (glyph_off_x + w) * recip_h + font_off_x;
|
||||
out_glyph->Y1 = (glyph_off_y + h) * recip_v + font_off_y;
|
||||
out_glyph->Visible = true;
|
||||
out_glyph->Colored = (ft_bitmap->pixel_mode == FT_PIXEL_MODE_BGRA);
|
||||
out_glyph->PackId = pack_id;
|
||||
ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, out_glyph, r, (const unsigned char*)temp_buffer, ImTextureFormat_RGBA32, w * 4);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreetype_FontSrcContainsGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImWchar codepoint)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
int glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
|
||||
return glyph_index != 0;
|
||||
}
|
||||
|
||||
const ImFontLoader* ImGuiFreeType::GetFontLoader()
|
||||
{
|
||||
static ImFontLoader loader;
|
||||
loader.Name = "FreeType";
|
||||
loader.LoaderInit = ImGui_ImplFreeType_LoaderInit;
|
||||
loader.LoaderShutdown = ImGui_ImplFreeType_LoaderShutdown;
|
||||
loader.FontSrcInit = ImGui_ImplFreeType_FontSrcInit;
|
||||
loader.FontSrcDestroy = ImGui_ImplFreeType_FontSrcDestroy;
|
||||
loader.FontSrcContainsGlyph = ImGui_ImplFreetype_FontSrcContainsGlyph;
|
||||
loader.FontBakedInit = ImGui_ImplFreeType_FontBakedInit;
|
||||
loader.FontBakedDestroy = ImGui_ImplFreeType_FontBakedDestroy;
|
||||
loader.FontBakedLoadGlyph = ImGui_ImplFreeType_FontBakedLoadGlyph;
|
||||
loader.FontBakedSrcLoaderDataSize = sizeof(ImGui_ImplFreeType_FontSrcBakedData);
|
||||
return &loader;
|
||||
}
|
||||
|
||||
void ImGuiFreeType::SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data)
|
||||
{
|
||||
GImGuiFreeTypeAllocFunc = alloc_func;
|
||||
GImGuiFreeTypeFreeFunc = free_func;
|
||||
GImGuiFreeTypeAllocatorUserData = user_data;
|
||||
}
|
||||
|
||||
bool ImGuiFreeType::DebugEditFontLoaderFlags(unsigned int* p_font_loader_flags)
|
||||
{
|
||||
bool edited = false;
|
||||
edited |= ImGui::CheckboxFlags("NoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoHinting);
|
||||
edited |= ImGui::CheckboxFlags("NoAutoHint", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoAutoHint);
|
||||
edited |= ImGui::CheckboxFlags("ForceAutoHint",p_font_loader_flags, ImGuiFreeTypeLoaderFlags_ForceAutoHint);
|
||||
edited |= ImGui::CheckboxFlags("LightHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LightHinting);
|
||||
edited |= ImGui::CheckboxFlags("MonoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_MonoHinting);
|
||||
edited |= ImGui::CheckboxFlags("Bold", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bold);
|
||||
edited |= ImGui::CheckboxFlags("Oblique", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Oblique);
|
||||
edited |= ImGui::CheckboxFlags("Monochrome", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Monochrome);
|
||||
edited |= ImGui::CheckboxFlags("LoadColor", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LoadColor);
|
||||
edited |= ImGui::CheckboxFlags("Bitmap", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bitmap);
|
||||
return edited;
|
||||
}
|
||||
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
// For more details, see https://gitlab.freedesktop.org/freetype/freetype-demos/-/blob/master/src/rsvg-port.c
|
||||
// The original code from the demo is licensed under CeCILL-C Free Software License Agreement (https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/LICENSE.TXT)
|
||||
struct LunasvgPortState
|
||||
{
|
||||
FT_Error err = FT_Err_Ok;
|
||||
lunasvg::Matrix matrix;
|
||||
std::unique_ptr<lunasvg::Document> svg = nullptr;
|
||||
};
|
||||
|
||||
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* _state)
|
||||
{
|
||||
*_state = IM_NEW(LunasvgPortState)();
|
||||
return FT_Err_Ok;
|
||||
}
|
||||
|
||||
static void ImGuiLunasvgPortFree(FT_Pointer* _state)
|
||||
{
|
||||
IM_DELETE(*(LunasvgPortState**)_state);
|
||||
}
|
||||
|
||||
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state)
|
||||
{
|
||||
LunasvgPortState* state = *(LunasvgPortState**)_state;
|
||||
|
||||
// If there was an error while loading the svg in ImGuiLunasvgPortPresetSlot(), the renderer hook still get called, so just returns the error.
|
||||
if (state->err != FT_Err_Ok)
|
||||
return state->err;
|
||||
|
||||
// rows is height, pitch (or stride) equals to width * sizeof(int32)
|
||||
lunasvg::Bitmap bitmap((uint8_t*)slot->bitmap.buffer, slot->bitmap.width, slot->bitmap.rows, slot->bitmap.pitch);
|
||||
#if LUNASVG_VERSION_MAJOR >= 3
|
||||
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
|
||||
#else
|
||||
state->svg->setMatrix(state->svg->matrix().identity()); // Reset the svg matrix to the default value
|
||||
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
|
||||
#endif
|
||||
state->err = FT_Err_Ok;
|
||||
return state->err;
|
||||
}
|
||||
|
||||
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state)
|
||||
{
|
||||
FT_SVG_Document document = (FT_SVG_Document)slot->other;
|
||||
LunasvgPortState* state = *(LunasvgPortState**)_state;
|
||||
FT_Size_Metrics& metrics = document->metrics;
|
||||
|
||||
// This function is called twice, once in the FT_Load_Glyph() and another right before ImGuiLunasvgPortRender().
|
||||
// If it's the latter, don't do anything because it's // already done in the former.
|
||||
if (cache)
|
||||
return state->err;
|
||||
|
||||
state->svg = lunasvg::Document::loadFromData((const char*)document->svg_document, document->svg_document_length);
|
||||
if (state->svg == nullptr)
|
||||
{
|
||||
state->err = FT_Err_Invalid_SVG_Document;
|
||||
return state->err;
|
||||
}
|
||||
|
||||
#if LUNASVG_VERSION_MAJOR >= 3
|
||||
lunasvg::Box box = state->svg->boundingBox();
|
||||
#else
|
||||
lunasvg::Box box = state->svg->box();
|
||||
#endif
|
||||
double scale = std::min(metrics.x_ppem / box.w, metrics.y_ppem / box.h);
|
||||
double xx = (double)document->transform.xx / (1 << 16);
|
||||
double xy = -(double)document->transform.xy / (1 << 16);
|
||||
double yx = -(double)document->transform.yx / (1 << 16);
|
||||
double yy = (double)document->transform.yy / (1 << 16);
|
||||
double x0 = (double)document->delta.x / 64 * box.w / metrics.x_ppem;
|
||||
double y0 = -(double)document->delta.y / 64 * box.h / metrics.y_ppem;
|
||||
|
||||
#if LUNASVG_VERSION_MAJOR >= 3
|
||||
// Scale, transform and pre-translate the matrix for the rendering step
|
||||
state->matrix = lunasvg::Matrix::translated(-box.x, -box.y);
|
||||
state->matrix.multiply(lunasvg::Matrix(xx, xy, yx, yy, x0, y0));
|
||||
state->matrix.scale(scale, scale);
|
||||
|
||||
// Apply updated transformation to the bounding box
|
||||
box.transform(state->matrix);
|
||||
#else
|
||||
// Scale and transform, we don't translate the svg yet
|
||||
state->matrix.identity();
|
||||
state->matrix.scale(scale, scale);
|
||||
state->matrix.transform(xx, xy, yx, yy, x0, y0);
|
||||
state->svg->setMatrix(state->matrix);
|
||||
|
||||
// Pre-translate the matrix for the rendering step
|
||||
state->matrix.translate(-box.x, -box.y);
|
||||
|
||||
// Get the box again after the transformation
|
||||
box = state->svg->box();
|
||||
#endif
|
||||
|
||||
// Calculate the bitmap size
|
||||
slot->bitmap_left = FT_Int(box.x);
|
||||
slot->bitmap_top = FT_Int(-box.y);
|
||||
slot->bitmap.rows = (unsigned int)(ImCeil((float)box.h));
|
||||
slot->bitmap.width = (unsigned int)(ImCeil((float)box.w));
|
||||
slot->bitmap.pitch = slot->bitmap.width * 4;
|
||||
slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA;
|
||||
|
||||
// Compute all the bearings and set them correctly. The outline is scaled already, we just need to use the bounding box.
|
||||
double metrics_width = box.w;
|
||||
double metrics_height = box.h;
|
||||
double horiBearingX = box.x;
|
||||
double horiBearingY = -box.y;
|
||||
double vertBearingX = slot->metrics.horiBearingX / 64.0 - slot->metrics.horiAdvance / 64.0 / 2.0;
|
||||
double vertBearingY = (slot->metrics.vertAdvance / 64.0 - slot->metrics.height / 64.0) / 2.0;
|
||||
slot->metrics.width = FT_Pos(IM_ROUND(metrics_width * 64.0)); // Using IM_ROUND() assume width and height are positive
|
||||
slot->metrics.height = FT_Pos(IM_ROUND(metrics_height * 64.0));
|
||||
slot->metrics.horiBearingX = FT_Pos(horiBearingX * 64);
|
||||
slot->metrics.horiBearingY = FT_Pos(horiBearingY * 64);
|
||||
slot->metrics.vertBearingX = FT_Pos(vertBearingX * 64);
|
||||
slot->metrics.vertBearingY = FT_Pos(vertBearingY * 64);
|
||||
|
||||
if (slot->metrics.vertAdvance == 0)
|
||||
slot->metrics.vertAdvance = FT_Pos(metrics_height * 1.2 * 64.0);
|
||||
|
||||
state->err = FT_Err_Ok;
|
||||
return state->err;
|
||||
}
|
||||
|
||||
#endif // #ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning (pop)
|
||||
#endif
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
83
libs/imgui/misc/freetype/imgui_freetype.h
Normal file
83
libs/imgui/misc/freetype/imgui_freetype.h
Normal file
@@ -0,0 +1,83 @@
|
||||
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
|
||||
// (headers)
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
// Usage:
|
||||
// - Add '#define IMGUI_ENABLE_FREETYPE' in your imconfig to automatically enable support
|
||||
// for imgui_freetype in imgui. It is equivalent to selecting the default loader with:
|
||||
// io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())
|
||||
|
||||
// Optional support for OpenType SVG fonts:
|
||||
// - Add '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG' to use plutosvg (not provided). See #7927.
|
||||
// - Add '#define IMGUI_ENABLE_FREETYPE_LUNASVG' to use lunasvg (not provided). See #6591.
|
||||
|
||||
// Forward declarations
|
||||
struct ImFontAtlas;
|
||||
struct ImFontLoader;
|
||||
|
||||
// Hinting greatly impacts visuals (and glyph sizes).
|
||||
// - By default, hinting is enabled and the font's native hinter is preferred over the auto-hinter.
|
||||
// - When disabled, FreeType generates blurrier glyphs, more or less matches the stb_truetype.h
|
||||
// - The Default hinting mode usually looks good, but may distort glyphs in an unusual way.
|
||||
// - The Light hinting mode generates fuzzier glyphs but better matches Microsoft's rasterizer.
|
||||
// You can set those flags globally in ImFontAtlas::FontLoaderFlags
|
||||
// You can set those flags on a per font basis in ImFontConfig::FontLoaderFlags
|
||||
typedef unsigned int ImGuiFreeTypeLoaderFlags;
|
||||
enum ImGuiFreeTypeLoaderFlags_
|
||||
{
|
||||
ImGuiFreeTypeLoaderFlags_NoHinting = 1 << 0, // Disable hinting. This generally generates 'blurrier' bitmap glyphs when the glyph are rendered in any of the anti-aliased modes.
|
||||
ImGuiFreeTypeLoaderFlags_NoAutoHint = 1 << 1, // Disable auto-hinter.
|
||||
ImGuiFreeTypeLoaderFlags_ForceAutoHint = 1 << 2, // Indicates that the auto-hinter is preferred over the font's native hinter.
|
||||
ImGuiFreeTypeLoaderFlags_LightHinting = 1 << 3, // A lighter hinting algorithm for gray-level modes. Many generated glyphs are fuzzier but better resemble their original shape. This is achieved by snapping glyphs to the pixel grid only vertically (Y-axis), as is done by Microsoft's ClearType and Adobe's proprietary font renderer. This preserves inter-glyph spacing in horizontal text.
|
||||
ImGuiFreeTypeLoaderFlags_MonoHinting = 1 << 4, // Strong hinting algorithm that should only be used for monochrome output.
|
||||
ImGuiFreeTypeLoaderFlags_Bold = 1 << 5, // Styling: Should we artificially embolden the font?
|
||||
ImGuiFreeTypeLoaderFlags_Oblique = 1 << 6, // Styling: Should we slant the font, emulating italic style?
|
||||
ImGuiFreeTypeLoaderFlags_Monochrome = 1 << 7, // Disable anti-aliasing. Combine this with MonoHinting for best results!
|
||||
ImGuiFreeTypeLoaderFlags_LoadColor = 1 << 8, // Enable FreeType color-layered glyphs
|
||||
ImGuiFreeTypeLoaderFlags_Bitmap = 1 << 9, // Enable FreeType bitmap glyphs
|
||||
|
||||
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
ImGuiFreeTypeBuilderFlags_NoHinting = ImGuiFreeTypeLoaderFlags_NoHinting,
|
||||
ImGuiFreeTypeBuilderFlags_NoAutoHint = ImGuiFreeTypeLoaderFlags_NoAutoHint,
|
||||
ImGuiFreeTypeBuilderFlags_ForceAutoHint = ImGuiFreeTypeLoaderFlags_ForceAutoHint,
|
||||
ImGuiFreeTypeBuilderFlags_LightHinting = ImGuiFreeTypeLoaderFlags_LightHinting,
|
||||
ImGuiFreeTypeBuilderFlags_MonoHinting = ImGuiFreeTypeLoaderFlags_MonoHinting,
|
||||
ImGuiFreeTypeBuilderFlags_Bold = ImGuiFreeTypeLoaderFlags_Bold,
|
||||
ImGuiFreeTypeBuilderFlags_Oblique = ImGuiFreeTypeLoaderFlags_Oblique,
|
||||
ImGuiFreeTypeBuilderFlags_Monochrome = ImGuiFreeTypeLoaderFlags_Monochrome,
|
||||
ImGuiFreeTypeBuilderFlags_LoadColor = ImGuiFreeTypeLoaderFlags_LoadColor,
|
||||
ImGuiFreeTypeBuilderFlags_Bitmap = ImGuiFreeTypeLoaderFlags_Bitmap,
|
||||
#endif
|
||||
};
|
||||
|
||||
// Obsolete names (will be removed)
|
||||
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
typedef ImGuiFreeTypeLoaderFlags_ ImGuiFreeTypeBuilderFlags_;
|
||||
#endif
|
||||
|
||||
namespace ImGuiFreeType
|
||||
{
|
||||
// This is automatically assigned when using '#define IMGUI_ENABLE_FREETYPE'.
|
||||
// If you need to dynamically select between multiple builders:
|
||||
// - you can manually assign this builder with 'atlas->SetFontLoader(ImGuiFreeType::GetFontLoader())'
|
||||
// - prefer deep-copying this into your own ImFontLoader instance if you use hot-reloading that messes up static data.
|
||||
IMGUI_API const ImFontLoader* GetFontLoader();
|
||||
|
||||
// Override allocators. By default ImGuiFreeType will use IM_ALLOC()/IM_FREE()
|
||||
// However, as FreeType does lots of allocations we provide a way for the user to redirect it to a separate memory heap if desired.
|
||||
IMGUI_API void SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data = nullptr);
|
||||
|
||||
// Display UI to edit ImFontAtlas::FontLoaderFlags (shared) or ImFontConfig::FontLoaderFlags (single source)
|
||||
IMGUI_API bool DebugEditFontLoaderFlags(ImGuiFreeTypeLoaderFlags* p_font_loader_flags);
|
||||
|
||||
// Obsolete names (will be removed)
|
||||
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
//IMGUI_API const ImFontBuilderIO* GetBuilderForFreeType(); // Renamed/changed in 1.92. Change 'io.Fonts->FontBuilderIO = ImGuiFreeType::GetBuilderForFreeType()' to 'io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())' if you need runtime selection.
|
||||
//static inline bool BuildFontAtlas(ImFontAtlas* atlas, unsigned int flags = 0) { atlas->FontBuilderIO = GetBuilderForFreeType(); atlas->FontLoaderFlags = flags; return atlas->Build(); } // Prefer using '#define IMGUI_ENABLE_FREETYPE'
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
3278
libs/nanosvg/nanosvg.h
Normal file
3278
libs/nanosvg/nanosvg.h
Normal file
File diff suppressed because it is too large
Load Diff
1472
libs/nanosvg/nanosvgrast.h
Normal file
1472
libs/nanosvg/nanosvgrast.h
Normal file
File diff suppressed because it is too large
Load Diff
0
prebuilt-binaries/drg-xmrig/.gitkeep
Normal file
0
prebuilt-binaries/drg-xmrig/.gitkeep
Normal file
Binary file not shown.
BIN
res/fonts/TwemojiMozilla-Color.ttf
Normal file
BIN
res/fonts/TwemojiMozilla-Color.ttf
Normal file
Binary file not shown.
BIN
res/img/backgrounds/texture/jade_bg.png
Normal file
BIN
res/img/backgrounds/texture/jade_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
23
res/img/logos/logo_dragonx.svg
Normal file
23
res/img/logos/logo_dragonx.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #d82652;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
|
||||
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
|
||||
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
125
res/lang/de.json
125
res/lang/de.json
@@ -135,10 +135,30 @@
|
||||
"change_pass_title": "Passphrase ändern",
|
||||
"characters": "Zeichen",
|
||||
"chat": "Chat",
|
||||
"chat_accent_amber": "Bernstein",
|
||||
"chat_accent_blue": "Blau",
|
||||
"chat_accent_green": "Grün",
|
||||
"chat_accent_pink": "Rosa",
|
||||
"chat_accent_purple": "Lila",
|
||||
"chat_accent_theme": "Design",
|
||||
"chat_add_contact": "Kontakt hinzufügen",
|
||||
"chat_awaiting_key": "Warten auf Antwort",
|
||||
"chat_bubble_minimal": "Minimal",
|
||||
"chat_bubble_rounded": "Abgerundet",
|
||||
"chat_bubble_square": "Eckig",
|
||||
"chat_buffer_loading": "Chat-Puffer: …",
|
||||
"chat_buffer_preparing": "Chat-Puffer: bereite %d/%d vor…",
|
||||
"chat_buffer_ready": "Chat-Puffer: %d/%d bereit",
|
||||
"chat_buffer_sending": "Chat: sende %d Nachrichten…",
|
||||
"chat_buffer_sending_one": "Chat: sende %d Nachricht…",
|
||||
"chat_cancel": "Abbrechen",
|
||||
"chat_contact_added": "Kontakt hinzugefügt – benenne ihn in Kontakte um",
|
||||
"chat_contact_request": "kontaktanfrage",
|
||||
"chat_copy_address_tip": "Zum Kopieren der Adresse klicken",
|
||||
"chat_density_comfortable": "Komfortabel",
|
||||
"chat_density_compact": "Kompakt",
|
||||
"chat_emoji_color": "Farbig",
|
||||
"chat_emoji_mono": "Monochrom",
|
||||
"chat_emoji_search": "Emoji suchen",
|
||||
"chat_empty_hint": "Noch keine Unterhaltungen. Nachrichten, die du erhältst, erscheinen hier.",
|
||||
"chat_empty_start": "Starte eine mit \"Neue Unterhaltung\".",
|
||||
@@ -147,6 +167,7 @@
|
||||
"chat_export_done": "Unterhaltung exportiert",
|
||||
"chat_export_failed": "Exportdatei konnte nicht geschrieben werden.",
|
||||
"chat_export_warn": "Speichert die entschlüsselten Nachrichten als Klartext. Bewahre die Datei sicher auf.",
|
||||
"chat_filter": "Chat",
|
||||
"chat_hidden_toast": "Unterhaltung ausgeblendet – eine neue Nachricht holt sie zurück",
|
||||
"chat_hide": "Ausblenden",
|
||||
"chat_hide_hidden": "Ausgeblendete verbergen",
|
||||
@@ -154,21 +175,39 @@
|
||||
"chat_len_over": "Nachricht zu lang",
|
||||
"chat_locked_hint": "Entsperre deine Wallet, um deine Chats zu laden.",
|
||||
"chat_mute": "Stummschalten",
|
||||
"chat_new_button": "Neue Unterhaltung",
|
||||
"chat_new_button": "Neuer Chat",
|
||||
"chat_new_message": "Nachricht",
|
||||
"chat_new_message_toast": "Neue verschlüsselte Chat-Nachricht",
|
||||
"chat_new_send": "Anfrage senden",
|
||||
"chat_new_title": "Neue Unterhaltung",
|
||||
"chat_new_title": "Neuer Chat",
|
||||
"chat_new_zaddr": "z-Adresse des Empfängers",
|
||||
"chat_no_matches": "Keine Unterhaltungen entsprechen deiner Suche.",
|
||||
"chat_no_z_contacts": "Noch keine Kontakte mit geschützter Adresse",
|
||||
"chat_opt_bubble_accent": "Blasenfarbe",
|
||||
"chat_opt_bubble_style": "Blasenstil",
|
||||
"chat_opt_density": "Nachrichtendichte",
|
||||
"chat_opt_emoji": "Emoji-Stil",
|
||||
"chat_opt_enter_sends": "Eingabetaste sendet",
|
||||
"chat_opt_font_size": "Textgröße",
|
||||
"chat_opt_global_clock": "Globales Uhrzeitformat",
|
||||
"chat_opt_poll": "Abrufrate",
|
||||
"chat_opt_timestamp": "Zeitstempel",
|
||||
"chat_pick_contact": "Aus Kontakten wählen…",
|
||||
"chat_rename": "Kontakt umbenennen",
|
||||
"chat_rename_hint": "Kontaktname",
|
||||
"chat_renamed": "Kontakt umbenannt",
|
||||
"chat_retry": "Wiederholen",
|
||||
"chat_search": "Unterhaltungen durchsuchen",
|
||||
"chat_sec_appearance": "DARSTELLUNG",
|
||||
"chat_sec_messaging": "NACHRICHTEN",
|
||||
"chat_select_hint": "Wähle eine Unterhaltung aus, um sie anzuzeigen.",
|
||||
"chat_send": "Senden",
|
||||
"chat_send_failed": "nicht gesendet",
|
||||
"chat_sending": "senden…",
|
||||
"chat_settings_done": "Fertig",
|
||||
"chat_settings_section": "CHAT & KONTAKTE",
|
||||
"chat_settings_tip": "Chat anpassen",
|
||||
"chat_settings_title": "Chat-Einstellungen",
|
||||
"chat_show_hidden": "Ausgeblendete anzeigen",
|
||||
"chat_time_now": "jetzt",
|
||||
"chat_toast_compose_failed": "Nachricht konnte nicht erstellt werden (zu lang?).",
|
||||
@@ -179,9 +218,16 @@
|
||||
"chat_toast_request_compose_failed": "Kontaktanfrage konnte nicht erstellt werden (ungültige Adresse / ungültiger Text?).",
|
||||
"chat_toast_request_queued": "Kontaktanfrage in Warteschlange.",
|
||||
"chat_toast_waiting_reply": "Warte auf die Antwort des Kontakts, bevor du ihm schreiben kannst.",
|
||||
"chat_today": "Heute",
|
||||
"chat_ts_12h": "12-Stunden",
|
||||
"chat_ts_24h": "24-Stunden",
|
||||
"chat_ts_global": "Global folgen",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unhide": "Einblenden",
|
||||
"chat_unmute": "Stummschaltung aufheben",
|
||||
"chat_verify_key": "Identitätsschlüssel – zum Verifizieren vergleichen",
|
||||
"chat_waiting_reply": "Warte auf die Antwort dieses Kontakts – sobald er antwortet, kannst du ihm schreiben.",
|
||||
"chat_yesterday": "Gestern",
|
||||
"chat_you": "Du",
|
||||
"choose_icon": "Symbol wählen",
|
||||
"clear": "Leeren",
|
||||
@@ -193,6 +239,7 @@
|
||||
"click_copy_address": "Klicken zum Kopieren der Adresse",
|
||||
"click_copy_uri": "Klicken zum Kopieren der URI",
|
||||
"click_to_copy": "Klicken zum Kopieren",
|
||||
"clock_format": "Uhrzeitformat",
|
||||
"close": "Schließen",
|
||||
"conf_count": "%d Best.",
|
||||
"confirm_and_send": "Bestätigen & Senden",
|
||||
@@ -230,12 +277,18 @@
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Automatisch scrollen",
|
||||
"console_available_commands": "Verfügbare Befehle:",
|
||||
"console_backend_reference": "Backend-Befehlsreferenz",
|
||||
"console_backend_unavailable": "Kein Backend",
|
||||
"console_capturing_output": "Erfasse Daemon-Ausgabe...",
|
||||
"console_cat_advanced": "Erweitert",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Steuerung",
|
||||
"console_cat_keys": "Schlüssel & Sicherheit",
|
||||
"console_cat_mining": "Mining",
|
||||
"console_cat_network": "Netzwerk",
|
||||
"console_cat_raw_transactions": "Rohtransaktionen",
|
||||
"console_cat_send": "Senden",
|
||||
"console_cat_sync": "Synchronisierung",
|
||||
"console_cat_utility": "Dienstprogramme",
|
||||
"console_cat_wallet": "Wallet",
|
||||
"console_clear": "Leeren",
|
||||
@@ -269,11 +322,14 @@
|
||||
"console_help_help": " help - Diese Hilfe anzeigen",
|
||||
"console_help_setgenerate": " setgenerate - Mining steuern",
|
||||
"console_help_stop": " stop - Daemon stoppen",
|
||||
"console_last_error": "Letzter Fehler:",
|
||||
"console_line_count": "%zu Zeilen",
|
||||
"console_matches": "Treffer",
|
||||
"console_new_lines": "%d neue Zeilen",
|
||||
"console_no_daemon": "Kein Daemon",
|
||||
"console_no_output": "(keine Ausgabe)",
|
||||
"console_not_connected": "Fehler: Nicht mit Daemon verbunden",
|
||||
"console_not_connected_lite": "Fehler: Keine Wallet geöffnet",
|
||||
"console_quit_note": "'quit'/'exit' werden hier nicht benötigt — schließen Sie einfach das Fenster.",
|
||||
"console_ref_builds": "Ergibt",
|
||||
"console_ref_cancel": "Abbrechen",
|
||||
@@ -289,12 +345,14 @@
|
||||
"console_ref_run_confirm": "%s jetzt ausführen? Dies ist ein folgenreicher Befehl.",
|
||||
"console_ref_search_hint": "Nach Name oder Aufgabe suchen…",
|
||||
"console_ref_select_hint": "Wählen Sie einen Befehl, um zu sehen, was er tut.",
|
||||
"console_ref_value": "Wert",
|
||||
"console_rpc_reference": "RPC-Befehlsreferenz",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Konsolen-Scanline",
|
||||
"console_search_commands": "Befehle suchen...",
|
||||
"console_select_all": "Alles auswählen",
|
||||
"console_show_app_output": "[App]-Wallet-Protokollzeilen anzeigen",
|
||||
"console_show_backend_ref": "Backend-Befehlsreferenz anzeigen",
|
||||
"console_show_daemon_output": "Daemon-Ausgabe anzeigen",
|
||||
"console_show_errors_only": "Nur Fehler anzeigen",
|
||||
"console_show_rpc_ref": "RPC-Befehlsreferenz anzeigen",
|
||||
@@ -307,6 +365,7 @@
|
||||
"console_status_stopped": "Gestoppt",
|
||||
"console_status_stopping": "Stoppt",
|
||||
"console_status_unknown": "Unbekannt",
|
||||
"console_stop_confirm_node": "'stop' fährt den Node herunter und trennt die Wallet. Geben Sie zur Bestätigung erneut 'stop' ein.",
|
||||
"console_tab_completion": "Tab zur Vervollständigung",
|
||||
"console_text_colors": "Textfarben",
|
||||
"console_toggle_accents": "Farbakzente der Zeilen umschalten",
|
||||
@@ -334,8 +393,15 @@
|
||||
"contact_preview_name": "Kontaktname",
|
||||
"contact_wallet_loading": "Die Wallet lädt noch — aktiviere „In jeder Wallet anzeigen“ oder versuche es gleich erneut.",
|
||||
"contacts": "Kontakte",
|
||||
"contacts_avatar_shape": "Avatarform",
|
||||
"contacts_list_scale": "Listengröße",
|
||||
"contacts_search_no_match": "Keine passenden Kontakte",
|
||||
"contacts_search_placeholder": "Kontakte durchsuchen...",
|
||||
"contacts_settings_tip": "Kontakte anpassen",
|
||||
"contacts_settings_title": "Kontakteinstellungen",
|
||||
"contacts_shape_circle": "Kreis",
|
||||
"contacts_shape_square": "Quadrat",
|
||||
"contacts_shape_tab": "Reiter",
|
||||
"copied": "Kopiert!",
|
||||
"copy": "Kopieren",
|
||||
"copy_address": "Vollständige Adresse kopieren",
|
||||
@@ -578,6 +644,7 @@
|
||||
"lite_birthday_backup": "Geburtstag: %llu (auch diesen sichern)",
|
||||
"lite_birthday_hint": "Blockhöhe, ab der gescannt werden soll. Bei 0 belassen, falls unbekannt (langsamerer vollständiger Scan).",
|
||||
"lite_birthday_label": "Geburtsblock",
|
||||
"lite_console_backend_commands": "Backend-Befehle:",
|
||||
"lite_console_help_passthrough": "Jede andere Eingabe wird als Lite-Wallet-Konsolenbefehl ausgeführt.",
|
||||
"lite_copy": "Kopieren",
|
||||
"lite_could_not_write": "Konnte nicht schreiben ",
|
||||
@@ -595,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://ihr-lite-server",
|
||||
"lite_net_checking": "wird geprüft…",
|
||||
"lite_net_connected": "Verbunden",
|
||||
"lite_net_connecting": "Verbinde…",
|
||||
"lite_net_custom": "Benutzerdefiniert",
|
||||
"lite_net_disconnected": "Nicht verbunden",
|
||||
"lite_net_hidden_section": "Ausgeblendete Server",
|
||||
@@ -682,6 +750,9 @@
|
||||
"market_cap": "Marktkapitalisierung",
|
||||
"market_cap_short": "Kap.",
|
||||
"market_chart_loading": "Preisverlauf wird geladen",
|
||||
"market_col_name": "Name",
|
||||
"market_col_trend": "Trend",
|
||||
"market_col_value": "Wert",
|
||||
"market_iv_1d": "1T",
|
||||
"market_iv_1h": "1S",
|
||||
"market_iv_1m": "1M",
|
||||
@@ -690,13 +761,18 @@
|
||||
"market_no_history": "Kein Preisverlauf verfügbar",
|
||||
"market_no_price": "Keine Preisdaten",
|
||||
"market_now": "Jetzt",
|
||||
"market_opt_chart_style": "Diagrammstil",
|
||||
"market_pct_shielded": "%.0f%% Abgeschirmt",
|
||||
"market_portfolio": "PORTFOLIO",
|
||||
"market_price_loading": "Preisdaten werden geladen...",
|
||||
"market_price_unavailable": "Preisdaten nicht verfügbar",
|
||||
"market_refresh_price": "Preisdaten aktualisieren",
|
||||
"market_settings_tip": "Marktoptionen",
|
||||
"market_settings_title": "Markteinstellungen",
|
||||
"market_style_candle": "Zu Kerzenchart wechseln",
|
||||
"market_style_candle_label": "Kerzen",
|
||||
"market_style_line": "Zum Liniendiagramm wechseln",
|
||||
"market_style_line_label": "Linie",
|
||||
"market_trade_on": "Handeln auf %s",
|
||||
"market_updated": "\\xc2\\xb7 Aktualisiert %s",
|
||||
"market_vol_short": "Vol.",
|
||||
@@ -1000,9 +1076,9 @@
|
||||
"portfolio_spark_min": "Minute",
|
||||
"portfolio_spark_month": "Monat",
|
||||
"portfolio_spark_week": "Woche",
|
||||
"portfolio_style_compact": "Kompakte Zeilen",
|
||||
"portfolio_style_detailed": "Detaillierte Zeilen",
|
||||
"portfolio_style_featured": "Hervorgehobene Zeilen",
|
||||
"portfolio_style_compact": "Tabelle",
|
||||
"portfolio_style_detailed": "Karten",
|
||||
"portfolio_style_featured": "Hervorgehoben",
|
||||
"portfolio_style_label": "Portfolio-Stil",
|
||||
"portfolio_untitled": "Ohne Titel",
|
||||
"portfolio_wallet_loading": "Warte, bis die Wallet fertig geladen ist, um eine Gruppe hinzuzufügen.",
|
||||
@@ -1384,10 +1460,20 @@
|
||||
"tt_blur": "Unschärfe-Stärke (0%% = aus, 100%% = maximum)",
|
||||
"tt_change_pass": "Die Wallet-Verschlüsselungspassphrase ändern",
|
||||
"tt_change_pin": "Ihre Entsperr-PIN ändern",
|
||||
"tt_chat_bubble_accent": "Akzentfarbe für deine ausgehenden Nachrichtenblasen (oder dem aktuellen Theme folgen)",
|
||||
"tt_chat_bubble_style": "Form der Nachrichtenblase: abgerundet, eckig oder minimal (flach, randlos)",
|
||||
"tt_chat_density": "Abstand zwischen Nachrichten: Komfortabel fügt mehr Abstand hinzu; Kompakt zeigt mehr auf dem Bildschirm",
|
||||
"tt_chat_emoji_style": "Emoji als einfarbige Umrisse oder in voller Farbe darstellen",
|
||||
"tt_chat_enter_sends": "Wenn aktiviert, sendet Enter die Nachricht und Shift+Enter fügt einen Zeilenumbruch ein; wenn deaktiviert, fügt Enter einen Zeilenumbruch ein",
|
||||
"tt_chat_font_size": "Skaliere den Chat-Nachrichtentext von 0.8x bis 1.5x. Betrifft nur den Chat-Tab, nicht den Rest der App",
|
||||
"tt_chat_poll_rate": "Wie oft auf neue und 0-conf-Nachrichten geprüft wird (0.5-15 s). Schneller ist reaktionsfreudiger, verbraucht aber mehr CPU",
|
||||
"tt_chat_timestamp": "Zeitstempelformat nur für diesen Tab: der app-weiten Uhr folgen oder 24-hour bzw. 12-hour erzwingen",
|
||||
"tt_clear_ztx": "Lokal zwischengespeicherten Z-Transaktionsverlauf löschen",
|
||||
"tt_clock_format": "24- oder 12-Stunden-Uhr, app-weit. Der Chat-Tab kann sie überschreiben.",
|
||||
"tt_custom_fees": "Manuelle Gebühreneingabe beim Senden von Transaktionen aktivieren",
|
||||
"tt_custom_theme": "Benutzerdefiniertes Theme aktiv",
|
||||
"tt_daemon_install_bundled": "Node stoppen, den installierten dragonxd mit der in diesem Wallet-Build enthaltenen Version überschreiben und dann neu starten",
|
||||
"tt_daemon_refresh": "Version, Größe und Datum des installierten und mitgelieferten dragonxd (oben angezeigt) erneut einlesen",
|
||||
"tt_daemon_update_check": "Den neuesten dragonxd-Full-Node vom Projekt-Gitea herunterladen und verifizieren, dann zum Anwenden neu starten",
|
||||
"tt_debug_collapse": "Debug-Protokollierungsoptionen einklappen",
|
||||
"tt_debug_expand": "Debug-Protokollierungsoptionen ausklappen",
|
||||
@@ -1405,7 +1491,30 @@
|
||||
"tt_keep_daemon": "Der Daemon wird beim Ausführen des Einrichtungsassistenten gestoppt",
|
||||
"tt_language": "Schnittstellensprache der Wallet-UI",
|
||||
"tt_layout_hotkey": "Hotkey: Links-/Rechts-Pfeiltasten zum Wechseln der Balance-Layouts",
|
||||
"tt_lite_copy": "Das angezeigte Geheimnis in die Zwischenablage kopieren",
|
||||
"tt_lite_decrypt_pass": "Gib deine Passphrase ein, um die Verschlüsselung von der Wallet zu entfernen",
|
||||
"tt_lite_encrypt": "Die Wallet mit der obigen Passphrase verschlüsseln; sie wird sofort gesperrt und benötigt die Passphrase zum Entsperren",
|
||||
"tt_lite_encrypt_pass": "Passphrase, mit der die Wallet verschlüsselt wird. Geht sie verloren, kann die Wallet nicht mehr entsperrt oder wiederhergestellt werden",
|
||||
"tt_lite_hide_wipe": "Das angezeigte Geheimnis ausblenden und sicher aus dem Speicher löschen",
|
||||
"tt_lite_import_key": "Einen privaten Ausgabe- oder Ansichtsschlüssel zum Importieren einfügen; dessen Verlauf erscheint nach der nächsten Synchronisierung",
|
||||
"tt_lite_import_key_btn": "Den eingegebenen privaten Schlüssel in diese Wallet importieren; Guthaben und Verlauf erscheinen nach der nächsten Synchronisierung",
|
||||
"tt_lite_lifecycle_op": "Wähle, ob eine neue Wallet erstellt, eine vorhandene geöffnet oder eine aus einer Seed-Phrase wiederhergestellt werden soll",
|
||||
"tt_lite_lifecycle_pass": "Passphrase, um die Wallet bei diesem Erstellen- / Öffnen- / Wiederherstellen-Vorgang zu entsperren oder zu setzen",
|
||||
"tt_lite_lifecycle_run": "Den ausgewählten Erstellen- / Öffnen- / Wiederherstellen-Vorgang mit den obigen Werten ausführen",
|
||||
"tt_lite_lifecycle_toggle": "Die Bedienelemente zum Erstellen / Öffnen / Wiederherstellen zur Verwaltung deiner Lite-Wallet-Datei ein- oder ausblenden",
|
||||
"tt_lite_lock": "Die Wallet jetzt sperren; zum Entsperren ist eine Passphrase erforderlich und jede Chat-Sitzung wird beendet",
|
||||
"tt_lite_redownload": "Alle Blöcke erneut vom Lite-Server herunterladen und neu scannen",
|
||||
"tt_lite_remove_encrypt": "Verschlüsselung entfernen und die Wallet ungeschützt speichern; zum Öffnen ist dann keine Passphrase mehr erforderlich",
|
||||
"tt_lite_restore_account": "HD-Konto-Index zum Wiederherstellen; belasse 0, sofern du nicht mehrere Konten unter diesem Seed verwendet hast",
|
||||
"tt_lite_restore_birthday": "Blockhöhe, bei der die Wallet erstellt wurde; das Scannen beginnt hier. Verwende 0 oder die früheste Höhe, falls unsicher",
|
||||
"tt_lite_restore_overwrite": "Eine vorhandene Wallet-Datei durch diese Wiederherstellung ersetzen. Warnung: überschreibt die aktuellen Wallet-Daten",
|
||||
"tt_lite_restore_seed": "Die 24-word-Wiederherstellungs-Seed-Phrase, aus der diese Wallet wiederhergestellt wird; bei der Eingabe ausgeblendet",
|
||||
"tt_lite_save_seed_file": "Seed und Erstellungsdatum in eine nur für den Eigentümer lesbare Datei (lite-seed-backup.txt) im Konfigurationsordner schreiben",
|
||||
"tt_lite_show_keys": "Die privaten Ausgabeschlüssel dieser Wallet anzeigen. Wer einen Schlüssel besitzt, kann das von ihm kontrollierte Guthaben ausgeben",
|
||||
"tt_lite_show_seed": "Die Wiederherstellungs-Seed-Phrase und das Erstellungsdatum dieser Wallet anzeigen. Wer den Seed besitzt, kann dein Guthaben ausgeben",
|
||||
"tt_lite_unlock": "Die verschlüsselte Wallet mit der obigen Passphrase entsperren",
|
||||
"tt_lite_unlock_pass": "Gib deine Passphrase ein, um die verschlüsselte Wallet zu entsperren",
|
||||
"tt_lite_wallet_path": "Pfad oder Name der Wallet-Datei, die geöffnet oder in die wiederhergestellt werden soll",
|
||||
"tt_lock": "Die Wallet sofort sperren",
|
||||
"tt_low_spec": "Alle aufwendigen visuellen Effekte deaktivieren\\nHotkey: Ctrl+Shift+Down",
|
||||
"tt_merge": "Mehrere UTXOs einer Adresse zusammenführen",
|
||||
@@ -1426,12 +1535,17 @@
|
||||
"tt_rpc_host": "Hostname des DragonX-Daemons",
|
||||
"tt_rpc_pass": "RPC-Authentifizierungspasswort",
|
||||
"tt_rpc_port": "Port für RPC-Verbindungen des Daemons",
|
||||
"tt_rpc_toggle": "Die schreibgeschützten RPC-Verbindungsdaten (Host, Port, Benutzer, Passwort) für den Daemon ein- oder ausblenden",
|
||||
"tt_rpc_user": "RPC-Authentifizierungsbenutzername",
|
||||
"tt_save_settings": "Alle Einstellungen auf der Festplatte speichern",
|
||||
"tt_save_ztx": "Z-Adresse-Transaktionsverlauf lokal für schnelleres Laden speichern",
|
||||
"tt_scan_themes": "Nach neuen Themes suchen.\\nTheme-Ordner ablegen in:\\n%s",
|
||||
"tt_scanline": "CRT-Scanlinieneffekt in der Konsole",
|
||||
"tt_screenshot_open_dir": "Den Screenshots-Ordner (unter dem Konfigurationsverzeichnis) im Dateimanager öffnen",
|
||||
"tt_screenshot_sweep": "Jedes Theme über jeden Tab durchlaufen und von jedem einen Screenshot in den Screenshots-Ordner der Konfiguration speichern (überschreibt den letzten Durchlauf)",
|
||||
"tt_screenshot_sweep_full": "Wie der Theme-Durchlauf, erfasst aber auch jedes Modal / jeden Dialog / jeden Ablauf mit temporären Offline-Demo-Wallet-Daten",
|
||||
"tt_seed_backup": "Die 24-Wort-Wiederherstellungsphrase Ihrer Wallet anzeigen und sichern",
|
||||
"tt_seed_demo_chat": "Beispielunterhaltungen in den Chat-Tab einfügen, damit ein Durchlauf dessen UI erfasst; nur im Speicher, beim Neustart verschwunden",
|
||||
"tt_seed_migrate": "Eine neue Wallet mit Wiederherstellungsphrase erstellen und Ihre Gelder dorthin übertragen",
|
||||
"tt_set_pin": "Eine 4-8-stellige PIN für schnelles Entsperren festlegen",
|
||||
"tt_shield_mining": "Transparente Mining-Belohnungen an eine geschirmte Adresse verschieben",
|
||||
@@ -1450,6 +1564,7 @@
|
||||
"tt_website": "Die DragonX-Website öffnen",
|
||||
"tt_window_opacity": "Hintergrund-Deckkraft (niedriger = Desktop durch Fenster sichtbar)",
|
||||
"tt_wizard": "Den Ersteinrichtungsassistenten erneut ausführen\\nDer Daemon wird neu gestartet",
|
||||
"tx_chat_badge": "Nachricht",
|
||||
"tx_confirmations": "%d Bestätigungen",
|
||||
"tx_details_title": "Transaktionsdetails",
|
||||
"tx_from_address": "Von Adresse:",
|
||||
|
||||
125
res/lang/es.json
125
res/lang/es.json
@@ -135,10 +135,30 @@
|
||||
"change_pass_title": "Cambiar frase de contraseña",
|
||||
"characters": "caracteres",
|
||||
"chat": "Chat",
|
||||
"chat_accent_amber": "Ámbar",
|
||||
"chat_accent_blue": "Azul",
|
||||
"chat_accent_green": "Verde",
|
||||
"chat_accent_pink": "Rosa",
|
||||
"chat_accent_purple": "Morado",
|
||||
"chat_accent_theme": "Tema",
|
||||
"chat_add_contact": "Añadir contacto",
|
||||
"chat_awaiting_key": "Esperando respuesta",
|
||||
"chat_bubble_minimal": "Mínima",
|
||||
"chat_bubble_rounded": "Redondeada",
|
||||
"chat_bubble_square": "Cuadrada",
|
||||
"chat_buffer_loading": "Búfer de chat: …",
|
||||
"chat_buffer_preparing": "Búfer de chat: preparando %d/%d…",
|
||||
"chat_buffer_ready": "Búfer de chat: %d/%d listos",
|
||||
"chat_buffer_sending": "Chat: enviando %d mensajes…",
|
||||
"chat_buffer_sending_one": "Chat: enviando %d mensaje…",
|
||||
"chat_cancel": "Cancelar",
|
||||
"chat_contact_added": "Contacto añadido: renómbralo en Contactos",
|
||||
"chat_contact_request": "solicitud de contacto",
|
||||
"chat_copy_address_tip": "Clic para copiar la dirección",
|
||||
"chat_density_comfortable": "Cómoda",
|
||||
"chat_density_compact": "Compacta",
|
||||
"chat_emoji_color": "Color",
|
||||
"chat_emoji_mono": "Monocromo",
|
||||
"chat_emoji_search": "Buscar emoji",
|
||||
"chat_empty_hint": "Aún no hay conversaciones. Los mensajes que recibas aparecerán aquí.",
|
||||
"chat_empty_start": "Inicia una con \"Nueva conversación\".",
|
||||
@@ -147,6 +167,7 @@
|
||||
"chat_export_done": "Conversación exportada",
|
||||
"chat_export_failed": "No se pudo escribir el archivo de exportación.",
|
||||
"chat_export_warn": "Guarda los mensajes descifrados como texto sin cifrar. Guarda el archivo de forma segura.",
|
||||
"chat_filter": "Chat",
|
||||
"chat_hidden_toast": "Conversación oculta: un mensaje nuevo la recupera",
|
||||
"chat_hide": "Ocultar",
|
||||
"chat_hide_hidden": "Ocultar ocultos",
|
||||
@@ -154,21 +175,39 @@
|
||||
"chat_len_over": "Mensaje demasiado largo",
|
||||
"chat_locked_hint": "Desbloquea tu monedero para cargar tus chats.",
|
||||
"chat_mute": "Silenciar",
|
||||
"chat_new_button": "Nueva conversación",
|
||||
"chat_new_button": "Nuevo chat",
|
||||
"chat_new_message": "Mensaje",
|
||||
"chat_new_message_toast": "Nuevo mensaje de chat cifrado",
|
||||
"chat_new_send": "Enviar solicitud",
|
||||
"chat_new_title": "Nueva conversación",
|
||||
"chat_new_title": "Nuevo chat",
|
||||
"chat_new_zaddr": "Dirección z del destinatario",
|
||||
"chat_no_matches": "Ninguna conversación coincide con tu búsqueda.",
|
||||
"chat_no_z_contacts": "Aún no hay contactos con dirección blindada",
|
||||
"chat_opt_bubble_accent": "Color de burbuja",
|
||||
"chat_opt_bubble_style": "Estilo de burbuja",
|
||||
"chat_opt_density": "Densidad de mensajes",
|
||||
"chat_opt_emoji": "Estilo de emoji",
|
||||
"chat_opt_enter_sends": "Enter envía el mensaje",
|
||||
"chat_opt_font_size": "Tamaño del texto",
|
||||
"chat_opt_global_clock": "Formato de reloj global",
|
||||
"chat_opt_poll": "Frecuencia de sondeo",
|
||||
"chat_opt_timestamp": "Marcas de tiempo",
|
||||
"chat_pick_contact": "Elegir de contactos…",
|
||||
"chat_rename": "Renombrar contacto",
|
||||
"chat_rename_hint": "Nombre del contacto",
|
||||
"chat_renamed": "Contacto renombrado",
|
||||
"chat_retry": "Reintentar",
|
||||
"chat_search": "Buscar conversaciones",
|
||||
"chat_sec_appearance": "APARIENCIA",
|
||||
"chat_sec_messaging": "MENSAJES",
|
||||
"chat_select_hint": "Selecciona una conversación para verla.",
|
||||
"chat_send": "Enviar",
|
||||
"chat_send_failed": "no enviado",
|
||||
"chat_sending": "enviando…",
|
||||
"chat_settings_done": "Listo",
|
||||
"chat_settings_section": "CHAT Y CONTACTOS",
|
||||
"chat_settings_tip": "Personalizar chat",
|
||||
"chat_settings_title": "Ajustes de chat",
|
||||
"chat_show_hidden": "Ver ocultos",
|
||||
"chat_time_now": "ahora",
|
||||
"chat_toast_compose_failed": "No se pudo componer el mensaje (¿demasiado largo?).",
|
||||
@@ -179,9 +218,16 @@
|
||||
"chat_toast_request_compose_failed": "No se pudo componer la solicitud de contacto (¿dirección o texto no válidos?).",
|
||||
"chat_toast_request_queued": "Solicitud de contacto en cola.",
|
||||
"chat_toast_waiting_reply": "Espera a que este contacto responda antes de poder escribirle.",
|
||||
"chat_today": "Hoy",
|
||||
"chat_ts_12h": "12 horas",
|
||||
"chat_ts_24h": "24 horas",
|
||||
"chat_ts_global": "Seguir global",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unhide": "Mostrar",
|
||||
"chat_unmute": "Reactivar",
|
||||
"chat_verify_key": "Clave de identidad: compárala para verificar",
|
||||
"chat_waiting_reply": "Esperando a que este contacto responda: podrás escribirle una vez lo haga.",
|
||||
"chat_yesterday": "Ayer",
|
||||
"chat_you": "Tú",
|
||||
"choose_icon": "Elegir Icono",
|
||||
"clear": "Limpiar",
|
||||
@@ -193,6 +239,7 @@
|
||||
"click_copy_address": "Clic para copiar dirección",
|
||||
"click_copy_uri": "Clic para copiar URI",
|
||||
"click_to_copy": "Clic para copiar",
|
||||
"clock_format": "Formato de hora",
|
||||
"close": "Cerrar",
|
||||
"conf_count": "%d conf",
|
||||
"confirm_and_send": "Confirmar y Enviar",
|
||||
@@ -230,12 +277,18 @@
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Auto-desplazamiento",
|
||||
"console_available_commands": "Comandos disponibles:",
|
||||
"console_backend_reference": "Referencia de Comandos del Backend",
|
||||
"console_backend_unavailable": "Sin backend",
|
||||
"console_capturing_output": "Capturando salida del daemon...",
|
||||
"console_cat_advanced": "Avanzado",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Control",
|
||||
"console_cat_keys": "Claves y seguridad",
|
||||
"console_cat_mining": "Minería",
|
||||
"console_cat_network": "Red",
|
||||
"console_cat_raw_transactions": "Transacciones sin procesar",
|
||||
"console_cat_send": "Enviar",
|
||||
"console_cat_sync": "Sincronización",
|
||||
"console_cat_utility": "Utilidades",
|
||||
"console_cat_wallet": "Cartera",
|
||||
"console_clear": "Limpiar",
|
||||
@@ -269,11 +322,14 @@
|
||||
"console_help_help": " help - Mostrar este mensaje de ayuda",
|
||||
"console_help_setgenerate": " setgenerate - Controlar minería",
|
||||
"console_help_stop": " stop - Detener el daemon",
|
||||
"console_last_error": "Último error:",
|
||||
"console_line_count": "%zu líneas",
|
||||
"console_matches": "coincidencias",
|
||||
"console_new_lines": "%d nuevas líneas",
|
||||
"console_no_daemon": "Sin daemon",
|
||||
"console_no_output": "(sin salida)",
|
||||
"console_not_connected": "Error: No conectado al daemon",
|
||||
"console_not_connected_lite": "Error: No hay ninguna cartera abierta",
|
||||
"console_quit_note": "'quit'/'exit' no son necesarios aquí — simplemente cierra la ventana.",
|
||||
"console_ref_builds": "Genera",
|
||||
"console_ref_cancel": "Cancelar",
|
||||
@@ -289,12 +345,14 @@
|
||||
"console_ref_run_confirm": "¿Ejecutar %s ahora? Es un comando con consecuencias.",
|
||||
"console_ref_search_hint": "Buscar por nombre o tarea…",
|
||||
"console_ref_select_hint": "Selecciona un comando para ver qué hace.",
|
||||
"console_ref_value": "valor",
|
||||
"console_rpc_reference": "Referencia de Comandos RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Líneas de consola",
|
||||
"console_search_commands": "Buscar comandos...",
|
||||
"console_select_all": "Seleccionar Todo",
|
||||
"console_show_app_output": "Mostrar las líneas de registro de la cartera [app]",
|
||||
"console_show_backend_ref": "Mostrar referencia de comandos del backend",
|
||||
"console_show_daemon_output": "Mostrar salida del daemon",
|
||||
"console_show_errors_only": "Mostrar solo errores",
|
||||
"console_show_rpc_ref": "Mostrar referencia de comandos RPC",
|
||||
@@ -307,6 +365,7 @@
|
||||
"console_status_stopped": "Detenido",
|
||||
"console_status_stopping": "Deteniendo",
|
||||
"console_status_unknown": "Desconocido",
|
||||
"console_stop_confirm_node": "'stop' apagará el nodo y desconectará la cartera. Escribe 'stop' de nuevo para confirmar.",
|
||||
"console_tab_completion": "Tab para completar",
|
||||
"console_text_colors": "Colores de texto",
|
||||
"console_toggle_accents": "Alternar acentos de color de línea",
|
||||
@@ -334,8 +393,15 @@
|
||||
"contact_preview_name": "Nombre del contacto",
|
||||
"contact_wallet_loading": "La cartera aún se está cargando: marca «Mostrar en todas las carteras» o inténtalo de nuevo en un momento.",
|
||||
"contacts": "Contactos",
|
||||
"contacts_avatar_shape": "Forma del avatar",
|
||||
"contacts_list_scale": "Escala de la lista",
|
||||
"contacts_search_no_match": "No hay contactos coincidentes",
|
||||
"contacts_search_placeholder": "Buscar contactos...",
|
||||
"contacts_settings_tip": "Personalizar contactos",
|
||||
"contacts_settings_title": "Ajustes de contactos",
|
||||
"contacts_shape_circle": "Círculo",
|
||||
"contacts_shape_square": "Cuadrado",
|
||||
"contacts_shape_tab": "Pestaña",
|
||||
"copied": "¡Copiado!",
|
||||
"copy": "Copiar",
|
||||
"copy_address": "Copiar Dirección Completa",
|
||||
@@ -578,6 +644,7 @@
|
||||
"lite_birthday_backup": "Cumpleaños: %llu (respalda esto también)",
|
||||
"lite_birthday_hint": "Altura de bloque desde la que empezar a escanear. Deja 0 si se desconoce (escaneo completo más lento).",
|
||||
"lite_birthday_label": "Fecha de creación",
|
||||
"lite_console_backend_commands": "Comandos del backend:",
|
||||
"lite_console_help_passthrough": "Cualquier otra entrada se ejecuta como un comando de consola de la cartera lite.",
|
||||
"lite_copy": "Copiar",
|
||||
"lite_could_not_write": "No se pudo escribir ",
|
||||
@@ -595,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://tu-servidor-lite",
|
||||
"lite_net_checking": "comprobando…",
|
||||
"lite_net_connected": "Conectado",
|
||||
"lite_net_connecting": "Conectando…",
|
||||
"lite_net_custom": "Personalizado",
|
||||
"lite_net_disconnected": "No conectado",
|
||||
"lite_net_hidden_section": "Servidores ocultos",
|
||||
@@ -682,6 +750,9 @@
|
||||
"market_cap": "Cap. de Mercado",
|
||||
"market_cap_short": "Cap.",
|
||||
"market_chart_loading": "Cargando historial de precios",
|
||||
"market_col_name": "Nombre",
|
||||
"market_col_trend": "Tendencia",
|
||||
"market_col_value": "Valor",
|
||||
"market_iv_1d": "1D",
|
||||
"market_iv_1h": "1H",
|
||||
"market_iv_1m": "1M",
|
||||
@@ -690,13 +761,18 @@
|
||||
"market_no_history": "No hay historial de precios disponible",
|
||||
"market_no_price": "Sin datos de precio",
|
||||
"market_now": "Ahora",
|
||||
"market_opt_chart_style": "Estilo de gráfico",
|
||||
"market_pct_shielded": "%.0f%% Protegido",
|
||||
"market_portfolio": "PORTAFOLIO",
|
||||
"market_price_loading": "Cargando datos de precio...",
|
||||
"market_price_unavailable": "Datos de precio no disponibles",
|
||||
"market_refresh_price": "Actualizar datos de precio",
|
||||
"market_settings_tip": "Opciones de mercado",
|
||||
"market_settings_title": "Ajustes de mercado",
|
||||
"market_style_candle": "Cambiar a velas",
|
||||
"market_style_candle_label": "Velas",
|
||||
"market_style_line": "Cambiar a gráfico de líneas",
|
||||
"market_style_line_label": "Línea",
|
||||
"market_trade_on": "Operar en %s",
|
||||
"market_updated": "\\xc2\\xb7 Actualizado %s",
|
||||
"market_vol_short": "Vol",
|
||||
@@ -1000,9 +1076,9 @@
|
||||
"portfolio_spark_min": "Minuto",
|
||||
"portfolio_spark_month": "Mes",
|
||||
"portfolio_spark_week": "Semana",
|
||||
"portfolio_style_compact": "Filas compactas",
|
||||
"portfolio_style_detailed": "Filas detalladas",
|
||||
"portfolio_style_featured": "Filas destacadas",
|
||||
"portfolio_style_compact": "Tabla",
|
||||
"portfolio_style_detailed": "Tarjetas",
|
||||
"portfolio_style_featured": "Destacado",
|
||||
"portfolio_style_label": "Estilo de cartera",
|
||||
"portfolio_untitled": "Sin título",
|
||||
"portfolio_wallet_loading": "Espera a que la cartera termine de cargar para añadir un grupo.",
|
||||
@@ -1384,10 +1460,20 @@
|
||||
"tt_blur": "Cantidad de desenfoque (0%% = apagado, 100%% = máximo)",
|
||||
"tt_change_pass": "Cambiar la contraseña de cifrado de la billetera",
|
||||
"tt_change_pin": "Cambiar su PIN de desbloqueo",
|
||||
"tt_chat_bubble_accent": "Color de acento para tus burbujas de mensaje salientes (o sigue el tema actual)",
|
||||
"tt_chat_bubble_style": "Forma de la burbuja de mensaje: redondeada, cuadrada o mínima (plana, sin borde)",
|
||||
"tt_chat_density": "Espaciado entre mensajes: Cómodo añade más relleno; Compacto muestra más en pantalla",
|
||||
"tt_chat_emoji_style": "Muestra los emoji con contorno monocromo o a todo color",
|
||||
"tt_chat_enter_sends": "Si está activado, Enter envía el mensaje y Shift+Enter añade un salto de línea; si está desactivado, Enter añade un salto de línea",
|
||||
"tt_chat_font_size": "Escala el texto de los mensajes de chat de 0.8x a 1.5x. Solo afecta a la pestaña de Chat, no al resto de la app",
|
||||
"tt_chat_poll_rate": "Con qué frecuencia se comprueban mensajes nuevos y de 0-conf (0.5-15 s). Más rápido responde mejor pero usa más CPU",
|
||||
"tt_chat_timestamp": "Formato de marca de tiempo solo para esta pestaña: seguir el reloj de toda la app, o forzar 24-hour o 12-hour",
|
||||
"tt_clear_ztx": "Eliminar historial de z-transacciones en caché local",
|
||||
"tt_clock_format": "Reloj de 24 o 12 horas, en toda la app. El chat puede anularlo.",
|
||||
"tt_custom_fees": "Habilitar entrada manual de comisiones al enviar transacciones",
|
||||
"tt_custom_theme": "Tema personalizado activo",
|
||||
"tt_daemon_install_bundled": "Detiene el nodo, sobrescribe el dragonxd instalado con la versión incluida en esta compilación de la cartera y luego lo reinicia",
|
||||
"tt_daemon_refresh": "Vuelve a leer la versión, el tamaño y la fecha de dragonxd instalado y del incluido que se muestran arriba",
|
||||
"tt_daemon_update_check": "Descarga y verifica el nodo completo dragonxd más reciente desde el Gitea del proyecto, y luego reinicia para aplicarlo",
|
||||
"tt_debug_collapse": "Colapsar opciones de registro de depuración",
|
||||
"tt_debug_expand": "Expandir opciones de registro de depuración",
|
||||
@@ -1405,7 +1491,30 @@
|
||||
"tt_keep_daemon": "El daemon se detendrá cuando ejecute el asistente de configuración",
|
||||
"tt_language": "Idioma de la interfaz de la billetera",
|
||||
"tt_layout_hotkey": "Atajo: teclas de flecha izquierda/derecha para cambiar diseños de Balance",
|
||||
"tt_lite_copy": "Copia el secreto revelado al portapapeles",
|
||||
"tt_lite_decrypt_pass": "Introduce tu frase de contraseña para quitar el cifrado de la cartera",
|
||||
"tt_lite_encrypt": "Cifra la cartera con la frase de contraseña de arriba; se bloquea de inmediato y requiere la frase para desbloquearse",
|
||||
"tt_lite_encrypt_pass": "Frase de contraseña con la que cifrar la cartera. Si se pierde, la cartera no se puede desbloquear ni recuperar",
|
||||
"tt_lite_hide_wipe": "Oculta el secreto revelado y lo borra de la memoria de forma segura",
|
||||
"tt_lite_import_key": "Pega una clave privada de gasto o de visualización para importar; su historial aparece tras la próxima sincronización",
|
||||
"tt_lite_import_key_btn": "Importa la clave privada introducida en esta cartera; los fondos y el historial aparecen tras la próxima sincronización",
|
||||
"tt_lite_lifecycle_op": "Elige si crear una cartera nueva, abrir una existente o restaurar una desde una frase de recuperación",
|
||||
"tt_lite_lifecycle_pass": "Frase de contraseña para desbloquear o establecer en la cartera durante esta operación de crear / abrir / restaurar",
|
||||
"tt_lite_lifecycle_run": "Ejecuta la operación de crear / abrir / restaurar seleccionada con los valores de arriba",
|
||||
"tt_lite_lifecycle_toggle": "Muestra u oculta los controles de crear / abrir / restaurar para gestionar tu archivo de cartera lite",
|
||||
"tt_lite_lock": "Bloquea la cartera ahora; se necesita una frase de contraseña para desbloquearla y se cierra cualquier sesión de chat",
|
||||
"tt_lite_redownload": "Volver a descargar y re-escanear todos los bloques del servidor lite",
|
||||
"tt_lite_remove_encrypt": "Quita el cifrado y guarda la cartera sin protección; no se requerirá ninguna frase de contraseña para abrirla",
|
||||
"tt_lite_restore_account": "Índice de cuenta HD a restaurar; deja 0 salvo que hayas usado varias cuentas con esta semilla",
|
||||
"tt_lite_restore_birthday": "Altura de bloque en la que se creó la cartera; el escaneo empieza aquí. Usa 0 o la altura más temprana si no estás seguro",
|
||||
"tt_lite_restore_overwrite": "Reemplaza un archivo de cartera existente con esta restauración. Advertencia: sobrescribe los datos de la cartera actual",
|
||||
"tt_lite_restore_seed": "La frase de recuperación de 24-word para restaurar esta cartera; se oculta mientras escribes",
|
||||
"tt_lite_save_seed_file": "Escribe la semilla y la fecha de creación en un archivo solo para el propietario (lite-seed-backup.txt) en la carpeta de configuración",
|
||||
"tt_lite_show_keys": "Revela las claves privadas de gasto de esta cartera. Cualquiera con una clave puede gastar los fondos que controla",
|
||||
"tt_lite_show_seed": "Revela la frase de recuperación y la fecha de creación de esta cartera. Cualquiera con la semilla puede gastar tus fondos",
|
||||
"tt_lite_unlock": "Desbloquea la cartera cifrada con la frase de contraseña de arriba",
|
||||
"tt_lite_unlock_pass": "Introduce tu frase de contraseña para desbloquear la cartera cifrada",
|
||||
"tt_lite_wallet_path": "Ruta o nombre del archivo de cartera que se abrirá o en el que se restaurará",
|
||||
"tt_lock": "Bloquear la billetera inmediatamente",
|
||||
"tt_low_spec": "Desactivar todos los efectos visuales pesados\\nAtajo: Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolidar múltiples UTXOs en una dirección",
|
||||
@@ -1426,12 +1535,17 @@
|
||||
"tt_rpc_host": "Nombre de host del daemon DragonX",
|
||||
"tt_rpc_pass": "Contraseña de autenticación RPC",
|
||||
"tt_rpc_port": "Puerto para conexiones RPC del daemon",
|
||||
"tt_rpc_toggle": "Muestra u oculta los datos de conexión RPC de solo lectura (host, puerto, usuario, contraseña) del daemon",
|
||||
"tt_rpc_user": "Nombre de usuario de autenticación RPC",
|
||||
"tt_save_settings": "Guardar todas las configuraciones en disco",
|
||||
"tt_save_ztx": "Almacenar historial de transacciones de z-address localmente para carga más rápida",
|
||||
"tt_scan_themes": "Buscar nuevos temas.\\nColoque carpetas de temas en:\\n%s",
|
||||
"tt_scanline": "Efecto de líneas de escaneo CRT en la consola",
|
||||
"tt_screenshot_open_dir": "Abre la carpeta de capturas (dentro del directorio de configuración) en tu explorador de archivos",
|
||||
"tt_screenshot_sweep": "Recorre cada tema en cada pestaña y guarda una captura de cada uno en la carpeta de capturas de la configuración (sobrescribe el último recorrido)",
|
||||
"tt_screenshot_sweep_full": "Como el recorrido de temas, pero además captura cada modal / diálogo / flujo usando datos de cartera de demostración temporales y sin conexión",
|
||||
"tt_seed_backup": "Muestra y respalda la frase de recuperación de 24 palabras de tu cartera",
|
||||
"tt_seed_demo_chat": "Inserta conversaciones de ejemplo en la pestaña de Chat para que un recorrido capture su interfaz; solo en memoria, se pierde al reiniciar",
|
||||
"tt_seed_migrate": "Crea una nueva cartera con frase de recuperación y traslada tus fondos a ella",
|
||||
"tt_set_pin": "Establecer un PIN de 4-8 dígitos para desbloqueo rápido",
|
||||
"tt_shield_mining": "Mover recompensas de minería transparentes a una dirección blindada",
|
||||
@@ -1450,6 +1564,7 @@
|
||||
"tt_website": "Abrir el sitio web de DragonX",
|
||||
"tt_window_opacity": "Opacidad del fondo (menor = escritorio visible a través de la ventana)",
|
||||
"tt_wizard": "Volver a ejecutar el asistente de configuración inicial\\nEl daemon será reiniciado",
|
||||
"tx_chat_badge": "Mensaje",
|
||||
"tx_confirmations": "%d confirmaciones",
|
||||
"tx_details_title": "Detalles de Transacción",
|
||||
"tx_from_address": "Dirección Origen:",
|
||||
|
||||
125
res/lang/fr.json
125
res/lang/fr.json
@@ -135,10 +135,30 @@
|
||||
"change_pass_title": "Changer la phrase secrète",
|
||||
"characters": "caractères",
|
||||
"chat": "Discussion",
|
||||
"chat_accent_amber": "Ambre",
|
||||
"chat_accent_blue": "Bleu",
|
||||
"chat_accent_green": "Vert",
|
||||
"chat_accent_pink": "Rose",
|
||||
"chat_accent_purple": "Violet",
|
||||
"chat_accent_theme": "Thème",
|
||||
"chat_add_contact": "Ajouter un contact",
|
||||
"chat_awaiting_key": "En attente de réponse",
|
||||
"chat_bubble_minimal": "Minimale",
|
||||
"chat_bubble_rounded": "Arrondie",
|
||||
"chat_bubble_square": "Carrée",
|
||||
"chat_buffer_loading": "Tampon de chat: …",
|
||||
"chat_buffer_preparing": "Tampon de chat: préparation %d/%d…",
|
||||
"chat_buffer_ready": "Tampon de chat: %d/%d prêts",
|
||||
"chat_buffer_sending": "Chat: envoi de %d messages…",
|
||||
"chat_buffer_sending_one": "Chat: envoi de %d message…",
|
||||
"chat_cancel": "Annuler",
|
||||
"chat_contact_added": "Contact ajouté — renommez-le dans Contacts",
|
||||
"chat_contact_request": "demande de contact",
|
||||
"chat_copy_address_tip": "Cliquer pour copier l'adresse",
|
||||
"chat_density_comfortable": "Confortable",
|
||||
"chat_density_compact": "Compacte",
|
||||
"chat_emoji_color": "Couleur",
|
||||
"chat_emoji_mono": "Monochrome",
|
||||
"chat_emoji_search": "Rechercher un emoji",
|
||||
"chat_empty_hint": "Aucune conversation pour l'instant. Les messages que vous recevez apparaîtront ici.",
|
||||
"chat_empty_start": "Commencez-en une avec « Nouvelle conversation ».",
|
||||
@@ -147,6 +167,7 @@
|
||||
"chat_export_done": "Conversation exportée",
|
||||
"chat_export_failed": "Impossible d'écrire le fichier d'exportation.",
|
||||
"chat_export_warn": "Enregistre les messages déchiffrés en texte clair. Conservez le fichier en lieu sûr.",
|
||||
"chat_filter": "Chat",
|
||||
"chat_hidden_toast": "Conversation masquée — un nouveau message la fait réapparaître",
|
||||
"chat_hide": "Masquer",
|
||||
"chat_hide_hidden": "Masquer masqués",
|
||||
@@ -154,21 +175,39 @@
|
||||
"chat_len_over": "Message trop long",
|
||||
"chat_locked_hint": "Déverrouillez votre portefeuille pour charger vos discussions.",
|
||||
"chat_mute": "Muet",
|
||||
"chat_new_button": "Nouvelle conversation",
|
||||
"chat_new_button": "Nouvelle discussion",
|
||||
"chat_new_message": "Message",
|
||||
"chat_new_message_toast": "Nouveau message chiffré",
|
||||
"chat_new_send": "Envoyer la demande",
|
||||
"chat_new_title": "Nouvelle conversation",
|
||||
"chat_new_title": "Nouvelle discussion",
|
||||
"chat_new_zaddr": "Adresse Z du destinataire",
|
||||
"chat_no_matches": "Aucune conversation ne correspond à votre recherche.",
|
||||
"chat_no_z_contacts": "Aucun contact avec adresse blindée pour l'instant",
|
||||
"chat_opt_bubble_accent": "Couleur de bulle",
|
||||
"chat_opt_bubble_style": "Style de bulle",
|
||||
"chat_opt_density": "Densité des messages",
|
||||
"chat_opt_emoji": "Style d'emoji",
|
||||
"chat_opt_enter_sends": "Entrée envoie le message",
|
||||
"chat_opt_font_size": "Taille du texte",
|
||||
"chat_opt_global_clock": "Format d'horloge global",
|
||||
"chat_opt_poll": "Fréquence d'actualisation",
|
||||
"chat_opt_timestamp": "Horodatage",
|
||||
"chat_pick_contact": "Choisir dans les contacts…",
|
||||
"chat_rename": "Renommer le contact",
|
||||
"chat_rename_hint": "Nom du contact",
|
||||
"chat_renamed": "Contact renommé",
|
||||
"chat_retry": "Réessayer",
|
||||
"chat_search": "Rechercher des conversations",
|
||||
"chat_sec_appearance": "APPARENCE",
|
||||
"chat_sec_messaging": "MESSAGERIE",
|
||||
"chat_select_hint": "Sélectionnez une conversation pour l'afficher.",
|
||||
"chat_send": "Envoyer",
|
||||
"chat_send_failed": "non envoyé",
|
||||
"chat_sending": "envoi…",
|
||||
"chat_settings_done": "Terminé",
|
||||
"chat_settings_section": "CHAT ET CONTACTS",
|
||||
"chat_settings_tip": "Personnaliser le chat",
|
||||
"chat_settings_title": "Paramètres du chat",
|
||||
"chat_show_hidden": "Afficher masqués",
|
||||
"chat_time_now": "à l'instant",
|
||||
"chat_toast_compose_failed": "Impossible de composer le message (trop long ?).",
|
||||
@@ -179,9 +218,16 @@
|
||||
"chat_toast_request_compose_failed": "Impossible de composer la demande de contact (adresse / texte invalide ?).",
|
||||
"chat_toast_request_queued": "Demande de contact mise en file d'attente.",
|
||||
"chat_toast_waiting_reply": "En attente de la réponse de ce contact avant de pouvoir lui écrire.",
|
||||
"chat_today": "Aujourd'hui",
|
||||
"chat_ts_12h": "12 heures",
|
||||
"chat_ts_24h": "24 heures",
|
||||
"chat_ts_global": "Suivre global",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unhide": "Afficher",
|
||||
"chat_unmute": "Réactiver",
|
||||
"chat_verify_key": "Clé d'identité — comparez pour vérifier",
|
||||
"chat_waiting_reply": "En attente de la réponse de ce contact — vous pourrez lui écrire dès qu'il aura répondu.",
|
||||
"chat_yesterday": "Hier",
|
||||
"chat_you": "Vous",
|
||||
"choose_icon": "Choisir une icône",
|
||||
"clear": "Effacer",
|
||||
@@ -193,6 +239,7 @@
|
||||
"click_copy_address": "Cliquez pour copier l'adresse",
|
||||
"click_copy_uri": "Cliquez pour copier l'URI",
|
||||
"click_to_copy": "Cliquez pour copier",
|
||||
"clock_format": "Format d'horloge",
|
||||
"close": "Fermer",
|
||||
"conf_count": "%d conf.",
|
||||
"confirm_and_send": "Confirmer & Envoyer",
|
||||
@@ -230,12 +277,18 @@
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Défilement auto",
|
||||
"console_available_commands": "Commandes disponibles :",
|
||||
"console_backend_reference": "Référence des commandes du backend",
|
||||
"console_backend_unavailable": "Aucun backend",
|
||||
"console_capturing_output": "Capture de la sortie du daemon...",
|
||||
"console_cat_advanced": "Avancé",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Contrôle",
|
||||
"console_cat_keys": "Clés et sécurité",
|
||||
"console_cat_mining": "Minage",
|
||||
"console_cat_network": "Réseau",
|
||||
"console_cat_raw_transactions": "Transactions brutes",
|
||||
"console_cat_send": "Envoyer",
|
||||
"console_cat_sync": "Synchronisation",
|
||||
"console_cat_utility": "Utilitaires",
|
||||
"console_cat_wallet": "Portefeuille",
|
||||
"console_clear": "Effacer",
|
||||
@@ -269,11 +322,14 @@
|
||||
"console_help_help": " help - Afficher ce message d'aide",
|
||||
"console_help_setgenerate": " setgenerate - Contrôler le minage",
|
||||
"console_help_stop": " stop - Arrêter le daemon",
|
||||
"console_last_error": "Dernière erreur :",
|
||||
"console_line_count": "%zu lignes",
|
||||
"console_matches": "correspondances",
|
||||
"console_new_lines": "%d nouvelles lignes",
|
||||
"console_no_daemon": "Pas de daemon",
|
||||
"console_no_output": "(aucune sortie)",
|
||||
"console_not_connected": "Erreur : Non connecté au daemon",
|
||||
"console_not_connected_lite": "Erreur : Aucun portefeuille ouvert",
|
||||
"console_quit_note": "'quit'/'exit' ne sont pas nécessaires ici — fermez simplement la fenêtre.",
|
||||
"console_ref_builds": "Génère",
|
||||
"console_ref_cancel": "Annuler",
|
||||
@@ -289,12 +345,14 @@
|
||||
"console_ref_run_confirm": "Exécuter %s maintenant ? C'est une commande à conséquences.",
|
||||
"console_ref_search_hint": "Rechercher par nom ou tâche…",
|
||||
"console_ref_select_hint": "Sélectionnez une commande pour voir ce qu'elle fait.",
|
||||
"console_ref_value": "valeur",
|
||||
"console_rpc_reference": "Référence des commandes RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Scanline de la console",
|
||||
"console_search_commands": "Rechercher des commandes...",
|
||||
"console_select_all": "Tout sélectionner",
|
||||
"console_show_app_output": "Afficher les lignes du journal du portefeuille [app]",
|
||||
"console_show_backend_ref": "Afficher la référence des commandes du backend",
|
||||
"console_show_daemon_output": "Afficher la sortie du daemon",
|
||||
"console_show_errors_only": "Afficher uniquement les erreurs",
|
||||
"console_show_rpc_ref": "Afficher la référence des commandes RPC",
|
||||
@@ -307,6 +365,7 @@
|
||||
"console_status_stopped": "Arrêté",
|
||||
"console_status_stopping": "Arrêt",
|
||||
"console_status_unknown": "Inconnu",
|
||||
"console_stop_confirm_node": "'stop' arrêtera le nœud et déconnectera le portefeuille. Tapez à nouveau 'stop' pour confirmer.",
|
||||
"console_tab_completion": "Tab pour compléter",
|
||||
"console_text_colors": "Couleurs du texte",
|
||||
"console_toggle_accents": "Basculer les accents de couleur des lignes",
|
||||
@@ -334,8 +393,15 @@
|
||||
"contact_preview_name": "Nom du contact",
|
||||
"contact_wallet_loading": "Le portefeuille se charge encore — cochez « Afficher dans chaque portefeuille » ou réessayez dans un instant.",
|
||||
"contacts": "Contacts",
|
||||
"contacts_avatar_shape": "Forme de l'avatar",
|
||||
"contacts_list_scale": "Échelle de la liste",
|
||||
"contacts_search_no_match": "Aucun contact correspondant",
|
||||
"contacts_search_placeholder": "Rechercher des contacts...",
|
||||
"contacts_settings_tip": "Personnaliser les contacts",
|
||||
"contacts_settings_title": "Paramètres des contacts",
|
||||
"contacts_shape_circle": "Cercle",
|
||||
"contacts_shape_square": "Carré",
|
||||
"contacts_shape_tab": "Onglet",
|
||||
"copied": "Copié !",
|
||||
"copy": "Copier",
|
||||
"copy_address": "Copier l'adresse complète",
|
||||
@@ -578,6 +644,7 @@
|
||||
"lite_birthday_backup": "Date de création : %llu (à sauvegarder également)",
|
||||
"lite_birthday_hint": "Hauteur de bloc à partir de laquelle commencer l'analyse. Laissez 0 si inconnue (analyse complète plus lente).",
|
||||
"lite_birthday_label": "Bloc de création",
|
||||
"lite_console_backend_commands": "Commandes du backend :",
|
||||
"lite_console_help_passthrough": "Toute autre entrée est exécutée comme une commande de la console du portefeuille lite.",
|
||||
"lite_copy": "Copier",
|
||||
"lite_could_not_write": "Impossible d'écrire ",
|
||||
@@ -595,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://votre-serveur-lite",
|
||||
"lite_net_checking": "vérification…",
|
||||
"lite_net_connected": "Connecté",
|
||||
"lite_net_connecting": "Connexion…",
|
||||
"lite_net_custom": "Personnalisé",
|
||||
"lite_net_disconnected": "Non connecté",
|
||||
"lite_net_hidden_section": "Serveurs masqués",
|
||||
@@ -682,6 +750,9 @@
|
||||
"market_cap": "Capitalisation",
|
||||
"market_cap_short": "Cap.",
|
||||
"market_chart_loading": "Chargement de l'historique des prix",
|
||||
"market_col_name": "Nom",
|
||||
"market_col_trend": "Tendance",
|
||||
"market_col_value": "Valeur",
|
||||
"market_iv_1d": "1J",
|
||||
"market_iv_1h": "1H",
|
||||
"market_iv_1m": "1M",
|
||||
@@ -690,13 +761,18 @@
|
||||
"market_no_history": "Aucun historique de prix disponible",
|
||||
"market_no_price": "Pas de données de prix",
|
||||
"market_now": "Maintenant",
|
||||
"market_opt_chart_style": "Style du graphique",
|
||||
"market_pct_shielded": "%.0f%% Blindé",
|
||||
"market_portfolio": "PORTEFEUILLE",
|
||||
"market_price_loading": "Chargement des données de prix...",
|
||||
"market_price_unavailable": "Données de prix indisponibles",
|
||||
"market_refresh_price": "Actualiser les données de prix",
|
||||
"market_settings_tip": "Options du marché",
|
||||
"market_settings_title": "Paramètres du marché",
|
||||
"market_style_candle": "Passer aux chandeliers",
|
||||
"market_style_candle_label": "Chandelier",
|
||||
"market_style_line": "Passer au graphique en ligne",
|
||||
"market_style_line_label": "Ligne",
|
||||
"market_trade_on": "Échanger sur %s",
|
||||
"market_updated": "\\xc2\\xb7 Mis à jour %s",
|
||||
"market_vol_short": "Vol",
|
||||
@@ -1000,9 +1076,9 @@
|
||||
"portfolio_spark_min": "Minute",
|
||||
"portfolio_spark_month": "Mois",
|
||||
"portfolio_spark_week": "Semaine",
|
||||
"portfolio_style_compact": "Lignes compactes",
|
||||
"portfolio_style_detailed": "Lignes détaillées",
|
||||
"portfolio_style_featured": "Lignes en vedette",
|
||||
"portfolio_style_compact": "Tableau",
|
||||
"portfolio_style_detailed": "Cartes",
|
||||
"portfolio_style_featured": "En vedette",
|
||||
"portfolio_style_label": "Style du portefeuille",
|
||||
"portfolio_untitled": "Sans titre",
|
||||
"portfolio_wallet_loading": "Attendez la fin du chargement du portefeuille pour ajouter un groupe.",
|
||||
@@ -1384,10 +1460,20 @@
|
||||
"tt_blur": "Quantité de flou (0%% = désactivé, 100%% = maximum)",
|
||||
"tt_change_pass": "Changer la phrase secrète de chiffrement du portefeuille",
|
||||
"tt_change_pin": "Changer votre PIN de déverrouillage",
|
||||
"tt_chat_bubble_accent": "Couleur d'accent de vos bulles de message sortantes (ou suivre le thème actuel)",
|
||||
"tt_chat_bubble_style": "Forme de la bulle de message : arrondie, carrée ou minimale (plate, sans bordure)",
|
||||
"tt_chat_density": "Espacement entre les messages : Confortable ajoute plus de marge ; Compact en affiche davantage à l'écran",
|
||||
"tt_chat_emoji_style": "Affiche les emoji en contour monochrome ou en couleur",
|
||||
"tt_chat_enter_sends": "Si activé, Enter envoie le message et Shift+Enter ajoute un saut de ligne ; si désactivé, Enter ajoute un saut de ligne",
|
||||
"tt_chat_font_size": "Met à l'échelle le texte des messages de chat de 0.8x à 1.5x. N'affecte que l'onglet Chat, pas le reste de l'application",
|
||||
"tt_chat_poll_rate": "Fréquence de vérification des messages nouveaux et 0-conf (0.5-15 s). Plus rapide est plus réactif mais utilise plus de CPU",
|
||||
"tt_chat_timestamp": "Format d'horodatage pour cet onglet uniquement : suivre l'horloge de l'application, ou forcer 24-hour ou 12-hour",
|
||||
"tt_clear_ztx": "Supprimer l'historique des z-transactions mis en cache localement",
|
||||
"tt_clock_format": "Horloge 24 h ou 12 h, dans toute l'app. Le chat peut la remplacer.",
|
||||
"tt_custom_fees": "Activer la saisie manuelle des frais lors de l'envoi de transactions",
|
||||
"tt_custom_theme": "Thème personnalisé actif",
|
||||
"tt_daemon_install_bundled": "Arrêter le nœud, remplacer le dragonxd installé par la version intégrée dans cette version du portefeuille, puis redémarrer",
|
||||
"tt_daemon_refresh": "Relit la version, la taille et la date de dragonxd installé et fourni affichées ci-dessus",
|
||||
"tt_daemon_update_check": "Télécharger et vérifier le dernier nœud complet dragonxd depuis le Gitea du projet, puis redémarrer pour l'appliquer",
|
||||
"tt_debug_collapse": "Réduire les options de journalisation de débogage",
|
||||
"tt_debug_expand": "Développer les options de journalisation de débogage",
|
||||
@@ -1405,7 +1491,30 @@
|
||||
"tt_keep_daemon": "Le daemon s'arrêtera lors de l'exécution de l'assistant de configuration",
|
||||
"tt_language": "Langue de l'interface du portefeuille",
|
||||
"tt_layout_hotkey": "Raccourci : touches fléchées gauche/droite pour changer les dispositions de Balance",
|
||||
"tt_lite_copy": "Copie le secret révélé dans le presse-papiers",
|
||||
"tt_lite_decrypt_pass": "Saisissez votre phrase de passe pour retirer le chiffrement du portefeuille",
|
||||
"tt_lite_encrypt": "Chiffre le portefeuille avec la phrase de passe ci-dessus ; il se verrouille immédiatement et requiert la phrase pour se déverrouiller",
|
||||
"tt_lite_encrypt_pass": "Phrase de passe pour chiffrer le portefeuille. En cas de perte, le portefeuille ne peut être ni déverrouillé ni récupéré",
|
||||
"tt_lite_hide_wipe": "Masque le secret révélé et l'efface de la mémoire de façon sécurisée",
|
||||
"tt_lite_import_key": "Collez une clé privée de dépense ou de lecture à importer ; son historique apparaît après la prochaine synchronisation",
|
||||
"tt_lite_import_key_btn": "Importe la clé privée saisie dans ce portefeuille ; les fonds et l'historique apparaissent après la prochaine synchronisation",
|
||||
"tt_lite_lifecycle_op": "Choisissez de créer un nouveau portefeuille, d'en ouvrir un existant ou d'en restaurer un à partir d'une phrase de récupération",
|
||||
"tt_lite_lifecycle_pass": "Phrase de passe pour déverrouiller ou définir sur le portefeuille lors de cette opération de création / ouverture / restauration",
|
||||
"tt_lite_lifecycle_run": "Exécute l'opération de création / ouverture / restauration sélectionnée avec les valeurs ci-dessus",
|
||||
"tt_lite_lifecycle_toggle": "Affiche ou masque les commandes de création / ouverture / restauration pour gérer votre fichier de portefeuille lite",
|
||||
"tt_lite_lock": "Verrouille le portefeuille maintenant ; une phrase de passe est requise pour le déverrouiller et toute session de chat est fermée",
|
||||
"tt_lite_redownload": "Re-télécharger et re-scanner tous les blocs depuis le serveur lite",
|
||||
"tt_lite_remove_encrypt": "Retire le chiffrement et stocke le portefeuille sans protection ; aucune phrase de passe ne sera requise pour l'ouvrir",
|
||||
"tt_lite_restore_account": "Index de compte HD à restaurer ; laissez 0 sauf si vous avez utilisé plusieurs comptes avec cette graine",
|
||||
"tt_lite_restore_birthday": "Hauteur de bloc à laquelle le portefeuille a été créé ; l'analyse commence ici. Utilisez 0 ou la hauteur la plus ancienne en cas de doute",
|
||||
"tt_lite_restore_overwrite": "Remplace un fichier de portefeuille existant par cette restauration. Attention : écrase les données du portefeuille actuel",
|
||||
"tt_lite_restore_seed": "La phrase de récupération de 24-word pour restaurer ce portefeuille ; masquée pendant la saisie",
|
||||
"tt_lite_save_seed_file": "Écrit la graine et la date de création dans un fichier réservé au propriétaire (lite-seed-backup.txt) dans le dossier de configuration",
|
||||
"tt_lite_show_keys": "Révèle les clés privées de dépense de ce portefeuille. Quiconque possède une clé peut dépenser les fonds qu'elle contrôle",
|
||||
"tt_lite_show_seed": "Révèle la phrase de récupération et la date de création de ce portefeuille. Quiconque possède la graine peut dépenser vos fonds",
|
||||
"tt_lite_unlock": "Déverrouille le portefeuille chiffré à l'aide de la phrase de passe ci-dessus",
|
||||
"tt_lite_unlock_pass": "Saisissez votre phrase de passe pour déverrouiller le portefeuille chiffré",
|
||||
"tt_lite_wallet_path": "Chemin ou nom du fichier de portefeuille à ouvrir ou dans lequel restaurer",
|
||||
"tt_lock": "Verrouiller le portefeuille immédiatement",
|
||||
"tt_low_spec": "Désactiver tous les effets visuels lourds\\nRaccourci : Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolider plusieurs UTXOs vers une adresse",
|
||||
@@ -1426,12 +1535,17 @@
|
||||
"tt_rpc_host": "Nom d'hôte du daemon DragonX",
|
||||
"tt_rpc_pass": "Mot de passe d'authentification RPC",
|
||||
"tt_rpc_port": "Port pour les connexions RPC du daemon",
|
||||
"tt_rpc_toggle": "Affiche ou masque les informations de connexion RPC en lecture seule (hôte, port, utilisateur, mot de passe) du daemon",
|
||||
"tt_rpc_user": "Nom d'utilisateur d'authentification RPC",
|
||||
"tt_save_settings": "Enregistrer tous les paramètres sur le disque",
|
||||
"tt_save_ztx": "Stocker l'historique des transactions z-address localement pour un chargement plus rapide",
|
||||
"tt_scan_themes": "Rechercher de nouveaux thèmes.\\nPlacez les dossiers de thèmes dans :\\n%s",
|
||||
"tt_scanline": "Effet de lignes de balayage CRT dans la console",
|
||||
"tt_screenshot_open_dir": "Ouvre le dossier de captures (sous le répertoire de configuration) dans votre gestionnaire de fichiers",
|
||||
"tt_screenshot_sweep": "Parcourt chaque thème sur chaque onglet et enregistre une capture de chacun dans le dossier de captures de la configuration (écrase le dernier parcours)",
|
||||
"tt_screenshot_sweep_full": "Comme le parcours des thèmes, mais capture aussi chaque modale / boîte de dialogue / flux à l'aide de données de portefeuille de démonstration temporaires et hors ligne",
|
||||
"tt_seed_backup": "Afficher et sauvegarder la phrase de récupération de 24 mots de votre portefeuille",
|
||||
"tt_seed_demo_chat": "Injecte des conversations d'exemple dans l'onglet Chat pour qu'un parcours capture son interface ; en mémoire uniquement, perdu au redémarrage",
|
||||
"tt_seed_migrate": "Créer un nouveau portefeuille à phrase de récupération et y transférer vos fonds",
|
||||
"tt_set_pin": "Définir un PIN de 4-8 chiffres pour un déverrouillage rapide",
|
||||
"tt_shield_mining": "Déplacer les récompenses de minage transparentes vers une adresse blindée",
|
||||
@@ -1450,6 +1564,7 @@
|
||||
"tt_website": "Ouvrir le site web DragonX",
|
||||
"tt_window_opacity": "Opacité de l'arrière-plan (plus bas = bureau visible à travers la fenêtre)",
|
||||
"tt_wizard": "Relancer l'assistant de configuration initiale\\nLe daemon sera redémarré",
|
||||
"tx_chat_badge": "Message",
|
||||
"tx_confirmations": "%d confirmations",
|
||||
"tx_details_title": "Détails de la transaction",
|
||||
"tx_from_address": "Adresse d'origine :",
|
||||
|
||||
125
res/lang/ja.json
125
res/lang/ja.json
@@ -135,10 +135,30 @@
|
||||
"change_pass_title": "パスフレーズを変更",
|
||||
"characters": "文字",
|
||||
"chat": "チャット",
|
||||
"chat_accent_amber": "琥珀",
|
||||
"chat_accent_blue": "青",
|
||||
"chat_accent_green": "緑",
|
||||
"chat_accent_pink": "ピンク",
|
||||
"chat_accent_purple": "紫",
|
||||
"chat_accent_theme": "テーマ",
|
||||
"chat_add_contact": "連絡先に追加",
|
||||
"chat_awaiting_key": "返信待ち",
|
||||
"chat_bubble_minimal": "ミニマル",
|
||||
"chat_bubble_rounded": "角丸",
|
||||
"chat_bubble_square": "角ばった",
|
||||
"chat_buffer_loading": "チャットバッファ:…",
|
||||
"chat_buffer_preparing": "チャットバッファ:%d/%d を準備中…",
|
||||
"chat_buffer_ready": "チャットバッファ:%d/%d 準備完了",
|
||||
"chat_buffer_sending": "チャット:%d 件のメッセージを送信中…",
|
||||
"chat_buffer_sending_one": "チャット:%d 件のメッセージを送信中…",
|
||||
"chat_cancel": "キャンセル",
|
||||
"chat_contact_added": "連絡先を追加しました — 連絡先で名前を変更できます",
|
||||
"chat_contact_request": "連絡リクエスト",
|
||||
"chat_copy_address_tip": "クリックしてアドレスをコピー",
|
||||
"chat_density_comfortable": "ゆったり",
|
||||
"chat_density_compact": "コンパクト",
|
||||
"chat_emoji_color": "カラー",
|
||||
"chat_emoji_mono": "モノクロ",
|
||||
"chat_emoji_search": "絵文字を検索",
|
||||
"chat_empty_hint": "まだ会話はありません。受信したメッセージはここに表示されます。",
|
||||
"chat_empty_start": "「新しい会話」から始めましょう。",
|
||||
@@ -147,6 +167,7 @@
|
||||
"chat_export_done": "会話をエクスポートしました",
|
||||
"chat_export_failed": "エクスポートファイルを書き込めませんでした。",
|
||||
"chat_export_warn": "復号したメッセージを平文で保存します。ファイルは安全に保管してください。",
|
||||
"chat_filter": "チャット",
|
||||
"chat_hidden_toast": "会話を非表示にしました — 新しいメッセージが届くと再表示されます",
|
||||
"chat_hide": "非表示",
|
||||
"chat_hide_hidden": "非表示を隠す",
|
||||
@@ -154,21 +175,39 @@
|
||||
"chat_len_over": "メッセージが長すぎます",
|
||||
"chat_locked_hint": "チャットを読み込むにはウォレットのロックを解除してください。",
|
||||
"chat_mute": "ミュート",
|
||||
"chat_new_button": "新しい会話",
|
||||
"chat_new_button": "新しいチャット",
|
||||
"chat_new_message": "メッセージ",
|
||||
"chat_new_message_toast": "新しい暗号化チャットメッセージ",
|
||||
"chat_new_send": "リクエストを送信",
|
||||
"chat_new_title": "新しい会話",
|
||||
"chat_new_title": "新しいチャット",
|
||||
"chat_new_zaddr": "宛先Zアドレス",
|
||||
"chat_no_matches": "検索に一致する会話がありません。",
|
||||
"chat_no_z_contacts": "シールドアドレスの連絡先はまだありません",
|
||||
"chat_opt_bubble_accent": "吹き出しの色",
|
||||
"chat_opt_bubble_style": "吹き出しスタイル",
|
||||
"chat_opt_density": "メッセージ密度",
|
||||
"chat_opt_emoji": "絵文字スタイル",
|
||||
"chat_opt_enter_sends": "Enterで送信",
|
||||
"chat_opt_font_size": "文字サイズ",
|
||||
"chat_opt_global_clock": "全体の時刻形式",
|
||||
"chat_opt_poll": "取得間隔",
|
||||
"chat_opt_timestamp": "タイムスタンプ",
|
||||
"chat_pick_contact": "連絡先から選択…",
|
||||
"chat_rename": "連絡先の名前を変更",
|
||||
"chat_rename_hint": "連絡先名",
|
||||
"chat_renamed": "連絡先の名前を変更しました",
|
||||
"chat_retry": "再送信",
|
||||
"chat_search": "会話を検索",
|
||||
"chat_sec_appearance": "外観",
|
||||
"chat_sec_messaging": "メッセージ",
|
||||
"chat_select_hint": "表示する会話を選択してください。",
|
||||
"chat_send": "送信",
|
||||
"chat_send_failed": "未送信",
|
||||
"chat_sending": "送信中…",
|
||||
"chat_settings_done": "完了",
|
||||
"chat_settings_section": "チャットと連絡先",
|
||||
"chat_settings_tip": "チャットのカスタマイズ",
|
||||
"chat_settings_title": "チャット設定",
|
||||
"chat_show_hidden": "非表示を表示",
|
||||
"chat_time_now": "たった今",
|
||||
"chat_toast_compose_failed": "メッセージを作成できませんでした(長すぎませんか?)。",
|
||||
@@ -179,9 +218,16 @@
|
||||
"chat_toast_request_compose_failed": "連絡リクエストを作成できませんでした(アドレスまたはテキストが無効?)。",
|
||||
"chat_toast_request_queued": "連絡リクエストを送信待ちに追加しました。",
|
||||
"chat_toast_waiting_reply": "メッセージを送るには、この相手からの返信を待つ必要があります。",
|
||||
"chat_today": "今日",
|
||||
"chat_ts_12h": "12時間",
|
||||
"chat_ts_24h": "24時間",
|
||||
"chat_ts_global": "全体設定に従う",
|
||||
"chat_ts_global_short": "全体",
|
||||
"chat_unhide": "再表示",
|
||||
"chat_unmute": "ミュート解除",
|
||||
"chat_verify_key": "識別鍵 — 照合して確認",
|
||||
"chat_waiting_reply": "この相手からの返信を待っています — 返信があればメッセージを送れます。",
|
||||
"chat_yesterday": "昨日",
|
||||
"chat_you": "自分",
|
||||
"choose_icon": "アイコンを選択",
|
||||
"clear": "クリア",
|
||||
@@ -193,6 +239,7 @@
|
||||
"click_copy_address": "クリックしてアドレスをコピー",
|
||||
"click_copy_uri": "クリックしてURIをコピー",
|
||||
"click_to_copy": "クリックしてコピー",
|
||||
"clock_format": "時刻形式",
|
||||
"close": "閉じる",
|
||||
"conf_count": "%d 確認",
|
||||
"confirm_and_send": "確認して送金",
|
||||
@@ -230,12 +277,18 @@
|
||||
"console_app": "アプリ",
|
||||
"console_auto_scroll": "自動スクロール",
|
||||
"console_available_commands": "利用可能なコマンド:",
|
||||
"console_backend_reference": "バックエンドコマンドリファレンス",
|
||||
"console_backend_unavailable": "バックエンドなし",
|
||||
"console_capturing_output": "デーモン出力をキャプチャ中...",
|
||||
"console_cat_advanced": "詳細設定",
|
||||
"console_cat_blockchain": "ブロックチェーン",
|
||||
"console_cat_control": "制御",
|
||||
"console_cat_keys": "鍵とセキュリティ",
|
||||
"console_cat_mining": "マイニング",
|
||||
"console_cat_network": "ネットワーク",
|
||||
"console_cat_raw_transactions": "生トランザクション",
|
||||
"console_cat_send": "送金",
|
||||
"console_cat_sync": "同期",
|
||||
"console_cat_utility": "ユーティリティ",
|
||||
"console_cat_wallet": "ウォレット",
|
||||
"console_clear": "クリア",
|
||||
@@ -269,11 +322,14 @@
|
||||
"console_help_help": " help - このヘルプを表示",
|
||||
"console_help_setgenerate": " setgenerate - マイニングを制御",
|
||||
"console_help_stop": " stop - デーモンを停止",
|
||||
"console_last_error": "最後のエラー:",
|
||||
"console_line_count": "%zu 行",
|
||||
"console_matches": "件一致",
|
||||
"console_new_lines": "%d 新しい行",
|
||||
"console_no_daemon": "デーモンなし",
|
||||
"console_no_output": "(出力なし)",
|
||||
"console_not_connected": "エラー:デーモンに接続されていません",
|
||||
"console_not_connected_lite": "エラー:ウォレットが開かれていません",
|
||||
"console_quit_note": "ここでは 'quit'/'exit' は不要です — ウィンドウを閉じるだけで構いません。",
|
||||
"console_ref_builds": "生成",
|
||||
"console_ref_cancel": "キャンセル",
|
||||
@@ -289,12 +345,14 @@
|
||||
"console_ref_run_confirm": "%s を今すぐ実行しますか? 影響の大きいコマンドです。",
|
||||
"console_ref_search_hint": "名前または用途で検索…",
|
||||
"console_ref_select_hint": "コマンドを選ぶと内容が表示されます。",
|
||||
"console_ref_value": "値",
|
||||
"console_rpc_reference": "RPCコマンドリファレンス",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "コンソールスキャンライン",
|
||||
"console_search_commands": "コマンドを検索...",
|
||||
"console_select_all": "すべて選択",
|
||||
"console_show_app_output": "[app] ウォレットのログ行を表示",
|
||||
"console_show_backend_ref": "バックエンドコマンドリファレンスを表示",
|
||||
"console_show_daemon_output": "デーモン出力を表示",
|
||||
"console_show_errors_only": "エラーのみ表示",
|
||||
"console_show_rpc_ref": "RPCコマンドリファレンスを表示",
|
||||
@@ -307,6 +365,7 @@
|
||||
"console_status_stopped": "停止済み",
|
||||
"console_status_stopping": "停止中",
|
||||
"console_status_unknown": "不明",
|
||||
"console_stop_confirm_node": "'stop' はノードを停止し、ウォレットを切断します。確認するにはもう一度 'stop' と入力してください。",
|
||||
"console_tab_completion": "Tabで補完",
|
||||
"console_text_colors": "テキスト色",
|
||||
"console_toggle_accents": "行のカラーアクセントを切り替え",
|
||||
@@ -334,8 +393,15 @@
|
||||
"contact_preview_name": "連絡先名",
|
||||
"contact_wallet_loading": "ウォレットを読み込み中です。「すべてのウォレットに表示」にチェックするか、少し待ってから再試行してください。",
|
||||
"contacts": "連絡先",
|
||||
"contacts_avatar_shape": "アバターの形",
|
||||
"contacts_list_scale": "リストの拡大率",
|
||||
"contacts_search_no_match": "一致する連絡先がありません",
|
||||
"contacts_search_placeholder": "連絡先を検索...",
|
||||
"contacts_settings_tip": "連絡先のカスタマイズ",
|
||||
"contacts_settings_title": "連絡先設定",
|
||||
"contacts_shape_circle": "円",
|
||||
"contacts_shape_square": "四角",
|
||||
"contacts_shape_tab": "左タブ",
|
||||
"copied": "コピーしました!",
|
||||
"copy": "コピー",
|
||||
"copy_address": "完全なアドレスをコピー",
|
||||
@@ -578,6 +644,7 @@
|
||||
"lite_birthday_backup": "誕生日:%llu (これもバックアップしてください)",
|
||||
"lite_birthday_hint": "スキャンを開始するブロック高。不明な場合は0のままにしてください(完全スキャンが遅くなります)。",
|
||||
"lite_birthday_label": "バースデー",
|
||||
"lite_console_backend_commands": "バックエンドコマンド:",
|
||||
"lite_console_help_passthrough": "その他の入力はライトウォレットのコンソールコマンドとして実行されます。",
|
||||
"lite_copy": "コピー",
|
||||
"lite_could_not_write": "書き込めませんでした ",
|
||||
@@ -595,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "確認中…",
|
||||
"lite_net_connected": "接続済み",
|
||||
"lite_net_connecting": "接続中…",
|
||||
"lite_net_custom": "カスタム",
|
||||
"lite_net_disconnected": "未接続",
|
||||
"lite_net_hidden_section": "非表示のサーバー",
|
||||
@@ -682,6 +750,9 @@
|
||||
"market_cap": "時価総額",
|
||||
"market_cap_short": "時価総額",
|
||||
"market_chart_loading": "価格履歴を読み込み中",
|
||||
"market_col_name": "名前",
|
||||
"market_col_trend": "トレンド",
|
||||
"market_col_value": "価値",
|
||||
"market_iv_1d": "1日",
|
||||
"market_iv_1h": "1時間",
|
||||
"market_iv_1m": "1ヶ月",
|
||||
@@ -690,13 +761,18 @@
|
||||
"market_no_history": "価格履歴がありません",
|
||||
"market_no_price": "価格データなし",
|
||||
"market_now": "現在",
|
||||
"market_opt_chart_style": "チャートスタイル",
|
||||
"market_pct_shielded": "%.0f%% シールド済み",
|
||||
"market_portfolio": "ポートフォリオ",
|
||||
"market_price_loading": "価格データを読み込み中...",
|
||||
"market_price_unavailable": "価格データが利用できません",
|
||||
"market_refresh_price": "価格データを更新",
|
||||
"market_settings_tip": "マーケットオプション",
|
||||
"market_settings_title": "マーケット設定",
|
||||
"market_style_candle": "ローソク足に切り替え",
|
||||
"market_style_candle_label": "ローソク足",
|
||||
"market_style_line": "折れ線チャートに切り替え",
|
||||
"market_style_line_label": "ライン",
|
||||
"market_trade_on": "%s で取引",
|
||||
"market_updated": "\\xc2\\xb7 更新: %s",
|
||||
"market_vol_short": "出来高",
|
||||
@@ -1000,9 +1076,9 @@
|
||||
"portfolio_spark_min": "分",
|
||||
"portfolio_spark_month": "月",
|
||||
"portfolio_spark_week": "週",
|
||||
"portfolio_style_compact": "コンパクト行",
|
||||
"portfolio_style_detailed": "詳細行",
|
||||
"portfolio_style_featured": "注目行",
|
||||
"portfolio_style_compact": "テーブル",
|
||||
"portfolio_style_detailed": "カード",
|
||||
"portfolio_style_featured": "スポットライト",
|
||||
"portfolio_style_label": "ポートフォリオスタイル",
|
||||
"portfolio_untitled": "無題",
|
||||
"portfolio_wallet_loading": "ウォレットの読み込みが終わってからグループを追加してください。",
|
||||
@@ -1384,10 +1460,20 @@
|
||||
"tt_blur": "ぼかし量(0%% = オフ、100%% = 最大)",
|
||||
"tt_change_pass": "ウォレットの暗号化パスフレーズを変更",
|
||||
"tt_change_pin": "アンロック PIN を変更",
|
||||
"tt_chat_bubble_accent": "送信メッセージの吹き出しのアクセントカラー(または現在のテーマに従う)",
|
||||
"tt_chat_bubble_style": "メッセージの吹き出しの形:角丸、四角、またはミニマル(フラット、枠なし)",
|
||||
"tt_chat_density": "メッセージ間の間隔:ゆったりは余白を増やし、コンパクトは画面に多く表示します",
|
||||
"tt_chat_emoji_style": "絵文字をモノクロの輪郭またはフルカラーで表示します",
|
||||
"tt_chat_enter_sends": "オンのとき、Enterでメッセージを送信し、Shift+Enterで改行します。オフのとき、Enterで改行します",
|
||||
"tt_chat_font_size": "チャットのメッセージ文字を0.8xから1.5xで拡大縮小します。チャットタブのみに影響し、アプリの他の部分には影響しません",
|
||||
"tt_chat_poll_rate": "新着および0-confメッセージを確認する頻度(0.5-15 s)。速いほど反応が良くなりますが、CPUをより多く使います",
|
||||
"tt_chat_timestamp": "このタブのみのタイムスタンプ形式:アプリ全体の時計に従うか、24-hourまたは12-hourを強制します",
|
||||
"tt_clear_ztx": "ローカルにキャッシュされた z-トランザクション履歴を削除",
|
||||
"tt_clock_format": "24時間または12時間表示(アプリ全体)。チャットで上書きできます。",
|
||||
"tt_custom_fees": "トランザクション送信時に手動手数料入力を有効化",
|
||||
"tt_custom_theme": "カスタムテーマがアクティブ",
|
||||
"tt_daemon_install_bundled": "ノードを停止し、インストール済みの dragonxd をこのウォレットビルドにバンドルされたバージョンで上書きしてから再起動します",
|
||||
"tt_daemon_refresh": "上に表示されているインストール済みおよび同梱のdragonxdのバージョン、サイズ、日付を再読み込みします",
|
||||
"tt_daemon_update_check": "プロジェクトの Gitea から最新の dragonxd フルノードをダウンロードして検証し、再起動して適用します",
|
||||
"tt_debug_collapse": "デバッグログオプションを折りたたむ",
|
||||
"tt_debug_expand": "デバッグログオプションを展開",
|
||||
@@ -1405,7 +1491,30 @@
|
||||
"tt_keep_daemon": "セットアップウィザード実行時にデーモンは停止します",
|
||||
"tt_language": "ウォレット UI のインターフェース言語",
|
||||
"tt_layout_hotkey": "ホットキー:左右矢印キーでバランスレイアウトを切り替え",
|
||||
"tt_lite_copy": "表示された秘密情報をクリップボードにコピーします",
|
||||
"tt_lite_decrypt_pass": "ウォレットの暗号化を解除するためにパスフレーズを入力します",
|
||||
"tt_lite_encrypt": "上のパスフレーズでウォレットを暗号化します。すぐにロックされ、解除にはパスフレーズが必要です",
|
||||
"tt_lite_encrypt_pass": "ウォレットを暗号化するパスフレーズ。失うとウォレットのロック解除も復元もできなくなります",
|
||||
"tt_lite_hide_wipe": "表示された秘密情報を隠し、メモリから安全に消去します",
|
||||
"tt_lite_import_key": "インポートする秘密鍵(送金用または閲覧用)を貼り付けます。その履歴は次回の同期後に表示されます",
|
||||
"tt_lite_import_key_btn": "入力した秘密鍵をこのウォレットにインポートします。資金と履歴は次回の同期後に表示されます",
|
||||
"tt_lite_lifecycle_op": "新しいウォレットを作成するか、既存のものを開くか、シードフレーズから復元するかを選びます",
|
||||
"tt_lite_lifecycle_pass": "この作成/開く/復元の操作でウォレットのロック解除または設定に使うパスフレーズ",
|
||||
"tt_lite_lifecycle_run": "上の値で、選択した作成/開く/復元の操作を実行します",
|
||||
"tt_lite_lifecycle_toggle": "ライトウォレットファイルを管理する作成/開く/復元のコントロールを表示または非表示にします",
|
||||
"tt_lite_lock": "ウォレットを今すぐロックします。解除にはパスフレーズが必要で、チャットセッションはすべて終了します",
|
||||
"tt_lite_redownload": "ライトサーバーからすべてのブロックを再ダウンロードして再スキャン",
|
||||
"tt_lite_remove_encrypt": "暗号化を解除し、ウォレットを保護なしで保存します。開くのにパスフレーズは不要になります",
|
||||
"tt_lite_restore_account": "復元するHDアカウントのインデックス。このシードで複数のアカウントを使っていない限り0のままにします",
|
||||
"tt_lite_restore_birthday": "ウォレットが作成されたブロック高。スキャンはここから始まります。不明な場合は0または最も古い高さを使ってください",
|
||||
"tt_lite_restore_overwrite": "既存のウォレットファイルをこの復元で置き換えます。警告:現在のウォレットデータを上書きします",
|
||||
"tt_lite_restore_seed": "このウォレットを復元するための24-wordのリカバリーシードフレーズ。入力中は非表示になります",
|
||||
"tt_lite_save_seed_file": "シードと作成時期を、設定フォルダ内の所有者のみが読めるファイル(lite-seed-backup.txt)に書き出します",
|
||||
"tt_lite_show_keys": "このウォレットの秘密鍵(送金用)を表示します。鍵を持つ人は誰でもそれが管理する資金を使えます",
|
||||
"tt_lite_show_seed": "このウォレットのリカバリーシードフレーズと作成時期を表示します。シードを持つ人は誰でもあなたの資金を使えます",
|
||||
"tt_lite_unlock": "上のパスフレーズを使って暗号化されたウォレットのロックを解除します",
|
||||
"tt_lite_unlock_pass": "暗号化されたウォレットのロックを解除するためにパスフレーズを入力します",
|
||||
"tt_lite_wallet_path": "開く、または復元先となるウォレットファイルのパスまたは名前",
|
||||
"tt_lock": "ウォレットを即座にロック",
|
||||
"tt_low_spec": "すべての重い視覚効果を無効化\\nホットキー:Ctrl+Shift+Down",
|
||||
"tt_merge": "複数の UTXO を一つのアドレスに統合",
|
||||
@@ -1426,12 +1535,17 @@
|
||||
"tt_rpc_host": "DragonX デーモンのホスト名",
|
||||
"tt_rpc_pass": "RPC 認証パスワード",
|
||||
"tt_rpc_port": "デーモン RPC 接続用ポート",
|
||||
"tt_rpc_toggle": "デーモンの読み取り専用のRPC接続情報(ホスト、ポート、ユーザー、パスワード)を表示または非表示にします",
|
||||
"tt_rpc_user": "RPC 認証ユーザー名",
|
||||
"tt_save_settings": "すべての設定をディスクに保存",
|
||||
"tt_save_ztx": "z-address トランザクション履歴をローカルに保存して高速読み込み",
|
||||
"tt_scan_themes": "新しいテーマをスキャン。\\nテーマフォルダーをここに配置:\\n%s",
|
||||
"tt_scanline": "コンソールでの CRT スキャンライン効果",
|
||||
"tt_screenshot_open_dir": "スクリーンショットフォルダ(設定ディレクトリ内)をファイルマネージャーで開きます",
|
||||
"tt_screenshot_sweep": "すべてのタブですべてのテーマを順に切り替え、それぞれのスクリーンショットを設定のスクリーンショットフォルダに保存します(前回の実行を上書きします)",
|
||||
"tt_screenshot_sweep_full": "テーマの実行と同様ですが、一時的なオフラインのデモウォレットデータを使って、すべてのモーダル/ダイアログ/フローも撮影します",
|
||||
"tt_seed_backup": "ウォレットの24単語の復元シードフレーズを表示してバックアップします",
|
||||
"tt_seed_demo_chat": "実行でUIを撮影できるように、チャットタブにサンプルの会話を挿入します。メモリ上のみで、再起動で消えます",
|
||||
"tt_seed_migrate": "新しいシードフレーズウォレットを作成し、資金をそこへ移動します",
|
||||
"tt_set_pin": "クイックアンロック用の 4-8 桁 PIN を設定",
|
||||
"tt_shield_mining": "透明マイニング報酬をシールドアドレスに移動",
|
||||
@@ -1450,6 +1564,7 @@
|
||||
"tt_website": "DragonX ウェブサイトを開く",
|
||||
"tt_window_opacity": "背景の不透明度(低い = デスクトップがウィンドウ越しに見える)",
|
||||
"tt_wizard": "初期セットアップウィザードを再実行\\nデーモンは再起動されます",
|
||||
"tx_chat_badge": "メッセージ",
|
||||
"tx_confirmations": "%d 確認",
|
||||
"tx_details_title": "取引の詳細",
|
||||
"tx_from_address": "送信元アドレス:",
|
||||
|
||||
125
res/lang/ko.json
125
res/lang/ko.json
@@ -135,10 +135,30 @@
|
||||
"change_pass_title": "암호 변경",
|
||||
"characters": "문자",
|
||||
"chat": "채팅",
|
||||
"chat_accent_amber": "황색",
|
||||
"chat_accent_blue": "파랑",
|
||||
"chat_accent_green": "초록",
|
||||
"chat_accent_pink": "분홍",
|
||||
"chat_accent_purple": "보라",
|
||||
"chat_accent_theme": "테마",
|
||||
"chat_add_contact": "연락처 추가",
|
||||
"chat_awaiting_key": "답장 대기 중",
|
||||
"chat_bubble_minimal": "미니멀",
|
||||
"chat_bubble_rounded": "둥근",
|
||||
"chat_bubble_square": "각진",
|
||||
"chat_buffer_loading": "채팅 버퍼: …",
|
||||
"chat_buffer_preparing": "채팅 버퍼: %d/%d 준비 중…",
|
||||
"chat_buffer_ready": "채팅 버퍼: %d/%d 준비됨",
|
||||
"chat_buffer_sending": "채팅: 메시지 %d개 보내는 중…",
|
||||
"chat_buffer_sending_one": "채팅: 메시지 %d개 보내는 중…",
|
||||
"chat_cancel": "취소",
|
||||
"chat_contact_added": "연락처 추가됨 — 연락처에서 이름을 변경하세요",
|
||||
"chat_contact_request": "연락 요청",
|
||||
"chat_copy_address_tip": "클릭하여 주소 복사",
|
||||
"chat_density_comfortable": "편안하게",
|
||||
"chat_density_compact": "촘촘하게",
|
||||
"chat_emoji_color": "컬러",
|
||||
"chat_emoji_mono": "단색",
|
||||
"chat_emoji_search": "이모지 검색",
|
||||
"chat_empty_hint": "아직 대화가 없습니다. 받은 메시지가 여기에 표시됩니다.",
|
||||
"chat_empty_start": "\"새 대화\"로 시작하세요.",
|
||||
@@ -147,6 +167,7 @@
|
||||
"chat_export_done": "대화를 내보냈습니다",
|
||||
"chat_export_failed": "내보내기 파일을 쓸 수 없습니다.",
|
||||
"chat_export_warn": "복호화된 메시지를 일반 텍스트로 저장합니다. 파일을 안전하게 보관하세요.",
|
||||
"chat_filter": "채팅",
|
||||
"chat_hidden_toast": "대화를 숨겼습니다 — 새 메시지가 오면 다시 표시됩니다",
|
||||
"chat_hide": "숨기기",
|
||||
"chat_hide_hidden": "숨김 접기",
|
||||
@@ -154,21 +175,39 @@
|
||||
"chat_len_over": "메시지가 너무 깁니다",
|
||||
"chat_locked_hint": "채팅을 불러오려면 지갑 잠금을 해제하세요.",
|
||||
"chat_mute": "음소거",
|
||||
"chat_new_button": "새 대화",
|
||||
"chat_new_button": "새 채팅",
|
||||
"chat_new_message": "메시지",
|
||||
"chat_new_message_toast": "새 암호화 채팅 메시지",
|
||||
"chat_new_send": "요청 보내기",
|
||||
"chat_new_title": "새 대화",
|
||||
"chat_new_title": "새 채팅",
|
||||
"chat_new_zaddr": "받는 사람 z-주소",
|
||||
"chat_no_matches": "검색과 일치하는 대화가 없습니다.",
|
||||
"chat_no_z_contacts": "보호 주소 연락처가 아직 없습니다",
|
||||
"chat_opt_bubble_accent": "말풍선 색상",
|
||||
"chat_opt_bubble_style": "말풍선 스타일",
|
||||
"chat_opt_density": "메시지 밀도",
|
||||
"chat_opt_emoji": "이모지 스타일",
|
||||
"chat_opt_enter_sends": "Enter로 전송",
|
||||
"chat_opt_font_size": "글자 크기",
|
||||
"chat_opt_global_clock": "전역 시간 형식",
|
||||
"chat_opt_poll": "폴링 주기",
|
||||
"chat_opt_timestamp": "타임스탬프",
|
||||
"chat_pick_contact": "연락처에서 선택…",
|
||||
"chat_rename": "연락처 이름 변경",
|
||||
"chat_rename_hint": "연락처 이름",
|
||||
"chat_renamed": "연락처 이름이 변경되었습니다",
|
||||
"chat_retry": "다시 시도",
|
||||
"chat_search": "대화 검색",
|
||||
"chat_sec_appearance": "모양",
|
||||
"chat_sec_messaging": "메시지",
|
||||
"chat_select_hint": "볼 대화를 선택하세요.",
|
||||
"chat_send": "전송",
|
||||
"chat_send_failed": "전송 안 됨",
|
||||
"chat_sending": "전송 중…",
|
||||
"chat_settings_done": "완료",
|
||||
"chat_settings_section": "채팅 및 연락처",
|
||||
"chat_settings_tip": "채팅 사용자 지정",
|
||||
"chat_settings_title": "채팅 설정",
|
||||
"chat_show_hidden": "숨김 보기",
|
||||
"chat_time_now": "방금",
|
||||
"chat_toast_compose_failed": "메시지를 작성할 수 없습니다 (너무 긴가요?).",
|
||||
@@ -179,9 +218,16 @@
|
||||
"chat_toast_request_compose_failed": "연락 요청을 작성할 수 없습니다 (잘못된 주소 / 텍스트?).",
|
||||
"chat_toast_request_queued": "연락 요청이 대기열에 추가되었습니다.",
|
||||
"chat_toast_waiting_reply": "상대방이 답장해야 메시지를 보낼 수 있습니다.",
|
||||
"chat_today": "오늘",
|
||||
"chat_ts_12h": "12시간",
|
||||
"chat_ts_24h": "24시간",
|
||||
"chat_ts_global": "전역 설정 따르기",
|
||||
"chat_ts_global_short": "전역",
|
||||
"chat_unhide": "다시 표시",
|
||||
"chat_unmute": "음소거 해제",
|
||||
"chat_verify_key": "신원 키 — 비교하여 확인",
|
||||
"chat_waiting_reply": "상대방의 답장을 기다리는 중입니다 — 답장하면 메시지를 보낼 수 있습니다.",
|
||||
"chat_yesterday": "어제",
|
||||
"chat_you": "나",
|
||||
"choose_icon": "아이콘 선택",
|
||||
"clear": "지우기",
|
||||
@@ -193,6 +239,7 @@
|
||||
"click_copy_address": "클릭하여 주소 복사",
|
||||
"click_copy_uri": "클릭하여 URI 복사",
|
||||
"click_to_copy": "복사하려면 클릭",
|
||||
"clock_format": "시간 형식",
|
||||
"close": "닫기",
|
||||
"conf_count": "%d 확인",
|
||||
"confirm_and_send": "확인 후 전송",
|
||||
@@ -230,12 +277,18 @@
|
||||
"console_app": "앱",
|
||||
"console_auto_scroll": "자동 스크롤",
|
||||
"console_available_commands": "사용 가능한 명령어:",
|
||||
"console_backend_reference": "백엔드 명령어 참조",
|
||||
"console_backend_unavailable": "백엔드 없음",
|
||||
"console_capturing_output": "데몬 출력 캡처 중...",
|
||||
"console_cat_advanced": "고급",
|
||||
"console_cat_blockchain": "블록체인",
|
||||
"console_cat_control": "제어",
|
||||
"console_cat_keys": "키 및 보안",
|
||||
"console_cat_mining": "채굴",
|
||||
"console_cat_network": "네트워크",
|
||||
"console_cat_raw_transactions": "원시 트랜잭션",
|
||||
"console_cat_send": "보내기",
|
||||
"console_cat_sync": "동기화",
|
||||
"console_cat_utility": "유틸리티",
|
||||
"console_cat_wallet": "지갑",
|
||||
"console_clear": "지우기",
|
||||
@@ -269,11 +322,14 @@
|
||||
"console_help_help": " help - 도움말 표시",
|
||||
"console_help_setgenerate": " setgenerate - 채굴 제어",
|
||||
"console_help_stop": " stop - 데몬 중지",
|
||||
"console_last_error": "마지막 오류:",
|
||||
"console_line_count": "%zu줄",
|
||||
"console_matches": "일치",
|
||||
"console_new_lines": "%d 새 줄",
|
||||
"console_no_daemon": "데몬 없음",
|
||||
"console_no_output": "(출력 없음)",
|
||||
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
|
||||
"console_not_connected_lite": "오류: 열린 지갑 없음",
|
||||
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.",
|
||||
"console_ref_builds": "생성",
|
||||
"console_ref_cancel": "취소",
|
||||
@@ -289,12 +345,14 @@
|
||||
"console_ref_run_confirm": "%s 을(를) 지금 실행할까요? 영향이 큰 명령입니다.",
|
||||
"console_ref_search_hint": "이름 또는 용도로 검색…",
|
||||
"console_ref_select_hint": "명령을 선택하면 설명이 표시됩니다.",
|
||||
"console_ref_value": "값",
|
||||
"console_rpc_reference": "RPC 명령어 참조",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "콘솔 스캔라인",
|
||||
"console_search_commands": "명령어 검색...",
|
||||
"console_select_all": "모두 선택",
|
||||
"console_show_app_output": "[app] 지갑 로그 줄 표시",
|
||||
"console_show_backend_ref": "백엔드 명령어 참조 표시",
|
||||
"console_show_daemon_output": "데몬 출력 표시",
|
||||
"console_show_errors_only": "오류만 표시",
|
||||
"console_show_rpc_ref": "RPC 명령어 참조 표시",
|
||||
@@ -307,6 +365,7 @@
|
||||
"console_status_stopped": "중지됨",
|
||||
"console_status_stopping": "중지 중",
|
||||
"console_status_unknown": "알 수 없음",
|
||||
"console_stop_confirm_node": "'stop'은 노드를 종료하고 지갑 연결을 끊습니다. 확인하려면 'stop'을 다시 입력하세요.",
|
||||
"console_tab_completion": "Tab으로 자동 완성",
|
||||
"console_text_colors": "텍스트 색상",
|
||||
"console_toggle_accents": "줄 색상 강조 전환",
|
||||
@@ -334,8 +393,15 @@
|
||||
"contact_preview_name": "연락처 이름",
|
||||
"contact_wallet_loading": "지갑을 아직 불러오는 중입니다 — “모든 지갑에 표시”를 선택하거나 잠시 후 다시 시도하세요.",
|
||||
"contacts": "연락처",
|
||||
"contacts_avatar_shape": "아바타 모양",
|
||||
"contacts_list_scale": "목록 배율",
|
||||
"contacts_search_no_match": "일치하는 연락처 없음",
|
||||
"contacts_search_placeholder": "연락처 검색...",
|
||||
"contacts_settings_tip": "연락처 사용자 지정",
|
||||
"contacts_settings_title": "연락처 설정",
|
||||
"contacts_shape_circle": "원",
|
||||
"contacts_shape_square": "사각형",
|
||||
"contacts_shape_tab": "왼쪽 탭",
|
||||
"copied": "복사됨!",
|
||||
"copy": "복사",
|
||||
"copy_address": "전체 주소 복사",
|
||||
@@ -578,6 +644,7 @@
|
||||
"lite_birthday_backup": "생성 블록: %llu (이 값도 백업하세요)",
|
||||
"lite_birthday_hint": "스캔을 시작할 블록 높이입니다. 모르면 0으로 두세요(전체 스캔이 느려짐).",
|
||||
"lite_birthday_label": "생일 블록",
|
||||
"lite_console_backend_commands": "백엔드 명령:",
|
||||
"lite_console_help_passthrough": "그 외 입력은 라이트 지갑 콘솔 명령으로 실행됩니다.",
|
||||
"lite_copy": "복사",
|
||||
"lite_could_not_write": "쓸 수 없습니다: ",
|
||||
@@ -595,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "확인 중…",
|
||||
"lite_net_connected": "연결됨",
|
||||
"lite_net_connecting": "연결 중…",
|
||||
"lite_net_custom": "사용자 지정",
|
||||
"lite_net_disconnected": "연결되지 않음",
|
||||
"lite_net_hidden_section": "숨겨진 서버",
|
||||
@@ -682,6 +750,9 @@
|
||||
"market_cap": "시가총액",
|
||||
"market_cap_short": "시총",
|
||||
"market_chart_loading": "가격 기록 불러오는 중",
|
||||
"market_col_name": "이름",
|
||||
"market_col_trend": "추세",
|
||||
"market_col_value": "가치",
|
||||
"market_iv_1d": "1일",
|
||||
"market_iv_1h": "1시간",
|
||||
"market_iv_1m": "1개월",
|
||||
@@ -690,13 +761,18 @@
|
||||
"market_no_history": "가격 내역 없음",
|
||||
"market_no_price": "가격 데이터 없음",
|
||||
"market_now": "현재",
|
||||
"market_opt_chart_style": "차트 스타일",
|
||||
"market_pct_shielded": "%.0f%% 차폐됨",
|
||||
"market_portfolio": "포트폴리오",
|
||||
"market_price_loading": "가격 데이터를 불러오는 중...",
|
||||
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
|
||||
"market_refresh_price": "가격 데이터 새로고침",
|
||||
"market_settings_tip": "마켓 옵션",
|
||||
"market_settings_title": "마켓 설정",
|
||||
"market_style_candle": "캔들차트로 전환",
|
||||
"market_style_candle_label": "캔들",
|
||||
"market_style_line": "선형 차트로 전환",
|
||||
"market_style_line_label": "라인",
|
||||
"market_trade_on": "%s에서 거래",
|
||||
"market_updated": "\\xc2\\xb7 업데이트됨 %s",
|
||||
"market_vol_short": "거래량",
|
||||
@@ -1000,9 +1076,9 @@
|
||||
"portfolio_spark_min": "분",
|
||||
"portfolio_spark_month": "월",
|
||||
"portfolio_spark_week": "주",
|
||||
"portfolio_style_compact": "간결한 행",
|
||||
"portfolio_style_detailed": "상세 행",
|
||||
"portfolio_style_featured": "강조 행",
|
||||
"portfolio_style_compact": "테이블",
|
||||
"portfolio_style_detailed": "카드",
|
||||
"portfolio_style_featured": "스포트라이트",
|
||||
"portfolio_style_label": "포트폴리오 스타일",
|
||||
"portfolio_untitled": "제목 없음",
|
||||
"portfolio_wallet_loading": "지갑 로딩이 끝난 후 그룹을 추가하세요.",
|
||||
@@ -1384,10 +1460,20 @@
|
||||
"tt_blur": "블러 양 (0%% = 끔, 100%% = 최대)",
|
||||
"tt_change_pass": "지갑 암호화 비밀번호 변경",
|
||||
"tt_change_pin": "잠금 해제 PIN 변경",
|
||||
"tt_chat_bubble_accent": "보내는 메시지 말풍선의 강조 색상(또는 현재 테마를 따름)",
|
||||
"tt_chat_bubble_style": "메시지 말풍선 모양: 둥근형, 사각형 또는 미니멀(평면, 테두리 없음)",
|
||||
"tt_chat_density": "메시지 간 간격: 편안함은 여백을 더 추가하고; 촘촘함은 화면에 더 많이 표시합니다",
|
||||
"tt_chat_emoji_style": "이모지를 단색 윤곽선 또는 전체 색상으로 렌더링합니다",
|
||||
"tt_chat_enter_sends": "켜면 Enter가 메시지를 보내고 Shift+Enter가 줄바꿈을 추가합니다; 끄면 Enter가 줄바꿈을 추가합니다",
|
||||
"tt_chat_font_size": "채팅 메시지 텍스트를 0.8x에서 1.5x까지 조정합니다. 채팅 탭에만 적용되며 앱의 나머지 부분에는 영향을 주지 않습니다",
|
||||
"tt_chat_poll_rate": "새 메시지 및 0-conf 메시지를 확인하는 빈도(0.5-15 s). 빠를수록 반응성이 좋지만 CPU를 더 사용합니다",
|
||||
"tt_chat_timestamp": "이 탭에만 적용되는 타임스탬프 형식: 앱 전체 시계를 따르거나 24-hour 또는 12-hour로 강제합니다",
|
||||
"tt_clear_ztx": "로컬에 캐시된 z-트랜잭션 기록 삭제",
|
||||
"tt_clock_format": "24시간 또는 12시간 형식(앱 전체). 채팅에서 재정의할 수 있습니다.",
|
||||
"tt_custom_fees": "거래 전송 시 수동 수수료 입력 활성화",
|
||||
"tt_custom_theme": "사용자 지정 테마 활성화됨",
|
||||
"tt_daemon_install_bundled": "노드를 중지하고 설치된 dragonxd를 이 지갑 빌드에 번들된 버전으로 덮어쓴 다음 재시작합니다",
|
||||
"tt_daemon_refresh": "위에 표시된 설치 및 번들 dragonxd의 버전, 크기, 날짜를 다시 읽어옵니다",
|
||||
"tt_daemon_update_check": "프로젝트 Gitea에서 최신 dragonxd 풀 노드를 다운로드하고 검증한 다음, 재시작하여 적용합니다",
|
||||
"tt_debug_collapse": "디버그 로깅 옵션 접기",
|
||||
"tt_debug_expand": "디버그 로깅 옵션 펼치기",
|
||||
@@ -1405,7 +1491,30 @@
|
||||
"tt_keep_daemon": "설정 마법사를 실행하면 데몬이 여전히 중지됩니다",
|
||||
"tt_language": "지갑 UI 인터페이스 언어",
|
||||
"tt_layout_hotkey": "단축키: 좌/우 화살표 키로 잔액 레이아웃 전환",
|
||||
"tt_lite_copy": "표시된 비밀을 클립보드에 복사합니다",
|
||||
"tt_lite_decrypt_pass": "지갑에서 암호화를 제거하려면 암호를 입력하세요",
|
||||
"tt_lite_encrypt": "위 암호로 지갑을 암호화합니다; 즉시 잠기며 잠금 해제하려면 암호가 필요합니다",
|
||||
"tt_lite_encrypt_pass": "지갑을 암호화할 암호. 분실하면 지갑을 잠금 해제하거나 복구할 수 없습니다",
|
||||
"tt_lite_hide_wipe": "표시된 비밀을 숨기고 메모리에서 안전하게 지웁니다",
|
||||
"tt_lite_import_key": "가져올 개인 지출 또는 조회 키를 붙여넣으세요; 다음 동기화 후 해당 내역이 나타납니다",
|
||||
"tt_lite_import_key_btn": "입력한 개인 키를 이 지갑으로 가져옵니다; 자금과 내역은 다음 동기화 후 나타납니다",
|
||||
"tt_lite_lifecycle_op": "새 지갑을 생성할지, 기존 지갑을 열지, 시드 문구로 복구할지 선택합니다",
|
||||
"tt_lite_lifecycle_pass": "이 생성 / 열기 / 복구 작업 중 지갑을 잠금 해제하거나 설정할 암호",
|
||||
"tt_lite_lifecycle_run": "위 값으로 선택한 생성 / 열기 / 복구 작업을 실행합니다",
|
||||
"tt_lite_lifecycle_toggle": "라이트 지갑 파일을 관리하기 위한 생성 / 열기 / 복구 컨트롤을 표시하거나 숨깁니다",
|
||||
"tt_lite_lock": "지금 지갑을 잠급니다; 잠금 해제하려면 암호가 필요하며 모든 채팅 세션이 종료됩니다",
|
||||
"tt_lite_redownload": "라이트 서버에서 모든 블록을 다시 다운로드하고 다시 스캔합니다",
|
||||
"tt_lite_remove_encrypt": "암호화를 제거하고 지갑을 보호되지 않은 상태로 저장합니다; 지갑을 열 때 암호가 필요하지 않습니다",
|
||||
"tt_lite_restore_account": "복구할 HD 계정 인덱스; 이 시드로 여러 계정을 사용하지 않았다면 0으로 두세요",
|
||||
"tt_lite_restore_birthday": "지갑이 생성된 블록 높이; 여기서부터 스캔이 시작됩니다. 확실하지 않으면 0 또는 가장 이른 높이를 사용하세요",
|
||||
"tt_lite_restore_overwrite": "기존 지갑 파일을 이 복구본으로 대체합니다. 경고: 현재 지갑 데이터를 덮어씁니다",
|
||||
"tt_lite_restore_seed": "이 지갑을 복구할 24-word 복구 시드 문구; 입력하는 동안 숨겨집니다",
|
||||
"tt_lite_save_seed_file": "시드와 생성 높이를 설정 폴더의 소유자 전용 파일(lite-seed-backup.txt)에 기록합니다",
|
||||
"tt_lite_show_keys": "이 지갑의 개인 지출 키를 표시합니다. 키를 가진 사람은 누구나 그 키가 제어하는 자금을 사용할 수 있습니다",
|
||||
"tt_lite_show_seed": "이 지갑의 복구 시드 문구와 생성 높이를 표시합니다. 시드를 가진 사람은 누구나 자금을 사용할 수 있습니다",
|
||||
"tt_lite_unlock": "위 암호를 사용하여 암호화된 지갑을 잠금 해제합니다",
|
||||
"tt_lite_unlock_pass": "암호화된 지갑을 잠금 해제하려면 암호를 입력하세요",
|
||||
"tt_lite_wallet_path": "열거나 복구할 지갑 파일의 경로 또는 이름",
|
||||
"tt_lock": "지갑 즉시 잠금",
|
||||
"tt_low_spec": "모든 고부하 시각 효과 비활성화\\n단축키: Ctrl+Shift+Down",
|
||||
"tt_merge": "여러 UTXO를 하나의 주소로 통합",
|
||||
@@ -1426,12 +1535,17 @@
|
||||
"tt_rpc_host": "DragonX 데몬 호스트 이름",
|
||||
"tt_rpc_pass": "RPC 인증 비밀번호",
|
||||
"tt_rpc_port": "데몬 RPC 연결 포트",
|
||||
"tt_rpc_toggle": "데몬의 읽기 전용 RPC 연결 정보(호스트, 포트, 사용자, 비밀번호)를 표시하거나 숨깁니다",
|
||||
"tt_rpc_user": "RPC 인증 사용자 이름",
|
||||
"tt_save_settings": "모든 설정을 디스크에 저장",
|
||||
"tt_save_ztx": "z-address 거래 기록을 로컬에 저장하여 빠른 로딩",
|
||||
"tt_scan_themes": "새 테마 검색.\\n테마 폴더를 여기에 배치:\\n%s",
|
||||
"tt_scanline": "콘솔에서 CRT 스캔라인 효과",
|
||||
"tt_screenshot_open_dir": "파일 관리자에서 screenshots 폴더(설정 디렉터리 아래)를 엽니다",
|
||||
"tt_screenshot_sweep": "모든 탭에서 모든 테마를 순환하며 각각의 스크린샷을 설정 screenshots 폴더에 저장합니다(마지막 스윕을 덮어씀)",
|
||||
"tt_screenshot_sweep_full": "테마 스윕과 유사하지만 임시 오프라인 데모 지갑 데이터를 사용하여 모든 모달 / 대화 상자 / 흐름도 캡처합니다",
|
||||
"tt_seed_backup": "지갑의 24단어 복구 시드 문구를 표시하고 백업합니다",
|
||||
"tt_seed_demo_chat": "스윕이 UI를 캡처하도록 샘플 대화를 채팅 탭에 삽입합니다; 메모리에만 저장되며 재시작 시 사라집니다",
|
||||
"tt_seed_migrate": "새 시드 문구 지갑을 만들고 자금을 그곳으로 옮깁니다",
|
||||
"tt_set_pin": "빠른 잠금 해제를 위한 4-8자리 PIN 설정",
|
||||
"tt_shield_mining": "투명 채굴 보상을 차폐 주소로 이동",
|
||||
@@ -1450,6 +1564,7 @@
|
||||
"tt_website": "DragonX 웹사이트 열기",
|
||||
"tt_window_opacity": "배경 불투명도 (낮을수록 = 창을 통해 바탕 화면이 보임)",
|
||||
"tt_wizard": "초기 설정 마법사 다시 실행\\n데몬이 재시작됩니다",
|
||||
"tx_chat_badge": "메시지",
|
||||
"tx_confirmations": "%d 확인",
|
||||
"tx_details_title": "거래 상세",
|
||||
"tx_from_address": "보낸 주소:",
|
||||
|
||||
125
res/lang/pt.json
125
res/lang/pt.json
@@ -135,10 +135,30 @@
|
||||
"change_pass_title": "Alterar senha",
|
||||
"characters": "caracteres",
|
||||
"chat": "Chat",
|
||||
"chat_accent_amber": "Âmbar",
|
||||
"chat_accent_blue": "Azul",
|
||||
"chat_accent_green": "Verde",
|
||||
"chat_accent_pink": "Rosa",
|
||||
"chat_accent_purple": "Roxo",
|
||||
"chat_accent_theme": "Tema",
|
||||
"chat_add_contact": "Adicionar contato",
|
||||
"chat_awaiting_key": "Aguardando resposta",
|
||||
"chat_bubble_minimal": "Mínimo",
|
||||
"chat_bubble_rounded": "Arredondado",
|
||||
"chat_bubble_square": "Quadrado",
|
||||
"chat_buffer_loading": "Buffer de chat: …",
|
||||
"chat_buffer_preparing": "Buffer de chat: preparando %d/%d…",
|
||||
"chat_buffer_ready": "Buffer de chat: %d/%d prontos",
|
||||
"chat_buffer_sending": "Chat: enviando %d mensagens…",
|
||||
"chat_buffer_sending_one": "Chat: enviando %d mensagem…",
|
||||
"chat_cancel": "Cancelar",
|
||||
"chat_contact_added": "Contato adicionado — renomeie em Contatos",
|
||||
"chat_contact_request": "solicitação de contato",
|
||||
"chat_copy_address_tip": "Clique para copiar o endereço",
|
||||
"chat_density_comfortable": "Confortável",
|
||||
"chat_density_compact": "Compacta",
|
||||
"chat_emoji_color": "Colorido",
|
||||
"chat_emoji_mono": "Monocromático",
|
||||
"chat_emoji_search": "Pesquisar emoji",
|
||||
"chat_empty_hint": "Nenhuma conversa ainda. As mensagens que você receber aparecerão aqui.",
|
||||
"chat_empty_start": "Inicie uma com \"Nova conversa\".",
|
||||
@@ -147,6 +167,7 @@
|
||||
"chat_export_done": "Conversa exportada",
|
||||
"chat_export_failed": "Não foi possível gravar o arquivo de exportação.",
|
||||
"chat_export_warn": "Salva as mensagens descriptografadas como texto simples. Guarde o arquivo com segurança.",
|
||||
"chat_filter": "Chat",
|
||||
"chat_hidden_toast": "Conversa ocultada — uma nova mensagem a traz de volta",
|
||||
"chat_hide": "Ocultar",
|
||||
"chat_hide_hidden": "Ocultar ocultas",
|
||||
@@ -154,21 +175,39 @@
|
||||
"chat_len_over": "Mensagem muito longa",
|
||||
"chat_locked_hint": "Desbloqueie sua carteira para carregar suas conversas.",
|
||||
"chat_mute": "Silenciar",
|
||||
"chat_new_button": "Nova conversa",
|
||||
"chat_new_button": "Novo chat",
|
||||
"chat_new_message": "Mensagem",
|
||||
"chat_new_message_toast": "Nova mensagem de chat criptografada",
|
||||
"chat_new_send": "Enviar solicitação",
|
||||
"chat_new_title": "Nova conversa",
|
||||
"chat_new_title": "Novo chat",
|
||||
"chat_new_zaddr": "Endereço-z do destinatário",
|
||||
"chat_no_matches": "Nenhuma conversa corresponde à sua pesquisa.",
|
||||
"chat_no_z_contacts": "Ainda não há contatos com endereço blindado",
|
||||
"chat_opt_bubble_accent": "Cor do balão",
|
||||
"chat_opt_bubble_style": "Estilo do balão",
|
||||
"chat_opt_density": "Densidade das mensagens",
|
||||
"chat_opt_emoji": "Estilo de emoji",
|
||||
"chat_opt_enter_sends": "Enter envia a mensagem",
|
||||
"chat_opt_font_size": "Tamanho do texto",
|
||||
"chat_opt_global_clock": "Formato de relógio global",
|
||||
"chat_opt_poll": "Taxa de atualização",
|
||||
"chat_opt_timestamp": "Carimbos de data/hora",
|
||||
"chat_pick_contact": "Escolher dos contatos…",
|
||||
"chat_rename": "Renomear contato",
|
||||
"chat_rename_hint": "Nome do contato",
|
||||
"chat_renamed": "Contato renomeado",
|
||||
"chat_retry": "Tentar novamente",
|
||||
"chat_search": "Pesquisar conversas",
|
||||
"chat_sec_appearance": "APARÊNCIA",
|
||||
"chat_sec_messaging": "MENSAGENS",
|
||||
"chat_select_hint": "Selecione uma conversa para visualizá-la.",
|
||||
"chat_send": "Enviar",
|
||||
"chat_send_failed": "não enviada",
|
||||
"chat_sending": "enviando…",
|
||||
"chat_settings_done": "Concluído",
|
||||
"chat_settings_section": "CHAT E CONTATOS",
|
||||
"chat_settings_tip": "Personalizar chat",
|
||||
"chat_settings_title": "Configurações de chat",
|
||||
"chat_show_hidden": "Ver ocultas",
|
||||
"chat_time_now": "agora",
|
||||
"chat_toast_compose_failed": "Não foi possível compor a mensagem (muito longa?).",
|
||||
@@ -179,9 +218,16 @@
|
||||
"chat_toast_request_compose_failed": "Não foi possível compor a solicitação de contato (endereço / texto inválido?).",
|
||||
"chat_toast_request_queued": "Solicitação de contato na fila.",
|
||||
"chat_toast_waiting_reply": "Aguardando a resposta do contato antes que você possa enviar mensagens a ele.",
|
||||
"chat_today": "Hoje",
|
||||
"chat_ts_12h": "12 horas",
|
||||
"chat_ts_24h": "24 horas",
|
||||
"chat_ts_global": "Seguir global",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unhide": "Mostrar",
|
||||
"chat_unmute": "Reativar som",
|
||||
"chat_verify_key": "Chave de identidade — compare para verificar",
|
||||
"chat_waiting_reply": "Aguardando a resposta deste contato — você poderá enviar mensagens assim que ele responder.",
|
||||
"chat_yesterday": "Ontem",
|
||||
"chat_you": "Você",
|
||||
"choose_icon": "Escolher Ícone",
|
||||
"clear": "Limpar",
|
||||
@@ -193,6 +239,7 @@
|
||||
"click_copy_address": "Clique para copiar o endereço",
|
||||
"click_copy_uri": "Clique para copiar a URI",
|
||||
"click_to_copy": "Clique para copiar",
|
||||
"clock_format": "Formato de hora",
|
||||
"close": "Fechar",
|
||||
"conf_count": "%d conf.",
|
||||
"confirm_and_send": "Confirmar & Enviar",
|
||||
@@ -230,12 +277,18 @@
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Rolagem automática",
|
||||
"console_available_commands": "Comandos disponíveis:",
|
||||
"console_backend_reference": "Referência de Comandos do Backend",
|
||||
"console_backend_unavailable": "Sem backend",
|
||||
"console_capturing_output": "Capturando saída do daemon...",
|
||||
"console_cat_advanced": "Avançado",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Controle",
|
||||
"console_cat_keys": "Chaves e segurança",
|
||||
"console_cat_mining": "Mineração",
|
||||
"console_cat_network": "Rede",
|
||||
"console_cat_raw_transactions": "Transações brutas",
|
||||
"console_cat_send": "Enviar",
|
||||
"console_cat_sync": "Sincronização",
|
||||
"console_cat_utility": "Utilitários",
|
||||
"console_cat_wallet": "Carteira",
|
||||
"console_clear": "Limpar",
|
||||
@@ -269,11 +322,14 @@
|
||||
"console_help_help": " help - Mostrar esta mensagem de ajuda",
|
||||
"console_help_setgenerate": " setgenerate - Controlar mineração",
|
||||
"console_help_stop": " stop - Parar o daemon",
|
||||
"console_last_error": "Último erro:",
|
||||
"console_line_count": "%zu linhas",
|
||||
"console_matches": "correspondências",
|
||||
"console_new_lines": "%d novas linhas",
|
||||
"console_no_daemon": "Sem daemon",
|
||||
"console_no_output": "(sem saída)",
|
||||
"console_not_connected": "Erro: Não conectado ao daemon",
|
||||
"console_not_connected_lite": "Erro: Nenhuma carteira aberta",
|
||||
"console_quit_note": "'quit'/'exit' não são necessários aqui — basta fechar a janela.",
|
||||
"console_ref_builds": "Gera",
|
||||
"console_ref_cancel": "Cancelar",
|
||||
@@ -289,12 +345,14 @@
|
||||
"console_ref_run_confirm": "Executar %s agora? Este é um comando com consequências.",
|
||||
"console_ref_search_hint": "Pesquisar por nome ou tarefa…",
|
||||
"console_ref_select_hint": "Selecione um comando para ver o que ele faz.",
|
||||
"console_ref_value": "valor",
|
||||
"console_rpc_reference": "Referência de Comandos RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Scanline do console",
|
||||
"console_search_commands": "Pesquisar comandos...",
|
||||
"console_select_all": "Selecionar Tudo",
|
||||
"console_show_app_output": "Mostrar linhas do log da carteira [app]",
|
||||
"console_show_backend_ref": "Mostrar referência de comandos do backend",
|
||||
"console_show_daemon_output": "Mostrar saída do daemon",
|
||||
"console_show_errors_only": "Mostrar apenas erros",
|
||||
"console_show_rpc_ref": "Mostrar referência de comandos RPC",
|
||||
@@ -307,6 +365,7 @@
|
||||
"console_status_stopped": "Parado",
|
||||
"console_status_stopping": "Parando",
|
||||
"console_status_unknown": "Desconhecido",
|
||||
"console_stop_confirm_node": "'stop' irá desligar o nó e desconectar a carteira. Digite 'stop' novamente para confirmar.",
|
||||
"console_tab_completion": "Tab para completar",
|
||||
"console_text_colors": "Cores do texto",
|
||||
"console_toggle_accents": "Alternar destaques de cor das linhas",
|
||||
@@ -334,8 +393,15 @@
|
||||
"contact_preview_name": "Nome do contato",
|
||||
"contact_wallet_loading": "A carteira ainda está carregando — marque “Mostrar em todas as carteiras” ou tente novamente em um momento.",
|
||||
"contacts": "Contatos",
|
||||
"contacts_avatar_shape": "Forma do avatar",
|
||||
"contacts_list_scale": "Escala da lista",
|
||||
"contacts_search_no_match": "Nenhum contato correspondente",
|
||||
"contacts_search_placeholder": "Pesquisar contatos...",
|
||||
"contacts_settings_tip": "Personalizar contatos",
|
||||
"contacts_settings_title": "Configurações de contatos",
|
||||
"contacts_shape_circle": "Círculo",
|
||||
"contacts_shape_square": "Quadrado",
|
||||
"contacts_shape_tab": "Aba",
|
||||
"copied": "Copiado!",
|
||||
"copy": "Copiar",
|
||||
"copy_address": "Copiar Endereço Completo",
|
||||
@@ -578,6 +644,7 @@
|
||||
"lite_birthday_backup": "Aniversário: %llu (faça o backup disto também)",
|
||||
"lite_birthday_hint": "Altura do bloco a partir da qual começar a escanear. Deixe 0 se desconhecida (escaneamento completo mais lento).",
|
||||
"lite_birthday_label": "Data de nascimento",
|
||||
"lite_console_backend_commands": "Comandos do backend:",
|
||||
"lite_console_help_passthrough": "Qualquer outra entrada é executada como um comando de console da carteira leve.",
|
||||
"lite_copy": "Copiar",
|
||||
"lite_could_not_write": "Não foi possível gravar ",
|
||||
@@ -595,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://seu-servidor-lite",
|
||||
"lite_net_checking": "verificando…",
|
||||
"lite_net_connected": "Conectado",
|
||||
"lite_net_connecting": "Conectando…",
|
||||
"lite_net_custom": "Personalizado",
|
||||
"lite_net_disconnected": "Não conectado",
|
||||
"lite_net_hidden_section": "Servidores ocultos",
|
||||
@@ -682,6 +750,9 @@
|
||||
"market_cap": "Capitalização",
|
||||
"market_cap_short": "Cap.",
|
||||
"market_chart_loading": "Carregando histórico de preços",
|
||||
"market_col_name": "Nome",
|
||||
"market_col_trend": "Tendência",
|
||||
"market_col_value": "Valor",
|
||||
"market_iv_1d": "1D",
|
||||
"market_iv_1h": "1H",
|
||||
"market_iv_1m": "1M",
|
||||
@@ -690,13 +761,18 @@
|
||||
"market_no_history": "Nenhum histórico de preços disponível",
|
||||
"market_no_price": "Sem dados de preço",
|
||||
"market_now": "Agora",
|
||||
"market_opt_chart_style": "Estilo do gráfico",
|
||||
"market_pct_shielded": "%.0f%% Blindado",
|
||||
"market_portfolio": "PORTFÓLIO",
|
||||
"market_price_loading": "Carregando dados de preço...",
|
||||
"market_price_unavailable": "Dados de preço indisponíveis",
|
||||
"market_refresh_price": "Atualizar dados de preço",
|
||||
"market_settings_tip": "Opções de mercado",
|
||||
"market_settings_title": "Configurações de mercado",
|
||||
"market_style_candle": "Mudar para velas",
|
||||
"market_style_candle_label": "Velas",
|
||||
"market_style_line": "Mudar para gráfico de linhas",
|
||||
"market_style_line_label": "Linha",
|
||||
"market_trade_on": "Negociar no %s",
|
||||
"market_updated": "\\xc2\\xb7 Atualizado %s",
|
||||
"market_vol_short": "Vol",
|
||||
@@ -1000,9 +1076,9 @@
|
||||
"portfolio_spark_min": "Minuto",
|
||||
"portfolio_spark_month": "Mês",
|
||||
"portfolio_spark_week": "Semana",
|
||||
"portfolio_style_compact": "Linhas compactas",
|
||||
"portfolio_style_detailed": "Linhas detalhadas",
|
||||
"portfolio_style_featured": "Linhas em destaque",
|
||||
"portfolio_style_compact": "Tabela",
|
||||
"portfolio_style_detailed": "Cartões",
|
||||
"portfolio_style_featured": "Destaque",
|
||||
"portfolio_style_label": "Estilo do portfólio",
|
||||
"portfolio_untitled": "Sem título",
|
||||
"portfolio_wallet_loading": "Aguarde a carteira terminar de carregar para adicionar um grupo.",
|
||||
@@ -1384,10 +1460,20 @@
|
||||
"tt_blur": "Quantidade de desfoque (0%% = desligado, 100%% = máximo)",
|
||||
"tt_change_pass": "Alterar a frase secreta de encriptação da carteira",
|
||||
"tt_change_pin": "Alterar seu PIN de desbloqueio",
|
||||
"tt_chat_bubble_accent": "Cor de destaque para seus balões de mensagem enviados (ou seguir o tema atual)",
|
||||
"tt_chat_bubble_style": "Formato do balão de mensagem: arredondado, quadrado ou minimalista (plano, sem borda)",
|
||||
"tt_chat_density": "Espaçamento entre mensagens: Confortável adiciona mais espaçamento; Compacto exibe mais na tela",
|
||||
"tt_chat_emoji_style": "Renderizar emojis em contorno monocromático ou colorido",
|
||||
"tt_chat_enter_sends": "Quando ativado, Enter envia a mensagem e Shift+Enter adiciona uma nova linha; quando desativado, Enter adiciona uma nova linha",
|
||||
"tt_chat_font_size": "Dimensionar o texto das mensagens de chat de 0.8x a 1.5x. Afeta apenas a aba Chat, não o restante do aplicativo",
|
||||
"tt_chat_poll_rate": "Com que frequência verificar mensagens novas e 0-conf (0.5-15 s). Mais rápido é mais responsivo, mas usa mais CPU",
|
||||
"tt_chat_timestamp": "Formato de horário apenas para esta aba: seguir o relógio geral do aplicativo, ou forçar 24-hour ou 12-hour",
|
||||
"tt_clear_ztx": "Excluir histórico de z-transações em cache local",
|
||||
"tt_clock_format": "Relógio de 24 ou 12 horas, em todo o app. O chat pode substituí-lo.",
|
||||
"tt_custom_fees": "Ativar entrada manual de taxas ao enviar transações",
|
||||
"tt_custom_theme": "Tema personalizado ativo",
|
||||
"tt_daemon_install_bundled": "Parar o nó, sobrescrever o dragonxd instalado com a versão incluída nesta compilação da carteira e reiniciar",
|
||||
"tt_daemon_refresh": "Reler a versão, o tamanho e a data do dragonxd instalado e do incorporado, mostrados acima",
|
||||
"tt_daemon_update_check": "Baixe e verifique o nó completo dragonxd mais recente do Gitea do projeto e, em seguida, reinicie para aplicar",
|
||||
"tt_debug_collapse": "Recolher opções de registro de depuração",
|
||||
"tt_debug_expand": "Expandir opções de registro de depuração",
|
||||
@@ -1405,7 +1491,30 @@
|
||||
"tt_keep_daemon": "O daemon será parado ao executar o assistente de configuração",
|
||||
"tt_language": "Idioma da interface da carteira",
|
||||
"tt_layout_hotkey": "Atalho: teclas de seta esquerda/direita para alternar layouts de Saldo",
|
||||
"tt_lite_copy": "Copiar o segredo revelado para a área de transferência",
|
||||
"tt_lite_decrypt_pass": "Digite sua frase-senha para remover a criptografia da carteira",
|
||||
"tt_lite_encrypt": "Criptografar a carteira com a frase-senha acima; ela é bloqueada imediatamente e exige a frase-senha para desbloquear",
|
||||
"tt_lite_encrypt_pass": "Frase-senha com a qual criptografar a carteira. Se perdida, a carteira não pode ser desbloqueada nem recuperada",
|
||||
"tt_lite_hide_wipe": "Ocultar o segredo revelado e apagá-lo com segurança da memória",
|
||||
"tt_lite_import_key": "Cole uma chave privada de gasto ou de visualização para importar; seu histórico aparece após a próxima sincronização",
|
||||
"tt_lite_import_key_btn": "Importar a chave privada informada para esta carteira; os fundos e o histórico aparecem após a próxima sincronização",
|
||||
"tt_lite_lifecycle_op": "Escolha entre criar uma nova carteira, abrir uma existente ou restaurar uma a partir de uma frase de recuperação",
|
||||
"tt_lite_lifecycle_pass": "Frase-senha para desbloquear ou definir na carteira durante esta operação de criar / abrir / restaurar",
|
||||
"tt_lite_lifecycle_run": "Executar a operação selecionada de criar / abrir / restaurar com os valores acima",
|
||||
"tt_lite_lifecycle_toggle": "Mostrar ou ocultar os controles de criar / abrir / restaurar para gerenciar o arquivo da sua carteira lite",
|
||||
"tt_lite_lock": "Bloquear a carteira agora; uma frase-senha é necessária para desbloquear e qualquer sessão de chat é encerrada",
|
||||
"tt_lite_redownload": "Rebaixar e reescanear todos os blocos do servidor lite",
|
||||
"tt_lite_remove_encrypt": "Remover a criptografia e armazenar a carteira desprotegida; nenhuma frase-senha será necessária para abri-la",
|
||||
"tt_lite_restore_account": "Índice da conta HD a restaurar; deixe 0 a menos que você tenha usado várias contas sob esta frase de recuperação",
|
||||
"tt_lite_restore_birthday": "Altura de bloco em que a carteira foi criada; a varredura começa aqui. Use 0 ou a altura mais antiga se não tiver certeza",
|
||||
"tt_lite_restore_overwrite": "Substituir um arquivo de carteira existente por esta restauração. Aviso: sobrescreve os dados da carteira atual",
|
||||
"tt_lite_restore_seed": "A frase de recuperação de 24-word para restaurar esta carteira; oculta enquanto você digita",
|
||||
"tt_lite_save_seed_file": "Gravar a frase de recuperação e a data de criação em um arquivo restrito ao dono (lite-seed-backup.txt) na pasta de configuração",
|
||||
"tt_lite_show_keys": "Revelar as chaves privadas de gasto desta carteira. Qualquer pessoa com uma chave pode gastar os fundos que ela controla",
|
||||
"tt_lite_show_seed": "Revelar a frase de recuperação e a data de criação desta carteira. Qualquer pessoa com a frase de recuperação pode gastar seus fundos",
|
||||
"tt_lite_unlock": "Desbloquear a carteira criptografada usando a frase-senha acima",
|
||||
"tt_lite_unlock_pass": "Digite sua frase-senha para desbloquear a carteira criptografada",
|
||||
"tt_lite_wallet_path": "Caminho ou nome do arquivo da carteira a abrir ou para o qual restaurar",
|
||||
"tt_lock": "Bloquear a carteira imediatamente",
|
||||
"tt_low_spec": "Desativar todos os efeitos visuais pesados\\nAtalho: Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolidar múltiplos UTXOs em um endereço",
|
||||
@@ -1426,12 +1535,17 @@
|
||||
"tt_rpc_host": "Nome do host do daemon DragonX",
|
||||
"tt_rpc_pass": "Senha de autenticação RPC",
|
||||
"tt_rpc_port": "Porta para conexões RPC do daemon",
|
||||
"tt_rpc_toggle": "Mostrar ou ocultar os detalhes de conexão RPC somente leitura (host, porta, usuário, senha) do daemon",
|
||||
"tt_rpc_user": "Nome de usuário de autenticação RPC",
|
||||
"tt_save_settings": "Salvar todas as configurações no disco",
|
||||
"tt_save_ztx": "Armazenar histórico de transações z-address localmente para carregamento mais rápido",
|
||||
"tt_scan_themes": "Procurar novos temas.\\nColoque pastas de temas em:\\n%s",
|
||||
"tt_scanline": "Efeito de linhas de varredura CRT no console",
|
||||
"tt_screenshot_open_dir": "Abrir a pasta screenshots (dentro do diretório de configuração) no seu gerenciador de arquivos",
|
||||
"tt_screenshot_sweep": "Percorrer cada tema em cada aba, salvando uma captura de tela de cada uma na pasta screenshots de configuração (sobrescreve a última varredura)",
|
||||
"tt_screenshot_sweep_full": "Como a varredura de temas, mas também captura cada modal / caixa de diálogo / fluxo usando dados temporários de carteira de demonstração offline",
|
||||
"tt_seed_backup": "Mostrar e fazer backup da frase de recuperação de 24 palavras da sua carteira",
|
||||
"tt_seed_demo_chat": "Injetar conversas de exemplo na aba Chat para que uma varredura capture sua interface; apenas na memória, some ao reiniciar",
|
||||
"tt_seed_migrate": "Criar uma nova carteira com frase de recuperação e mover seus fundos para ela",
|
||||
"tt_set_pin": "Definir um PIN de 4-8 dígitos para desbloqueio rápido",
|
||||
"tt_shield_mining": "Mover recompensas de mineração transparentes para um endereço blindado",
|
||||
@@ -1450,6 +1564,7 @@
|
||||
"tt_website": "Abrir o site do DragonX",
|
||||
"tt_window_opacity": "Opacidade do fundo (menor = área de trabalho visível através da janela)",
|
||||
"tt_wizard": "Executar novamente o assistente de configuração inicial\\nO daemon será reiniciado",
|
||||
"tx_chat_badge": "Mensagem",
|
||||
"tx_confirmations": "%d confirmações",
|
||||
"tx_details_title": "Detalhes da Transação",
|
||||
"tx_from_address": "Endereço de Origem:",
|
||||
|
||||
125
res/lang/ru.json
125
res/lang/ru.json
@@ -135,10 +135,30 @@
|
||||
"change_pass_title": "Сменить пароль",
|
||||
"characters": "символов",
|
||||
"chat": "Чат",
|
||||
"chat_accent_amber": "Янтарный",
|
||||
"chat_accent_blue": "Синий",
|
||||
"chat_accent_green": "Зелёный",
|
||||
"chat_accent_pink": "Розовый",
|
||||
"chat_accent_purple": "Фиолетовый",
|
||||
"chat_accent_theme": "Тема",
|
||||
"chat_add_contact": "Добавить контакт",
|
||||
"chat_awaiting_key": "Ожидание ответа",
|
||||
"chat_bubble_minimal": "Минимальный",
|
||||
"chat_bubble_rounded": "Скруглённый",
|
||||
"chat_bubble_square": "Прямоугольный",
|
||||
"chat_buffer_loading": "Буфер чата: …",
|
||||
"chat_buffer_preparing": "Буфер чата: подготовка %d/%d…",
|
||||
"chat_buffer_ready": "Буфер чата: %d/%d готово",
|
||||
"chat_buffer_sending": "Чат: отправка %d сообщений…",
|
||||
"chat_buffer_sending_one": "Чат: отправка %d сообщения…",
|
||||
"chat_cancel": "Отмена",
|
||||
"chat_contact_added": "Контакт добавлен — переименуйте его в Контактах",
|
||||
"chat_contact_request": "запрос контакта",
|
||||
"chat_copy_address_tip": "Нажмите, чтобы скопировать адрес",
|
||||
"chat_density_comfortable": "Свободная",
|
||||
"chat_density_compact": "Компактная",
|
||||
"chat_emoji_color": "Цветной",
|
||||
"chat_emoji_mono": "Монохромный",
|
||||
"chat_emoji_search": "Поиск эмодзи",
|
||||
"chat_empty_hint": "Пока нет переписок. Полученные сообщения появятся здесь.",
|
||||
"chat_empty_start": "Начните новый с помощью «Новый разговор».",
|
||||
@@ -147,6 +167,7 @@
|
||||
"chat_export_done": "Разговор экспортирован",
|
||||
"chat_export_failed": "Не удалось записать файл экспорта.",
|
||||
"chat_export_warn": "Сохраняет расшифрованные сообщения в виде обычного текста. Храните файл в надёжном месте.",
|
||||
"chat_filter": "Чат",
|
||||
"chat_hidden_toast": "Разговор скрыт — новое сообщение вернёт его",
|
||||
"chat_hide": "Скрыть",
|
||||
"chat_hide_hidden": "Скрыть скрытые",
|
||||
@@ -154,21 +175,39 @@
|
||||
"chat_len_over": "Сообщение слишком длинное",
|
||||
"chat_locked_hint": "Разблокируйте кошелёк, чтобы загрузить переписку.",
|
||||
"chat_mute": "Отключить уведомления",
|
||||
"chat_new_button": "Новая переписка",
|
||||
"chat_new_button": "Новый чат",
|
||||
"chat_new_message": "Сообщение",
|
||||
"chat_new_message_toast": "Новое зашифрованное сообщение",
|
||||
"chat_new_send": "Отправить запрос",
|
||||
"chat_new_title": "Новая переписка",
|
||||
"chat_new_title": "Новый чат",
|
||||
"chat_new_zaddr": "Z-адрес получателя",
|
||||
"chat_no_matches": "Нет разговоров, соответствующих запросу.",
|
||||
"chat_no_z_contacts": "Пока нет контактов с защищённым адресом",
|
||||
"chat_opt_bubble_accent": "Цвет пузырька",
|
||||
"chat_opt_bubble_style": "Стиль пузырька",
|
||||
"chat_opt_density": "Плотность сообщений",
|
||||
"chat_opt_emoji": "Стиль эмодзи",
|
||||
"chat_opt_enter_sends": "Enter отправляет сообщение",
|
||||
"chat_opt_font_size": "Размер текста",
|
||||
"chat_opt_global_clock": "Глобальный формат времени",
|
||||
"chat_opt_poll": "Частота опроса",
|
||||
"chat_opt_timestamp": "Метки времени",
|
||||
"chat_pick_contact": "Выбрать из контактов…",
|
||||
"chat_rename": "Переименовать контакт",
|
||||
"chat_rename_hint": "Имя контакта",
|
||||
"chat_renamed": "Контакт переименован",
|
||||
"chat_retry": "Повторить",
|
||||
"chat_search": "Поиск разговоров",
|
||||
"chat_sec_appearance": "ВИД",
|
||||
"chat_sec_messaging": "СООБЩЕНИЯ",
|
||||
"chat_select_hint": "Выберите переписку для просмотра.",
|
||||
"chat_send": "Отправить",
|
||||
"chat_send_failed": "не отправлено",
|
||||
"chat_sending": "отправка…",
|
||||
"chat_settings_done": "Готово",
|
||||
"chat_settings_section": "ЧАТ И КОНТАКТЫ",
|
||||
"chat_settings_tip": "Настройка чата",
|
||||
"chat_settings_title": "Настройки чата",
|
||||
"chat_show_hidden": "Показать скрытые",
|
||||
"chat_time_now": "сейчас",
|
||||
"chat_toast_compose_failed": "Не удалось составить сообщение (слишком длинное?).",
|
||||
@@ -179,9 +218,16 @@
|
||||
"chat_toast_request_compose_failed": "Не удалось составить запрос контакта (неверный адрес / текст?).",
|
||||
"chat_toast_request_queued": "Запрос контакта поставлен в очередь.",
|
||||
"chat_toast_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему только после этого.",
|
||||
"chat_today": "Сегодня",
|
||||
"chat_ts_12h": "12 часов",
|
||||
"chat_ts_24h": "24 часа",
|
||||
"chat_ts_global": "Как глобально",
|
||||
"chat_ts_global_short": "Общий",
|
||||
"chat_unhide": "Показать",
|
||||
"chat_unmute": "Включить уведомления",
|
||||
"chat_verify_key": "Ключ личности — сравните для проверки",
|
||||
"chat_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему, как только он ответит.",
|
||||
"chat_yesterday": "Вчера",
|
||||
"chat_you": "Вы",
|
||||
"choose_icon": "Выбрать иконку",
|
||||
"clear": "Очистить",
|
||||
@@ -193,6 +239,7 @@
|
||||
"click_copy_address": "Нажмите, чтобы скопировать адрес",
|
||||
"click_copy_uri": "Нажмите, чтобы скопировать URI",
|
||||
"click_to_copy": "Нажмите для копирования",
|
||||
"clock_format": "Формат времени",
|
||||
"close": "Закрыть",
|
||||
"conf_count": "%d подтв.",
|
||||
"confirm_and_send": "Подтвердить и отправить",
|
||||
@@ -230,12 +277,18 @@
|
||||
"console_app": "Прил.",
|
||||
"console_auto_scroll": "Авто-прокрутка",
|
||||
"console_available_commands": "Доступные команды:",
|
||||
"console_backend_reference": "Справочник команд бэкенда",
|
||||
"console_backend_unavailable": "Нет бэкенда",
|
||||
"console_capturing_output": "Захват вывода daemon...",
|
||||
"console_cat_advanced": "Дополнительно",
|
||||
"console_cat_blockchain": "Блокчейн",
|
||||
"console_cat_control": "Управление",
|
||||
"console_cat_keys": "Ключи и безопасность",
|
||||
"console_cat_mining": "Майнинг",
|
||||
"console_cat_network": "Сеть",
|
||||
"console_cat_raw_transactions": "Сырые транзакции",
|
||||
"console_cat_send": "Отправка",
|
||||
"console_cat_sync": "Синхронизация",
|
||||
"console_cat_utility": "Утилиты",
|
||||
"console_cat_wallet": "Кошелёк",
|
||||
"console_clear": "Очистить",
|
||||
@@ -269,11 +322,14 @@
|
||||
"console_help_help": " help - Показать эту справку",
|
||||
"console_help_setgenerate": " setgenerate - Управление майнингом",
|
||||
"console_help_stop": " stop - Остановить daemon",
|
||||
"console_last_error": "Последняя ошибка:",
|
||||
"console_line_count": "%zu строк",
|
||||
"console_matches": "совпадений",
|
||||
"console_new_lines": "%d новых строк",
|
||||
"console_no_daemon": "Нет daemon",
|
||||
"console_no_output": "(нет вывода)",
|
||||
"console_not_connected": "Ошибка: Не подключено к daemon",
|
||||
"console_not_connected_lite": "Ошибка: Нет открытого кошелька",
|
||||
"console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.",
|
||||
"console_ref_builds": "Формирует",
|
||||
"console_ref_cancel": "Отмена",
|
||||
@@ -289,12 +345,14 @@
|
||||
"console_ref_run_confirm": "Выполнить %s сейчас? Это ответственная команда.",
|
||||
"console_ref_search_hint": "Поиск по названию или задаче…",
|
||||
"console_ref_select_hint": "Выберите команду, чтобы увидеть, что она делает.",
|
||||
"console_ref_value": "значение",
|
||||
"console_rpc_reference": "Справочник RPC-команд",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Скан-линия консоли",
|
||||
"console_search_commands": "Поиск команд...",
|
||||
"console_select_all": "Выбрать всё",
|
||||
"console_show_app_output": "Показать строки журнала кошелька [app]",
|
||||
"console_show_backend_ref": "Показать справочник команд бэкенда",
|
||||
"console_show_daemon_output": "Показать вывод daemon",
|
||||
"console_show_errors_only": "Показать только ошибки",
|
||||
"console_show_rpc_ref": "Показать справочник RPC-команд",
|
||||
@@ -307,6 +365,7 @@
|
||||
"console_status_stopped": "Остановлен",
|
||||
"console_status_stopping": "Остановка",
|
||||
"console_status_unknown": "Неизвестно",
|
||||
"console_stop_confirm_node": "'stop' остановит узел и отключит кошелёк. Введите 'stop' ещё раз для подтверждения.",
|
||||
"console_tab_completion": "Tab для дополнения",
|
||||
"console_text_colors": "Цвета текста",
|
||||
"console_toggle_accents": "Переключить цветовые акценты строк",
|
||||
@@ -334,8 +393,15 @@
|
||||
"contact_preview_name": "Имя контакта",
|
||||
"contact_wallet_loading": "Кошелёк ещё загружается — отметьте «Показывать во всех кошельках» или повторите чуть позже.",
|
||||
"contacts": "Контакты",
|
||||
"contacts_avatar_shape": "Форма аватара",
|
||||
"contacts_list_scale": "Масштаб списка",
|
||||
"contacts_search_no_match": "Совпадающих контактов нет",
|
||||
"contacts_search_placeholder": "Поиск контактов...",
|
||||
"contacts_settings_tip": "Настройка контактов",
|
||||
"contacts_settings_title": "Настройки контактов",
|
||||
"contacts_shape_circle": "Круг",
|
||||
"contacts_shape_square": "Квадрат",
|
||||
"contacts_shape_tab": "Вкладка",
|
||||
"copied": "Скопировано!",
|
||||
"copy": "Копировать",
|
||||
"copy_address": "Копировать полный адрес",
|
||||
@@ -578,6 +644,7 @@
|
||||
"lite_birthday_backup": "Дата рождения: %llu (сохраните её тоже)",
|
||||
"lite_birthday_hint": "Высота блока, с которой начинать сканирование. Оставьте 0, если неизвестно (медленное полное сканирование).",
|
||||
"lite_birthday_label": "Дата рождения",
|
||||
"lite_console_backend_commands": "Команды бэкенда:",
|
||||
"lite_console_help_passthrough": "Любой другой ввод выполняется как команда консоли лайт-кошелька.",
|
||||
"lite_copy": "Копировать",
|
||||
"lite_could_not_write": "Не удалось записать ",
|
||||
@@ -595,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "проверка…",
|
||||
"lite_net_connected": "Подключено",
|
||||
"lite_net_connecting": "Подключение…",
|
||||
"lite_net_custom": "Свой",
|
||||
"lite_net_disconnected": "Не подключено",
|
||||
"lite_net_hidden_section": "Скрытые серверы",
|
||||
@@ -682,6 +750,9 @@
|
||||
"market_cap": "Рыночная капитализация",
|
||||
"market_cap_short": "Кап.",
|
||||
"market_chart_loading": "Загрузка истории цен",
|
||||
"market_col_name": "Название",
|
||||
"market_col_trend": "Тренд",
|
||||
"market_col_value": "Стоимость",
|
||||
"market_iv_1d": "1Д",
|
||||
"market_iv_1h": "1Ч",
|
||||
"market_iv_1m": "1М",
|
||||
@@ -690,13 +761,18 @@
|
||||
"market_no_history": "Нет истории цен",
|
||||
"market_no_price": "Нет данных о ценах",
|
||||
"market_now": "Сейчас",
|
||||
"market_opt_chart_style": "Стиль графика",
|
||||
"market_pct_shielded": "%.0f%% Экранировано",
|
||||
"market_portfolio": "ПОРТФЕЛЬ",
|
||||
"market_price_loading": "Загрузка данных о ценах...",
|
||||
"market_price_unavailable": "Данные о ценах недоступны",
|
||||
"market_refresh_price": "Обновить данные о ценах",
|
||||
"market_settings_tip": "Параметры рынка",
|
||||
"market_settings_title": "Настройки рынка",
|
||||
"market_style_candle": "Переключить на свечи",
|
||||
"market_style_candle_label": "Свечи",
|
||||
"market_style_line": "Переключить на линейный график",
|
||||
"market_style_line_label": "Линия",
|
||||
"market_trade_on": "Торговать на %s",
|
||||
"market_updated": "\\xc2\\xb7 Обновлено %s",
|
||||
"market_vol_short": "Объём",
|
||||
@@ -1000,9 +1076,9 @@
|
||||
"portfolio_spark_min": "Минута",
|
||||
"portfolio_spark_month": "Месяц",
|
||||
"portfolio_spark_week": "Неделя",
|
||||
"portfolio_style_compact": "Компактные строки",
|
||||
"portfolio_style_detailed": "Подробные строки",
|
||||
"portfolio_style_featured": "Избранные строки",
|
||||
"portfolio_style_compact": "Таблица",
|
||||
"portfolio_style_detailed": "Карточки",
|
||||
"portfolio_style_featured": "Витрина",
|
||||
"portfolio_style_label": "Стиль портфеля",
|
||||
"portfolio_untitled": "Без названия",
|
||||
"portfolio_wallet_loading": "Дождитесь загрузки кошелька, чтобы добавить группу.",
|
||||
@@ -1384,10 +1460,20 @@
|
||||
"tt_blur": "Степень размытия (0%% = выкл., 100%% = максимум)",
|
||||
"tt_change_pass": "Сменить пароль шифрования кошелька",
|
||||
"tt_change_pin": "Изменить PIN-код разблокировки",
|
||||
"tt_chat_bubble_accent": "Акцентный цвет для ваших исходящих пузырьков сообщений (или следовать текущей теме)",
|
||||
"tt_chat_bubble_style": "Форма пузырька сообщения: скруглённая, квадратная или минимальная (плоская, без границы)",
|
||||
"tt_chat_density": "Интервал между сообщениями: Комфортный добавляет больше отступов; Компактный вмещает больше на экране",
|
||||
"tt_chat_emoji_style": "Отображать эмодзи в монохромном контуре или полноцветно",
|
||||
"tt_chat_enter_sends": "Когда включено, Enter отправляет сообщение, а Shift+Enter добавляет новую строку; когда выключено, Enter добавляет новую строку",
|
||||
"tt_chat_font_size": "Масштаб текста сообщений чата от 0.8x до 1.5x. Влияет только на вкладку «Чат», не на остальную часть приложения",
|
||||
"tt_chat_poll_rate": "Как часто проверять новые и 0-conf сообщения (0.5-15 s). Быстрее — отзывчивее, но использует больше CPU",
|
||||
"tt_chat_timestamp": "Формат времени только для этой вкладки: следовать общим настройкам часов приложения либо принудительно 24-hour или 12-hour",
|
||||
"tt_clear_ztx": "Удалить локально кешированную историю z-транзакций",
|
||||
"tt_clock_format": "24- или 12-часовой формат для всего приложения. Чат может переопределить.",
|
||||
"tt_custom_fees": "Включить ручной ввод комиссий при отправке транзакций",
|
||||
"tt_custom_theme": "Пользовательская тема активна",
|
||||
"tt_daemon_install_bundled": "Остановить узел, перезаписать установленный dragonxd версией, встроенной в эту сборку кошелька, затем перезапустить",
|
||||
"tt_daemon_refresh": "Перечитать версию, размер и дату установленного и встроенного dragonxd, показанные выше",
|
||||
"tt_daemon_update_check": "Скачать и проверить последний полный узел dragonxd из проектного Gitea, затем перезапустить для применения",
|
||||
"tt_debug_collapse": "Свернуть параметры журнала отладки",
|
||||
"tt_debug_expand": "Развернуть параметры журнала отладки",
|
||||
@@ -1405,7 +1491,30 @@
|
||||
"tt_keep_daemon": "Демон будет остановлен при запуске мастера настройки",
|
||||
"tt_language": "Язык интерфейса кошелька",
|
||||
"tt_layout_hotkey": "Горячая клавиша: стрелки влево/вправо для переключения раскладок Баланса",
|
||||
"tt_lite_copy": "Скопировать показанный секрет в буфер обмена",
|
||||
"tt_lite_decrypt_pass": "Введите пароль, чтобы снять шифрование с кошелька",
|
||||
"tt_lite_encrypt": "Зашифровать кошелёк паролем выше; он блокируется сразу же и требует пароль для разблокировки",
|
||||
"tt_lite_encrypt_pass": "Пароль для шифрования кошелька. При утрате кошелёк невозможно разблокировать или восстановить",
|
||||
"tt_lite_hide_wipe": "Скрыть показанный секрет и безопасно стереть его из памяти",
|
||||
"tt_lite_import_key": "Вставьте приватный ключ расходования или просмотра для импорта; его история появится после следующей синхронизации",
|
||||
"tt_lite_import_key_btn": "Импортировать введённый приватный ключ в этот кошелёк; средства и история появятся после следующей синхронизации",
|
||||
"tt_lite_lifecycle_op": "Выберите, создать новый кошелёк, открыть существующий или восстановить его из seed-фразы",
|
||||
"tt_lite_lifecycle_pass": "Пароль для разблокировки или установки на кошелёк во время этой операции создания / открытия / восстановления",
|
||||
"tt_lite_lifecycle_run": "Выполнить выбранную операцию создания / открытия / восстановления со значениями выше",
|
||||
"tt_lite_lifecycle_toggle": "Показать или скрыть элементы управления создания / открытия / восстановления для управления файлом лёгкого кошелька",
|
||||
"tt_lite_lock": "Заблокировать кошелёк сейчас; для разблокировки потребуется пароль, а любая сессия чата будет прервана",
|
||||
"tt_lite_redownload": "Заново загрузить и пересканировать все блоки с лёгкого сервера",
|
||||
"tt_lite_remove_encrypt": "Снять шифрование и хранить кошелёк без защиты; пароль для его открытия не потребуется",
|
||||
"tt_lite_restore_account": "Индекс HD-аккаунта для восстановления; оставьте 0, если только вы не использовали несколько аккаунтов с этой seed-фразой",
|
||||
"tt_lite_restore_birthday": "Высота блока, на которой был создан кошелёк; отсюда начинается сканирование. Если не уверены, используйте 0 или самую раннюю высоту",
|
||||
"tt_lite_restore_overwrite": "Заменить существующий файл кошелька этим восстановлением. Внимание: перезаписывает текущие данные кошелька",
|
||||
"tt_lite_restore_seed": "24-word seed-фраза для восстановления этого кошелька; скрывается по мере ввода",
|
||||
"tt_lite_save_seed_file": "Записать seed-фразу и дату создания в файл, доступный только владельцу (lite-seed-backup.txt), в папке конфигурации",
|
||||
"tt_lite_show_keys": "Показать приватные ключи расходования этого кошелька. Любой, у кого есть ключ, может потратить контролируемые им средства",
|
||||
"tt_lite_show_seed": "Показать seed-фразу восстановления и дату создания этого кошелька. Любой, у кого есть seed-фраза, может потратить ваши средства",
|
||||
"tt_lite_unlock": "Разблокировать зашифрованный кошелёк с помощью пароля выше",
|
||||
"tt_lite_unlock_pass": "Введите пароль для разблокировки зашифрованного кошелька",
|
||||
"tt_lite_wallet_path": "Путь или имя файла кошелька для открытия или восстановления",
|
||||
"tt_lock": "Немедленно заблокировать кошелёк",
|
||||
"tt_low_spec": "Отключить все тяжёлые визуальные эффекты\\nГорячая клавиша: Ctrl+Shift+Down",
|
||||
"tt_merge": "Объединить несколько UTXO в один адрес",
|
||||
@@ -1426,12 +1535,17 @@
|
||||
"tt_rpc_host": "Имя хоста демона DragonX",
|
||||
"tt_rpc_pass": "Пароль аутентификации RPC",
|
||||
"tt_rpc_port": "Порт для RPC-подключений демона",
|
||||
"tt_rpc_toggle": "Показать или скрыть параметры RPC-подключения только для чтения (хост, порт, пользователь, пароль) для демона",
|
||||
"tt_rpc_user": "Имя пользователя аутентификации RPC",
|
||||
"tt_save_settings": "Сохранить все настройки на диск",
|
||||
"tt_save_ztx": "Хранить историю транзакций z-адреса локально для более быстрой загрузки",
|
||||
"tt_scan_themes": "Поиск новых тем.\\nРазместите папки тем в:\\n%s",
|
||||
"tt_scanline": "Эффект развёртки ЭЛТ в консоли",
|
||||
"tt_screenshot_open_dir": "Открыть папку скриншотов (в каталоге конфигурации) в вашем файловом менеджере",
|
||||
"tt_screenshot_sweep": "Перебрать каждую тему по всем вкладкам, сохраняя скриншот каждой в папку скриншотов конфигурации (перезаписывает предыдущий проход)",
|
||||
"tt_screenshot_sweep_full": "Как проход по темам, но также захватывает каждое модальное окно / диалог / поток, используя временные офлайн-данные демонстрационного кошелька",
|
||||
"tt_seed_backup": "Показать и создать резервную копию сид-фразы восстановления вашего кошелька из 24 слов",
|
||||
"tt_seed_demo_chat": "Добавить примеры переписок во вкладку «Чат», чтобы проход захватил её интерфейс; только в памяти, исчезает при перезапуске",
|
||||
"tt_seed_migrate": "Создать новый кошелёк с сид-фразой и перевести в него ваши средства",
|
||||
"tt_set_pin": "Установить 4-8-значный PIN для быстрой разблокировки",
|
||||
"tt_shield_mining": "Перевести прозрачные вознаграждения за майнинг на экранированный адрес",
|
||||
@@ -1450,6 +1564,7 @@
|
||||
"tt_website": "Открыть сайт DragonX",
|
||||
"tt_window_opacity": "Непрозрачность фона (ниже = рабочий стол виден сквозь окно)",
|
||||
"tt_wizard": "Повторно запустить мастер начальной настройки\\nДемон будет перезапущен",
|
||||
"tx_chat_badge": "Сообщение",
|
||||
"tx_confirmations": "%d подтверждений",
|
||||
"tx_details_title": "Детали транзакции",
|
||||
"tx_from_address": "Адрес отправителя:",
|
||||
|
||||
125
res/lang/zh.json
125
res/lang/zh.json
@@ -135,10 +135,30 @@
|
||||
"change_pass_title": "更改密码短语",
|
||||
"characters": "字符",
|
||||
"chat": "聊天",
|
||||
"chat_accent_amber": "琥珀色",
|
||||
"chat_accent_blue": "蓝色",
|
||||
"chat_accent_green": "绿色",
|
||||
"chat_accent_pink": "粉色",
|
||||
"chat_accent_purple": "紫色",
|
||||
"chat_accent_theme": "主题",
|
||||
"chat_add_contact": "添加联系人",
|
||||
"chat_awaiting_key": "等待回复",
|
||||
"chat_bubble_minimal": "极简",
|
||||
"chat_bubble_rounded": "圆角",
|
||||
"chat_bubble_square": "方形",
|
||||
"chat_buffer_loading": "聊天缓冲:…",
|
||||
"chat_buffer_preparing": "聊天缓冲:正在准备 %d/%d…",
|
||||
"chat_buffer_ready": "聊天缓冲:%d/%d 已就绪",
|
||||
"chat_buffer_sending": "聊天:正在发送 %d 条消息…",
|
||||
"chat_buffer_sending_one": "聊天:正在发送 %d 条消息…",
|
||||
"chat_cancel": "取消",
|
||||
"chat_contact_added": "已添加联系人——可在联系人中重命名",
|
||||
"chat_contact_request": "联系人请求",
|
||||
"chat_copy_address_tip": "点击复制地址",
|
||||
"chat_density_comfortable": "宽松",
|
||||
"chat_density_compact": "紧凑",
|
||||
"chat_emoji_color": "彩色",
|
||||
"chat_emoji_mono": "单色",
|
||||
"chat_emoji_search": "搜索表情",
|
||||
"chat_empty_hint": "暂无对话。您收到的消息将显示在此处。",
|
||||
"chat_empty_start": "点击\"新建会话\"开始。",
|
||||
@@ -147,6 +167,7 @@
|
||||
"chat_export_done": "会话已导出",
|
||||
"chat_export_failed": "无法写入导出文件。",
|
||||
"chat_export_warn": "将解密后的消息保存为纯文本。请妥善保管该文件。",
|
||||
"chat_filter": "聊天",
|
||||
"chat_hidden_toast": "会话已隐藏——收到新消息后会重新显示",
|
||||
"chat_hide": "隐藏",
|
||||
"chat_hide_hidden": "收起已隐藏",
|
||||
@@ -154,21 +175,39 @@
|
||||
"chat_len_over": "消息过长",
|
||||
"chat_locked_hint": "解锁钱包以加载您的聊天记录。",
|
||||
"chat_mute": "静音",
|
||||
"chat_new_button": "新建对话",
|
||||
"chat_new_button": "新聊天",
|
||||
"chat_new_message": "消息",
|
||||
"chat_new_message_toast": "新的加密聊天消息",
|
||||
"chat_new_send": "发送请求",
|
||||
"chat_new_title": "新建对话",
|
||||
"chat_new_title": "新聊天",
|
||||
"chat_new_zaddr": "收款方 z 地址",
|
||||
"chat_no_matches": "没有与搜索匹配的会话。",
|
||||
"chat_no_z_contacts": "暂无使用隐私地址的联系人",
|
||||
"chat_opt_bubble_accent": "气泡颜色",
|
||||
"chat_opt_bubble_style": "气泡样式",
|
||||
"chat_opt_density": "消息密度",
|
||||
"chat_opt_emoji": "表情样式",
|
||||
"chat_opt_enter_sends": "回车发送消息",
|
||||
"chat_opt_font_size": "文字大小",
|
||||
"chat_opt_global_clock": "全局时间格式",
|
||||
"chat_opt_poll": "轮询频率",
|
||||
"chat_opt_timestamp": "时间戳",
|
||||
"chat_pick_contact": "从联系人中选择…",
|
||||
"chat_rename": "重命名联系人",
|
||||
"chat_rename_hint": "联系人名称",
|
||||
"chat_renamed": "联系人已重命名",
|
||||
"chat_retry": "重试",
|
||||
"chat_search": "搜索会话",
|
||||
"chat_sec_appearance": "外观",
|
||||
"chat_sec_messaging": "消息",
|
||||
"chat_select_hint": "选择一个对话以查看。",
|
||||
"chat_send": "发送",
|
||||
"chat_send_failed": "未发送",
|
||||
"chat_sending": "发送中…",
|
||||
"chat_settings_done": "完成",
|
||||
"chat_settings_section": "聊天与联系人",
|
||||
"chat_settings_tip": "聊天自定义",
|
||||
"chat_settings_title": "聊天设置",
|
||||
"chat_show_hidden": "显示已隐藏",
|
||||
"chat_time_now": "刚刚",
|
||||
"chat_toast_compose_failed": "无法编写该消息(内容过长?)。",
|
||||
@@ -179,9 +218,16 @@
|
||||
"chat_toast_request_compose_failed": "无法编写联系人请求(地址或文本无效?)。",
|
||||
"chat_toast_request_queued": "联系人请求已排队。",
|
||||
"chat_toast_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
||||
"chat_today": "今天",
|
||||
"chat_ts_12h": "12小时",
|
||||
"chat_ts_24h": "24小时",
|
||||
"chat_ts_global": "跟随全局",
|
||||
"chat_ts_global_short": "全局",
|
||||
"chat_unhide": "取消隐藏",
|
||||
"chat_unmute": "取消静音",
|
||||
"chat_verify_key": "身份密钥 — 对比以验证",
|
||||
"chat_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
||||
"chat_yesterday": "昨天",
|
||||
"chat_you": "我",
|
||||
"choose_icon": "选择图标",
|
||||
"clear": "清除",
|
||||
@@ -193,6 +239,7 @@
|
||||
"click_copy_address": "点击复制地址",
|
||||
"click_copy_uri": "点击复制 URI",
|
||||
"click_to_copy": "点击复制",
|
||||
"clock_format": "时间格式",
|
||||
"close": "关闭",
|
||||
"conf_count": "%d 确认",
|
||||
"confirm_and_send": "确认并发送",
|
||||
@@ -230,12 +277,18 @@
|
||||
"console_app": "应用",
|
||||
"console_auto_scroll": "自动滚动",
|
||||
"console_available_commands": "可用命令:",
|
||||
"console_backend_reference": "后端命令参考",
|
||||
"console_backend_unavailable": "无后端",
|
||||
"console_capturing_output": "正在捕获守护进程输出...",
|
||||
"console_cat_advanced": "高级",
|
||||
"console_cat_blockchain": "区块链",
|
||||
"console_cat_control": "控制",
|
||||
"console_cat_keys": "密钥与安全",
|
||||
"console_cat_mining": "挖矿",
|
||||
"console_cat_network": "网络",
|
||||
"console_cat_raw_transactions": "原始交易",
|
||||
"console_cat_send": "发送",
|
||||
"console_cat_sync": "同步",
|
||||
"console_cat_utility": "实用工具",
|
||||
"console_cat_wallet": "钱包",
|
||||
"console_clear": "清除",
|
||||
@@ -269,11 +322,14 @@
|
||||
"console_help_help": " help - 显示此帮助信息",
|
||||
"console_help_setgenerate": " setgenerate - 控制挖矿",
|
||||
"console_help_stop": " stop - 停止守护进程",
|
||||
"console_last_error": "上次错误:",
|
||||
"console_line_count": "%zu 行",
|
||||
"console_matches": "个匹配",
|
||||
"console_new_lines": "%d 新行",
|
||||
"console_no_daemon": "无守护进程",
|
||||
"console_no_output": "(无输出)",
|
||||
"console_not_connected": "错误:未连接到守护进程",
|
||||
"console_not_connected_lite": "错误:没有打开的钱包",
|
||||
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。",
|
||||
"console_ref_builds": "生成",
|
||||
"console_ref_cancel": "取消",
|
||||
@@ -289,12 +345,14 @@
|
||||
"console_ref_run_confirm": "立即运行 %s?这是一个有重大影响的命令。",
|
||||
"console_ref_search_hint": "按名称或用途搜索…",
|
||||
"console_ref_select_hint": "选择一个命令以查看其功能。",
|
||||
"console_ref_value": "值",
|
||||
"console_rpc_reference": "RPC 命令参考",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "控制台扫描线",
|
||||
"console_search_commands": "搜索命令...",
|
||||
"console_select_all": "全选",
|
||||
"console_show_app_output": "显示[应用]钱包日志行",
|
||||
"console_show_backend_ref": "显示后端命令参考",
|
||||
"console_show_daemon_output": "显示守护进程输出",
|
||||
"console_show_errors_only": "仅显示错误",
|
||||
"console_show_rpc_ref": "显示 RPC 命令参考",
|
||||
@@ -307,6 +365,7 @@
|
||||
"console_status_stopped": "已停止",
|
||||
"console_status_stopping": "停止中",
|
||||
"console_status_unknown": "未知",
|
||||
"console_stop_confirm_node": "'stop' 将关闭节点并断开钱包连接。再次输入 'stop' 以确认。",
|
||||
"console_tab_completion": "Tab 补全",
|
||||
"console_text_colors": "文本颜色",
|
||||
"console_toggle_accents": "切换行颜色强调",
|
||||
@@ -334,8 +393,15 @@
|
||||
"contact_preview_name": "联系人名称",
|
||||
"contact_wallet_loading": "钱包仍在加载——请选中“在每个钱包中显示”,或稍后再试。",
|
||||
"contacts": "联系人",
|
||||
"contacts_avatar_shape": "头像形状",
|
||||
"contacts_list_scale": "列表缩放",
|
||||
"contacts_search_no_match": "没有匹配的联系人",
|
||||
"contacts_search_placeholder": "搜索联系人...",
|
||||
"contacts_settings_tip": "联系人自定义",
|
||||
"contacts_settings_title": "联系人设置",
|
||||
"contacts_shape_circle": "圆形",
|
||||
"contacts_shape_square": "方形",
|
||||
"contacts_shape_tab": "左标签",
|
||||
"copied": "已复制!",
|
||||
"copy": "复制",
|
||||
"copy_address": "复制完整地址",
|
||||
@@ -578,6 +644,7 @@
|
||||
"lite_birthday_backup": "生日区块:%llu (也请一并备份)",
|
||||
"lite_birthday_hint": "开始扫描的区块高度。如未知请保留 0(完整扫描更慢)。",
|
||||
"lite_birthday_label": "诞生区块",
|
||||
"lite_console_backend_commands": "后端命令:",
|
||||
"lite_console_help_passthrough": "其他任何输入都将作为轻钱包控制台命令运行。",
|
||||
"lite_copy": "复制",
|
||||
"lite_could_not_write": "无法写入 ",
|
||||
@@ -595,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "检查中…",
|
||||
"lite_net_connected": "已连接",
|
||||
"lite_net_connecting": "连接中…",
|
||||
"lite_net_custom": "自定义",
|
||||
"lite_net_disconnected": "未连接",
|
||||
"lite_net_hidden_section": "隐藏的服务器",
|
||||
@@ -682,6 +750,9 @@
|
||||
"market_cap": "市值",
|
||||
"market_cap_short": "市值",
|
||||
"market_chart_loading": "正在加载价格历史",
|
||||
"market_col_name": "名称",
|
||||
"market_col_trend": "趋势",
|
||||
"market_col_value": "价值",
|
||||
"market_iv_1d": "1天",
|
||||
"market_iv_1h": "1时",
|
||||
"market_iv_1m": "1M",
|
||||
@@ -690,13 +761,18 @@
|
||||
"market_no_history": "无价格历史",
|
||||
"market_no_price": "无价格数据",
|
||||
"market_now": "现在",
|
||||
"market_opt_chart_style": "图表样式",
|
||||
"market_pct_shielded": "%.0f%% 屏蔽",
|
||||
"market_portfolio": "投资组合",
|
||||
"market_price_loading": "正在加载价格数据...",
|
||||
"market_price_unavailable": "价格数据不可用",
|
||||
"market_refresh_price": "刷新价格数据",
|
||||
"market_settings_tip": "市场选项",
|
||||
"market_settings_title": "市场设置",
|
||||
"market_style_candle": "切换到蜡烛图",
|
||||
"market_style_candle_label": "K线",
|
||||
"market_style_line": "切换到折线图",
|
||||
"market_style_line_label": "折线",
|
||||
"market_trade_on": "在 %s 交易",
|
||||
"market_updated": "\\xc2\\xb7 已更新 %s",
|
||||
"market_vol_short": "成交量",
|
||||
@@ -1000,9 +1076,9 @@
|
||||
"portfolio_spark_min": "分钟",
|
||||
"portfolio_spark_month": "月",
|
||||
"portfolio_spark_week": "周",
|
||||
"portfolio_style_compact": "紧凑行",
|
||||
"portfolio_style_detailed": "详细行",
|
||||
"portfolio_style_featured": "特色行",
|
||||
"portfolio_style_compact": "表格",
|
||||
"portfolio_style_detailed": "卡片",
|
||||
"portfolio_style_featured": "聚焦",
|
||||
"portfolio_style_label": "投资组合样式",
|
||||
"portfolio_untitled": "未命名",
|
||||
"portfolio_wallet_loading": "请等待钱包加载完成后再添加分组。",
|
||||
@@ -1384,10 +1460,20 @@
|
||||
"tt_blur": "模糊程度(0%% = 关闭,100%% = 最大)",
|
||||
"tt_change_pass": "更改钱包加密密码",
|
||||
"tt_change_pin": "更改您的解锁 PIN",
|
||||
"tt_chat_bubble_accent": "你发出的消息气泡的强调色(或跟随当前主题)",
|
||||
"tt_chat_bubble_style": "消息气泡形状:圆角、方形或极简(扁平、无边框)",
|
||||
"tt_chat_density": "消息之间的间距:宽松增加更多留白;紧凑在屏幕上容纳更多内容",
|
||||
"tt_chat_emoji_style": "以单色轮廓或全彩渲染表情符号",
|
||||
"tt_chat_enter_sends": "开启时,Enter 发送消息,Shift+Enter 换行;关闭时,Enter 换行",
|
||||
"tt_chat_font_size": "将聊天消息文字从 0.8x 缩放到 1.5x。仅影响聊天标签页,不影响应用的其余部分",
|
||||
"tt_chat_poll_rate": "检查新消息和 0-conf 消息的频率(0.5-15 s)。越快响应越及时,但占用更多 CPU",
|
||||
"tt_chat_timestamp": "仅此标签页的时间戳格式:跟随全应用时钟,或强制使用 24-hour 或 12-hour",
|
||||
"tt_clear_ztx": "删除本地缓存的 z-交易历史",
|
||||
"tt_clock_format": "24 或 12 小时制,应用全局。聊天可覆盖。",
|
||||
"tt_custom_fees": "发送交易时启用手动费用输入",
|
||||
"tt_custom_theme": "自定义主题已激活",
|
||||
"tt_daemon_install_bundled": "停止节点,用此钱包版本内置的 dragonxd 覆盖已安装的版本,然后重启",
|
||||
"tt_daemon_refresh": "重新读取上方显示的已安装及内置 dragonxd 版本、大小和日期",
|
||||
"tt_daemon_update_check": "从项目 Gitea 下载并验证最新的 dragonxd 全节点,然后重启以应用",
|
||||
"tt_debug_collapse": "折叠调试日志选项",
|
||||
"tt_debug_expand": "展开调试日志选项",
|
||||
@@ -1405,7 +1491,30 @@
|
||||
"tt_keep_daemon": "运行设置向导时守护进程仍会停止",
|
||||
"tt_language": "钱包界面语言",
|
||||
"tt_layout_hotkey": "快捷键:左/右箭头键切换余额布局",
|
||||
"tt_lite_copy": "将显示的机密复制到剪贴板",
|
||||
"tt_lite_decrypt_pass": "输入你的密码以移除钱包的加密",
|
||||
"tt_lite_encrypt": "用上方的密码加密钱包;加密后立即锁定,需要该密码才能解锁",
|
||||
"tt_lite_encrypt_pass": "用于加密钱包的密码。若丢失,钱包将无法解锁或恢复",
|
||||
"tt_lite_hide_wipe": "隐藏显示的机密并将其从内存中安全擦除",
|
||||
"tt_lite_import_key": "粘贴要导入的私有花费或查看密钥;其历史记录会在下次同步后出现",
|
||||
"tt_lite_import_key_btn": "将输入的私钥导入此钱包;资金和历史记录会在下次同步后出现",
|
||||
"tt_lite_lifecycle_op": "选择是创建新钱包、打开现有钱包,还是从助记词恢复钱包",
|
||||
"tt_lite_lifecycle_pass": "在此次创建 / 打开 / 恢复操作中用于解锁或设置钱包的密码",
|
||||
"tt_lite_lifecycle_run": "使用上方的值执行所选的创建 / 打开 / 恢复操作",
|
||||
"tt_lite_lifecycle_toggle": "显示或隐藏用于管理轻钱包文件的创建 / 打开 / 恢复控件",
|
||||
"tt_lite_lock": "立即锁定钱包;解锁需要密码,任何聊天会话都会被中断",
|
||||
"tt_lite_redownload": "从轻钱包服务器重新下载并重新扫描所有区块",
|
||||
"tt_lite_remove_encrypt": "移除加密并以未受保护的方式存储钱包;打开它将不再需要密码",
|
||||
"tt_lite_restore_account": "要恢复的 HD 账户索引;除非你在此助记词下使用了多个账户,否则保持为 0",
|
||||
"tt_lite_restore_birthday": "钱包创建时的区块高度;扫描从此处开始。不确定时请填 0 或最早的高度",
|
||||
"tt_lite_restore_overwrite": "用此次恢复替换现有的钱包文件。警告:这会覆盖当前的钱包数据",
|
||||
"tt_lite_restore_seed": "用于恢复此钱包的 24-word 助记词恢复短语;输入时会隐藏",
|
||||
"tt_lite_save_seed_file": "将助记词和创建高度写入配置文件夹中一个仅所有者可读的文件(lite-seed-backup.txt)",
|
||||
"tt_lite_show_keys": "显示此钱包的私有花费密钥。任何拥有密钥的人都能动用它所控制的资金",
|
||||
"tt_lite_show_seed": "显示此钱包的助记词恢复短语和创建高度。任何拥有助记词的人都能动用你的资金",
|
||||
"tt_lite_unlock": "使用上方的密码解锁已加密的钱包",
|
||||
"tt_lite_unlock_pass": "输入你的密码以解锁已加密的钱包",
|
||||
"tt_lite_wallet_path": "要打开或恢复到的钱包文件路径或名称",
|
||||
"tt_lock": "立即锁定钱包",
|
||||
"tt_low_spec": "禁用所有重度视觉效果\\n快捷键:Ctrl+Shift+Down",
|
||||
"tt_merge": "将多个 UTXO 合并到一个地址",
|
||||
@@ -1426,12 +1535,17 @@
|
||||
"tt_rpc_host": "DragonX 守护进程主机名",
|
||||
"tt_rpc_pass": "RPC 认证密码",
|
||||
"tt_rpc_port": "守护进程 RPC 连接端口",
|
||||
"tt_rpc_toggle": "显示或隐藏守护进程的只读 RPC 连接详情(主机、端口、用户、密码)",
|
||||
"tt_rpc_user": "RPC 认证用户名",
|
||||
"tt_save_settings": "将所有设置保存到磁盘",
|
||||
"tt_save_ztx": "将 z-address 交易历史存储在本地以加快加载速度",
|
||||
"tt_scan_themes": "扫描新主题。\\n将主题文件夹放在:\\n%s",
|
||||
"tt_scanline": "控制台中的 CRT 扫描线效果",
|
||||
"tt_screenshot_open_dir": "在你的文件管理器中打开截图文件夹(位于配置目录下)",
|
||||
"tt_screenshot_sweep": "在每个标签页遍历每种主题,将每种主题的截图保存到配置文件夹的 screenshots 目录(覆盖上一次遍历)",
|
||||
"tt_screenshot_sweep_full": "与主题遍历类似,但同时捕获每个使用临时离线演示钱包数据的模态框 / 对话框 / 流程",
|
||||
"tt_seed_backup": "显示并备份您钱包的 24 词恢复助记词",
|
||||
"tt_seed_demo_chat": "向聊天标签页注入示例对话,以便遍历能捕获其界面;仅在内存中,重启后消失",
|
||||
"tt_seed_migrate": "创建一个新的助记词钱包并将您的资金转入其中",
|
||||
"tt_set_pin": "设置 4-8 位 PIN 以快速解锁",
|
||||
"tt_shield_mining": "将透明挖矿奖励转移到屏蔽地址",
|
||||
@@ -1450,6 +1564,7 @@
|
||||
"tt_website": "打开 DragonX 网站",
|
||||
"tt_window_opacity": "背景不透明度(越低 = 桌面透过窗口可见)",
|
||||
"tt_wizard": "重新运行初始设置向导\\n守护进程将被重启",
|
||||
"tx_chat_badge": "消息",
|
||||
"tx_confirmations": "%d 次确认",
|
||||
"tx_details_title": "交易详情",
|
||||
"tx_from_address": "发送地址:",
|
||||
|
||||
190
res/themes/jade.toml
Normal file
190
res/themes/jade.toml
Normal file
@@ -0,0 +1,190 @@
|
||||
[theme]
|
||||
name = "Jade"
|
||||
author = "The Hush Developers"
|
||||
dark = true
|
||||
elevation = { --elevation-0 = "#071210", --elevation-1 = "#0C1A16", --elevation-2 = "#16261F", --elevation-3 = "#1D3128", --elevation-4 = "#243B30" }
|
||||
images = { background_image = "backgrounds/texture/jade_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
||||
|
||||
[theme.palette]
|
||||
--primary = "#2FA07A"
|
||||
--primary-variant = "#1E7357"
|
||||
--primary-light = "#7FD1B5"
|
||||
--secondary = "#C9A24E"
|
||||
--secondary-variant = "#A8842F"
|
||||
--secondary-light = "#E0C583"
|
||||
--background = "#071210"
|
||||
--surface = "#0C1A16"
|
||||
--surface-variant = "#16261F"
|
||||
--on-primary = "#FFFFFF"
|
||||
--on-secondary = "#000000"
|
||||
--on-background = "#DCEDE4"
|
||||
--on-surface = "#DCEDE4"
|
||||
--on-surface-medium = "rgba(220,237,228,0.85)"
|
||||
--on-surface-disabled = "rgba(220,237,228,0.58)"
|
||||
--error = "#CF6679"
|
||||
--on-error = "#000000"
|
||||
--success = "#81C784"
|
||||
--on-success = "#000000"
|
||||
--warning = "#FFB74D"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(130,205,170,0.14)"
|
||||
--outline = "rgba(130,205,170,0.16)"
|
||||
--scrim = "rgba(0,0,0,0.6)"
|
||||
--surface-hover = "rgba(130,205,170,0.07)"
|
||||
--surface-alt = "rgba(130,205,170,0.05)"
|
||||
--surface-active = "rgba(130,205,170,0.10)"
|
||||
--glass-button = "rgba(130,205,170,0.06)"
|
||||
--glass-button-hover = "rgba(130,205,170,0.12)"
|
||||
--card-border = "rgba(130,205,170,0.26)"
|
||||
--text-shadow = "rgba(0,0,0,0.50)"
|
||||
--input-overlay-text = "rgba(220,237,228,0.30)"
|
||||
--slider-text = "rgba(220,237,228,0.85)"
|
||||
--thumb-fill = "rgba(130,205,170,0.15)"
|
||||
--thumb-border = "rgba(130,205,170,0.50)"
|
||||
--disabled-label = "rgba(130,205,170,0.18)"
|
||||
--chart-grid = "rgba(130,205,170,0.05)"
|
||||
--chart-crosshair = "rgba(130,205,170,0.15)"
|
||||
--chart-hover-ring = "rgba(130,205,170,0.30)"
|
||||
--tooltip-bg = "rgba(9,20,16,0.92)"
|
||||
--tooltip-border = "rgba(130,205,170,0.12)"
|
||||
--glass-fill = "rgba(130,205,170,0.08)"
|
||||
--glass-border = "rgba(47,160,122,0.30)"
|
||||
--glass-noise-tint = "rgba(130,205,170,0.03)"
|
||||
--tactile-top = "rgba(130,205,170,0.06)"
|
||||
--tactile-bottom = "rgba(130,205,170,0.0)"
|
||||
--hover-overlay = "rgba(130,205,170,0.05)"
|
||||
--active-overlay = "rgba(130,205,170,0.10)"
|
||||
--rim-light = "rgba(130,205,170,0.14)"
|
||||
--status-divider = "rgba(130,205,170,0.08)"
|
||||
--sidebar-hover = "rgba(130,205,170,0.10)"
|
||||
--sidebar-icon = "rgba(130,205,170,0.42)"
|
||||
--sidebar-badge = "rgba(220,237,228,1.0)"
|
||||
--sidebar-divider = "rgba(130,205,170,0.06)"
|
||||
--chart-line = "rgba(130,205,170,0.10)"
|
||||
--window-control = "rgba(220,237,228,0.78)"
|
||||
--window-control-hover = "rgba(130,205,170,0.12)"
|
||||
--window-close-hover = "rgba(232,17,35,0.78)"
|
||||
--spinner-track = "rgba(130,205,170,0.10)"
|
||||
--spinner-active = "rgba(79,184,154,0.85)"
|
||||
--shutdown-panel-bg = "rgba(7,18,14,0.90)"
|
||||
--shutdown-panel-border = "rgba(130,205,170,0.07)"
|
||||
--ram-bar-app = "#2FA07A"
|
||||
--ram-bar-system = "rgba(255,255,255,0.18)"
|
||||
--accent-total = "#7FD1B5"
|
||||
--accent-shielded = "#4FB89A"
|
||||
--accent-transparent = "#C9A24E"
|
||||
--accent-action = "#2FA07A"
|
||||
--accent-market = "#4FB89A"
|
||||
--accent-portfolio = "#7FD1B5"
|
||||
--toast-info-accent = "#2FA07A"
|
||||
--toast-info-text = "#7FD1B5"
|
||||
--toast-success-accent = "rgba(50,180,80,1.0)"
|
||||
--toast-success-text = "rgba(180,255,180,1.0)"
|
||||
--toast-warning-accent = "rgba(204,166,50,1.0)"
|
||||
--toast-warning-text = "rgba(255,230,130,1.0)"
|
||||
--toast-error-accent = "rgba(204,64,64,1.0)"
|
||||
--toast-error-text = "rgba(255,153,153,1.0)"
|
||||
--snackbar-bg = "rgba(24,40,34,0.95)"
|
||||
--snackbar-text = "rgba(220,237,228,0.87)"
|
||||
--snackbar-action = "rgba(79,184,154,1.0)"
|
||||
--snackbar-action-hover = "rgba(127,209,181,1.0)"
|
||||
--switch-track-off = "rgba(130,205,170,0.12)"
|
||||
--switch-track-on = "rgba(47,160,122,0.50)"
|
||||
--switch-thumb-off = "#A0C0B4"
|
||||
--switch-thumb-on = "#DCEDE4"
|
||||
--control-shadow = "rgba(0,0,0,0.24)"
|
||||
--checkbox-check = "#000000"
|
||||
--app-bar-shadow = "rgba(0,0,0,0.25)"
|
||||
|
||||
[backdrop]
|
||||
base-color-top = "rgba(14,32,26,210)"
|
||||
base-color-bottom = "rgba(6,18,14,210)"
|
||||
texture-tint-alpha = 120
|
||||
gradient-top-r = 10
|
||||
gradient-top-g = 30
|
||||
gradient-top-b = 22
|
||||
gradient-top-a = 90
|
||||
gradient-bottom-r = 5
|
||||
gradient-bottom-g = 16
|
||||
gradient-bottom-b = 12
|
||||
gradient-bottom-a = 70
|
||||
background-alpha = 0.42
|
||||
surface-alpha = 0.56
|
||||
frame-alpha = 0.78
|
||||
surface-inline-alpha = 0.58
|
||||
background-inline-alpha = 0.40
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Theme Visual Effects — Jade (veins of gold shifting through the stone)
|
||||
# Jade's signature is a slow jade→gold color-shifting border on every glass
|
||||
# panel + the active nav button — a vein of gold surfacing through nephrite.
|
||||
# It's drawn via AddRect so it hugs the real rounded corners (no polygonal
|
||||
# edge-trace). Sparse jade motes drift up the viewport. No other theme turns
|
||||
# gradient-border-panels on, so the panel-wide vein is Jade's own —
|
||||
# deliberately NOT Obsidian's specular glare.
|
||||
# ---------------------------------------------------------------------------
|
||||
[effects]
|
||||
hue-cycle-enabled = { size = 0.0 }
|
||||
rainbow-border-enabled = { size = 0.0 }
|
||||
|
||||
# No shimmer sweep — replaced by specular glare
|
||||
shimmer-enabled = { size = 0.0 }
|
||||
|
||||
positional-hue-enabled = { size = 0.0 }
|
||||
|
||||
glow-pulse-enabled = { size = 0.0 }
|
||||
|
||||
# Edge-trace OFF — its hand-walked perimeter chamfers rounded corners.
|
||||
# Jade's vein is the gradient-border below (corner-clean via AddRect).
|
||||
edge-trace-enabled = { size = 0.0 }
|
||||
edge-trace-speed = { size = 0.16 }
|
||||
edge-trace-length = { size = 0.34 }
|
||||
edge-trace-thickness = { size = 1.6 }
|
||||
edge-trace-alpha = { size = 0.55 }
|
||||
edge-trace-color = { color = "#C9A24E" }
|
||||
|
||||
# Specular glare OFF — that's Obsidian's signature; Jade shouldn't echo it.
|
||||
specular-glare-enabled = { size = 0.0 }
|
||||
specular-glare-speed = { size = 0.018 }
|
||||
specular-glare-intensity = { size = 0.008 }
|
||||
specular-glare-radius = { size = 0.65 }
|
||||
specular-glare-count = { size = 1.0 }
|
||||
specular-glare-color = { color = "rgba(150,220,180,1.0)" }
|
||||
|
||||
# HERO — vein of gold: a slow jade→gold color-shifting border on the active
|
||||
# nav button AND (via gradient-border-panels) every glass panel. Drawn with
|
||||
# AddRect so it follows the rounded corners exactly. Panels drift at a softer
|
||||
# alpha and position-phased offset, so a screenful reads like veins at
|
||||
# different depths rather than one synchronized pulse.
|
||||
gradient-border-enabled = { size = 1.0 }
|
||||
gradient-border-panels = { size = 1.0 }
|
||||
gradient-border-speed = { size = 0.10 }
|
||||
gradient-border-thickness = { size = 1.5 }
|
||||
gradient-border-alpha = { size = 0.55 }
|
||||
gradient-border-color-a = { color = "#7FD1B5" }
|
||||
gradient-border-color-b = { color = "#C9A24E" }
|
||||
|
||||
# Ambient jade motes — sparse, slow, cool green particles drifting up the
|
||||
# viewport (recolored ember-rise; a different mood from dragonx's fire embers).
|
||||
ember-rise-enabled = { size = 1.0 }
|
||||
ember-rise-count = { size = 5.0 }
|
||||
ember-rise-speed = { size = 0.18 }
|
||||
ember-rise-particle-size = { size = 1.4 }
|
||||
ember-rise-alpha = { size = 0.26 }
|
||||
ember-rise-color = { color = "#7FD1B5" }
|
||||
|
||||
# Shader-like viewport overlay — deep green stone atmosphere
|
||||
viewport-wash-enabled = { size = 1.0 }
|
||||
viewport-wash-alpha = { size = 0.05 }
|
||||
viewport-wash-tl = { color = "#12402E" }
|
||||
viewport-wash-tr = { color = "#0E3828" }
|
||||
viewport-wash-bl = { color = "#16442E" }
|
||||
viewport-wash-br = { color = "#1A4A34" }
|
||||
viewport-wash-rotate = { size = 0.015 }
|
||||
viewport-wash-pulse = { size = 0.0 }
|
||||
viewport-wash-pulse-depth = { size = 0.0 }
|
||||
|
||||
viewport-vignette-enabled = { size = 1.0 }
|
||||
viewport-vignette-color = { color = "#04140D" }
|
||||
viewport-vignette-radius = { size = 0.22 }
|
||||
viewport-vignette-alpha = { size = 0.15 }
|
||||
94
scripts/build-freetype-mingw.sh
Executable file
94
scripts/build-freetype-mingw.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cross-build a MINIMAL static FreeType for the mingw-w64 (Windows) target.
|
||||
#
|
||||
# Why: the wallet's optional color-emoji rendering needs FreeType (to rasterize the COLR/CPAL Twemoji
|
||||
# font). Native Linux/macOS pick up the system FreeType via find_package; the Debian/Ubuntu mingw-w64
|
||||
# cross toolchain ships no FreeType, so we build one here. The Twemoji font is COLRv0 (layered vector),
|
||||
# which FreeType renders WITHOUT libpng / harfbuzz / brotli / zlib — so this is a dependency-free static
|
||||
# build (no external libs to also cross-compile), producing a self-contained libfreetype.a.
|
||||
#
|
||||
# Output: <prefix>/include/freetype2/... + <prefix>/lib/libfreetype.a (default prefix: third_party/freetype-mingw)
|
||||
# build.sh --win-release runs this automatically and passes -DDRAGONX_MINGW_FREETYPE_PREFIX to CMake.
|
||||
set -euo pipefail
|
||||
|
||||
FT_VERSION="2.13.3"
|
||||
FT_SHA256="5c3a8e78f7b24c20b25b54ee575d6daa40007a5f4eea2845861c3409b3021747" # freetype-2.13.3.tar.gz
|
||||
FT_URL="https://download.savannah.gnu.org/releases/freetype/freetype-${FT_VERSION}.tar.gz"
|
||||
FT_URL_MIRROR="https://downloads.sourceforge.net/project/freetype/freetype2/${FT_VERSION}/freetype-${FT_VERSION}.tar.gz"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PREFIX="${1:-$SCRIPT_DIR/third_party/freetype-mingw}"
|
||||
WORK="$SCRIPT_DIR/third_party/.freetype-mingw-build"
|
||||
|
||||
# Already built? (libfreetype.a present) → nothing to do.
|
||||
if [[ -f "$PREFIX/lib/libfreetype.a" && -d "$PREFIX/include/freetype2" ]]; then
|
||||
echo "FreeType (mingw) already built at: $PREFIX"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Pick the mingw compilers (posix threads variant preferred, matching build.sh).
|
||||
if command -v x86_64-w64-mingw32-gcc-posix &>/dev/null; then
|
||||
MINGW_GCC=x86_64-w64-mingw32-gcc-posix; MINGW_GXX=x86_64-w64-mingw32-g++-posix
|
||||
elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then
|
||||
MINGW_GCC=x86_64-w64-mingw32-gcc; MINGW_GXX=x86_64-w64-mingw32-g++
|
||||
else
|
||||
echo "ERROR: x86_64-w64-mingw32-gcc not found (install mingw-w64)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$WORK"
|
||||
cd "$WORK"
|
||||
|
||||
TARBALL="freetype-${FT_VERSION}.tar.gz"
|
||||
if [[ ! -f "$TARBALL" ]]; then
|
||||
echo "Downloading FreeType ${FT_VERSION} ..."
|
||||
curl -fsSL -o "$TARBALL" "$FT_URL" || curl -fsSL -o "$TARBALL" "$FT_URL_MIRROR"
|
||||
fi
|
||||
|
||||
echo "Verifying SHA-256 ..."
|
||||
echo "${FT_SHA256} ${TARBALL}" | sha256sum -c - || {
|
||||
echo "ERROR: FreeType tarball checksum mismatch (expected ${FT_SHA256})." >&2
|
||||
echo " got: $(sha256sum "$TARBALL" | cut -d' ' -f1)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rm -rf "freetype-${FT_VERSION}"
|
||||
tar xf "$TARBALL"
|
||||
SRC="$WORK/freetype-${FT_VERSION}"
|
||||
|
||||
# Minimal mingw toolchain for FreeType's own CMake.
|
||||
cat > "$WORK/ft-mingw-toolchain.cmake" <<TOOLCHAIN
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
set(CMAKE_C_COMPILER ${MINGW_GCC})
|
||||
set(CMAKE_CXX_COMPILER ${MINGW_GXX})
|
||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
TOOLCHAIN
|
||||
|
||||
echo "Configuring FreeType (static, no external deps) ..."
|
||||
rm -rf "$WORK/build"
|
||||
cmake -S "$SRC" -B "$WORK/build" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$WORK/ft-mingw-toolchain.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="$PREFIX" \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DFT_DISABLE_ZLIB=ON \
|
||||
-DFT_DISABLE_BZIP2=ON \
|
||||
-DFT_DISABLE_PNG=ON \
|
||||
-DFT_DISABLE_HARFBUZZ=ON \
|
||||
-DFT_DISABLE_BROTLI=ON
|
||||
|
||||
echo "Building + installing FreeType ..."
|
||||
cmake --build "$WORK/build" -j "$(nproc)"
|
||||
cmake --install "$WORK/build"
|
||||
|
||||
if [[ -f "$PREFIX/lib/libfreetype.a" ]]; then
|
||||
echo "OK: mingw FreeType -> $PREFIX/lib/libfreetype.a"
|
||||
else
|
||||
echo "ERROR: build did not produce libfreetype.a" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,5 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script uses bash 4+ features (mapfile, safe empty-array expansion under
|
||||
# `set -u`). macOS ships bash 3.2, so re-exec under a newer bash when one is
|
||||
# present (Homebrew), and fail with a clear message otherwise.
|
||||
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
|
||||
for _newer_bash in /opt/homebrew/bin/bash /usr/local/bin/bash; do
|
||||
[ -x "$_newer_bash" ] && exec "$_newer_bash" "$0" "$@"
|
||||
done
|
||||
echo "ERROR: build-lite-backend-artifact.sh requires bash 4+ (found ${BASH_VERSION:-unknown})." >&2
|
||||
echo " On macOS: brew install bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -67,25 +79,9 @@ Options:
|
||||
--backend-dir PATH SilentDragonXLite/lib source directory.
|
||||
--silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path.
|
||||
--out-dir PATH Output directory for copied artifact and metadata.
|
||||
--artifact PATH Inventory an existing artifact instead of building.
|
||||
--no-build Do not run cargo; requires --artifact.
|
||||
--reproducible Add deterministic Rust path remaps for clean builds.
|
||||
--remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible.
|
||||
--builder NAME Redacted builder/provenance label. Default: local.
|
||||
--signature-required Fail if verified signature metadata is not supplied.
|
||||
--signature-file PATH Existing sidecar signature file to record.
|
||||
--signature-format FORMAT Signature format: minisign, gpg, sigstore, external, or other.
|
||||
--signature-verification-tool T Verification tool and version used by the release builder.
|
||||
--signature-verification-command C
|
||||
Verification command already run by the release builder.
|
||||
--signature-key-fingerprint F Reviewed public-key fingerprint, when applicable.
|
||||
--signature-certificate-identity ID
|
||||
Reviewed certificate identity, when applicable.
|
||||
--signature-certificate-issuer I
|
||||
Reviewed certificate issuer, when applicable.
|
||||
--signature-transparency-log-url URL
|
||||
Transparency log entry, when applicable.
|
||||
--signature-verified-sha256 SHA Artifact SHA-256 verified by the signature check.
|
||||
-j, --jobs N Cargo parallel jobs.
|
||||
--cargo-arg ARG Extra argument forwarded to cargo build.
|
||||
-h, --help Show this help.
|
||||
@@ -95,9 +91,13 @@ Outputs:
|
||||
<out>/<platform>/lite-backend-symbols.txt
|
||||
<out>/<platform>/lite-backend-artifact-manifest.json
|
||||
|
||||
The script captures symbols, checksums, and optional read-only signature
|
||||
verification metadata only. It does not load the library, resolve function
|
||||
pointers, call SDXL, sign, upload, or publish artifacts.
|
||||
The lite backend is always built from the vendored in-tree source
|
||||
(third_party/silentdragonxlite), which is the trust root. Prebuilt artifacts
|
||||
and self-attested signature metadata are NOT accepted (F15-1) — the previous
|
||||
scheme only recorded an unverified "verified" claim. The script captures the
|
||||
freshly-built artifact's symbols and checksum, and records build provenance.
|
||||
It does not load the library, resolve function pointers, call SDXL, sign,
|
||||
upload, or publish artifacts.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -166,15 +166,8 @@ while [[ $# -gt 0 ]]; do
|
||||
OUT_DIR="$(absolute_path "$2")"
|
||||
shift 2
|
||||
;;
|
||||
--artifact)
|
||||
[[ $# -ge 2 ]] || die "--artifact requires a value"
|
||||
ARTIFACT_PATH="$(absolute_path "$2")"
|
||||
BUILD_ARTIFACT=false
|
||||
shift 2
|
||||
;;
|
||||
--no-build)
|
||||
BUILD_ARTIFACT=false
|
||||
shift
|
||||
--artifact|--no-build)
|
||||
die "$1 was removed (F15-1): the lite backend must be built from the vendored in-tree source (third_party/silentdragonxlite); prebuilt artifacts are no longer accepted."
|
||||
;;
|
||||
--reproducible)
|
||||
REPRODUCIBLE=true
|
||||
@@ -191,54 +184,11 @@ while [[ $# -gt 0 ]]; do
|
||||
BUILDER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-required)
|
||||
SIGNATURE_REQUIRED=true
|
||||
shift
|
||||
;;
|
||||
--signature-file|--signature-path)
|
||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||
SIGNATURE_FILE="$(absolute_path "$2")"
|
||||
shift 2
|
||||
;;
|
||||
--signature-format)
|
||||
[[ $# -ge 2 ]] || die "--signature-format requires a value"
|
||||
SIGNATURE_FORMAT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-verification-tool|--signature-tool)
|
||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||
SIGNATURE_VERIFICATION_TOOL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-verification-command)
|
||||
[[ $# -ge 2 ]] || die "--signature-verification-command requires a value"
|
||||
SIGNATURE_VERIFICATION_COMMAND="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-key-fingerprint)
|
||||
[[ $# -ge 2 ]] || die "--signature-key-fingerprint requires a value"
|
||||
SIGNATURE_KEY_FINGERPRINT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-certificate-identity)
|
||||
[[ $# -ge 2 ]] || die "--signature-certificate-identity requires a value"
|
||||
SIGNATURE_CERTIFICATE_IDENTITY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-certificate-issuer)
|
||||
[[ $# -ge 2 ]] || die "--signature-certificate-issuer requires a value"
|
||||
SIGNATURE_CERTIFICATE_ISSUER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-transparency-log-url)
|
||||
[[ $# -ge 2 ]] || die "--signature-transparency-log-url requires a value"
|
||||
SIGNATURE_TRANSPARENCY_LOG_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-verified-sha256)
|
||||
[[ $# -ge 2 ]] || die "--signature-verified-sha256 requires a value"
|
||||
SIGNATURE_VERIFIED_SHA256="$2"
|
||||
shift 2
|
||||
--signature-required|--signature-file|--signature-path|--signature-format|\
|
||||
--signature-verification-tool|--signature-tool|--signature-verification-command|\
|
||||
--signature-key-fingerprint|--signature-certificate-identity|--signature-certificate-issuer|\
|
||||
--signature-transparency-log-url|--signature-verified-sha256)
|
||||
die "signature-attestation flags were removed (F15-1): they recorded a self-attested \"verified\" claim without running any cryptographic verifier. The lite backend is built from the vendored in-tree source, which is the trust root."
|
||||
;;
|
||||
-j|--jobs)
|
||||
[[ $# -ge 2 ]] || die "--jobs requires a value"
|
||||
@@ -374,6 +324,9 @@ prepare_backend_source() {
|
||||
ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src"
|
||||
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
|
||||
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
|
||||
# Honor the pinned Rust toolchain (rust-toolchain.toml) inside the prepared root too,
|
||||
# so builds using --silentdragonxlitelib-dir still select rustc 1.63.
|
||||
[[ -f "$BACKEND_SOURCE_DIR/rust-toolchain.toml" ]] && ln -s "$BACKEND_SOURCE_DIR/rust-toolchain.toml" "$prepared_root/rust-toolchain.toml"
|
||||
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
|
||||
# Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is
|
||||
# "vendor" relative to the build root, so expose it inside the prepared root too.
|
||||
@@ -766,6 +719,7 @@ MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json"
|
||||
printf ' },\n'
|
||||
printf ' "provenance": {\n'
|
||||
printf ' "owner_ready": true,\n'
|
||||
printf ' "built_from_source": true,\n'
|
||||
printf ' "metadata_provided": true,\n'
|
||||
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
|
||||
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'
|
||||
|
||||
@@ -24,18 +24,27 @@ if [ ! -f "${BUILD_DIR}/bin/ObsidianDragon" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for appimagetool
|
||||
# Check for appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
|
||||
# The old "continuous" tag is a moving, unverified network download that runs on the release
|
||||
# builder; verify it or refuse to package.
|
||||
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
|
||||
APPIMAGETOOL=""
|
||||
if command -v appimagetool &> /dev/null; then
|
||||
APPIMAGETOOL="appimagetool"
|
||||
elif [ -f "${BUILD_DIR}/appimagetool-x86_64.AppImage" ]; then
|
||||
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
|
||||
else
|
||||
print_status "Downloading appimagetool..."
|
||||
wget -q -O "${BUILD_DIR}/appimagetool-x86_64.AppImage" \
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x "${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
AT="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
if [ ! -f "$AT" ] || ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
|
||||
print_status "Downloading appimagetool 1.9.0 (pinned)..."
|
||||
wget -q -O "$AT" "$APPIMAGETOOL_URL"
|
||||
if ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
|
||||
print_error "appimagetool SHA-256 verification failed — refusing to use it"
|
||||
rm -f "$AT"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$AT"
|
||||
fi
|
||||
APPIMAGETOOL="$AT"
|
||||
fi
|
||||
|
||||
print_status "Creating AppDir structure..."
|
||||
|
||||
@@ -256,8 +256,8 @@ HEADER_START
|
||||
echo -e "${YELLOW}Note: Daemon binaries not found in prebuilt-binaries/dragonxd-win/ — wallet only${NC}"
|
||||
fi
|
||||
|
||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
if [ -f "$XMRIG_DIR/xmrig.exe" ]; then
|
||||
cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe"
|
||||
echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sign dragonx full-node release archives for the wallet's in-app daemon updater (ed25519).
|
||||
# Package the prebuilt dragonx full-node binaries into per-platform release archives and sign them
|
||||
# for the wallet's in-app daemon updater (ed25519 over the EXACT archive bytes).
|
||||
#
|
||||
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
|
||||
# key pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is
|
||||
# MANDATORY (kDaemonRequireSignature = true): an in-app update is refused unless a valid signature
|
||||
# is published. For each archive <name>.zip this produces <name>.zip.sig holding the base64 of the
|
||||
# raw 64-byte ed25519 signature — upload that .sig next to the .zip as a release asset.
|
||||
# The wallet verifies a detached ed25519 signature over the archive bytes against a public key
|
||||
# pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is MANDATORY
|
||||
# (kDaemonRequireSignature = true): an in-app update is refused unless a valid "<archive>.sig" is
|
||||
# published next to the archive. The wallet also checks each archive's SHA-256 against a markdown
|
||||
# checksum table in the release body, so `release` prints that table for you to paste in.
|
||||
#
|
||||
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032),
|
||||
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible
|
||||
# (the same flow the wallet's unit tests verify for the miner updater).
|
||||
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl. OpenSSL's ed25519 is PureEdDSA (RFC 8032), the
|
||||
# same primitive libsodium's crypto_sign_verify_detached checks, so the signatures are compatible.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>...# -> <file>.sig per file
|
||||
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>... # sign existing files -> <file>.sig
|
||||
# scripts/sign-daemon-release.sh release <secret.key> <version> [--src DIR] [--out DIR]
|
||||
# # zip prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,macos,
|
||||
# # win64}.zip, sign each, and print the SHA-256 checksum table. Platforms with no dragonxd
|
||||
# # binary staged are skipped.
|
||||
#
|
||||
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
|
||||
# Keep the secret key (.ed25519.key) OFFLINE (mode 600). Paste the base64 public key into
|
||||
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
|
||||
|
||||
@@ -27,6 +33,26 @@ command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25
|
||||
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
|
||||
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
|
||||
|
||||
sha256_of() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}';
|
||||
else shasum -a 256 "$1" | awk '{print $1}'; fi
|
||||
}
|
||||
|
||||
# Detached ed25519 signature over the raw file bytes -> <file>.sig (base64 of the 64-byte sig).
|
||||
sign_file() {
|
||||
local key="$1" f="$2" raw
|
||||
raw="$(mktemp)"
|
||||
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
||||
openssl base64 -A -in "$raw" > "$f.sig"
|
||||
printf '\n' >> "$f.sig"
|
||||
rm -f "$raw"
|
||||
}
|
||||
|
||||
# platform -> (staging dir under prebuilt-binaries, release token, expected daemon binary name)
|
||||
plat_dir() { case "$1" in linux) echo dragonxd-linux;; mac) echo dragonxd-mac;; win) echo dragonxd-win;; esac; }
|
||||
plat_token() { case "$1" in linux) echo linux-amd64;; mac) echo macos;; win) echo win64;; esac; }
|
||||
plat_daemon() { case "$1" in win) echo dragonxd.exe;; *) echo dragonxd;; esac; }
|
||||
|
||||
cmd="${1:-}"; shift || true
|
||||
case "$cmd" in
|
||||
keygen)
|
||||
@@ -42,26 +68,90 @@ case "$cmd" in
|
||||
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
|
||||
echo " $pub"
|
||||
;;
|
||||
|
||||
pubkey)
|
||||
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
|
||||
pubkey_b64 "$1"
|
||||
;;
|
||||
|
||||
sign)
|
||||
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
|
||||
key="$1"; shift
|
||||
[ -f "$key" ] || die "no such key: $key"
|
||||
for f in "$@"; do
|
||||
[ -f "$f" ] || die "no such file: $f"
|
||||
raw="$(mktemp)"
|
||||
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
||||
openssl base64 -A -in "$raw" > "$f.sig"
|
||||
printf '\n' >> "$f.sig"
|
||||
rm -f "$raw"
|
||||
sign_file "$key" "$f"
|
||||
echo "signed: $f -> $f.sig"
|
||||
done
|
||||
echo "Upload each .sig as a release asset next to its archive."
|
||||
;;
|
||||
|
||||
release)
|
||||
[ $# -ge 2 ] || die "usage: release <secret.key> <version> [--src DIR] [--out DIR]"
|
||||
key="$1"; version="$2"; shift 2
|
||||
src="$PROJECT_ROOT/prebuilt-binaries"
|
||||
out="$PROJECT_ROOT/release/daemon"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--src) [ $# -ge 2 ] || die "--src needs a value"; src="$2"; shift 2 ;;
|
||||
--out) [ $# -ge 2 ] || die "--out needs a value"; out="$2"; shift 2 ;;
|
||||
*) die "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[ -f "$key" ] || die "no such key: $key"
|
||||
[ -d "$src" ] || die "no such source dir: $src"
|
||||
command -v zip >/dev/null 2>&1 || die "zip not found (install 'zip')"
|
||||
mkdir -p "$out"
|
||||
|
||||
# Sanity: warn if this key does not match the public key pinned in the wallet (the wallet would
|
||||
# then reject every signature made with it — only expected when deliberately rotating the key).
|
||||
pinned="$(grep -oE '"[A-Za-z0-9+/]{43}="' "$PROJECT_ROOT/src/util/daemon_updater.h" 2>/dev/null | head -1 | tr -d '"')"
|
||||
mine="$(pubkey_b64 "$key")"
|
||||
if [ -n "$pinned" ] && [ "$pinned" != "$mine" ]; then
|
||||
echo "WARNING: this key's public key does not match the one pinned in daemon_updater.h:" >&2
|
||||
echo " signing key -> $mine" >&2
|
||||
echo " pinned key -> $pinned" >&2
|
||||
echo " The wallet will REJECT these signatures unless you are rotating the pinned key." >&2
|
||||
echo >&2
|
||||
fi
|
||||
|
||||
made=0
|
||||
table=""
|
||||
for plat in linux mac win; do
|
||||
d="$src/$(plat_dir "$plat")"
|
||||
daemon="$d/$(plat_daemon "$plat")"
|
||||
if [ ! -f "$daemon" ]; then
|
||||
echo "skip $plat: no $(plat_daemon "$plat") staged in $d" >&2
|
||||
continue
|
||||
fi
|
||||
archive="dragonx-$version-$(plat_token "$plat").zip"
|
||||
apath="$out/$archive"
|
||||
rm -f "$apath"
|
||||
# Zip the staged files at the archive root (binaries + sapling params + asmap), excluding
|
||||
# the .gitkeep placeholder. The updater flattens paths via baseName(), so a flat zip is fine.
|
||||
files=()
|
||||
while IFS= read -r fn; do files+=("$fn"); done < <(cd "$d" && ls -A | grep -vx '.gitkeep')
|
||||
[ "${#files[@]}" -gt 0 ] || { echo "skip $plat: nothing to package in $d" >&2; continue; }
|
||||
( cd "$d" && zip -q -X "$apath" "${files[@]}" )
|
||||
sign_file "$key" "$apath"
|
||||
sum="$(sha256_of "$apath")"
|
||||
table+="| $archive | \`$sum\` |"$'\n'
|
||||
echo "packaged + signed: $apath (+ .sig) sha256=$sum"
|
||||
made=$((made + 1))
|
||||
done
|
||||
[ "$made" -gt 0 ] || die "no platform had a staged daemon binary under $src/dragonxd-{linux,mac,win}/"
|
||||
|
||||
echo
|
||||
echo "Checksum table (paste into the release body so the wallet can verify SHA-256):"
|
||||
echo "| Archive | SHA-256 |"
|
||||
echo "|---|---|"
|
||||
printf '%s' "$table"
|
||||
echo
|
||||
echo "Upload each .zip AND its .zip.sig as release assets. Wallet enforces the ed25519 signature"
|
||||
echo "(kDaemonRequireSignature=true) and the SHA-256 from the table above."
|
||||
;;
|
||||
|
||||
*)
|
||||
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
|
||||
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>... | release <secret.key> <version> [--src DIR] [--out DIR]}"
|
||||
;;
|
||||
esac
|
||||
|
||||
90
setup.sh
90
setup.sh
@@ -133,7 +133,7 @@ pkgs_core_arch="base-devel cmake git pkg-config
|
||||
libxkbcommon wayland libsodium curl
|
||||
autoconf automake libtool wget python xxd"
|
||||
|
||||
pkgs_core_macos="cmake python xxd"
|
||||
pkgs_core_macos="bash cmake python xxd"
|
||||
|
||||
# Windows cross-compile (from Linux)
|
||||
pkgs_win_debian="mingw-w64 zip"
|
||||
@@ -284,18 +284,27 @@ fi
|
||||
header "Windows Cross-Compile"
|
||||
|
||||
if $SETUP_WIN; then
|
||||
win_pkgs="$(get_pkgs win)"
|
||||
if [[ -n "$win_pkgs" ]]; then
|
||||
install_pkgs "$win_pkgs" "Windows cross-compile"
|
||||
fi
|
||||
# Only touch apt / update-alternatives (which need sudo) when the toolchain is missing. If it is
|
||||
# already installed, skip them so `./setup.sh --win` can run WITHOUT sudo — important because the
|
||||
# daemon cross-compile that follows should run as the invoking user. Running the whole setup under
|
||||
# sudo leaves root-owned build artifacts under external/dragonx, which then break `make clean` on
|
||||
# a later non-sudo build (stale objects get relinked -> the mingw link failure recurs).
|
||||
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
|
||||
ok "Windows cross-compile toolchain already present — skipping apt install"
|
||||
else
|
||||
win_pkgs="$(get_pkgs win)"
|
||||
if [[ -n "$win_pkgs" ]]; then
|
||||
install_pkgs "$win_pkgs" "Windows cross-compile"
|
||||
fi
|
||||
|
||||
# Set posix thread model if available
|
||||
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
|
||||
if ! $CHECK_ONLY; then
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
|
||||
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
|
||||
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
||||
# Set posix thread model if available
|
||||
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
|
||||
if ! $CHECK_ONLY; then
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
|
||||
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
|
||||
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -391,11 +400,26 @@ elif $SETUP_SAPLING; then
|
||||
|
||||
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
|
||||
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
|
||||
# Consensus-critical MPC parameters with fixed, well-known SHA-256 (identical across every
|
||||
# Zcash-family node; also pinned in scripts/build-lite-backend-artifact.sh). z.cash is
|
||||
# plain HTTPS with no signature, so verify the digest and refuse a tampered/corrupt file.
|
||||
SPEND_SHA256="8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
|
||||
OUTPUT_SHA256="2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
|
||||
|
||||
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
|
||||
ok "Downloaded sapling-spend.params"
|
||||
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
|
||||
ok "Downloaded sapling-output.params"
|
||||
if curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" \
|
||||
&& echo "${SPEND_SHA256} $PARAMS_DIR/sapling-spend.params" | sha256sum -c --status; then
|
||||
ok "Downloaded + verified sapling-spend.params"
|
||||
else
|
||||
rm -f "$PARAMS_DIR/sapling-spend.params"
|
||||
err "sapling-spend.params download or SHA-256 verification failed — not installed"
|
||||
fi
|
||||
if curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" \
|
||||
&& echo "${OUTPUT_SHA256} $PARAMS_DIR/sapling-output.params" | sha256sum -c --status; then
|
||||
ok "Downloaded + verified sapling-output.params"
|
||||
else
|
||||
rm -f "$PARAMS_DIR/sapling-output.params"
|
||||
err "sapling-output.params download or SHA-256 verification failed — not installed"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
|
||||
@@ -684,11 +708,13 @@ if [[ "$STALE_DAEMON" -eq 1 ]]; then
|
||||
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
|
||||
fi
|
||||
|
||||
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
||||
header "xmrig-hac Mining Binary"
|
||||
# ── 7. drg-xmrig (mining binary) ────────────────────────────────────────────
|
||||
header "drg-xmrig Mining Binary"
|
||||
|
||||
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
|
||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
XMRIG_SRC="$PROJECT_DIR/external/drg-xmrig"
|
||||
# Output dir bundled by build.sh (Linux zip, AppImage, Windows embed, mac .app)
|
||||
# and scripts/legacy/build-windows.sh — keep this path in sync with those.
|
||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
|
||||
# Clean previous prebuilt xmrig binaries so we always rebuild
|
||||
# Only clean the binary for the platform(s) we are actually building,
|
||||
@@ -700,14 +726,14 @@ if ! $CHECK_ONLY; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Helper: clone xmrig-hac if not present
|
||||
# Helper: clone drg-xmrig if not present
|
||||
clone_xmrig_if_needed() {
|
||||
if [[ ! -d "$XMRIG_SRC" ]]; then
|
||||
info "Cloning xmrig-hac..."
|
||||
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC"
|
||||
info "Cloning drg-xmrig..."
|
||||
git clone https://git.dragonx.is/DragonX/drg-xmrig.git "$XMRIG_SRC"
|
||||
else
|
||||
ok "xmrig-hac source already present"
|
||||
info "Pulling latest xmrig-hac..."
|
||||
ok "drg-xmrig source already present"
|
||||
info "Pulling latest drg-xmrig..."
|
||||
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
|
||||
fi
|
||||
}
|
||||
@@ -728,15 +754,15 @@ else
|
||||
rm -rf "$XMRIG_SRC/build"
|
||||
|
||||
# Build dependencies (libuv, hwloc, openssl)
|
||||
info "Building xmrig-hac dependencies (libuv, hwloc, openssl)..."
|
||||
info "Building drg-xmrig dependencies (libuv, hwloc, openssl)..."
|
||||
(
|
||||
cd "$XMRIG_SRC/scripts"
|
||||
sh build_deps.sh
|
||||
)
|
||||
ok "xmrig-hac dependencies built"
|
||||
ok "drg-xmrig dependencies built"
|
||||
|
||||
# Build xmrig
|
||||
info "Building xmrig-hac (Linux)..."
|
||||
info "Building drg-xmrig (Linux)..."
|
||||
mkdir -p "$XMRIG_SRC/build"
|
||||
(
|
||||
cd "$XMRIG_SRC/build"
|
||||
@@ -753,7 +779,7 @@ else
|
||||
mkdir -p "$XMRIG_PREBUILT"
|
||||
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
|
||||
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
|
||||
ok "xmrig (Linux) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||
ok "xmrig (Linux) built and installed to prebuilt-binaries/drg-xmrig/"
|
||||
else
|
||||
err "xmrig (Linux) build failed — binary not found"
|
||||
MISSING=$((MISSING + 1))
|
||||
@@ -777,7 +803,7 @@ else
|
||||
# Clean previous Windows build
|
||||
rm -rf "$XMRIG_SRC/build-windows"
|
||||
|
||||
info "Building xmrig-hac (Windows cross-compile)..."
|
||||
info "Building drg-xmrig (Windows cross-compile)..."
|
||||
(
|
||||
cd "$XMRIG_SRC/scripts"
|
||||
bash build_windows.sh
|
||||
@@ -787,7 +813,7 @@ else
|
||||
mkdir -p "$XMRIG_PREBUILT"
|
||||
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
|
||||
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
|
||||
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/drg-xmrig/"
|
||||
else
|
||||
err "xmrig.exe (Windows) build failed — binary not found"
|
||||
MISSING=$((MISSING + 1))
|
||||
@@ -797,7 +823,7 @@ fi
|
||||
# ── 8. Binary directories ───────────────────────────────────────────────────
|
||||
header "Binary Directories"
|
||||
|
||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
|
||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac drg-xmrig; do
|
||||
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
|
||||
if [[ -d "$dir" ]]; then
|
||||
# Count actual files (not .gitkeep)
|
||||
|
||||
291
src/app.cpp
291
src/app.cpp
@@ -73,6 +73,9 @@
|
||||
#include "util/text_format.h"
|
||||
#include "util/payment_uri.h"
|
||||
#include "util/texture_loader.h"
|
||||
#include "util/svg_texture.h"
|
||||
#include "ui/material/colors.h"
|
||||
#include "logo_dragonx_svg.h"
|
||||
#include "util/bootstrap.h"
|
||||
#include "util/secure_vault.h"
|
||||
#include "resources/embedded_resources.h"
|
||||
@@ -293,6 +296,9 @@ bool App::init()
|
||||
if (!settings_->load()) {
|
||||
DEBUG_LOGF("Warning: Could not load settings, using defaults\n");
|
||||
}
|
||||
// The initial font atlas is built (in main, before App renders) with monochrome emoji. If the saved
|
||||
// setting wants color, request a rebuild so the first preFrame switches to the FreeType color atlas.
|
||||
if (settings_->getChatEmojiColor()) font_rebuild_requested_ = true;
|
||||
// On upgrade (version mismatch), re-save to persist new defaults + current version
|
||||
if (settings_->needsUpgradeSave()) {
|
||||
DEBUG_LOGF("[INFO] Wallet upgraded — re-saving settings with new defaults\n");
|
||||
@@ -532,6 +538,21 @@ void App::preFrame()
|
||||
DEBUG_LOGF("App: Font atlas rebuilt after user font-scale change (%.1fx)\n",
|
||||
ui::Layout::userFontScale());
|
||||
}
|
||||
|
||||
// Keep Typography's emoji-style flag in sync with the setting so any reload (font-scale, DPI, or the
|
||||
// explicit request below) picks up the right emoji font. Inert unless this is a FreeType build.
|
||||
if (settings_) ui::material::Typography::instance().setColorEmoji(settings_->getChatEmojiColor());
|
||||
|
||||
// App-wide clock format (24h/12h) — drives every user-facing timestamp via util::formatClock*.
|
||||
if (settings_) util::setClock12h(settings_->getTimeFormat() == 1);
|
||||
|
||||
// Explicit rebuild request (e.g. the chat color-emoji toggle) — reload picks up the new emoji style.
|
||||
if (font_rebuild_requested_) {
|
||||
font_rebuild_requested_ = false;
|
||||
auto& typo = ui::material::Typography::instance();
|
||||
typo.reload(io, typo.getDpiScale());
|
||||
DEBUG_LOGF("App: Font atlas rebuilt on request (chat emoji style)\n");
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -717,20 +738,40 @@ void App::update()
|
||||
// HushChat (lite): harvest chat memos from the refreshed transactions and thread them
|
||||
// (no-op when the feature is off or no identity; the store dedups across refreshes).
|
||||
ingestLiteChatMemos(liteModel);
|
||||
// Chat note-buffer: recompute the verified/maturing self-note estimates from the fresh model.
|
||||
refreshChatNoteBudget(liteModel);
|
||||
}
|
||||
// Deliver a completed async send/shield result to the waiting send_tab callback.
|
||||
// Deliver a completed async send/shield result. Route by the in-flight op that owns the single
|
||||
// broadcast channel: a user Send-tab send goes to lite_send_callback_; a chat send / contact
|
||||
// request resolves its echo (real status — not the old optimistic "Sent"); a note-buffer split
|
||||
// just logs. Because the controller runs one broadcast at a time, inflight_op_ correlates the
|
||||
// one global result with no txid matching.
|
||||
wallet::LiteBroadcastResult broadcast;
|
||||
if (lite_wallet_->takeBroadcastResult(broadcast)) {
|
||||
// Mirror failures into the lite Console (copyable) in addition to the toast the send UI
|
||||
// shows — transient toasts are easy to miss and impossible to copy.
|
||||
if (!broadcast.ok)
|
||||
wallet::liteLog("Send/shield failed: " + broadcast.error);
|
||||
if (lite_send_callback_) {
|
||||
lite_send_callback_(broadcast.ok, broadcast.ok ? broadcast.txid : broadcast.error);
|
||||
lite_send_callback_ = nullptr;
|
||||
const LiteInflightOp finished = inflight_op_;
|
||||
inflight_op_ = LiteInflightOp{}; // channel is free again
|
||||
switch (finished.kind) {
|
||||
case LiteOpKind::ChatSend:
|
||||
case LiteOpKind::ContactRequest:
|
||||
onChatBroadcastResult(finished, broadcast.ok, broadcast.error);
|
||||
break;
|
||||
case LiteOpKind::Split:
|
||||
// Buffer split done; the next refresh re-counts the new (maturing) notes.
|
||||
break;
|
||||
case LiteOpKind::UserSend:
|
||||
case LiteOpKind::None:
|
||||
default:
|
||||
if (lite_send_callback_) {
|
||||
lite_send_callback_(broadcast.ok, broadcast.ok ? broadcast.txid : broadcast.error);
|
||||
lite_send_callback_ = nullptr;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Startup lock screen: once the first refresh reveals the (auto-opened) wallet is
|
||||
// encrypted, prompt to unlock if it's locked. Soft by design — balances stay viewable via
|
||||
// viewing keys while locked; only spending needs the passphrase, so the user may dismiss
|
||||
@@ -747,6 +788,13 @@ void App::update()
|
||||
// it can act; compiled away when the feature is off (constexpr gate inside).
|
||||
maybeProvisionChatIdentity();
|
||||
|
||||
// Chat note-buffer coordinator (BOTH variants). Full-node only: refresh the per-note counts via a
|
||||
// rate-limited z_listunspent worker scan (lite gets its counts from the refresh model, above). Then
|
||||
// pump: drain queued chat sends against verified notes and build/refill the buffer during idle. Both
|
||||
// self-gate to no-ops unless chat is engaged; the pump serializes to one send outstanding at a time.
|
||||
refreshChatNoteBudgetNode();
|
||||
pumpChatNoteBuffer();
|
||||
|
||||
// One-time reminder to back up the wallet's seed phrase (mnemonic wallets only).
|
||||
maybeRemindSeedBackup();
|
||||
|
||||
@@ -1174,7 +1222,8 @@ void App::update()
|
||||
// normal harvest only re-scans on a new block. Self-gated (no-op without a chat identity /
|
||||
// connection / on lite); the in-flight guard keeps overlapping RPCs from stacking.
|
||||
chat_fast_scan_accum_ += ImGui::GetIO().DeltaTime;
|
||||
if (chat_fast_scan_accum_ >= 2.5f) {
|
||||
const float chatPoll = settings_ ? settings_->getChatPollRateSec() : 2.5f; // user-configurable
|
||||
if (chat_fast_scan_accum_ >= chatPoll) {
|
||||
chat_fast_scan_accum_ = 0.0f;
|
||||
fastScanChatMemos();
|
||||
}
|
||||
@@ -1307,6 +1356,103 @@ void App::handleGlobalShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
// Load (and recolor per theme) the DragonX header logo texture. Called at the top of render() — BEFORE
|
||||
// the first-run-wizard / lock-screen paths — so every screen shows the mark. The SVG is recolored to the
|
||||
// theme accent, so it re-rasterizes whenever the accent or the dark↔light variant changes.
|
||||
void App::ensureLogoTexture()
|
||||
{
|
||||
const bool wantDark = ui::material::IsDarkTheme();
|
||||
const ImU32 logoAccent = ui::material::Primary();
|
||||
if (logo_loaded_ && wantDark == logo_is_dark_variant_ && logoAccent == logo_accent_)
|
||||
return; // already current
|
||||
logo_loaded_ = true;
|
||||
logo_is_dark_variant_ = wantDark;
|
||||
logo_accent_ = logoAccent;
|
||||
// The SVG's white highlight (detail) stays white on dark skins, but darkens to the theme's on-surface
|
||||
// colour on light skins so it doesn't wash out against a light card/background.
|
||||
const ImU32 detailCol = wantDark ? IM_COL32(255, 255, 255, 255) : ui::material::OnSurface();
|
||||
|
||||
// ":drgx:" custom chat emoji — themed to the accent like the logo (re-rasterized on theme change).
|
||||
if (drgx_emoji_tex_) util::DestroyTexture(drgx_emoji_tex_);
|
||||
drgx_emoji_tex_ = 0; drgx_emoji_w_ = 0; drgx_emoji_h_ = 0;
|
||||
util::LoadTextureFromSvg(embedded::kLogoDragonXSvg, 96, logoAccent, detailCol,
|
||||
&drgx_emoji_tex_, &drgx_emoji_w_, &drgx_emoji_h_);
|
||||
|
||||
if (logo_tex_) util::DestroyTexture(logo_tex_); // free the previous texture before replacing
|
||||
logo_tex_ = 0; logo_w_ = 0; logo_h_ = 0;
|
||||
|
||||
// Coin/currency icon (the big DragonX mark on the balance card) — the SAME themed SVG recolored to
|
||||
// the accent, rasterized here so it changes with the skin too (done first, since the header block
|
||||
// below early-returns on success). Falls back to the PNG coin icon if rasterization fails.
|
||||
if (coin_logo_tex_) util::DestroyTexture(coin_logo_tex_);
|
||||
coin_logo_tex_ = 0; coin_logo_w_ = 0; coin_logo_h_ = 0;
|
||||
coin_logo_loaded_ = true;
|
||||
if (!util::LoadTextureFromSvg(embedded::kLogoDragonXSvg, 128, logoAccent, detailCol,
|
||||
&coin_logo_tex_, &coin_logo_w_, &coin_logo_h_)) {
|
||||
auto coinElem = ui::schema::UI().drawElement("components.main-window", "coin-icon");
|
||||
auto cit = coinElem.extraColors.find("icon");
|
||||
std::string coinFile = (cit != coinElem.extraColors.end() && !cit->second.empty())
|
||||
? cit->second : "logos/logo_dragonx_128.png";
|
||||
std::string coinPath = util::getExecutableDirectory() + "/res/img/" + coinFile;
|
||||
std::error_code coinEc;
|
||||
if (!(std::filesystem::exists(coinPath, coinEc) &&
|
||||
util::LoadTextureFromFile(coinPath.c_str(), &coin_logo_tex_, &coin_logo_w_, &coin_logo_h_))) {
|
||||
std::string coinBasename = std::filesystem::path(coinFile).filename().string();
|
||||
const auto* coinRes = resources::getEmbeddedResource(coinBasename);
|
||||
if (coinRes && coinRes->data && coinRes->size > 0)
|
||||
util::LoadTextureFromMemory(coinRes->data, coinRes->size, &coin_logo_tex_, &coin_logo_w_, &coin_logo_h_);
|
||||
}
|
||||
}
|
||||
|
||||
// 0) DragonX mark — rasterize the embedded SVG recolored to the theme (body = accent, detail = white)
|
||||
// at ~2x the 128px viewBox for crisp downscaling. This is the branding on every skin; the per-skin
|
||||
// PNG path below is only a fallback if rasterization ever fails.
|
||||
if (util::LoadTextureFromSvg(embedded::kLogoDragonXSvg, 256, logoAccent,
|
||||
detailCol, &logo_tex_, &logo_w_, &logo_h_)) {
|
||||
DEBUG_LOGF("Rendered DragonX SVG logo (%dx%d, accent %08X)\n", logo_w_, logo_h_, logoAccent);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Fallback — theme-override logo from the active skin
|
||||
const auto* activeSkin = ui::schema::SkinManager::instance().findById(
|
||||
ui::schema::SkinManager::instance().activeSkinId());
|
||||
std::string logoPath;
|
||||
if (activeSkin && !activeSkin->logoPath.empty()) {
|
||||
logoPath = activeSkin->logoPath;
|
||||
} else {
|
||||
// 2) Read icon filename from ui.toml (dark/light variant)
|
||||
auto iconElem = ui::schema::UI().drawElement("components.main-window", "header-icon");
|
||||
const char* iconKey = wantDark ? "icon-dark" : "icon-light";
|
||||
auto it = iconElem.extraColors.find(iconKey);
|
||||
std::string iconFile;
|
||||
if (it != iconElem.extraColors.end() && !it->second.empty())
|
||||
iconFile = it->second;
|
||||
else
|
||||
iconFile = wantDark ? "logos/logo_ObsidianDragon_dark.png" : "logos/logo_ObsidianDragon_light.png";
|
||||
logoPath = util::getExecutableDirectory() + "/res/img/" + iconFile;
|
||||
}
|
||||
// Only attempt the disk read when the file is actually present (dev build / theme drop-in). The
|
||||
// portable single-file build has no res/img/ beside it, so skip straight to the embedded copy.
|
||||
std::error_code logoEc;
|
||||
if (std::filesystem::exists(logoPath, logoEc) &&
|
||||
util::LoadTextureFromFile(logoPath.c_str(), &logo_tex_, &logo_w_, &logo_h_)) {
|
||||
DEBUG_LOGF("Loaded header logo from %s (%dx%d)\n", logoPath.c_str(), logo_w_, logo_h_);
|
||||
} else {
|
||||
std::string embeddedName = std::filesystem::path(logoPath).filename().string();
|
||||
const auto* logoRes = resources::getEmbeddedResource(embeddedName);
|
||||
if (!logoRes || !logoRes->data || logoRes->size == 0)
|
||||
logoRes = resources::getEmbeddedResource(resources::RESOURCE_LOGO);
|
||||
if (logoRes && logoRes->data && logoRes->size > 0) {
|
||||
if (util::LoadTextureFromMemory(logoRes->data, logoRes->size, &logo_tex_, &logo_w_, &logo_h_))
|
||||
DEBUG_LOGF("Loaded header logo from embedded: %s (%dx%d)\n", embeddedName.c_str(), logo_w_, logo_h_);
|
||||
else
|
||||
DEBUG_LOGF("Note: Failed to decode embedded logo (text-only header)\n");
|
||||
} else {
|
||||
DEBUG_LOGF("Note: Header logo not found at %s (text-only header)\n", logoPath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void App::render()
|
||||
{
|
||||
// Advance the screenshot sweep FIRST — before the first-run-wizard early-return below — so the
|
||||
@@ -1314,6 +1460,9 @@ void App::render()
|
||||
// (Pins current_page_ too, before the sidebar reads it further down.)
|
||||
updateScreenshotSweep();
|
||||
|
||||
// DragonX logo — load/recolor before the wizard/lock early-returns so every screen shows it.
|
||||
ensureLogoTexture();
|
||||
|
||||
// First-run wizard gate — blocks all normal UI
|
||||
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
|
||||
renderFirstRunWizard();
|
||||
@@ -1514,94 +1663,9 @@ void App::render()
|
||||
sbStatus.miningActive = state_.mining.generate || state_.pool_mining.xmrig_running;
|
||||
sbStatus.chatUnreadCount = chatUnreadCount(); // unread badge on the Chat nav item (Q1)
|
||||
|
||||
// Load logo texture lazily on first frame (or after theme change)
|
||||
// Also reload when dark↔light mode changes so the correct variant shows
|
||||
{
|
||||
bool wantDark = ui::material::IsDarkTheme();
|
||||
if (!logo_loaded_ || (wantDark != logo_is_dark_variant_)) {
|
||||
logo_loaded_ = true;
|
||||
logo_is_dark_variant_ = wantDark;
|
||||
logo_tex_ = 0; logo_w_ = 0; logo_h_ = 0;
|
||||
// (DragonX logo is loaded at the top of render() via ensureLogoTexture().)
|
||||
|
||||
// 1) Check for theme-override logo from active skin
|
||||
const auto* activeSkin = ui::schema::SkinManager::instance().findById(
|
||||
ui::schema::SkinManager::instance().activeSkinId());
|
||||
std::string logoPath;
|
||||
if (activeSkin && !activeSkin->logoPath.empty()) {
|
||||
logoPath = activeSkin->logoPath;
|
||||
} else {
|
||||
// 2) Read icon filename from ui.toml (dark/light variant)
|
||||
auto iconElem = ui::schema::UI().drawElement("components.main-window", "header-icon");
|
||||
const char* iconKey = wantDark ? "icon-dark" : "icon-light";
|
||||
auto it = iconElem.extraColors.find(iconKey);
|
||||
std::string iconFile;
|
||||
if (it != iconElem.extraColors.end() && !it->second.empty()) {
|
||||
iconFile = it->second;
|
||||
} else {
|
||||
// Fallback filenames
|
||||
iconFile = wantDark ? "logos/logo_ObsidianDragon_dark.png" : "logos/logo_ObsidianDragon_light.png";
|
||||
}
|
||||
logoPath = util::getExecutableDirectory() + "/res/img/" + iconFile;
|
||||
}
|
||||
// Only attempt the disk read when the file is actually present (dev build / theme drop-in).
|
||||
// The portable single-file build has no res/img/ beside it, so skip straight to the
|
||||
// embedded copy instead of logging a spurious "failed to read".
|
||||
std::error_code logoEc;
|
||||
if (std::filesystem::exists(logoPath, logoEc) &&
|
||||
util::LoadTextureFromFile(logoPath.c_str(), &logo_tex_, &logo_w_, &logo_h_)) {
|
||||
DEBUG_LOGF("Loaded header logo from %s (%dx%d)\n", logoPath.c_str(), logo_w_, logo_h_);
|
||||
} else {
|
||||
// Try embedded data fallback — use actual filename from path
|
||||
// so light/dark variants resolve correctly on Windows single-file
|
||||
std::string embeddedName = std::filesystem::path(logoPath).filename().string();
|
||||
const auto* logoRes = resources::getEmbeddedResource(embeddedName);
|
||||
if (!logoRes || !logoRes->data || logoRes->size == 0) {
|
||||
// Final fallback: try the default dark logo constant
|
||||
logoRes = resources::getEmbeddedResource(resources::RESOURCE_LOGO);
|
||||
}
|
||||
if (logoRes && logoRes->data && logoRes->size > 0) {
|
||||
if (util::LoadTextureFromMemory(logoRes->data, logoRes->size, &logo_tex_, &logo_w_, &logo_h_)) {
|
||||
DEBUG_LOGF("Loaded header logo from embedded: %s (%dx%d)\n", embeddedName.c_str(), logo_w_, logo_h_);
|
||||
} else {
|
||||
DEBUG_LOGF("Note: Failed to decode embedded logo (text-only header)\n");
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("Note: Header logo not found at %s (text-only header)\n", logoPath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load coin logo texture lazily (DragonX currency icon for balance tab)
|
||||
if (!coin_logo_loaded_) {
|
||||
coin_logo_loaded_ = true;
|
||||
coin_logo_tex_ = 0; coin_logo_w_ = 0; coin_logo_h_ = 0;
|
||||
|
||||
// Read coin icon filename from ui.toml
|
||||
auto coinElem = ui::schema::UI().drawElement("components.main-window", "coin-icon");
|
||||
auto cit = coinElem.extraColors.find("icon");
|
||||
std::string coinFile = (cit != coinElem.extraColors.end() && !cit->second.empty())
|
||||
? cit->second : "logos/logo_dragonx_128.png";
|
||||
std::string coinPath = util::getExecutableDirectory() + "/res/img/" + coinFile;
|
||||
std::error_code coinEc;
|
||||
if (std::filesystem::exists(coinPath, coinEc) &&
|
||||
util::LoadTextureFromFile(coinPath.c_str(), &coin_logo_tex_, &coin_logo_w_, &coin_logo_h_)) {
|
||||
DEBUG_LOGF("Loaded coin logo from %s (%dx%d)\n", coinPath.c_str(), coin_logo_w_, coin_logo_h_);
|
||||
} else {
|
||||
// Try embedded resource fallback (Windows single-file distribution)
|
||||
std::string coinBasename = std::filesystem::path(coinFile).filename().string();
|
||||
const auto* coinRes = resources::getEmbeddedResource(coinBasename);
|
||||
if (coinRes && coinRes->data && coinRes->size > 0) {
|
||||
if (util::LoadTextureFromMemory(coinRes->data, coinRes->size, &coin_logo_tex_, &coin_logo_w_, &coin_logo_h_)) {
|
||||
DEBUG_LOGF("Loaded coin logo from embedded: %s (%dx%d)\n", coinBasename.c_str(), coin_logo_w_, coin_logo_h_);
|
||||
} else {
|
||||
DEBUG_LOGF("Note: Failed to decode embedded coin logo\n");
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("Note: Coin logo not found at %s\n", coinPath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
// (coin logo is loaded/themed at the top of render() via ensureLogoTexture().)
|
||||
|
||||
if (logo_tex_ != 0) {
|
||||
sbStatus.logoTexID = logo_tex_;
|
||||
@@ -1986,8 +2050,8 @@ void App::render()
|
||||
// Send confirm popup
|
||||
ui::RenderSendConfirmPopup(this);
|
||||
|
||||
// Console RPC Command Reference popup
|
||||
console_tab_.renderCommandsPopupModal();
|
||||
// Console command-reference popup (full-node RPC reference / lite backend verbs).
|
||||
console_tab_.renderCommandsPopupModal(console_exec_.get());
|
||||
|
||||
// Key export dialog (triggered from balance tab context menu)
|
||||
ui::KeyExportDialog::render(this);
|
||||
@@ -2305,17 +2369,32 @@ void App::renderStatusBar()
|
||||
// Connection / daemon status sits to the left of the version string
|
||||
// with a small gap.
|
||||
float gap = sbSectionGap;
|
||||
float occupiedX = versionX; // leftmost X used by the version + connection status so far
|
||||
if (!connection_status_.empty() && connection_status_ != "Connected") {
|
||||
float statusW = ImGui::CalcTextSize(connection_status_.c_str()).x;
|
||||
float statusX = versionX - statusW - gap;
|
||||
ImGui::SameLine(statusX);
|
||||
ImGui::TextDisabled("%s", connection_status_.c_str());
|
||||
occupiedX = statusX;
|
||||
} else if (!daemon_status_.empty() && daemon_status_.find("Error") != std::string::npos) {
|
||||
const char* errText = TR("sb_daemon_not_found");
|
||||
float statusW = ImGui::CalcTextSize(errText).x;
|
||||
float statusX = versionX - statusW - gap;
|
||||
ImGui::SameLine(statusX);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "%s", errText);
|
||||
occupiedX = statusX;
|
||||
}
|
||||
|
||||
// Chat note-buffer status — only while the Chat tab is active. Sits left of the version/connection
|
||||
// block. Surfaces the buffer filling/draining (and doubles as a diagnostic for send readiness).
|
||||
if (current_page_ == ui::NavPage::Chat) {
|
||||
const std::string cb = chatBufferStatusText();
|
||||
if (!cb.empty()) {
|
||||
float cbW = ImGui::CalcTextSize(cb.c_str()).x;
|
||||
float cbX = occupiedX - cbW - gap;
|
||||
ImGui::SameLine(cbX);
|
||||
ImGui::TextUnformatted(cb.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Version always at far right
|
||||
@@ -2368,12 +2447,15 @@ void App::reloadThemeImages(const std::string& bgPath, const std::string& logoPa
|
||||
gradient_tex_ = 0;
|
||||
}
|
||||
|
||||
// Reset logo loaded flags — will reload on next render frame
|
||||
// Reset logo loaded flags — will reload on next render frame (ensureLogoTexture re-rasterizes the
|
||||
// SVG for the new theme). Free the current texture first so a theme switch doesn't leak it.
|
||||
if (logo_tex_) util::DestroyTexture(logo_tex_);
|
||||
logo_loaded_ = false;
|
||||
logo_tex_ = 0;
|
||||
logo_w_ = 0;
|
||||
logo_h_ = 0;
|
||||
coin_logo_loaded_ = false;
|
||||
if (coin_logo_tex_) util::DestroyTexture(coin_logo_tex_);
|
||||
coin_logo_tex_ = 0;
|
||||
coin_logo_w_ = 0;
|
||||
coin_logo_h_ = 0;
|
||||
@@ -5238,6 +5320,11 @@ void App::renderLoadingOverlay(float contentH)
|
||||
|
||||
void App::shutdown()
|
||||
{
|
||||
// Wipe any copied secret from the OS clipboard before we exit — the 45s auto-clear timer
|
||||
// never fires if the user quits sooner, which would otherwise leave a key/seed resident.
|
||||
// (ImGui context is still alive here; App::shutdown() runs before ImGui::DestroyContext().)
|
||||
clearSecretClipboardIfArmed();
|
||||
|
||||
// Clean up bootstrap if running
|
||||
if (bootstrap_) {
|
||||
bootstrap_->cancel();
|
||||
@@ -5440,10 +5527,9 @@ void App::copySecretToClipboard(const std::string& secret)
|
||||
ui::Notifications::instance().info("Copied — clipboard auto-clears in 45s", 4.0f);
|
||||
}
|
||||
|
||||
void App::pumpSecretClipboardClear()
|
||||
void App::clearSecretClipboardIfArmed()
|
||||
{
|
||||
if (clipboard_clear_deadline_ <= 0.0) return;
|
||||
if (ImGui::GetTime() < clipboard_clear_deadline_) return;
|
||||
if (clipboard_secret_hash_ == 0) return;
|
||||
// Only clear if the clipboard STILL holds our secret (the user may have copied something else).
|
||||
if (const char* cb = ImGui::GetClipboardText()) {
|
||||
std::uint64_t h = 1469598103934665603ULL;
|
||||
@@ -5454,6 +5540,13 @@ void App::pumpSecretClipboardClear()
|
||||
clipboard_secret_hash_ = 0;
|
||||
}
|
||||
|
||||
void App::pumpSecretClipboardClear()
|
||||
{
|
||||
if (clipboard_clear_deadline_ <= 0.0) return;
|
||||
if (ImGui::GetTime() < clipboard_clear_deadline_) return;
|
||||
clearSecretClipboardIfArmed();
|
||||
}
|
||||
|
||||
void App::maybeFinishTransactionSendProgress()
|
||||
{
|
||||
using Job = services::NetworkRefreshService::Job;
|
||||
|
||||
90
src/app.h
90
src/app.h
@@ -13,6 +13,7 @@
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <deque>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "data/address_book.h"
|
||||
@@ -171,6 +172,9 @@ public:
|
||||
daemon::EmbeddedDaemon* consoleDaemon();
|
||||
daemon::XmrigManager* consoleXmrig();
|
||||
config::Settings* settings() { return settings_.get(); }
|
||||
// Request a font-atlas rebuild before the next frame (e.g. after toggling color emoji). Handled in
|
||||
// preFrame() via Typography::reload — safe to call from UI code mid-frame.
|
||||
void requestFontRebuild() { font_rebuild_requested_ = true; }
|
||||
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
||||
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
||||
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
|
||||
@@ -475,6 +479,10 @@ public:
|
||||
// Coin logo texture accessor (DragonX currency icon for balance tab)
|
||||
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
|
||||
|
||||
// DragonX custom chat emoji (the ":drgx:" shortcode) — the mark recolored to the theme accent (like
|
||||
// the logo), re-rasterized on theme change. Used by the emoji picker tile + inline in chat bubbles.
|
||||
ImTextureID getDrgxEmojiTexture() const { return drgx_emoji_tex_; }
|
||||
|
||||
/**
|
||||
* @brief Reload theme images (background gradient + logo) from new paths
|
||||
* @param bgPath Path to background image override (empty = use default)
|
||||
@@ -482,6 +490,10 @@ public:
|
||||
*/
|
||||
void reloadThemeImages(const std::string& bgPath, const std::string& logoPath);
|
||||
|
||||
// Load / recolor-per-theme the DragonX header logo (SVG rasterized to the theme accent). Called at
|
||||
// the top of render() so the wizard, lock screen, and main header all show it.
|
||||
void ensureLogoTexture();
|
||||
|
||||
// Wizard / first-run
|
||||
WizardPhase getWizardPhase() const { return wizard_phase_; }
|
||||
bool isFirstRun() const;
|
||||
@@ -561,6 +573,9 @@ public:
|
||||
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
|
||||
void copySecretToClipboard(const std::string& secret);
|
||||
void pumpSecretClipboardClear();
|
||||
// Immediately clear the clipboard if it still holds the armed secret (ignores the 45s timer).
|
||||
// Called on app shutdown so a copied key/seed does not outlive the process in the OS clipboard.
|
||||
void clearSecretClipboardIfArmed();
|
||||
bool isTransactionRefreshInProgress() const {
|
||||
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
||||
}
|
||||
@@ -602,10 +617,13 @@ private:
|
||||
// the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
|
||||
// When markFeeGapRetry is set, the returned opid is recorded in send_feegap_retried_opids_ so a
|
||||
// retry of a retry is reported as a real error.
|
||||
// background=true (autonomous chat sends / note-buffer splits): keep the single-flight + opid
|
||||
// accounting but DON'T raise the global "transaction in progress" UI (status is on the chat message).
|
||||
void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
|
||||
const std::string& memo, const nlohmann::json& recipients,
|
||||
const char* traceLabel, bool markFeeGapRetry,
|
||||
std::function<void(bool, const std::string&)> callback);
|
||||
std::function<void(bool, const std::string&)> callback,
|
||||
bool background = false);
|
||||
void markPendingSendTransactionSucceeded(const std::string& opid,
|
||||
const std::string& txid);
|
||||
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||
@@ -692,6 +710,72 @@ private:
|
||||
// thread is viewed (markChatConversationSeen); wiped in resetChatSession so unread doesn't leak across
|
||||
// wallets. In-memory only (resets on app restart).
|
||||
std::map<std::string, std::int64_t> chat_seen_watermark_;
|
||||
|
||||
// ── Chat note buffer (BOTH variants) ────────────────────────────────────────────────────────
|
||||
// Each chat message is a shielded tx that spends a note; its change needs a few confirmations before
|
||||
// it's spendable again (lite: backend ANCHOR_OFFSET+1 = 5; full node: z_sendmany minconf = 1), so
|
||||
// rapid sends run out of verified funds. We keep a buffer of ~kChatBufferTarget small self-notes so a
|
||||
// burst of messages each spends a separate verified note, refilled from change + background self-
|
||||
// splits. Chat sends, self-splits and user sends share the wallet's single send channel; inflight_op_
|
||||
// + (lite) lite_send_callback_ / (full node) send_submissions_in_flight_+pending_opids_ serialize them
|
||||
// so exactly one send is ever outstanding. Implemented in app_network.cpp; pumped from update().
|
||||
enum class LiteOpKind { None, ChatSend, ContactRequest, Split, UserSend };
|
||||
struct LiteInflightOp {
|
||||
LiteOpKind kind = LiteOpKind::None;
|
||||
std::string echoLocalId; // ChatSend/ContactRequest: the echo to resolve when it completes
|
||||
int sessionGen = 0; // chat_session_generation_ snapshot at submit (stale-guard)
|
||||
double submittedAt = 0.0;
|
||||
};
|
||||
struct QueuedChatOp {
|
||||
LiteOpKind kind = LiteOpKind::ChatSend;
|
||||
chat::OutgoingChatMemos memos; // kept so a transient-funds retry re-broadcasts, never recomposes
|
||||
std::string echoLocalId;
|
||||
int sessionGen = 0;
|
||||
int retries = 0;
|
||||
};
|
||||
LiteInflightOp inflight_op_; // the single chat/split op currently on the send channel
|
||||
std::deque<QueuedChatOp> chat_send_queue_; // chat/contact sends awaiting a free channel + verified note
|
||||
// Note-availability estimate between refreshes/scans: reset from a fresh count, decremented on each chat
|
||||
// submit (the count lags a spend by a cycle, but the wallet still picks a fresh note per send). Zeroed on
|
||||
// a transient-funds failure so we stop draining until the next refresh/scan restores the truth.
|
||||
int chat_verified_note_budget_ = 0;
|
||||
int chat_pipeline_note_count_ = 0; // verified + maturing self-notes (drives shouldSplit)
|
||||
std::uint64_t chat_verified_shielded_zat_ = 0; // verified shielded balance (split affordability)
|
||||
bool chat_note_model_seen_ = false; // saw a refresh/scan carrying per-note visibility
|
||||
// Single-split-in-flight guard: a self-split's OUTPUT notes are invisible until mined (~1 block), far
|
||||
// longer than any wall-clock cooldown — so we permit only ONE outstanding split and clear the flag when
|
||||
// the pipeline recovers (outputs mined) or a watchdog expires (a split that never mines mustn't wedge
|
||||
// refill forever). Prevents runaway splitting that would drain balance into fees.
|
||||
bool chat_split_outstanding_ = false;
|
||||
double chat_split_submitted_at_ = 0.0; // ImGui time the outstanding split was submitted (watchdog)
|
||||
// Full-node only: a coordinator-owned z_listunspent worker scan feeds the per-note counts (the shared
|
||||
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
|
||||
bool chat_note_scan_in_flight_ = false;
|
||||
double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit)
|
||||
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
|
||||
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
|
||||
// this cache rather than requiring the current model to have both — else the budget flickers to 0.
|
||||
std::int64_t chat_last_chain_height_ = 0;
|
||||
int chat_fast_scan_last_seen_ = -1; // dedup: last memo-note count logged by the 0-conf scan
|
||||
|
||||
// Coordinator helpers (both variants unless noted; see app_network.cpp).
|
||||
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
|
||||
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan
|
||||
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
|
||||
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||
bool shouldSplitChatBuffer();
|
||||
bool broadcastSelfSplitLite(int noteCount); // lite: self-send minting noteCount reply-address notes
|
||||
bool broadcastSelfSplitNode(int noteCount); // full node: z_sendmany self-send minting noteCount notes
|
||||
void enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId);
|
||||
void onChatBroadcastResult(const LiteInflightOp& op, bool ok, const std::string& error);
|
||||
static bool isTransientVerifiedFundsError(const std::string& error);
|
||||
int chatConfsRequired() const; // verified-note confs threshold: 5 (lite) / 1 (full node)
|
||||
public:
|
||||
// Status-bar summary of the chat note buffer (empty when not applicable). Shown while the Chat tab is
|
||||
// active so the buffer's state (ready / building / sending) is visible.
|
||||
std::string chatBufferStatusText();
|
||||
private:
|
||||
public:
|
||||
// Total unread incoming chat messages across all conversations (for the sidebar badge). 0 when the
|
||||
// feature is off / no identity.
|
||||
@@ -736,6 +820,7 @@ private:
|
||||
void fastScanChatMemos();
|
||||
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
|
||||
float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll)
|
||||
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
|
||||
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
||||
bool lite_firstrun_dismissed_ = false;
|
||||
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
|
||||
@@ -1006,6 +1091,9 @@ private:
|
||||
int logo_h_ = 0;
|
||||
bool logo_loaded_ = false;
|
||||
bool logo_is_dark_variant_ = true; // tracks which variant is currently loaded
|
||||
ImU32 logo_accent_ = 0; // theme accent the SVG logo was last rasterized with (re-render on change)
|
||||
ImTextureID drgx_emoji_tex_ = 0; // ":drgx:" custom chat emoji (themed to the accent, like the logo)
|
||||
int drgx_emoji_w_ = 0, drgx_emoji_h_ = 0;
|
||||
|
||||
// Coin logo texture (DragonX currency icon, separate from wallet branding)
|
||||
ImTextureID coin_logo_tex_ = 0;
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "config/settings.h"
|
||||
#include "wallet/lite_wallet_controller.h" // lite send/new-address routing
|
||||
#include "wallet/lite_wallet_state_mapper.h" // LiteWalletAppRefreshModel for lite chat harvest
|
||||
#include "wallet/lite_diagnostics.h" // liteLog — chat note-buffer coordinator diagnostics
|
||||
#include "config/version.h"
|
||||
#include "daemon/daemon_controller.h"
|
||||
#include "daemon/embedded_daemon.h"
|
||||
@@ -629,6 +630,11 @@ void App::onDisconnected(const std::string& reason)
|
||||
consecutive_core_failures_ = 0;
|
||||
send_progress_active_ = false;
|
||||
send_submissions_in_flight_ = 0;
|
||||
// Release the chat note-buffer's single-split guard on a plain disconnect (which doesn't run
|
||||
// resetChatSession) so a re-split isn't suppressed until the 1200s watchdog after reconnect.
|
||||
chat_split_outstanding_ = false;
|
||||
chat_split_submitted_at_ = 0.0;
|
||||
inflight_op_ = LiteInflightOp{}; // the in-flight chat send's callback was just fired above (Failed)
|
||||
network_refresh_.resetJobs();
|
||||
rescan_status_poll_in_progress_ = false;
|
||||
opid_poll_in_progress_ = false;
|
||||
@@ -2756,6 +2762,25 @@ void App::provisionChatIdentityFromSecret(std::string secret)
|
||||
// local — peers only ever exchange public keys).
|
||||
void App::resetChatSession()
|
||||
{
|
||||
// Lite send channel — reset UNCONDITIONALLY (independent of the chat feature flag; user sends set
|
||||
// inflight_op_/lite_send_callback_ even with chat off). Clear the note-buffer coordinator so wallet A's
|
||||
// queued sends can't drain under wallet B, and DON'T orphan a user Send callback across a controller
|
||||
// rebuild: rebuildLiteWallet(force) destroys the controller holding the in-flight send, so its result
|
||||
// slot dies with it and the callback would hang the Send dialog forever. Fire the orphaned callback on a
|
||||
// moved-out copy AFTER the channel state is reset, so a re-entrant sendTransaction stays clean.
|
||||
auto orphanedSend = std::move(lite_send_callback_);
|
||||
lite_send_callback_ = nullptr;
|
||||
chat_send_queue_.clear();
|
||||
inflight_op_ = LiteInflightOp{};
|
||||
chat_verified_note_budget_ = 0;
|
||||
chat_pipeline_note_count_ = 0;
|
||||
chat_verified_shielded_zat_ = 0;
|
||||
chat_note_model_seen_ = false;
|
||||
chat_split_outstanding_ = false;
|
||||
chat_split_submitted_at_ = 0.0;
|
||||
chat_note_scan_in_flight_ = false; // a stale in-flight note scan is dropped by its scanGen guard
|
||||
if (orphanedSend) orphanedSend(false, "Wallet/server changed — send aborted");
|
||||
|
||||
if (!chat::hushChatFeatureEnabledAtBuild()) return; // constexpr — folds away in OFF builds
|
||||
// Drop identity + decrypted plaintext in RAM, lock the seed-encrypted DB, re-arm provisioning.
|
||||
chat_service_.clearIdentity();
|
||||
@@ -2955,18 +2980,19 @@ bool App::broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::st
|
||||
const auto sessionGen = chat_session_generation_;
|
||||
|
||||
if (lite_wallet_) {
|
||||
const bool ok = broadcastChatMemosLite(memos);
|
||||
// The lite backend has no per-send completion callback here; resolve optimistically on a
|
||||
// successful queue (its own broadcast log surfaces a later failure).
|
||||
if (ok && sessionGen == chat_session_generation_)
|
||||
chat_service_.resolveOutgoing(echoLocalId, chat::ChatDelivery::Sent);
|
||||
return ok;
|
||||
// Lite chat is owned by the note-buffer coordinator (enqueue + drain + resolve from the real
|
||||
// broadcast result). Route any caller here through the queue rather than resolving optimistically
|
||||
// (the old code marked a merely-queued send "Sent", masking real failures). Kept for safety —
|
||||
// the send/Retry paths call enqueueChatSend directly and don't hit this branch.
|
||||
(void)sessionGen;
|
||||
enqueueChatSend(LiteOpKind::ChatSend, memos, echoLocalId);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!state_.connected || !rpc_ || !worker_) {
|
||||
ui::Notifications::instance().error(TR("chat_toast_not_connected"));
|
||||
return false;
|
||||
}
|
||||
// Coordinator-owned primitive (pumpChatNoteBuffer): the pump already gates on connected/unlocked and a
|
||||
// verified-note budget, so these refusals are belt-and-suspenders. Return false SILENTLY (no per-frame
|
||||
// toast) — the pump leaves the message queued ("Sending") and retries when the guard clears.
|
||||
if (!state_.connected || !rpc_ || !worker_) return false;
|
||||
|
||||
// Chat moves 0 value, and dragonxd REJECTS a 0-value tx whose fee exceeds the default miners fee
|
||||
// (0.0001) — so pin chat to exactly kChatMinFeeDrgx, independent of the user's default-fee setting
|
||||
@@ -2976,10 +3002,7 @@ bool App::broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::st
|
||||
// Pay from a z-address that can actually cover the fee. The memo still advertises the identity
|
||||
// reply address, so the peer replies to the right place regardless of which note paid.
|
||||
const std::string from = chatPayFromZaddr(fee);
|
||||
if (from.empty()) {
|
||||
ui::Notifications::instance().error(TR("chat_toast_need_funds"));
|
||||
return false;
|
||||
}
|
||||
if (from.empty()) return false; // no fundable note yet — pump keeps it queued, retries after a scan
|
||||
|
||||
// Full-node: each memo needs the daemon's "utf8:" prefix (raw JSON/hex is otherwise rejected).
|
||||
const auto outputs = chat::chatSendOutputs(memos, /*utf8Prefix=*/true);
|
||||
@@ -2994,14 +3017,20 @@ bool App::broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::st
|
||||
|
||||
// markFeeGapRetry=true is deliberate: it suppresses the fee-gap auto-retry, which rebuilds a
|
||||
// SINGLE-recipient tx from the scalar to/amount/memo and would drop the second (payload) output.
|
||||
// The callback flips the echo to Sent/Failed once the async op resolves — real delivery status.
|
||||
// This is now the note-buffer coordinator's full-node submit primitive (called only from the pump).
|
||||
// The terminal callback (fired once by the opid poller) releases the send channel and routes the
|
||||
// result through onChatBroadcastResult, so a transient "Insufficient shielded funds" requeues instead
|
||||
// of hard-failing. The sessionGen guard drops a stale wallet's echo AND protects the inflight_op_
|
||||
// clear from a callback landing after a wallet switch (which would otherwise clear a NEW op's token).
|
||||
submitZSendMany(from, memos.recipientZaddr, 0.0, fee, /*memo*/"", recipients,
|
||||
"HushChat / broadcast", /*markFeeGapRetry*/ true,
|
||||
[this, echoLocalId, sessionGen](bool ok, const std::string& /*result*/) {
|
||||
if (sessionGen != chat_session_generation_) return; // wallet locked/switched — stale
|
||||
chat_service_.resolveOutgoing(echoLocalId,
|
||||
ok ? chat::ChatDelivery::Sent : chat::ChatDelivery::Failed);
|
||||
});
|
||||
[this, echoLocalId, sessionGen](bool ok, const std::string& result) {
|
||||
if (sessionGen != chat_session_generation_) return; // stale wallet — don't touch new session
|
||||
inflight_op_ = LiteInflightOp{}; // release the channel for this session
|
||||
LiteInflightOp op; op.echoLocalId = echoLocalId; op.sessionGen = sessionGen;
|
||||
onChatBroadcastResult(op, ok, result);
|
||||
},
|
||||
/*background*/ true); // status is shown on the chat message, not the global send UI
|
||||
return true; // submitted (async build/broadcast; the callback resolves the final status)
|
||||
}
|
||||
|
||||
@@ -3021,11 +3050,9 @@ bool App::broadcastChatMemosLite(const chat::OutgoingChatMemos& memos)
|
||||
recipient.memo = out.memo;
|
||||
req.recipients.push_back(std::move(recipient));
|
||||
}
|
||||
if (!lite_wallet_->sendTransaction(req)) {
|
||||
ui::Notifications::instance().error(TR("chat_toast_lite_busy"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// No busy toast here: the coordinator (pumpChatNoteBuffer) owns this call, retries next frame when the
|
||||
// single broadcast channel frees, and surfaces status on the message itself.
|
||||
return lite_wallet_->sendTransaction(req);
|
||||
}
|
||||
|
||||
void App::sendChatMessage(const std::string& conversationId, const std::string& text)
|
||||
@@ -3073,10 +3100,9 @@ void App::sendChatMessage(const std::string& conversationId, const std::string&
|
||||
echo.payload_position = 0;
|
||||
echo.delivery = chat::ChatDelivery::Sending; // shows immediately; the broadcast callback resolves it
|
||||
chat_service_.recordOutgoingPending(echo); // in-memory now; persisted with the final status
|
||||
// A synchronous refusal (not connected / no funds) resolves to Failed right away so the Retry
|
||||
// affordance appears; otherwise the async callback flips it to Sent/Failed.
|
||||
if (!broadcastChatMemos(memos, echo.txid))
|
||||
chat_service_.resolveOutgoing(echo.txid, chat::ChatDelivery::Failed);
|
||||
// Both variants: hand to the note-buffer coordinator, which drains the queue against verified notes
|
||||
// (building/refilling the buffer as needed) and resolves the echo Sent/Failed from the REAL result.
|
||||
enqueueChatSend(LiteOpKind::ChatSend, memos, echo.txid);
|
||||
}
|
||||
|
||||
void App::startChatConversation(const std::string& peerZaddr, const std::string& text)
|
||||
@@ -3116,10 +3142,369 @@ void App::sendContactRequestForCid(const std::string& cid, const std::string& pe
|
||||
echo.payload_position = 0;
|
||||
echo.delivery = chat::ChatDelivery::Sending;
|
||||
chat_service_.recordOutgoingPending(echo);
|
||||
if (broadcastChatMemos(memos, echo.txid))
|
||||
ui::Notifications::instance().success(TR("chat_toast_request_queued"));
|
||||
else
|
||||
chat_service_.resolveOutgoing(echo.txid, chat::ChatDelivery::Failed);
|
||||
// Both variants: enqueue through the coordinator (resolves the echo from the real result).
|
||||
enqueueChatSend(LiteOpKind::ContactRequest, memos, echo.txid);
|
||||
ui::Notifications::instance().success(TR("chat_toast_request_queued"));
|
||||
}
|
||||
|
||||
// ── Chat note-buffer coordinator (lite only) ─────────────────────────────────────────────────────
|
||||
// Design in app.h. Keeps ~kChatBufferTarget verified self-notes so a burst of messages each spends a
|
||||
// separate note; refilled from change + background self-splits. Shares the controller's single broadcast
|
||||
// channel with user sends (inflight_op_ tracks ownership). Pumped from App::update().
|
||||
namespace {
|
||||
constexpr int kChatBufferTarget = 10; // verified self-notes we aim to keep
|
||||
constexpr int kChatRefillTrigger = 4; // (re)build only when the maturing pipeline drops below this
|
||||
// A note is spendable when its witness count > ANCHOR_OFFSET, i.e. block depth >= ANCHOR_OFFSET+1 = 5
|
||||
// (backend data.rs:543 needs anchor_offset+1 witnesses; ANCHOR_OFFSET=4 in lib.rs:25). Depth 4 is NOT yet
|
||||
// spendable — counting it there wastes a proving round-trip on a guaranteed "insufficient verified funds".
|
||||
constexpr int kChatConfsRequired = 5;
|
||||
constexpr double kChatNoteSizeDrgx = 0.001; // each buffer note (change after a send stays >= fee)
|
||||
constexpr double kChatSplitWatchdogSecs = 1200.0; // 20 min: clear a stuck split flag if it never mines (expiry/reorg)
|
||||
constexpr int kMaxChatSendRetries = 20; // transient-funds requeues before giving up
|
||||
inline std::uint64_t chatDrgxToZat(double drgx) {
|
||||
return static_cast<std::uint64_t>(std::llround(drgx * 100000000.0));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Confirmations a self-note needs before it's spendable: lite requires ANCHOR_OFFSET+1 witnesses (=5,
|
||||
// data.rs:543); the full node's chat z_sendmany uses minconf=1 (app_network.cpp z_sendmany call).
|
||||
int App::chatConfsRequired() const { return lite_wallet_ ? kChatConfsRequired : 1; }
|
||||
|
||||
// Status-bar summary of the chat note buffer (shown while the Chat tab is active). Empty unless chat has
|
||||
// an identity. Surfaces the buffer state so the user can see it filling / draining, and it doubles as a
|
||||
// diagnostic: "…" = no note data yet; "0/10 ready" = counted but no verified notes; "N/10 ready" = armed.
|
||||
std::string App::chatBufferStatusText() {
|
||||
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return "";
|
||||
const int queued = static_cast<int>(chat_send_queue_.size());
|
||||
const int verified = std::max(0, std::min(chat_verified_note_budget_, kChatBufferTarget));
|
||||
const int target = static_cast<int>(kChatBufferTarget);
|
||||
char buf[128];
|
||||
if (queued > 0) {
|
||||
snprintf(buf, sizeof(buf), TR(queued == 1 ? "chat_buffer_sending_one" : "chat_buffer_sending"), queued);
|
||||
return buf;
|
||||
}
|
||||
if (chat_split_outstanding_) {
|
||||
snprintf(buf, sizeof(buf), TR("chat_buffer_preparing"), verified, target);
|
||||
return buf;
|
||||
}
|
||||
if (!chat_note_model_seen_)
|
||||
return TR("chat_buffer_loading");
|
||||
snprintf(buf, sizeof(buf), TR("chat_buffer_ready"), verified, target);
|
||||
return buf;
|
||||
}
|
||||
|
||||
bool App::isTransientVerifiedFundsError(const std::string& error) {
|
||||
// Both variants' "only unconfirmed change is short" phrasing, treated as transient (retry after the
|
||||
// buffer matures): lite = "Insufficient verified funds" (lightwallet.rs:2402); full node =
|
||||
// "Insufficient shielded funds" (asyncrpcoperation_sendmany, parsed at parseInsufficientShielded).
|
||||
return error.find("Insufficient verified funds") != std::string::npos
|
||||
|| error.find("Insufficient shielded funds") != std::string::npos;
|
||||
}
|
||||
|
||||
// Verified notes = ANY spendable shielded note (>= a fee) with >= kChatConfsRequired confirmations — the
|
||||
// ones a chat send can spend RIGHT NOW. We count notes at ALL addresses, not just the reply address,
|
||||
// because a chat send pays via chatPayFromZaddr (any funded z-addr) — gating on reply-address-only notes
|
||||
// would queue a sendable message whenever funds sit elsewhere. NB: the model's per-note `spendable` flag
|
||||
// means "unspent", NOT verified (lite_result_parsers.cpp recomputes it), so we derive it from depth here.
|
||||
int App::verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model) {
|
||||
if (!model.hasSpendableOutputs) return 0;
|
||||
// Use the cached tip (refreshChatNoteBudget updates it), not this model's — a notes-carrying model may
|
||||
// not carry sync status that cycle, which would otherwise zero the whole count.
|
||||
const std::int64_t tip = chat_last_chain_height_;
|
||||
if (tip <= 0) return 0;
|
||||
const std::uint64_t minVal = chatDrgxToZat(kChatMinFeeDrgx);
|
||||
int n = 0;
|
||||
for (const auto& o : model.spendableOutputs) {
|
||||
if (o.kind != wallet::LiteSpendableOutputKind::UnspentNote) continue; // shielded notes only
|
||||
if (o.spent || o.unconfirmedSpent || o.pending) continue;
|
||||
if (o.valueZatoshis < minVal) continue; // skip dust below a fee
|
||||
if (!o.createdInBlock.has_value()) continue; // unknown depth -> not verified
|
||||
if (tip - *o.createdInBlock + 1 >= chatConfsRequired()) ++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Notes that are verified OR still maturing (any unspent shielded note of usable size) — i.e. notes that
|
||||
// will become spendable WITHOUT another split. Drives the refill trigger so we don't over-split while
|
||||
// change is confirming or when the wallet already holds enough notes anywhere.
|
||||
int App::pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model) {
|
||||
if (!model.hasSpendableOutputs) return 0;
|
||||
const std::uint64_t minVal = chatDrgxToZat(kChatMinFeeDrgx);
|
||||
int n = 0;
|
||||
for (const auto& o : model.spendableOutputs) {
|
||||
if (o.kind != wallet::LiteSpendableOutputKind::UnspentNote) continue;
|
||||
if (o.spent || o.unconfirmedSpent) continue; // already consumed
|
||||
if (o.valueZatoshis < minVal) continue;
|
||||
++n; // verified or maturing (any addr)
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Recompute the cached note estimates from a fresh refresh model (called from update() when one arrives).
|
||||
void App::refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model) {
|
||||
if (!lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild()) return;
|
||||
// Cache the chain tip from whatever height source THIS model carries (sync status or chain info) — a
|
||||
// model with notes may lack sync that cycle, so verifiedSelfNoteCount reads the cache below.
|
||||
if (model.hasSyncStatus && model.sync.chainHeight > 0)
|
||||
chat_last_chain_height_ = static_cast<std::int64_t>(model.sync.chainHeight);
|
||||
else if (model.hasChainInfo && model.chain.latestBlockHeight.has_value() &&
|
||||
*model.chain.latestBlockHeight > 0)
|
||||
chat_last_chain_height_ = static_cast<std::int64_t>(*model.chain.latestBlockHeight);
|
||||
if (model.hasSpendableOutputs) {
|
||||
chat_note_model_seen_ = true;
|
||||
chat_verified_note_budget_ = verifiedSelfNoteCount(model);
|
||||
chat_pipeline_note_count_ = pipelineSelfNoteCount(model);
|
||||
}
|
||||
if (model.hasBalance)
|
||||
chat_verified_shielded_zat_ = model.balance.verifiedShieldedZatoshis;
|
||||
// Release the single-split guard once the outstanding split's outputs are mined+visible (pipeline
|
||||
// recovered) — or once a watchdog window elapses, so a split that never mines can't wedge refill.
|
||||
if (chat_split_outstanding_ &&
|
||||
(chat_pipeline_note_count_ >= kChatRefillTrigger ||
|
||||
ImGui::GetTime() - chat_split_submitted_at_ > kChatSplitWatchdogSecs)) {
|
||||
chat_split_outstanding_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Full-node twin of refreshChatNoteBudget: the shared balance poll discards per-note data, so run our own
|
||||
// rate-limited z_listunspent worker scan (mirrors fastScanChatMemos) to count verified/maturing spendable
|
||||
// notes. z_listunspent returns only the wallet's OWN notes, and a chat send pays from any of them
|
||||
// (chatPayFromZaddr), so we count ALL addresses — gating on the reply address only would queue a sendable
|
||||
// message when funds sit elsewhere. minconf=0 exposes maturing change so the pipeline count doesn't over-
|
||||
// split. On an older daemon that rejects z_listunspent it leaves chat_note_model_seen_ false → the pump
|
||||
// won't drain a phantom budget or split (graceful degradation to naive one-at-a-time sends).
|
||||
void App::refreshChatNoteBudgetNode() {
|
||||
if (lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
|
||||
if (!state_.connected || !rpc_ || !worker_ || state_.isLocked()) return;
|
||||
if (chat_note_scan_in_flight_) return; // one scan at a time
|
||||
const double now = ImGui::GetTime();
|
||||
if (now - chat_note_scan_last_ < 4.0) return; // rate limit; self-throttles via the in-flight flag too
|
||||
|
||||
chat_note_scan_last_ = now;
|
||||
chat_note_scan_in_flight_ = true;
|
||||
const int scanGen = chat_session_generation_;
|
||||
const int confsNeeded = chatConfsRequired(); // == 1 for full node
|
||||
const std::uint64_t minVal = chatDrgxToZat(kChatMinFeeDrgx);
|
||||
worker_->post([this, scanGen, confsNeeded, minVal]() -> rpc::RPCWorker::MainCb {
|
||||
int verified = 0, pipeline = 0;
|
||||
std::uint64_t verifiedZat = 0;
|
||||
bool ok = false;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("HushChat / note-buffer scan");
|
||||
nlohmann::json notes = rpc_->call("z_listunspent", nlohmann::json::array({0})); // 0 = include maturing
|
||||
if (notes.is_array()) {
|
||||
ok = true;
|
||||
for (const auto& nz : notes) {
|
||||
if (!nz.is_object()) continue;
|
||||
if (nz.value("locked", false)) continue; // tied up by an in-flight send
|
||||
const std::uint64_t amtZat = chatDrgxToZat(nz.value("amount", 0.0));
|
||||
if (amtZat < minVal) continue; // skip dust below a fee
|
||||
// rawconfirmations is the TRUE depth; `confirmations` is dPoW-clamped to 1 and understates it.
|
||||
const int confs = (nz.contains("rawconfirmations") && nz["rawconfirmations"].is_number_integer())
|
||||
? nz["rawconfirmations"].get<int>()
|
||||
: nz.value("confirmations", 0);
|
||||
++pipeline; // unspent self-note (verified or maturing)
|
||||
if (confs >= confsNeeded) { ++verified; verifiedZat += amtZat; }
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {}
|
||||
return [this, scanGen, ok, verified, pipeline, verifiedZat]() {
|
||||
if (scanGen != chat_session_generation_) return; // wallet switched — drop (reset cleared the flag)
|
||||
chat_note_scan_in_flight_ = false;
|
||||
if (!ok) return; // z_listunspent unavailable — leave caches as-is
|
||||
chat_note_model_seen_ = true;
|
||||
chat_verified_note_budget_ = verified;
|
||||
chat_pipeline_note_count_ = pipeline;
|
||||
chat_verified_shielded_zat_ = verifiedZat;
|
||||
if (chat_split_outstanding_ &&
|
||||
(chat_pipeline_note_count_ >= kChatRefillTrigger ||
|
||||
ImGui::GetTime() - chat_split_submitted_at_ > kChatSplitWatchdogSecs)) {
|
||||
chat_split_outstanding_ = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void App::enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId) {
|
||||
QueuedChatOp op;
|
||||
op.kind = kind;
|
||||
op.memos = memos; // kept so a transient-funds retry re-broadcasts, never recomposes
|
||||
op.echoLocalId = echoLocalId;
|
||||
op.sessionGen = chat_session_generation_;
|
||||
chat_send_queue_.push_back(std::move(op));
|
||||
wallet::liteLog("chat: queued outgoing " +
|
||||
std::string(kind == LiteOpKind::ContactRequest ? "contact request" : "message") +
|
||||
" (queue depth " + std::to_string(chat_send_queue_.size()) +
|
||||
", verified notes ~" + std::to_string(chat_verified_note_budget_) + ")");
|
||||
// pumpChatNoteBuffer() (next update tick) submits it when the channel is free + a verified note exists.
|
||||
}
|
||||
|
||||
// Self-send that mints `noteCount` reply-address notes (funds stay in the wallet), building/refilling the
|
||||
// buffer. Returns false if the single broadcast channel is busy (the pump retries next frame).
|
||||
bool App::broadcastSelfSplitLite(int noteCount) {
|
||||
if (!lite_wallet_ || noteCount < 1) return false;
|
||||
const std::string myZ = chatReplyZaddr();
|
||||
if (myZ.empty()) return false;
|
||||
wallet::LiteSendRequest req;
|
||||
const std::uint64_t noteZat = chatDrgxToZat(kChatNoteSizeDrgx);
|
||||
for (int i = 0; i < noteCount; ++i) {
|
||||
wallet::LiteSendRecipient r;
|
||||
r.address = myZ;
|
||||
r.amountZatoshis = noteZat;
|
||||
req.recipients.push_back(std::move(r));
|
||||
}
|
||||
if (!lite_wallet_->sendTransaction(req)) return false; // channel busy — retry next frame
|
||||
wallet::liteLog("Chat buffer: splitting funds into " + std::to_string(noteCount) +
|
||||
" notes (self-send) so messages can be sent back-to-back");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Full-node twin of broadcastSelfSplitLite: a z_sendmany self-send with `noteCount` outputs to our reply
|
||||
// address (funds stay in the wallet), building/refilling the buffer. Caps outputs to stay well under the
|
||||
// per-tx size limit after Sietch decoy padding. markFeeGapRetry=true is load-bearing — the fee-gap retry
|
||||
// rebuilds a SINGLE-recipient tx and would collapse the multi-output split. The terminal callback releases
|
||||
// the send channel; chat_split_outstanding_ stays set until a scan sees the outputs.
|
||||
bool App::broadcastSelfSplitNode(int noteCount) {
|
||||
if (lite_wallet_ || noteCount < 1) return false;
|
||||
if (!state_.connected || !rpc_ || !worker_) return false;
|
||||
const std::string myZ = chatReplyZaddr();
|
||||
if (myZ.empty()) return false;
|
||||
const double fee = kChatMinFeeDrgx;
|
||||
// Pay from any funded z-address; the split's OUTPUTS still go to the reply address (building the buffer
|
||||
// there). The note count now includes notes at every address and is sized against the total verified
|
||||
// balance, so paying from wherever the funds actually sit is consistent — and it can't stall when funds
|
||||
// aren't at the reply address (which paying strictly from myZ would).
|
||||
const std::string from = chatPayFromZaddr(fee);
|
||||
if (from.empty()) return false; // no fundable note — pump retries after a scan
|
||||
const int n = std::min(noteCount, 40); // bounded so the split tx stays under max_tx_size after decoys
|
||||
nlohmann::json recipients = nlohmann::json::array();
|
||||
for (int i = 0; i < n; ++i) {
|
||||
nlohmann::json r;
|
||||
r["address"] = myZ;
|
||||
r["amount"] = util::formatAmountFixed(kChatNoteSizeDrgx);
|
||||
recipients.push_back(std::move(r));
|
||||
}
|
||||
const auto sessionGen = chat_session_generation_;
|
||||
submitZSendMany(from, myZ, 0.0, fee, /*memo*/"", recipients,
|
||||
"HushChat / note-buffer split", /*markFeeGapRetry*/ true,
|
||||
[this, sessionGen](bool ok, const std::string& /*result*/) {
|
||||
if (sessionGen != chat_session_generation_) return; // stale wallet
|
||||
inflight_op_ = LiteInflightOp{}; // release the channel
|
||||
// On success the outputs aren't mined yet — chat_split_outstanding_ clears on the
|
||||
// next scan (pipeline recovered) or the watchdog. On FAILURE (e.g. a stale-scan
|
||||
// insufficient-funds throw) clear it now so the pump can retry after the next scan
|
||||
// instead of waiting out the 1200s watchdog. (No notes were created, so it's safe.)
|
||||
if (!ok) chat_split_outstanding_ = false;
|
||||
},
|
||||
/*background*/ true); // autonomous buffer maintenance — no global send UI
|
||||
wallet::liteLog("Chat buffer: splitting funds into " + std::to_string(n) +
|
||||
" notes (self-send) so messages can be sent back-to-back");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool App::shouldSplitChatBuffer() {
|
||||
// Only for engaged chat users (has a conversation or a queued send) — never split for someone who
|
||||
// never chats. Connected, unlocked, channel free, no split already outstanding, pipeline genuinely
|
||||
// depleted, funded. NB: a wall-clock cooldown is deliberately NOT used — a split's outputs stay
|
||||
// invisible until mined (~1 block, far longer than any UI cooldown), so we gate on a single-split-
|
||||
// in-flight flag (cleared when the pipeline recovers or the watchdog fires) to prevent runaway splits.
|
||||
if (chat_service_.store().conversationIds().empty() && chat_send_queue_.empty()) return false;
|
||||
if (!state_.connected || state_.isLocked()) return false;
|
||||
// Channel busy = our own op inflight, OR a user send holds the shared channel. Lite user sends set
|
||||
// lite_send_callback_; full-node user sends don't touch inflight_op_, so they're detected via the
|
||||
// shared single-flight counter + any outstanding opid (a chat/user/sweep z_sendmany not yet resolved).
|
||||
if (inflight_op_.kind != LiteOpKind::None) return false;
|
||||
if (lite_wallet_ ? (bool)lite_send_callback_
|
||||
: (send_submissions_in_flight_ > 0 || !pending_opids_.empty())) return false;
|
||||
if (chat_split_outstanding_) return false;
|
||||
if (!chat_note_model_seen_) return false;
|
||||
if (chat_pipeline_note_count_ >= kChatRefillTrigger) return false; // change refill covers it
|
||||
if (chat_verified_shielded_zat_ < chatDrgxToZat(kChatNoteSizeDrgx + kChatMinFeeDrgx)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve the just-completed chat/contact broadcast onto its echo. The in-flight op is the queue FRONT
|
||||
// (the pump submits it without popping) so we can requeue it in place on a transient-funds failure.
|
||||
void App::onChatBroadcastResult(const LiteInflightOp& op, bool ok, const std::string& error) {
|
||||
if (op.sessionGen != chat_session_generation_) return; // wallet switched since submit — echo is gone
|
||||
const bool frontMatches = !chat_send_queue_.empty()
|
||||
&& chat_send_queue_.front().echoLocalId == op.echoLocalId;
|
||||
if (ok) {
|
||||
chat_service_.resolveOutgoing(op.echoLocalId, chat::ChatDelivery::Sent);
|
||||
if (frontMatches) chat_send_queue_.pop_front();
|
||||
wallet::liteLog("chat: message broadcast (Sent); " + std::to_string(chat_send_queue_.size()) +
|
||||
" still queued");
|
||||
return;
|
||||
}
|
||||
if (frontMatches && isTransientVerifiedFundsError(error)
|
||||
&& chat_send_queue_.front().retries < kMaxChatSendRetries) {
|
||||
// The optimistic budget was wrong (change not confirmed yet). Stop draining until the next refresh
|
||||
// restores the true verified-note count; keep the message queued ("Sending") and retry then.
|
||||
chat_verified_note_budget_ = 0;
|
||||
++chat_send_queue_.front().retries;
|
||||
wallet::liteLog("chat: send deferred — funds still confirming (retry " +
|
||||
std::to_string(chat_send_queue_.front().retries) + "); message stays 'Sending'");
|
||||
return; // leave at front
|
||||
}
|
||||
chat_service_.resolveOutgoing(op.echoLocalId, chat::ChatDelivery::Failed); // hard failure -> Retry affordance
|
||||
if (frontMatches) chat_send_queue_.pop_front();
|
||||
wallet::liteLog("chat: send FAILED: " + error);
|
||||
}
|
||||
|
||||
// Per-frame pump: drain the chat queue against verified notes (priority 1); otherwise build/refill the
|
||||
// buffer during idle (priority 2). One op per frame — the single broadcast channel serializes the rest.
|
||||
void App::pumpChatNoteBuffer() {
|
||||
if (!chat::hushChatFeatureEnabledAtBuild()) return;
|
||||
if (!chat_service_.hasIdentity() || !state_.connected) return;
|
||||
// A locked wallet can't spend: submitting would return a non-transient lock error and flap the queued
|
||||
// message to Failed. Hold everything (send + split) until unlock — messages stay "Sending".
|
||||
if (state_.isLocked()) return;
|
||||
// Channel busy — exactly one send must be outstanding. Lite serializes via inflight_op_ + the send
|
||||
// callback; the full node has no single-broadcast channel, so we also honour the shared single-flight
|
||||
// counter and any outstanding opid (a chat/user/sweep z_sendmany not yet terminally resolved). Without
|
||||
// the latter two a chat send could overlap a user Send-tab send (which never sets inflight_op_).
|
||||
if (inflight_op_.kind != LiteOpKind::None) return;
|
||||
if (lite_wallet_ ? (bool)lite_send_callback_
|
||||
: (send_submissions_in_flight_ > 0 || !pending_opids_.empty())) return;
|
||||
|
||||
// Priority 1: drain a queued chat send while we (optimistically) still have a verified note.
|
||||
if (!chat_send_queue_.empty()) {
|
||||
QueuedChatOp& front = chat_send_queue_.front();
|
||||
if (front.sessionGen != chat_session_generation_) { chat_send_queue_.pop_front(); return; }
|
||||
if (chat_verified_note_budget_ >= 1) {
|
||||
// Submit onto the wallet's send channel; NOT popped until the async result resolves (so a
|
||||
// transient-funds failure can requeue it in place). Full-node broadcastChatMemos registers a
|
||||
// terminal opid callback that clears inflight_op_ + routes onChatBroadcastResult.
|
||||
const bool submitted = lite_wallet_ ? broadcastChatMemosLite(front.memos)
|
||||
: broadcastChatMemos(front.memos, front.echoLocalId);
|
||||
if (submitted) {
|
||||
inflight_op_ = LiteInflightOp{ front.kind, front.echoLocalId, front.sessionGen, ImGui::GetTime() };
|
||||
--chat_verified_note_budget_; // this note is now (optimistically) spent
|
||||
}
|
||||
return; // one op per frame; the front waits until resolved
|
||||
}
|
||||
// No verified note available: leave it queued ("Sending"); fall through to maybe build the buffer.
|
||||
}
|
||||
|
||||
// Priority 2: build/refill the buffer during idle.
|
||||
if (shouldSplitChatBuffer()) {
|
||||
const int need = kChatBufferTarget - chat_pipeline_note_count_;
|
||||
const std::uint64_t noteZat = chatDrgxToZat(kChatNoteSizeDrgx);
|
||||
const std::uint64_t feeZat = chatDrgxToZat(kChatMinFeeDrgx);
|
||||
int affordable = 0;
|
||||
if (chat_verified_shielded_zat_ > feeZat && noteZat > 0)
|
||||
affordable = static_cast<int>((chat_verified_shielded_zat_ - feeZat) / noteZat);
|
||||
const int n = std::min(need, affordable);
|
||||
const bool submitted = n >= 1 && (lite_wallet_ ? broadcastSelfSplitLite(n)
|
||||
: broadcastSelfSplitNode(n));
|
||||
if (submitted) {
|
||||
inflight_op_ = LiteInflightOp{ LiteOpKind::Split, {}, chat_session_generation_, ImGui::GetTime() };
|
||||
// Block further splits until this one's outputs mine in (or the watchdog fires): the outputs are
|
||||
// invisible to the note count for ~1 block, so without this we'd fire a fresh split every frame.
|
||||
chat_split_outstanding_ = true;
|
||||
chat_split_submitted_at_ = ImGui::GetTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HushChat (lite variant): the full-node harvest works off z_viewtransaction, but the lite wallet's
|
||||
@@ -3133,6 +3518,7 @@ void App::ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model)
|
||||
|
||||
std::unordered_map<std::string, chat::HushChatTransactionInput> byTxid;
|
||||
std::unordered_map<std::string, std::int64_t> txTimestamps;
|
||||
bool anyUnconfirmedChatTx = false; // did a chat-carrying tx arrive still-unconfirmed (0-conf mempool)?
|
||||
for (const auto& tx : model.transactions) {
|
||||
if (tx.kind != wallet::LiteWalletAppTransactionKind::Receive) continue; // only incoming carry chat
|
||||
if (tx.memo.empty() || !tx.position) continue;
|
||||
@@ -3140,6 +3526,7 @@ void App::ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model)
|
||||
input.txid = tx.txid;
|
||||
input.outputs.push_back({static_cast<std::size_t>(*tx.position), tx.memo});
|
||||
txTimestamps[tx.txid] = tx.timestamp;
|
||||
if (tx.unconfirmed) anyUnconfirmedChatTx = true;
|
||||
}
|
||||
if (byTxid.empty()) return;
|
||||
|
||||
@@ -3151,6 +3538,10 @@ void App::ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model)
|
||||
if (!metadata.empty()) {
|
||||
std::vector<std::string> newChatCids;
|
||||
chat_service_.ingest(metadata, txTimestamps, std::time(nullptr), &newChatCids);
|
||||
if (!newChatCids.empty())
|
||||
wallet::liteLog("chat: harvested " + std::to_string(newChatCids.size()) + " new message(s) " +
|
||||
(anyUnconfirmedChatTx ? "(0-conf / mempool — monitor working)"
|
||||
: "(confirmed only — not seen at mempool speed)"));
|
||||
// Mirror the full-node paths: a new incoming message un-hides a hidden conversation (you can't
|
||||
// un-receive), and a non-muted new message toasts when we're off the Chat tab.
|
||||
if (settings_) {
|
||||
@@ -3182,6 +3573,8 @@ void App::fastScanChatMemos()
|
||||
const int scanGen = chat_session_generation_; // guard: drop the result if the wallet switches/locks
|
||||
worker_->post([this, addr, scanGen]() -> rpc::RPCWorker::MainCb {
|
||||
std::vector<chat::HushChatTransactionMetadata> metadata;
|
||||
int rawMemoCount = 0; // received notes carrying a memo at the reply addr (0-conf visibility signal)
|
||||
std::string scanError;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("HushChat / 0-conf fast scan");
|
||||
nlohmann::json received = rpc_->call("z_listreceivedbyaddress", {addr, 0}); // 0 = include mempool
|
||||
@@ -3194,6 +3587,7 @@ void App::fastScanChatMemos()
|
||||
const std::string txid = note.value("txid", std::string());
|
||||
const std::string memo = note.value("memoStr", std::string());
|
||||
if (txid.empty() || memo.empty()) continue;
|
||||
++rawMemoCount;
|
||||
std::size_t pos = fallbackPos;
|
||||
for (const char* key : {"position", "outputIndex", "outindex"})
|
||||
if (note.contains(key) && note[key].is_number_integer() && note[key].get<int>() >= 0) {
|
||||
@@ -3209,13 +3603,25 @@ void App::fastScanChatMemos()
|
||||
for (auto& m : extracted.metadata) metadata.push_back(std::move(m));
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {}
|
||||
return [this, scanGen, metadata = std::move(metadata)]() mutable {
|
||||
} catch (const std::exception& e) { scanError = e.what(); }
|
||||
const int parsedCount = static_cast<int>(metadata.size());
|
||||
return [this, scanGen, metadata = std::move(metadata), rawMemoCount, parsedCount, scanError]() mutable {
|
||||
// The wallet was switched/locked between post and now — this metadata belongs to the previous
|
||||
// session. resetChatSession already reset the in-flight flag, so just drop; clearing it here
|
||||
// would clobber a new session's own in-flight scan (mirrors the broadcast/identity guards).
|
||||
if (scanGen != chat_session_generation_) return;
|
||||
chat_fast_scan_in_flight_ = false;
|
||||
// Diagnostic (console App/chat channel): log when the memo-note count at the reply address
|
||||
// CHANGES, so a stable mempool doesn't spam every 2.5s. Reveals whether inbound 0-conf messages
|
||||
// are reaching us at all, and how many parse as chat vs are unrelated memos.
|
||||
if (!scanError.empty()) {
|
||||
wallet::liteLog("chat[0conf]: reply-addr scan failed: " + scanError);
|
||||
} else if (rawMemoCount != chat_fast_scan_last_seen_) {
|
||||
chat_fast_scan_last_seen_ = rawMemoCount;
|
||||
if (rawMemoCount > 0)
|
||||
wallet::liteLog("chat[0conf]: reply-addr scan sees " + std::to_string(rawMemoCount) +
|
||||
" memo note(s), " + std::to_string(parsedCount) + " parse as chat");
|
||||
}
|
||||
if (metadata.empty()) return;
|
||||
// Skip HIDDEN conversations — the mempool fast path deliberately doesn't surface them; they
|
||||
// still come back through the normal confirmed harvest (which un-hides on a new message).
|
||||
@@ -3229,6 +3635,9 @@ void App::fastScanChatMemos()
|
||||
std::unordered_map<std::string, std::int64_t> noTimes; // mempool: no block time → ingest uses now
|
||||
std::vector<std::string> newChatCids;
|
||||
chat_service_.ingest(visible, noTimes, std::time(nullptr), &newChatCids);
|
||||
if (!newChatCids.empty())
|
||||
wallet::liteLog("chat[0conf]: harvested " + std::to_string(newChatCids.size()) +
|
||||
" new message(s) from the mempool");
|
||||
if (current_page_ != ui::NavPage::Chat &&
|
||||
std::any_of(newChatCids.begin(), newChatCids.end(),
|
||||
[this](const std::string& cid){ return !(settings_ && settings_->isChatMuted(cid)); }))
|
||||
@@ -4087,6 +4496,9 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
||||
return;
|
||||
}
|
||||
lite_send_callback_ = std::move(callback); // delivered from update()
|
||||
// Claim the shared broadcast channel so the chat note-buffer coordinator yields to this user send
|
||||
// and routes the one global broadcast result back to lite_send_callback_ (not a chat echo).
|
||||
inflight_op_ = LiteInflightOp{ LiteOpKind::UserSend, {}, chat_session_generation_, ImGui::GetTime() };
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4149,12 +4561,13 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
||||
void App::submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
|
||||
const std::string& memo, const nlohmann::json& recipients,
|
||||
const char* traceLabel, bool markFeeGapRetry,
|
||||
std::function<void(bool, const std::string&)> callback)
|
||||
std::function<void(bool, const std::string&)> callback,
|
||||
bool background)
|
||||
{
|
||||
send_progress_active_ = true;
|
||||
if (!background) send_progress_active_ = true; // autonomous chat/split: status shown on the message
|
||||
++send_submissions_in_flight_;
|
||||
worker_->post([this, from, to, amount, fee, memo, recipients, callback, traceLabel,
|
||||
markFeeGapRetry]() -> rpc::RPCWorker::MainCb {
|
||||
markFeeGapRetry, background]() -> rpc::RPCWorker::MainCb {
|
||||
bool ok = false;
|
||||
std::string result_str;
|
||||
try {
|
||||
@@ -4169,7 +4582,7 @@ void App::submitZSendMany(const std::string& from, const std::string& to, double
|
||||
// below would never run, leaving a stuck "Sending…" spinner.
|
||||
result_str = "Send failed (unknown error)";
|
||||
}
|
||||
return [this, callback, ok, result_str, from, to, amount, fee, memo, markFeeGapRetry]() {
|
||||
return [this, callback, ok, result_str, from, to, amount, fee, memo, markFeeGapRetry, background]() {
|
||||
if (send_submissions_in_flight_ > 0) --send_submissions_in_flight_;
|
||||
if (ok) {
|
||||
// A send changes address balances — refresh on next cycle
|
||||
@@ -4192,7 +4605,7 @@ void App::submitZSendMany(const std::string& from, const std::string& to, double
|
||||
callback(true, result_str); // no opid to track — report as-is
|
||||
}
|
||||
} else {
|
||||
send_progress_active_ = false;
|
||||
if (!background) send_progress_active_ = false;
|
||||
if (callback) callback(false, result_str);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -228,6 +228,17 @@ bool ChatDatabase::ensureOpen()
|
||||
exec("PRAGMA journal_mode=WAL");
|
||||
exec("PRAGMA synchronous=NORMAL");
|
||||
|
||||
// C3-1: restrict the chat DB and its WAL/SHM sidecars to owner-only. sqlite creates them with
|
||||
// umask-derived permissions (often world/group-readable); they hold per-row nonces + AEAD
|
||||
// ciphertext of the user's messages. Best-effort (errors swallowed; a no-op-ish on Windows).
|
||||
{
|
||||
std::error_code perr;
|
||||
const auto ownerOnly = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write;
|
||||
std::filesystem::permissions(database_path_, ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
std::filesystem::permissions(database_path_ + "-wal", ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
std::filesystem::permissions(database_path_ + "-shm", ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
}
|
||||
|
||||
if (!createSchema()) {
|
||||
close();
|
||||
return false;
|
||||
|
||||
@@ -16,7 +16,8 @@ std::string buildHeaderMemo(const std::string& replyZaddr,
|
||||
const std::string& conversationId,
|
||||
const char* type,
|
||||
const std::string& streamHeaderHex,
|
||||
const std::string& publicKeyHex)
|
||||
const std::string& publicKeyHex,
|
||||
std::int64_t sentAt)
|
||||
{
|
||||
nlohmann::json header;
|
||||
header["h"] = 1; // header number (>= 1)
|
||||
@@ -26,6 +27,7 @@ std::string buildHeaderMemo(const std::string& replyZaddr,
|
||||
header["t"] = type; // "Memo" or "Cont"
|
||||
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont)
|
||||
header["p"] = publicKeyHex; // my 64-hex crypto_kx public key
|
||||
if (sentAt > 0) header["ts"] = sentAt; // optional sender compose time (Unix s) — receiver shows this
|
||||
return header.dump();
|
||||
}
|
||||
|
||||
@@ -67,7 +69,8 @@ ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
||||
|
||||
OutgoingChatMemos memos;
|
||||
memos.recipientZaddr = peerZaddr;
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex);
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex,
|
||||
static_cast<std::int64_t>(std::time(nullptr)));
|
||||
memos.payloadMemo = ciphertextHex;
|
||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
||||
@@ -95,7 +98,8 @@ ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
||||
|
||||
OutgoingChatMemos memos;
|
||||
memos.recipientZaddr = peerZaddr;
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex);
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex,
|
||||
static_cast<std::int64_t>(std::time(nullptr)));
|
||||
memos.payloadMemo = requestText;
|
||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
||||
|
||||
@@ -124,6 +124,10 @@ HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
|
||||
if (!readRequiredString(object, "t", type, error)) return fail(error);
|
||||
if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error);
|
||||
if (!readRequiredString(object, "p", header.public_key_hex, error)) return fail(error);
|
||||
// Optional sender compose time (Unix seconds). Absent on older senders — leave sent_at = 0 so the
|
||||
// receiver falls back to the tx/receive time. Read leniently; never fail the header on a bad value.
|
||||
if (auto it = object.find("ts"); it != object.end() && it->is_number_integer())
|
||||
header.sent_at = it->get<std::int64_t>();
|
||||
|
||||
if (header.header_number < 1) return fail("header number must be positive");
|
||||
if (header.version != kHushChatSupportedVersion) return fail("unsupported HushChat version");
|
||||
@@ -303,6 +307,7 @@ HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
||||
metadata.sender_public_key_hex = pair.header.public_key_hex;
|
||||
metadata.secretstream_header_hex = pair.header.secretstream_header_hex;
|
||||
metadata.payload_memo = pair.payload_memo;
|
||||
metadata.sent_at = pair.header.sent_at; // carry the sender's compose time (0 if absent)
|
||||
result.metadata.push_back(std::move(metadata));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ struct HushChatHeader {
|
||||
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||
std::string secretstream_header_hex;
|
||||
std::string public_key_hex;
|
||||
// Optional sender-stamped compose time (header "ts", Unix seconds). 0 = absent (older sender) → the
|
||||
// receiver falls back to the tx/receive time. Lets both sides show the SAME (send) time.
|
||||
std::int64_t sent_at = 0;
|
||||
};
|
||||
|
||||
struct HushChatHeaderParseResult {
|
||||
@@ -80,6 +83,7 @@ struct HushChatTransactionMetadata {
|
||||
std::string sender_public_key_hex; // header "p": peer crypto_kx public key (hex)
|
||||
std::string secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest)
|
||||
std::string payload_memo; // ciphertext hex (Message) or plaintext request text (ContactRequest)
|
||||
std::int64_t sent_at = 0; // header "ts": sender compose time (Unix s); 0 = absent → use tx time
|
||||
};
|
||||
|
||||
struct HushChatTransactionExtractionResult {
|
||||
|
||||
@@ -45,8 +45,21 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
||||
message.conversation_id = meta.conversation_id;
|
||||
message.peer_zaddr = meta.reply_zaddr;
|
||||
message.peer_public_key_hex = meta.sender_public_key_hex;
|
||||
// Reference time: the tx/receive time (block time if confirmed, else the receiver's wall clock for
|
||||
// a mempool receive).
|
||||
const auto timeIt = txTimestamps.find(meta.txid);
|
||||
message.timestamp = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
|
||||
const std::int64_t refTime = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
|
||||
// Prefer the sender's stamped compose time (header "ts") — the true send time, shown identically on
|
||||
// both ends. But REJECT a value implausibly in the FUTURE vs the reference: a wrong/ahead peer clock
|
||||
// would otherwise pin their messages to the bottom of the thread forever. A compose time in the
|
||||
// PAST is fine — the note buffer can broadcast a queued message long after it was composed, and a
|
||||
// confirmed tx's block time is always >= the compose time.
|
||||
constexpr std::int64_t kSenderTsFutureToleranceSec = 3600; // 1 hour of clock skew tolerated
|
||||
if (meta.sent_at > 0 && (refTime <= 0 || meta.sent_at <= refTime + kSenderTsFutureToleranceSec)) {
|
||||
message.timestamp = meta.sent_at;
|
||||
} else {
|
||||
message.timestamp = refTime;
|
||||
}
|
||||
message.payload_position = meta.payload_position;
|
||||
|
||||
if (meta.type == HushChatHeaderType::ContactRequest) {
|
||||
|
||||
@@ -28,6 +28,16 @@ public:
|
||||
// Distinct conversation ids, in first-seen order.
|
||||
std::vector<std::string> conversationIds() const;
|
||||
|
||||
// The set of on-chain txids that carried a chat message (sent or received, messages + contact
|
||||
// requests) — used by the History tab to badge / filter chat transactions. O(messages), no copies.
|
||||
std::unordered_set<std::string> chatTxids() const {
|
||||
std::unordered_set<std::string> out;
|
||||
out.reserve(messages_.size());
|
||||
for (const auto& m : messages_)
|
||||
if (!m.txid.empty()) out.insert(m.txid);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::size_t size() const { return messages_.size(); }
|
||||
bool empty() const { return messages_.empty(); }
|
||||
void clear();
|
||||
|
||||
@@ -157,6 +157,16 @@ bool Settings::load(const std::string& path)
|
||||
for (const auto& c : j["hidden_chat_cids"])
|
||||
if (c.is_string()) hidden_chat_cids_.push_back(c.get<std::string>());
|
||||
}
|
||||
// Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
|
||||
loadScalar(j, "chat_emoji_color", chat_emoji_color_);
|
||||
loadScalar(j, "chat_poll_rate_sec", chat_poll_rate_sec_); setChatPollRateSec(chat_poll_rate_sec_);
|
||||
loadScalar(j, "chat_bubble_style", chat_bubble_style_); setChatBubbleStyle(chat_bubble_style_);
|
||||
loadScalar(j, "chat_bubble_accent", chat_bubble_accent_); setChatBubbleAccent(chat_bubble_accent_);
|
||||
loadScalar(j, "chat_density", chat_density_); setChatDensity(chat_density_);
|
||||
loadScalar(j, "chat_font_scale", chat_font_scale_); setChatFontScale(chat_font_scale_);
|
||||
loadScalar(j, "chat_time_format", chat_time_format_); setChatTimeFormat(chat_time_format_);
|
||||
loadScalar(j, "chat_enter_sends", chat_enter_sends_);
|
||||
loadScalar(j, "time_format", time_format_); setTimeFormat(time_format_);
|
||||
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
|
||||
loadScalar(j, "acrylic_quality", acrylic_quality_);
|
||||
loadScalar(j, "blur_multiplier", blur_multiplier_);
|
||||
@@ -186,6 +196,8 @@ bool Settings::load(const std::string& path)
|
||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
|
||||
if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
|
||||
loadScalar(j, "contacts_avatar_shape", contacts_avatar_shape_); setContactsAvatarShape(contacts_avatar_shape_);
|
||||
loadScalar(j, "contacts_list_scale", contacts_list_scale_); setContactsListScale(contacts_list_scale_);
|
||||
loadScalar(j, "animate_avatars", animate_avatars_);
|
||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||
loadScalar(j, "console_line_accents", console_line_accents_);
|
||||
@@ -438,6 +450,15 @@ bool Settings::save(const std::string& path)
|
||||
j["hidden_chat_cids"] = json::array();
|
||||
for (const auto& c : hidden_chat_cids_)
|
||||
j["hidden_chat_cids"].push_back(c);
|
||||
j["chat_emoji_color"] = chat_emoji_color_;
|
||||
j["chat_poll_rate_sec"] = chat_poll_rate_sec_;
|
||||
j["chat_bubble_style"] = chat_bubble_style_;
|
||||
j["chat_bubble_accent"] = chat_bubble_accent_;
|
||||
j["chat_density"] = chat_density_;
|
||||
j["chat_font_scale"] = chat_font_scale_;
|
||||
j["chat_time_format"] = chat_time_format_;
|
||||
j["chat_enter_sends"] = chat_enter_sends_;
|
||||
j["time_format"] = time_format_;
|
||||
j["acrylic_enabled"] = acrylic_enabled_;
|
||||
j["acrylic_quality"] = acrylic_quality_;
|
||||
j["blur_multiplier"] = blur_multiplier_;
|
||||
@@ -448,6 +469,8 @@ bool Settings::save(const std::string& path)
|
||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||
j["portfolio_style"] = portfolio_style_;
|
||||
j["contacts_view_mode"] = contacts_view_mode_;
|
||||
j["contacts_avatar_shape"] = contacts_avatar_shape_;
|
||||
j["contacts_list_scale"] = contacts_list_scale_;
|
||||
j["animate_avatars"] = animate_avatars_;
|
||||
j["scanline_enabled"] = scanline_enabled_;
|
||||
j["console_line_accents"] = console_line_accents_;
|
||||
|
||||
@@ -91,7 +91,8 @@ public:
|
||||
bool showValue = true; // show the converted/fiat value on the card
|
||||
bool show24h = false; // show the 24h % change (live-market bases only)
|
||||
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
|
||||
int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history)
|
||||
int sparklineInterval = 4; // 0=minute 1=hour 2=day 3=week 4=month (default month: a real curve
|
||||
// from the daily series, vs the young in-session minute buffer)
|
||||
// Per-wallet visibility: "" (shown in every wallet — legacy/global) or a wallet-identity
|
||||
// hash (shown only when that wallet is active). New entries are tagged with the current
|
||||
// wallet so a portfolio built for wallet A doesn't clutter wallet B.
|
||||
@@ -143,6 +144,27 @@ public:
|
||||
hidden_chat_cids_.end());
|
||||
}
|
||||
|
||||
// ── Chat-tab customization (chat settings modal + Settings → Chat & Contacts) ──────
|
||||
bool getChatEmojiColor() const { return chat_emoji_color_; }
|
||||
void setChatEmojiColor(bool v) { chat_emoji_color_ = v; }
|
||||
float getChatPollRateSec() const { return chat_poll_rate_sec_; }
|
||||
void setChatPollRateSec(float v) { chat_poll_rate_sec_ = std::max(0.5f, std::min(15.0f, v)); }
|
||||
int getChatBubbleStyle() const { return chat_bubble_style_; }
|
||||
void setChatBubbleStyle(int v) { chat_bubble_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
int getChatBubbleAccent() const { return chat_bubble_accent_; }
|
||||
void setChatBubbleAccent(int v) { chat_bubble_accent_ = (v < 0 || v > 5) ? 0 : v; }
|
||||
int getChatDensity() const { return chat_density_; }
|
||||
void setChatDensity(int v) { chat_density_ = (v < 0 || v > 1) ? 0 : v; }
|
||||
float getChatFontScale() const { return chat_font_scale_; }
|
||||
void setChatFontScale(float v) { chat_font_scale_ = std::max(0.8f, std::min(1.5f, v)); }
|
||||
int getChatTimeFormat() const { return chat_time_format_; } // 0=follow global, 1=24h, 2=12h
|
||||
void setChatTimeFormat(int v) { chat_time_format_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
bool getChatEnterSends() const { return chat_enter_sends_; }
|
||||
void setChatEnterSends(bool v) { chat_enter_sends_ = v; }
|
||||
// Global clock format (0=24h, 1=12h) — chat can override it for the Chat tab only.
|
||||
int getTimeFormat() const { return time_format_; }
|
||||
void setTimeFormat(int v) { time_format_ = (v < 0 || v > 1) ? 0 : v; }
|
||||
|
||||
// Privacy
|
||||
bool getSaveZtxs() const { return save_ztxs_; }
|
||||
void setSaveZtxs(bool save) { save_ztxs_ = save; }
|
||||
@@ -205,13 +227,19 @@ public:
|
||||
std::string getBalanceLayout() const { return balance_layout_; }
|
||||
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
|
||||
|
||||
// Market-tab portfolio row style: 0 = single-line, 1 = two-line, 2 = value-hero. Cycled with
|
||||
// Left/Right arrows on the Market tab (like the Overview layouts).
|
||||
// Market-tab portfolio row style: 0 = Table (borderless grid), 1 = Cards (glass card + Z/T bar),
|
||||
// 2 = Spotlight (hero value). Cycled with Left/Right arrows or set in the Market settings modal.
|
||||
int getPortfolioStyle() const { return portfolio_style_; }
|
||||
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
|
||||
int getContactsViewMode() const { return contacts_view_mode_; }
|
||||
void setContactsViewMode(int v) { contacts_view_mode_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
// Contacts customization (gear modal): avatar shape + card/list row scale.
|
||||
// Avatar shape: 0 = circle, 1 = rounded square, 2 = full-row-height left tab (rounded-left, flat right).
|
||||
int getContactsAvatarShape() const { return contacts_avatar_shape_; }
|
||||
void setContactsAvatarShape(int v) { contacts_avatar_shape_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
float getContactsListScale() const { return contacts_list_scale_; }
|
||||
void setContactsListScale(float v) { contacts_list_scale_ = std::max(0.8f, std::min(1.5f, v)); }
|
||||
// Play animated contact avatars (GIF/WebP). Off = show the first frame only.
|
||||
bool getAnimateAvatars() const { return animate_avatars_; }
|
||||
void setAnimateAvatars(bool v) { animate_avatars_ = v; }
|
||||
@@ -500,6 +528,16 @@ private:
|
||||
std::string chat_reply_zaddr_;
|
||||
std::vector<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
|
||||
std::vector<std::string> hidden_chat_cids_; // hidden chat conversations by cid
|
||||
// Chat-tab customization (chat settings modal + Settings → Chat & Contacts).
|
||||
bool chat_emoji_color_ = true; // true = color (needs FreeType; falls back to mono if absent), false = monochrome
|
||||
float chat_poll_rate_sec_ = 2.5f; // 0-conf chat fast-scan cadence (full node)
|
||||
int chat_bubble_style_ = 0; // 0 = rounded, 1 = square, 2 = minimal
|
||||
int chat_bubble_accent_ = 0; // outgoing-bubble accent preset (0 = theme primary)
|
||||
int chat_density_ = 0; // 0 = comfortable, 1 = compact
|
||||
float chat_font_scale_ = 1.0f; // message text scale
|
||||
int chat_time_format_ = 0; // 0 = follow global, 1 = 24h, 2 = 12h (Chat tab only)
|
||||
bool chat_enter_sends_ = true; // Enter sends (vs. inserts newline; Ctrl+Enter sends)
|
||||
int time_format_ = 0; // global clock: 0 = 24h, 1 = 12h
|
||||
bool save_ztxs_ = true;
|
||||
bool auto_shield_ = true;
|
||||
bool use_tor_ = false;
|
||||
@@ -522,8 +560,10 @@ private:
|
||||
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||
#endif
|
||||
std::string balance_layout_ = "classic";
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 Table / 1 Cards / 2 Spotlight)
|
||||
int contacts_view_mode_ = 0; // Contacts address-list view (0 cards / 1 list / 2 table)
|
||||
int contacts_avatar_shape_ = 0; // 0 = circle, 1 = rounded square, 2 = full-height left tab
|
||||
float contacts_list_scale_ = 1.0f; // card/list row scale (does not affect the table view)
|
||||
bool animate_avatars_ = true; // play animated (GIF/WebP) contact avatars
|
||||
bool scanline_enabled_ = true;
|
||||
bool console_line_accents_ = true; // left color accent bars in console output
|
||||
|
||||
@@ -136,6 +136,14 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
|
||||
}
|
||||
}
|
||||
|
||||
// W1-2: never hand back a live seed on a failure path. If the mnemonic was exported but a
|
||||
// later step failed (empty address, or z_getnewaddress threw), the caller discards this
|
||||
// result without wiping it, which would leave the seed resident. Success keeps it deliberately.
|
||||
if (!r.ok && !r.seedPhrase.empty()) {
|
||||
sodium_memzero(&r.seedPhrase[0], r.seedPhrase.size());
|
||||
r.seedPhrase.clear();
|
||||
}
|
||||
|
||||
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
|
||||
cli.disconnect();
|
||||
temp.stop(20000);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// xmrig_manager.cpp — Pool mining process management via xmrig-hac.
|
||||
// xmrig_manager.cpp — Pool mining process management via drg-xmrig.
|
||||
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
|
||||
|
||||
#include "xmrig_manager.h"
|
||||
@@ -208,19 +208,20 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
|
||||
|
||||
try {
|
||||
fs::create_directories(fs::path(outPath).parent_path());
|
||||
std::ofstream ofs(outPath);
|
||||
std::ofstream ofs(outPath, std::ios::trunc);
|
||||
if (!ofs.is_open()) {
|
||||
last_error_ = "Cannot write xmrig config: " + outPath;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
ofs << j.dump(4);
|
||||
ofs.close();
|
||||
|
||||
#ifndef _WIN32
|
||||
// 0600 permissions — only owner can read/write
|
||||
// Restrict to owner (0600) BEFORE writing any secret material (API token, wallet
|
||||
// address, worker name). The file is still empty here, so the config is never
|
||||
// world-readable — closing the window between creation and the previous post-write chmod.
|
||||
chmod(outPath.c_str(), 0600);
|
||||
#endif
|
||||
ofs << j.dump(4);
|
||||
ofs.close();
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
last_error_ = std::string("Config write error: ") + e.what();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "wallet_state.h"
|
||||
#include "../util/text_format.h" // util::formatClockDateTime (app-wide 24h/12h clock)
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <sstream>
|
||||
@@ -44,13 +45,7 @@ int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses)
|
||||
std::string TransactionInfo::getTimeString() const
|
||||
{
|
||||
if (timestamp == 0) return "Unknown";
|
||||
|
||||
std::time_t t = static_cast<std::time_t>(timestamp);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(tm, "%Y-%m-%d %H:%M");
|
||||
return ss.str();
|
||||
return util::formatClockDateTime(timestamp);
|
||||
}
|
||||
|
||||
std::string TransactionInfo::getTypeDisplay() const
|
||||
@@ -77,13 +72,7 @@ std::string PeerInfo::getConnectionTime() const
|
||||
std::string BannedPeer::getBannedUntilString() const
|
||||
{
|
||||
if (banned_until == 0) return "Never";
|
||||
|
||||
std::time_t t = static_cast<std::time_t>(banned_until);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(tm, "%Y-%m-%d %H:%M");
|
||||
return ss.str();
|
||||
return util::formatClockDateTime(banned_until);
|
||||
}
|
||||
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -16,3 +16,4 @@ INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf"
|
||||
INCBIN(mdi_pickaxe_subset, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf");
|
||||
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");
|
||||
INCBIN(noto_emoji_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoEmoji-Subset.ttf");
|
||||
INCBIN(twemoji_color, "@CMAKE_SOURCE_DIR@/res/fonts/TwemojiMozilla-Color.ttf");
|
||||
|
||||
@@ -38,4 +38,8 @@ extern "C" {
|
||||
|
||||
extern const unsigned char g_noto_emoji_subset_data[];
|
||||
extern const unsigned int g_noto_emoji_subset_size;
|
||||
|
||||
// Twemoji COLR/CPAL color-emoji font (used only when color emoji is enabled + FreeType is available).
|
||||
extern const unsigned char g_twemoji_color_data[];
|
||||
extern const unsigned int g_twemoji_color_size;
|
||||
}
|
||||
|
||||
39
src/embedded/logo_dragonx_svg.h
Normal file
39
src/embedded/logo_dragonx_svg.h
Normal file
@@ -0,0 +1,39 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Embedded source of the DragonX mark (res/img/logos/logo_dragonx.svg). Kept as a string so the logo
|
||||
// is available in every build (dev + portable single-file) with no resource-pipeline or file dependency.
|
||||
// It is rasterized + recolored per theme at runtime (see util/svg_texture.*). Two fills: the crimson
|
||||
// body (.cls-2 #d82652) becomes the theme accent; the white detail (.cls-1 #fff) becomes the light tone.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Global `embedded` namespace to match the generated embedded resources (embedded::ui_toml_data, etc.).
|
||||
namespace embedded {
|
||||
|
||||
inline constexpr const char* kLogoDragonXSvg = R"SVG(<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #d82652;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
|
||||
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
|
||||
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>)SVG";
|
||||
|
||||
} // namespace embedded
|
||||
@@ -25,9 +25,11 @@ namespace {
|
||||
|
||||
// Recursively zero every string value in a JSON tree in place — used to wipe a discarded parse tree
|
||||
// that held a secret (B7). Operates on the underlying std::string buffers via get_ref.
|
||||
void scrubJsonSecrets(nlohmann::json& j) {
|
||||
// Templated so it works on both nlohmann::json and nlohmann::ordered_json (callRaw uses the latter).
|
||||
template <typename J>
|
||||
void scrubJsonSecrets(J& j) {
|
||||
if (j.is_string()) {
|
||||
auto& s = j.get_ref<std::string&>();
|
||||
auto& s = j.template get_ref<std::string&>();
|
||||
if (!s.empty()) sodium_memzero(&s[0], s.size());
|
||||
} else if (j.is_object() || j.is_array()) {
|
||||
for (auto& el : j) scrubJsonSecrets(el);
|
||||
@@ -96,6 +98,10 @@ void RPCClient::setTraceSource(std::string source)
|
||||
// Callback for libcurl to write response data
|
||||
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
|
||||
size_t totalSize = size * nmemb;
|
||||
// Bound accumulation so a hostile/compromised daemon cannot OOM the client with an unbounded
|
||||
// response body. 256 MiB is far above any legitimate JSON-RPC response yet prevents exhaustion.
|
||||
static constexpr size_t kMaxRpcResponseBytes = 256u * 1024 * 1024;
|
||||
if (userp->size() + totalSize > kMaxRpcResponseBytes) return 0; // short count aborts the transfer
|
||||
userp->append((char*)contents, totalSize);
|
||||
return totalSize;
|
||||
}
|
||||
@@ -202,6 +208,10 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||
// budget for the TCP + TLS handshake over real network latency (1s would spuriously fail).
|
||||
const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L;
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, connectTimeout);
|
||||
// Enforce TLS certificate + hostname verification explicitly rather than relying on libcurl's
|
||||
// build defaults. Harmless on the localhost http:// case; essential for a remote https daemon.
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
|
||||
// Test connection with getinfo. Use a SHORT timeout for the probe on localhost: a healthy
|
||||
// local daemon answers in milliseconds and a warming one returns -28 just as fast, so a long
|
||||
@@ -508,14 +518,22 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
|
||||
}
|
||||
|
||||
auto& result = oj["result"];
|
||||
std::string out;
|
||||
if (result.is_null()) {
|
||||
return "null";
|
||||
out = "null";
|
||||
} else if (result.is_string()) {
|
||||
// Return the raw string (not JSON-encoded) — caller wraps as needed
|
||||
return result.get<std::string>();
|
||||
out = result.get<std::string>();
|
||||
} else {
|
||||
return result.dump(4);
|
||||
out = result.dump(4);
|
||||
}
|
||||
// B7: this raw path serves arbitrary console commands including dumpprivkey / z_exportkey,
|
||||
// whose response carries plaintext key material. Zero the raw buffer and the parsed tree so
|
||||
// the secret does not linger in freed heap (matching callSecret). The single returned copy is
|
||||
// the caller's to manage.
|
||||
scrubJsonSecrets(oj);
|
||||
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
void RPCClient::doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err)
|
||||
|
||||
@@ -434,8 +434,15 @@ std::optional<NetworkRefreshService::PriceRefreshResult> NetworkRefreshService::
|
||||
result.market.market_cap = data.value("usd_market_cap", 0.0);
|
||||
|
||||
char buf[64];
|
||||
std::tm* tm = std::localtime(&fetchedAt);
|
||||
if (tm && std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm) > 0) {
|
||||
// Runs on the RPC worker thread — std::localtime shares a process-wide static tm, so use the
|
||||
// reentrant variant into a local tm (matches the rest of the codebase).
|
||||
std::tm tmv{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&tmv, &fetchedAt);
|
||||
#else
|
||||
localtime_r(&fetchedAt, &tmv);
|
||||
#endif
|
||||
if (std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tmv) > 0) {
|
||||
result.market.last_updated = buf;
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -160,6 +160,10 @@ void ThemeEffects::loadFromTheme() {
|
||||
|
||||
// ---- Gradient Border Shift ----
|
||||
gradient_border_.enabled = eff("gradient-border-enabled").sizeOr(0.0f) > 0.5f;
|
||||
// Opt-in: also draw the shifting border on every glass panel (not just the
|
||||
// active nav button). Off by default so themes that only want the button
|
||||
// accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero.
|
||||
gradient_border_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
|
||||
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f);
|
||||
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f);
|
||||
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
|
||||
@@ -512,11 +516,15 @@ void ThemeEffects::drawGlowPulse(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
// ============================================================================
|
||||
|
||||
void ThemeEffects::drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const {
|
||||
float rounding, float phaseOffset,
|
||||
float alphaMul) const {
|
||||
if (!enabled_ || !gradient_border_.enabled) return;
|
||||
|
||||
// Smooth sinusoidal oscillation between color A and color B
|
||||
float phase = std::sin(time_ * gradient_border_.speed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
|
||||
// Smooth sinusoidal oscillation between color A and color B.
|
||||
// phaseOffset shifts where in the cycle this element sits so a wall of
|
||||
// panels reads like veins at different depths rather than one pulse.
|
||||
float phase = std::sin((time_ * gradient_border_.speed + phaseOffset)
|
||||
* 2.0f * 3.14159265f) * 0.5f + 0.5f;
|
||||
|
||||
// Extract RGBA from both colors and lerp
|
||||
RGB ca = unpackRGB(gradient_border_.colorA);
|
||||
@@ -525,7 +533,7 @@ void ThemeEffects::drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 p
|
||||
int r = ca.r + (int)((cb.r - ca.r) * phase);
|
||||
int g = ca.g + (int)((cb.g - ca.g) * phase);
|
||||
int b = ca.b + (int)((cb.b - ca.b) * phase);
|
||||
int a = scaledAlpha(gradient_border_.alpha, bgOpacity_);
|
||||
int a = scaledAlpha(gradient_border_.alpha * alphaMul, bgOpacity_);
|
||||
|
||||
// Draw the shifting border
|
||||
dl->AddRect(pMin, pMax, IM_COL32(r, g, b, a),
|
||||
@@ -896,6 +904,22 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const {
|
||||
if (!enabled_ || effects::isLowSpecMode()) return;
|
||||
|
||||
// Gradient border on panels — a slow color-shifting outline that hugs the
|
||||
// panel's rounded corners (drawn via AddRect, so it follows the rounding
|
||||
// exactly — no polygonal corners). Position-based phase offset makes each
|
||||
// panel drift like a vein at a different depth; softer than the active
|
||||
// nav button (alphaMul 0.6) so a screenful of panels stays calm.
|
||||
if (gradient_border_.enabled && gradient_border_.panels) {
|
||||
float w = pMax.x - pMin.x;
|
||||
float h = pMax.y - pMin.y;
|
||||
if (w > 80 && h > 40) { // skip small panels
|
||||
float posKey = (pMin.x * 0.0073f + pMin.y * 0.0137f);
|
||||
posKey = posKey - (int)posKey; // fractional 0..1
|
||||
if (posKey < 0) posKey += 1.0f;
|
||||
drawGradientBorderShift(dl, pMin, pMax, rounding, posKey, 0.6f);
|
||||
}
|
||||
}
|
||||
|
||||
// Edge trace on panels — use position-based phase offset so each
|
||||
// panel's tracer is at a different position around the border
|
||||
if (edge_trace_.enabled) {
|
||||
|
||||
@@ -69,9 +69,12 @@ public:
|
||||
void drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const;
|
||||
|
||||
/// Draw a border that shifts between two colors over time (gem-like)
|
||||
/// Draw a border that shifts between two colors over time (gem-like).
|
||||
/// phaseOffset (0..1) shifts this element's point in the color cycle so
|
||||
/// many panels don't pulse in unison; alphaMul scales the whole effect.
|
||||
void drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const;
|
||||
float rounding, float phaseOffset = 0.0f,
|
||||
float alphaMul = 1.0f) const;
|
||||
|
||||
/// Draw ember particles that rise from an element (fire theme)
|
||||
void drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const;
|
||||
@@ -186,6 +189,7 @@ private:
|
||||
|
||||
struct GradientBorderConfig {
|
||||
bool enabled = false;
|
||||
bool panels = false; ///< also draw on glass panels, not just the active nav button
|
||||
float speed = 0.15f; ///< full color shift cycles per second
|
||||
float thickness = 1.5f; ///< border line thickness in pixels
|
||||
float alpha = 0.6f; ///< peak alpha
|
||||
|
||||
@@ -57,6 +57,21 @@ inline ImU32 ReadableError() {
|
||||
return IM_COL32(r, g, b, (e >> IM_COL32_A_SHIFT) & 0xFF);
|
||||
}
|
||||
|
||||
// Middle-ellipsis truncation ("front...back", roughly equal halves) so `text` fits within
|
||||
// maxWidth pixels when drawn with `font` at `fontSize`. Returns `text` unchanged if it already
|
||||
// fits (or maxWidth is non-positive). Display-only — never mutate the underlying value with this.
|
||||
inline std::string TruncateToWidth(const std::string& text, ImFont* font, float fontSize, float maxWidth) {
|
||||
if (text.empty() || !font || maxWidth <= 0.0f) return text;
|
||||
if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, text.c_str()).x <= maxWidth) return text;
|
||||
const int n = static_cast<int>(text.size());
|
||||
for (int f = n / 2; f >= 3; --f) {
|
||||
const int b = (f - 2 > 3) ? (f - 2) : 3; // keep the two halves roughly equal
|
||||
std::string t = text.substr(0, f) + "..." + text.substr(n - b);
|
||||
if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, t.c_str()).x <= maxWidth) return t;
|
||||
}
|
||||
return n > 6 ? (text.substr(0, 3) + "..." + text.substr(n - 3)) : text;
|
||||
}
|
||||
|
||||
// Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase.
|
||||
inline const char* LoadingDots() {
|
||||
int n = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
@@ -1347,10 +1362,17 @@ inline int SegmentedControl(ImDrawList* dl, ImVec2 origin, float totalW, float h
|
||||
ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp),
|
||||
WithAlpha(Primary(), 210), (height - 4.0f * dp) * 0.5f);
|
||||
ImVec2 ts = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0, labels[i]);
|
||||
// Center when the label fits; otherwise left-align with a small pad (so a long translation clips
|
||||
// on the right, not on BOTH sides). Clip to the cell so no label can bleed into a neighbouring
|
||||
// segment or past the rounded track — English fits, but de/es/fr/pt/ru labels can overrun.
|
||||
const float lpad = 4.0f * dp;
|
||||
const float tx = (ts.x <= cellW - 2.0f * lpad) ? (cellW - ts.x) * 0.5f : lpad;
|
||||
dl->PushClipRect(cMin, cMax, true);
|
||||
dl->AddText(font, font->LegacySize,
|
||||
ImVec2(cMin.x + (cellW - ts.x) * 0.5f, cMin.y + (height - ts.y) * 0.5f),
|
||||
ImVec2(cMin.x + tx, cMin.y + (height - ts.y) * 0.5f),
|
||||
active ? IM_COL32(255, 255, 255, 255) : (hov ? OnSurface() : OnSurfaceMedium()),
|
||||
labels[i]);
|
||||
dl->PopClipRect();
|
||||
ImGui::PushID(i);
|
||||
ImGui::SetCursorScreenPos(cMin);
|
||||
if (ImGui::InvisibleButton(idBase, ImVec2(cellW, height))) clicked = i;
|
||||
|
||||
193
src/ui/material/settings_controls.h
Normal file
193
src/ui/material/settings_controls.h
Normal file
@@ -0,0 +1,193 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Settings design-system controls — the polished chat-settings look (accent subsection headers,
|
||||
// labeled rows with right-aligned controls, iOS-style segmented controls) promoted to reusable
|
||||
// components, plus a tiered ActionButton (Primary/Secondary/Tertiary/Destructive) with optional
|
||||
// leading Material icon and a ButtonFlow that wraps rows of buttons instead of shrinking them.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "draw_helpers.h" // colors, type, layout, icons, WithAlpha, ScaleAlpha, DrawButtonGlassOverlay
|
||||
#include "imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace material {
|
||||
|
||||
// Accent small-caps subsection header ("KEYS & BACKUP", "APPEARANCE"…) — the chat-settings section() look.
|
||||
inline void SettingsSubheader(const char* text) {
|
||||
const float dp = Layout::dpiScale();
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, WithAlpha(Primary(), 235));
|
||||
ImGui::TextUnformatted(text);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
ImGui::Dummy(ImVec2(0.0f, 2.0f * dp));
|
||||
}
|
||||
|
||||
// A labeled settings row: label left, control right-aligned in a fixed column. Construct once per card
|
||||
// section with the content width (0 = auto), then call .label(text) before drawing each control (leaves
|
||||
// the cursor at the control origin and sets the next item width to the control column).
|
||||
struct SettingsRow {
|
||||
float leftX, rowW, ctrlW, rowGap;
|
||||
explicit SettingsRow(float contentWidth, float controlWidth = 250.0f) {
|
||||
const float dp = Layout::dpiScale();
|
||||
leftX = ImGui::GetCursorPosX();
|
||||
rowW = (contentWidth > 0.0f) ? contentWidth : ImGui::GetContentRegionAvail().x;
|
||||
ctrlW = controlWidth * dp;
|
||||
rowGap = 5.0f * dp;
|
||||
}
|
||||
void label(const char* text) {
|
||||
ImGui::Dummy(ImVec2(0.0f, rowGap));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(text);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), leftX + std::max(0.0f, rowW - ctrlW)));
|
||||
ImGui::SetNextItemWidth(ctrlW);
|
||||
}
|
||||
};
|
||||
|
||||
// iOS-style segmented control (rounded track + inset pill on the selection). Draws its own labeled row
|
||||
// and returns the (possibly changed) index.
|
||||
inline int SegmentedControl(SettingsRow& row, const char* label, const char* const* items, int count, int value) {
|
||||
const float dp = Layout::dpiScale();
|
||||
row.label(label);
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const float h = ImGui::GetFrameHeight();
|
||||
const float seg = row.ctrlW / static_cast<float>(count);
|
||||
const float round = 7.0f * dp;
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(origin, ImVec2(origin.x + row.ctrlW, origin.y + h), WithAlpha(OnSurface(), 20), round);
|
||||
int result = value;
|
||||
ImGui::PushID(label);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
ImGui::PushID(i);
|
||||
const ImVec2 mn(origin.x + i * seg, origin.y), mx(origin.x + (i + 1) * seg, origin.y + h);
|
||||
ImGui::SetCursorScreenPos(mn);
|
||||
if (ImGui::InvisibleButton("##s", ImVec2(seg, h))) result = i;
|
||||
const bool hov = ImGui::IsItemHovered();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
const bool sel = (value == i);
|
||||
if (sel) {
|
||||
const float in = 2.0f * dp;
|
||||
dl->AddRectFilled(ImVec2(mn.x + in, mn.y + in), ImVec2(mx.x - in, mx.y - in),
|
||||
WithAlpha(Primary(), 210), std::max(1.0f, round - in));
|
||||
} else if (hov) {
|
||||
dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), 26), round);
|
||||
}
|
||||
const ImVec2 ts = ImGui::CalcTextSize(items[i]);
|
||||
const float lpad = 4.0f * dp;
|
||||
const float tx = (ts.x <= seg - 2.0f * lpad) ? (seg - ts.x) * 0.5f : lpad;
|
||||
ImGui::PushClipRect(mn, mx, true);
|
||||
dl->AddText(ImVec2(mn.x + tx, mn.y + (h - ts.y) * 0.5f),
|
||||
sel ? IM_COL32(255, 255, 255, 236) : OnSurfaceMedium(), items[i]);
|
||||
ImGui::PopClipRect();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopID();
|
||||
ImGui::SetCursorScreenPos(origin);
|
||||
ImGui::Dummy(ImVec2(row.ctrlW, h)); // reserve the control's rect for layout flow
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Tiered action buttons ───────────────────────────────────────────────────
|
||||
// Primary = filled accent (the one main action of a group)
|
||||
// Secondary = glass (default — common actions)
|
||||
// Tertiary = ghost / low-emphasis (rarely used)
|
||||
// Destructive = error-tinted outline (delete / reset)
|
||||
enum class ActionTier { Primary, Secondary, Tertiary, Destructive };
|
||||
|
||||
// The width an ActionButton will occupy (for ButtonFlow / manual layout).
|
||||
inline float ActionButtonWidth(const char* label, const char* icon, float minWidth = 0.0f) {
|
||||
ImFont* lf = Type().button();
|
||||
ImFont* icf = Type().iconSmall();
|
||||
const float dp = Layout::dpiScale();
|
||||
const float padX = 12.0f * dp, gap = 6.0f * dp;
|
||||
const float labelW = lf->CalcTextSizeA(lf->LegacySize, FLT_MAX, 0, label).x;
|
||||
const float iconW = (icon && icon[0] && icf) ? icf->CalcTextSizeA(icf->LegacySize, FLT_MAX, 0, icon).x : 0.0f;
|
||||
const float w = padX * 2.0f + iconW + (iconW > 0.0f ? gap : 0.0f) + labelW;
|
||||
return std::max(w, minWidth);
|
||||
}
|
||||
|
||||
// A tiered action button with an optional leading Material icon (ICON_MD_* or nullptr). Auto-sizes to
|
||||
// its content (>= minWidth). Respects BeginDisabled() (dims via the style alpha, not clickable).
|
||||
inline bool ActionButton(const char* id, const char* label, const char* icon, ActionTier tier, float minWidth = 0.0f) {
|
||||
ImFont* lf = Type().button();
|
||||
ImFont* icf = Type().iconSmall();
|
||||
const float dp = Layout::dpiScale();
|
||||
const float gap = 6.0f * dp;
|
||||
const float h = ImGui::GetFrameHeight();
|
||||
const bool hasIcon = icon && icon[0] && icf;
|
||||
const float labelW = lf->CalcTextSizeA(lf->LegacySize, FLT_MAX, 0, label).x;
|
||||
const float iconW = hasIcon ? icf->CalcTextSizeA(icf->LegacySize, FLT_MAX, 0, icon).x : 0.0f;
|
||||
const float w = ActionButtonWidth(label, icon, minWidth);
|
||||
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
const bool pressed = ImGui::InvisibleButton(id, ImVec2(w, h));
|
||||
const bool hov = ImGui::IsItemHovered(), act = ImGui::IsItemActive();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 pMax(pos.x + w, pos.y + h);
|
||||
const float round = ImGui::GetStyle().FrameRounding;
|
||||
const float a = ImGui::GetStyle().Alpha; // BeginDisabled() lowers this
|
||||
|
||||
ImU32 bg = 0, border = 0, fg = OnSurface();
|
||||
bool glass = false;
|
||||
switch (tier) {
|
||||
case ActionTier::Primary:
|
||||
bg = WithAlpha(Primary(), act ? 255 : (hov ? 245 : 220));
|
||||
fg = IM_COL32(255, 255, 255, 240);
|
||||
break;
|
||||
case ActionTier::Secondary:
|
||||
bg = WithAlpha(OnSurface(), hov ? 26 : 16);
|
||||
border = WithAlpha(OnSurface(), 40);
|
||||
glass = true;
|
||||
fg = OnSurface();
|
||||
break;
|
||||
case ActionTier::Tertiary:
|
||||
bg = hov ? WithAlpha(OnSurface(), 18) : 0;
|
||||
fg = OnSurfaceMedium();
|
||||
break;
|
||||
case ActionTier::Destructive:
|
||||
bg = hov ? WithAlpha(Error(), 32) : WithAlpha(Error(), 12);
|
||||
border = WithAlpha(Error(), 90);
|
||||
fg = Error();
|
||||
break;
|
||||
}
|
||||
if (bg) dl->AddRectFilled(pos, pMax, ScaleAlpha(bg, a), round);
|
||||
if (border) dl->AddRect(pos, pMax, ScaleAlpha(border, a), round, 0, 1.0f);
|
||||
if (glass) DrawButtonGlassOverlay(dl, pos, pMax, round, act, hov);
|
||||
fg = ScaleAlpha(fg, a);
|
||||
|
||||
const float contentW = iconW + (iconW > 0.0f ? gap : 0.0f) + labelW;
|
||||
float cx = pos.x + (w - contentW) * 0.5f;
|
||||
if (hasIcon) {
|
||||
dl->AddText(icf, icf->LegacySize, ImVec2(cx, pos.y + (h - icf->LegacySize) * 0.5f), fg, icon);
|
||||
cx += iconW + gap;
|
||||
}
|
||||
dl->AddText(lf, lf->LegacySize, ImVec2(cx, pos.y + (h - lf->LegacySize) * 0.5f), fg, label);
|
||||
return pressed;
|
||||
}
|
||||
|
||||
// Places ActionButtons left→right, wrapping to a new row when the next one won't fit (instead of the
|
||||
// old font-scale-to-fit). Call next(width) before each ActionButton.
|
||||
struct ButtonFlow {
|
||||
float availW, gap; float x = 0.0f; bool firstOnRow = true;
|
||||
explicit ButtonFlow(float availWidth, float gapPx = 8.0f) : availW(availWidth) {
|
||||
gap = gapPx * Layout::dpiScale();
|
||||
}
|
||||
void next(float w) {
|
||||
if (firstOnRow) { firstOnRow = false; x = w; return; }
|
||||
if (x + gap + w <= availW) { ImGui::SameLine(0, gap); x += gap + w; }
|
||||
else { x = w; } // natural newline wraps to the next row
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace material
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -15,6 +15,11 @@
|
||||
#include "../embedded/IconsMaterialDesign.h" // Icon codepoint defines
|
||||
#include "../../util/logger.h"
|
||||
|
||||
#ifdef DRAGONX_HAVE_FREETYPE
|
||||
#include "imgui_internal.h" // ImFontAtlasGetFontLoaderForStbTruetype
|
||||
#include "misc/freetype/imgui_freetype.h" // ImGuiFreeType::GetFontLoader + LoadColor flag
|
||||
#endif
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace material {
|
||||
@@ -125,7 +130,16 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
|
||||
float scale = dpiScale * Layout::kFontScale() * Layout::userFontScale();
|
||||
DEBUG_LOGF("Typography: Loading Material Design type scale (DPI: %.2f, fontScale: %.2f, userFontScale: %.2f, combined: %.2f)\n",
|
||||
dpiScale, Layout::kFontScale(), Layout::userFontScale(), scale);
|
||||
|
||||
|
||||
#ifdef DRAGONX_HAVE_FREETYPE
|
||||
// Choose the atlas font loader BEFORE any font is added: FreeType (required to rasterize COLR
|
||||
// color-emoji glyphs) when color emoji is enabled, else the default stb_truetype loader. Toggling
|
||||
// the setting + reload() flips this cleanly.
|
||||
io.Fonts->SetFontLoader(color_emoji_ ? ImGuiFreeType::GetFontLoader()
|
||||
: ImFontAtlasGetFontLoaderForStbTruetype());
|
||||
DEBUG_LOGF("Typography: font loader = %s\n", color_emoji_ ? "FreeType (color emoji)" : "stb_truetype");
|
||||
#endif
|
||||
|
||||
// For ImGui, we need to load fonts at specific pixel sizes.
|
||||
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
|
||||
|
||||
@@ -335,9 +349,22 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
};
|
||||
bool wantEmoji = false;
|
||||
for (const char* n : kEmojiFonts) if (strcmp(name, n) == 0) { wantEmoji = true; break; }
|
||||
if (wantEmoji && g_noto_emoji_subset_size > 0) {
|
||||
void* emojiCopy = IM_ALLOC(g_noto_emoji_subset_size);
|
||||
memcpy(emojiCopy, g_noto_emoji_subset_data, g_noto_emoji_subset_size);
|
||||
|
||||
// Emoji blob: the COLR/CPAL color font (FreeType-rendered) when color emoji is enabled and this
|
||||
// is a FreeType build, else the monochrome subset (default / non-FreeType path).
|
||||
const unsigned char* emojiData = g_noto_emoji_subset_data;
|
||||
unsigned int emojiSize = g_noto_emoji_subset_size;
|
||||
bool colorGlyphs = false;
|
||||
#ifdef DRAGONX_HAVE_FREETYPE
|
||||
if (color_emoji_ && g_twemoji_color_size > 0) {
|
||||
emojiData = g_twemoji_color_data;
|
||||
emojiSize = g_twemoji_color_size;
|
||||
colorGlyphs = true;
|
||||
}
|
||||
#endif
|
||||
if (wantEmoji && emojiSize > 0) {
|
||||
void* emojiCopy = IM_ALLOC(emojiSize);
|
||||
memcpy(emojiCopy, emojiData, emojiSize);
|
||||
|
||||
ImFontConfig emojiCfg;
|
||||
emojiCfg.FontDataOwnedByAtlas = true;
|
||||
@@ -346,6 +373,9 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
emojiCfg.OversampleV = 1;
|
||||
emojiCfg.PixelSnapH = true;
|
||||
emojiCfg.GlyphMinAdvanceX = 0;
|
||||
#ifdef DRAGONX_HAVE_FREETYPE
|
||||
if (colorGlyphs) emojiCfg.FontLoaderFlags |= ImGuiFreeTypeLoaderFlags_LoadColor; // render COLR in color
|
||||
#endif
|
||||
// The base Ubuntu font already owns U+2600–26FF etc.; MergeMode keeps the first-loaded glyph,
|
||||
// so its text-style symbols win and only the codepoints it lacks fall through to emoji.
|
||||
static const ImWchar emojiRanges[] = {
|
||||
@@ -355,11 +385,13 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
0,
|
||||
};
|
||||
emojiCfg.GlyphRanges = emojiRanges;
|
||||
snprintf(emojiCfg.Name, sizeof(emojiCfg.Name), "NotoEmoji %.0fpx (merge)", size);
|
||||
snprintf(emojiCfg.Name, sizeof(emojiCfg.Name), "%s %.0fpx (merge)",
|
||||
colorGlyphs ? "Twemoji" : "NotoEmoji", size);
|
||||
|
||||
ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(emojiCopy, g_noto_emoji_subset_size, size, &emojiCfg);
|
||||
ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(emojiCopy, emojiSize, size, &emojiCfg);
|
||||
if (emojiMerge) {
|
||||
DEBUG_LOGF("Typography: Merged emoji (%u bytes) into %s OK\n", g_noto_emoji_subset_size, name);
|
||||
DEBUG_LOGF("Typography: Merged %s emoji (%u bytes) into %s OK\n",
|
||||
colorGlyphs ? "color" : "mono", emojiSize, name);
|
||||
} else {
|
||||
DEBUG_LOGF("Typography: WARNING — emoji merge FAILED for %s (size=%u)\n", name, size);
|
||||
}
|
||||
|
||||
@@ -112,6 +112,14 @@ public:
|
||||
* @brief Get the current DPI scale
|
||||
*/
|
||||
float getDpiScale() const { return dpiScale_; }
|
||||
|
||||
/**
|
||||
* @brief Select color vs monochrome emoji for the next (re)load. Color needs a FreeType-enabled
|
||||
* build (DRAGONX_HAVE_FREETYPE); otherwise this is inert and monochrome is always used.
|
||||
* Set before load()/reload() (App does this from the chat_emoji_color setting).
|
||||
*/
|
||||
void setColorEmoji(bool enabled) { color_emoji_ = enabled; }
|
||||
bool colorEmoji() const { return color_emoji_; }
|
||||
|
||||
/**
|
||||
* @brief Get font for a type style
|
||||
@@ -261,7 +269,8 @@ private:
|
||||
|
||||
bool loaded_ = false;
|
||||
float dpiScale_ = 1.0f;
|
||||
|
||||
bool color_emoji_ = false; // when true + FreeType present, merge the COLR color-emoji font
|
||||
|
||||
// Fonts for each type style
|
||||
ImFont* fonts_[15] = {};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,7 @@ AddressRowLayout ComputeAddressRowLayout(float rowX,
|
||||
float spacingSm,
|
||||
float spacingXs)
|
||||
{
|
||||
(void)spacingXs; // trailing button now insets by rowPadLeft (mirrors the left margin)
|
||||
AddressRowLayout layout;
|
||||
layout.contentStartX = rowX + rowPadLeft;
|
||||
layout.contentStartY = rowY + spacingMd;
|
||||
@@ -77,7 +78,9 @@ AddressRowLayout ComputeAddressRowLayout(float rowX,
|
||||
|
||||
const float buttonY = rowY + (rowHeight - layout.buttonSize) * 0.5f;
|
||||
const float rightEdge = rowX + rowWidth;
|
||||
const float favoriteX = rightEdge - layout.buttonSize - spacingXs;
|
||||
// Inset the trailing (favorite/star) button by the card's inner padding — the same margin the
|
||||
// left content uses (rowPadLeft) — so it mirrors the left edge instead of hugging the card edge.
|
||||
const float favoriteX = rightEdge - layout.buttonSize - rowPadLeft;
|
||||
const float visibilityX = favoriteX - spacingSm - layout.buttonSize;
|
||||
|
||||
layout.favoriteButton = {favoriteX, buttonY, layout.buttonSize, layout.buttonSize};
|
||||
|
||||
@@ -203,10 +203,7 @@ void BlockInfoDialog::render(App* app)
|
||||
ImGui::Text("%s", TR("block_timestamp"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
if (s_block_time > 0) {
|
||||
std::time_t t = static_cast<std::time_t>(s_block_time);
|
||||
char time_buf[64];
|
||||
std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
||||
ImGui::Text("%s", time_buf);
|
||||
ImGui::Text("%s", dragonx::util::formatClockDateTime(s_block_time, /*withSeconds=*/true).c_str());
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", TR("unknown"));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,18 @@ namespace ui {
|
||||
*/
|
||||
void RenderChatTab(App* app);
|
||||
|
||||
/**
|
||||
* @brief Render the chat-customization controls (emoji style, poll rate, bubble style/color,
|
||||
* density, text size, chat timestamps, Enter-to-send). Shared by the Chat tab's settings
|
||||
* "notch" modal and the Settings tab's Chat & Contacts section. The app-wide clock format
|
||||
* (which chat timestamps fall back to) lives separately in Settings → General.
|
||||
* @param app Pointer to the app instance.
|
||||
* @param contentWidth Explicit row width for right-aligning controls; pass the card's inner content
|
||||
* width from the Settings tab (whose GlassCard doesn't narrow the content region). 0 = auto
|
||||
* (use the current content region, correct inside the chat modal's dialog).
|
||||
*/
|
||||
void RenderChatSettingsControls(App* app, float contentWidth = 0.0f);
|
||||
|
||||
/**
|
||||
* @brief Securely wipe the Chat tab's UI-local state (composer / new-conversation
|
||||
* plaintext buffers + the selected-conversation ids). Called by
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "console_command_executor.h"
|
||||
#include "console_channel.h"
|
||||
#include "console_command_reference.h" // consoleCommandCategories / liteConsoleCommandCategories
|
||||
#include "console_input_model.h" // BuildConsoleRpcCall
|
||||
|
||||
#include "../../app.h"
|
||||
@@ -28,6 +29,10 @@ namespace ui {
|
||||
|
||||
using namespace material;
|
||||
|
||||
// Classifies a diagnostics line into a console channel (defined below in an anonymous namespace); both
|
||||
// executors drain the same LiteDiagnostics ring, so declare it up front for the full-node drain.
|
||||
namespace { ConsoleChannel liteLogChannel(const std::string& line); }
|
||||
|
||||
// ============================================================================
|
||||
// FullNodeConsoleExecutor
|
||||
// ============================================================================
|
||||
@@ -172,6 +177,25 @@ void FullNodeConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add)
|
||||
} else {
|
||||
last_xmrig_output_size_ = 0; // reset so we get fresh output when it restarts
|
||||
}
|
||||
|
||||
// Chat diagnostics ring: chat code logs via wallet::liteLog on both variants (send lifecycle, the
|
||||
// 0-conf harvest, the note buffer). Surface those lines here as App messages so the full-node console
|
||||
// shows chat activity too. Same generation-cursor drain the lite console uses.
|
||||
{
|
||||
auto& diag = wallet::LiteDiagnostics::instance();
|
||||
const std::uint64_t gen = diag.generation();
|
||||
if (gen != diag_gen_) {
|
||||
const auto snap = diag.snapshot();
|
||||
std::size_t startIdx = 0;
|
||||
if (diag_gen_ != static_cast<std::uint64_t>(-1)) {
|
||||
const std::uint64_t added = gen - diag_gen_;
|
||||
startIdx = (added >= snap.size()) ? 0 : snap.size() - static_cast<std::size_t>(added);
|
||||
}
|
||||
for (std::size_t i = startIdx; i < snap.size(); ++i)
|
||||
add(snap[i], liteLogChannel(snap[i]));
|
||||
diag_gen_ = gen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FullNodeConsoleExecutor::printHelp(const ConsoleAddLineFn& add)
|
||||
@@ -194,6 +218,11 @@ void FullNodeConsoleExecutor::printHelp(const ConsoleAddLineFn& add)
|
||||
add(TR("console_tab_completion"), ConsoleChannel::Info);
|
||||
}
|
||||
|
||||
const std::vector<ConsoleCommandCategory>* FullNodeConsoleExecutor::commandReference() const
|
||||
{
|
||||
return &consoleCommandCategories();
|
||||
}
|
||||
|
||||
ConsoleStatusLine FullNodeConsoleExecutor::toolbarStatus() const
|
||||
{
|
||||
ConsoleStatusLine s;
|
||||
@@ -258,7 +287,7 @@ bool LiteConsoleExecutor::pollResult(std::string& result, bool& isError)
|
||||
if (!lw) return false;
|
||||
wallet::LiteConsoleResult res;
|
||||
if (!lw->takeConsoleResult(res)) return false;
|
||||
result = res.response.empty() ? std::string("(no output)") : res.response;
|
||||
result = res.response.empty() ? std::string(TR("console_no_output")) : res.response;
|
||||
isError = !res.ok;
|
||||
return true;
|
||||
}
|
||||
@@ -286,6 +315,19 @@ void LiteConsoleExecutor::printHelp(const ConsoleAddLineFn& add)
|
||||
add(TR("console_help_clear"), ConsoleChannel::None);
|
||||
add(TR("console_help_help"), ConsoleChannel::None);
|
||||
add(TR("lite_console_help_passthrough"), ConsoleChannel::Info);
|
||||
// The lite backend's own command verbs (literal command tokens, not RPC methods — mirrors the
|
||||
// backend's get_commands() registry). Listed for discoverability: the C++ tab intercepts `help`
|
||||
// before it can reach the backend's own HelpCommand, so its "type help" advice would dead-end.
|
||||
add(TR("lite_console_backend_commands"), ConsoleChannel::Info);
|
||||
add(" sync syncstatus balance addresses height info list notes encryptionstatus", ConsoleChannel::None);
|
||||
add(" send shield new seed import timport export", ConsoleChannel::None);
|
||||
add(" encrypt decrypt lock unlock rescan save sietch saplingtree coinsupply", ConsoleChannel::None);
|
||||
add(TR("console_click_commands"), ConsoleChannel::Info);
|
||||
}
|
||||
|
||||
const std::vector<ConsoleCommandCategory>* LiteConsoleExecutor::commandReference() const
|
||||
{
|
||||
return &liteConsoleCommandCategories();
|
||||
}
|
||||
|
||||
std::vector<ConsoleStatusLine> LiteConsoleExecutor::statusLines() const
|
||||
@@ -294,20 +336,20 @@ std::vector<ConsoleStatusLine> LiteConsoleExecutor::statusLines() const
|
||||
wallet::LiteWalletController* lw = app_->liteWallet();
|
||||
if (lw && lw->walletOpen()) {
|
||||
const SyncInfo& sync = app_->state().sync;
|
||||
char buf[96];
|
||||
char buf[128];
|
||||
if (sync.syncing && !sync.isSynced()) {
|
||||
double vp = sync.verification_progress;
|
||||
if (vp < 0.0) vp = 0.0; else if (vp > 1.0) vp = 1.0;
|
||||
std::snprintf(buf, sizeof(buf), "Syncing %.1f%% (block %d / %d)",
|
||||
vp * 100.0, sync.blocks, sync.headers);
|
||||
std::snprintf(buf, sizeof(buf), "%s %.1f%% (block %d / %d)",
|
||||
TR("lite_net_syncing"), vp * 100.0, sync.blocks, sync.headers);
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "Synced (block %d)", sync.blocks);
|
||||
std::snprintf(buf, sizeof(buf), "%s (block %d)", TR("lite_net_synced"), sync.blocks);
|
||||
}
|
||||
out.push_back({std::string(buf), OnSurfaceMedium(), false});
|
||||
}
|
||||
const std::string& err = app_->liteOpenError();
|
||||
if (!err.empty() && (!lw || !lw->walletOpen()))
|
||||
out.push_back({std::string("Last error: ") + err, Error(), false});
|
||||
out.push_back({std::string(TR("console_last_error")) + " " + err, Error(), false});
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -315,10 +357,10 @@ ConsoleStatusLine LiteConsoleExecutor::toolbarStatus() const
|
||||
{
|
||||
ConsoleStatusLine s;
|
||||
wallet::LiteWalletController* lw = app_->liteWallet();
|
||||
if (!lw) { s.text = "No backend"; s.color = Error(); return s; }
|
||||
if (lw->walletOpen()) { s.text = "Connected"; s.color = Success(); }
|
||||
else if (lw->openInProgress()) { s.text = "Connecting"; s.color = Warning(); s.pulse = true; }
|
||||
else { s.text = "Disconnected"; s.color = Error(); }
|
||||
if (!lw) { s.text = TR("console_backend_unavailable"); s.color = Error(); return s; }
|
||||
if (lw->walletOpen()) { s.text = TR("lite_net_connected"); s.color = Success(); }
|
||||
else if (lw->openInProgress()) { s.text = TR("lite_net_connecting"); s.color = Warning(); s.pulse = true; }
|
||||
else { s.text = TR("lite_net_disconnected"); s.color = Error(); }
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace dragonx {
|
||||
class App;
|
||||
namespace ui {
|
||||
|
||||
struct ConsoleCommandCategory; // console_command_reference.h — command-reference table
|
||||
|
||||
using ConsoleAddLineFn = std::function<void(const std::string&, ConsoleChannel)>;
|
||||
|
||||
struct ConsoleStatusLine {
|
||||
@@ -64,7 +66,13 @@ public:
|
||||
virtual void pollLogLines(const ConsoleAddLineFn& add) { (void)add; }
|
||||
|
||||
// UI-chrome capabilities.
|
||||
virtual bool hasRpcReference() const { return false; } // show the RPC command-reference popup
|
||||
// True when this backend speaks JSON-RPC to a daemon (the full node). Gates daemon-specific
|
||||
// console behavior (the 'stop' shutdown confirm, "not connected to daemon" wording) and the
|
||||
// command-reference modal's JSON string-arg quoting — the lite backend takes bare tokens.
|
||||
virtual bool hasRpcReference() const { return false; }
|
||||
// The command-reference table this backend offers (browsed by the console's reference modal),
|
||||
// or nullptr for none. Full node = the JSON-RPC reference; lite = its own backend verbs.
|
||||
virtual const std::vector<ConsoleCommandCategory>* commandReference() const { return nullptr; }
|
||||
// Which log-filter toggles the toolbar should show (default: none).
|
||||
virtual ConsoleLogFilterCaps logFilterCaps() const { return {}; }
|
||||
|
||||
@@ -88,6 +96,7 @@ public:
|
||||
bool pollResult(std::string& result, bool& isError) override;
|
||||
void pollLogLines(const ConsoleAddLineFn& add) override;
|
||||
bool hasRpcReference() const override { return true; }
|
||||
const std::vector<ConsoleCommandCategory>* commandReference() const override;
|
||||
// Full node: daemon/xmrig log, errors-only, RPC trace, and app messages.
|
||||
ConsoleLogFilterCaps logFilterCaps() const override { return {true, true, true, true}; }
|
||||
void printHelp(const ConsoleAddLineFn& add) override;
|
||||
@@ -104,6 +113,9 @@ private:
|
||||
bool last_rpc_connected_ = false;
|
||||
std::deque<std::pair<std::string, bool>> results_; // {text, isError}
|
||||
std::mutex results_mutex_;
|
||||
// Chat diagnostics ring cursor: chat code logs via wallet::liteLog on BOTH variants, so the full-node
|
||||
// console surfaces those lines (as App messages) too — mirrors LiteConsoleExecutor's diag drain.
|
||||
std::uint64_t diag_gen_ = static_cast<std::uint64_t>(-1);
|
||||
};
|
||||
|
||||
// ── Lite: diagnostics ring + backend console command ─────────────────────────
|
||||
@@ -119,6 +131,7 @@ public:
|
||||
// Lite: no daemon log / RPC trace — its diagnostics ring maps to the App + Error
|
||||
// channels, so offer errors-only + app-messages (plus the always-shown text filter).
|
||||
ConsoleLogFilterCaps logFilterCaps() const override { return {false, true, false, true}; }
|
||||
const std::vector<ConsoleCommandCategory>* commandReference() const override;
|
||||
void printHelp(const ConsoleAddLineFn& add) override;
|
||||
std::vector<ConsoleStatusLine> statusLines() const override;
|
||||
ConsoleStatusLine toolbarStatus() const override;
|
||||
|
||||
@@ -280,6 +280,105 @@ const ConsoleCommandEntry kUtilityCommands[] = {
|
||||
"reconsiderblock \"0000000000abc123\"", "undo invalidate accept block again re-enable block restore chain reconsider fix rollback fork", true},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Lite backend command set — the verbs the SDXL lite backend accepts (see its
|
||||
// commands.rs get_commands()). Unlike the full node these are NOT JSON-RPC: the
|
||||
// backend takes plain, space-separated tokens (no quoting), so the reference
|
||||
// modal inserts them bare. clear/help/quit are intentionally omitted — the C++
|
||||
// console tab intercepts those before they reach the backend.
|
||||
// ============================================================================
|
||||
|
||||
const ConsoleCommandEntry kLiteWalletCommands[] = {
|
||||
{"balance", "Show your DRGX balance", "",
|
||||
"Shows the DRGX balance held in this wallet across all its shielded and transparent addresses.",
|
||||
"balance", "money funds amount total holdings"},
|
||||
{"addresses", "List all addresses in the wallet", "",
|
||||
"Lists every address this wallet owns \xE2\x80\x94 shielded (zs1...) and transparent (R.../t...) \xE2\x80\x94 so you can pick one to receive to.",
|
||||
"addresses", "receive address list wallet mine deposit"},
|
||||
{"new", "Create a new address in this wallet", "type",
|
||||
"Creates a fresh receive address. Pass zs for a shielded (private) sapling address, or R (a capital R) for a transparent one \xE2\x80\x94 those exact tokens (case-sensitive).",
|
||||
"new zs", "create address receive generate new shielded transparent zs"},
|
||||
{"list", "List all transactions in the wallet", "",
|
||||
"Shows the wallet's transaction history \xE2\x80\x94 sends, receives and shields \xE2\x80\x94 with amounts, addresses and confirmations.",
|
||||
"list", "history transactions txs payments activity sent received"},
|
||||
{"notes", "List sapling notes and UTXOs", "[all]",
|
||||
"Lists the individual shielded notes and transparent UTXOs that make up your balance. Pass all to include spent ones.",
|
||||
"notes", "utxo notes unspent coins inputs sapling"},
|
||||
{"info", "Get the lightwalletd server's info", "",
|
||||
"Reports the lightwalletd server the wallet is connected to \xE2\x80\x94 its version, chain and block height.",
|
||||
"info", "server node lightwalletd version connection status"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kLiteSyncCommands[] = {
|
||||
{"sync", "Download compact blocks and sync to the server", "",
|
||||
"Fetches new compact blocks from the lightwalletd server and scans them for transactions belonging to this wallet.",
|
||||
"sync", "update refresh scan blocks download catch up"},
|
||||
{"syncstatus", "Get the sync status of the wallet", "",
|
||||
"Reports how far the wallet has synced \xE2\x80\x94 whether a sync is in progress and the block it has reached.",
|
||||
"syncstatus", "progress status syncing scanning percent blocks"},
|
||||
{"height", "Get the latest block height the wallet is at", "",
|
||||
"Shows the block height the wallet has scanned up to. Compare with the network height to gauge sync.",
|
||||
"height", "block height number chain tip synced"},
|
||||
{"rescan", "Rescan the wallet from scratch", "",
|
||||
"Discards the scanned state and re-downloads/re-scans every block from the wallet's birthday. Slow, but fixes a stuck or incomplete balance.",
|
||||
"rescan", "rescan resync repair fix balance rebuild from scratch"},
|
||||
{"save", "Save the wallet file to disk", "",
|
||||
"Writes the current wallet state to disk. The wallet also saves automatically after a sync or send.",
|
||||
"save", "save persist write disk store"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kLiteSendCommands[] = {
|
||||
{"send", "Send DRGX to an address", "[{\"address\":\"zs1...\",\"amount\":0}]",
|
||||
"Sends DRGX from the console using a JSON array of recipients (the Send tab is the easy way; this is the power-user form). amount is in puposhis (the base unit); memo is optional and delivered privately to shielded recipients.",
|
||||
"send [{\"address\":\"zs1exampleaddress\",\"amount\":100000000,\"memo\":\"thanks\"}]",
|
||||
"pay transfer send spend money transaction json", true},
|
||||
{"shield", "Shield transparent DRGX into a sapling address", "[address]",
|
||||
"Moves your transparent (public) DRGX into a shielded sapling address for privacy. With no address it shields to the wallet's own sapling address.",
|
||||
"shield", "shield private sapling transparent move protect", true},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kLiteKeyCommands[] = {
|
||||
{"seed", "Display the wallet seed phrase", "",
|
||||
"Reveals the seed phrase that backs up this wallet. Anyone who sees it can spend your funds \xE2\x80\x94 keep it secret and offline.",
|
||||
"seed", "seed phrase mnemonic backup recovery words secret", true},
|
||||
{"export", "Export the private key for an address", "[address]",
|
||||
"Prints the private/spending key for a wallet address \xE2\x80\x94 anyone with it controls the funds. With no address it exports every key.",
|
||||
"export zs1exampleaddress", "export private key spending backup secret", true},
|
||||
{"import", "Import a spending or viewing key", "key",
|
||||
"Imports a shielded spending or viewing key (pass just the key). The wallet rescans from the sapling activation height to find the key's transactions.",
|
||||
"import somekey", "import restore key spending viewing add watch"},
|
||||
{"timport", "Import a transparent WIF private key", "wif",
|
||||
"Imports a transparent private key in WIF format (begins with U, 5, K or L) so the wallet can spend its funds.",
|
||||
"timport somewifkey", "import transparent wif private key taddr"},
|
||||
{"encrypt", "Encrypt the wallet with a password", "password",
|
||||
"Encrypts the wallet with a password and locks it immediately. You will need the password to send or reveal keys afterwards. If you forget it, only the seed phrase can recover the wallet.",
|
||||
"encrypt strongpassword", "encrypt password protect lock secure passphrase", true},
|
||||
{"decrypt", "Completely remove wallet encryption", "password",
|
||||
"Permanently removes the wallet's password encryption, leaving it unprotected on disk. Requires the current password.",
|
||||
"decrypt strongpassword", "decrypt remove encryption password unprotect", true},
|
||||
{"unlock", "Unlock the wallet for spending", "password",
|
||||
"Temporarily unlocks an encrypted wallet so it can send or reveal keys. Use lock to re-lock it.",
|
||||
"unlock strongpassword", "unlock password spend open temporarily"},
|
||||
{"lock", "Lock a temporarily-unlocked wallet", "",
|
||||
"Re-locks a wallet that was unlocked for spending, without removing its encryption.",
|
||||
"lock", "lock secure re-lock protect close"},
|
||||
{"encryptionstatus", "Check if the wallet is encrypted and locked", "",
|
||||
"Reports whether the wallet is encrypted and, if so, whether it is currently locked or unlocked.",
|
||||
"encryptionstatus", "encryption status locked unlocked encrypted state"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kLiteAdvancedCommands[] = {
|
||||
{"sietch", "Create a Sietch address", "[type]",
|
||||
"Creates a Sietch address, used for enhanced-privacy sends. Pass zs for a sapling Sietch address.",
|
||||
"sietch zs", "sietch privacy address decoy sapling advanced"},
|
||||
{"saplingtree", "Dump the latest Sapling commitment tree (debug)", "",
|
||||
"Prints the latest Sapling commitment tree state \xE2\x80\x94 a debugging aid, not needed for everyday use.",
|
||||
"saplingtree", "sapling tree commitment debug advanced merkle"},
|
||||
{"coinsupply", "Get the coin supply info", "",
|
||||
"Reports coin-supply figures for the chain as seen by the wallet's server.",
|
||||
"coinsupply", "supply coins total emission circulating amount"},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<ConsoleCommandCategory>& consoleCommandCategories()
|
||||
@@ -296,5 +395,17 @@ const std::vector<ConsoleCommandCategory>& consoleCommandCategories()
|
||||
return categories;
|
||||
}
|
||||
|
||||
const std::vector<ConsoleCommandCategory>& liteConsoleCommandCategories()
|
||||
{
|
||||
static const std::vector<ConsoleCommandCategory> categories = {
|
||||
{"Wallet", kLiteWalletCommands, CountOf(kLiteWalletCommands)},
|
||||
{"Sync", kLiteSyncCommands, CountOf(kLiteSyncCommands)},
|
||||
{"Send", kLiteSendCommands, CountOf(kLiteSendCommands)},
|
||||
{"Keys & Security", kLiteKeyCommands, CountOf(kLiteKeyCommands)},
|
||||
{"Advanced", kLiteAdvancedCommands, CountOf(kLiteAdvancedCommands)},
|
||||
};
|
||||
return categories;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -23,7 +23,12 @@ struct ConsoleCommandCategory {
|
||||
int count;
|
||||
};
|
||||
|
||||
// The full-node daemon's JSON-RPC command reference (browsed by the console's command-reference modal).
|
||||
const std::vector<ConsoleCommandCategory>& consoleCommandCategories();
|
||||
|
||||
// The lite backend's own command set (the ~25 verbs the SDXL backend accepts) — the lite-variant
|
||||
// analog of the RPC reference, shown by the same modal when the executor is the lite one.
|
||||
const std::vector<ConsoleCommandCategory>& liteConsoleCommandCategories();
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -311,8 +311,12 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
computeVisibleLines(has_text_filter_, filter_lower_);
|
||||
|
||||
// Main console layout
|
||||
// NoScrollWithMouse: the inner ConsoleOutput owns wheel scrolling (via ApplySmoothScroll). Without
|
||||
// this, a wheel over the output would scroll BOTH the output (smooth-scroll) and this outer container
|
||||
// (ImGui forwards the NoScrollWithMouse child's wheel to its scrollable ancestor) — a double-scroll.
|
||||
// Safe because the output panel is sized to fill the remaining height, so this outer never overflows.
|
||||
ImGui::BeginChild("ConsoleContainer", ImVec2(0, 0), false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
// Optional status header (lite: sync + last error) above the toolbar.
|
||||
renderStatusHeader(exec);
|
||||
@@ -460,12 +464,18 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void ConsoleTab::renderCommandsPopupModal()
|
||||
void ConsoleTab::renderCommandsPopupModal(ConsoleCommandExecutor* exec)
|
||||
{
|
||||
if (!show_commands_popup_) {
|
||||
return;
|
||||
}
|
||||
renderCommandsPopup();
|
||||
// Need a backend that offers a reference table. If the console hasn't built its executor yet
|
||||
// (popup can't have been opened normally) or the backend has none, just dismiss.
|
||||
if (!exec || !exec->commandReference()) {
|
||||
show_commands_popup_ = false;
|
||||
return;
|
||||
}
|
||||
renderCommandsPopup(*exec);
|
||||
}
|
||||
|
||||
void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
@@ -540,14 +550,16 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// Commands reference button (full-node RPC reference only)
|
||||
if (exec.hasRpcReference()) {
|
||||
// Commands reference button — shown whenever the backend offers a reference table (full-node
|
||||
// JSON-RPC commands, or the lite backend's own verbs).
|
||||
if (exec.commandReference()) {
|
||||
if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||
command_search_[0] = '\0'; // fresh search each open (dismiss paths don't all reset it)
|
||||
show_commands_popup_ = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s", TR("console_show_rpc_ref"));
|
||||
material::Tooltip("%s", exec.hasRpcReference() ? TR("console_show_rpc_ref")
|
||||
: TR("console_show_backend_ref"));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
@@ -568,12 +580,14 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
ImGui::SameLine();
|
||||
{
|
||||
float btnSz = ImGui::GetFrameHeight();
|
||||
// Capture the flag BEFORE the button can flip it — the push/pop guards must use the SAME value,
|
||||
// or a click leaves the colour stack unbalanced (ImGui then draws a red error rect on the window).
|
||||
const bool dim = !s_line_accents_enabled;
|
||||
const char* icon = s_line_accents_enabled ? ICON_MD_FORMAT_COLOR_FILL : ICON_MD_FORMAT_COLOR_RESET;
|
||||
if (!s_line_accents_enabled)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
if (dim) ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
if (TactileButton(icon, ImVec2(btnSz, btnSz), Type().iconMed()))
|
||||
s_line_accents_enabled = !s_line_accents_enabled;
|
||||
if (!s_line_accents_enabled) ImGui::PopStyleColor();
|
||||
if (dim) ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_accents"));
|
||||
}
|
||||
|
||||
@@ -581,11 +595,11 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
ImGui::SameLine();
|
||||
{
|
||||
float btnSz = ImGui::GetFrameHeight();
|
||||
if (!s_line_text_color_enabled)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
const bool dim = !s_line_text_color_enabled; // capture before the click flips it (balanced push/pop)
|
||||
if (dim) ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
if (TactileButton(ICON_MD_FORMAT_COLOR_TEXT, ImVec2(btnSz, btnSz), Type().iconMed()))
|
||||
s_line_text_color_enabled = !s_line_text_color_enabled;
|
||||
if (!s_line_text_color_enabled) ImGui::PopStyleColor();
|
||||
if (dim) ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_text_color"));
|
||||
}
|
||||
|
||||
@@ -605,7 +619,7 @@ void ConsoleTab::drawToolbarStatus(ConsoleCommandExecutor& exec)
|
||||
ImVec2 cp = ImGui::GetCursorScreenPos();
|
||||
float dotR = schema::UI().drawElement("tabs.console", "status-dot-radius-base").size + schema::UI().drawElement("tabs.console", "status-dot-radius-scale").size * Layout::hScale();
|
||||
float dotY = cp.y + ImGui::GetTextLineHeight() * 0.5f;
|
||||
float dotX = cp.x + dotR + 2;
|
||||
float dotX = cp.x + dotR + 2.0f * Layout::dpiScale();
|
||||
|
||||
if (st.pulse) {
|
||||
float a = schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-fast").size);
|
||||
@@ -615,7 +629,7 @@ void ConsoleTab::drawToolbarStatus(ConsoleCommandExecutor& exec)
|
||||
dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, st.color);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(dotR * 2 + 6, 0));
|
||||
ImGui::Dummy(ImVec2(dotR * 2 + 6.0f * Layout::dpiScale(), 0));
|
||||
ImGui::SameLine();
|
||||
Type().textColored(TypeStyle::Caption, st.color, st.text.c_str());
|
||||
} else {
|
||||
@@ -1428,18 +1442,22 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
|
||||
exec.printHelp(add);
|
||||
} else if (first == "quit" || first == "exit") {
|
||||
addLine(TR("console_quit_note"), ConsoleChannel::Info);
|
||||
} else if (first == "stop") {
|
||||
} else if (first == "stop" && exec.hasRpcReference()) {
|
||||
// Full-node 'stop' shuts down the daemon (destructive) — gate behind a confirming second
|
||||
// 'stop'. Lite has no node: `stop` isn't a backend verb, so it falls through below and the
|
||||
// backend reports it as unknown — no misleading "shut down the node" warning or dead gate.
|
||||
if (!stop_confirm_pending_) {
|
||||
stop_confirm_pending_ = true;
|
||||
addLine("'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm.",
|
||||
ConsoleChannel::Warning);
|
||||
addLine(TR("console_stop_confirm_node"), ConsoleChannel::Warning);
|
||||
} else {
|
||||
stop_confirm_pending_ = false;
|
||||
if (!exec.isReady()) addLine(TR("console_not_connected"), ConsoleChannel::Error);
|
||||
else exec.submit(cmd);
|
||||
}
|
||||
} else if (!exec.isReady()) {
|
||||
addLine(TR("console_not_connected"), ConsoleChannel::Error);
|
||||
// Full node connects to a daemon; the lite backend opens a wallet — word the error per variant.
|
||||
addLine(exec.hasRpcReference() ? TR("console_not_connected") : TR("console_not_connected_lite"),
|
||||
ConsoleChannel::Error);
|
||||
} else {
|
||||
exec.submit(cmd);
|
||||
}
|
||||
@@ -1551,6 +1569,11 @@ const char* consoleCategoryLabel(const char* name)
|
||||
if (!std::strcmp(name, "Wallet")) return TR("console_cat_wallet");
|
||||
if (!std::strcmp(name, "Raw Transactions")) return TR("console_cat_raw_transactions");
|
||||
if (!std::strcmp(name, "Utility")) return TR("console_cat_utility");
|
||||
// Lite backend reference categories.
|
||||
if (!std::strcmp(name, "Sync")) return TR("console_cat_sync");
|
||||
if (!std::strcmp(name, "Send")) return TR("console_cat_send");
|
||||
if (!std::strcmp(name, "Keys & Security")) return TR("console_cat_keys");
|
||||
if (!std::strcmp(name, "Advanced")) return TR("console_cat_advanced");
|
||||
return name;
|
||||
}
|
||||
} // namespace
|
||||
@@ -1570,7 +1593,7 @@ void ConsoleTab::insertCommandToInput(const ConsoleCommandEntry& cmd)
|
||||
show_commands_popup_ = false;
|
||||
}
|
||||
|
||||
void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel)
|
||||
void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel, bool jsonArgs)
|
||||
{
|
||||
using namespace material;
|
||||
float dp = Layout::dpiScale();
|
||||
@@ -1621,8 +1644,11 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(labelW);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
// Lite backend args are bare freeform tokens, so its param types (string/number) don't
|
||||
// apply — show a neutral "value" hint there instead of a misleading "number".
|
||||
std::string typeHint = jsonArgs ? s.type : std::string(TR("console_ref_value"));
|
||||
std::string hint = s.optional
|
||||
? (s.type + " \xC2\xB7 " + std::string(TR("console_ref_optional"))) : s.type;
|
||||
? (typeHint + " \xC2\xB7 " + std::string(TR("console_ref_optional"))) : typeHint;
|
||||
ImGui::InputTextWithHint("##pv", hint.c_str(), cmd_param_bufs_[k], sizeof(cmd_param_bufs_[k]));
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -1652,7 +1678,9 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
|
||||
built += " " + specs[k].raw;
|
||||
complete = false;
|
||||
} else {
|
||||
if (specs[k].type == "string" && val.front() != '"' && val.front() != '\'' &&
|
||||
// JSON-RPC (full node) auto-quotes string args; the lite backend takes bare tokens, so
|
||||
// leave the value exactly as typed there (quoting would break its address/key parsing).
|
||||
if (jsonArgs && specs[k].type == "string" && val.front() != '"' && val.front() != '\'' &&
|
||||
val.front() != '[' && val.front() != '{')
|
||||
val = "\"" + val + "\"";
|
||||
built += " " + val;
|
||||
@@ -1725,13 +1753,16 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
|
||||
}
|
||||
}
|
||||
|
||||
void ConsoleTab::renderCommandsPopup()
|
||||
void ConsoleTab::renderCommandsPopup(ConsoleCommandExecutor& exec)
|
||||
{
|
||||
using namespace material;
|
||||
float dp = Layout::dpiScale();
|
||||
|
||||
// Full node speaks JSON-RPC (quote string args); the lite backend takes bare tokens.
|
||||
const bool jsonArgs = exec.hasRpcReference();
|
||||
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("console_rpc_reference");
|
||||
ov.title = jsonArgs ? TR("console_rpc_reference") : TR("console_backend_reference");
|
||||
ov.p_open = &show_commands_popup_;
|
||||
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
|
||||
ov.cardWidth = 960.0f; // wide enough for two panes
|
||||
@@ -1760,7 +1791,7 @@ void ConsoleTab::renderCommandsPopup()
|
||||
std::transform(q.begin(), q.end(), q.begin(), ::tolower);
|
||||
const bool searching = !q.empty();
|
||||
|
||||
const auto& categories = consoleCommandCategories();
|
||||
const auto& categories = *exec.commandReference(); // non-null: guarded by renderCommandsPopupModal
|
||||
|
||||
// Flat display order of (cat,idx): ranked when searching, category order when browsing. Drives
|
||||
// keyboard nav + auto-selection; the browse view still renders grouped headers below.
|
||||
@@ -1902,7 +1933,7 @@ void ConsoleTab::renderCommandsPopup()
|
||||
if (cmd_sel_cat_ >= 0 && cmd_sel_cat_ < (int)categories.size() &&
|
||||
cmd_sel_idx_ >= 0 && cmd_sel_idx_ < categories[cmd_sel_cat_].count) {
|
||||
renderCommandDetail(categories[cmd_sel_cat_].commands[cmd_sel_idx_],
|
||||
consoleCategoryLabel(categories[cmd_sel_cat_].name));
|
||||
consoleCategoryLabel(categories[cmd_sel_cat_].name), jsonArgs);
|
||||
} else {
|
||||
ImVec2 av = ImGui::GetContentRegionAvail();
|
||||
ImGui::SetCursorPosY(av.y * 0.4f);
|
||||
|
||||
@@ -44,10 +44,11 @@ public:
|
||||
void render(ConsoleCommandExecutor& exec);
|
||||
|
||||
/**
|
||||
* @brief Render the RPC Command Reference popup at top-level scope.
|
||||
* Must be called outside any child window so the modal blocks all input.
|
||||
* @brief Render the command-reference popup at top-level scope.
|
||||
* Must be called outside any child window so the modal blocks all input. `exec` supplies the
|
||||
* reference table (full-node RPC vs lite backend verbs); may be null (no console yet) -> no-op.
|
||||
*/
|
||||
void renderCommandsPopupModal();
|
||||
void renderCommandsPopupModal(ConsoleCommandExecutor* exec);
|
||||
|
||||
// Debug/UI-sweep hook: force the RPC command-reference popup open/closed so the full UI sweep
|
||||
// can capture it (the popup is otherwise opened only by a toolbar button).
|
||||
@@ -115,8 +116,9 @@ private:
|
||||
// Format a completed command result (JSON role -> channel) into console lines.
|
||||
void addFormattedResult(const std::string& result, bool is_error);
|
||||
void renderStatusHeader(ConsoleCommandExecutor& exec);
|
||||
void renderCommandsPopup();
|
||||
void renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel); // right pane
|
||||
void renderCommandsPopup(ConsoleCommandExecutor& exec);
|
||||
// right pane; jsonArgs=true (full-node RPC) auto-quotes string args, false (lite) inserts bare
|
||||
void renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel, bool jsonArgs);
|
||||
void insertCommandToInput(const ConsoleCommandEntry& cmd); // fill input + close the modal
|
||||
|
||||
// renderToolbar() draws the top bar; these are its sub-steps:
|
||||
@@ -186,10 +188,11 @@ private:
|
||||
// consumed by the renderer + hit-testing.
|
||||
mutable ConsoleLayout layout_;
|
||||
|
||||
// Commands popup (RPC command explorer)
|
||||
// Commands popup (command explorer) — indexes into the active executor's commandReference()
|
||||
// table (full-node RPC commands or the lite backend verbs), not always the full-node one.
|
||||
bool show_commands_popup_ = false;
|
||||
char command_search_[128] = {0}; // RPC-reference search filter (cleared when the modal opens)
|
||||
int cmd_sel_cat_ = -1; // detail-pane selection: category index into consoleCommandCategories()
|
||||
char command_search_[128] = {0}; // reference search filter (cleared when the modal opens)
|
||||
int cmd_sel_cat_ = -1; // detail-pane selection: category index into that table
|
||||
int cmd_sel_idx_ = -1; // detail-pane selection: command index within that category
|
||||
std::string pending_submit_; // command to run next frame (deferred so the modal needs no executor)
|
||||
const ConsoleCommandEntry* run_confirm_cmd_ = nullptr; // destructive "Insert & run" awaiting confirmation
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace ui {
|
||||
static int s_selected_index = -1;
|
||||
static bool s_show_add_dialog = false;
|
||||
static bool s_show_edit_dialog = false;
|
||||
static bool s_show_contacts_settings = false; // Contacts customization modal (gear button)
|
||||
static bool s_focus_edit_field = false; // focus the first field the frame the add/edit dialog opens
|
||||
static int s_confirm_delete_idx = -1; // armed storage index; a 2nd Delete confirms
|
||||
static char s_edit_label[128] = "";
|
||||
@@ -81,6 +82,8 @@ struct AvatarTex {
|
||||
static std::unordered_map<std::string, AvatarTex> s_avatarTexCache;
|
||||
static int s_avatarLoadsThisFrame = 0;
|
||||
static bool s_animateAvatars = true; // mirrors the setting; refreshed each frame in RenderContactsTab
|
||||
static int s_contactsShape = 0; // avatar shape (mirrors getContactsAvatarShape: 0/1/2)
|
||||
static float s_contactsScale = 1.0f; // card/list row scale (mirrors getContactsListScale)
|
||||
static bool s_avatarAnimatedThisFrame = false; // set when a live animated frame is drawn -> keep redrawing
|
||||
static constexpr int kAvatarLoadsPerFrame = 2;
|
||||
static constexpr int kAvatarMaxFrames = 300; // cap frames per animated avatar (bounds VRAM/decode)
|
||||
@@ -134,22 +137,48 @@ static void invalidateAvatarTexture(const std::string& path) {
|
||||
s_avatarTexCache.erase(it);
|
||||
}
|
||||
|
||||
// Draw a contact's avatar into the circle at `c` (radius `r`): a custom image (circular-cropped), a
|
||||
// Material icon in a tinted circle, or — the default — the Z/T type badge.
|
||||
// Draw a contact's avatar: a custom image, a Material icon in a tinted shape, or the Z/T type badge.
|
||||
// `shape` picks the outline: 0 = circle (centered at `c`, radius `r`); 1 = rounded square (same box as
|
||||
// the circle, all corners rounded); 2 = a full-row-height tab spanning [tabMin, tabMax] with rounded
|
||||
// LEFT corners (radius tabRound) + a straight right edge. The inner glyph is sized to the shape, so it
|
||||
// stays proportional at any list scale.
|
||||
static void drawContactAvatar(ImDrawList* dl, ImVec2 c, float r, const data::AddressBookEntry& e,
|
||||
bool shielded, ImU32 typeCol, bool light, float dp,
|
||||
ImFont* letterFont, ImFont* iconFont, bool onScreen = true) {
|
||||
ImFont* letterFont, ImFont* iconFont, bool onScreen = true, int shape = 0,
|
||||
ImVec2 tabMin = ImVec2(0, 0), ImVec2 tabMax = ImVec2(0, 0), float tabRound = 0.0f) {
|
||||
const bool circle = (shape == 0);
|
||||
// Resolve the shape rectangle, its rounding, the content center + radius, and the glyph size.
|
||||
ImVec2 cc = c; float cr = r;
|
||||
ImVec2 sMin(c.x - r, c.y - r), sMax(c.x + r, c.y + r);
|
||||
float sRound = r * 0.42f;
|
||||
ImDrawFlags sFlags = ImDrawFlags_RoundCornersAll;
|
||||
if (shape == 2) {
|
||||
sMin = tabMin; sMax = tabMax; sRound = tabRound; sFlags = ImDrawFlags_RoundCornersLeft;
|
||||
cc = ImVec2((tabMin.x + tabMax.x) * 0.5f, (tabMin.y + tabMax.y) * 0.5f);
|
||||
cr = (tabMax.y - tabMin.y) * 0.5f;
|
||||
}
|
||||
const float glyphSz = cr * 0.92f; // ~46% of the shape diameter — matches the classic badge ratio
|
||||
auto fillShape = [&](ImU32 col) {
|
||||
if (circle) dl->AddCircleFilled(cc, cr, col);
|
||||
else dl->AddRectFilled(sMin, sMax, col, sRound, sFlags);
|
||||
};
|
||||
auto borderShape = [&](ImU32 col, float th) {
|
||||
if (circle) dl->AddCircle(cc, cr, col, 0, th);
|
||||
else dl->AddRect(sMin, sMax, col, sRound, sFlags, th);
|
||||
};
|
||||
const std::string& av = e.avatar;
|
||||
if (av.rfind("img:", 0) == 0) {
|
||||
const AvatarTex* t = getAvatarTexture(av.substr(4));
|
||||
ImTextureID tex = currentAvatarFrame(t, onScreen);
|
||||
if (tex) {
|
||||
float u0 = 0, v0 = 0, u1 = 1, v1 = 1; // centre-crop to a square so the circle isn't stretched
|
||||
if (t->w > t->h) { float m = (t->w - t->h) * 0.5f / t->w; u0 = m; u1 = 1 - m; }
|
||||
else if (t->h > t->w) { float m = (t->h - t->w) * 0.5f / t->h; v0 = m; v1 = 1 - m; }
|
||||
dl->AddImageRounded(tex, ImVec2(c.x - r, c.y - r), ImVec2(c.x + r, c.y + r),
|
||||
ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32_WHITE, r);
|
||||
dl->AddCircle(c, r, material::WithAlpha(material::OnSurface(), 45), 0, 1.0f * dp);
|
||||
const float aspect = (sMax.x - sMin.x) / std::max(1.0f, sMax.y - sMin.y); // crop to the shape's aspect
|
||||
float u0 = 0, v0 = 0, u1 = 1, v1 = 1;
|
||||
const float tw = static_cast<float>(t->w), th2 = static_cast<float>(t->h);
|
||||
if (tw / th2 > aspect) { float keep = aspect * th2 / tw; float m = (1 - keep) * 0.5f; u0 = m; u1 = 1 - m; }
|
||||
else { float keep = (tw / aspect) / th2; float m = (1 - keep) * 0.5f; v0 = m; v1 = 1 - m; }
|
||||
dl->AddImageRounded(tex, sMin, sMax, ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32_WHITE,
|
||||
circle ? cr : sRound, circle ? ImDrawFlags_RoundCornersAll : sFlags);
|
||||
borderShape(material::WithAlpha(material::OnSurface(), 45), 1.0f * dp);
|
||||
return;
|
||||
}
|
||||
// fall through to the type badge if the image failed to load
|
||||
@@ -158,19 +187,19 @@ static void drawContactAvatar(ImDrawList* dl, ImVec2 c, float r, const data::Add
|
||||
const bool known = (iconName == material::project_icons::kPickaxeName) ||
|
||||
(material::project_icons::glyphForName(iconName) != nullptr);
|
||||
if (known) {
|
||||
dl->AddCircleFilled(c, r, material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
dl->AddCircle(c, r, material::WithAlpha(typeCol, 190), 0, 1.4f * dp);
|
||||
material::project_icons::drawByName(dl, iconName, c, typeCol, iconFont, iconFont->LegacySize);
|
||||
fillShape(material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
borderShape(material::WithAlpha(typeCol, 190), 1.4f * dp);
|
||||
material::project_icons::drawByName(dl, iconName, cc, typeCol, iconFont, glyphSz);
|
||||
return;
|
||||
}
|
||||
// fall through to the type badge if the icon name is unknown
|
||||
}
|
||||
// Default: Z/T type badge.
|
||||
dl->AddCircleFilled(c, r, material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
dl->AddCircle(c, r, material::WithAlpha(typeCol, 190), 0, 1.4f * dp);
|
||||
fillShape(material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
borderShape(material::WithAlpha(typeCol, 190), 1.4f * dp);
|
||||
const char* letter = shielded ? "Z" : "T";
|
||||
const ImVec2 ls = letterFont->CalcTextSizeA(letterFont->LegacySize, FLT_MAX, 0, letter);
|
||||
dl->AddText(letterFont, letterFont->LegacySize, ImVec2(c.x - ls.x * 0.5f, c.y - ls.y * 0.5f), typeCol, letter);
|
||||
const ImVec2 ls = letterFont->CalcTextSizeA(glyphSz, FLT_MAX, 0, letter);
|
||||
dl->AddText(letterFont, glyphSz, ImVec2(cc.x - ls.x * 0.5f, cc.y - ls.y * 0.5f), typeCol, letter);
|
||||
}
|
||||
|
||||
static bool isShieldedAddr(const std::string& a) {
|
||||
@@ -268,6 +297,8 @@ void RenderContactsTab(App* app)
|
||||
{
|
||||
s_avatarLoadsThisFrame = 0; // reset the per-frame avatar-decode budget (see getAvatarTexture)
|
||||
s_animateAvatars = !app->settings() || app->settings()->getAnimateAvatars();
|
||||
s_contactsShape = app->settings() ? app->settings()->getContactsAvatarShape() : 0;
|
||||
s_contactsScale = app->settings() ? app->settings()->getContactsListScale() : 1.0f;
|
||||
auto& S = schema::UI();
|
||||
// Reuse the existing address-book schema/column config for the table + add/edit form.
|
||||
auto addrTable = S.table("dialogs.address-book", "address-table");
|
||||
@@ -416,9 +447,12 @@ void RenderContactsTab(App* app)
|
||||
const bool sh = isShieldedAddr(s_edit_address);
|
||||
const ImU32 tu = contactTypeColor(sh, light);
|
||||
ImVec2 avC(pMin.x + pad + avR, pMin.y + ph * 0.5f);
|
||||
drawContactAvatar(pdl, avC, avR, pv, sh, tu, light, dp, letF, icoF);
|
||||
const float pTabW = ph; // full preview-height square tab (shape 2)
|
||||
drawContactAvatar(pdl, avC, avR, pv, sh, tu, light, dp, letF, icoF, true,
|
||||
s_contactsShape, pMin, ImVec2(pMin.x + pTabW, pMax.y), 10.0f * dp);
|
||||
|
||||
float tx = avC.x + avR + Layout::spacingMd();
|
||||
float tx = (s_contactsShape == 2) ? (pMin.x + pTabW + Layout::spacingMd())
|
||||
: (avC.x + avR + Layout::spacingMd());
|
||||
float textRight = pMax.x - pad;
|
||||
float ty = pMin.y + (ph - blockH) * 0.5f;
|
||||
const bool hasName = s_edit_label[0] != '\0';
|
||||
@@ -819,7 +853,8 @@ void RenderContactsTab(App* app)
|
||||
// Inline tab content lives in a scroll child (mirrors peers_tab / explorer_tab).
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
ImGui::BeginChild("##ContactsScroll", avail, false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoScrollWithMouse); // inner ##contactList owns the wheel (smooth scroll)
|
||||
|
||||
// Toolbar — icon + label actions; the primary "Add New" is accented so it stands out.
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
@@ -885,8 +920,10 @@ void RenderContactsTab(App* app)
|
||||
const char* segIcons[3] = { ICON_MD_VIEW_AGENDA, ICON_MD_VIEW_LIST, ICON_MD_TABLE_ROWS };
|
||||
const float segH = ImGui::GetFrameHeight();
|
||||
const float segW = 44.0f * dp * 3.0f;
|
||||
const float segGap = 6.0f * dp;
|
||||
const float grpW = segW + segGap + segH; // view toggle + gap + a square settings gear
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - segW));
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - grpW));
|
||||
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, segW, segH,
|
||||
segIcons, 3, viewMode, material::Type().iconSmall(),
|
||||
@@ -896,11 +933,21 @@ void RenderContactsTab(App* app)
|
||||
app->settings()->save();
|
||||
viewMode = segClk;
|
||||
}
|
||||
ImGui::Dummy(ImVec2(segW, segH));
|
||||
// Contacts-customization gear, immediately right of the view toggle.
|
||||
ImGui::SetCursorScreenPos(ImVec2(segOrigin.x + segW + segGap, segOrigin.y));
|
||||
material::IconButtonStyle gst;
|
||||
gst.tooltip = TR("contacts_settings_tip");
|
||||
gst.hoverBg = material::WithAlpha(material::OnSurface(), 30);
|
||||
gst.restBg = material::WithAlpha(material::OnSurface(), 20);
|
||||
gst.bgRounding = 7.0f * dp;
|
||||
if (material::IconButton("##contactsSettings", ICON_MD_SETTINGS, material::Type().iconSmall(),
|
||||
ImVec2(segH, segH), gst))
|
||||
s_show_contacts_settings = true;
|
||||
ImGui::SetCursorScreenPos(segOrigin);
|
||||
ImGui::Dummy(ImVec2(grpW, segH));
|
||||
}
|
||||
|
||||
// Search / filter
|
||||
ImGui::Spacing();
|
||||
// Search / filter (tight against the toolbar row above — no extra spacer)
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
ImGui::InputTextWithHint("##ContactSearch", TR("contacts_search_placeholder"),
|
||||
s_search, sizeof(s_search));
|
||||
@@ -958,9 +1005,23 @@ void RenderContactsTab(App* app)
|
||||
|
||||
if (viewMode == 2) {
|
||||
// ── TABLE mode (material-ized): no outer/grid borders, row backgrounds, interactive sort. ──
|
||||
// Frosted-glass panel behind the table so it blurs the backdrop like the card/list view (the
|
||||
// table's translucent row backgrounds let the blur show through). A BeginTable can't take
|
||||
// AlwaysUseWindowPadding, so inset the table itself inside the glass so its headers/cells
|
||||
// don't hug the container edges.
|
||||
ImDrawList* tpdl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 tpMin = ImGui::GetCursorScreenPos();
|
||||
const float tpW = ImGui::GetContentRegionAvail().x;
|
||||
const float tIpad = 12.0f * dp, tVpad = 10.0f * dp;
|
||||
{
|
||||
material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34;
|
||||
material::DrawGlassPanel(tpdl, tpMin, ImVec2(tpMin.x + tpW, tpMin.y + listH), g);
|
||||
}
|
||||
ImGui::SetCursorScreenPos(ImVec2(tpMin.x + tIpad, tpMin.y + tVpad));
|
||||
if (ImGui::BeginTable("AddressBookTable", 3,
|
||||
ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, listH)))
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY,
|
||||
ImVec2(tpW - 2.0f * tIpad, listH - 2.0f * tVpad)))
|
||||
{
|
||||
ImGui::TableSetupColumn(TR("label"), ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_DefaultSort, 1.5f);
|
||||
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch, 2.6f);
|
||||
@@ -1009,9 +1070,12 @@ void RenderContactsTab(App* app)
|
||||
s_selected_index = static_cast<int>(i); s_confirm_delete_idx = -1; openContextMenu = true;
|
||||
}
|
||||
bool tAvOnScreen = ImGui::IsRectVisible(tAvP, ImVec2(tAvP.x + tAvR * 2.0f, tAvP.y + tLineH));
|
||||
// The table is a compact inline view: a full-height "tab" (shape 2) can't render inside a
|
||||
// table cell, so it falls back to the rounded square there (table scale is fixed too).
|
||||
const int tShape = (s_contactsShape == 2) ? 1 : s_contactsShape;
|
||||
drawContactAvatar(ImGui::GetWindowDrawList(), ImVec2(tAvP.x + tAvR, tAvP.y + tLineH * 0.5f),
|
||||
tAvR, entry, shielded, typeColor(shielded), lightTheme, dp,
|
||||
material::Type().caption(), material::Type().iconSmall(), tAvOnScreen);
|
||||
material::Type().caption(), material::Type().iconSmall(), tAvOnScreen, tShape);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::PushFont(material::Type().subtitle2());
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(typeColor(shielded)), "%s", shielded ? "Z" : "T");
|
||||
@@ -1033,6 +1097,10 @@ void RenderContactsTab(App* app)
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
// Land the cursor at the glass-panel bottom (tpMin.y + listH) so the count footer lines up
|
||||
// with the Cards/List views. The table is inset by tVpad and its outer_size is listH-2*tVpad,
|
||||
// so it would otherwise end tVpad higher and pull the footer up.
|
||||
ImGui::SetCursorScreenPos(ImVec2(tpMin.x, tpMin.y + listH));
|
||||
} else {
|
||||
// ── CARDS (0) / LIST (1) mode — tactile Material items, no grid lines. ──
|
||||
const bool asCard = (viewMode == 0);
|
||||
@@ -1043,8 +1111,22 @@ void RenderContactsTab(App* app)
|
||||
return toLower(entries[a].label).compare(toLower(entries[b].label)) < 0;
|
||||
});
|
||||
}
|
||||
// Frosted-glass list container so the contacts area blurs the backdrop (peers_tab pattern:
|
||||
// glass behind a NoBackground list child) instead of showing the raw texture through the pane.
|
||||
{
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||
const float pW = ImGui::GetContentRegionAvail().x;
|
||||
material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34;
|
||||
material::DrawGlassPanel(pdl, pMin, ImVec2(pMin.x + pW, pMin.y + listH), g);
|
||||
}
|
||||
// AlwaysUseWindowPadding: a borderless child ignores WindowPadding without it, so the rows
|
||||
// would hug the frosted container's edges.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f * dp, 10.0f * dp));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0,0,0,0));
|
||||
ImGui::BeginChild("##contactList", ImVec2(0, listH), false, ImGuiWindowFlags_NoBackground);
|
||||
ImGui::BeginChild("##contactList", ImVec2(0, listH), ImGuiChildFlags_AlwaysUseWindowPadding,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
material::ApplySmoothScroll(); // wheel-lerp scroll, matching the rest of the app
|
||||
if (visibleRows.empty()) {
|
||||
emptyState(ICON_MD_CONTACTS, book.empty() ? "address_book_empty" : "contacts_search_no_match");
|
||||
} else {
|
||||
@@ -1056,10 +1138,13 @@ void RenderContactsTab(App* app)
|
||||
ImFont* lblF = material::Type().subtitle2();
|
||||
ImFont* adrF = material::Type().caption();
|
||||
ImFont* icoF = material::Type().iconSmall();
|
||||
const float sc = s_contactsScale; // card/list scale (avatar + text + row height; not the table)
|
||||
const float pad = 12.0f * dp;
|
||||
const float avR = 15.0f * dp;
|
||||
const float avR = 15.0f * dp * sc;
|
||||
const float rowH = avR * 2.0f + (asCard ? 16.0f * dp : 12.0f * dp);
|
||||
const float round = 10.0f * dp;
|
||||
const float lblSz = lblF->LegacySize * sc; // scaled label + address text
|
||||
const float adrSz = adrF->LegacySize * sc;
|
||||
bool rowDeleteRequested = false; // per-row delete is deferred until after the loop
|
||||
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
|
||||
size_t i = visibleRows[vi];
|
||||
@@ -1103,18 +1188,23 @@ void RenderContactsTab(App* app)
|
||||
// Only animate when the row is actually on-screen, so a scrolled-off animated avatar
|
||||
// doesn't hold the app awake (this loop has no clipper).
|
||||
const bool rowOnScreen = ImGui::IsRectVisible(mn, mx);
|
||||
drawContactAvatar(dl, avC, avR, entry, shielded, tu, lightTheme, dp, lblF, icoF, rowOnScreen);
|
||||
// Shape 2 = a full-row-height left tab (a square the height of the row, rounded-left to
|
||||
// match the row corner); shapes 0/1 are inset badges. Text shifts to clear the tab.
|
||||
const float tabW = rowH;
|
||||
drawContactAvatar(dl, avC, avR, entry, shielded, tu, lightTheme, dp, lblF, icoF, rowOnScreen,
|
||||
s_contactsShape, mn, ImVec2(mn.x + tabW, mx.y), asCard ? round : 0.0f);
|
||||
// Label (line 1) + muted address (line 2) — reserve trailing room for the globe + actions.
|
||||
const float actHit = icoF->LegacySize + 8.0f * dp; // per-action square hit area
|
||||
const float globeW = entry.isGlobal() ? (icoF->LegacySize + 8.0f * dp) : 0.0f;
|
||||
const float trailW = 3.0f * actHit + 6.0f * dp + globeW;
|
||||
const float cy = mn.y + rowH * 0.5f;
|
||||
const float tx = mn.x + pad + avR * 2.0f + pad;
|
||||
const float tx = (s_contactsShape == 2) ? (mn.x + tabW + pad)
|
||||
: (mn.x + pad + avR * 2.0f + pad);
|
||||
const float textMaxX = mx.x - pad - trailW;
|
||||
const float blockH = lblF->LegacySize + adrF->LegacySize + 3.0f * dp;
|
||||
const float blockH = lblSz + adrSz + 3.0f * dp;
|
||||
const float ty = cy - blockH * 0.5f;
|
||||
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
|
||||
dl->AddText(lblF, lblF->LegacySize, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
|
||||
dl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
|
||||
dl->PopClipRect();
|
||||
// Un-collapse to the full address on hover (clipped to the text column so it never
|
||||
// runs under the trailing actions); middle-truncated otherwise.
|
||||
@@ -1122,7 +1212,7 @@ void RenderContactsTab(App* app)
|
||||
? entry.address
|
||||
: util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
|
||||
dl->AddText(adrF, adrF->LegacySize, ImVec2(tx, ty + lblF->LegacySize + 3.0f * dp),
|
||||
dl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp),
|
||||
material::OnSurfaceMedium(), addr.c_str());
|
||||
dl->PopClipRect();
|
||||
// Trailing: the globe badge stays pinned far-right (global contacts); per-row copy/edit/
|
||||
@@ -1183,6 +1273,7 @@ void RenderContactsTab(App* app)
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar(); // ##contactList inner WindowPadding
|
||||
}
|
||||
|
||||
// Shared right-click context menu (opened by either view's row right-click; acts on the selection).
|
||||
@@ -1226,6 +1317,119 @@ void RenderContactsTab(App* app)
|
||||
ImGui::EndChild();
|
||||
|
||||
renderEntryDialog();
|
||||
|
||||
// ---- Contacts customization modal (opened by the toolbar gear) — house BlurFloat overlay ----
|
||||
if (s_show_contacts_settings && app->settings()) {
|
||||
auto* st = app->settings();
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("contacts_settings_title");
|
||||
ov.p_open = &s_show_contacts_settings;
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 460.0f; ov.idSuffix = "contactsettings";
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
const float ctrlW = 210.0f * dp;
|
||||
auto rowStart = [&](const char* label) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - ctrlW));
|
||||
ImGui::SetNextItemWidth(ctrlW);
|
||||
};
|
||||
// Draw one sample contact card (avatar + label + muted address) at a given shape/scale, using
|
||||
// the SAME drawContactAvatar + row geometry the real list uses — so the preview matches reality.
|
||||
auto drawSampleRow = [&](ImDrawList* pdl, ImVec2 mn, float w,
|
||||
const data::AddressBookEntry& e, int shape, float sc) {
|
||||
ImFont* lblF = material::Type().subtitle2();
|
||||
ImFont* adrF = material::Type().caption();
|
||||
ImFont* icoF = material::Type().iconSmall();
|
||||
const float pad = 12.0f * dp;
|
||||
const float avR = 15.0f * dp * sc;
|
||||
const float rowH = avR * 2.0f + 16.0f * dp;
|
||||
const float round = 10.0f * dp;
|
||||
const float lblSz = lblF->LegacySize * sc;
|
||||
const float adrSz = adrF->LegacySize * sc;
|
||||
const ImVec2 mx(mn.x + w, mn.y + rowH);
|
||||
pdl->AddRectFilled(mn, mx, material::WithAlpha(material::OnSurface(), 18), round);
|
||||
const bool shielded = isShieldedAddr(e.address);
|
||||
const ImU32 tu = contactTypeColor(shielded, lightTheme);
|
||||
const ImVec2 avC(mn.x + pad + avR, mn.y + rowH * 0.5f);
|
||||
const float tabW = rowH; // shape 2 = full-row-height square tab on the left
|
||||
drawContactAvatar(pdl, avC, avR, e, shielded, tu, lightTheme, dp, lblF, icoF, true,
|
||||
shape, mn, ImVec2(mn.x + tabW, mx.y), round);
|
||||
const float tx = (shape == 2) ? (mn.x + tabW + pad) : (mn.x + pad + avR * 2.0f + pad);
|
||||
const float blockH = lblSz + adrSz + 3.0f * dp;
|
||||
const float ty = mn.y + rowH * 0.5f - blockH * 0.5f;
|
||||
const std::string addr = util::truncateMiddle(e.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
pdl->PushClipRect(ImVec2(tx, mn.y), ImVec2(mx.x - pad, mx.y), true);
|
||||
pdl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), e.label.c_str());
|
||||
pdl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp), material::OnSurfaceMedium(), addr.c_str());
|
||||
pdl->PopClipRect();
|
||||
};
|
||||
// ---- Live preview: two sample cards at the current shape + scale (updates as controls move). ----
|
||||
{
|
||||
const float sc = st->getContactsListScale();
|
||||
const int shape = st->getContactsAvatarShape();
|
||||
data::AddressBookEntry sA; sA.label = "Ava Reyes"; sA.address = "zs1preview0shielded0contact0demo0address0q9k";
|
||||
data::AddressBookEntry sB; sB.label = "Node Ops"; sB.address = "t1PreviewTransparentContactDemoAddr8xQ";
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
const float padIn = 10.0f * dp, gap = 8.0f * dp;
|
||||
const float w = ImGui::GetContentRegionAvail().x;
|
||||
const float avR = 15.0f * dp * sc;
|
||||
const float rowH = avR * 2.0f + 16.0f * dp;
|
||||
const float panelH = 2.0f * rowH + gap + 2.0f * padIn;
|
||||
const ImVec2 pOrigin = ImGui::GetCursorScreenPos();
|
||||
material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34;
|
||||
material::DrawGlassPanel(pdl, pOrigin, ImVec2(pOrigin.x + w, pOrigin.y + panelH), g);
|
||||
const ImVec2 rmn(pOrigin.x + padIn, pOrigin.y + padIn);
|
||||
const float rowW = w - 2.0f * padIn;
|
||||
drawSampleRow(pdl, rmn, rowW, sA, shape, sc);
|
||||
drawSampleRow(pdl, ImVec2(rmn.x, rmn.y + rowH + gap), rowW, sB, shape, sc);
|
||||
ImGui::Dummy(ImVec2(w, panelH));
|
||||
ImGui::Dummy(ImVec2(0.0f, 10.0f * dp));
|
||||
}
|
||||
// Avatar shape — circle, rounded square, or a full-row-height left tab (segmented control,
|
||||
// matching the chat settings modal's segmented style; the live preview above reflects it).
|
||||
{
|
||||
rowStart(TR("contacts_avatar_shape"));
|
||||
const char* items[] = { TR("contacts_shape_circle"), TR("contacts_shape_square"),
|
||||
TR("contacts_shape_tab") };
|
||||
const float segH = ImGui::GetFrameHeight();
|
||||
const ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||
const int cur = st->getContactsAvatarShape();
|
||||
const int clk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, ctrlW, segH,
|
||||
items, 3, cur, material::Type().caption(),
|
||||
"##contactsShape", dp);
|
||||
ImGui::SetCursorScreenPos(segOrigin);
|
||||
ImGui::Dummy(ImVec2(ctrlW, segH)); // restore + reserve the control's rect for layout flow
|
||||
if (clk >= 0 && clk != cur) { st->setContactsAvatarShape(clk); st->save(); }
|
||||
}
|
||||
// Card/list row scale (does not affect the table view).
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0.0f, 4.0f * dp));
|
||||
rowStart(TR("contacts_list_scale"));
|
||||
float v = st->getContactsListScale();
|
||||
if (ImGui::SliderFloat("##contactsScale", &v, 0.8f, 1.5f, "%.2fx", ImGuiSliderFlags_AlwaysClamp)) {
|
||||
st->setContactsListScale(v); st->save();
|
||||
}
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
|
||||
{
|
||||
// Done: sized to its text (+ a little shoulder room) and centered, not full width.
|
||||
const char* doneLbl = TR("chat_settings_done");
|
||||
ImGui::PushFont(material::Type().button());
|
||||
const float bw = ImGui::CalcTextSize(doneLbl).x + ImGui::GetStyle().FramePadding.x * 2.0f + 28.0f * dp;
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
|
||||
if (material::TactileButton(doneLbl, ImVec2(bw, 0)))
|
||||
s_show_contacts_settings = false;
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ContactsSweepOpenEditDialog(int avatarMode)
|
||||
|
||||
@@ -1127,10 +1127,8 @@ static void renderBlockDetailModal(App* app) {
|
||||
// Row 1: Timestamp | Confirmations
|
||||
drawLabelValue(dl, gx, gy, labelW, TR("block_timestamp"), "", capFont, sub1);
|
||||
if (s_detail_time > 0) {
|
||||
std::time_t t = static_cast<std::time_t>(s_detail_time);
|
||||
char time_buf[64];
|
||||
std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(gx + labelW, gy), OnSurface(), time_buf);
|
||||
const std::string tstr = dragonx::util::formatClockDateTime(s_detail_time, /*withSeconds=*/true);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(gx + labelW, gy), OnSurface(), tstr.c_str());
|
||||
}
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(gx + halfW, gy), OnSurfaceMedium(), TR("confirmations"));
|
||||
snprintf(buf, sizeof(buf), "%d", s_detail_confirmations);
|
||||
|
||||
@@ -211,12 +211,11 @@ void ExportAllKeysDialog::render(App* app)
|
||||
std::string filepath = configDir + "/" + filename;
|
||||
bool writeOk = false;
|
||||
if (exported > 0) {
|
||||
std::ofstream file(filepath);
|
||||
if (file.is_open()) {
|
||||
file << keys;
|
||||
file.close();
|
||||
writeOk = true;
|
||||
}
|
||||
// Write the plaintext private keys 0600 + atomically (never
|
||||
// world/group-readable, never a half-written file) — the same restricted
|
||||
// atomic-write idiom the PIN vault uses. A default ofstream would create
|
||||
// the key dump with umask-derived (often 0644) permissions.
|
||||
writeOk = util::Platform::writeFileAtomically(filepath, keys, /*restrictPermissions=*/true);
|
||||
}
|
||||
if (!keys.empty()) sodium_memzero(&keys[0], keys.size()); // don't leave every key in freed heap
|
||||
|
||||
|
||||
@@ -36,21 +36,30 @@ static bool s_exporting = false;
|
||||
// Helper to escape CSV field
|
||||
static std::string escapeCSV(const std::string& field)
|
||||
{
|
||||
if (field.find(',') != std::string::npos ||
|
||||
field.find('"') != std::string::npos ||
|
||||
field.find('\n') != std::string::npos) {
|
||||
// Neutralize spreadsheet formula injection: a field beginning with '=', '+', '-', '@',
|
||||
// tab, or CR is interpreted as a formula by Excel/LibreOffice, letting an attacker-supplied
|
||||
// memo/address execute on open. Prefix such fields with a single quote so they render as text.
|
||||
std::string safe = field;
|
||||
if (!safe.empty()) {
|
||||
const char c0 = safe.front();
|
||||
if (c0 == '=' || c0 == '+' || c0 == '-' || c0 == '@' || c0 == '\t' || c0 == '\r')
|
||||
safe.insert(safe.begin(), '\'');
|
||||
}
|
||||
if (safe.find(',') != std::string::npos ||
|
||||
safe.find('"') != std::string::npos ||
|
||||
safe.find('\n') != std::string::npos) {
|
||||
// Escape quotes and wrap in quotes
|
||||
std::string escaped;
|
||||
escaped.reserve(field.size() + 4);
|
||||
escaped.reserve(safe.size() + 4);
|
||||
escaped += '"';
|
||||
for (char c : field) {
|
||||
for (char c : safe) {
|
||||
if (c == '"') escaped += "\"\"";
|
||||
else escaped += c;
|
||||
}
|
||||
escaped += '"';
|
||||
return escaped;
|
||||
}
|
||||
return field;
|
||||
return safe;
|
||||
}
|
||||
|
||||
void ExportTransactionsDialog::show()
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
@@ -132,7 +133,7 @@ struct PfEditState {
|
||||
bool showValue = true;
|
||||
bool show24h = false;
|
||||
bool showSparkline = false;
|
||||
int sparkInterval = 0; // 0=min 1=hour 2=day 3=week 4=month
|
||||
int sparkInterval = 4; // 0=min 1=hour 2=day 3=week 4=month (default month)
|
||||
};
|
||||
static PfEditState s_pfEdit;
|
||||
|
||||
@@ -162,16 +163,16 @@ static bool pfProjectSeries(const std::vector<double>& hist, ImVec2 mn, ImVec2 m
|
||||
}
|
||||
|
||||
static void pfDrawSparkline(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
const std::vector<double>& hist, ImU32 col)
|
||||
const std::vector<double>& hist, ImU32 col, float dp)
|
||||
{
|
||||
std::vector<ImVec2> pts;
|
||||
if (!pfProjectSeries(hist, mn, mx, pts)) return;
|
||||
dl->AddPolyline(pts.data(), (int)pts.size(), col, ImDrawFlags_None, 1.2f);
|
||||
dl->AddPolyline(pts.data(), (int)pts.size(), col, ImDrawFlags_None, 1.2f * dp);
|
||||
}
|
||||
|
||||
// Sparkline with a soft fill under the line — for the card's dedicated bottom strip.
|
||||
static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
const std::vector<double>& hist, ImU32 lineCol)
|
||||
const std::vector<double>& hist, ImU32 lineCol, float dp)
|
||||
{
|
||||
std::vector<ImVec2> pts;
|
||||
if (!pfProjectSeries(hist, mn, mx, pts)) return;
|
||||
@@ -180,7 +181,7 @@ static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
dl->PathLineTo(ImVec2(pts[n - 1].x, mx.y));
|
||||
dl->PathLineTo(ImVec2(pts[0].x, mx.y));
|
||||
dl->PathFillConcave(WithAlpha(lineCol, 28));
|
||||
dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f);
|
||||
dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f * dp);
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +284,7 @@ static void PortfolioBeginEdit(App* app, int index)
|
||||
s_pfEdit.showValue = true;
|
||||
s_pfEdit.show24h = false;
|
||||
s_pfEdit.showSparkline = false;
|
||||
s_pfEdit.sparkInterval = 0;
|
||||
s_pfEdit.sparkInterval = 4; // default month (a real curve from the daily series)
|
||||
}
|
||||
s_pfEdit.search[0] = '\0';
|
||||
s_pfEdit.typeFilter = 0;
|
||||
@@ -667,14 +668,18 @@ static void pfDrawAddressSection(App* app)
|
||||
}
|
||||
filtered.push_back(&a);
|
||||
}
|
||||
// Build the selected-address set once — the sort comparator and the per-row membership test below
|
||||
// both consulted it via a linear PortfolioEntryContains scan (O(n) each) every frame.
|
||||
std::unordered_set<std::string> selSet(s_pfEdit.addrs.begin(), s_pfEdit.addrs.end());
|
||||
std::sort(filtered.begin(), filtered.end(), [&](const AddressInfo* x, const AddressInfo* y) {
|
||||
bool sx = data::PortfolioEntryContains(s_pfEdit.addrs, x->address);
|
||||
bool sy = data::PortfolioEntryContains(s_pfEdit.addrs, y->address);
|
||||
bool sx = selSet.count(x->address) != 0;
|
||||
bool sy = selSet.count(y->address) != 0;
|
||||
if (sx != sy) return sx;
|
||||
return x->balance > y->balance;
|
||||
});
|
||||
if (selAllClicked)
|
||||
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pfEdit.addrs, a->address);
|
||||
for (const AddressInfo* a : filtered)
|
||||
if (data::PortfolioEntryAdd(s_pfEdit.addrs, a->address)) selSet.insert(a->address);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImDrawList* ldl = material::BeginFadeScrollChild("##pfAddrList", s_pfEdit.addrFade,
|
||||
ImVec2(Layout::spacingMd(), Layout::spacingSm()), dp);
|
||||
@@ -684,7 +689,7 @@ static void pfDrawAddressSection(App* app)
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_addr_match"));
|
||||
for (const AddressInfo* ap : filtered) {
|
||||
const AddressInfo& a = *ap;
|
||||
bool inSet = data::PortfolioEntryContains(s_pfEdit.addrs, a.address);
|
||||
bool inSet = selSet.count(a.address) != 0;
|
||||
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rmx(rmn.x + lw, rmn.y + rowH);
|
||||
bool rhov = ImGui::IsMouseHoveringRect(rmn, rmx);
|
||||
@@ -733,8 +738,8 @@ static void pfDrawAddressSection(App* app)
|
||||
ImGui::PushID(a.address.c_str());
|
||||
ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
if (inSet) data::PortfolioEntryRemove(s_pfEdit.addrs, a.address);
|
||||
else data::PortfolioEntryAdd(s_pfEdit.addrs, a.address);
|
||||
if (inSet) { data::PortfolioEntryRemove(s_pfEdit.addrs, a.address); selSet.erase(a.address); }
|
||||
else { data::PortfolioEntryAdd(s_pfEdit.addrs, a.address); selSet.insert(a.address); }
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -956,7 +961,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 pMax(pMin.x + pw, pMin.y + ph);
|
||||
ImU32 accent = s_pfEdit.color ? (ImU32)s_pfEdit.color : 0;
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = 22; g.borderAlpha = 40;
|
||||
GlassPanelSpec g; g.rounding = 10.0f * dp; g.fillAlpha = 22; g.borderAlpha = 40;
|
||||
DrawGlassPanel(pdl, pMin, pMax, g);
|
||||
if (accent) {
|
||||
int oa = (int)(std::max(0, std::min(100, s_pfEdit.outlineOpacity)) * 2.55f + 0.5f);
|
||||
@@ -968,7 +973,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
|
||||
ImVec2 spMin(pMin.x + ppad, pMin.y + ph * 0.46f);
|
||||
ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f);
|
||||
pfDrawSparkline(pdl, spMin, spMax, h, spCol);
|
||||
pfDrawSparkline(pdl, spMin, spMax, h, spCol, dp);
|
||||
}
|
||||
}
|
||||
float rowY = pMin.y + ppad, nameX = pMin.x + ppad;
|
||||
@@ -1080,23 +1085,63 @@ bool PortfolioEditorActive() { return s_pfEdit.open; }
|
||||
// 2=value-hero. No left accent strip — identity comes from the tinted icon + a subtle accent
|
||||
// border. Fixed right-aligned columns keep numbers aligned across rows; on-but-absent fields show a
|
||||
// muted em-dash so a row never looks broken. Pure drawing into dl.
|
||||
// TABLE-style (portfolio_style 0) right-edge column anchors — shared by pfDrawRow's row body and the
|
||||
// column-header strip so the header labels sit exactly over the values they name.
|
||||
// TABLE-style (style 0) column widths (logical px) — shared by pfTableCols (which turns them into
|
||||
// right-edge anchors) and pfDrawRow (which feeds valW/drgxW to fit() truncation), so truncated text
|
||||
// always lines up with the column edges. Change here and both stay in sync.
|
||||
static constexpr float kPfChgColW = 62.0f;
|
||||
static constexpr float kPfValColW = 108.0f;
|
||||
static constexpr float kPfDrgxColW = 118.0f;
|
||||
|
||||
struct PfTableCols { float labelR, drgxR, valR, chgR; };
|
||||
static PfTableCols pfTableCols(float right, float dp)
|
||||
{
|
||||
const float colGap = Layout::spacingMd();
|
||||
PfTableCols c;
|
||||
c.chgR = right;
|
||||
c.valR = c.chgR - kPfChgColW * dp - colGap;
|
||||
c.drgxR = c.valR - kPfValColW * dp - colGap;
|
||||
c.labelR = c.drgxR - kPfDrgxColW * dp - colGap;
|
||||
return c;
|
||||
}
|
||||
|
||||
// Portfolio row height + inter-row gap by style — one source shared by the row drawing
|
||||
// (mktDrawPortfolio) and the scroll-region height budget (RenderMarketTab) so they can't drift and clip.
|
||||
// Row heights: Table 48dp (a dedicated trend column needs vertical room), Cards 68dp (two-zone),
|
||||
// Spotlight 92dp (a dominant bottom chart band under the hero value). Uniform per style.
|
||||
static float pfRowHeight(int style, float dp) { return (style == 0 ? 48.0f : style == 1 ? 68.0f : 92.0f) * dp; }
|
||||
// Fraction of the Table label+trend zone given to the label; the rest is the centre TREND column.
|
||||
// Shared by the row body and the header strip so the sparkline and its "TREND" label line up.
|
||||
static constexpr float kPfTableTrendFrac = 0.38f;
|
||||
static float pfRowGapFor(int style, float dp) { return style == 0 ? 2.0f * dp : Layout::spacingSm(); }
|
||||
|
||||
static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
const config::Settings::PortfolioEntry& e, const WalletState& state,
|
||||
const MarketInfo& market, int style, float dp,
|
||||
ImFont* sub1, ImFont* capFont, bool hov)
|
||||
ImFont* sub1, ImFont* capFont, bool hov, bool tableReserveSpark = false)
|
||||
{
|
||||
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
const double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
ImU32 accent = e.color ? (ImU32)e.color : 0;
|
||||
const bool zeroBal = (bal <= 0.0); // empty groups render dimmed (content only, not the container)
|
||||
|
||||
// Card — a touch more fill than before so rows read as distinct cards, plus a subtle accent
|
||||
// border for identity (the left accent strip was removed).
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
if (accent) {
|
||||
int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f);
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), 10.0f, 0, hov ? 1.8f : 1.2f);
|
||||
} else if (hov) {
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), 10.0f, 0, 1.2f);
|
||||
// ---- Per-style CONTAINER: Table (0) = borderless ledger row (hairline + hover wash); Cards (1) =
|
||||
// glass card with a faint identity tint; Spotlight (2) = a heavier glass tile. No accent line —
|
||||
// the group's colour already reads from its icon and the sparkline, so it adds nothing here. ----
|
||||
const float round = 10.0f * dp;
|
||||
if (style == 0) {
|
||||
if (hov) dl->AddRectFilled(rowMin, rowMax, WithAlpha(OnSurface(), 14), 0.0f);
|
||||
dl->AddLine(ImVec2(rowMin.x, rowMax.y - 0.5f), ImVec2(rowMax.x, rowMax.y - 0.5f),
|
||||
WithAlpha(OnSurface(), 24), 1.0f);
|
||||
} else if (style == 1) {
|
||||
GlassPanelSpec g; g.rounding = round; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
if (accent) dl->AddRectFilled(rowMin, rowMax, WithAlpha(accent, hov ? 16 : 10), round); // faint identity wash
|
||||
if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), round, 0, 1.2f);
|
||||
} else {
|
||||
GlassPanelSpec g; g.rounding = round; g.fillAlpha = hov ? 56 : 46; g.borderAlpha = 60;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), round, 0, 1.2f);
|
||||
}
|
||||
|
||||
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
|
||||
@@ -1130,76 +1175,138 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
float right = rowMax.x - padX;
|
||||
float midY = (rowMin.y + rowMax.y) * 0.5f;
|
||||
|
||||
// Empty groups: dim the BODY content (text/pill/bar/sparkline) to ~half alpha so a $0 group reads as
|
||||
// muted, while the container (card/tick) stays full-strength. Done by scaling the alpha of the
|
||||
// vertices this body emits — one place instead of tinting every draw call.
|
||||
const int zdVtx = zeroBal ? dl->VtxBuffer.Size : -1;
|
||||
|
||||
if (style == 0) {
|
||||
// ---- Compact: icon + label (left); fixed right-aligned columns DRGX | value | 24h | spark.
|
||||
float iconSz = sub1->LegacySize, x = left;
|
||||
// ---- TABLE: icon + label (left); a dedicated centre TREND column (aligned + header-labelled);
|
||||
// right-aligned numeric columns DRGX | value | 24h pill. The trend column is present for the
|
||||
// whole list when any group opts in (tableReserveSpark); each row fills it when it has one.
|
||||
const PfTableCols cols = pfTableCols(right, dp);
|
||||
const float chgR = cols.chgR, valR = cols.valR, drgxR = cols.drgxR, labelR = cols.labelR;
|
||||
const float valW = kPfValColW*dp, drgxW = kPfDrgxColW*dp; // fit() widths (shared with pfTableCols)
|
||||
float iconSz = capFont->LegacySize, x = left;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, midY), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
}
|
||||
const float sparkW = 92.0f*dp, chgW = 60.0f*dp, valW = 112.0f*dp, drgxW = 122.0f*dp;
|
||||
float sparkL = right - sparkW;
|
||||
float chgR = sparkL - colGap;
|
||||
float valR = chgR - chgW - colGap;
|
||||
float drgxR = valR - valW - colGap;
|
||||
float labelR = drgxR - drgxW - colGap;
|
||||
|
||||
if (e.showDrgx) rtext(capFont, drgxR, midY, OnSurfaceMedium(), fit(drgxStr, capFont, drgxW));
|
||||
if (wantValue) rtext(sub1, valR, midY, hasValue?OnSurface():OnSurfaceDisabled(), hasValue?fit(valStr,sub1,valW):kDash);
|
||||
if (wantChange) rtext(capFont, chgR, midY, hasChange?chgCol:OnSurfaceDisabled(), hasChange?chgStr:kDash);
|
||||
if (hasSpark) pfDrawSparkline(dl, ImVec2(sparkL, rowMin.y+padY), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
|
||||
float lblMaxW = std::max(24.0f*dp, labelR - x);
|
||||
if (wantChange) {
|
||||
const std::string s = hasChange ? chgStr : kDash;
|
||||
const float pw = tw(capFont, s) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
|
||||
const ImVec2 pmn(chgR - pw, midY - ph*0.5f);
|
||||
if (hasChange) dl->AddRectFilled(pmn, ImVec2(chgR, midY + ph*0.5f), WithAlpha(chgCol, 32), ph*0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(pmn.x + 6.0f*dp, midY - capFont->LegacySize*0.5f),
|
||||
hasChange?chgCol:OnSurfaceDisabled(), s.c_str());
|
||||
}
|
||||
// Dedicated centre TREND column: when the list has any sparkline (tableReserveSpark), cap the
|
||||
// label to a fixed boundary so the column + its header label align across every row; each row
|
||||
// fills it only when that group opts in. Rows without any list-wide trend keep the full label.
|
||||
// Trend-column origin from the fixed LEFT (icon-independent) so it lines up with the "TREND"
|
||||
// header and across rows whether or not a group has an icon (x may be shifted past the icon).
|
||||
const float zoneW = std::max(24.0f*dp, labelR - left);
|
||||
const float trendX0 = left + zoneW * kPfTableTrendFrac;
|
||||
const float lblMaxW = tableReserveSpark ? std::max(24.0f*dp, trendX0 - colGap - x) : std::max(24.0f*dp, labelR - x);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(x, midY - sub1->LegacySize*0.5f), OnSurface(), fit(e.label,sub1,lblMaxW).c_str());
|
||||
if (hasSpark && tableReserveSpark && labelR - trendX0 > 40.0f * dp)
|
||||
pfDrawSparklineFilled(dl, ImVec2(trendX0, rowMin.y + padY), ImVec2(labelR, rowMax.y - padY), spark, sparkCol, dp);
|
||||
} else if (style == 1) {
|
||||
// ---- Detailed: 2x2 grid — label/value on top, DRGX/24h below — plus a tall sparkline strip.
|
||||
float iconSz = sub1->LegacySize, x = left;
|
||||
const float sparkW = 150.0f*dp;
|
||||
float sparkL = right - sparkW;
|
||||
float textR = hasSpark ? (sparkL - colGap) : right;
|
||||
float topY = rowMin.y + padY;
|
||||
float botY = rowMax.y - padY - capFont->LegacySize;
|
||||
// ---- CARDS: a balanced snapshot — a LEFT-aligned info stack (icon + label / value+delta chip /
|
||||
// DRGX) grouped on the left, with a full-height sparkline filling the rest of the width.
|
||||
const float zoneTop = rowMin.y + padY;
|
||||
const float zoneBot = rowMax.y - padY;
|
||||
// Icon centred in the info zone; the label/value/DRGX stack sits to its right.
|
||||
const float iconSz = capFont->LegacySize;
|
||||
float textX = left;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(left + iconSz*0.5f, (zoneTop + zoneBot)*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
textX = left + iconSz + Layout::spacingSm();
|
||||
}
|
||||
// value (top-right, emphasized)
|
||||
std::string vTop = wantValue ? (hasValue?valStr:kDash) : std::string();
|
||||
if (wantValue) dl->AddText(sub1, sub1->LegacySize, ImVec2(textR - tw(sub1,vTop), topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str());
|
||||
// label (top-left, fills up to the value column)
|
||||
float labelR = wantValue ? (textR - tw(sub1,vTop) - colGap) : textR;
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(x, topY), OnSurface(), fit(e.label,sub1,std::max(24.0f*dp, labelR - x)).c_str());
|
||||
// DRGX (bottom-left, muted) + 24h (bottom-right, colored)
|
||||
if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), OnSurfaceMedium(), drgxStr.c_str());
|
||||
if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(textR - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); }
|
||||
// tall sparkline strip spanning both lines
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, topY), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
// Info zone driven by the ACTUAL content width so a short group hands the extra room to the chart;
|
||||
// bounded [28%, 52%] so the info never cramps and the chart always keeps a decent band.
|
||||
float infoR = right, cSparkL = right;
|
||||
if (hasSpark) {
|
||||
const std::string vTxt0 = wantValue ? (hasValue?valStr:kDash) : std::string();
|
||||
const float chipW = (wantChange && hasChange) ? (Layout::spacingSm() + tw(capFont, chgStr) + 12.0f*dp) : 0.0f;
|
||||
const float valLineW = wantValue ? (tw(sub1, vTxt0) + chipW) : 0.0f;
|
||||
const float contentW = std::max(std::max(tw(capFont, e.label), valLineW), e.showDrgx ? tw(capFont, drgxStr) : 0.0f);
|
||||
const float span = right - left;
|
||||
const float infoW = std::min(std::max((textX - left) + contentW + colGap * 2.0f, span * 0.28f), span * 0.52f);
|
||||
cSparkL = left + infoW;
|
||||
infoR = cSparkL - colGap;
|
||||
}
|
||||
const float gap = Layout::spacingXs();
|
||||
const float stackH = capFont->LegacySize + gap + sub1->LegacySize + gap + capFont->LegacySize;
|
||||
float cy = zoneTop + std::max(0.0f, (zoneBot - zoneTop - stackH) * 0.5f);
|
||||
// label (muted)
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, cy), OnSurfaceMedium(), fit(e.label, capFont, std::max(24.0f*dp, infoR - textX)).c_str());
|
||||
cy += capFont->LegacySize + gap;
|
||||
// value (leads via a soft shadow) + delta chip trailing it
|
||||
if (wantValue) {
|
||||
const std::string vTxt = hasValue ? valStr : kDash;
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(textX, cy), hasValue?OnSurface():OnSurfaceDisabled(), vTxt.c_str());
|
||||
if (wantChange && hasChange) {
|
||||
const float pw = tw(capFont, chgStr) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
|
||||
const float chipX = textX + tw(sub1, vTxt) + Layout::spacingSm();
|
||||
const float chipY = cy + (sub1->LegacySize - ph)*0.5f;
|
||||
if (chipX + pw <= infoR) {
|
||||
dl->AddRectFilled(ImVec2(chipX, chipY), ImVec2(chipX+pw, chipY+ph), WithAlpha(chgCol, 34), ph*0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(chipX+6.0f*dp, chipY + (ph-capFont->LegacySize)*0.5f), chgCol, chgStr.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
cy += sub1->LegacySize + gap;
|
||||
// DRGX (muted)
|
||||
if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, cy), OnSurfaceMedium(), fit(drgxStr, capFont, std::max(24.0f*dp, infoR - textX)).c_str());
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(cSparkL, zoneTop), ImVec2(right, zoneBot), spark, sparkCol, dp);
|
||||
} else {
|
||||
// ---- Value-hero: label; big neutral value; 24h; DRGX + sparkline fill the right.
|
||||
// ---- SPOTLIGHT: "big number over a chart" hero tile — a top zone (muted name + DRGX, then a
|
||||
// LARGE value coloured by 24h with a delta chip) over a dominant full-width sparkline band
|
||||
// filling the bottom. The value spans the full width now (no competing right strip).
|
||||
const float heroSz = sub1->LegacySize * 1.5f; // ImGui 1.92 rebakes at this size (crisp, not upscaled)
|
||||
const float topY = rowMin.y + padY;
|
||||
float iconSz = capFont->LegacySize, x = left;
|
||||
const float sparkW = (right-left)*0.40f;
|
||||
float sparkL = right - sparkW;
|
||||
float topY = rowMin.y + padY;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+capFont->LegacySize*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
}
|
||||
float rightZoneL = hasSpark ? sparkL : right;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY), OnSurfaceMedium(), fit(e.label,capFont,std::max(24.0f*dp, rightZoneL - x - colGap)).c_str());
|
||||
// hero value — neutral bold (accent stays in the icon/border, so it can't clash with the 24h)
|
||||
float valY = topY + capFont->LegacySize + Layout::spacingXs();
|
||||
std::string hero = hasValue ? valStr : (e.showDrgx ? drgxStr : kDash);
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(left, valY), hasValue?OnSurface():OnSurfaceDisabled(), hero.c_str());
|
||||
// 24h below the value (colored / dash)
|
||||
float botY = valY + sub1->LegacySize + Layout::spacingXs();
|
||||
if (wantChange) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), hasChange?chgCol:OnSurfaceDisabled(), (hasChange?chgStr:kDash).c_str());
|
||||
// DRGX — right-aligned on the value line; fills the right when there's no sparkline.
|
||||
if (e.showDrgx && hasValue) {
|
||||
float dr = hasSpark ? (sparkL - colGap) : right;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(dr - tw(capFont,drgxStr), valY + (sub1->LegacySize - capFont->LegacySize)*0.5f), OnSurfaceMedium(), drgxStr.c_str());
|
||||
// name (top-left, muted) + DRGX (top-right, muted)
|
||||
const float drgxW2 = e.showDrgx ? tw(capFont, drgxStr) : 0.0f;
|
||||
if (e.showDrgx) rtext(capFont, right, topY + capFont->LegacySize*0.5f, OnSurfaceMedium(), drgxStr);
|
||||
const float nameMaxW = std::max(24.0f*dp, (right - (e.showDrgx ? drgxW2 + colGap : 0.0f)) - x);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY), OnSurfaceMedium(), fit(e.label,capFont,nameMaxW).c_str());
|
||||
// hero value — coloured by 24h direction (neutral when no live change)
|
||||
const float valY = topY + capFont->LegacySize + Layout::spacingXs();
|
||||
const std::string hero = hasValue ? valStr : (e.showDrgx ? drgxStr : kDash);
|
||||
const ImU32 heroCol = !hasValue ? OnSurfaceDisabled() : (hasChange ? chgCol : OnSurface());
|
||||
DrawTextShadow(dl, sub1, heroSz, ImVec2(left, valY), heroCol, hero.c_str());
|
||||
const float heroW = sub1->CalcTextSizeA(heroSz, FLT_MAX, 0, hero.c_str()).x;
|
||||
// delta chip trailing the hero value
|
||||
if (wantChange && hasChange) {
|
||||
const float pw = tw(capFont, chgStr) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
|
||||
const float chipX = left + heroW + Layout::spacingSm();
|
||||
const float chipY = valY + (heroSz - ph)*0.5f;
|
||||
if (chipX + pw <= right) {
|
||||
dl->AddRectFilled(ImVec2(chipX, chipY), ImVec2(chipX+pw, chipY+ph), WithAlpha(chgCol, 34), ph*0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(chipX+6.0f*dp, chipY + (ph-capFont->LegacySize)*0.5f), chgCol, chgStr.c_str());
|
||||
}
|
||||
}
|
||||
// full-width sparkline band filling the bottom of the tile, below the hero value
|
||||
if (hasSpark) {
|
||||
const float bandTop = valY + heroSz + Layout::spacingXs();
|
||||
if (rowMax.y - padY - bandTop > 12.0f * dp)
|
||||
pfDrawSparklineFilled(dl, ImVec2(left, bandTop), ImVec2(right, rowMax.y - padY), spark, sparkCol, dp);
|
||||
}
|
||||
}
|
||||
|
||||
if (zdVtx >= 0) { // fade the just-emitted body vertices for an empty group
|
||||
for (int vi = zdVtx; vi < dl->VtxBuffer.Size; ++vi) {
|
||||
ImDrawVert& v = dl->VtxBuffer[vi];
|
||||
v.col = material::ScaleAlpha(v.col, 0.5f);
|
||||
}
|
||||
// filled sparkline (right ~40%)
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, valY - 2*dp), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1396,6 +1503,23 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
||||
float tradeBtnX = cardMax.x - pad - tradeBtnW;
|
||||
float tradeBtnY = cardMin.y + Layout::spacingSm();
|
||||
|
||||
// Chart line/candle segmented control, immediately left of the trade button — only when the
|
||||
// selected range has per-exchange candles (Live / aggregate is line-only, nothing to toggle).
|
||||
if (cx.chartCandles && cx.chartCandles->size() >= 2) {
|
||||
const char* styleIcons[2] = { ICON_MD_SHOW_CHART, ICON_MD_CANDLESTICK_CHART };
|
||||
const float csW = 2.0f * tradeBtnH;
|
||||
const ImVec2 csOrigin(tradeBtnX - csW - Layout::spacingSm(), tradeBtnY);
|
||||
const ImVec2 csSaved = ImGui::GetCursorScreenPos();
|
||||
const int csCur = s_mkt.chartStyle;
|
||||
const int csClk = SegmentedControl(dl, csOrigin, csW, tradeBtnH, styleIcons, 2, csCur,
|
||||
Type().iconSmall(), "##chartStyleSeg", dp);
|
||||
if (csClk >= 0 && csClk != csCur) {
|
||||
s_mkt.chartStyle = csClk;
|
||||
if (app->settings()) { app->settings()->setChartStyle(csClk); app->settings()->save(); }
|
||||
}
|
||||
ImGui::SetCursorScreenPos(csSaved);
|
||||
}
|
||||
|
||||
ImVec2 tMin(tradeBtnX, tradeBtnY), tMax(tradeBtnX + tradeBtnW, tradeBtnY + tradeBtnH);
|
||||
bool tradeHov = material::IsRectHovered(tMin, tMax);
|
||||
|
||||
@@ -1518,30 +1642,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
bx += bw + Layout::spacingXs();
|
||||
}
|
||||
|
||||
// Line/candle toggle — only when the selected range has per-exchange candles (the aggregate /
|
||||
// Live view is line-only, so the toggle is hidden there).
|
||||
if (cx.chartCandles && cx.chartCandles->size() >= 2) {
|
||||
bx += Layout::spacingSm();
|
||||
ImFont* icoF = material::Typography::instance().iconSmall();
|
||||
const bool isCandle = (s_mkt.chartStyle == 1);
|
||||
const char* styleIcon = isCandle ? ICON_MD_CANDLESTICK_CHART : ICON_MD_SHOW_CHART;
|
||||
ImVec2 tmn(bx, rowTop), tmx(bx + pillH, rowTop + pillH);
|
||||
bool thov = material::IsRectHovered(tmn, tmx);
|
||||
if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f * mktDp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
|
||||
ImVec2 tiSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, styleIcon);
|
||||
dl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(tmn.x + (pillH - tiSz.x) * 0.5f, tmn.y + (pillH - tiSz.y) * 0.5f),
|
||||
thov ? OnSurface() : OnSurfaceMedium(), styleIcon);
|
||||
ImGui::SetCursorScreenPos(tmn);
|
||||
if (ImGui::InvisibleButton("##ChartStyle", ImVec2(pillH, pillH))) {
|
||||
s_mkt.chartStyle = isCandle ? 0 : 1;
|
||||
if (app->settings()) { app->settings()->setChartStyle(s_mkt.chartStyle); app->settings()->save(); }
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
material::Tooltip("%s", TR(isCandle ? "market_style_line" : "market_style_candle"));
|
||||
bx += pillH;
|
||||
}
|
||||
// (Line/candle chart-style toggle now lives top-right of the price hero, left of the trade button.)
|
||||
|
||||
// Refresh button (far right).
|
||||
float rEdge = chartMax.x - chartPad;
|
||||
@@ -1625,7 +1726,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
// Keep the axis label inside the card even on narrow windows (min-padding may be
|
||||
// smaller than the label width) so it never spills onto the tab background.
|
||||
float lblX = std::max(chartMin.x + 3.0f, plotLeft - labelSz.x - 6);
|
||||
float lblX = std::max(chartMin.x + 3.0f * mktDp, plotLeft - labelSz.x - 6.0f * mktDp);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(lblX, gy - labelSz.y * 0.5f),
|
||||
OnSurfaceDisabled(), buf);
|
||||
@@ -1688,8 +1789,8 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
tipX = std::max(plotLeft, tipX);
|
||||
float tipY = plotTop + 4.0f * mktDp;
|
||||
ImVec2 tMin(tipX, tipY), tMax(tipX + tw + pad * 2, tipY + th);
|
||||
dl->AddRectFilled(tMin, tMax, IM_COL32(20, 20, 30, 235), 4.0f);
|
||||
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
|
||||
dl->AddRectFilled(tMin, tMax, IM_COL32(20, 20, 30, 235), 4.0f * mktDp);
|
||||
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, 30), 4.0f * mktDp, 0, 1.0f);
|
||||
ImU32 cCol = (hc.close >= hc.open) ? Success() : Error();
|
||||
dl->AddText(capFont, lh, ImVec2(tipX + pad, tipY + pad), OnSurface(), when);
|
||||
dl->AddText(capFont, lh, ImVec2(tipX + pad, tipY + pad + lh + gap), cCol, l2);
|
||||
@@ -1792,16 +1893,16 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
snprintf(buf, sizeof(buf), "%s", FormatPrice(s_mkt.history[idx]).c_str());
|
||||
ImVec2 tipSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
float tipPad = Layout::spacingSm() + Layout::spacingXs();
|
||||
float tipX = px + 10;
|
||||
float tipY = py - tipSz.y - tipPad * 2 - 4;
|
||||
float tipX = px + 10.0f * mktDp;
|
||||
float tipY = py - tipSz.y - tipPad * 2 - 4.0f * mktDp;
|
||||
if (tipX + tipSz.x + tipPad * 2 > plotRight)
|
||||
tipX = px - tipSz.x - tipPad * 2 - 10;
|
||||
if (tipY < plotTop) tipY = py + 10;
|
||||
tipX = px - tipSz.x - tipPad * 2 - 10.0f * mktDp;
|
||||
if (tipY < plotTop) tipY = py + 10.0f * mktDp;
|
||||
|
||||
ImVec2 tipMin(tipX, tipY);
|
||||
ImVec2 tipMax(tipX + tipSz.x + tipPad * 2, tipY + tipSz.y + tipPad * 2);
|
||||
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f);
|
||||
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
|
||||
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f * mktDp);
|
||||
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f * mktDp, 0, 1.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
|
||||
}
|
||||
@@ -1867,12 +1968,29 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
}
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
||||
// "Manage…" button, right-aligned on the header row — opens the portfolio editor.
|
||||
// Portfolio-style segmented control (Table / Cards / Spotlight) + "Manage…" button, right-aligned on
|
||||
// the header row (style picker left of Manage). Manage opens the portfolio editor.
|
||||
{
|
||||
const char* ml = TR("portfolio_manage");
|
||||
float mBtnW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, ml).x + Layout::spacingMd() * 2;
|
||||
const char* styleItems[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
|
||||
TR("portfolio_style_featured") };
|
||||
const float segH = ImGui::GetFrameHeight();
|
||||
const float segW = 210.0f * mktDp;
|
||||
const float sGap = Layout::spacingSm();
|
||||
ImGui::SameLine();
|
||||
material::RightAlignX(mBtnW);
|
||||
RightAlignX(segW + sGap + mBtnW); // right-align the [style picker | Manage] group
|
||||
const ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||
const int curStyle = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
||||
const int segClk = SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, segW, segH,
|
||||
styleItems, 3, curStyle, Type().caption(), "##pfStyleSeg", mktDp);
|
||||
if (segClk >= 0 && segClk != curStyle && app->settings()) {
|
||||
app->settings()->setPortfolioStyle(segClk);
|
||||
app->settings()->save();
|
||||
}
|
||||
ImGui::SetCursorScreenPos(segOrigin);
|
||||
ImGui::Dummy(ImVec2(segW, segH)); // reserve + restore layout flow
|
||||
ImGui::SameLine(0.0f, sGap);
|
||||
if (material::TactileButton(ml, ImVec2(mBtnW, 0))) {
|
||||
// Open the editor on the first group visible to this wallet (or the empty state if none) —
|
||||
// never a raw index 0 that might belong to a different wallet.
|
||||
@@ -1982,8 +2100,16 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
}
|
||||
|
||||
int style = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
||||
float rowH = (style == 0 ? 46.0f : style == 1 ? 64.0f : 84.0f) * mktDp;
|
||||
float rowGap = Layout::spacingSm();
|
||||
// The Table's dedicated TREND column (+ its header label) is present for the whole list when any
|
||||
// visible group opts into a sparkline, so the column aligns and doesn't flicker per row.
|
||||
bool anySpark = false;
|
||||
for (int i : vis) {
|
||||
const auto& e = allEntries[i];
|
||||
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) { anySpark = true; break; }
|
||||
}
|
||||
float rowH = pfRowHeight(style, mktDp);
|
||||
// Table rows abut like a ledger (thin gap); Cards/Spotlight breathe.
|
||||
float rowGap = pfRowGapFor(style, mktDp);
|
||||
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH));
|
||||
@@ -2002,6 +2128,31 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||
} else {
|
||||
// TABLE column-header strip (style 0): a thin static header labelling the numeric columns —
|
||||
// a signature the card styles don't have. Uses the shared pfTableCols so it aligns with rows.
|
||||
if (style == 0) {
|
||||
const float headerH = capFont->LegacySize + 8.0f * mktDp;
|
||||
const ImVec2 hp = ImGui::GetCursorScreenPos();
|
||||
const float hLeft = hp.x + Layout::spacingMd();
|
||||
const float hRight = hp.x + rowW - Layout::spacingMd();
|
||||
const PfTableCols hc = pfTableCols(hRight, mktDp);
|
||||
ImFont* ovF = Type().overline();
|
||||
const float ty = hp.y + (headerH - ovF->LegacySize) * 0.5f;
|
||||
const ImU32 hcol = OnSurfaceDisabled();
|
||||
auto hdr = [&](float xr, const char* s){
|
||||
const float w = ovF->CalcTextSizeA(ovF->LegacySize, FLT_MAX, 0, s).x;
|
||||
rdl->AddText(ovF, ovF->LegacySize, ImVec2(xr - w, ty), hcol, s);
|
||||
};
|
||||
rdl->AddText(ovF, ovF->LegacySize, ImVec2(hLeft, ty), hcol, TR("market_col_name"));
|
||||
if (anySpark) // TREND column header, left-aligned at the same x the row sparklines start
|
||||
rdl->AddText(ovF, ovF->LegacySize, ImVec2(hLeft + (hc.labelR - hLeft) * kPfTableTrendFrac, ty), hcol, TR("market_col_trend"));
|
||||
hdr(hc.drgxR, DRAGONX_TICKER);
|
||||
hdr(hc.valR, TR("market_col_value"));
|
||||
hdr(hc.chgR, TR("market_24h"));
|
||||
rdl->AddLine(ImVec2(hLeft, hp.y + headerH - 1.0f), ImVec2(hRight, hp.y + headerH - 1.0f),
|
||||
WithAlpha(OnSurface(), 20), 1.0f);
|
||||
ImGui::Dummy(ImVec2(rowW, headerH));
|
||||
}
|
||||
for (int vi = 0; vi < (int)vis.size(); vi++) {
|
||||
int i = vis[vi];
|
||||
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||
@@ -2011,7 +2162,7 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
bool hov = ImGui::IsItemHovered();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, capFont, hov);
|
||||
pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, capFont, hov, anySpark);
|
||||
if (clicked) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; }
|
||||
if (vi + 1 < (int)vis.size()) ImGui::Dummy(ImVec2(rowW, rowGap));
|
||||
}
|
||||
@@ -2047,6 +2198,10 @@ void RenderMarketTab(App* app)
|
||||
// Also kick the historical price-chart fetch (self-throttled) so the chart's hour/day/week/
|
||||
// month intervals populate promptly when the user opens the Market tab.
|
||||
app->refreshMarketChart();
|
||||
// ...and the SELECTED pair's per-exchange OHLC candles (self-throttled, in-flight-guarded). Without
|
||||
// this, the candle series only loaded on a pair-chip / refresh click, so the candlestick toggle was
|
||||
// missing (and "switch to candlesticks" silently did nothing) when the tab was just opened.
|
||||
app->refreshExchangeChart();
|
||||
const auto& registry = EffectiveRegistry(market);
|
||||
|
||||
// Load persisted exchange/pair on first frame
|
||||
@@ -2058,14 +2213,16 @@ void RenderMarketTab(App* app)
|
||||
|
||||
// Left/Right arrows: cycle portfolio row styles (compact / detailed / featured), mirroring the
|
||||
// Overview tab's layout switch. Skip while typing, when Ctrl is held (theme cycle), or while the
|
||||
// portfolio editor modal is open.
|
||||
if (app->settings() && !s_pfEdit.open && !ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
||||
// portfolio editor / market-settings modal is open.
|
||||
if (app->settings() && !s_pfEdit.open &&
|
||||
!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
||||
bool prev = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
|
||||
bool next = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
|
||||
if (prev || next) {
|
||||
int st = app->settings()->getPortfolioStyle();
|
||||
st = next ? (st + 1) % 3 : (st + 2) % 3;
|
||||
app->settings()->setPortfolioStyle(st);
|
||||
app->settings()->save(); // persist the choice (matches the gear-modal control)
|
||||
const char* names[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
|
||||
TR("portfolio_style_featured") };
|
||||
Notifications::instance().info(std::string(TR("portfolio_style_label")) + ": " + names[st]);
|
||||
@@ -2123,15 +2280,16 @@ void RenderMarketTab(App* app)
|
||||
for (const auto& e : pfEntriesGeo)
|
||||
if (e.scope.empty() || (!pfActiveHash.empty() && e.scope == pfActiveHash)) pfVisN++;
|
||||
int pfStyle = app->settings()->getPortfolioStyle();
|
||||
float pfRowH = (pfStyle == 0 ? 46.0f : pfStyle == 1 ? 64.0f : 84.0f) * mktDp;
|
||||
float pfRowGap = Layout::spacingSm();
|
||||
float pfRowH = pfRowHeight(pfStyle, mktDp);
|
||||
float pfRowGap = pfRowGapFor(pfStyle, mktDp); // Table rows abut
|
||||
float pfHeaderH = (pfStyle == 0) ? (capFont->LegacySize + 8.0f * mktDp) : 0.0f; // Table column strip
|
||||
const int pfMaxVisibleRows = 4; // rows shown before the list scrolls
|
||||
// Height for up to pfMaxVisibleRows rows. ItemSpacing is zeroed inside the row child, so the
|
||||
// content is exactly rows + inter-row gaps; one extra gap of bottom breathing room keeps the
|
||||
// last visible row off the clip edge. More groups than this scroll internally.
|
||||
int pfVisRows = std::min(pfVisN, pfMaxVisibleRows);
|
||||
float pfGroupsH = (pfVisRows > 0)
|
||||
? (pfVisRows * pfRowH + (pfVisRows - 1) * pfRowGap + pfRowGap)
|
||||
? (pfHeaderH + pfVisRows * pfRowH + (pfVisRows - 1) * pfRowGap + pfRowGap)
|
||||
: 0.0f;
|
||||
float portfolioH = pfSummaryH + pfGroupsH;
|
||||
|
||||
|
||||
@@ -57,7 +57,10 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
|
||||
// --- Compute thread grid layout based on controls card width ---
|
||||
// Estimate controlsW first to compute cols correctly
|
||||
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size, miningBtnMaxW) - miningBtnGap;
|
||||
// The Mine button is square (= card height, which scales with DPI), so the width we
|
||||
// reserve for it here must scale too — a RAW clamp under-reserves at >100% scaling, which
|
||||
// over-estimates the grid width and lets the thread cells overflow the card (e.g. at 150%).
|
||||
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size * dp, miningBtnMaxW) - miningBtnGap;
|
||||
float innerW = estControlsW - pad * 2;
|
||||
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f));
|
||||
float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size);
|
||||
|
||||
@@ -308,11 +308,14 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
|
||||
if (rowHov && !inXZone)
|
||||
pdl->AddRectFilled(rowMin, rowMax, StateHover());
|
||||
// Item text with internal padding
|
||||
// Item text with internal padding, middle-truncated so a long saved URL
|
||||
// can't run under the trailing X (delete) button.
|
||||
float textY = rowMin.y + (rowH - rowFontSz) * 0.5f;
|
||||
float maxTextW = popupInnerW - xZoneW - textPadX * 2.0f;
|
||||
std::string urlDisp = material::TruncateToWidth(url, rowFont, rowFontSz, maxTextW);
|
||||
pdl->AddText(rowFont, rowFontSz,
|
||||
ImVec2(rowMin.x + textPadX, textY),
|
||||
isCurrent ? Primary() : OnSurface(), url.c_str());
|
||||
isCurrent ? Primary() : OnSurface(), urlDisp.c_str());
|
||||
// X button — flush with right edge, icon centered
|
||||
{
|
||||
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
|
||||
@@ -467,11 +470,14 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
|
||||
if (rowHov && !inXZone)
|
||||
pdl->AddRectFilled(rowMin, rowMax, StateHover());
|
||||
// Full address text with internal padding
|
||||
// Address text with internal padding, middle-truncated so a full z-address
|
||||
// (~78 chars) can't run under the trailing X (delete) button.
|
||||
float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f;
|
||||
float wMaxTextW = wPopupInnerW - wXZoneW - wTextPadX * 2.0f;
|
||||
std::string addrDisp = material::TruncateToWidth(addr, wRowFont, wRowFontSz, wMaxTextW);
|
||||
pdl->AddText(wRowFont, wRowFontSz,
|
||||
ImVec2(rowMin.x + wTextPadX, textY),
|
||||
isCurrent ? Primary() : OnSurface(), addr.c_str());
|
||||
isCurrent ? Primary() : OnSurface(), addrDisp.c_str());
|
||||
// Tooltip for long addresses
|
||||
if (rowHov && !inXZone)
|
||||
material::Tooltip("%s", addr.c_str());
|
||||
|
||||
@@ -98,7 +98,12 @@ void RenderPeersTab(App* app)
|
||||
|
||||
// Scrollable child to contain all content within available space
|
||||
ImVec2 peersAvail = ImGui::GetContentRegionAvail();
|
||||
ImGui::BeginChild("##PeersScroll", peersAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
// NoScrollWithMouse: the inner ##PeersList owns wheel scrolling (via ApplySmoothScroll). Without
|
||||
// this, a wheel over the list would scroll BOTH the list (smooth-scroll) and this outer container
|
||||
// (ImGui forwards the NoScrollWithMouse child's wheel to its scrollable ancestor) — a double-scroll.
|
||||
// Safe because the peer panel is sized to fill the remaining height, so this outer never overflows.
|
||||
ImGui::BeginChild("##PeersScroll", peersAvail, false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
// Responsive: scale factors per frame
|
||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <ctime>
|
||||
#include <cmath>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
@@ -64,10 +65,7 @@ struct DisplayTx {
|
||||
|
||||
std::string DisplayTx::getTimeString() const {
|
||||
if (timestamp <= 0) return TR("pending");
|
||||
std::time_t t = static_cast<std::time_t>(timestamp);
|
||||
char buf[64];
|
||||
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", std::localtime(&t));
|
||||
return buf;
|
||||
return dragonx::util::formatClockDateTime(timestamp); // honors the app-wide 24h/12h clock
|
||||
}
|
||||
|
||||
// Relative time string (localized long form, e.g. "5 minutes ago")
|
||||
@@ -144,6 +142,12 @@ void RenderTransactionsTab(App* app)
|
||||
const auto addrLabel = S.label("tabs.transactions", "address-label");
|
||||
const auto& state = app->state();
|
||||
|
||||
// Txids that carried a chat message (sent or received) — drives the "Message" badge and the Chat
|
||||
// filter below. Rebuilt each frame from the in-memory chat store (cheap: O(chat messages), which
|
||||
// is small); empty when chat is disabled or the wallet has no chat identity. Both variants populate
|
||||
// the chat store before this tab renders, so the same lookup serves full-node and lite.
|
||||
const std::unordered_set<std::string> chatTxids = app->chatService().store().chatTxids();
|
||||
|
||||
// Responsive scale factors (recomputed every frame)
|
||||
ImVec2 contentAvail = ImGui::GetContentRegionAvail();
|
||||
const float hs = Layout::hScale(contentAvail.x);
|
||||
@@ -253,8 +257,9 @@ void RenderTransactionsTab(App* app)
|
||||
innerPad, iconSz, glassSpec, ovFont, capFont, body2, "mined", goldCol, "mined_upper",
|
||||
minedCount, minedTotal, "+", 3, type_filter);
|
||||
|
||||
// Selected card accent
|
||||
if (type_filter > 0) {
|
||||
// Selected card accent (only the three summary cards map to a card position — the Chat filter
|
||||
// (4) has no card, so guard the idx_map lookup to 1..3).
|
||||
if (type_filter > 0 && type_filter <= 3) {
|
||||
int idx_map[] = {-1, 1, 0, 2};
|
||||
int idx = idx_map[type_filter];
|
||||
float xOff = idx * (cardW + cardGap);
|
||||
@@ -284,7 +289,8 @@ void RenderTransactionsTab(App* app)
|
||||
ImGui::SameLine(0, filterGap);
|
||||
float comboW = std::max(80.0f, ((filterCombo.width > 0) ? filterCombo.width : 120.0f) * hs);
|
||||
ImGui::SetNextItemWidth(comboW);
|
||||
const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"), TR("mined_filter") };
|
||||
const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"),
|
||||
TR("mined_filter"), TR("chat_filter") };
|
||||
ImGui::Combo("##TxType", &type_filter, types, IM_ARRAYSIZE(types));
|
||||
|
||||
// Sort selector
|
||||
@@ -479,6 +485,7 @@ void RenderTransactionsTab(App* app)
|
||||
if (type_filter == 1 && dtx.display_type != "send" && dtx.display_type != "shield") continue;
|
||||
if (type_filter == 2 && dtx.display_type != "receive") continue;
|
||||
if (type_filter == 3 && dtx.display_type != "generate" && dtx.display_type != "immature" && dtx.display_type != "mined") continue;
|
||||
if (type_filter == 4 && chatTxids.count(dtx.txid) == 0) continue; // chat-only
|
||||
}
|
||||
if (!search_str.empty()) {
|
||||
if (!containsIgnoreCase(dtx.address, search_str) &&
|
||||
@@ -660,6 +667,7 @@ void RenderTransactionsTab(App* app)
|
||||
|
||||
// Determine type info
|
||||
bool shieldedDisplay = tx.display_type == "shield";
|
||||
const bool isChatTx = chatTxids.count(tx.txid) > 0; // carried a chat message
|
||||
ImU32 iconCol;
|
||||
const char* typeStr;
|
||||
if (shieldedDisplay) {
|
||||
@@ -752,12 +760,17 @@ void RenderTransactionsTab(App* app)
|
||||
}
|
||||
// Position status badge in the middle-right area
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
const char* shieldedStr = TR("shielded_type");
|
||||
ImVec2 shieldSz = shieldedDisplay
|
||||
? capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, shieldedStr)
|
||||
// Optional "top" badge stacked above the status pill: "Message" for a chat tx or
|
||||
// "Shielded" for an autoshield. Chat txs are send/receive (not the "shield" type),
|
||||
// so the two are mutually exclusive; chat takes precedence.
|
||||
const bool showTopBadge = isChatTx || shieldedDisplay;
|
||||
const char* topBadgeStr = isChatTx ? TR("tx_chat_badge") : TR("shielded_type");
|
||||
const ImU32 topBadgeCol = isChatTx ? Secondary() : Primary();
|
||||
ImVec2 topSz = showTopBadge
|
||||
? capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, topBadgeStr)
|
||||
: ImVec2(0, 0);
|
||||
float shieldPillW = shieldSz.x + Layout::spacingSm() * 2.0f;
|
||||
float stackW = shieldedDisplay ? std::max(sSz.x, shieldPillW) : sSz.x;
|
||||
float topPillW = topSz.x + Layout::spacingSm() * 2.0f;
|
||||
float stackW = showTopBadge ? std::max(sSz.x, topPillW) : sSz.x;
|
||||
float statusX = amtX - stackW - Layout::spacingXxl();
|
||||
float minStatusX = cx + innerW * 0.25f; // don't overlap address
|
||||
if (statusX < minStatusX) statusX = minStatusX;
|
||||
@@ -766,18 +779,17 @@ void RenderTransactionsTab(App* app)
|
||||
// row background on every skin (faint alpha-30 fills used to vanish on dark-red /
|
||||
// near-white / gradient skins). Fill 48, border 90 of the state color's RGB.
|
||||
const float pillRound = schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size;
|
||||
if (shieldedDisplay) {
|
||||
float shieldX = statusX + (stackW - shieldSz.x) * 0.5f;
|
||||
ImU32 shieldCol = Primary();
|
||||
ImVec2 shieldPillMin(shieldX - Layout::spacingSm(), cy - 1.0f);
|
||||
ImVec2 shieldPillMax(shieldX + shieldSz.x + Layout::spacingSm(),
|
||||
shieldPillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(shieldPillMin, shieldPillMax,
|
||||
(shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(48) << 24), pillRound);
|
||||
dl->AddRect(shieldPillMin, shieldPillMax,
|
||||
(shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(90) << 24), pillRound, 0, 1.0f);
|
||||
if (showTopBadge) {
|
||||
float topX = statusX + (stackW - topSz.x) * 0.5f;
|
||||
ImVec2 topPillMin(topX - Layout::spacingSm(), cy - 1.0f);
|
||||
ImVec2 topPillMax(topX + topSz.x + Layout::spacingSm(),
|
||||
topPillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(topPillMin, topPillMax,
|
||||
(topBadgeCol & 0x00FFFFFFu) | (static_cast<ImU32>(48) << 24), pillRound);
|
||||
dl->AddRect(topPillMin, topPillMax,
|
||||
(topBadgeCol & 0x00FFFFFFu) | (static_cast<ImU32>(90) << 24), pillRound, 0, 1.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(shieldX, cy), shieldCol, shieldedStr);
|
||||
ImVec2(topX, cy), topBadgeCol, topBadgeStr);
|
||||
}
|
||||
// Background pill
|
||||
ImVec2 pillMin(statusTextX - Layout::spacingSm(), cy + body2->LegacySize + 1);
|
||||
|
||||
@@ -31,6 +31,31 @@ static bool endsWith(const std::string& s, const std::string& suffix) {
|
||||
return s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
|
||||
}
|
||||
|
||||
// Reject archive member names that would escape the extraction root (zip-slip).
|
||||
// The snapshot legitimately carries sub-paths (blocks/, chainstate/), so — unlike the
|
||||
// updaters, which flatten to baseName() — we keep the relative path but refuse any entry
|
||||
// that is absolute, drive/UNC-rooted, or contains a ".." component.
|
||||
static bool isSafeArchivePath(const std::string& name) {
|
||||
if (name.empty()) return false;
|
||||
std::string n = name;
|
||||
std::replace(n.begin(), n.end(), '\\', '/'); // normalize Windows separators
|
||||
if (n.front() == '/') return false; // absolute POSIX path
|
||||
const char c0 = n[0];
|
||||
if (n.size() >= 2 && n[1] == ':' &&
|
||||
((c0 >= 'A' && c0 <= 'Z') || (c0 >= 'a' && c0 <= 'z')))
|
||||
return false; // Windows drive letter (C:...)
|
||||
size_t start = 0;
|
||||
while (start <= n.size()) {
|
||||
const size_t slash = n.find('/', start);
|
||||
const std::string comp =
|
||||
n.substr(start, slash == std::string::npos ? std::string::npos : slash - start);
|
||||
if (comp == "..") return false; // path traversal component
|
||||
if (slash == std::string::npos) break;
|
||||
start = slash + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static size_t writeFileCallback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
size_t total = size * nmemb;
|
||||
FILE* fp = static_cast<FILE*>(userp);
|
||||
@@ -383,6 +408,16 @@ bool Bootstrap::extract(const std::string& zipPath, const std::string& dataDir)
|
||||
|
||||
std::string filename = stat.m_filename;
|
||||
|
||||
// *** SECURITY: reject zip-slip / path-traversal entries before building any path ***
|
||||
// A legitimate snapshot from the project host never contains these; an entry that does
|
||||
// indicates a malicious/corrupt archive, so abort rather than silently skip.
|
||||
if (!isSafeArchivePath(filename)) {
|
||||
DEBUG_LOGF("[Bootstrap] Unsafe archive path rejected: %s\n", filename.c_str());
|
||||
setProgress(State::Failed, "Refusing to extract unsafe archive entry: " + filename);
|
||||
mz_zip_reader_end(&zip);
|
||||
return false;
|
||||
}
|
||||
|
||||
// *** CRITICAL: Skip wallet.dat ***
|
||||
if (filename == "wallet.dat" || endsWith(filename, "/wallet.dat")) {
|
||||
DEBUG_LOGF("[Bootstrap] Skipping wallet.dat (protected)\n");
|
||||
|
||||
@@ -19,10 +19,19 @@ namespace util {
|
||||
|
||||
namespace {
|
||||
|
||||
// Cap for in-memory text/JSON responses (release metadata, price/candle data). Far above any
|
||||
// legitimate body, but bounds memory if a hostile/MITM'd server streams an unbounded response.
|
||||
constexpr std::size_t kMaxMetadataBytes = 16u * 1024 * 1024;
|
||||
|
||||
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
|
||||
{
|
||||
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb);
|
||||
return size * nmemb;
|
||||
auto* s = static_cast<std::string*>(userp);
|
||||
const size_t n = size * nmemb;
|
||||
// Hard cap: a chunked response omits Content-Length, so CURLOPT_MAXFILESIZE cannot catch it —
|
||||
// aborting here (short count) makes curl fail the transfer instead of exhausting memory.
|
||||
if (s->size() + n > kMaxMetadataBytes) return 0;
|
||||
s->append(static_cast<char*>(contents), n);
|
||||
return n;
|
||||
}
|
||||
|
||||
size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp)
|
||||
@@ -52,6 +61,7 @@ std::string httpGetString(const std::string& url, const char* logTag)
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast<curl_off_t>(kMaxMetadataBytes)); // reject oversized metadata
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
const CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
@@ -217,11 +217,11 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_you"] = "You";
|
||||
strings_["chat_contact_request"] = "contact request";
|
||||
strings_["chat_send_failed"] = "not sent";
|
||||
strings_["chat_new_button"] = "New conversation";
|
||||
strings_["chat_new_button"] = "New chat";
|
||||
strings_["chat_select_hint"] = "Select a conversation to view it.";
|
||||
strings_["chat_waiting_reply"] = "Waiting for this contact to reply — you can message them once they do.";
|
||||
strings_["chat_send"] = "Send";
|
||||
strings_["chat_new_title"] = "New conversation";
|
||||
strings_["chat_new_title"] = "New chat";
|
||||
strings_["chat_new_zaddr"] = "Recipient z-address";
|
||||
strings_["chat_new_message"] = "Message";
|
||||
strings_["chat_new_send"] = "Send request";
|
||||
@@ -260,6 +260,54 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_hidden_toast"] = "Conversation hidden — a new message brings it back";
|
||||
strings_["chat_pick_contact"] = "Choose from contacts\xE2\x80\xA6";
|
||||
strings_["chat_no_z_contacts"] = "No shielded-address contacts yet";
|
||||
strings_["chat_copy_address_tip"] = "Click to copy address";
|
||||
strings_["chat_verify_key"] = "Identity key \xE2\x80\x94 compare to verify";
|
||||
strings_["chat_awaiting_key"] = "Waiting for reply";
|
||||
strings_["chat_rename"] = "Rename contact";
|
||||
strings_["chat_rename_hint"] = "Contact name";
|
||||
strings_["chat_renamed"] = "Contact renamed";
|
||||
// Chat note-buffer status (status bar, while the Chat tab is active). %d are counts;
|
||||
// keep the format specifiers intact in translations.
|
||||
strings_["chat_buffer_sending_one"] = "Chat: sending %d message\xE2\x80\xA6";
|
||||
strings_["chat_buffer_sending"] = "Chat: sending %d messages\xE2\x80\xA6";
|
||||
strings_["chat_buffer_preparing"] = "Chat buffer: preparing %d/%d\xE2\x80\xA6";
|
||||
strings_["chat_buffer_loading"] = "Chat buffer: \xE2\x80\xA6";
|
||||
strings_["chat_buffer_ready"] = "Chat buffer: %d/%d ready";
|
||||
// Chat customization (gear modal + Settings → Chat & Contacts)
|
||||
strings_["chat_settings_title"] = "Chat settings";
|
||||
strings_["chat_settings_tip"] = "Chat customization";
|
||||
strings_["chat_settings_section"] = "CHAT & CONTACTS";
|
||||
strings_["chat_opt_emoji"] = "Emoji style";
|
||||
strings_["chat_emoji_mono"] = "Monochrome";
|
||||
strings_["chat_emoji_color"] = "Color";
|
||||
strings_["chat_opt_poll"] = "Message poll rate";
|
||||
strings_["chat_opt_bubble_style"] = "Bubble style";
|
||||
strings_["chat_bubble_rounded"] = "Rounded";
|
||||
strings_["chat_bubble_square"] = "Square";
|
||||
strings_["chat_bubble_minimal"] = "Minimal";
|
||||
strings_["chat_opt_bubble_accent"] = "Bubble color";
|
||||
strings_["chat_accent_theme"] = "Theme";
|
||||
strings_["chat_accent_blue"] = "Blue";
|
||||
strings_["chat_accent_green"] = "Green";
|
||||
strings_["chat_accent_purple"] = "Purple";
|
||||
strings_["chat_accent_amber"] = "Amber";
|
||||
strings_["chat_accent_pink"] = "Pink";
|
||||
strings_["chat_opt_density"] = "Message density";
|
||||
strings_["chat_density_comfortable"] = "Comfortable";
|
||||
strings_["chat_density_compact"] = "Compact";
|
||||
strings_["chat_opt_font_size"] = "Text size";
|
||||
strings_["chat_opt_timestamp"] = "Timestamps";
|
||||
strings_["chat_ts_global"] = "Follow global";
|
||||
strings_["chat_ts_24h"] = "24-hour";
|
||||
strings_["chat_ts_12h"] = "12-hour";
|
||||
strings_["chat_opt_enter_sends"] = "Enter sends message";
|
||||
strings_["chat_opt_global_clock"] = "Global clock format";
|
||||
strings_["chat_settings_done"] = "Done";
|
||||
strings_["chat_ts_global_short"] = "Global";
|
||||
strings_["chat_sec_appearance"] = "APPEARANCE";
|
||||
strings_["chat_sec_messaging"] = "MESSAGING";
|
||||
strings_["chat_today"] = "Today";
|
||||
strings_["chat_yesterday"] = "Yesterday";
|
||||
// Seed-phrase backup (full-node)
|
||||
strings_["seed_backup_button"] = "Seed phrase";
|
||||
strings_["tt_seed_backup"] = "Show and back up your wallet's 24-word recovery seed phrase";
|
||||
@@ -391,6 +439,13 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["mig_done_detail"] = "The daemon is rescanning to show your funds — this can take a few minutes. Your previous wallet was saved as a .bak in the data folder.";
|
||||
strings_["mig_done_btn"] = "Done";
|
||||
strings_["contacts_search_placeholder"] = "Search contacts...";
|
||||
strings_["contacts_settings_title"] = "Contacts settings";
|
||||
strings_["contacts_settings_tip"] = "Contacts customization";
|
||||
strings_["contacts_avatar_shape"] = "Avatar shape";
|
||||
strings_["contacts_shape_circle"] = "Circle";
|
||||
strings_["contacts_shape_square"] = "Square";
|
||||
strings_["contacts_shape_tab"] = "Left tab";
|
||||
strings_["contacts_list_scale"] = "List scale";
|
||||
strings_["contacts_search_no_match"] = "No matching contacts";
|
||||
strings_["address_book_confirm_delete"] = "Confirm delete?";
|
||||
strings_["mining"] = "Mining";
|
||||
@@ -666,6 +721,47 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["tt_idle_delay"] = "How long to wait before starting mining";
|
||||
strings_["tt_wizard"] = "Re-run the initial setup wizard\nDaemon will be restarted";
|
||||
strings_["tt_download_bootstrap"] = "Download blockchain bootstrap to speed up sync\nExisting block data will be replaced";
|
||||
// --- Full-node Node & Security tooltips ---
|
||||
strings_["tt_rpc_toggle"] = "Show or hide the read-only RPC connection details (host, port, user, password) for the daemon";
|
||||
strings_["tt_daemon_refresh"] = "Re-read the installed and bundled dragonxd version, size, and date shown above";
|
||||
// --- Lite wallet Node & Security tooltips ---
|
||||
strings_["tt_lite_lifecycle_toggle"] = "Show or hide the create / open / restore controls for managing your lite wallet file";
|
||||
strings_["tt_lite_lifecycle_op"] = "Choose whether to create a new wallet, open an existing one, or restore one from a seed phrase";
|
||||
strings_["tt_lite_wallet_path"] = "Path or name of the wallet file to open or restore into";
|
||||
strings_["tt_lite_restore_seed"] = "The 24-word recovery seed phrase to restore this wallet from; hidden as you type";
|
||||
strings_["tt_lite_restore_birthday"] = "Block height the wallet was created at; scanning starts here. Use 0 or the earliest height if unsure";
|
||||
strings_["tt_lite_restore_account"] = "HD account index to restore; leave 0 unless you used multiple accounts under this seed";
|
||||
strings_["tt_lite_restore_overwrite"] = "Replace an existing wallet file with this restore. Warning: overwrites the current wallet data";
|
||||
strings_["tt_lite_lifecycle_pass"] = "Passphrase to unlock or set on the wallet during this create / open / restore operation";
|
||||
strings_["tt_lite_lifecycle_run"] = "Run the selected create / open / restore operation with the values above";
|
||||
strings_["tt_lite_show_seed"] = "Reveal this wallet's recovery seed phrase and birthday. Anyone with the seed can spend your funds";
|
||||
strings_["tt_lite_show_keys"] = "Reveal this wallet's private spending keys. Anyone with a key can spend the funds it controls";
|
||||
strings_["tt_lite_copy"] = "Copy the revealed secret to the clipboard";
|
||||
strings_["tt_lite_save_seed_file"] = "Write the seed and birthday to an owner-only file (lite-seed-backup.txt) in the config folder";
|
||||
strings_["tt_lite_hide_wipe"] = "Hide the revealed secret and securely wipe it from memory";
|
||||
strings_["tt_lite_import_key"] = "Paste a private spending or viewing key to import; its history appears after the next sync";
|
||||
strings_["tt_lite_import_key_btn"] = "Import the entered private key into this wallet; funds and history appear after the next sync";
|
||||
strings_["tt_lite_encrypt_pass"] = "Passphrase to encrypt the wallet with. If lost, the wallet cannot be unlocked or recovered";
|
||||
strings_["tt_lite_encrypt"] = "Encrypt the wallet with the passphrase above; it locks immediately and requires the passphrase to unlock";
|
||||
strings_["tt_lite_unlock_pass"] = "Enter your passphrase to unlock the encrypted wallet";
|
||||
strings_["tt_lite_unlock"] = "Unlock the encrypted wallet using the passphrase above";
|
||||
strings_["tt_lite_lock"] = "Lock the wallet now; a passphrase is required to unlock and any chat session is torn down";
|
||||
strings_["tt_lite_decrypt_pass"] = "Enter your passphrase to remove encryption from the wallet";
|
||||
strings_["tt_lite_remove_encrypt"] = "Remove encryption and store the wallet unprotected; no passphrase will be required to open it";
|
||||
// --- Chat & Contacts tooltips ---
|
||||
strings_["tt_chat_emoji_style"] = "Render emoji in monochrome outline or full color";
|
||||
strings_["tt_chat_bubble_style"] = "Message bubble shape: rounded, square, or minimal (flat, borderless)";
|
||||
strings_["tt_chat_bubble_accent"] = "Accent color for your outgoing message bubbles (or follow the current theme)";
|
||||
strings_["tt_chat_density"] = "Spacing between messages: Comfortable adds more padding; Compact fits more on screen";
|
||||
strings_["tt_chat_font_size"] = "Scale chat message text from 0.8x to 1.5x. Affects only the Chat tab, not the rest of the app";
|
||||
strings_["tt_chat_poll_rate"] = "How often to check for new and 0-conf messages (0.5-15 s). Faster is more responsive but uses more CPU";
|
||||
strings_["tt_chat_timestamp"] = "Timestamp format for this tab only: follow the app-wide clock, or force 24-hour or 12-hour";
|
||||
strings_["tt_chat_enter_sends"] = "When on, Enter sends the message and Shift+Enter adds a newline; when off, Enter adds a newline";
|
||||
// --- Debug Options tooltips ---
|
||||
strings_["tt_screenshot_sweep"] = "Cycle every theme across every tab, saving a screenshot of each into the config screenshots folder (overwrites the last sweep)";
|
||||
strings_["tt_screenshot_sweep_full"] = "Like the theme sweep but also captures every modal / dialog / flow using temporary offline demo wallet data";
|
||||
strings_["tt_screenshot_open_dir"] = "Open the screenshots folder (under the config directory) in your file manager";
|
||||
strings_["tt_seed_demo_chat"] = "Inject sample conversations into the Chat tab so a sweep captures its UI; in-memory only, gone on restart";
|
||||
strings_["download_bootstrap"] = "Download Bootstrap";
|
||||
strings_["download"] = "Download";
|
||||
strings_["retry"] = "Retry";
|
||||
@@ -956,6 +1052,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["sent_filter"] = "Sent";
|
||||
strings_["received_filter"] = "Received";
|
||||
strings_["mined_filter"] = "Mined";
|
||||
strings_["chat_filter"] = "Chat";
|
||||
strings_["sort_date_newest"] = "Newest first";
|
||||
strings_["sort_date_oldest"] = "Oldest first";
|
||||
strings_["sort_amount_high"] = "Largest amount";
|
||||
@@ -971,6 +1068,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["address_upper"] = "ADDRESS";
|
||||
strings_["memo_upper"] = "MEMO";
|
||||
strings_["shielded_type"] = "Shielded";
|
||||
strings_["tx_chat_badge"] = "Message";
|
||||
strings_["recv_type"] = "Recv";
|
||||
strings_["sent_type"] = "Sent";
|
||||
strings_["immature_type"] = "Immature";
|
||||
@@ -1145,6 +1243,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["network"] = "Network";
|
||||
strings_["theme"] = "Theme";
|
||||
strings_["language"] = "Language";
|
||||
strings_["clock_format"] = "Clock format";
|
||||
strings_["tt_clock_format"] = "24-hour or 12-hour clock, used across the app. The Chat tab can override it in its own settings.";
|
||||
strings_["dragonx_green"] = "DragonX (Green)";
|
||||
strings_["dark"] = "Dark";
|
||||
strings_["light"] = "Light";
|
||||
@@ -1345,6 +1445,12 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["console_available_commands"] = "Available commands:";
|
||||
strings_["console_quit_note"] = "'quit'/'exit' aren't needed here — just close the window.";
|
||||
strings_["lite_console_help_passthrough"] = "Any other input runs as a lite-wallet console command.";
|
||||
strings_["lite_console_backend_commands"] = "Backend commands:";
|
||||
strings_["console_no_output"] = "(no output)";
|
||||
strings_["console_backend_unavailable"] = "No backend";
|
||||
strings_["console_last_error"] = "Last error:";
|
||||
strings_["console_not_connected_lite"] = "Error: no wallet open";
|
||||
strings_["console_stop_confirm_node"] = "'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm.";
|
||||
strings_["console_capturing_output"] = "Capturing daemon output...";
|
||||
strings_["console_clear"] = "Clear";
|
||||
strings_["console_clear_console"] = "Clear Console";
|
||||
@@ -1384,6 +1490,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["console_no_daemon"] = "No daemon";
|
||||
strings_["console_not_connected"] = "Error: Not connected to daemon";
|
||||
strings_["console_rpc_reference"] = "RPC Command Reference";
|
||||
strings_["console_backend_reference"] = "Backend Command Reference";
|
||||
strings_["console_rpc_trace"] = "RPC";
|
||||
strings_["console_app"] = "App";
|
||||
strings_["console_show_app_output"] = "Show [app] wallet log lines";
|
||||
@@ -1392,6 +1499,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["console_show_daemon_output"] = "Show daemon output";
|
||||
strings_["console_show_errors_only"] = "Show errors only";
|
||||
strings_["console_show_rpc_ref"] = "Show RPC command reference";
|
||||
strings_["console_show_backend_ref"] = "Show backend command reference";
|
||||
strings_["console_show_rpc_trace"] = "Show app RPC calls";
|
||||
strings_["console_showing_lines"] = "Showing %zu of %zu lines";
|
||||
strings_["console_starting_node"] = "Starting node...";
|
||||
@@ -1417,10 +1525,15 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["console_cat_wallet"] = "Wallet";
|
||||
strings_["console_cat_raw_transactions"] = "Raw Transactions";
|
||||
strings_["console_cat_utility"] = "Utility";
|
||||
strings_["console_cat_sync"] = "Sync";
|
||||
strings_["console_cat_send"] = "Send";
|
||||
strings_["console_cat_keys"] = "Keys & Security";
|
||||
strings_["console_cat_advanced"] = "Advanced";
|
||||
strings_["console_ref_search_hint"] = "Search by name or task\xE2\x80\xA6";
|
||||
strings_["console_ref_parameters"] = "Parameters";
|
||||
strings_["console_ref_no_params"] = "Takes no parameters.";
|
||||
strings_["console_ref_optional"] = "optional";
|
||||
strings_["console_ref_value"] = "value";
|
||||
strings_["console_ref_example"] = "Example";
|
||||
strings_["console_ref_builds"] = "Builds";
|
||||
strings_["console_ref_destructive"] = "Consequential";
|
||||
@@ -1516,6 +1629,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["market_12h"] = "12h";
|
||||
strings_["market_18h"] = "18h";
|
||||
strings_["market_24h"] = "24h";
|
||||
strings_["market_col_name"] = "Name";
|
||||
strings_["market_col_value"] = "Value";
|
||||
strings_["market_col_trend"] = "Trend";
|
||||
strings_["market_24h_volume"] = "24H VOLUME";
|
||||
strings_["market_6h"] = "6h";
|
||||
strings_["market_iv_live"] = "Live";
|
||||
@@ -1532,6 +1648,11 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["market_chart_loading"] = "Loading price history";
|
||||
strings_["market_style_line"] = "Switch to line chart";
|
||||
strings_["market_style_candle"] = "Switch to candlesticks";
|
||||
strings_["market_style_line_label"] = "Line";
|
||||
strings_["market_style_candle_label"] = "Candlestick";
|
||||
strings_["market_settings_title"] = "Market settings";
|
||||
strings_["market_settings_tip"] = "Market options";
|
||||
strings_["market_opt_chart_style"] = "Chart style";
|
||||
strings_["market_no_price"] = "No price data";
|
||||
strings_["market_now"] = "Now";
|
||||
strings_["market_pct_shielded"] = "%.0f%% Shielded";
|
||||
@@ -1579,9 +1700,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["portfolio_clear_sel"] = "Clear";
|
||||
strings_["portfolio_no_entries"] = "No custom entries yet. Add one to track a group of addresses.";
|
||||
strings_["portfolio_style_label"] = "Portfolio style";
|
||||
strings_["portfolio_style_compact"] = "Compact rows";
|
||||
strings_["portfolio_style_detailed"] = "Detailed rows";
|
||||
strings_["portfolio_style_featured"] = "Featured rows";
|
||||
strings_["portfolio_style_compact"] = "Table";
|
||||
strings_["portfolio_style_detailed"] = "Cards";
|
||||
strings_["portfolio_style_featured"] = "Spotlight";
|
||||
strings_["portfolio_edit"] = "Edit";
|
||||
strings_["portfolio_delete"] = "Delete";
|
||||
strings_["portfolio_save"] = "Save";
|
||||
@@ -1796,6 +1917,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["lite_net_show_hidden"] = "Show hidden servers";
|
||||
strings_["lite_net_hidden_section"] = "Hidden servers";
|
||||
strings_["lite_net_connected"] = "Connected";
|
||||
strings_["lite_net_connecting"] = "Connecting";
|
||||
strings_["lite_net_disconnected"] = "Not connected";
|
||||
strings_["lite_net_syncing"] = "Syncing";
|
||||
strings_["lite_net_synced"] = "Synced";
|
||||
|
||||
@@ -57,7 +57,15 @@ void Logger::write(const std::string& message)
|
||||
now.time_since_epoch()) % 1000;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S");
|
||||
// Reachable from worker/monitor threads — std::localtime shares a process-wide static tm, so use the
|
||||
// reentrant variant into a local tm (the logger's own mutex can't protect other localtime callers).
|
||||
std::tm tmv{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&tmv, &time);
|
||||
#else
|
||||
localtime_r(&time, &tmv);
|
||||
#endif
|
||||
ss << std::put_time(&tmv, "%Y-%m-%d %H:%M:%S");
|
||||
ss << '.' << std::setfill('0') << std::setw(3) << ms.count();
|
||||
ss << " | " << message;
|
||||
|
||||
|
||||
@@ -30,14 +30,6 @@ const std::vector<KnownPool>& knownPools()
|
||||
"https://pool.dragonx.is/api/stats", PoolStatsSchema::DragonXIs,
|
||||
/*miningcorePoolId=*/"", /*feePercent=*/0.0, /*official=*/true,
|
||||
},
|
||||
KnownPool{
|
||||
// The mining (stratum) host is us.dragonx.cc — pool.dragonx.cc is the
|
||||
// Cloudflare-proxied web/API host and does NOT accept stratum on :3333.
|
||||
// Stats still come from pool.dragonx.cc/api/pools (proxied HTTP is fine).
|
||||
"dragonx-cc-pplns", "pool.dragonx.cc", "us.dragonx.cc:3333", "rx/dragonx",
|
||||
"https://pool.dragonx.cc/api/pools", PoolStatsSchema::Miningcore,
|
||||
/*miningcorePoolId=*/"dragonx-pplns", /*feePercent=*/3.0, /*official=*/true,
|
||||
},
|
||||
};
|
||||
return pools;
|
||||
}
|
||||
|
||||
75
src/util/svg_texture.cpp
Normal file
75
src/util/svg_texture.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "svg_texture.h"
|
||||
#include "texture_loader.h" // CreateRawTexture
|
||||
#include "logger.h"
|
||||
|
||||
// nanosvg (memononen, zlib/public-domain, vendored in libs/nanosvg). Implementations compiled ONLY here.
|
||||
#define NANOSVG_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvg.h"
|
||||
#define NANOSVGRAST_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvgrast.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
// Perceptual brightness of a nanosvg color (byte order R,G,B,A — matches ImU32/IM_COL32).
|
||||
static inline float svgLuma(unsigned int c) {
|
||||
const float r = (c & 0xFFu) / 255.0f;
|
||||
const float g = ((c >> 8) & 0xFFu) / 255.0f;
|
||||
const float b = ((c >> 16) & 0xFFu) / 255.0f;
|
||||
return 0.299f * r + 0.587f * g + 0.114f * b;
|
||||
}
|
||||
|
||||
bool LoadTextureFromSvg(const char* svgText, int pxSize, ImU32 bodyColor, ImU32 detailColor,
|
||||
ImTextureID* outTex, int* outW, int* outH) {
|
||||
if (!svgText || pxSize <= 0 || !outTex) return false;
|
||||
|
||||
// nsvgParse mutates its input buffer — parse a copy.
|
||||
std::string buf(svgText);
|
||||
NSVGimage* img = nsvgParse(&buf[0], "px", 96.0f);
|
||||
if (!img || img->width <= 0.0f || img->height <= 0.0f) {
|
||||
if (img) nsvgDelete(img);
|
||||
DEBUG_LOGF("LoadTextureFromSvg: parse failed / empty image\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recolor by brightness: light fills -> detail, darker fills -> body. Force opaque so a theme color
|
||||
// with partial alpha can't make the mark translucent. ImU32 and nanosvg color share byte order.
|
||||
const unsigned int body = (static_cast<unsigned int>(bodyColor) & 0x00FFFFFFu) | 0xFF000000u;
|
||||
const unsigned int detail = (static_cast<unsigned int>(detailColor) & 0x00FFFFFFu) | 0xFF000000u;
|
||||
for (NSVGshape* s = img->shapes; s; s = s->next) {
|
||||
if (s->fill.type == NSVG_PAINT_COLOR)
|
||||
s->fill.color = (svgLuma(s->fill.color) > 0.6f) ? detail : body;
|
||||
if (s->stroke.type == NSVG_PAINT_COLOR)
|
||||
s->stroke.color = (svgLuma(s->stroke.color) > 0.6f) ? detail : body;
|
||||
}
|
||||
|
||||
const float scale = static_cast<float>(pxSize) / img->width;
|
||||
const int w = pxSize;
|
||||
const int h = std::max(1, static_cast<int>(img->height * scale + 0.5f));
|
||||
|
||||
std::vector<unsigned char> rgba(static_cast<size_t>(w) * h * 4, 0);
|
||||
NSVGrasterizer* rast = nsvgCreateRasterizer();
|
||||
if (!rast) { nsvgDelete(img); return false; }
|
||||
nsvgRasterize(rast, img, 0.0f, 0.0f, scale, rgba.data(), w, h, w * 4);
|
||||
nsvgDeleteRasterizer(rast);
|
||||
nsvgDelete(img);
|
||||
|
||||
if (!CreateRawTexture(rgba.data(), w, h, /*repeat=*/false, outTex)) {
|
||||
DEBUG_LOGF("LoadTextureFromSvg: CreateRawTexture failed (%dx%d)\n", w, h);
|
||||
return false;
|
||||
}
|
||||
if (outW) *outW = w;
|
||||
if (outH) *outH = h;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
24
src/util/svg_texture.h
Normal file
24
src/util/svg_texture.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Rasterize a (two-tone) SVG to a themed RGBA GPU texture via nanosvg. Used for the DragonX logo /
|
||||
// custom emoji: the darker fill becomes `bodyColor` (the theme accent), the light fill `detailColor`.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
// Rasterize `svgText` (nul-terminated SVG markup) to a texture of width `pxSize` (height follows the
|
||||
// SVG aspect). Fills are recolored by brightness — light fills -> detailColor, darker fills -> bodyColor
|
||||
// (both forced opaque). Returns false on parse/rasterize/upload failure; on success *outTex is a texture
|
||||
// the caller owns (release via util::DestroyTexture). Safe to call off the initial frame (needs a live
|
||||
// GL/DX device, like the other texture loaders).
|
||||
bool LoadTextureFromSvg(const char* svgText, int pxSize, ImU32 bodyColor, ImU32 detailColor,
|
||||
ImTextureID* outTex, int* outW, int* outH);
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
@@ -19,8 +19,38 @@ std::int64_t secondsAgo(std::int64_t timestamp)
|
||||
std::int64_t diff = now - timestamp;
|
||||
return diff < 0 ? 0 : diff;
|
||||
}
|
||||
bool g_clock12h = false; // app-wide 12-hour clock preference (synced from settings each frame)
|
||||
} // namespace
|
||||
|
||||
void setClock12h(bool enabled) { g_clock12h = enabled; }
|
||||
bool clock12h() { return g_clock12h; }
|
||||
|
||||
std::string formatClockDateTime(std::int64_t timestamp, bool withSeconds)
|
||||
{
|
||||
if (timestamp <= 0) return {};
|
||||
std::time_t t = static_cast<std::time_t>(timestamp);
|
||||
std::tm* tm = std::localtime(&t); // UI thread only
|
||||
if (!tm) return {};
|
||||
const char* fmt = g_clock12h
|
||||
? (withSeconds ? "%Y-%m-%d %I:%M:%S %p" : "%Y-%m-%d %I:%M %p")
|
||||
: (withSeconds ? "%Y-%m-%d %H:%M:%S" : "%Y-%m-%d %H:%M");
|
||||
char buf[40];
|
||||
return std::strftime(buf, sizeof(buf), fmt, tm) > 0 ? std::string(buf) : std::string();
|
||||
}
|
||||
|
||||
std::string formatClockTime(std::int64_t timestamp, bool withSeconds)
|
||||
{
|
||||
if (timestamp <= 0) return {};
|
||||
std::time_t t = static_cast<std::time_t>(timestamp);
|
||||
std::tm* tm = std::localtime(&t); // UI thread only
|
||||
if (!tm) return {};
|
||||
const char* fmt = g_clock12h
|
||||
? (withSeconds ? "%I:%M:%S %p" : "%I:%M %p")
|
||||
: (withSeconds ? "%H:%M:%S" : "%H:%M");
|
||||
char buf[24];
|
||||
return std::strftime(buf, sizeof(buf), fmt, tm) > 0 ? std::string(buf) : std::string();
|
||||
}
|
||||
|
||||
std::string formatTimeAgo(std::int64_t timestamp)
|
||||
{
|
||||
if (timestamp <= 0) return {};
|
||||
|
||||
@@ -14,6 +14,19 @@
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
// App-wide clock format (24-hour vs 12-hour). Set once per frame by the App from the global
|
||||
// `time_format` setting (setClock12h); the formatters below read it so a single preference drives
|
||||
// every user-facing timestamp. The Chat tab resolves its own override separately.
|
||||
void setClock12h(bool enabled);
|
||||
bool clock12h();
|
||||
|
||||
// Format an epoch timestamp as a local wall-clock date+time honoring the app clock setting:
|
||||
// 24h -> "YYYY-MM-DD HH:MM[:SS]", 12h -> "YYYY-MM-DD hh:MM[:SS] AM/PM". Empty when ts <= 0.
|
||||
std::string formatClockDateTime(std::int64_t timestamp, bool withSeconds = false);
|
||||
|
||||
// Just the time-of-day portion honoring the clock setting: 24h "HH:MM[:SS]" / 12h "hh:MM[:SS] AM/PM".
|
||||
std::string formatClockTime(std::int64_t timestamp, bool withSeconds = false);
|
||||
|
||||
// Localized relative time, e.g. "5 minutes ago" (via i18n). Empty when timestamp <= 0.
|
||||
std::string formatTimeAgo(std::int64_t timestamp);
|
||||
|
||||
|
||||
@@ -314,6 +314,12 @@ void XmrigUpdater::installResolved(const std::string& targetDir, const XmrigRele
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (kXmrigRequireSignature) {
|
||||
// Signatures are required but no key is pinned in this build: fail closed rather than
|
||||
// silently downgrading to checksum-only (the checksum is same-origin as the archive).
|
||||
fs::remove(zipPath, ec);
|
||||
setProgress(State::Failed, "No signing key is pinned in this build — refusing to install.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,10 +81,56 @@ LiteConnectionSettings defaultLiteConnectionSettings()
|
||||
return settings;
|
||||
}
|
||||
|
||||
// Strict dotted-decimal check for the 127.0.0.0/8 loopback block: exactly four numeric octets
|
||||
// (each 0-255) with the first equal to 127. This must NOT be a prefix match — "127.0.0.1.evil.com"
|
||||
// and "127.evil.com" are attacker-controlled DNS names that a startsWith("127.") test would wrongly
|
||||
// treat as loopback, reopening the plaintext-downgrade hole.
|
||||
static bool isNumericIpv4Loopback(const std::string& h)
|
||||
{
|
||||
int parts = 0;
|
||||
size_t start = 0;
|
||||
while (true) {
|
||||
const size_t dot = h.find('.', start);
|
||||
const std::string seg = h.substr(start, dot == std::string::npos ? std::string::npos : dot - start);
|
||||
if (seg.empty() || seg.size() > 3) return false;
|
||||
int v = 0;
|
||||
for (char c : seg) { if (c < '0' || c > '9') return false; v = v * 10 + (c - '0'); }
|
||||
if (v > 255) return false;
|
||||
if (parts == 0 && v != 127) return false; // 127.0.0.0/8 only
|
||||
++parts;
|
||||
if (dot == std::string::npos) break;
|
||||
start = dot + 1;
|
||||
}
|
||||
return parts == 4;
|
||||
}
|
||||
|
||||
static bool isLoopbackLiteHostSpec(const std::string& hostPort)
|
||||
{
|
||||
std::string h = hostPort;
|
||||
const size_t term = h.find_first_of("/?#"); // strip path/query/fragment
|
||||
if (term != std::string::npos) h = h.substr(0, term);
|
||||
const size_t at = h.rfind('@'); // strip userinfo (user:pass@host)
|
||||
if (at != std::string::npos) h = h.substr(at + 1);
|
||||
if (!h.empty() && h.front() == '[') { // [::1]:port (bracketed IPv6)
|
||||
const size_t close = h.find(']');
|
||||
h = (close == std::string::npos) ? h : h.substr(1, close - 1);
|
||||
} else { // host or host:port
|
||||
const size_t colon = h.find(':');
|
||||
if (colon != std::string::npos) h = h.substr(0, colon);
|
||||
}
|
||||
return h == "localhost" || h == "::1" || isNumericIpv4Loopback(h);
|
||||
}
|
||||
|
||||
bool isLiteServerUrlUsable(const std::string& serverUrl)
|
||||
{
|
||||
const std::string normalized = liteTrimCopy(serverUrl);
|
||||
return startsWith(normalized, "https://") || startsWith(normalized, "http://");
|
||||
if (startsWith(normalized, "https://")) return true;
|
||||
// SECURITY: plaintext http:// is a TLS downgrade for lightwalletd traffic (view keys,
|
||||
// transactions, addresses). Permit it only for loopback (a local dev lightwalletd);
|
||||
// reject remote plaintext servers instead of silently accepting the downgrade.
|
||||
if (startsWith(normalized, "http://"))
|
||||
return isLoopbackLiteHostSpec(normalized.substr(sizeof("http://") - 1));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isOfficialLiteServer(const std::string& serverUrl)
|
||||
|
||||
@@ -1066,7 +1066,16 @@ LiteEncryptionResult LiteWalletController::encryptWallet(std::string passphrase)
|
||||
}
|
||||
out = parseEncryptionOpResponse(bridge_->execute("encrypt", passphrase));
|
||||
secureWipeLiteSecret(passphrase);
|
||||
if (out.ok) bridge_->execute("save", ""); // persist the now-encrypted wallet
|
||||
if (out.ok) {
|
||||
// Persist the now-encrypted wallet. If the save fails, do NOT report success — the
|
||||
// on-disk wallet would still be unencrypted, contradicting what the user was told.
|
||||
const auto saved = bridge_->execute("save", "");
|
||||
if (!saved.ok) {
|
||||
out.ok = false;
|
||||
out.error = "wallet encrypted in memory but saving to disk failed" +
|
||||
(saved.error.empty() ? std::string() : (": " + saved.error));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1084,7 +1093,16 @@ LiteEncryptionResult LiteWalletController::decryptWallet(std::string passphrase)
|
||||
}
|
||||
out = parseEncryptionOpResponse(bridge_->execute("decrypt", passphrase));
|
||||
secureWipeLiteSecret(passphrase);
|
||||
if (out.ok) bridge_->execute("save", ""); // persist the now-unencrypted wallet
|
||||
if (out.ok) {
|
||||
// Persist the now-unencrypted wallet. If the save fails, do NOT report success — the
|
||||
// on-disk wallet would still be encrypted, contradicting what the user was told.
|
||||
const auto saved = bridge_->execute("save", "");
|
||||
if (!saved.ok) {
|
||||
out.ok = false;
|
||||
out.error = "wallet decrypted in memory but saving to disk failed" +
|
||||
(saved.error.empty() ? std::string() : (": " + saved.error));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -3434,9 +3434,11 @@ void testBalanceAddressListModel()
|
||||
EXPECT_NEAR(layout.contentStartX, 22.0, 0.0001);
|
||||
EXPECT_NEAR(layout.contentStartY, 26.0, 0.0001);
|
||||
EXPECT_NEAR(layout.buttonSize, 38.0, 0.0001);
|
||||
EXPECT_NEAR(layout.favoriteButton.x, 270.0, 0.0001);
|
||||
EXPECT_NEAR(layout.visibilityButton.x, 228.0, 0.0001);
|
||||
EXPECT_NEAR(layout.contentRight, 224.0, 0.0001);
|
||||
// Trailing (favorite) button is inset by rowPadLeft (12) so it mirrors the left margin
|
||||
// instead of hugging the card edge: 310 - 38 - 12 = 260.
|
||||
EXPECT_NEAR(layout.favoriteButton.x, 260.0, 0.0001);
|
||||
EXPECT_NEAR(layout.visibilityButton.x, 218.0, 0.0001);
|
||||
EXPECT_NEAR(layout.contentRight, 214.0, 0.0001);
|
||||
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(2.0, 3.5), std::string("$7.00"));
|
||||
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.001, 2.0), std::string("$0.002000"));
|
||||
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.0, 2.0), std::string(""));
|
||||
@@ -5300,8 +5302,11 @@ void testXmrigAssetSelection()
|
||||
EXPECT_TRUE(linux >= 0);
|
||||
EXPECT_TRUE(win >= 0);
|
||||
EXPECT_TRUE(linux != win);
|
||||
EXPECT_TRUE(rel.assets[linux].name.find("linux-x64.zip") != std::string::npos);
|
||||
EXPECT_TRUE(rel.assets[win].name.find("win-x64.zip") != std::string::npos);
|
||||
// Guard the index reads: select*Asset returns -1 when nothing matches, and EXPECT_TRUE doesn't
|
||||
// abort this harness, so an unguarded rel.assets[-1] on a fixture/parser regression would SIGSEGV
|
||||
// the whole suite instead of reporting the failed EXPECT above.
|
||||
if (linux >= 0) EXPECT_TRUE(rel.assets[linux].name.find("linux-x64.zip") != std::string::npos);
|
||||
if (win >= 0) EXPECT_TRUE(rel.assets[win].name.find("win-x64.zip") != std::string::npos);
|
||||
// No macOS build in this fixture -> graceful "not found".
|
||||
EXPECT_EQ(selectXmrigAsset(rel, "macos-x86_64"), -1);
|
||||
EXPECT_EQ(selectXmrigAsset(rel, "macos-arm64"), -1);
|
||||
@@ -5607,9 +5612,11 @@ void testDaemonAssetSelection()
|
||||
const int win = selectDaemonAsset(rel, "win64");
|
||||
EXPECT_TRUE(lin >= 0 && mac >= 0 && win >= 0);
|
||||
EXPECT_TRUE(lin != mac && mac != win && lin != win);
|
||||
EXPECT_TRUE(rel.assets[lin].name.find("linux-amd64.zip") != std::string::npos);
|
||||
EXPECT_TRUE(rel.assets[mac].name.find("macos.zip") != std::string::npos);
|
||||
EXPECT_TRUE(rel.assets[win].name.find("win64.zip") != std::string::npos);
|
||||
// Guard the index reads (select*Asset returns -1 on no match; EXPECT_TRUE doesn't abort here) so a
|
||||
// fixture/parser regression reports the failed EXPECT above instead of an OOB rel.assets[-1] crash.
|
||||
if (lin >= 0) EXPECT_TRUE(rel.assets[lin].name.find("linux-amd64.zip") != std::string::npos);
|
||||
if (mac >= 0) EXPECT_TRUE(rel.assets[mac].name.find("macos.zip") != std::string::npos);
|
||||
if (win >= 0) EXPECT_TRUE(rel.assets[win].name.find("win64.zip") != std::string::npos);
|
||||
// Wrong/foreign tokens (e.g. the miner's naming) must NOT match the daemon archives.
|
||||
EXPECT_EQ(selectDaemonAsset(rel, "linux-x64"), -1);
|
||||
EXPECT_EQ(selectDaemonAsset(rel, "linux-arm64"), -1);
|
||||
@@ -5769,23 +5776,20 @@ void testXmrigLiveInstall()
|
||||
void testPoolRegistryLookup()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
EXPECT_EQ(knownPools().size(), static_cast<std::size_t>(2));
|
||||
// pool.dragonx.cc was removed from the built-in defaults; pool.dragonx.is is the sole entry.
|
||||
EXPECT_EQ(knownPools().size(), static_cast<std::size_t>(1));
|
||||
|
||||
// The algo follows the pool. Note pool.dragonx.cc's stratum host is us.dragonx.cc
|
||||
// (the .cc domain is only the Cloudflare-proxied web/API host).
|
||||
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/dragonx"));
|
||||
// The algo follows the pool: pool.dragonx.is resolves to rx/hush regardless of the caller default.
|
||||
EXPECT_EQ(resolvePoolAlgo("pool.dragonx.is:3433", "rx/dragonx"), std::string("rx/hush"));
|
||||
// Bare host (no port) still matches; scheme + path are tolerated.
|
||||
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc", "rx/hush"), std::string("rx/dragonx"));
|
||||
EXPECT_EQ(resolvePoolAlgo("stratum+tcp://us.dragonx.cc:3333/x", "rx/hush"),
|
||||
std::string("rx/dragonx"));
|
||||
// Unknown host -> fallback algo.
|
||||
// The former us.dragonx.cc (pool.dragonx.cc) host is now unknown -> caller's fallback algo.
|
||||
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/hush"));
|
||||
EXPECT_EQ(resolvePoolAlgo("my.pool.example:1234", "rx/hush"), std::string("rx/hush"));
|
||||
|
||||
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") != nullptr);
|
||||
EXPECT_TRUE(findKnownPoolByUrl("pool.dragonx.is:3433") != nullptr);
|
||||
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") == nullptr); // removed built-in default
|
||||
EXPECT_TRUE(findKnownPoolByUrl("unknown.host:1") == nullptr);
|
||||
// A mismatched explicit port must NOT match a known pool.
|
||||
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:9999") == nullptr);
|
||||
EXPECT_TRUE(findKnownPoolByUrl("pool.dragonx.is:9999") == nullptr);
|
||||
}
|
||||
|
||||
// Schema-aware pool hashrate parsing (the two pools speak different APIs).
|
||||
@@ -6193,12 +6197,17 @@ void testHushChatOutgoing()
|
||||
|
||||
ChatService bobSvc;
|
||||
bobSvc.setIdentity(bob);
|
||||
EXPECT_EQ(bobSvc.ingest(extracted.metadata, {}, 5), 1);
|
||||
// Realistic receive-time fallback (the header "ts" is a real std::time stamp; an artificially-tiny
|
||||
// fallback would trip the future-plausibility clamp in ingest and reject the real ts).
|
||||
EXPECT_EQ(bobSvc.ingest(extracted.metadata, {}, static_cast<std::int64_t>(std::time(nullptr))), 1);
|
||||
std::vector<ChatMessage> conv = bobSvc.store().conversation(cid);
|
||||
EXPECT_EQ((int)conv.size(), 1);
|
||||
EXPECT_EQ(conv[0].body, std::string("hello bob"));
|
||||
EXPECT_EQ(conv[0].peer_public_key_hex, ra.public_key_hex); // Bob learns Alice's key
|
||||
EXPECT_EQ(conv[0].peer_zaddr, aliceZ);
|
||||
// The header now carries the sender's compose time (header "ts"); ingest prefers it over the fallback
|
||||
// (5) so both ends show the same send time. A real std::time() stamp is well past year-2001.
|
||||
EXPECT_TRUE(conv[0].timestamp > 1000000000);
|
||||
|
||||
// Plaintext contact request Alice -> Bob.
|
||||
OutgoingChatMemos creq;
|
||||
@@ -6235,7 +6244,9 @@ void testHushChatOutgoing()
|
||||
echo.peer_zaddr = aliceZ;
|
||||
echo.peer_public_key_hex = ra.public_key_hex;
|
||||
echo.body = "reply!";
|
||||
echo.timestamp = 7;
|
||||
// A reply is sent AFTER the received message; the incoming "hello bob" now carries a real compose-time
|
||||
// "ts" (~now), so stamp the echo just after it (not the old artificial 7, which would sort before it).
|
||||
echo.timestamp = static_cast<std::int64_t>(std::time(nullptr)) + 10;
|
||||
echo.txid = "out:local1";
|
||||
echo.payload_position = 0;
|
||||
EXPECT_TRUE(bobSvc.recordOutgoing(echo));
|
||||
@@ -6243,6 +6254,27 @@ void testHushChatOutgoing()
|
||||
EXPECT_EQ((int)conv2.size(), 2);
|
||||
EXPECT_TRUE(conv2.back().direction == ChatDirection::Outgoing);
|
||||
EXPECT_EQ(conv2.back().body, std::string("reply!"));
|
||||
|
||||
// Future-clock clamp: a header "ts" implausibly ahead of the receive time is rejected (falls back to
|
||||
// the receive time), so a wrong/ahead-clocked peer can't pin their messages to the bottom of a thread.
|
||||
{
|
||||
const std::int64_t nowSec = static_cast<std::int64_t>(std::time(nullptr));
|
||||
const std::int64_t futureTs = nowSec + 10 * 24 * 3600; // 10 days ahead — well past the 1h tolerance
|
||||
nlohmann::json h;
|
||||
h["h"] = 1; h["v"] = 0; h["z"] = aliceZ; h["cid"] = "conv-clamp";
|
||||
h["t"] = "Cont"; h["e"] = ""; h["p"] = ra.public_key_hex; h["ts"] = futureTs;
|
||||
HushChatTransactionInput ctx;
|
||||
ctx.txid = "txclamp";
|
||||
ctx.outputs.push_back({0, h.dump()}); // header at the lower position
|
||||
ctx.outputs.push_back({1, "please add"}); // contact-request plaintext payload (must not start with '{')
|
||||
auto exc = extractHushChatTransactionMetadata(ctx, true);
|
||||
EXPECT_EQ((int)exc.metadata.size(), 1);
|
||||
EXPECT_EQ(exc.metadata[0].sent_at, futureTs); // the future ts parses through
|
||||
bobSvc.ingest(exc.metadata, {}, nowSec); // realistic receive time
|
||||
auto cc = bobSvc.store().conversation("conv-clamp");
|
||||
EXPECT_EQ((int)cc.size(), 1);
|
||||
EXPECT_EQ(cc[0].timestamp, nowSec); // clamped to receive time, NOT the future ts
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: the transport encoding (chatSendOutputs) — header first, "utf8:" for full-node / raw for
|
||||
@@ -6357,8 +6389,11 @@ void testAddressBookScope()
|
||||
fs::create_directories(tmp);
|
||||
setenv("HOME", tmp.string().c_str(), 1);
|
||||
|
||||
// A pre-scoping addressbook.json (no "scope" field) migrates to global on load.
|
||||
fs::path cfg = tmp / ".config" / "ObsidianDragon";
|
||||
// A pre-scoping addressbook.json (no "scope" field) migrates to global on load. Write to the
|
||||
// SAME per-variant config dir AddressBook::load() reads from (Lite -> ObsidianDragonLite/),
|
||||
// resolved under the temp HOME set above. Hardcoding ".config/ObsidianDragon" made the lite-build
|
||||
// ctest write where load() never looks -> 0 entries -> the entries()[0] below segfaulted.
|
||||
fs::path cfg = dragonx::util::Platform::getConfigDir();
|
||||
fs::create_directories(cfg);
|
||||
std::ofstream(cfg / "addressbook.json")
|
||||
<< R"({"entries":[{"label":"Legacy","address":"zs1legacy","notes":""}]})";
|
||||
@@ -6366,7 +6401,9 @@ void testAddressBookScope()
|
||||
AddressBook book;
|
||||
EXPECT_TRUE(book.load());
|
||||
EXPECT_EQ(book.size(), (size_t)1);
|
||||
EXPECT_TRUE(book.entries()[0].isGlobal()); // migrated -> global
|
||||
// Guard the [0] access — EXPECT_EQ doesn't abort this harness, so an empty book here must not
|
||||
// SIGSEGV the whole suite (it would take every later test down with it).
|
||||
EXPECT_TRUE(!book.entries().empty() && book.entries()[0].isGlobal()); // migrated -> global
|
||||
}
|
||||
|
||||
// Same address may be a contact in two DIFFERENT wallets, but not twice in one; and a global
|
||||
|
||||
12
third_party/silentdragonxlite/lib/rust-toolchain.toml
vendored
Normal file
12
third_party/silentdragonxlite/lib/rust-toolchain.toml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Rust toolchain pin for the vendored SilentDragonXLite (SDXL) backend.
|
||||
#
|
||||
# The pinned librustzcash + transitive crates (notably traitobject 0.1.0) rely on
|
||||
# pre-1.70 trait-coherence rules and fail to compile on newer rustc with
|
||||
# error[E0119]: conflicting implementations of trait `Trait` for type `(dyn Send + Sync)`
|
||||
# so the backend must be built with 1.63 (the toolchain scripts/build-lite-backend-artifact.sh
|
||||
# and CLAUDE.md target). rustup auto-selects this when cargo runs in this tree, so no
|
||||
# RUSTUP_TOOLCHAIN / `cargo +1.63.0` is needed.
|
||||
#
|
||||
# Install it once with: rustup toolchain install 1.63.0
|
||||
[toolchain]
|
||||
channel = "1.63.0"
|
||||
Reference in New Issue
Block a user