Compare commits
63
Commits
ac49f44f84
...
v2.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46189da0da | ||
|
|
bc6c1de7d6 | ||
|
|
ec88dac459 | ||
|
|
d058e5fc2a | ||
|
|
86b9d2e00d | ||
|
|
0056c46929 | ||
|
|
1151141271 | ||
|
|
fae8b20fb7 | ||
|
|
cc2d383166 | ||
|
|
f30fdc5ed1 | ||
|
|
290d3c31a2 | ||
|
|
27fb83ba04 | ||
|
|
0e78fe02e4 | ||
|
|
7914e9e9bb | ||
|
|
06d34dff5e | ||
|
|
c7e48c16f8 | ||
|
|
4ee0f524d4 | ||
|
|
43c7be55c6 | ||
|
|
d9fa00bb38 | ||
|
|
0a042df8e0 | ||
|
|
a60a2f8e39 | ||
|
|
d27f387d6d | ||
|
|
ed675f90d8 | ||
|
|
4492aa3425 | ||
|
|
56d93b6128 | ||
|
|
398fb274fa | ||
|
|
0343d48c13 | ||
|
|
a3892c0fd3 | ||
|
|
f7df315695 | ||
|
|
aec996a9ce | ||
|
|
90e02b1ddd | ||
|
|
ef8ceeaf9a | ||
|
|
ba1d760bb3 | ||
|
|
0942691eb3 | ||
|
|
a2f84be2d4 | ||
|
|
7e8b99a82b | ||
|
|
29274c2f48 | ||
|
|
870793433b | ||
|
|
08cfeb0e08 | ||
|
|
5daf2d83b6 | ||
|
|
6d26ccd0ed | ||
|
|
a7514becbc | ||
|
|
558cfcbe56 | ||
|
|
d0bd55b9c1 | ||
|
|
b37d3d97b6 | ||
|
|
8778398d31 | ||
|
|
d5c30237d2 | ||
|
|
a598217975 | ||
|
|
9205addf55 | ||
|
|
ce8c7696d4 | ||
|
|
99d1e73676 | ||
|
|
755cf22ad0 | ||
|
|
e24ca015d1 | ||
|
|
0de44569b7 | ||
|
|
06afbee4f8 | ||
|
|
6ee81a5abe | ||
|
|
ea26c0cbbb | ||
|
|
13b225d8f5 | ||
|
|
08aed34bbe | ||
|
|
5edbe8a276 | ||
|
|
001e85ac1a | ||
|
|
393f3d147e | ||
|
|
c252e60f7f |
+9
-5
@@ -15,7 +15,7 @@ if(APPLE)
|
||||
endif()
|
||||
|
||||
project(ObsidianDragon
|
||||
VERSION 2.0.1
|
||||
VERSION 2.0.2
|
||||
LANGUAGES C CXX
|
||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||
)
|
||||
@@ -26,7 +26,7 @@ set(DRAGONX_VERSION_SUFFIX "")
|
||||
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
|
||||
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
|
||||
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
|
||||
set(DRAGONX_LITE_VERSION "1.0.0")
|
||||
set(DRAGONX_LITE_VERSION "1.1.1")
|
||||
set(DRAGONX_LITE_VERSION_SUFFIX "")
|
||||
|
||||
# C++17 standard
|
||||
@@ -213,7 +213,7 @@ include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
json
|
||||
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
||||
GIT_TAG v3.11.3
|
||||
GIT_TAG 9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 # v3.11.3 — pinned to immutable commit (L-08); tags are mutable
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
@@ -222,7 +222,7 @@ FetchContent_MakeAvailable(json)
|
||||
FetchContent_Declare(
|
||||
tomlplusplus
|
||||
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
|
||||
GIT_TAG v3.4.0
|
||||
GIT_TAG 30172438cee64926dc41fdd9c11fb3ba5b2ba9de # v3.4.0 — pinned to immutable commit (L-08); tags are mutable
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(tomlplusplus)
|
||||
@@ -289,7 +289,7 @@ message(STATUS "Fetching libwebp (decode-only, static)...")
|
||||
FetchContent_Declare(
|
||||
libwebp
|
||||
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
|
||||
GIT_TAG v1.4.0
|
||||
GIT_TAG 845d5476a866141ba35ac133f856fa62f0b7445f # v1.4.0 — pinned to immutable commit (L-08); tags are mutable
|
||||
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
|
||||
@@ -521,6 +521,8 @@ set(APP_SOURCES
|
||||
src/ui/windows/settings_window.cpp
|
||||
src/ui/pages/settings_page.cpp
|
||||
src/ui/windows/about_dialog.cpp
|
||||
src/ui/windows/faq_dialog.cpp
|
||||
src/ui/windows/faq_content.cpp
|
||||
src/ui/windows/key_export_dialog.cpp
|
||||
src/ui/windows/transaction_details_dialog.cpp
|
||||
src/ui/windows/qr_popup_dialog.cpp
|
||||
@@ -657,6 +659,8 @@ set(APP_HEADERS
|
||||
src/ui/windows/console_tab_helpers.h
|
||||
src/ui/windows/settings_window.h
|
||||
src/ui/windows/about_dialog.h
|
||||
src/ui/windows/faq_dialog.h
|
||||
src/ui/windows/faq_content.h
|
||||
src/ui/windows/key_export_dialog.h
|
||||
src/ui/windows/transaction_details_dialog.h
|
||||
src/ui/windows/qr_popup_dialog.h
|
||||
|
||||
@@ -195,6 +195,39 @@ should_bundle_full_node_assets() {
|
||||
! $DO_LITE
|
||||
}
|
||||
|
||||
# The offline wallet-rebuild helper is the ONLY thing that repairs a genuinely BDB-inconsistent
|
||||
# wallet.dat — plain "Restore" just re-triggers the daemon's salvage cascade. A full-node release must
|
||||
# NEVER ship without it (the in-app "Repair automatically" option silently disappears otherwise), so
|
||||
# treat a missing helper as a HARD build failure instead of degrading recovery to Restore-only.
|
||||
# $1 = built helper path (e.g. bin/dragonx-wallet-rebuild[.exe]); $2 = the BDB depends dir for the hint.
|
||||
require_wallet_rebuild_helper() {
|
||||
local helper="$1" depends="$2"
|
||||
should_bundle_full_node_assets || return 0 # lite builds have no BDB wallet.dat to rebuild
|
||||
if [[ ! -f "$helper" ]]; then
|
||||
err "wallet-rebuild helper was NOT built: $helper"
|
||||
err " → the recovery 'Repair automatically' option would be MISSING from this release."
|
||||
err " Cause: the vendored Berkeley DB depends are absent, so CMake skipped the dragonx-wallet-rebuild target."
|
||||
err " Fix: provide ${depends}/{lib/libdb-6.2.a,include/db.h} (same static libdb the daemon links), then rebuild."
|
||||
exit 1
|
||||
fi
|
||||
info " wallet-rebuild helper present: $helper"
|
||||
}
|
||||
|
||||
# Vendored Berkeley DB depends directory for the macOS wallet-rebuild helper, by TARGET arch. The daemon
|
||||
# links a static libdb-6.2; the helper must match the build arch, so derive the triple from it rather than
|
||||
# hardcoding one. Single-arch: x86_64 -> x86_64-apple-darwin, arm64 -> aarch64-apple-darwin. "universal"
|
||||
# (or anything unexpected) falls back to the host arch — a universal .app needs a universal libdb, so if
|
||||
# only a single-arch libdb is vendored, build single-arch via DRAGONX_MAC_ARCHS.
|
||||
mac_bdb_dir() {
|
||||
local arch="$1" triple
|
||||
case "$arch" in
|
||||
x86_64) triple="x86_64-apple-darwin" ;;
|
||||
arm64) triple="aarch64-apple-darwin" ;;
|
||||
*) [[ "$(uname -m)" == "arm64" ]] && triple="aarch64-apple-darwin" || triple="x86_64-apple-darwin" ;;
|
||||
esac
|
||||
printf '%s/external/dragonx/depends/%s\n' "$SCRIPT_DIR" "$triple"
|
||||
}
|
||||
|
||||
# ── Helper: find resource files ──────────────────────────────────────────────
|
||||
find_sapling_params() {
|
||||
local dirs=(
|
||||
@@ -286,13 +319,8 @@ bundle_linux_daemon() {
|
||||
# asmap.dat
|
||||
find_asmap && cp "$ASMAP_DAT" "$dest/asmap.dat" && info " Bundled asmap.dat"
|
||||
|
||||
# dragonx-wallet-rebuild helper (offline recovery for a BDB-inconsistent wallet.dat)
|
||||
for p in "$SCRIPT_DIR/build/linux/bin/dragonx-wallet-rebuild" "$SCRIPT_DIR/../dragonx-wallet-rebuild"; do
|
||||
if [[ -f "$p" ]]; then
|
||||
cp "$p" "$dest/dragonx-wallet-rebuild"; chmod +x "$dest/dragonx-wallet-rebuild"
|
||||
info " Bundled dragonx-wallet-rebuild"; break
|
||||
fi
|
||||
done
|
||||
# (The dragonx-wallet-rebuild recovery helper is built into bin/ by CMake and packaged explicitly
|
||||
# by each release path — required via require_wallet_rebuild_helper — so it is not copied here.)
|
||||
|
||||
return $found
|
||||
}
|
||||
@@ -351,6 +379,10 @@ build_release_linux() {
|
||||
local BDB_ARGS=()
|
||||
if [[ -f "$lin_bdb/lib/libdb-6.2.a" && -f "$lin_bdb/include/db.h" ]]; then
|
||||
BDB_ARGS=( -DBDB_INCLUDE_DIR="$lin_bdb/include" -DBDB_LIBRARY="$lin_bdb/lib/libdb-6.2.a" )
|
||||
elif should_bundle_full_node_assets; then
|
||||
err "Vendored Berkeley DB depends missing at $lin_bdb — the wallet-rebuild recovery helper cannot be built."
|
||||
err " A full-node release must ship it; aborting rather than degrading recovery to Restore-only."
|
||||
exit 1
|
||||
fi
|
||||
info "Configuring ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
@@ -365,6 +397,9 @@ build_release_linux() {
|
||||
|
||||
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
|
||||
|
||||
# A full-node release MUST include the recovery helper — fail loudly, never ship without it.
|
||||
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild" "$lin_bdb"
|
||||
|
||||
info "Stripping ..."
|
||||
strip "bin/${APP_BASENAME}"
|
||||
[[ -f "bin/dragonx-wallet-rebuild" ]] && strip "bin/dragonx-wallet-rebuild"
|
||||
@@ -402,6 +437,9 @@ build_release_linux() {
|
||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
|
||||
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
||||
# Offline wallet-rebuild recovery helper — required (asserted above); ships next to the app.
|
||||
cp bin/dragonx-wallet-rebuild "$dist_dir/" && chmod +x "$dist_dir/dragonx-wallet-rebuild"
|
||||
info " Bundled dragonx-wallet-rebuild"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
@@ -435,6 +473,8 @@ build_release_linux() {
|
||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
|
||||
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
||||
# Offline wallet-rebuild recovery helper — required (asserted above); ships next to the app.
|
||||
cp bin/dragonx-wallet-rebuild "$APPDIR/usr/bin/" && chmod +x "$APPDIR/usr/bin/dragonx-wallet-rebuild"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
@@ -656,6 +696,30 @@ HDR
|
||||
info "Lite mode: skipping embedded daemon binaries"
|
||||
fi
|
||||
|
||||
# ── Wallet-rebuild recovery helper ───────────────────────────────
|
||||
# Built in-tree (not a prebuilt like the daemon), so compile it standalone HERE — before the
|
||||
# main app compiles embedded_resources.cpp — and INCBIN it, so a bare, self-extracting
|
||||
# ObsidianDragon.exe carries the recovery tool exactly like it does the daemon.
|
||||
if should_bundle_full_node_assets; then
|
||||
local WBDB="$SCRIPT_DIR/external/dragonx/depends/x86_64-w64-mingw32"
|
||||
if [[ -f "$WBDB/lib/libdb-6.2.a" && -f "$WBDB/include/db.h" ]]; then
|
||||
info "Compiling + embedding wallet-rebuild helper ..."
|
||||
x86_64-w64-mingw32-g++ -std=c++17 -O2 -static -static-libgcc -static-libstdc++ \
|
||||
-I"$SCRIPT_DIR/src" -I"$WBDB/include" \
|
||||
"$SCRIPT_DIR/tools/wallet_rebuild/main.cpp" \
|
||||
"$WBDB/lib/libdb-6.2.a" -lws2_32 \
|
||||
-o "$RES/dragonx-wallet-rebuild.exe" \
|
||||
|| { err "wallet-rebuild helper failed to compile for embedding"; exit 1; }
|
||||
x86_64-w64-mingw32-strip "$RES/dragonx-wallet-rebuild.exe" 2>/dev/null || true
|
||||
echo -e "\n#define HAS_EMBEDDED_WALLET_REBUILD 1" >> "$GEN/embedded_data.h"
|
||||
echo "INCBIN(dragonx_wallet_rebuild_exe, \"$RES/dragonx-wallet-rebuild.exe\");" >> "$GEN/embedded_data.h"
|
||||
info " Embedded dragonx-wallet-rebuild.exe ($(du -h "$RES/dragonx-wallet-rebuild.exe" | cut -f1))"
|
||||
else
|
||||
err "Vendored mingw Berkeley DB missing at $WBDB — cannot embed the wallet-rebuild recovery helper."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 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
|
||||
@@ -775,6 +839,10 @@ HDR
|
||||
local BDB_ARGS=()
|
||||
if [[ -f "$win_bdb/lib/libdb-6.2.a" && -f "$win_bdb/include/db.h" ]]; then
|
||||
BDB_ARGS=( -DBDB_INCLUDE_DIR="$win_bdb/include" -DBDB_LIBRARY="$win_bdb/lib/libdb-6.2.a" )
|
||||
elif should_bundle_full_node_assets; then
|
||||
err "Vendored Berkeley DB depends missing at $win_bdb — the wallet-rebuild recovery helper cannot be built."
|
||||
err " A full-node release must ship it; aborting rather than degrading recovery to Restore-only."
|
||||
exit 1
|
||||
fi
|
||||
info "Configuring (cross-compile) ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
@@ -789,8 +857,16 @@ HDR
|
||||
cmake --build . -j "$JOBS"
|
||||
|
||||
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
|
||||
# Strip the app exe — the Linux and macOS release paths already strip theirs, and even the Windows
|
||||
# helper (dragonx-wallet-rebuild.exe) is stripped, but the main app exe was shipping unstripped
|
||||
# (~5MB of symbols on the full node, ~19MB on the params-heavy lite build). Best-effort.
|
||||
x86_64-w64-mingw32-strip --strip-all "bin/${APP_BASENAME}.exe" 2>/dev/null \
|
||||
|| warn " strip unavailable — shipping unstripped ${APP_BASENAME}.exe"
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
|
||||
|
||||
# A full-node release MUST include the recovery helper — fail loudly, never ship without it.
|
||||
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild.exe" "$win_bdb"
|
||||
|
||||
# ── Package: release/windows/ ────────────────────────────────────────────
|
||||
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
|
||||
mkdir -p "$out"
|
||||
@@ -806,8 +882,9 @@ HDR
|
||||
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
||||
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
||||
done
|
||||
# dragonx-wallet-rebuild helper (offline recovery for a BDB-inconsistent wallet.dat)
|
||||
[[ -f "bin/dragonx-wallet-rebuild.exe" ]] && { cp "bin/dragonx-wallet-rebuild.exe" "$dist_dir/"; info " Bundled dragonx-wallet-rebuild.exe"; }
|
||||
# dragonx-wallet-rebuild helper (offline recovery for a BDB-inconsistent wallet.dat) — required.
|
||||
cp "bin/dragonx-wallet-rebuild.exe" "$dist_dir/" && info " Bundled dragonx-wallet-rebuild.exe"
|
||||
[[ -f "$dist_dir/dragonx-wallet-rebuild.exe" ]] || { err "Failed to bundle dragonx-wallet-rebuild.exe"; exit 1; }
|
||||
|
||||
# Bundle Sapling params + asmap for the zip distribution
|
||||
# (The single-file exe has these embedded via INCBIN, but the zip
|
||||
@@ -1021,6 +1098,12 @@ TOOLCHAIN
|
||||
fi
|
||||
|
||||
info "Configuring (cross-compile via osxcross) ..."
|
||||
# Vendored static libdb for the recovery helper (full-node only), by target arch; see mac_bdb_dir.
|
||||
local mac_bdb; mac_bdb="$(mac_bdb_dir "$MAC_ARCH")"
|
||||
local BDB_ARGS=()
|
||||
if should_bundle_full_node_assets && [[ -f "$mac_bdb/lib/libdb-6.2.a" && -f "$mac_bdb/include/db.h" ]]; then
|
||||
BDB_ARGS=( -DBDB_INCLUDE_DIR="$mac_bdb/include" -DBDB_LIBRARY="$mac_bdb/lib/libdb-6.2.a" )
|
||||
fi
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$bd/osxcross-toolchain.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
@@ -1028,6 +1111,7 @@ TOOLCHAIN
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
|
||||
"${BDB_ARGS[@]}" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
else
|
||||
# Build libsodium as universal if needed
|
||||
@@ -1049,12 +1133,19 @@ TOOLCHAIN
|
||||
fi
|
||||
|
||||
info "Configuring (native macOS, arch: $MAC_ARCHS) ..."
|
||||
# Point CMake at the vendored static libdb for the recovery helper (full-node only); see mac_bdb_dir.
|
||||
local mac_bdb; mac_bdb="$(mac_bdb_dir "$MAC_ARCH")"
|
||||
local BDB_ARGS=()
|
||||
if should_bundle_full_node_assets && [[ -f "$mac_bdb/lib/libdb-6.2.a" && -f "$mac_bdb/include/db.h" ]]; then
|
||||
BDB_ARGS=( -DBDB_INCLUDE_DIR="$mac_bdb/include" -DBDB_LIBRARY="$mac_bdb/lib/libdb-6.2.a" )
|
||||
fi
|
||||
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="$MAC_ARCHS" \
|
||||
"${BDB_ARGS[@]}" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
fi
|
||||
|
||||
@@ -1063,6 +1154,11 @@ TOOLCHAIN
|
||||
|
||||
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
|
||||
|
||||
# A full-node release MUST include the recovery helper. macOS needs a static libdb-6.2 (Homebrew
|
||||
# berkeley-db for a native build, or a vendored external/dragonx/depends/<triple>) — otherwise CMake
|
||||
# skips the target and this fails loudly rather than shipping a mac release with no recovery option.
|
||||
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild" "$(mac_bdb_dir "$MAC_ARCH")"
|
||||
|
||||
# Strip — use osxcross strip for cross-builds
|
||||
if $IS_CROSS; then
|
||||
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
|
||||
@@ -1135,6 +1231,8 @@ TOOLCHAIN
|
||||
else
|
||||
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
|
||||
fi
|
||||
# Offline wallet-rebuild recovery helper — required (asserted after build); next to the daemon.
|
||||
cp "bin/dragonx-wallet-rebuild" "$MACOS/" && chmod +x "$MACOS/dragonx-wallet-rebuild" && info " Bundled dragonx-wallet-rebuild"
|
||||
else
|
||||
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
**DragonX (DRGX) Lite Desktop Wallet — v1.0.0**
|
||||
|
||||
ObsidianDragonLite is a lightweight companion to the ObsidianDragon full-node wallet. It skips the embedded full node entirely — **no multi-gigabyte blockchain download and near-instant startup** — by connecting to a DragonX lite-wallet (lightwalletd-style) server, while keeping the same native ImGui interface and shielded-first workflow.
|
||||
|
||||
This is the **first release** of the Lite variant.
|
||||
|
||||
---
|
||||
|
||||
## Why Lite?
|
||||
|
||||
- **No blockchain to download.** Sync in seconds instead of hours; a few MB of state instead of many GB.
|
||||
- **Same wallet, lighter footprint.** The familiar ObsidianDragon UI, shielded and transparent addresses, and address book — without running a node or miner.
|
||||
- **Portable.** A single self-contained binary that stores its data in its own `ObsidianDragonLite` folder, so it coexists cleanly alongside the full-node wallet.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Fast, node-free operation** — connect to a DragonX lite-wallet server and sync in seconds; minimal disk and RAM.
|
||||
- **Wallet lifecycle** — create a new wallet, restore from a seed phrase, or open an existing one; wallets auto-open on startup, with a first-run welcome prompt and **guided seed backup** on creation.
|
||||
- **Shielded + transparent** — send, shield, and receive DRGX; per-address balances computed from unspent notes and UTXOs.
|
||||
- **Keys & seed** — export your seed phrase and keys, and import keys, from Settings.
|
||||
- **Passphrase encryption** — encrypt, unlock, lock, and decrypt the wallet; you're prompted to unlock at send time and on startup when the wallet is locked.
|
||||
- **Encrypted messaging (HushChat)** — built-in **Contacts** and **Chat** with a seed-derived, **SilentDragonXLite-compatible** identity and a seed-encrypted local message store, so you can message other DragonX users end-to-end.
|
||||
- **Server management** — a built-in **server browser with automatic failover**; switch servers with live reconnect/recovery, and a **"Redownload blocks"** action to rescan from the server.
|
||||
- **Status & diagnostics** — a **Network tab** showing connection and sync status, plus an **interactive Console** for running backend commands and copyable error output.
|
||||
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
|
||||
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (DMG + .app), x86-64.
|
||||
- QR code generation for receiving addresses
|
||||
- Extensive theme and appearance options
|
||||
|
||||
---
|
||||
|
||||
## How it differs from the full-node wallet
|
||||
|
||||
- **No embedded `dragonxd`, no mining, no local blockchain explorer.** Chain data comes from the lite-wallet server rather than being verified locally by a full node — a lighter footprint in exchange for trusting the server for chain state.
|
||||
- Everything key-related stays **on your device**: seed, keys, and (when enabled) the encryption passphrase never leave the machine.
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragonLite-1.0.0.AppImage | `a1459d6081124a6b8df47aa898b60c0237875e8cbb132e4fd8ae637673055ed4` |
|
||||
| ObsidianDragonLite-1.0.0-Linux-x64.zip | `573af502a6372334572e4dc88bd26e0cf36ed543b9df1e835fd8e4abd295108d` |
|
||||
| ObsidianDragonLite-1.0.0.exe | `28659efb770fa573bd1b1d0fcab1c1a3f9c7e46574185c3916cc9f669684852c` |
|
||||
| ObsidianDragonLite-1.0.0-Windows-x64.zip | `f6434a931e873e5936ebe249b044d6c7a413694767c667374c91ee438259b50c` |
|
||||
| ObsidianDragonLite-1.0.0-macOS-x86_64.dmg | `10f976f1453e6b1978cb7a85026fd033c85a10c1e66f436475aa628379a298c7` |
|
||||
| ObsidianDragonLite-1.0.0-macOS-x86_64.app.zip | `3e7259c363f7be2e9027c5282c94b742f4be280c998203752dc0a0551f6ab68b` |
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
|
||||
- **Windows:** x86-64, Windows 10 or later
|
||||
- **macOS:** x86-64 (Intel; runs on Apple Silicon via Rosetta 2), macOS 10.15 Catalina or later
|
||||
- Network access to a DragonX lite-wallet server.
|
||||
|
||||
## License
|
||||
|
||||
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.
|
||||
@@ -0,0 +1,43 @@
|
||||
**DragonX (DRGX) Lite Desktop Wallet — v1.1.1**
|
||||
|
||||
ObsidianDragonLite is the lightweight, node-free companion to ObsidianDragon — it connects to a DragonX lite-wallet server for near-instant startup, with the same native ImGui interface and shielded-first workflow. **v1.1.1 is a maintenance release** centered on an important wallet-encryption fix.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Security fix — please read
|
||||
|
||||
**Creating or restoring a Lite wallet with a passphrase now actually encrypts it.**
|
||||
|
||||
In prior versions, the create / restore / open flow used by the Settings UI **silently discarded the passphrase you entered**:
|
||||
|
||||
- A **newly created or restored** wallet was saved to disk **unencrypted** — the seed and spending keys were stored in the clear even though you set a passphrase.
|
||||
- **Opening** an already-encrypted wallet did not unlock it with the supplied passphrase (it opened locked and re-prompted).
|
||||
|
||||
v1.1.1 fixes this: create/restore now **encrypt** the wallet with your passphrase (the backend encrypts and locks it), and open now **unlocks** it.
|
||||
|
||||
**What you should do:** if you created or restored a Lite wallet **with a passphrase** on an earlier version, treat that wallet's on-disk data as unencrypted. Open it in v1.1.1 and re-apply encryption from **Settings → encrypt** (and, if you're concerned the plaintext wallet file may have been exposed, move the funds to a freshly created encrypted wallet). Wallets you never set a passphrase on are unaffected.
|
||||
|
||||
---
|
||||
|
||||
## Also fixed
|
||||
|
||||
- **Chinese, Japanese, and Korean FAQ text renders correctly.** Several Help/FAQ titles showed `??` instead of characters (e.g. 什么, 门) after the previous translation back-fill; the bundled CJK font subset now includes those glyphs.
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
macOS builds are x86-64 (Intel; run on Apple Silicon via Rosetta 2).
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragonLite-1.1.1.AppImage | `323ff5f8658a5550aa12678ce4be37e32a2bfd3f3b0543074cc86a4267895dcf` |
|
||||
| ObsidianDragonLite-1.1.1-Linux-x64.zip | `e8604e479aed7e020946df45bf0d9373401354907e10565011694efa9978e631` |
|
||||
| ObsidianDragonLite-1.1.1.exe | `4cdcf291fe43c4fed04258aa55bb4e9b591f52780f9db5fb8f8bef522c43a351` |
|
||||
| ObsidianDragonLite-1.1.1-Windows-x64.zip | `c387bfcda508f32195c41fabec9f23d8b094d693a5dba41fbf8ec4abab6f2f6e` |
|
||||
| ObsidianDragonLite-1.1.1-macOS-x86_64.dmg | `bb0c491cb8f90ae55d162ec7c04c8f30988fa2f79c7911471220cddab44f99a5` |
|
||||
| ObsidianDragonLite-1.1.1-macOS-x86_64.app.zip | `a2a75fe40bf1f4db3abed8206dcc40d9fc74d8db2df7704afc9c834106b1c247` |
|
||||
|
||||
---
|
||||
|
||||
For the full feature set (node-free operation, wallet lifecycle, shielded + transparent send/shield/receive, HushChat, server browser with failover, 8-language UI), see the **lite-v1.0.0** release notes — v1.1.1 changes only the items listed above.
|
||||
@@ -0,0 +1,86 @@
|
||||
**DragonX (DRGX) Full-Node Desktop Wallet — v2.0.0**
|
||||
|
||||
This is ObsidianDragon, a native ImGui-based wallet for the DragonX network. It ships with an embedded full-node daemon (`dragonxd`) and an integrated CPU miner (DRG-XMRig), giving users a complete, self-contained experience on Linux, Windows, and macOS.
|
||||
|
||||
---
|
||||
|
||||
## What's New in v2.0.0
|
||||
|
||||
v2.0.0 is a major release. It adds **end-to-end encrypted HushChat messaging** and **BIP39 seed-phrase wallets** (with migrate-to-seed for legacy wallets), lets the wallet **update its own node and miner** with each install cryptographically verified, and ships a **security-hardening pass** across the updater, RPC, and secret-storage surfaces — plus full support for the **new multi-threaded `dragonxd`** and a large stability and UX overhaul.
|
||||
|
||||
### New Features
|
||||
|
||||
- **Encrypted messaging (HushChat)** — new **Contacts** and **Chat** tabs bring DRGX-native, end-to-end encrypted messaging. Your chat identity is derived from your wallet seed, messages are encrypted with libsodium (XChaCha20-Poly1305 / secretstream) and stored in a **seed-encrypted local database**, and the wire format is interoperable with SilentDragonX / SilentDragonXLite.
|
||||
- **BIP39 seed-phrase wallets & migrate-to-seed** — new wallets are backed by a mnemonic **seed phrase** you can back up from Settings. Existing **legacy wallets can be migrated into a seed wallet** — the wallet mints a new mnemonic wallet, sweeps your funds to it, and adopts it only once the sweep is mined, keeping a timestamped `wallet.dat` backup throughout. (The bundled `dragonxd` supports the required mnemonic RPCs; older nodes degrade gracefully.)
|
||||
- **In-app daemon updater** — Settings → **Node & Security → Daemon binary → "Check for updates…"** downloads the latest `dragonxd` from the project Gitea, verifies its **SHA-256 and a detached ed25519 signature** before installing, and replaces the binary **atomically while the node keeps running** (the new build takes effect on the next daemon start). A two-pane version picker lets you pin, downgrade, or install any published/pre-release build.
|
||||
- **In-app miner updater** — an **"Update miner…"** action in the Mining tab fetches, verifies (SHA-256 + ed25519), and installs the latest DRG-XMRig, with the same version picker and a display of current vs. latest.
|
||||
- **Daemon binary management** — the wallet no longer auto-overwrites a node you've dropped in; a dedicated Settings panel shows the managed binary and gathers all node actions (restart, rescan, repair) onto one toolbar.
|
||||
- **Repair Wallet** — a one-click `-zapwallettxes=2` recovery in Settings, plus automatic wallet reconciliation after a bootstrap and runtime rescan support for pruned nodes.
|
||||
- **Explorer search** — fuzzy, live (debounced) filtering of the block list by partial hash or height.
|
||||
- **History sorting & fast load** — sort transactions by date or amount, a "Loading older history (N%)" indicator during the initial bulk load, and persisted history that surfaces pending sends immediately.
|
||||
- **Smarter mining** — the thread benchmark now measures **sustained (thermally-throttled) hashrate** instead of an inflated initial burst, with GPU-aware idle mining and corrected idle thread scaling.
|
||||
|
||||
### Security & Integrity
|
||||
|
||||
- **Mandatory signature verification** — both the daemon and miner updaters **require** a valid ed25519 signature (checked against a key pinned in the wallet) in addition to the SHA-256; an install is refused if the signature is missing or invalid.
|
||||
- **Hardened secret handling** — RPC credentials are wiped from memory after use, RPC responses are size-capped and scrubbed of secrets in logs, generated node config is written **owner-only (0600)**, and secrets copied to the clipboard **auto-clear**.
|
||||
- **Safe on-disk writes** — settings, address book, and secret files are written **atomically with owner-only permissions**; the PIN vault fsyncs its secure-delete overwrite; SQLite cache growth is bounded.
|
||||
- **Recipient validation** — sends validate recipient address checksums (Base58Check + Bech32) before building a transaction.
|
||||
- **Path-traversal protection** — archive extraction (chain bootstrap and the updaters) rejects any entry that would escape the target directory (zip-slip), and the bootstrap fails closed on a missing checksum rather than trusting an unverified archive.
|
||||
- **Robustness** — malformed RPC error JSON is guarded and sends are single-flight, preventing duplicate/ill-formed submissions.
|
||||
|
||||
### Improvements
|
||||
|
||||
- **New multi-threaded daemon support** — live Sapling note-witness rebuild progress, accurate sync-speed display, reliable rescan-completion detection, and RPC polling throttled during sync so block download isn't slowed.
|
||||
- **Networking** — DragonX DNS seed nodes, `-maxconnections` passed to the daemon, and a peer count that stays current on every tab.
|
||||
- **Node resilience** — non-blocking warmup so you can connect while the daemon is still initializing, a live daemon-console tail on the startup overlay, fast-failing connect probes, and mid-session disconnect detection that keeps the UI responsive (in-flight RPC calls abort cleanly on disconnect/shutdown).
|
||||
- **Address list** — modernized with drag-to-transfer, labels, and view-only handling.
|
||||
- **UI / DPI** — overlay dialogs scale correctly with the font/DPI setting, improved CJK font rendering, native language names in the language picker, and format-incompatible translations are rejected.
|
||||
- **Settings** — an "Open data folder" button and confirmation modals for rescan and restart-daemon.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
Extensive fixes across history (shielded-tx ordering, stuck "refreshing history" banners, unconfirmed-badge stickiness), sends (correct fee passed to `z_sendmany`, fast-lane worker restart on reconnect, note-selection fee-gap workaround), rescan (accurate completion detection, no false "complete", no per-second error flood), RPC (mid-session disconnect handling, stale-refresh invalidation), and UI layout/i18n. See the commit history for the full list.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Full-node wallet** — send, receive, and verify DRGX transactions with a bundled `dragonxd` daemon; no external setup required.
|
||||
- **Self-updating** — verify-and-install the latest node and miner from within the app (SHA-256 + ed25519 signature enforced).
|
||||
- **Encrypted messaging** — built-in HushChat with a seed-derived identity, end-to-end encryption, and a seed-encrypted local message store, interoperable with SilentDragonX / SilentDragonXLite.
|
||||
- **Seed-phrase wallets** — BIP39 mnemonic backup, and migrate-to-seed to upgrade a legacy wallet into a seed wallet.
|
||||
- **Built-in CPU mining** — start/stop DRG-XMRig from the Mining tab with real-time hashrate and pool statistics, mine-when-idle support with configurable delay, and sustained-hashrate benchmarking.
|
||||
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
|
||||
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (**universal** DMG + .app); Linux/Windows x86-64, macOS Intel + Apple Silicon.
|
||||
- Shielded (z-address) and transparent (t-address) send/receive
|
||||
- QR code generation for receiving addresses
|
||||
- Transaction history with memo support, date/amount sorting, and pending-send tracking
|
||||
- Blockchain explorer with live block/transaction search
|
||||
- Blockchain rescan and Sapling witness-rebuild with status-bar progress
|
||||
- Repair Wallet and daemon-binary management in Settings
|
||||
- Built-in console
|
||||
- Extensive theme and appearance options
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragon-2.0.0.AppImage | `34c9ea57ec27bf415e59d2890d995ed9069bfce04d979d7d8d58aa18f4570aa1` |
|
||||
| ObsidianDragon-2.0.0-Linux-x64.zip | `563004b4e45650dbfa9411d62fa1c59a3ca2199bd43a647b1614f2683bbdb5fa` |
|
||||
| ObsidianDragon-2.0.0.exe | `56100ae59dc3c67445d015cac5412ca40465bf459ba182c5ad2477a3b95ff548` |
|
||||
| ObsidianDragon-2.0.0-Windows-x64.zip | `ce9d067f36c5296288a473d3c2cceca7fcf807672a93a2084afc53c3b6e780d9` |
|
||||
| ObsidianDragon-2.0.0-macOS-universal.dmg | `8a01b6c0c2b5bebd64a222ba4a5ad04a1b8851138f28458b5c7f767fbb65db66` |
|
||||
| ObsidianDragon-2.0.0-macOS-universal.app.zip | `1bce1e5be15d9a2f3c6f42c95b23a0e89df142a73d65895252e4d8f50d19f541` |
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
|
||||
- **Windows:** x86-64, Windows 10 or later
|
||||
- **macOS:** Intel & Apple Silicon (universal binary), macOS 10.15 Catalina or later
|
||||
|
||||
## License
|
||||
|
||||
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.
|
||||
@@ -0,0 +1,41 @@
|
||||
**DragonX (DRGX) Full-Node Desktop Wallet — v2.0.2**
|
||||
|
||||
ObsidianDragon is a native ImGui-based full-node wallet for the DragonX network, with an embedded `dragonxd` daemon and the integrated DRG-XMRig CPU miner. **v2.0.2 is a maintenance release** — a focused set of transaction-history, sync-safety, and correctness fixes on top of v2.0.x. No configuration changes are required; just replace the binary.
|
||||
|
||||
---
|
||||
|
||||
## What's Fixed in v2.0.2
|
||||
|
||||
### Transaction history
|
||||
|
||||
- **Outgoing shielded sends now appear in History.** Sends *from* a shielded (z-)address — both **z→z** (shielded-to-shielded) and **z→t** (deshield to a transparent address) — were missing from the History tab, because the daemon's `listtransactions` doesn't report them and `z_viewtransaction` doesn't expose the transparent leg. The wallet now enumerates them via `z_listsentbyaddress`, so **your History matches your balance.** (Your funds were never affected — only the display.)
|
||||
- **More accurate send rows** — a single transaction with two equal-value outputs to different recipients is no longer collapsed into one row, and a transparent self-send / change output is no longer double-counted as both sent and received.
|
||||
|
||||
### Sync & fund safety
|
||||
|
||||
- **A node that loses all its peers is no longer reported as "synced."** Previously, a wallet whose node dropped to **0 peers** could keep showing a stale balance as if it were current — and allow a Send against it. The wallet now treats a node with no peer-derived chain tip as *not synced*, and a new **"Connected, but no network peers — the node can't sync"** banner explains the stall instead of silently looking healthy.
|
||||
- **Migrate-to-seed hardening.** If a migrate-to-seed was interrupted (app or daemon restart) *after* the sweep transaction had broadcast but *before* it confirmed, the wallet could mistake the temporarily-zero balance for an empty wallet and adopt the new seed wallet — swapping `wallet.dat` — before the sweep was mined. It now records that a sweep is in flight and **will not swap your wallet until the sweep confirms**, so funds can't be stranded in the timestamped backup.
|
||||
- **Fixed a rare crash** caused by a data race on the daemon-status text (the daemon-monitor thread writing it while the UI thread read it).
|
||||
|
||||
### Display / internationalization
|
||||
|
||||
- **Chinese, Japanese, and Korean FAQ text renders correctly.** Several Help/FAQ titles showed `??` instead of characters (e.g. 什么, 门) after the previous translation back-fill; the bundled CJK font subset now includes those glyphs.
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
macOS builds are x86-64 (Intel; run on Apple Silicon via Rosetta 2).
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragon-2.0.2.AppImage | `9bf7328965c6348113ba7a30b0284f15787c635af17efae3dfa8a87e56e10c9b` |
|
||||
| ObsidianDragon-2.0.2-Linux-x64.zip | `1371880c4187a94d4cb56d535471142c5f1a8053ef73fffd99d59e7b78c75c42` |
|
||||
| ObsidianDragon-2.0.2.exe | `673347d2696fb6f5bd09dc25e415d5ae8e532abde75b8d48c3cd26545461de50` |
|
||||
| ObsidianDragon-2.0.2-Windows-x64.zip | `f5ea7c45e66c6c1b90a8127f96dee04235691acb913ddc86327f2060798cd28c` |
|
||||
| ObsidianDragon-2.0.2-macOS-x86_64.dmg | `7038580807a8e93e5443bdc04f2aede3ae1c30e5bc9f8776a207edb2e87771d1` |
|
||||
| ObsidianDragon-2.0.2-macOS-x86_64.app.zip | `78159512ca684b95528231fd46f2d07b5bcc6f2da4d815fc1ecb2ff52127f41c` |
|
||||
|
||||
---
|
||||
|
||||
For the full feature set (HushChat messaging, seed-phrase wallets & migrate-to-seed, in-app node/miner updaters, built-in mining, 8-language UI), see the **v2.0.0** release notes — v2.0.2 changes only the fixes listed above.
|
||||
Binary file not shown.
+568
-17
@@ -19,6 +19,7 @@
|
||||
"about_license_text": "Diese Software wird unter der GNU General Public License v3 (GPLv3) veröffentlicht. Sie dürfen diese Software gemäß den Lizenzbedingungen frei verwenden, modifizieren und verbreiten.",
|
||||
"about_peers_count": "%zu Peers",
|
||||
"about_release": "Release",
|
||||
"about_source": "Quelle",
|
||||
"about_title": "Über ObsidianDragon",
|
||||
"about_version": "Version:",
|
||||
"about_website": "Webseite",
|
||||
@@ -60,7 +61,84 @@
|
||||
"amount_label": "Betrag:",
|
||||
"animate_avatars": "Avatare animieren",
|
||||
"appearance": "ERSCHEINUNGSBILD",
|
||||
"appx_back": "Zurück",
|
||||
"appx_back_up_seed_phrase_title": "Sichern Sie Ihre Seed-Phrase",
|
||||
"appx_birthday_block_height": "Geburtstag (Blockhöhe): %llu — sichern Sie auch dies.",
|
||||
"appx_blockchain_data_deleted": "Blockchain-Daten gelöscht (%d Elemente). Der Daemon wird neu gestartet, um sich erneut mit dem Netzwerk zu synchronisieren.",
|
||||
"appx_blockchain_maintenance_in_progress": "Eine Blockchain-Wartung läuft bereits.",
|
||||
"appx_blockchain_rescan_complete": "Blockchain-Neuscan abgeschlossen",
|
||||
"appx_bootstrap_complete_reconciling": "Bootstrap abgeschlossen — Ihr Wallet wird mit den neuen Chain-Daten abgeglichen.",
|
||||
"appx_cancel": "Abbrechen",
|
||||
"appx_cleaning_up": "Aufräumen...",
|
||||
"appx_confirm_your_backup": "Backup bestätigen",
|
||||
"appx_copied_clipboard_autoclears": "Kopiert — Zwischenablage wird in 45s automatisch geleert",
|
||||
"appx_copy": "Kopieren",
|
||||
"appx_could_not_start_restore": "Wiederherstellung konnte nicht gestartet werden",
|
||||
"appx_create_failed_prefix": "Erstellen fehlgeschlagen: ",
|
||||
"appx_creating_your_wallet": "Ihr Wallet wird erstellt…",
|
||||
"appx_daemon_error": "Daemon-Fehler",
|
||||
"appx_daemon_reinstall_in_progress": "Die Daemon-Neuinstallation läuft bereits.",
|
||||
"appx_disconnecting": "Verbindung wird getrennt...",
|
||||
"appx_done": "Fertig",
|
||||
"appx_dragonxd_output": "dragonxd-Ausgabe",
|
||||
"appx_encrypting_wallet": "Wallet wird verschlüsselt...",
|
||||
"appx_fullnode_lifecycle_unavailable_lite": "Full-Node-Lebenszyklusaktionen sind im Lite-Build nicht verfügbar",
|
||||
"appx_installing_bundled_daemon": "Mitgelieferter Daemon wird installiert — der Node stoppt, aktualisiert und startet neu...",
|
||||
"appx_invalid_payment_uri_prefix": "Ungültige Zahlungs-URI: ",
|
||||
"appx_ive_written_it_down": "Ich habe sie notiert",
|
||||
"appx_keep_node_running_and_quit": "Node weiterlaufen lassen & beenden",
|
||||
"appx_last_block_n": "Letzter Block: %d",
|
||||
"appx_last_used_wallet_not_found_prefix": "Ihre zuletzt verwendete Wallet-Datei (",
|
||||
"appx_last_used_wallet_not_found_suffix": ") wurde nicht gefunden — stattdessen wurde das Standard-Wallet geöffnet. Wenn Sie sie verschoben haben, stellen Sie sie wieder her und wechseln Sie über die Wallet-Liste zurück.",
|
||||
"appx_low_spec_mode_disabled": "Low-Spec-Modus deaktiviert",
|
||||
"appx_low_spec_mode_enabled": "Low-Spec-Modus aktiviert",
|
||||
"appx_miner_stopped_prefix": "Miner gestoppt: ",
|
||||
"appx_miner_stopped_unexpectedly": "Miner wurde unerwartet gestoppt.",
|
||||
"appx_n_min_n_sec": "%d Min %d Sek",
|
||||
"appx_n_seconds": "%d Sekunden",
|
||||
"appx_no_bundled_daemon_to_install": "Dieser Build enthält keinen mitgelieferten Daemon zur Installation",
|
||||
"appx_no_embedded_daemon_to_install": "Dieser Build enthält keinen eingebetteten Daemon zur Installation",
|
||||
"appx_node_busy_restarting": "Der Node ist mit dem Neustart beschäftigt — versuchen Sie es gleich erneut.",
|
||||
"appx_node_rebuilding_witness_cache": "Node baut seinen Witness-Cache neu auf",
|
||||
"appx_not_next_word": " — das ist nicht das nächste Wort",
|
||||
"appx_payment_request_loaded": "Zahlungsanforderung geladen",
|
||||
"appx_pool_miner_connected_and_hashing": "Pool-Miner verbunden und aktiv.",
|
||||
"appx_progress_n_of_n": "Fortschritt: %d / %d",
|
||||
"appx_rebuilding_sapling_note_witnesses": "Sapling-Note-Witnesses werden neu aufgebaut…",
|
||||
"appx_rebuilding_witness_cache_blocks_left": "Witness-Cache wird neu aufgebaut %.0f%% — %d Blöcke verbleiben",
|
||||
"appx_rebuilding_witness_cache_pct": "Witness-Cache wird neu aufgebaut %.0f%%",
|
||||
"appx_recovery_phrase_word_count": "Die Wiederherstellungsphrase sollte 24 Wörter haben — Sie haben %d.",
|
||||
"appx_restarting_daemon_rescan_flag": "Daemon wird mit -rescan-Flag neu gestartet...",
|
||||
"appx_restarting_daemon_zapwallettxes": "Daemon wird mit -zapwallettxes=2 neu gestartet (Wallet-Reparatur)...",
|
||||
"appx_restoring_your_wallet": "Ihr Wallet wird wiederhergestellt…",
|
||||
"appx_seed_backup_warning": "Diese 24 Wörter sind die EINZIGE Möglichkeit, Ihr Wallet wiederherzustellen. Notieren Sie sie in der richtigen Reihenfolge, bewahren Sie sie offline auf und geben Sie sie niemals weiter. Wenn Sie sie verlieren, sind Ihre Guthaben für immer verloren.",
|
||||
"appx_seed_not_backed_up_warning": "Sie haben Ihren Seed nicht gesichert — Guthaben könnten verloren gehen. Trotzdem überspringen?",
|
||||
"appx_sending_stop_command_to_daemon": "Stopp-Befehl wird an Daemon gesendet...",
|
||||
"appx_setting_initial_sapling_witnesses": "Initiale Sapling-Witnesses werden gesetzt %.0f%%",
|
||||
"appx_shutdown_complete": "Herunterfahren abgeschlossen",
|
||||
"appx_simple_background_disabled": "Einfacher Hintergrund deaktiviert",
|
||||
"appx_simple_background_enabled": "Einfacher Hintergrund aktiviert",
|
||||
"appx_skip": "Überspringen",
|
||||
"appx_skip_anyway": "Trotzdem überspringen",
|
||||
"appx_still_status_prefix": "Weiterhin \"",
|
||||
"appx_still_status_suffix": "\" — ein erzwungenes Beenden jetzt kann die Chain-Daten beschädigen.",
|
||||
"appx_stop_anyway_and_quit": "Trotzdem stoppen & beenden",
|
||||
"appx_stopping_daemon_deleting_blockchain": "Daemon wird gestoppt und Blockchain-Daten werden gelöscht...",
|
||||
"appx_stopping_node_discards_rebuild": "Wenn Sie den Node jetzt stoppen, wird der laufende Neuaufbau verworfen und beim nächsten Öffnen des Wallets neu gestartet (mehrere Minuten). Sie können den Node stattdessen weiterlaufen lassen.",
|
||||
"appx_stopping_pool_miner": "Pool-Miner wird gestoppt...",
|
||||
"appx_syncing_pct_block_n_of_n": "Synchronisierung %.1f%% — Block %d / %d",
|
||||
"appx_tap_words_in_order": "Tippen Sie die Wörter in der richtigen Reihenfolge an, um zu bestätigen, dass Sie sie gespeichert haben.",
|
||||
"appx_theme_effects_disabled": "Design-Effekte deaktiviert",
|
||||
"appx_theme_effects_enabled": "Design-Effekte aktiviert",
|
||||
"appx_theme_prefix": "Design: ",
|
||||
"appx_use_settings_restart_daemon_hint": "Verwenden Sie Einstellungen > Daemon neu starten, um es erneut zu versuchen",
|
||||
"appx_waiting_for_daemon_to_encrypt_wallet": "Warten, bis der Daemon das Wallet verschlüsselt...",
|
||||
"appx_wallet_created_and_backed_up": "Wallet erstellt und gesichert.",
|
||||
"appx_wallet_open_failed_prefix": "Öffnen des Wallets fehlgeschlagen: ",
|
||||
"auto_shield": "Mining automatisch abschirmen",
|
||||
"autoshield_by_node": "Ihr Node schützt für Sie gemäß Coinbase automatisch.",
|
||||
"autoshield_off_backup_seed": "Geschürfte Coinbase-Coins werden noch nicht automatisch geschützt. Sichern Sie Ihre Wiederherstellungsphrase (Einstellungen → Sicherung & Daten), damit Ihr Node dies sicher aktivieren kann.",
|
||||
"autoshield_off_generic": "Ihr Node schützt geschürfte Coinbase-Coins derzeit nicht automatisch.",
|
||||
"av_intro": "Mining-Software wird oft als potenziell unerwünscht eingestuft. Führen Sie diese Schritte aus, um das Pool-Mining zu aktivieren:",
|
||||
"av_open_security": "Windows-Sicherheit öffnen",
|
||||
"av_step1": "Schritt 1: Ausnahme hinzufügen",
|
||||
@@ -74,6 +152,9 @@
|
||||
"av_title": "Windows Defender hat den Miner blockiert",
|
||||
"available": "Verfügbar",
|
||||
"backup_backing_up": "Sicherung läuft...",
|
||||
"backup_col_backup": "SICHERUNG",
|
||||
"backup_col_export": "EXPORTIEREN",
|
||||
"backup_col_import": "IMPORTIEREN & WIEDERHERSTELLEN",
|
||||
"backup_create": "Sicherung erstellen",
|
||||
"backup_created": "Wallet-Sicherung erstellt",
|
||||
"backup_data": "SICHERUNG & DATEN",
|
||||
@@ -92,8 +173,22 @@
|
||||
"balance": "Guthaben",
|
||||
"balance_history_collecting": "Guthabenverlauf — Daten werden gesammelt...",
|
||||
"balance_layout": "Guthaben-Layout",
|
||||
"balance_layout_switched": "Layout: %s",
|
||||
"balance_mining_rate": "Schürfe %s",
|
||||
"balance_shielded_fmt": "Abgeschirmt: %.8f",
|
||||
"balance_syncing_pct": "Synchronisiere %.1f%%",
|
||||
"balance_transparent_fmt": "Transparent: %.8f",
|
||||
"baltab_market": "Markt",
|
||||
"baltab_market_price_4dp": "Markt: $%.4f",
|
||||
"baltab_market_price_8dp": "Markt: $%.8f",
|
||||
"baltab_pct_change_24h": "%s%.1f%% 24h",
|
||||
"baltab_pct_of_total_zaddr": "%.0f%% des Gesamtbetrags · %d Z-addr",
|
||||
"baltab_shielded": "Geschützt",
|
||||
"baltab_shielded_amount": "Geschützt %.8f",
|
||||
"baltab_t_addresses_count": "%d T-Adressen",
|
||||
"baltab_total_balance": "Gesamtguthaben",
|
||||
"baltab_transparent": "Transparent",
|
||||
"baltab_transparent_amount": "Transparent %.8f",
|
||||
"ban": "Sperren",
|
||||
"banned_peers": "Gesperrte Peers",
|
||||
"block": "Block",
|
||||
@@ -101,6 +196,12 @@
|
||||
"block_click_copy": "Klicken zum Kopieren",
|
||||
"block_click_next": "Klicken für nächsten Block",
|
||||
"block_click_prev": "Klicken für vorherigen Block",
|
||||
"block_db_reindex_body": "Dies passiert meist nach einem Daemon-Update, das das Format auf der Festplatte ändert, oder wenn der Block-Index beschädigt ist. Ihre Wallet und Ihre Coins sind sicher — der Node kann die Chain nur nicht laden, daher wird das Guthaben mit null angezeigt.\n\nDas Neuaufbauen liest Ihre vorhandenen Block-Dateien erneut ein und kann eine Weile dauern (dabei wird auch Ihre Wallet erneut gescannt). Es wird nichts heruntergeladen.",
|
||||
"block_db_reindex_confirm": "Block-Datenbank neu aufbauen",
|
||||
"block_db_reindex_notify": "Der Node kann seine Block-Datenbank nicht lesen (oft nach einem Daemon-Update). Bauen Sie sie neu auf, um Ihr Guthaben wiederherzustellen — siehe die Aufforderung oder Einstellungen › Node.",
|
||||
"block_db_reindex_started": "Die Block-Datenbank wird aus Ihren Blöcken neu aufgebaut — dies kann eine Weile dauern.",
|
||||
"block_db_reindex_title": "Block-Datenbank neu aufbauen?",
|
||||
"block_db_reindex_warn": "Der Node kann seine Block-Datenbank nicht lesen.",
|
||||
"block_explorer": "Block-Explorer",
|
||||
"block_get_info": "Block-Info abrufen",
|
||||
"block_hash": "Block-Hash:",
|
||||
@@ -132,6 +233,7 @@
|
||||
"bootstrap_verifying": "Prüfsummen werden überprüft...",
|
||||
"bootstrap_wallet_protected": "(wallet.dat ist geschützt)",
|
||||
"bootstrap_warning": "Vorhandene Blockdaten (blocks, chainstate, notarizations) werden gelöscht und ersetzt. Ihre wallet.dat wird NICHT verändert oder gelöscht.",
|
||||
"byte_count_fmt": "%zu / %zu Byte",
|
||||
"cancel": "Abbrechen",
|
||||
"change_pass_confirm": "Neue bestätigen:",
|
||||
"change_pass_current": "Aktuelle Passphrase:",
|
||||
@@ -147,6 +249,10 @@
|
||||
"chat_accent_theme": "Design",
|
||||
"chat_add_contact": "Kontakt hinzufügen",
|
||||
"chat_awaiting_key": "Warten auf Antwort",
|
||||
"chat_blocked_desc": "Blockierte Unterhaltungen werden entfernt und ihre Nachrichten verworfen — alte wie neue — bis Sie die Blockierung aufheben. Beim Aufheben wird die Unterhaltung erneut aus der Chain importiert.",
|
||||
"chat_blocked_manage": "Blockiert",
|
||||
"chat_blocked_title": "Blockierte Unterhaltungen",
|
||||
"chat_blocked_toast": "Unterhaltung gelöscht & blockiert",
|
||||
"chat_bubble_minimal": "Minimal",
|
||||
"chat_bubble_rounded": "Abgerundet",
|
||||
"chat_bubble_square": "Eckig",
|
||||
@@ -159,6 +265,16 @@
|
||||
"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_delete": "Unterhaltung löschen",
|
||||
"chat_delete_block": "Löschen & blockieren",
|
||||
"chat_delete_body_prefix": "Ihre lokale Kopie der Unterhaltung mit ",
|
||||
"chat_delete_body_suffix": " löschen?",
|
||||
"chat_delete_confirm": "Löschen",
|
||||
"chat_delete_failed": "Die Unterhaltung konnte nicht gelöscht werden — es wurde nichts geändert.",
|
||||
"chat_delete_local_note": "Dies betrifft nur dieses Gerät — die Nachrichten bleiben auf der Blockchain, und die andere Person behält ihre Kopie.",
|
||||
"chat_delete_revive_note": "„Löschen“ entfernt den Verlauf auf diesem Gerät. Wenn Ihnen die Person erneut schreibt, kommt die Unterhaltung zurück.",
|
||||
"chat_delete_title": "Unterhaltung löschen?",
|
||||
"chat_deleted_toast": "Unterhaltung gelöscht",
|
||||
"chat_density_comfortable": "Komfortabel",
|
||||
"chat_density_compact": "Kompakt",
|
||||
"chat_emoji_color": "Farbig",
|
||||
@@ -182,6 +298,7 @@
|
||||
"chat_new_button": "Neuer Chat",
|
||||
"chat_new_message": "Nachricht",
|
||||
"chat_new_message_toast": "Neue verschlüsselte Chat-Nachricht",
|
||||
"chat_new_needs_zaddr": "Chat benötigt eine geschützte (z-)Adresse — transparente (t-)Adressen können keine verschlüsselten Nachrichten empfangen.",
|
||||
"chat_new_send": "Anfrage senden",
|
||||
"chat_new_title": "Neuer Chat",
|
||||
"chat_new_zaddr": "z-Adresse des Empfängers",
|
||||
@@ -227,6 +344,8 @@
|
||||
"chat_ts_24h": "24-Stunden",
|
||||
"chat_ts_global": "Global folgen",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unblock": "Blockierung aufheben",
|
||||
"chat_unblocked_toast": "Blockierung der Unterhaltung aufgehoben",
|
||||
"chat_unhide": "Einblenden",
|
||||
"chat_unmute": "Stummschaltung aufheben",
|
||||
"chat_verify_key": "Identitätsschlüssel – zum Verifizieren vergleichen",
|
||||
@@ -279,6 +398,7 @@
|
||||
"console": "Konsole",
|
||||
"console_accents": "Farbakzente",
|
||||
"console_app": "App",
|
||||
"console_auto_focus": "Eingabe beim Öffnen fokussieren",
|
||||
"console_auto_scroll": "Automatisch scrollen",
|
||||
"console_available_commands": "Verfügbare Befehle:",
|
||||
"console_backend_reference": "Backend-Befehlsreferenz",
|
||||
@@ -373,11 +493,15 @@
|
||||
"console_tab_completion": "Tab zur Vervollständigung",
|
||||
"console_text_colors": "Textfarben",
|
||||
"console_toggle_accents": "Farbakzente der Zeilen umschalten",
|
||||
"console_toggle_auto_focus": "Den Cursor beim Öffnen des Konsolen-Tabs im Befehlsfeld platzieren",
|
||||
"console_toggle_text_color": "Textfarben der Zeilen umschalten",
|
||||
"console_type_help": "Geben Sie 'help' ein für verfügbare Befehle",
|
||||
"console_welcome": "Willkommen bei ObsidianDragon Konsole",
|
||||
"console_zoom_in": "Vergrößern",
|
||||
"console_zoom_out": "Verkleinern",
|
||||
"consolidate_desc": "Fassen Sie viele kleine Eingaben zu einer einzigen geschützten Note zusammen. Weniger Notes bedeuten eine kleinere Wallet-Datei und mehr Privatsphäre.",
|
||||
"consolidate_funds_btn": "Konsolidieren",
|
||||
"consolidate_title": "Guthaben konsolidieren",
|
||||
"contact_avatar": "AVATAR",
|
||||
"contact_avatar_bad_image": "Dieses Bild konnte nicht geladen werden.",
|
||||
"contact_avatar_badge": "Abzeichen",
|
||||
@@ -407,6 +531,7 @@
|
||||
"contacts_shape_square": "Quadrat",
|
||||
"contacts_shape_tab": "Reiter",
|
||||
"copied": "Kopiert!",
|
||||
"copied_to_clipboard": "In die Zwischenablage kopiert",
|
||||
"copy": "Kopieren",
|
||||
"copy_address": "Vollständige Adresse kopieren",
|
||||
"copy_error": "Fehler kopieren",
|
||||
@@ -419,11 +544,16 @@
|
||||
"daemon_bundled": "Gebündelt",
|
||||
"daemon_install_bundled": "Gebündelten installieren",
|
||||
"daemon_installed": "Installiert",
|
||||
"daemon_maintenance_label": "WARTUNG",
|
||||
"daemon_none_bundled": "keiner in diesem Build",
|
||||
"daemon_not_installed": "nicht installiert",
|
||||
"daemon_status_diff": "Version weicht ab",
|
||||
"daemon_status_differ": "Installierte Binärdatei unterscheidet sich von der gebündelten Version.",
|
||||
"daemon_status_match": "Installierte Binärdatei entspricht der gebündelten Version.",
|
||||
"daemon_status_missing": "Kein Daemon installiert — installieren Sie die mitgelieferte Version.",
|
||||
"daemon_status_none": "Nicht installiert",
|
||||
"daemon_status_ok": "Aktuell",
|
||||
"daemon_update_active_tt": "Der derzeit installierte (aktive) Node",
|
||||
"daemon_update_available": "Eine neue Node-Version ist verfügbar",
|
||||
"daemon_update_body": "Dieser Wallet-Build enthält einen neueren DragonX-Node als den derzeit installierten. Beim Aktualisieren wird der installierte dragonxd (sowie dragonx-cli/dragonx-tx) ersetzt und der Node anschließend gestoppt und neu gestartet, damit die neue Version wirksam wird. Empfohlen — ein neuerer Node kann Funktionen (z. B. Unterstützung für Wiederherstellungsphrasen) bieten, die der alte nicht hat.",
|
||||
"daemon_update_browse": "Alle Releases durchsuchen…",
|
||||
@@ -433,18 +563,28 @@
|
||||
"daemon_update_download_install": "Herunterladen & installieren",
|
||||
"daemon_update_downloading": "Wird heruntergeladen…",
|
||||
"daemon_update_failed": "Update fehlgeschlagen",
|
||||
"daemon_update_install_this": "Diese Version installieren",
|
||||
"daemon_update_installed": "Installiert:",
|
||||
"daemon_update_installed_ok": "Node installiert",
|
||||
"daemon_update_installing": "Wird installiert…",
|
||||
"daemon_update_keep": "Aktuellen behalten",
|
||||
"daemon_update_later": "Später",
|
||||
"daemon_update_latest": "Neueste:",
|
||||
"daemon_update_latest_badge": "neueste",
|
||||
"daemon_update_latest_tt": "Neueste verfügbare Version",
|
||||
"daemon_update_loading": "Releases werden geladen…",
|
||||
"daemon_update_no_notes": "Keine Versionshinweise für diese Version.",
|
||||
"daemon_update_now": "Jetzt aktualisieren",
|
||||
"daemon_update_prerelease_tt": "Vorabversion / Testbuild",
|
||||
"daemon_update_prompt_title": "Node-Daemon aktualisieren?",
|
||||
"daemon_update_reinstall": "Neu installieren",
|
||||
"daemon_update_released": "Veröffentlicht:",
|
||||
"daemon_update_restart_note": "Starten Sie den Daemon neu, um die neue Version auszuführen.",
|
||||
"daemon_update_restart_now": "Daemon jetzt neu starten",
|
||||
"daemon_update_safe": "Ihre Wallet, Ihre Schlüssel und Ihre Blockchain-Daten bleiben unberührt — nur die Programmdateien des Daemons werden ersetzt. Wenn Sie bewusst einen eigenen Node betreiben, wählen Sie „Aktuellen behalten“.",
|
||||
"daemon_update_status_current": "Diese Version ist installiert",
|
||||
"daemon_update_status_latest": "Neueste Version",
|
||||
"daemon_update_status_older": "Älter als Ihre installierte Version",
|
||||
"daemon_update_title": "Node-Daemon aktualisieren?",
|
||||
"daemon_update_unavailable_body": "Für diese Plattform ist kein Node-Build verfügbar.",
|
||||
"daemon_update_unavailable_title": "Node-Updates nicht verfügbar",
|
||||
@@ -453,12 +593,20 @@
|
||||
"daemon_update_verify_note": "Der Download wird vor der Installation anhand der veröffentlichten SHA-256-Prüfsumme des Releases und einer fest hinterlegten ed25519-Signatur verifiziert.",
|
||||
"daemon_update_verifying": "Wird verifiziert…",
|
||||
"daemon_update_version": "Version:",
|
||||
"daemon_updates_label": "AKTUALISIERUNGEN",
|
||||
"daemon_version": "Daemon",
|
||||
"dark": "Dunkel",
|
||||
"data_stale_prefix": "Aktualisiert",
|
||||
"data_stale_tooltip": "Der Kontostand ist möglicherweise veraltet – die Wallet hat kürzlich keine Aktualisierung erhalten. Überprüfe deine Node-Verbindung.",
|
||||
"date": "Datum",
|
||||
"date_label": "Datum:",
|
||||
"debug_gate_confirm": "Debug-Optionen anzeigen",
|
||||
"debug_gate_incorrect": "Falsche PIN oder Passphrase.",
|
||||
"debug_gate_pass_prompt": "Geben Sie Ihre Wallet-Passphrase ein, um fortzufahren:",
|
||||
"debug_gate_pin_prompt": "Geben Sie Ihre Entsperr-PIN ein, um fortzufahren:",
|
||||
"debug_gate_title": "Debug-Optionen anzeigen?",
|
||||
"debug_gate_verifying": "Wird überprüft…",
|
||||
"debug_gate_warning": "Dies sind erweiterte Diagnose- und Node-Feinabstimmungsoptionen. Sie können vertrauliche Informationen offenlegen und die Funktionsweise Ihres Nodes verändern. Fahren Sie nur fort, wenn Sie wissen, was Sie tun.",
|
||||
"debug_logging": "FEHLERPROTOKOLLIERUNG",
|
||||
"decrypt_desc": "Die Wallet wird exportiert, der Daemon mit einer frischen, unverschlüsselten Wallet neu gestartet und alle Schlüssel werden erneut importiert. Je nach Wallet-Größe kann dies einige Minuten dauern.",
|
||||
"decrypt_error_title": "Entschlüsselung fehlgeschlagen",
|
||||
@@ -481,10 +629,22 @@
|
||||
"disconnected": "Getrennt",
|
||||
"dismiss": "Verwerfen",
|
||||
"display": "Anzeige",
|
||||
"done": "Fertig",
|
||||
"download": "Herunterladen",
|
||||
"download_bootstrap": "Bootstrap herunterladen",
|
||||
"dragonx_green": "DragonX (Grün)",
|
||||
"edit": "Bearbeiten",
|
||||
"empty_wallet_keys_suffix": "Schlüssel",
|
||||
"empty_wallet_open_manager": "Wallet-Verwaltung öffnen",
|
||||
"empty_wallet_restore": "Mein Wallet wiederherstellen",
|
||||
"empty_wallet_salvage_body": "Dieses Wallet ist leer, weil eine frühere automatische Reparatur Ihr ursprüngliches Wallet als Sicherung beiseitegelegt hat. Ihre Coins befinden sich fast sicher in dieser Sicherung und sind nicht verloren. Stellen Sie sie wieder her, um Ihr Guthaben erneut zu laden — nichts wird gelöscht; die aktuelle Datei wird zuerst beiseitegelegt.",
|
||||
"empty_wallet_salvage_headline": "Ihre Coins sind sicher in einer Sicherungsdatei.",
|
||||
"empty_wallet_salvage_title": "Ihr Wallet wurde möglicherweise repariert",
|
||||
"empty_wallet_warning_body": "Dieses Wallet hat keine Adressen und kein Guthaben, aber eine andere Wallet-Datei in Ihrem DragonX-Ordner enthält Schlüssel. Ihre Coins befinden sich höchstwahrscheinlich dort und sind nicht verloren. Öffnen Sie die Wallet-Verwaltung, um zu dem Wallet mit Ihrem Guthaben zu wechseln.",
|
||||
"empty_wallet_warning_dismiss": "Für dieses Wallet nicht mehr warnen",
|
||||
"empty_wallet_warning_dismiss_tip": "Beendet diese Warnung nur für die aktuelle Wallet-Datei. Wenn Sie später zu einem anderen leeren Wallet wechseln, kann die Warnung erneut erscheinen.",
|
||||
"empty_wallet_warning_headline": "Möglicherweise haben Sie das falsche Wallet geöffnet.",
|
||||
"empty_wallet_warning_title": "Dieses Wallet ist leer",
|
||||
"enc_confirm": "Bestätigen:",
|
||||
"enc_desc": "Die Verschlüsselung Ihrer Wallet schützt Ihre privaten Schlüssel mit einer Passphrase. Nach der Verschlüsselung wird der Daemon neu gestartet.",
|
||||
"enc_encrypting": "Wallet wird verschlüsselt...",
|
||||
@@ -523,6 +683,7 @@
|
||||
"explorer_tx_outputs": "Ausgaben",
|
||||
"explorer_tx_size": "Größe",
|
||||
"explorer_txid": "TxID:",
|
||||
"explorer_urls_hdr": "URLS",
|
||||
"export": "Exportieren",
|
||||
"export_csv": "CSV exportieren",
|
||||
"export_keys_btn": "Schlüssel exportieren",
|
||||
@@ -550,6 +711,151 @@
|
||||
"export_viewing_key": "Betrachtungsschlüssel exportieren",
|
||||
"failed_create_shielded": "Abgeschirmte Adresse konnte nicht erstellt werden",
|
||||
"failed_create_transparent": "Transparente Adresse konnte nicht erstellt werden",
|
||||
"faq": "FAQ",
|
||||
"faq_d_mgmt_1_a": "Der eingebettete Node startet automatisch mit der Wallet und stoppt bei Bedarf, sodass Sie ihn normalerweise nicht manuell verwalten müssen. Erweiterte Steuerelemente (einschließlich des Neustarts des Daemons) finden Sie unter Einstellungen > Node & Sicherheit.",
|
||||
"faq_d_mgmt_1_q": "Wie starte oder stoppe ich den Node?",
|
||||
"faq_d_mgmt_2_a": "Wenn Sie die Wallet mit einem selbst gestarteten Node verbinden, entscheidet diese Option, ob das Schließen der Wallet auch diesen Node beendet. Lassen Sie sie deaktiviert, wenn Ihr Node nach dem Schließen der Wallet weiterlaufen soll.",
|
||||
"faq_d_mgmt_2_q": "Was bewirkt „externen Daemon beim Beenden schließen“?",
|
||||
"faq_d_mgmt_3_a": "Node- und Wallet-Daten befinden sich im DragonX-Datenverzeichnis unter Ihrem Benutzerprofil (zum Beispiel in AppData unter Windows oder Ihrem Home-Ordner unter Linux/macOS). Die Blockchain ist der große Teil davon; halten Sie genug freien Speicherplatz für ihr Wachstum bereit.",
|
||||
"faq_d_mgmt_3_q": "Wo werden die Blockchain- und Wallet-Daten gespeichert?",
|
||||
"faq_d_mgmt_title": "Node-Verwaltung",
|
||||
"faq_d_mine_1_a": "Im Tab „Mining“ können Sie mit Ihrer CPU minen. DragonX verwendet den RandomX-Algorithmus, der für gewöhnliche Prozessoren ausgelegt ist. Stellen Sie die Anzahl der Threads ein und starten Sie — Belohnungen fließen in Ihre Wallet.",
|
||||
"faq_d_mine_1_q": "Wie mine ich DragonX?",
|
||||
"faq_d_mine_2_a": "Solo-Mining sendet Belohnungen direkt an Sie, zahlt aber nur, wenn Sie einen Block finden — das ist selten, sofern Sie nicht über viel Hashrate verfügen. Pool-Mining teilt die Arbeit mit anderen für kleinere, aber gleichmäßigere Auszahlungen. Der Mining-Tab unterstützt beides.",
|
||||
"faq_d_mine_2_q": "Solo- oder Pool-Mining — was soll ich wählen?",
|
||||
"faq_d_mine_3_a": "Im Pool-Bereich des Mining-Tabs gibt es die Schaltfläche „Miner aktualisieren…“, die den neuesten optimierten DragonX-Miner herunterlädt, verifiziert und installiert. Wie beim Node-Updater wird jeder Download vor der Installation per Prüfsumme und Signatur verifiziert.",
|
||||
"faq_d_mine_3_q": "Wie aktualisiere ich den Miner?",
|
||||
"faq_d_mine_4_a": "Der aktuelle Node kann einen integrierten Stratum-Server betreiben, sodass andere Miner sich mit Ihrem Rechner verbinden können. Dafür gibt es einen Schalter unter Einstellungen > Node & Sicherheit. Standardmäßig lauscht er nur lokal; ihn für andere Rechner zu öffnen, erfordert bewusstes Freischalten.",
|
||||
"faq_d_mine_4_q": "Kann ich einen Mining-Pool hosten?",
|
||||
"faq_d_mine_title": "Mining & Pools",
|
||||
"faq_d_net_1_a": "Direkt nach dem Start braucht der Node einen Moment, um Peers zu finden — ein kurzzeitiges „0 Peers“ ist also normal. Hält es an, prüfen Sie Ihre Internetverbindung und ob eine Firewall den Node blockiert. Der Node findet Peers automatisch über eingebaute Seeds.",
|
||||
"faq_d_net_1_q": "Die Wallet zeigt keine Peers oder Verbindungen an — was ist los?",
|
||||
"faq_d_net_2_a": "Ja — DragonX-Nodes kommunizieren über TLS miteinander, Peer-Verbindungen sind also verschlüsselt. Auch die Wallet spricht mit ihrem eigenen Node über einen sicheren lokalen Kanal.",
|
||||
"faq_d_net_2_q": "Ist meine Verbindung zum Netzwerk verschlüsselt?",
|
||||
"faq_d_net_title": "Peers & Netzwerk",
|
||||
"faq_d_node_1_a": "Der Daemon, dragonxd, ist der DragonX Full Node. Er lädt und verifiziert die gesamte Blockchain, leitet Transaktionen an das Netzwerk weiter und beantwortet die Anfragen der Wallet. Einen eigenen Node zu betreiben bedeutet, dass Sie keinem fremden Server vertrauen müssen.",
|
||||
"faq_d_node_1_q": "Was ist der Daemon (dragonxd)?",
|
||||
"faq_d_node_2_a": "Standardmäßig startet und verwaltet die Wallet ihren eigenen mitgelieferten Node (eingebettet) — Sie müssen nichts tun. Wenn Sie dragonxd bereits selbst betreiben, kann die Wallet sich stattdessen mit diesem externen Node verbinden. Die Wallet erkennt automatisch, welche Situation vorliegt, und verhält sich entsprechend.",
|
||||
"faq_d_node_2_q": "Eingebetteter vs. externer Node — was ist der Unterschied?",
|
||||
"faq_d_node_3_a": "Ein Full Node prüft jede Regel der Blockchain eigenständig, sodass Ihre Wallet der Mathematik vertraut statt einem Dritten. Das stärkt außerdem das Netzwerk. Der Preis dafür sind Speicherplatz und eine anfängliche Synchronisierung.",
|
||||
"faq_d_node_3_q": "Warum überhaupt einen Full Node betreiben?",
|
||||
"faq_d_node_title": "Der Full Node",
|
||||
"faq_d_perf_1_a": "Die Blockchain ist der größte Posten und wächst mit der Zeit, halten Sie also mehrere Gigabyte frei. Der Node nutzt für mehr Geschwindigkeit einen Datenbank-Cache; die mitgelieferten Standardwerte sind auf typische Rechner abgestimmt. Fortgeschrittene Nutzer können den Datenbank-Cache des Nodes anpassen, indem sie dbcache in der Konfigurationsdatei (DRAGONX.conf) bearbeiten.",
|
||||
"faq_d_perf_1_q": "Wie viel Speicherplatz nutzt der Node, und kann ich den Speicherverbrauch anpassen?",
|
||||
"faq_d_perf_2_a": "Aktivieren Sie den Bootstrap-Download (im Einrichtungsassistenten angeboten), um eine aktuelle, verifizierte Kopie der Chain zu laden, statt jeden Block von Grund auf zu prüfen. Auch eine schnelle Festplatte (SSD) hilft erheblich.",
|
||||
"faq_d_perf_2_q": "Wie beschleunige ich die anfängliche Synchronisierung?",
|
||||
"faq_d_perf_title": "Speicher & Leistung",
|
||||
"faq_d_sync_1_a": "Beim Start lädt und prüft der Node Blöcke, bevor er die Wallet bedienen kann. Der Bildschirm „Blöcke werden verarbeitet / Blöcke werden angewendet, um den aktuellen Chain-Status aufzubauen“ ist dieses Aufwärmen. Das ist normal; die Wallet wird nutzbar, sobald der Node fertig ist und seine Höhe meldet.",
|
||||
"faq_d_sync_1_q": "Was bedeutet „Blöcke werden verarbeitet“ beim Start?",
|
||||
"faq_d_sync_2_a": "Für Ihre geschützten Guthaben führt der Node kryptografische „Witnesses“, die private Ausgaben ermöglichen. Nach bestimmten Neustarts oder Updates baut er diesen Cache neu auf, indem er aktuelle Blöcke erneut durchsucht. Bei einer Wallet mit vielen Transaktionen kann das mehrere Minuten dauern und zusätzlichen Speicher benötigen.\n\nSie hängt dabei nicht fest — lassen Sie sie fertig werden. Ein erzwungenes Beenden mittendrin führt nur dazu, dass der Neuaufbau beim nächsten Mal von vorn beginnt.",
|
||||
"faq_d_sync_2_q": "Was bedeutet „Witness-Cache wird neu aufgebaut“?",
|
||||
"faq_d_sync_3_a": "Nahe der Chain-Spitze konkurriert die Arbeit, die Ihre Wallet durchsucht (Guthaben und Notes prüfen), mit dem Node um dieselbe interne Sperre, was das Verbinden der letzten Blöcke bei einer großen Wallet verlangsamen kann. Neuere Versionen drosseln dieses Durchsuchen, damit der Node aufholen kann. Auf einem leichteren Tab zu bleiben (oder einfach zu warten) lässt es schneller fertig werden.",
|
||||
"faq_d_sync_3_q": "Warum ist die Synchronisierung langsam, obwohl ich fast aufgeholt habe?",
|
||||
"faq_d_sync_4_a": "Die erste Synchronisierung lädt und prüft die gesamte Chain und kann je nach Verbindung und Festplatte eine Weile dauern. Sie können sie deutlich beschleunigen, indem Sie im Einrichtungsassistenten den Bootstrap-Download aktivieren, der eine aktuelle, verifizierte Kopie der Chain-Daten lädt.",
|
||||
"faq_d_sync_4_q": "Wie lange dauert die anfängliche Synchronisierung?",
|
||||
"faq_d_sync_title": "Synchronisierung & Blockchain",
|
||||
"faq_d_trbl_1_a": "Normalerweise nicht. Bei einer großen Wallet kann der Node mehrere Minuten damit verbringen, geschützte Witnesses neu aufzubauen oder Blöcke anzuwenden — währenddessen wirkt er pausiert und reagiert möglicherweise langsam. Geben Sie ihm Zeit — das Debug-Log (Einstellungen) zeigt stetigen Fortschritt, wenn er arbeitet.\n\nVermeiden Sie ein erzwungenes Beenden während dieser Phase; es startet die Arbeit beim nächsten Start von vorn.",
|
||||
"faq_d_trbl_1_q": "Der Node scheint bei „Beste Chain wird aktiviert“ hängen zu bleiben — ist er eingefroren?",
|
||||
"faq_d_trbl_2_a": "Wenn eine Wallet-Datei wiederhergestellt wurde, aber ein Teil ihrer Schlüsseldaten fehlt, öffnet der Node sie möglicherweise in einem eingeschränkten „degraded“-Modus, in dem er keine neuen Adressen ableiten oder Guthaben schützen kann. Die sicherste Lösung ist, aus Ihrer Seed-Wiederherstellungsphrase eine frische Wallet wiederherzustellen.",
|
||||
"faq_d_trbl_2_q": "Was ist der „degraded“-Modus?",
|
||||
"faq_d_trbl_3_a": "Ein Re-Scan lässt die Wallet die Chain erneut lesen, um Ihre Transaktionen wiederzufinden (nützlich nach dem Import von Schlüsseln). Ein Re-Index baut die Blockchain-Datenbank des Nodes neu auf und ist nur nötig, wenn diese Datenbank nach einem Versionswechsel beschädigt oder inkompatibel ist. Beides kann eine Weile dauern; starten Sie es unter Einstellungen > Node & Sicherheit.",
|
||||
"faq_d_trbl_3_q": "Wann sollte ich re-indizieren oder re-scannen?",
|
||||
"faq_d_trbl_4_a": "Der Speicherverbrauch steigt bei intensiver Arbeit wie einem Witness-Neuaufbau oder einer anfänglichen Synchronisierung stark an und fällt danach wieder ab. Anhaltend hoher Speicherverbrauch im Leerlauf ist ungewöhnlich — ein Neustart der Wallet behebt das. Stellen Sie auf einem Rechner mit wenig RAM sicher, dass keine anderen speicherhungrigen Apps konkurrieren.",
|
||||
"faq_d_trbl_4_q": "Der Node nutzt viel Speicher — ist das normal?",
|
||||
"faq_d_trbl_title": "Problembehandlung",
|
||||
"faq_d_upd_1_a": "Öffnen Sie Einstellungen > Node & Sicherheit > Daemon-Binärdatei und nutzen Sie „Nach Updates suchen…“. Die Wallet lädt den neuesten verifizierten Node-Build herunter und installiert ihn; die neue Version wird beim nächsten Start des Daemons wirksam.",
|
||||
"faq_d_upd_1_q": "Wie aktualisiere ich den Node?",
|
||||
"faq_d_upd_2_a": "Ja — der Update-Dialog listet jede Version auf, sodass Sie einen bestimmten oder älteren Build wählen können. Seien Sie beim Downgrade vorsichtig: Ein älterer Node akzeptiert möglicherweise keine von einem neueren geschriebenen Blockchain-Daten und benötigt eventuell einen Re-Index.",
|
||||
"faq_d_upd_2_q": "Kann ich eine bestimmte Node-Version installieren?",
|
||||
"faq_d_upd_3_a": "Jedes heruntergeladene Node-Archiv wird vor der Installation gegen eine Prüfsumme und eine kryptografische Signatur verifiziert. Schlägt die Verifizierung fehl, verweigert die Wallet das Update.",
|
||||
"faq_d_upd_3_q": "Ist das Update sicher?",
|
||||
"faq_d_upd_title": "Node aktualisieren",
|
||||
"faq_group_daemon": "Daemon",
|
||||
"faq_group_wallet": "Wallet",
|
||||
"faq_intro": "Antworten zu Ihrer Wallet und dem DragonX-Node. Suchen Sie oder blättern Sie unten nach Thema.",
|
||||
"faq_intro_lite": "Antworten zu Ihrer Lite-Wallet. Suchen Sie oder blättern Sie unten nach Thema.",
|
||||
"faq_l_gs_1_a": "ObsidianDragonLite ist die schlanke DragonX-Wallet. Statt einen Full Node zu betreiben, verbindet sie sich mit einem DragonX-Lite-Server, startet also schnell und lädt nicht die gesamte Blockchain herunter.\n\nSie verwaltet Ihre Schlüssel, Guthaben und Sendevorgänge genau wie die vollständige Wallet — siehe den Abschnitt „Lite-Wallet“ unten für die Funktionsweise der Serververbindung und den Kompromiss bei der Privatsphäre.",
|
||||
"faq_l_gs_1_q": "Was ist ObsidianDragonLite?",
|
||||
"faq_l_lite_1_a": "Die Lite-Wallet spricht mit einem DragonX-Lite-Server, statt einen eigenen Full Node zu betreiben. Das macht sie schnell startbereit und schont Speicherplatz und Bandbreite, da sie die gesamte Blockchain nicht selbst herunterlädt oder prüft. Ihre privaten Schlüssel verbleiben stets auf Ihrem Gerät — sie werden nie an den Server gesendet.",
|
||||
"faq_l_lite_1_q": "Wie unterscheidet sich die Lite-Wallet von der vollständigen Wallet?",
|
||||
"faq_l_lite_2_a": "Öffnen Sie Einstellungen und wählen Sie einen Server. Sie können jederzeit den Server wechseln, die Wallet synchronisiert sich dann neu mit dem neuen Server. Ist ein Server langsam oder nicht erreichbar, versuchen Sie einen anderen.",
|
||||
"faq_l_lite_2_q": "Wie wähle oder ändere ich den Server?",
|
||||
"faq_l_lite_3_a": "Ihre Schlüssel verlassen niemals Ihr Gerät, und geschützte Beträge bleiben verschlüsselt. Der Lite-Server sieht jedoch, welche Adressen Ihre Wallet abfragt, sowie Ihre IP-Adresse, wodurch er Ihre Adressen miteinander verknüpfen kann — ein Kompromiss zwischen Komfort und Privatsphäre. Für maximale Privatsphäre nutzen Sie die Full-Node-Wallet, die die Kette selbst verifiziert und Ihre Adressen keinem Server preisgibt.",
|
||||
"faq_l_lite_3_q": "Ist die Lite-Wallet genauso privat wie der Full Node?",
|
||||
"faq_l_lite_title": "Lite-Wallet",
|
||||
"faq_l_sec_1_a": "Öffnen Sie Einstellungen > Wallet und legen Sie im Sicherheitsbereich eine Passphrase fest. Die Verschlüsselung schützt Ihre privaten Schlüssel auf der Festplatte, sodass jemand mit Zugriff auf die Dateien Ihre Coins trotzdem nicht ausgeben kann.\n\nWählen Sie eine starke Passphrase und verlieren Sie sie nicht — ohne sie gibt es keine Möglichkeit, eine verschlüsselte Wallet wiederherzustellen.",
|
||||
"faq_no_results": "Keine Ergebnisse. Versuchen Sie einen anderen Suchbegriff.",
|
||||
"faq_open_tooltip": "Hilfe & FAQ",
|
||||
"faq_search_hint": "FAQ durchsuchen…",
|
||||
"faq_title": "Hilfe & FAQ",
|
||||
"faq_w_addr_1_a": "Transparente Adressen (sie beginnen mit R oder t) funktionieren wie bei den meisten Coins: Beträge und Adressen sind öffentlich auf der Blockchain einsehbar.\n\nGeschützte Adressen (z-Adressen, beginnend mit zs) nutzen Zero-Knowledge-Kryptografie — Betrag, Absender und Empfänger sind on-chain verschlüsselt. Guthaben auf geschützten Adressen sind privat.",
|
||||
"faq_w_addr_1_q": "Was ist der Unterschied zwischen transparenten und geschützten Adressen?",
|
||||
"faq_w_addr_2_a": "Bevorzugen Sie wann immer möglich geschützte (z-)Adressen — sie halten Ihr Guthaben und Ihren Zahlungsverlauf privat. DragonX ist eine Privacy-Coin, und geschützt ist die Standardeinstellung für empfangene Gelder.\n\nVerwenden Sie eine transparente Adresse nur, wenn ein Dienst, mit dem Sie interagieren, keine geschützten Adressen verarbeiten kann.",
|
||||
"faq_w_addr_2_q": "Welchen Adresstyp sollte ich verwenden?",
|
||||
"faq_w_addr_3_a": "Bei geschützten Adressen ist die Wiederverwendung unbedenklich und gibt keinen Verlauf preis. Bei transparenten Adressen verbessert eine neue Adresse pro Zahlung die Privatsphäre. Neue Adressen erstellen Sie im Tab Empfangen.",
|
||||
"faq_w_addr_3_q": "Sollte ich eine Adresse wiederverwenden oder eine neue erstellen?",
|
||||
"faq_w_addr_4_a": "Beim Schützen werden Coins von einer transparenten Adresse in eine geschützte verschoben und dadurch privat gemacht. Neu geschürfte Coins kommen transparent an und werden automatisch für Sie geschützt.\n\nSie können auch manuell aus der Wallet heraus schützen; ein Schutzvorgang ist einfach eine spezielle Transaktion und benötigt daher die normale Bestätigungszeit.",
|
||||
"faq_w_addr_4_q": "Was bedeutet „Schützen“?",
|
||||
"faq_w_addr_title": "Adressen & Privatsphäre",
|
||||
"faq_w_bal_1_a": "Solange Ihre Wallet noch synchronisiert, ist Ihr Guthaben unvollständig und kann 0 anzeigen — es wurden noch nicht alle Transaktionen gescannt. Es füllt sich, sobald die Synchronisierung abgeschlossen ist.\n\nBeobachten Sie die Statusleiste unten: Sie zeigt den Verbindungsstatus und die Blockhöhe an. Sobald die Synchronisierung abgeschlossen ist, ist Ihr Guthaben korrekt.",
|
||||
"faq_w_bal_1_q": "Warum ist mein Guthaben 0 oder aktualisiert sich nicht?",
|
||||
"faq_w_bal_2_a": "Jeder neue Block, der auf den Block mit Ihrer Transaktion folgt, fügt eine Bestätigung hinzu. Mehr Bestätigungen bedeuten, dass die Zahlung fester verankert ist. Empfangene Gelder werden nach der ersten Bestätigung ausgabefähig.",
|
||||
"faq_w_bal_2_q": "Was sind Bestätigungen?",
|
||||
"faq_w_bal_3_a": "Das Gesamtguthaben umfasst Gelder, die noch bestätigt werden oder vorübergehend gesperrt sind (zum Beispiel Coins, die gerade geschützt werden). Verfügbar ist das, was Sie jetzt sofort senden können. Beide Werte gleichen sich an, sobald Transaktionen bestätigt werden.",
|
||||
"faq_w_bal_3_q": "Was ist der Unterschied zwischen Gesamt- und verfügbarem Guthaben?",
|
||||
"faq_w_bal_title": "Guthaben & Synchronisierung",
|
||||
"faq_w_chat_1_a": "Chat ist ein verschlüsselter, on-chain-basierter Messenger, der in die Wallet integriert ist. Nachrichten werden als private, geschützte Memos gesendet, sodass nur Sie und Ihr Kontakt sie lesen können.",
|
||||
"faq_w_chat_1_q": "Was ist die Chat-Funktion?",
|
||||
"faq_w_chat_2_a": "Ihre Chat-Identität wird aus der Wiederherstellungsphrase Ihrer Wallet abgeleitet, sie ist also an Ihre Wallet gebunden — stellen Sie die Wallet wieder her, und Ihre Identität kehrt zurück. Sie erstellen kein separates Konto oder Passwort.",
|
||||
"faq_w_chat_2_q": "Wie funktioniert meine Chat-Identität?",
|
||||
"faq_w_chat_3_a": "Fügen Sie einen Kontakt über dessen Adresse im Tab Kontakte hinzu, optional mit Namen und Avatar. Kontakte können privat für die aktuelle Wallet bleiben oder über alle Wallets hinweg geteilt werden, die Sie auf diesem Computer öffnen.",
|
||||
"faq_w_chat_3_q": "Wie funktionieren Kontakte?",
|
||||
"faq_w_chat_4_a": "Öffnen Sie eine Unterhaltung und verwenden Sie die Symbole in deren Kopfzeile. Ausblenden (das Auge) entfernt sie aus der Liste, behält aber alle Nachrichten — eine neue Nachricht blendet sie wieder ein. Löschen (der Papierkorb) öffnet einen Dialog: „Löschen“ entfernt den Verlauf auf diesem Gerät (schreibt Ihnen die Person erneut, kommt die Unterhaltung zurück), während „Löschen & blockieren“ zusätzlich zukünftige Nachrichten stoppt, bis Sie die Person aus der Liste „Blockiert“ oberhalb der Unterhaltungsliste entblocken. All dies gilt nur lokal für dieses Gerät — die Nachrichten bleiben auf der Blockchain, und die andere Person behält ihre eigene Kopie; es ist also kein „Zurückrufen“.",
|
||||
"faq_w_chat_4_q": "Wie blende ich eine Unterhaltung aus, lösche oder blockiere sie?",
|
||||
"faq_w_chat_title": "Chat & Kontakte",
|
||||
"faq_w_gs_1_a": "ObsidianDragon ist eine Full-Node-Wallet für DragonX (DRGX). Sie verwaltet Ihre Coins und betreibt im Desktop-Build im Hintergrund einen DragonX-Full-Node (den Daemon), sodass Ihre Wallet die Blockchain selbst verifiziert, statt einem Server eines Drittanbieters zu vertrauen.\n\nStellen Sie sich das als zwei zusammenarbeitende Teile vor: die Wallet (Schlüssel, Guthaben, Senden) und den Node (Blockchain und Netzwerk). Die Fragen hier sind entsprechend aufgeteilt — siehe die Tabs Wallet und Daemon oben.",
|
||||
"faq_w_gs_1_q": "Was ist ObsidianDragon?",
|
||||
"faq_w_gs_2_a": "Beim ersten Start erstellt der Einrichtungsassistent automatisch eine Wallet für Sie. Neue Wallets basieren auf einer geheimen Wiederherstellungsphrase (einer Liste von Wörtern). Notieren Sie diese Phrase und bewahren Sie sie offline auf — es ist die einzige Möglichkeit, Ihre Gelder wiederherzustellen, falls dieser Computer verloren geht.\n\nSie können die Phrase jederzeit unter Einstellungen > Sicherung & Daten > Seed phrase einsehen oder sichern.",
|
||||
"faq_w_gs_2_q": "Wie erstelle ich eine neue Wallet?",
|
||||
"faq_w_gs_3_a": "Verwenden Sie den Wiederherstellungs-Ablauf per Seed-Phrase, wenn Sie eine Wiederherstellungsphrase besitzen. (Im Full-Node-Build können Sie vor dem Start auch eine vorhandene wallet.dat-Datei in das Datenverzeichnis der Wallet kopieren.)\n\nNach der Wiederherstellung scannt die Wallet erneut, um Ihre vergangenen Transaktionen zu finden — Guthaben und Verlauf können daher beim ersten Mal etwas dauern, bis sie erscheinen.",
|
||||
"faq_w_gs_3_q": "Wie stelle ich eine bereits vorhandene Wallet wieder her?",
|
||||
"faq_w_gs_4_a": "Der Assistent lässt Sie ein Erscheinungsbild/Theme wählen, Verschlüsselung und eine PIN einrichten und — im Full-Node-Build — optional einen Bootstrap herunterladen, um die erste Synchronisierung zu beschleunigen. Sie können jeden Schritt überspringen und all dies später in den Einstellungen ändern.",
|
||||
"faq_w_gs_4_q": "Was macht die Ersteinrichtung?",
|
||||
"faq_w_gs_title": "Erste Schritte",
|
||||
"faq_w_sec_1_a": "Öffnen Sie Einstellungen > Node & Sicherheit und legen Sie eine Passphrase fest (Bereich SICHERHEIT). Die Verschlüsselung schützt Ihre privaten Schlüssel auf der Festplatte, sodass jemand mit Zugriff auf die Dateien Ihre Coins trotzdem nicht ausgeben kann.\n\nWählen Sie eine starke Passphrase und verlieren Sie sie nicht — ohne sie gibt es keine Möglichkeit, eine verschlüsselte Wallet wiederherzustellen.",
|
||||
"faq_w_sec_1_q": "Wie verschlüssele ich meine Wallet?",
|
||||
"faq_w_sec_2_a": "Die PIN sperrt den Bildschirm der App, sodass Guthaben und Aktionen verborgen sind, wenn Sie kurz weg sind. Sie ist eine Komfortsperre zusätzlich zur Verschlüsselung; Sie richten sie im Assistenten oder in den Einstellungen ein, nachdem die Wallet verschlüsselt wurde.",
|
||||
"faq_w_sec_2_q": "Was ist die PIN / der Sperrbildschirm?",
|
||||
"faq_w_sec_3_a": "Nein. Die Wallet-Verschlüsselung kann nicht umgangen werden. Wenn Sie noch Ihre Wiederherstellungsphrase besitzen, können Sie die Wallet daraus in eine neue Wallet wiederherstellen und eine neue Passphrase festlegen. Ohne Passphrase oder Seed-Phrase können die Gelder nicht wiederhergestellt werden.",
|
||||
"faq_w_sec_3_q": "Ich habe meine Passphrase vergessen — kann sie wiederhergestellt werden?",
|
||||
"faq_w_sec_title": "Sicherheit & Verschlüsselung",
|
||||
"faq_w_seed_1_a": "Gehen Sie zu Einstellungen > Sicherung & Daten > Seed phrase. Notieren Sie die Wörter der Reihe nach auf Papier und bewahren Sie sie an einem sicheren, offline zugänglichen Ort auf. Jeder, der die Phrase kennt, kann Ihre Coins ausgeben — speichern Sie sie daher niemals als Foto, in einer E-Mail oder einer Cloud-Notiz.",
|
||||
"faq_w_seed_1_q": "Wie sichere ich meine Wiederherstellungsphrase?",
|
||||
"faq_w_seed_2_a": "Ältere (Legacy-)Wallets basierten nicht auf einer Seed-Phrase. Die Wallet kann eine Legacy-Wallet in eine moderne, seed-basierte Wallet migrieren: Dabei wird eine neue Seed-Wallet erstellt und Ihre Gelder werden dorthin überführt. Suchen Sie nach „Zu Seed-Wallet migrieren…“ unter Einstellungen > Sicherung & Daten. Diese Funktion benötigt die aktuelle Daemon-Version.",
|
||||
"faq_w_seed_2_q": "Meine Wallet hat keine Wiederherstellungsphrase — kann ich eine hinzufügen?",
|
||||
"faq_w_seed_3_a": "Verwenden Sie beim Einrichten einer Wallet den Seed-Wiederherstellungs-Ablauf und geben Sie Ihre Wörter der Reihe nach ein. Ihre Wallet scannt anschließend erneut, um Guthaben und Verlauf wiederherzustellen, was beim ersten Mal etwas dauern kann.",
|
||||
"faq_w_seed_3_q": "Wie stelle ich aus meiner Wiederherstellungsphrase wieder her?",
|
||||
"faq_w_seed_4_a": "Ihre Seed-Phrase ist die primäre Sicherung und reicht aus, um alles wiederherzustellen. Eine Kopie Ihrer Wallet-Datei ist eine praktische zusätzliche Sicherung, die außerdem Bezeichnungen und Einstellungen erhält. Bewahren Sie jede Sicherung offline und privat auf.",
|
||||
"faq_w_seed_4_q": "Sollte ich auch wallet.dat sichern?",
|
||||
"faq_w_seed_title": "Seed-Phrase & Sicherung",
|
||||
"faq_w_send_1_a": "Öffnen Sie den Tab Senden, fügen Sie die Adresse des Empfängers ein, geben Sie einen Betrag ein und bestätigen Sie. Wenn die Wallet verschlüsselt ist, werden Sie gebeten, sie für die Transaktion zu entsperren.\n\nSendungen an geschützte Adressen sind privat; Sendungen an transparente Adressen sind öffentlich.",
|
||||
"faq_w_send_1_q": "Wie sende ich Gelder?",
|
||||
"faq_w_send_2_a": "DragonX-Gebühren sind sehr niedrig und werden automatisch festgelegt. Die Gebühr wird angezeigt, bevor Sie bestätigen. Normalerweise müssen Sie sie nicht ändern.",
|
||||
"faq_w_send_2_q": "Welche Gebühr zahle ich?",
|
||||
"faq_w_send_3_a": "Ja — beim Senden an eine geschützte (z-)Adresse können Sie ein verschlüsseltes Memo anhängen. Nur der Empfänger kann es lesen. Transparente Adressen unterstützen keine Memos.",
|
||||
"faq_w_send_3_q": "Kann ich einer Zahlung eine Nachricht beifügen?",
|
||||
"faq_w_send_4_a": "Eine Transaktion ist „ausstehend“, bis sie in einen Block aufgenommen wird und Bestätigungen erhält. Das dauert normalerweise ein bis zwei Minuten. Geschützte Transaktionen erfordern außerdem, dass Ihre Wallet synchronisiert ist, um den Beweis zu erstellen.\n\nBleibt eine Sendung ungewöhnlich lange ausstehend, prüfen Sie, ob Ihre Wallet verbunden und synchronisiert ist (siehe die Statusleiste unten im Fenster).",
|
||||
"faq_w_send_4_q": "Ich habe eine Zahlung gesendet, aber sie zeigt „ausstehend“ — warum?",
|
||||
"faq_w_send_title": "Senden & Empfangen",
|
||||
"faq_w_set_1_a": "Öffnen Sie Einstellungen > Erscheinungsbild, um ein Design und eine Akzentfarbe zu wählen. Mehrere helle und dunkle Skins sind enthalten, und Effekte wie Weichzeichnen lassen sich für leistungsschwächere Rechner abschalten.",
|
||||
"faq_w_set_1_q": "Wie ändere ich das Design?",
|
||||
"faq_w_set_2_a": "Ja — Einstellungen > Erscheinungsbild enthält eine Sprachauswahl. Die Wallet wird mit mehreren Übersetzungen ausgeliefert; alles, was noch nicht übersetzt ist, greift auf Englisch zurück.",
|
||||
"faq_w_set_2_q": "Kann ich die Sprache ändern?",
|
||||
"faq_w_set_3_a": "Verwenden Sie die Schriftskalierung in Einstellungen > Erscheinungsbild. Sie skaliert die gesamte Oberfläche, was auf hochauflösenden (HiDPI-)Bildschirmen hilfreich ist, auf denen die App sonst klein dargestellt werden könnte.",
|
||||
"faq_w_set_3_q": "Der Text ist zu klein (oder zu groß) — kann ich ihn skalieren?",
|
||||
"faq_w_set_4_a": "Aktivieren Sie den Preisabruf in den Einstellungen. Die Wallet zeigt dann einen ungefähren Fiat-Wert neben den Guthaben an und kann ein kleines Marktdiagramm anzeigen. Preise dienen nur zur Information.",
|
||||
"faq_w_set_4_q": "Wie zeige ich Fiat-Preise an?",
|
||||
"faq_w_set_title": "Einstellungen & Erscheinungsbild",
|
||||
"favorite_address": "Als Favorit markieren",
|
||||
"fee": "Gebühr",
|
||||
"fee_high": "Hoch",
|
||||
@@ -572,6 +878,63 @@
|
||||
"general": "Allgemein",
|
||||
"generating": "Wird generiert",
|
||||
"go_to_receive": "Zum Empfangen",
|
||||
"grpa_current_block_paren": "(Aktuell: %d)",
|
||||
"grpa_days_ago": "vor %lld Tagen",
|
||||
"grpa_dbg_addrman": "Verfolgung und Verwaltung von Peer-Adressen",
|
||||
"grpa_dbg_alert": "Meldungen des Alarmsystems",
|
||||
"grpa_dbg_bench": "Benchmark-Zeiten für Operationen",
|
||||
"grpa_dbg_coindb": "Lese-/Schreiboperationen der Coin-Datenbank",
|
||||
"grpa_dbg_db": "Berkeley-DB-Operationen",
|
||||
"grpa_dbg_estimatefee": "Algorithmus zur Gebührenschätzung",
|
||||
"grpa_dbg_http": "Aktivität des HTTP-RPC-Servers",
|
||||
"grpa_dbg_libevent": "Libevent-Netzwerkbibliothek",
|
||||
"grpa_dbg_lock": "Debugging von Lock-Konflikten",
|
||||
"grpa_dbg_mempool": "Aktivität des Transaktions-Mempools",
|
||||
"grpa_dbg_net": "Netzwerkverbindungen und -nachrichten",
|
||||
"grpa_dbg_paymentdisclosure": "Zahlungsoffenlegungsprotokoll",
|
||||
"grpa_dbg_pow": "Proof-of-Work-Mining-Aktivität",
|
||||
"grpa_dbg_proxy": "SOCKS5-Proxy-Verbindungen",
|
||||
"grpa_dbg_prune": "Block-Pruning-Operationen",
|
||||
"grpa_dbg_rand": "Zufallszahlengenerierung",
|
||||
"grpa_dbg_reindex": "Fortschritt der Blockchain-Neuindizierung",
|
||||
"grpa_dbg_rpc": "Verarbeitung von RPC-Befehlen",
|
||||
"grpa_dbg_selectcoins": "Coin-Auswahl für Transaktionen",
|
||||
"grpa_dbg_tor": "Tor-Integration und Circuit-Informationen",
|
||||
"grpa_dbg_zmq": "ZeroMQ-Benachrichtigungssystem",
|
||||
"grpa_dbg_zrpc": "Geschützte (z-addr) RPC-Operationen",
|
||||
"grpa_enter_private_key_to_import": "Geben Sie einen privaten Schlüssel zum Importieren ein.",
|
||||
"grpa_error_prefix": "Fehler: ",
|
||||
"grpa_hr_ago": "vor %lld Std",
|
||||
"grpa_invalid_response_from_daemon": "Ungültige Antwort vom Daemon",
|
||||
"grpa_invalid_suffix": " (ungültig)",
|
||||
"grpa_min_ago": "vor %lld Min",
|
||||
"grpa_sec_ago": "vor %lld Sek",
|
||||
"grpa_seed_demo_chat": "Seed-Demo-Chat",
|
||||
"grpa_showing_first_100_of": "... die ersten 100 von %d werden angezeigt",
|
||||
"grpa_tab_about": "Über",
|
||||
"grpa_tab_appearance": "Darstellung",
|
||||
"grpa_tab_backup_data": "Backup & Daten",
|
||||
"grpa_tab_chat": "Chat",
|
||||
"grpa_tab_explorer": "Explorer",
|
||||
"grpa_tab_node_security": "Node & Sicherheit",
|
||||
"grpa_tab_wallet": "Wallet",
|
||||
"grpa_unexpected_getblockhash_result": "unerwartetes getblockhash-Ergebnis",
|
||||
"grpb_copy": "Kopieren",
|
||||
"grpb_max": "Max",
|
||||
"grpb_new_badge_suffix": " [NEU]",
|
||||
"grpb_preview_msg_payment_through": "Ist die Zahlung durchgegangen? 🙂",
|
||||
"grpb_preview_msg_sending_rest": "Sende jetzt den Rest 👍",
|
||||
"grpb_preview_msg_yep_confirmed": "Ja — gerade bestätigt ✅",
|
||||
"grpb_selected_suffix": "\n(ausgewählt)",
|
||||
"grpb_tooltip_address_balance": "%s\nGuthaben: %.8f %s%s",
|
||||
"grpb_undo_clear": "Löschen rückgängig",
|
||||
"grpc_benchmark_inconclusive": "Benchmark nicht aussagekräftig: es wurden keine Hashrate-Werte aufgezeichnet. Prüfen Sie die Pool-Verbindung und versuchen Sie es erneut.",
|
||||
"grpc_benchmark_takes_secs": "Der Benchmark dauert ~%ds und unterbricht das Mining. Erneut klicken zum Starten.",
|
||||
"grpc_bootstrap_failed": "Bootstrap fehlgeschlagen",
|
||||
"grpc_bootstrap_not_initialized": "Bootstrap nicht initialisiert",
|
||||
"grpc_hashrate_fee": "%s %s%% Gebühr",
|
||||
"grpc_key_not_available": "Schlüssel für diese Adresse nicht verfügbar",
|
||||
"grpc_na": "N/V",
|
||||
"height": "Höhe",
|
||||
"help": "Hilfe",
|
||||
"hidden_tag": " (versteckt)",
|
||||
@@ -646,6 +1009,7 @@
|
||||
"light": "Hell",
|
||||
"lite_account_label": "Konto",
|
||||
"lite_action": "Aktion",
|
||||
"lite_backend_unavailable": "Lite-Wallet-Backend nicht verfügbar",
|
||||
"lite_backup_keys": "Sicherung & Schlüssel",
|
||||
"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).",
|
||||
@@ -653,9 +1017,12 @@
|
||||
"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_start": "Der Vorgang konnte nicht gestartet werden",
|
||||
"lite_could_not_write": "Konnte nicht schreiben ",
|
||||
"lite_encrypt_wallet": "Wallet verschlüsseln",
|
||||
"lite_encryption_removed": "Verschlüsselung entfernt",
|
||||
"lite_enter_all_seed_words": "Alle 24 Seed-Wörter zur Wiederherstellung eingeben (%d erhalten)",
|
||||
"lite_enter_wallet_path": "Wallet-Pfad eingeben",
|
||||
"lite_hide_wipe": "Ausblenden & löschen",
|
||||
"lite_import": "Importieren",
|
||||
"lite_import_key_label": "Schlüssel importieren",
|
||||
@@ -792,9 +1159,27 @@
|
||||
"memo_optional": "MEMO (OPTIONAL)",
|
||||
"memo_upper": "MEMO",
|
||||
"memo_z_only": "Hinweis: Memos sind nur beim Senden an abgeschirmte (z) Adressen verfügbar",
|
||||
"merge_addr_created": "Geschützte Adresse erstellt.",
|
||||
"merge_advanced": "Erweitert",
|
||||
"merge_back": "Zurück",
|
||||
"merge_batch_fmt": "Führt bis zu %d Inputs pro Durchgang zusammen — wiederholen Sie den Vorgang, um den Rest abzuschließen.",
|
||||
"merge_confirm_btn": "Bestätigen",
|
||||
"merge_confirm_fmt": "~%s DRGX aus %d Input(s) in %s konsolidieren?",
|
||||
"merge_create_zaddr": "Geschützte Adresse erstellen",
|
||||
"merge_creating": "Adresse wird erstellt…",
|
||||
"merge_description": "Mehrere UTXOs zu einer einzelnen abgeschirmten Adresse zusammenführen. Dies kann die Wallet-Größe reduzieren und die Privatsphäre verbessern.",
|
||||
"merge_fee_hint": "Netzwerkgebühr für diese Transaktion.",
|
||||
"merge_funds": "Gelder zusammenführen",
|
||||
"merge_max_inputs": "Max. Inputs pro Batch",
|
||||
"merge_no_spendable": "Noch keine verfügbaren Inputs zum Konsolidieren.",
|
||||
"merge_progress": "Konsolidierung läuft… dies kann einige Minuten dauern. Sie können dieses Fenster schließen.",
|
||||
"merge_scope_fmt": "%d transparente + %d geschützte Inputs · ~%s DRGX verfügbar",
|
||||
"merge_scope_loading": "Ihre Inputs werden geprüft…",
|
||||
"merge_send_failed": "Zusammenführen fehlgeschlagen: ",
|
||||
"merge_source": "Konsolidieren",
|
||||
"merge_src_both": "Beide",
|
||||
"merge_src_shielded": "Geschützt",
|
||||
"merge_src_transparent": "Transparent",
|
||||
"merge_started": "Zusammenführung gestartet",
|
||||
"merge_title": "An Adresse zusammenführen",
|
||||
"mig_adopt": "Dies zu meiner Wallet machen",
|
||||
@@ -875,6 +1260,7 @@
|
||||
"mining_difficulty_copied": "Schwierigkeit kopiert",
|
||||
"mining_est_block": "Gesch. Block",
|
||||
"mining_est_daily": "Gesch. täglich",
|
||||
"mining_est_daily_pool_sub": "grobe Solo-Äquivalenz, vor Pool-Gebühr",
|
||||
"mining_filter_all": "Alle",
|
||||
"mining_filter_tip_all": "Alle Einnahmen anzeigen",
|
||||
"mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen",
|
||||
@@ -903,10 +1289,12 @@
|
||||
"mining_open_in_explorer": "Im Explorer öffnen",
|
||||
"mining_payout_address": "Auszahlungsadresse",
|
||||
"mining_payout_foreign": "⚠ Diese Auszahlungsadresse befindet sich nicht in Ihrer aktuellen Wallet — geschürfte Belohnungen würden an eine andere Wallet gehen. Aktualisieren Sie sie, wenn Sie die Wallet gewechselt haben.",
|
||||
"mining_payout_invalid": "Keine gültige DragonX-Adresse — vor dem Start korrigieren, sonst gehen die Mining-Belohnungen verloren.",
|
||||
"mining_payout_tooltip": "Adresse für Mining-Belohnungen",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_fee": "Gebühr",
|
||||
"mining_pool_hashrate": "Pool-Hashrate",
|
||||
"mining_pool_needs_payout_tooltip": "Zuerst eine Auszahlungsadresse eingeben (Z-Adresse erzeugen)",
|
||||
"mining_pool_url": "Pool-URL",
|
||||
"mining_pools_header": "POOLS",
|
||||
"mining_recent_blocks": "LETZTE BLÖCKE",
|
||||
@@ -936,6 +1324,9 @@
|
||||
"mining_syncing_tooltip": "Blockchain synchronisiert...",
|
||||
"mining_tag": " · Mining",
|
||||
"mining_threads": "Mining-Threads",
|
||||
"mining_threads_input_tooltip": "Genaue Thread-Anzahl eingeben (Enter zum Übernehmen)",
|
||||
"mining_threads_minus_tooltip": "Weniger Threads",
|
||||
"mining_threads_plus_tooltip": "Mehr Threads",
|
||||
"mining_to_save": "zum Speichern",
|
||||
"mining_today": "Heute",
|
||||
"mining_uptime": "Laufzeit",
|
||||
@@ -1028,6 +1419,13 @@
|
||||
"peers_upper": "PEERS",
|
||||
"peers_version": "Version",
|
||||
"pending": "Ausstehend",
|
||||
"picker_dat_count": "%d Wallet-Datei(en) in diesem Ordner",
|
||||
"picker_dat_none": "Keine Wallet-Dateien direkt in diesem Ordner",
|
||||
"picker_empty": "Dieser Ordner enthält keine Unterordner oder Wallet-Dateien.",
|
||||
"picker_home": "Home-Ordner",
|
||||
"picker_select": "Diesen Ordner durchsuchen",
|
||||
"picker_title": "Ordner zum Durchsuchen auswählen",
|
||||
"picker_up": "Eine Ebene nach oben",
|
||||
"pin_change_desc": "Ändern Sie Ihre Entsperr-PIN. Sie benötigen Ihre aktuelle PIN und eine neue PIN.",
|
||||
"pin_confirm_new_label": "Neue PIN bestätigen:",
|
||||
"pin_current_label": "Aktuelle PIN:",
|
||||
@@ -1105,6 +1503,8 @@
|
||||
"qr_failed": "QR-Code konnte nicht generiert werden",
|
||||
"qr_title": "QR-Code",
|
||||
"qr_unavailable": "QR nicht verfügbar",
|
||||
"quick_receive": "Schnell empfangen",
|
||||
"quick_send": "Schnell senden",
|
||||
"ram_daemon_gb": "Daemon: %.1f GB (%s)",
|
||||
"ram_daemon_mb": "Daemon: %.0f MB (%s)",
|
||||
"ram_system_gb": "System: %.1f / %.0f GB",
|
||||
@@ -1154,6 +1554,7 @@
|
||||
"rpc_connection": "RPC-Verbindung...",
|
||||
"rpc_host": "RPC-Host",
|
||||
"rpc_pass": "Passwort",
|
||||
"rpc_plaintext_remote_warning": "Die Remote-RPC-Verbindung verwendet unverschlüsseltes HTTP. Füge rpctls=1 zur DRAGONX.conf hinzu, falls dein Daemon TLS unterstützt.",
|
||||
"rpc_port": "Port",
|
||||
"rpc_user": "Benutzername",
|
||||
"save": "Speichern",
|
||||
@@ -1161,6 +1562,7 @@
|
||||
"save_z_transactions": "Z-Tx in Tx-Liste speichern",
|
||||
"sb_auth_failed": "Authentifizierung fehlgeschlagen — rpcuser/rpcpassword prüfen",
|
||||
"sb_block": "Block: %d",
|
||||
"sb_block_db_unreadable": "Blockdatenbank unlesbar — Neuaufbau erforderlich",
|
||||
"sb_building_witnesses": "Zeugen werden erstellt",
|
||||
"sb_building_witnesses_pct": "Witnesses werden neu aufgebaut %.0f%%",
|
||||
"sb_connecting_daemon": "Verbindung zu dragonxd...",
|
||||
@@ -1176,6 +1578,7 @@
|
||||
"sb_dragonxd_stopped": "dragonxd gestoppt",
|
||||
"sb_dragonxd_stopping": "dragonxd wird gestoppt...",
|
||||
"sb_extracting_sapling": "Sapling-Parameter werden extrahiert...",
|
||||
"sb_finishing_repair": "Wallet-Reparatur wird abgeschlossen — erneutes Scannen…",
|
||||
"sb_importing_keys": "Schlüssel importieren",
|
||||
"sb_loading_config": "Konfiguration laden...",
|
||||
"sb_mining_hs": "%.1f H/s",
|
||||
@@ -1197,14 +1600,55 @@
|
||||
"sb_waiting_config": "Warten auf Daemon-Konfiguration...",
|
||||
"sb_waiting_daemon": "Warten auf dragonxd...",
|
||||
"sb_waiting_daemon_err": "Warten auf dragonxd — %s",
|
||||
"sb_wallet_needs_recovery": "Wallet-Reparatur verfügbar",
|
||||
"sb_warming_up": "Aufwärmen...",
|
||||
"sb_witness_cache": "Zeugen werden neu aufgebaut",
|
||||
"scale_effects": "SKALIERUNG & EFFEKTE",
|
||||
"screenshot_open_dir": "Speicherort öffnen",
|
||||
"screenshot_sweep": "Screenshot-Durchlauf ausführen",
|
||||
"screenshot_sweep_desc": "Durchläuft jedes Design über jeden Tab und speichert von jedem einen Screenshot in tab-spezifischen Unterordnern im Screenshots-Ordner des Konfigurationsverzeichnisses (überschreibt den vorherigen Durchlauf). Läuft einige Sekunden.",
|
||||
"screenshot_sweep_full": "Vollständiger UI-Durchlauf",
|
||||
"search_icons": "Symbole suchen...",
|
||||
"search_placeholder": "Suchen...",
|
||||
"sec_changing_passphrase": "Passphrase wird geändert...",
|
||||
"sec_changing_pin": "PIN wird geändert...",
|
||||
"sec_couldnt_lock_wallet": "Wallet konnte nicht gesperrt werden — es ist weiterhin entsperrt. Prüfen Sie die Daemon-Verbindung.",
|
||||
"sec_encrypted_backup_suffix": "\nVerschlüsseltes Backup: wallet.dat.encrypted.bak",
|
||||
"sec_encrypting_wallet": "Wallet wird verschlüsselt...",
|
||||
"sec_encryption_did_not_complete": "Die Wallet-Verschlüsselung wurde nicht abgeschlossen — Ihr Wallet ist NICHT verschlüsselt. Öffnen Sie die Einstellungen, um die Verschlüsselung abzuschließen.",
|
||||
"sec_encryption_failed_prefix": "Verschlüsselung fehlgeschlagen: ",
|
||||
"sec_failed_prefix": "Fehlgeschlagen: ",
|
||||
"sec_failed_to_create_vault": "Tresor konnte nicht erstellt werden",
|
||||
"sec_importing_keys_rescanning": "Schlüssel werden importiert & Blockchain neu gescannt — Wallet ist währenddessen nutzbar",
|
||||
"sec_incorrect_current_pin": "Aktuelle PIN falsch",
|
||||
"sec_incorrect_passphrase_decrypt": "Falsche Passphrase",
|
||||
"sec_incorrect_passphrase_pin_setup": "Falsche Passphrase",
|
||||
"sec_incorrect_pin_remove": "Falsche PIN",
|
||||
"sec_internal_error_change_pin": "Interner Fehler",
|
||||
"sec_internal_error_remove_pin": "Interner Fehler",
|
||||
"sec_mode_passphrase": " Passphrase",
|
||||
"sec_not_connected_to_daemon": "Nicht mit Daemon verbunden",
|
||||
"sec_not_connected_to_daemon_pin": "Nicht mit Daemon verbunden",
|
||||
"sec_passphrase_changed_successfully": "Passphrase erfolgreich geändert",
|
||||
"sec_pin_changed_successfully": "PIN erfolgreich geändert",
|
||||
"sec_pin_removed": "PIN entfernt",
|
||||
"sec_pin_set_successfully": "PIN erfolgreich festgelegt",
|
||||
"sec_restart_daemon_for_encryption": "Bitte starten Sie Ihren Daemon neu, damit die Verschlüsselung wirksam wird.",
|
||||
"sec_too_many_attempts_wait": "Zu viele Versuche. Warten Sie %.0f Sekunden...",
|
||||
"sec_total_elapsed_fmt": "Gesamtdauer: %dm %02ds",
|
||||
"sec_unlock_button": "Entsperren",
|
||||
"sec_unlock_failed_prefix": "Entsperren fehlgeschlagen: ",
|
||||
"sec_unlocking_fmt": "Entsperren%s",
|
||||
"sec_use_passphrase_instead": "Stattdessen Passphrase verwenden",
|
||||
"sec_use_pin_instead": "Stattdessen PIN verwenden",
|
||||
"sec_verifying_passphrase": "Passphrase wird überprüft...",
|
||||
"sec_verifying_pin": "PIN wird überprüft...",
|
||||
"sec_wallet_decrypted_all_keys_imported": "Wallet erfolgreich entschlüsselt! Alle Schlüssel importiert.",
|
||||
"sec_wallet_encrypted_and_pin_set": "Wallet verschlüsselt & PIN festgelegt",
|
||||
"sec_wallet_encrypted_but_pin_vault_failed": "Wallet verschlüsselt, aber PIN-Tresor fehlgeschlagen",
|
||||
"sec_wallet_encrypted_restarting_daemon": "Wallet verschlüsselt. Daemon wird neu gestartet...",
|
||||
"sec_wallet_encrypted_successfully": "Wallet erfolgreich verschlüsselt",
|
||||
"sec_wallet_locked_title": "Wallet gesperrt",
|
||||
"security": "SICHERHEIT",
|
||||
"seed_backup_button": "Wiederherstellungsphrase",
|
||||
"seed_backup_close": "Schließen",
|
||||
@@ -1228,6 +1672,7 @@
|
||||
"send_amount": "Betrag",
|
||||
"send_amount_details": "BETRAGSDETAILS",
|
||||
"send_amount_upper": "BETRAG",
|
||||
"send_available_note": "verfügbar",
|
||||
"send_cannot_send_now": "Senden derzeit nicht möglich — prüfen Sie Verbindung, Synchronisierung und verfügbares Guthaben.",
|
||||
"send_clear_fields": "Alle Formularfelder leeren?",
|
||||
"send_contacts_button": "Aus Kontakten auswählen",
|
||||
@@ -1263,6 +1708,7 @@
|
||||
"send_tooltip_not_connected": "Nicht mit Daemon verbunden",
|
||||
"send_tooltip_select_source": "Wählen Sie zuerst eine Quelladresse",
|
||||
"send_tooltip_syncing": "Warten Sie auf die Blockchain-Synchronisierung",
|
||||
"send_tooltip_view_only": "Nur-Lese-Adresse — kein Spending Key, Senden nicht möglich",
|
||||
"send_total": "Gesamt",
|
||||
"send_transaction": "Transaktion senden",
|
||||
"send_tx_failed": "Transaktion fehlgeschlagen",
|
||||
@@ -1282,16 +1728,16 @@
|
||||
"sent_filter": "Gesendet",
|
||||
"sent_type": "Gesendet",
|
||||
"sent_upper": "GESENDET",
|
||||
"set_label": "Label setzen...",
|
||||
"set_label": "Label setzen",
|
||||
"settings": "Einstellungen",
|
||||
"settings_about_text": "Eine geschirmte Kryptowährungs-Wallet für DragonX (DRGX), erstellt mit Dear ImGui für ein leichtes, portables Erlebnis.",
|
||||
"settings_acrylic_level": "Acrylstufe:",
|
||||
"settings_address_book": "Adressbuch...",
|
||||
"settings_address_book": "Adressbuch…",
|
||||
"settings_auto_detected": "Automatisch erkannt aus DRAGONX.conf",
|
||||
"settings_auto_lock": "AUTO-SPERRE",
|
||||
"settings_auto_shield_desc": "Transparente Guthaben automatisch an geschirmte Adressen verschieben",
|
||||
"settings_auto_shield_funds": "Transparente Guthaben automatisch abschirmen",
|
||||
"settings_backup": "Sicherung...",
|
||||
"settings_backup": "Sicherung…",
|
||||
"settings_block_explorer_urls": "Block-Explorer-URLs",
|
||||
"settings_builtin": "Integriert",
|
||||
"settings_change_passphrase": "Passphrase ändern",
|
||||
@@ -1305,7 +1751,7 @@
|
||||
"settings_copy_diagnostics": "Diagnose kopieren",
|
||||
"settings_copyright": "Copyright 2024-2026 DragonX-Entwickler | GPLv3-Lizenz",
|
||||
"settings_custom": "Benutzerdefiniert",
|
||||
"settings_data_dir": "Datenverzeichnis:",
|
||||
"settings_data_dir": "Datenverzeichnis",
|
||||
"settings_debug_changed": "Debug-Kategorien geändert — Daemon neu starten zum Anwenden",
|
||||
"settings_debug_restart_note": "Änderungen werden nach einem Neustart des Daemons wirksam.",
|
||||
"settings_debug_select": "Kategorien auswählen, um Daemon-Fehlerprotokollierung zu aktivieren (-debug= Flags).",
|
||||
@@ -1313,19 +1759,20 @@
|
||||
"settings_encrypt_first_pin": "Verschlüsseln Sie zuerst die Wallet, um PIN zu aktivieren",
|
||||
"settings_encrypt_wallet": "Wallet verschlüsseln",
|
||||
"settings_explorer_hint": "URLs sollten einen abschließenden Schrägstrich enthalten. Die txid/Adresse wird angehängt.",
|
||||
"settings_export_all": "Alle exportieren...",
|
||||
"settings_export_csv": "CSV exportieren...",
|
||||
"settings_export_key": "Schlüssel exportieren...",
|
||||
"settings_export_all": "Alle exportieren…",
|
||||
"settings_export_csv": "CSV exportieren…",
|
||||
"settings_export_key": "Schlüssel exportieren…",
|
||||
"settings_gradient_bg": "Hintergrund-Verlauf",
|
||||
"settings_gradient_desc": "Strukturierte Hintergründe durch sanfte Verläufe ersetzen",
|
||||
"settings_idle_after": "nach",
|
||||
"settings_import_key": "Privaten Schlüssel importieren...",
|
||||
"settings_import_viewkey": "Anzeigeschlüssel importieren...",
|
||||
"settings_import_key": "Privaten Schlüssel importieren…",
|
||||
"settings_import_viewkey": "Anzeigeschlüssel importieren…",
|
||||
"settings_language_note": "Hinweis: Manche Texte erfordern einen Neustart zur Aktualisierung",
|
||||
"settings_lock_now": "Jetzt sperren",
|
||||
"settings_locked": "Gesperrt",
|
||||
"settings_merge_to_address": "An Adresse zusammenführen...",
|
||||
"settings_merge_to_address": "An Adresse zusammenführen…",
|
||||
"settings_noise_opacity": "Rauschdichte:",
|
||||
"settings_not_connected": "Nicht mit dem Daemon verbunden",
|
||||
"settings_not_encrypted": "Nicht verschlüsselt",
|
||||
"settings_not_found": "Nicht gefunden",
|
||||
"settings_open_app_dir": "App-Ordner öffnen",
|
||||
@@ -1336,29 +1783,36 @@
|
||||
"settings_privacy": "Datenschutz",
|
||||
"settings_quick_unlock_pin": "Schnell-Entsperr-PIN",
|
||||
"settings_reduce_transparency": "Transparenz reduzieren",
|
||||
"settings_reloaded": "Einstellungen von der Festplatte neu geladen",
|
||||
"settings_remove_encryption": "Verschlüsselung entfernen",
|
||||
"settings_remove_pin": "PIN entfernen",
|
||||
"settings_request_payment": "Zahlung anfordern...",
|
||||
"settings_request_payment": "Zahlung anfordern…",
|
||||
"settings_rescan_desc": "Blockchain nach fehlenden Transaktionen neu scannen",
|
||||
"settings_restart_daemon": "Daemon neu starten",
|
||||
"settings_rpc_connection": "RPC-Verbindung",
|
||||
"settings_rpc_error_prefix": "RPC-Fehler: ",
|
||||
"settings_rpc_note": "Hinweis: Verbindungseinstellungen werden automatisch aus DRAGONX.conf erkannt",
|
||||
"settings_rpc_ok": "RPC-Verbindung OK",
|
||||
"settings_save_shielded_desc": "Speichert z-addr Transaktionen in einer lokalen Datei zur Ansicht",
|
||||
"settings_save_shielded_local": "Geschirmten Transaktionsverlauf lokal speichern",
|
||||
"settings_saved": "Einstellungen gespeichert",
|
||||
"settings_set_pin": "PIN festlegen",
|
||||
"settings_shield_mining": "Mining abschirmen...",
|
||||
"settings_shield_mining": "Mining abschirmen…",
|
||||
"settings_solid_colors_desc": "Feste Farben anstelle von Unschärfe-Effekten verwenden (Barrierefreiheit)",
|
||||
"settings_theme_refreshed": "Themenliste aktualisiert",
|
||||
"settings_tor_desc": "Alle Verbindungen für erhöhte Privatsphäre über Tor leiten",
|
||||
"settings_unlocked": "Entsperrt",
|
||||
"settings_use_tor_network": "Tor für Netzwerkverbindungen verwenden",
|
||||
"settings_validate_address": "Adresse überprüfen...",
|
||||
"settings_validate_address": "Adresse überprüfen…",
|
||||
"settings_visual_effects": "Visuelle Effekte",
|
||||
"settings_wallet_file_size": "Wallet-Dateigröße: %s",
|
||||
"settings_wallet_info": "Wallet-Informationen",
|
||||
"settings_wallet_location": "Wallet-Speicherort: %s",
|
||||
"settings_wallet_maintenance": "Wallet-Wartung",
|
||||
"settings_wallet_not_found": "Wallet-Datei nicht gefunden",
|
||||
"settings_wallet_size_label": "Wallet-Größe:",
|
||||
"settings_wallet_size_label": "Wallet-Größe",
|
||||
"settings_ztx_cleared": "Z-Transaktionsverlauf gelöscht",
|
||||
"settings_ztx_not_found": "Keine Verlaufsdatei gefunden",
|
||||
"setup_wizard": "Einrichtungsassistent",
|
||||
"share": "Teilen",
|
||||
"shield_check_status": "Status prüfen",
|
||||
@@ -1406,17 +1860,34 @@
|
||||
"status": "Status",
|
||||
"stop_external": "Externen Daemon stoppen",
|
||||
"stop_mining": "Mining stoppen",
|
||||
"stratum_allowip_hint": "Miner von IP oder CIDR zulassen (leer = nur dieser Computer)",
|
||||
"stratum_expose_warn": "Öffnet einen Mining-Port für das von Ihnen zugelassene Netzwerk — nur in einem vertrauenswürdigen LAN verwenden.",
|
||||
"stratum_host": "Mining-Pool hosten (Stratum)",
|
||||
"stratum_host_hint": "Miner verbinden sich mit diesem Computer über Port 22769 (RPC-Port + 1000) mit einem RandomX-Stratum-Miner. Starten Sie den Daemon neu, um die Änderung zu übernehmen.",
|
||||
"stratum_host_section": "MINING-POOL-HOSTING",
|
||||
"submitting_transaction": "Transaktion wird übermittelt...",
|
||||
"success": "Erfolg",
|
||||
"summary": "Zusammenfassung",
|
||||
"sweep_button": "Fegen",
|
||||
"sweep_caveat": "Importiert den Schlüssel, um eine Transaktion zu signieren, die alle Mittel an Ihre Adresse verschiebt. Der Schlüssel bleibt mit leerem Guthaben in Ihrer Wallet.",
|
||||
"sweep_current_theme_only": "Nur aktuelles Design",
|
||||
"sweep_dest_label": "Gefegte Mittel senden an",
|
||||
"sweep_dest_new": "Neue abgeschirmte Adresse (empfohlen)",
|
||||
"sweep_done": "Fertig — Mittel an Ihre Adresse gefegt.",
|
||||
"sweep_to": "Gefegt an:",
|
||||
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
|
||||
"sweep_tx": "Transaktion:",
|
||||
"swin_connection_failed": "Verbindung fehlgeschlagen: ",
|
||||
"swin_connection_successful": "Verbindung erfolgreich!\ndragonxd-Version: ",
|
||||
"swin_invalid_suffix": " (ungültig)",
|
||||
"swin_no_history_file_found": "Keine Verlaufsdatei gefunden",
|
||||
"swin_rescan_failed": "Neuscan fehlgeschlagen: ",
|
||||
"swin_rescan_started_from_block": "Neuscan gestartet ab Block ",
|
||||
"swin_rescan_to": " bis ",
|
||||
"swin_rpc_client_not_initialized": "RPC-Client nicht initialisiert",
|
||||
"swin_settings_saved": "Einstellungen gespeichert",
|
||||
"swin_theme_list_refreshed": "Design-Liste aktualisiert",
|
||||
"swin_ztx_history_cleared": "Z-Transaktionsverlauf gelöscht",
|
||||
"switch_corrupt_body": "Diese Wallet scheint beschädigt zu sein – der Knoten konnte sie nicht öffnen. Aus einem Backup wiederherstellen, neu erstellen oder eine Reparatur versuchen.",
|
||||
"switch_corrupt_repair": "Reparatur versuchen (Salvage)",
|
||||
"switch_progress_background": "Im Hintergrund fortsetzen",
|
||||
@@ -1441,6 +1912,7 @@
|
||||
"theme": "Design",
|
||||
"theme_effects": "Design-Effekte",
|
||||
"theme_language": "THEMA & SPRACHE",
|
||||
"tile_click_to_open": "Zum Öffnen klicken",
|
||||
"time_days_ago": "vor %d Tagen",
|
||||
"time_hours_ago": "vor %d Stunden",
|
||||
"time_minutes_ago": "vor %d Minuten",
|
||||
@@ -1455,7 +1927,9 @@
|
||||
"to_upper": "AN",
|
||||
"tools": "WERKZEUGE",
|
||||
"tools_actions": "Werkzeuge & Aktionen...",
|
||||
"tools_actions_hdr": "WERKZEUGE & AKTIONEN",
|
||||
"total": "Gesamt",
|
||||
"total_balance_label": "Gesamtguthaben",
|
||||
"transaction_id": "TRANSAKTIONS-ID",
|
||||
"transaction_sent": "Transaktion erfolgreich gesendet",
|
||||
"transaction_sent_msg": "Transaktion gesendet!",
|
||||
@@ -1477,7 +1951,7 @@
|
||||
"tt_auto_shield": "Transparentes Guthaben automatisch an geschirmte Adressen für Datenschutz verschieben",
|
||||
"tt_backup": "Eine Sicherungskopie Ihrer wallet.dat erstellen",
|
||||
"tt_block_explorer": "Den DragonX Block-Explorer im Browser öffnen",
|
||||
"tt_blur": "Unschärfe-Stärke (0%% = aus, 100%% = maximum)",
|
||||
"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)",
|
||||
@@ -1540,7 +2014,7 @@
|
||||
"tt_low_spec": "Alle aufwendigen visuellen Effekte deaktivieren\\nHotkey: Ctrl+Shift+Down",
|
||||
"tt_merge": "Mehrere UTXOs einer Adresse zusammenführen",
|
||||
"tt_mine_idle": "Mining automatisch starten, wenn das\\nSystem inaktiv ist (keine Tastatur-/Mauseingabe)",
|
||||
"tt_noise": "Körnungstextur-Intensität (0%% = aus, 100%% = maximum)",
|
||||
"tt_noise": "Körnungstextur-Intensität (0% = aus, 100% = maximum)",
|
||||
"tt_open_app_dir": "Den ObsidianDragon-Ordner (Einstellungen, Themes, Logs) im Dateimanager öffnen",
|
||||
"tt_open_data_dir": "Den Ordner mit Ihren Wallet- und Blockchain-Daten im Dateimanager öffnen",
|
||||
"tt_open_dir": "Klicken, um im Dateimanager zu öffnen",
|
||||
@@ -1574,14 +2048,17 @@
|
||||
"tt_simple_bg": "Einen einfachen Verlauf für den Hintergrund verwenden\\nHotkey: Ctrl+Up",
|
||||
"tt_simple_bg_alt": "Eine Verlaufsversion des Theme-Hintergrundbilds verwenden\\nHotkey: Ctrl+Up",
|
||||
"tt_stop_external": "Gilt bei Verbindung zu einem Daemon,\\nder außerhalb dieser Wallet gestartet wurde",
|
||||
"tt_stratum_host": "Betreibt einen RandomX-Stratum-Pool-Server auf diesem Node, sodass andere RandomX-Miner diesen Computer ansprechen können. Erfordert einen Node ab v1.3.0 und einen Daemon-Neustart, um wirksam zu werden.",
|
||||
"tt_sweep_current_theme_only": "Nur das aktive Design durchsuchen, statt jedes Design zu durchlaufen",
|
||||
"tt_test_conn": "Die RPC-Verbindung zum Daemon überprüfen",
|
||||
"tt_theme_effects": "Schimmer, Glühen, Farbton-Zyklen pro Theme",
|
||||
"tt_theme_hotkey": "Hotkey: Ctrl+Links/Rechts zum Wechseln der Themes",
|
||||
"tt_tor": "Daemon-Verbindungen für Anonymität über das Tor-Netzwerk leiten",
|
||||
"tt_tx_url": "Basis-URL zum Anzeigen von Transaktionen in einem Block-Explorer",
|
||||
"tt_ui_opacity": "Karten- und Seitenleisten-Deckkraft (100%% = vollständig undurchsichtig, niedriger = durchsichtiger)",
|
||||
"tt_ui_opacity": "Karten- und Seitenleisten-Deckkraft (100% = vollständig undurchsichtig, niedriger = durchsichtiger)",
|
||||
"tt_validate": "Prüfen, ob eine DragonX-Adresse gültig ist",
|
||||
"tt_verbose": "Detaillierte Verbindungsdiagnosen,\\nDaemon-Status und Port-Besitzer-Info\\nin der Konsolen-Registerkarte protokollieren",
|
||||
"tt_wallet_size_warn": "Geschützte Wallets wachsen mit den Witness-Daten jedes Notes — das Zusammenführen vieler Notes in eine Adresse verringert die Größe. Zuerst sichern.",
|
||||
"tt_wallets_button": "Ihre Wallet-Dateien auflisten und zwischen ihnen wechseln",
|
||||
"tt_website": "Die DragonX-Website öffnen",
|
||||
"tt_window_opacity": "Hintergrund-Deckkraft (niedriger = Desktop durch Fenster sichtbar)",
|
||||
@@ -1632,6 +2109,7 @@
|
||||
"validate_not_mine": "Nicht im Besitz dieser Wallet",
|
||||
"validate_ownership": "Eigentum:",
|
||||
"validate_results": "Ergebnisse:",
|
||||
"validate_results_placeholder": "Ergebnisse erscheinen hier",
|
||||
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
|
||||
"validate_status": "Status:",
|
||||
"validate_title": "Adresse validieren",
|
||||
@@ -1647,8 +2125,68 @@
|
||||
"view_on_explorer": "Im Explorer anzeigen",
|
||||
"waiting_for_daemon": "Warte auf Daemon-Verbindung...",
|
||||
"wallet": "WALLET",
|
||||
"wallet_degraded_notify": "Ihre Wallet wurde im eingeschränkten Modus geöffnet: Vorhandene Guthaben sind sicher und verfügbar, aber das Erstellen neuer Adressen und das Schützen von Guthaben sind deaktiviert. Sichern Sie Ihre Seed-Phrase und stellen Sie sie wieder her, um die Wallet vollständig zu reparieren.",
|
||||
"wallet_diagnostics_hdr": "DIAGNOSE",
|
||||
"wallet_empty": "Ihre Wallet ist leer",
|
||||
"wallet_empty_hint": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
|
||||
"wallet_options_hdr": "OPTIONEN",
|
||||
"wallet_rebuild_failed": "Der Neuaufbau hat keine gültige Wallet erzeugt, daher wurde nichts geändert. Ihre Wallet ist unangetastet.",
|
||||
"wallet_rebuild_install_failed": "Die neu aufgebaute Wallet konnte nicht installiert werden; Ihre aktuelle Wallet wurde beibehalten.",
|
||||
"wallet_rebuild_no_helper": "Der Wallet-Neuaufbau-Helfer ist in diesem Build nicht verfügbar. Verwenden Sie Wiederherstellen, oder bauen Sie manuell neu auf.",
|
||||
"wallet_rebuild_no_source": "Es konnte keine lesbare Wallet zum Neuaufbau gefunden werden. Nichts wurde geändert.",
|
||||
"wallet_rebuild_ok": "Wallet-Datenbank neu aufgebaut — der Node lädt sie und scannt erneut nach Ihrem Guthaben.",
|
||||
"wallet_rebuild_started": "Ihre Wallet-Datei wird repariert — bitte schließen Sie dieses Fenster nicht.",
|
||||
"wallet_recovered_body": "Es wurde nichts gelöscht. Ihre ursprüngliche Wallet ist weiterhin als datierte Sicherungsdatei auf Ihrem Computer gespeichert, und jede Option unten kopiert oder benennt Dateien nur um — gelöscht wird nie etwas. Ihre Schlüssel werden nie neu erzeugt, nur erneut eingelesen.\n\nDie jetzt geladene reparierte Kopie fehlen möglicherweise ein paar aktuelle Transaktionen, daher kann Ihr Guthaben etwas niedrig erscheinen, bis Sie unten abschließen und es neu gescannt wird.",
|
||||
"wallet_recovered_dismiss": "Nicht jetzt — reparierte Kopie behalten",
|
||||
"wallet_recovered_dismiss_sub": "Es werden keine Dateien geändert. Sie können dies jederzeit über die Statusleiste erneut öffnen; Ihr Original bleibt so oder so sicher gesichert.",
|
||||
"wallet_recovered_notify": "Ihre Wallet-Datei musste repariert werden — Ihr Original wurde sicher gesichert. Öffnen Sie die App, um Ihre Optionen zu prüfen.",
|
||||
"wallet_recovered_open_folder": "Dateien anzeigen",
|
||||
"wallet_recovered_open_folder_sub": "Öffnet den Wallet-Datenordner, damit Sie die Sicherungsdateien selbst prüfen können — es wird nichts geändert.",
|
||||
"wallet_recovered_rebuild": "Automatisch reparieren (empfohlen)",
|
||||
"wallet_recovered_rebuild_sub": "Liest jeden wiederherstellbaren Datensatz aus Ihrer vollständigsten Wallet-Datei erneut ein und schreibt eine saubere neue Datei, dann wird neu gestartet. Die gründlichste Option — Ihre aktuelle Datei wird so oder so als datierte Sicherung behalten.",
|
||||
"wallet_recovered_restore": "Stattdessen die Originaldatei wiederherstellen",
|
||||
"wallet_recovered_restore_sub": "Setzt Ihre größte unveränderte Sicherung unverändert zurück, dann wird neu gescannt — etwas schneller, aber nur so vollständig, wie diese eine Datei war. Ihre aktuelle Datei wird so oder so als datierte Sicherung behalten.",
|
||||
"wallet_recovered_safety": "Ihre Coins sind sicher.",
|
||||
"wallet_recovered_title": "Ihre Wallet-Datei benötigt eine kurze Reparatur",
|
||||
"wallet_recovered_warn": "Beim Start der App wurde festgestellt, dass Ihre Wallet-Datei die Konsistenzprüfung nicht bestanden hat — das passiert meist nach einem App-Update oder einem unsauberen Herunterfahren. Die App hat Ihre Daten bereits geschützt: Sie hat die alte Datei beiseitegelegt und eine reparierte Kopie geladen, damit Sie nicht feststecken.",
|
||||
"wallet_recovery_decide_later": "Später entscheiden",
|
||||
"wallet_recovery_decide_later_tip": "Schließt dies und behält die jetzt geladene Kopie. Es wird nichts geändert, und Sie können jederzeit reparieren — die Statusleiste behält einen Link „Wallet-Reparatur verfügbar“.",
|
||||
"wallet_recovery_details_label": "Technische Details anzeigen",
|
||||
"wallet_recovery_done": "Fertig",
|
||||
"wallet_recovery_failure_body": "Die Reparatur wurde ausgeführt, aber ihr Ergebnis hat die Überprüfung nicht bestanden (es konnte nicht gelesen werden oder hatte keine Adressen), daher wurde es automatisch verworfen, bevor es jemals etwas ersetzt hat. Ihre Wallet ist genau so, wie sie vor dem Klick war — auf der Festplatte hat sich nichts geändert.",
|
||||
"wallet_recovery_failure_title": "Die Reparatur wurde nicht abgeschlossen",
|
||||
"wallet_recovery_files_detail": "Ihre ursprüngliche Wallet ist noch vorhanden — die App hat sie in Ihrem Datenordner in eine datierte Sicherung (wallet.<Zahlen>.bak) umbenannt und nichts gelöscht. „Reparieren“ liest jeden Datensatz aus Ihrer vollständigsten Wallet in eine brandneue, saubere Datei ein. „Wiederherstellen“ kopiert Ihre größte Sicherung exakt so zurück, wie sie ist. Die gerade geladene Kopie wird ebenfalls als Sicherung behalten.",
|
||||
"wallet_recovery_files_label": "Was passiert mit meinen Dateien?",
|
||||
"wallet_recovery_notnow_short": "Nicht jetzt",
|
||||
"wallet_recovery_other_options": "Weitere Optionen",
|
||||
"wallet_recovery_rebuild_card": "Automatisch reparieren",
|
||||
"wallet_recovery_rebuild_card_desc": "Liest jeden wiederherstellbaren Datensatz in eine saubere Datei ein, dann wird neu gestartet. Die gründlichste Option.",
|
||||
"wallet_recovery_recommended": "EMPFOHLEN",
|
||||
"wallet_recovery_repair_go": "Reparieren",
|
||||
"wallet_recovery_rescan_body": "Die Blockchain wird neu gescannt, um Ihr Guthaben und Ihren Transaktionsverlauf wiederherzustellen. Dies kann mehrere Minuten dauern — Sie müssen nichts tun, und starten Sie den Node bitte nicht neu, während dies läuft.",
|
||||
"wallet_recovery_rescan_elapsed": "Arbeitet seit %s",
|
||||
"wallet_recovery_rescan_size": "Die reparierte Wallet ist jetzt %s groß und füllt sich weiter",
|
||||
"wallet_recovery_rescan_slow": "Dies dauert länger als gewöhnlich, was bei einer großen Wallet normal ist. Es ist sicher, es weiterlaufen zu lassen — Sie können den Fortschritt unter Erweitert ▸ Konsole beobachten.",
|
||||
"wallet_recovery_rescan_title": "Ihre Wallet-Reparatur wird abgeschlossen",
|
||||
"wallet_recovery_restore_card": "Original wiederherstellen",
|
||||
"wallet_recovery_restore_card_desc": "Setzt Ihre größte unveränderte Sicherung unverändert zurück — nur so vollständig, wie diese Datei war.",
|
||||
"wallet_recovery_restore_go": "Wiederherstellen",
|
||||
"wallet_recovery_success_body": "Die App hat Ihre reparierte Wallet geladen und scannt neu, um Ihr Guthaben zu summieren — dies kann einige Minuten dauern. Sie liest jeden Datensatz, den sie kann, aber bei seltenen beschädigten Dateien kann sie etwas weniger — oder gelegentlich mehr — Adressen wiederherstellen als zuvor. Prüfen Sie nach Abschluss des Neuscans, ob Guthaben und Verlauf korrekt aussehen.",
|
||||
"wallet_recovery_success_restore": "Ihre größte Sicherungsdatei ist wieder an ihrem Platz und wird jetzt geladen, und die App scannt neu. Sie wurde unverändert wiederhergestellt, ist also genau so vollständig, wie diese Datei war — prüfen Sie Ihr Guthaben, sobald der Neuscan abgeschlossen ist.",
|
||||
"wallet_recovery_try_other": "Die andere Option versuchen",
|
||||
"wallet_recovery_whats_happened": "Was ist passiert?",
|
||||
"wallet_recovery_working_label": "Wird ausgeführt",
|
||||
"wallet_restore_bad_backup": "Die gesicherte Wallet-Datei scheint unlesbar zu sein, daher wurde sie NICHT wiederhergestellt — Ihre aktuelle Wallet ist unverändert. Stellen Sie stattdessen von Ihrer eigenen Sicherung wieder her.",
|
||||
"wallet_restore_busy": "Der Node startet gerade neu — versuchen Sie es gleich noch einmal.",
|
||||
"wallet_restore_copy_failed": "Die gesicherte Wallet konnte nicht installiert werden; Ihre aktuelle Wallet wurde beibehalten.",
|
||||
"wallet_restore_move_failed": "Die aktuelle Wallet konnte nicht beiseitegelegt werden — es wurde nichts geändert.",
|
||||
"wallet_restore_no_backup": "Es konnte keine wallet.<Zeitstempel>.bak zum Wiederherstellen gefunden werden. Es wurde nichts geändert.",
|
||||
"wallet_restore_no_restart": "Ihre ursprüngliche Wallet wurde wiederhergestellt, aber der Node wurde nicht neu gestartet — starten Sie ihn über die Einstellungen.",
|
||||
"wallet_restore_ok": "Ursprüngliche Wallet wiederhergestellt. Der Node lädt sie jetzt.",
|
||||
"wallet_restore_started": "Ihre Originaldatei wird wiederhergestellt — bitte schließen Sie dieses Fenster nicht.",
|
||||
"wallet_restore_stop_failed": "Der Node hat nicht rechtzeitig gestoppt, daher wurde nichts geändert. Versuchen Sie es erneut.",
|
||||
"wallet_size_consolidate": "Notizen konsolidieren…",
|
||||
"wallet_size_warn": "Diese Wallet-Datei ist groß. Das Konsolidieren Ihrer Notizen kann weiteres Wachstum eindämmen.",
|
||||
"wallets_active": "Aktiv",
|
||||
"wallets_add": "Ordner hinzufügen",
|
||||
"wallets_add_folder": "Auch einen weiteren Ordner nach Wallet-Dateien durchsuchen:",
|
||||
@@ -1691,6 +2229,7 @@
|
||||
"wallets_open_inplace_tt": "Öffnet diese Wallet an ihrem Ort — im Datenverzeichnis verlinkt (keine Kopie)",
|
||||
"wallets_remove_folder": "Diesen Ordner nicht mehr durchsuchen",
|
||||
"wallets_reveal": "Ordner anzeigen",
|
||||
"wallets_scan_folder": "Einen weiteren Ordner nach Wallets durchsuchen…",
|
||||
"wallets_scanned_folders": "Durchsuchte Ordner:",
|
||||
"wallets_sort_addresses": "Adressen",
|
||||
"wallets_sort_asc": "Aufsteigend (ältestes / wenigste / kleinstes zuerst)",
|
||||
@@ -1760,18 +2299,30 @@
|
||||
"wiz_welcome_sub": "Ein paar schnelle Einstellungen und Ihr Full Node ist bereit.",
|
||||
"wiz_welcome_title": "Willkommen bei ObsidianDragon!",
|
||||
"wizard_daemon_start_failed": "Daemon-Start fehlgeschlagen — wird automatisch wiederholt",
|
||||
"xmrig_active_tt": "Der aktuell installierte (aktive) Miner",
|
||||
"xmrig_browse_releases": "Alle Releases durchsuchen…",
|
||||
"xmrig_checking": "Nach dem neuesten Miner wird gesucht…",
|
||||
"xmrig_current": "Aktuell:",
|
||||
"xmrig_downgrade_note": "Das Installieren einer anderen Miner-Version ersetzt die aktuelle; stoppen Sie zuerst das Mining, dann wird sie beim nächsten Start wirksam.",
|
||||
"xmrig_download_install": "Herunterladen & installieren",
|
||||
"xmrig_downloading": "Wird heruntergeladen…",
|
||||
"xmrig_install_this": "Diese Version installieren",
|
||||
"xmrig_installed": "Installiert:",
|
||||
"xmrig_installed_ok": "Miner installiert",
|
||||
"xmrig_installing": "Wird installiert…",
|
||||
"xmrig_latest": "Neueste:",
|
||||
"xmrig_latest_badge": "neueste",
|
||||
"xmrig_latest_tt": "Neueste verfügbare Version",
|
||||
"xmrig_loading_releases": "Releases werden geladen…",
|
||||
"xmrig_no_notes": "Keine Versionshinweise für diese Version.",
|
||||
"xmrig_none": "keiner",
|
||||
"xmrig_prerelease_tt": "Vorabversion / Testbuild",
|
||||
"xmrig_reinstall": "Neu installieren",
|
||||
"xmrig_released": "Veröffentlicht:",
|
||||
"xmrig_releases": "xmrig-Releases",
|
||||
"xmrig_status_current": "Diese Version ist installiert",
|
||||
"xmrig_status_latest": "Neueste Version",
|
||||
"xmrig_status_older": "Älter als Ihre installierte Version",
|
||||
"xmrig_stop_mining_first": "Stoppen Sie das Mining, bevor Sie den Miner aktualisieren.",
|
||||
"xmrig_unavailable_body": "Für diese Plattform ist kein Miner-Build verfügbar.",
|
||||
"xmrig_unavailable_title": "Miner-Updates nicht verfügbar",
|
||||
|
||||
+568
-17
@@ -19,6 +19,7 @@
|
||||
"about_license_text": "Este software se distribuye bajo la Licencia Pública General de GNU v3 (GPLv3). Usted es libre de usar, modificar y distribuir este software bajo los términos de la licencia.",
|
||||
"about_peers_count": "%zu nodos",
|
||||
"about_release": "Producción",
|
||||
"about_source": "Código fuente",
|
||||
"about_title": "Acerca de ObsidianDragon",
|
||||
"about_version": "Versión:",
|
||||
"about_website": "Sitio Web",
|
||||
@@ -60,7 +61,84 @@
|
||||
"amount_label": "Cantidad:",
|
||||
"animate_avatars": "Animar avatares",
|
||||
"appearance": "APARIENCIA",
|
||||
"appx_back": "Atrás",
|
||||
"appx_back_up_seed_phrase_title": "Haz una copia de tu frase semilla",
|
||||
"appx_birthday_block_height": "Fecha de creación (altura de bloque): %llu — guárdala también.",
|
||||
"appx_blockchain_data_deleted": "Datos de la cadena de bloques eliminados (%d elementos). El daemon se está reiniciando para volver a sincronizar desde la red.",
|
||||
"appx_blockchain_maintenance_in_progress": "Ya hay una operación de mantenimiento de la cadena de bloques en curso.",
|
||||
"appx_blockchain_rescan_complete": "Reescaneo de la cadena de bloques completado",
|
||||
"appx_bootstrap_complete_reconciling": "Arranque inicial completado; reconciliando tu cartera con los nuevos datos de la cadena.",
|
||||
"appx_cancel": "Cancelar",
|
||||
"appx_cleaning_up": "Limpiando...",
|
||||
"appx_confirm_your_backup": "Confirma tu copia de seguridad",
|
||||
"appx_copied_clipboard_autoclears": "Copiado; el portapapeles se borra automáticamente en 45 s",
|
||||
"appx_copy": "Copiar",
|
||||
"appx_could_not_start_restore": "No se pudo iniciar la restauración",
|
||||
"appx_create_failed_prefix": "Error al crear: ",
|
||||
"appx_creating_your_wallet": "Creando tu cartera…",
|
||||
"appx_daemon_error": "Error del daemon",
|
||||
"appx_daemon_reinstall_in_progress": "La reinstalación del daemon ya está en curso.",
|
||||
"appx_disconnecting": "Desconectando...",
|
||||
"appx_done": "Listo",
|
||||
"appx_dragonxd_output": "Salida de dragonxd",
|
||||
"appx_encrypting_wallet": "Cifrando la cartera...",
|
||||
"appx_fullnode_lifecycle_unavailable_lite": "Las acciones de ciclo de vida del nodo completo no están disponibles en la versión lite",
|
||||
"appx_installing_bundled_daemon": "Instalando el daemon incluido; el nodo se detendrá, se actualizará y se reiniciará...",
|
||||
"appx_invalid_payment_uri_prefix": "URI de pago no válida: ",
|
||||
"appx_ive_written_it_down": "Ya la anoté",
|
||||
"appx_keep_node_running_and_quit": "Mantener el nodo y salir",
|
||||
"appx_last_block_n": "Último bloque: %d",
|
||||
"appx_last_used_wallet_not_found_prefix": "Tu último archivo de cartera usado (",
|
||||
"appx_last_used_wallet_not_found_suffix": ") no se encontró; se abrió la cartera predeterminada en su lugar. Si lo moviste, restáuralo y vuelve a él desde la lista de carteras.",
|
||||
"appx_low_spec_mode_disabled": "Modo de bajos recursos desactivado",
|
||||
"appx_low_spec_mode_enabled": "Modo de bajos recursos activado",
|
||||
"appx_miner_stopped_prefix": "Minero detenido: ",
|
||||
"appx_miner_stopped_unexpectedly": "El minero se detuvo inesperadamente.",
|
||||
"appx_n_min_n_sec": "%d min %d s",
|
||||
"appx_n_seconds": "%d segundos",
|
||||
"appx_no_bundled_daemon_to_install": "Esta versión no incluye un daemon para instalar",
|
||||
"appx_no_embedded_daemon_to_install": "Esta versión no incluye un daemon integrado para instalar",
|
||||
"appx_node_busy_restarting": "El nodo está ocupado reiniciándose; inténtalo de nuevo en un momento.",
|
||||
"appx_node_rebuilding_witness_cache": "El nodo está reconstruyendo su caché de testigos",
|
||||
"appx_not_next_word": " — esa no es la siguiente palabra",
|
||||
"appx_payment_request_loaded": "Solicitud de pago cargada",
|
||||
"appx_pool_miner_connected_and_hashing": "Minero de pool conectado y calculando hashes.",
|
||||
"appx_progress_n_of_n": "Progreso: %d / %d",
|
||||
"appx_rebuilding_sapling_note_witnesses": "Reconstruyendo testigos de notas Sapling…",
|
||||
"appx_rebuilding_witness_cache_blocks_left": "Reconstruyendo la caché de testigos %.0f%% — %d bloques restantes",
|
||||
"appx_rebuilding_witness_cache_pct": "Reconstruyendo la caché de testigos %.0f%%",
|
||||
"appx_recovery_phrase_word_count": "La frase de recuperación debe tener 24 palabras; tú tienes %d.",
|
||||
"appx_restarting_daemon_rescan_flag": "Reiniciando el daemon con la opción -rescan...",
|
||||
"appx_restarting_daemon_zapwallettxes": "Reiniciando el daemon con -zapwallettxes=2 (reparación de cartera)...",
|
||||
"appx_restoring_your_wallet": "Restaurando tu cartera…",
|
||||
"appx_seed_backup_warning": "Estas 24 palabras son la ÚNICA forma de restaurar tu cartera. Anótalas en orden, guárdalas sin conexión y nunca las compartas. Si las pierdes, tus fondos se perderán para siempre.",
|
||||
"appx_seed_not_backed_up_warning": "No has hecho una copia de tu semilla; podrías perder tus fondos. ¿Omitir de todos modos?",
|
||||
"appx_sending_stop_command_to_daemon": "Enviando el comando de parada al daemon...",
|
||||
"appx_setting_initial_sapling_witnesses": "Estableciendo testigos Sapling iniciales %.0f%%",
|
||||
"appx_shutdown_complete": "Apagado completado",
|
||||
"appx_simple_background_disabled": "Fondo simple desactivado",
|
||||
"appx_simple_background_enabled": "Fondo simple activado",
|
||||
"appx_skip": "Omitir",
|
||||
"appx_skip_anyway": "Omitir de todos modos",
|
||||
"appx_still_status_prefix": "Aún \"",
|
||||
"appx_still_status_suffix": "\" — forzar el cierre ahora podría dañar los datos de la cadena.",
|
||||
"appx_stop_anyway_and_quit": "Detener de todos modos y salir",
|
||||
"appx_stopping_daemon_deleting_blockchain": "Deteniendo el daemon y eliminando los datos de la cadena de bloques...",
|
||||
"appx_stopping_node_discards_rebuild": "Detener el nodo ahora descarta la reconstrucción en curso y la reinicia (varios minutos) la próxima vez que abras la cartera. También puedes dejar el nodo en ejecución.",
|
||||
"appx_stopping_pool_miner": "Deteniendo el minero de pool...",
|
||||
"appx_syncing_pct_block_n_of_n": "Sincronizando %.1f%% — Bloque %d / %d",
|
||||
"appx_tap_words_in_order": "Toca las palabras en el orden correcto para confirmar que las guardaste.",
|
||||
"appx_theme_effects_disabled": "Efectos de tema desactivados",
|
||||
"appx_theme_effects_enabled": "Efectos de tema activados",
|
||||
"appx_theme_prefix": "Tema: ",
|
||||
"appx_use_settings_restart_daemon_hint": "Usa Ajustes > Reiniciar daemon para intentarlo de nuevo",
|
||||
"appx_waiting_for_daemon_to_encrypt_wallet": "Esperando a que el daemon cifre la cartera...",
|
||||
"appx_wallet_created_and_backed_up": "Cartera creada y respaldada.",
|
||||
"appx_wallet_open_failed_prefix": "Error al abrir la cartera: ",
|
||||
"auto_shield": "Auto-proteger minería",
|
||||
"autoshield_by_node": "Tu nodo blinda automáticamente el coinbase minado por ti.",
|
||||
"autoshield_off_backup_seed": "El coinbase minado aún no se está blindando automáticamente. Respalda tu frase de recuperación (Ajustes → Copia de seguridad y datos) para que tu nodo pueda activarlo de forma segura.",
|
||||
"autoshield_off_generic": "Tu nodo no está blindando automáticamente el coinbase minado en este momento.",
|
||||
"av_intro": "El software de minería suele marcarse como potencialmente no deseado. Sigue estos pasos para habilitar la minería en pool:",
|
||||
"av_open_security": "Abrir Seguridad de Windows",
|
||||
"av_step1": "Paso 1: Agregar exclusión",
|
||||
@@ -74,6 +152,9 @@
|
||||
"av_title": "Windows Defender bloqueó el minero",
|
||||
"available": "Disponible",
|
||||
"backup_backing_up": "Respaldando...",
|
||||
"backup_col_backup": "COPIA DE SEGURIDAD",
|
||||
"backup_col_export": "EXPORTAR",
|
||||
"backup_col_import": "IMPORTAR Y RESTAURAR",
|
||||
"backup_create": "Crear Respaldo",
|
||||
"backup_created": "Respaldo de cartera creado",
|
||||
"backup_data": "RESPALDO Y DATOS",
|
||||
@@ -92,8 +173,22 @@
|
||||
"balance": "Saldo",
|
||||
"balance_history_collecting": "Historial de saldo — recopilando datos...",
|
||||
"balance_layout": "Diseño de Saldo",
|
||||
"balance_layout_switched": "Diseño: %s",
|
||||
"balance_mining_rate": "Minando %s",
|
||||
"balance_shielded_fmt": "Protegido: %.8f",
|
||||
"balance_syncing_pct": "Sincronizando %.1f%%",
|
||||
"balance_transparent_fmt": "Transparente: %.8f",
|
||||
"baltab_market": "Mercado",
|
||||
"baltab_market_price_4dp": "Mercado: $%.4f",
|
||||
"baltab_market_price_8dp": "Mercado: $%.8f",
|
||||
"baltab_pct_change_24h": "%s%.1f%% 24h",
|
||||
"baltab_pct_of_total_zaddr": "%.0f%% del total · %d Z-addr",
|
||||
"baltab_shielded": "Blindado",
|
||||
"baltab_shielded_amount": "Blindado %.8f",
|
||||
"baltab_t_addresses_count": "%d T-addresses",
|
||||
"baltab_total_balance": "Saldo total",
|
||||
"baltab_transparent": "Transparente",
|
||||
"baltab_transparent_amount": "Transparente %.8f",
|
||||
"ban": "Bloquear",
|
||||
"banned_peers": "Nodos Bloqueados",
|
||||
"block": "Bloque",
|
||||
@@ -101,6 +196,12 @@
|
||||
"block_click_copy": "Clic para copiar",
|
||||
"block_click_next": "Clic para ver bloque siguiente",
|
||||
"block_click_prev": "Clic para ver bloque anterior",
|
||||
"block_db_reindex_body": "Esto suele ocurrir después de que una actualización del daemon cambia el formato en disco, o si el índice de bloques está dañado. Tu cartera y tus monedas están a salvo — el nodo simplemente no puede cargar la cadena, así que los saldos aparecen en cero.\n\nReconstruir vuelve a leer tus archivos de bloques existentes y puede tardar un rato (también reescanea tu cartera). No se descarga nada.",
|
||||
"block_db_reindex_confirm": "Reconstruir base de datos de bloques",
|
||||
"block_db_reindex_notify": "El nodo no puede leer su base de datos de bloques (suele pasar tras una actualización del daemon). Reconstrúyela para restaurar tu saldo — consulta el aviso, o Ajustes › Nodo.",
|
||||
"block_db_reindex_started": "Reconstruyendo la base de datos de bloques a partir de tus bloques — esto puede tardar un rato.",
|
||||
"block_db_reindex_title": "¿Reconstruir base de datos de bloques?",
|
||||
"block_db_reindex_warn": "El nodo no puede leer su base de datos de bloques.",
|
||||
"block_explorer": "Explorador de Bloques",
|
||||
"block_get_info": "Obtener Info del Bloque",
|
||||
"block_hash": "Hash del Bloque:",
|
||||
@@ -132,6 +233,7 @@
|
||||
"bootstrap_verifying": "Verificando sumas de comprobación...",
|
||||
"bootstrap_wallet_protected": "(wallet.dat está protegido)",
|
||||
"bootstrap_warning": "Los datos de bloques existentes (blocks, chainstate, notarizations) se eliminarán y reemplazarán. Su wallet.dat NO será modificado ni eliminado.",
|
||||
"byte_count_fmt": "%zu / %zu bytes",
|
||||
"cancel": "Cancelar",
|
||||
"change_pass_confirm": "Confirmar nueva:",
|
||||
"change_pass_current": "Frase de contraseña actual:",
|
||||
@@ -147,6 +249,10 @@
|
||||
"chat_accent_theme": "Tema",
|
||||
"chat_add_contact": "Añadir contacto",
|
||||
"chat_awaiting_key": "Esperando respuesta",
|
||||
"chat_blocked_desc": "Las conversaciones bloqueadas se eliminan y sus mensajes se descartan — antiguos y nuevos — hasta que las desbloquees. Desbloquear reimporta la conversación desde la cadena.",
|
||||
"chat_blocked_manage": "Bloqueadas",
|
||||
"chat_blocked_title": "Conversaciones bloqueadas",
|
||||
"chat_blocked_toast": "Conversación eliminada y bloqueada",
|
||||
"chat_bubble_minimal": "Mínima",
|
||||
"chat_bubble_rounded": "Redondeada",
|
||||
"chat_bubble_square": "Cuadrada",
|
||||
@@ -159,6 +265,16 @@
|
||||
"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_delete": "Eliminar conversación",
|
||||
"chat_delete_block": "Eliminar y bloquear",
|
||||
"chat_delete_body_prefix": "¿Eliminar tu copia local de la conversación con ",
|
||||
"chat_delete_body_suffix": "?",
|
||||
"chat_delete_confirm": "Eliminar",
|
||||
"chat_delete_failed": "No se pudo eliminar la conversación — no se cambió nada.",
|
||||
"chat_delete_local_note": "Esto solo afecta a este dispositivo — los mensajes permanecen en la cadena de bloques y la otra persona conserva su copia.",
|
||||
"chat_delete_revive_note": "«Eliminar» borra el historial en este dispositivo. Si esa persona vuelve a escribirte, la conversación reaparece.",
|
||||
"chat_delete_title": "¿Eliminar conversación?",
|
||||
"chat_deleted_toast": "Conversación eliminada",
|
||||
"chat_density_comfortable": "Cómoda",
|
||||
"chat_density_compact": "Compacta",
|
||||
"chat_emoji_color": "Color",
|
||||
@@ -182,6 +298,7 @@
|
||||
"chat_new_button": "Nuevo chat",
|
||||
"chat_new_message": "Mensaje",
|
||||
"chat_new_message_toast": "Nuevo mensaje de chat cifrado",
|
||||
"chat_new_needs_zaddr": "El chat necesita una dirección blindada (z) — las direcciones transparentes (t) no pueden recibir mensajes cifrados.",
|
||||
"chat_new_send": "Enviar solicitud",
|
||||
"chat_new_title": "Nuevo chat",
|
||||
"chat_new_zaddr": "Dirección z del destinatario",
|
||||
@@ -227,6 +344,8 @@
|
||||
"chat_ts_24h": "24 horas",
|
||||
"chat_ts_global": "Seguir global",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unblock": "Desbloquear",
|
||||
"chat_unblocked_toast": "Conversación desbloqueada",
|
||||
"chat_unhide": "Mostrar",
|
||||
"chat_unmute": "Reactivar",
|
||||
"chat_verify_key": "Clave de identidad: compárala para verificar",
|
||||
@@ -279,6 +398,7 @@
|
||||
"console": "Consola",
|
||||
"console_accents": "Acentos de color",
|
||||
"console_app": "App",
|
||||
"console_auto_focus": "Enfocar la entrada al abrir",
|
||||
"console_auto_scroll": "Auto-desplazamiento",
|
||||
"console_available_commands": "Comandos disponibles:",
|
||||
"console_backend_reference": "Referencia de Comandos del Backend",
|
||||
@@ -373,11 +493,15 @@
|
||||
"console_tab_completion": "Tab para completar",
|
||||
"console_text_colors": "Colores de texto",
|
||||
"console_toggle_accents": "Alternar acentos de color de línea",
|
||||
"console_toggle_auto_focus": "Coloca el cursor en el cuadro de comandos al abrir la pestaña Consola",
|
||||
"console_toggle_text_color": "Alternar colores de texto de línea",
|
||||
"console_type_help": "Escribe 'help' para ver los comandos disponibles",
|
||||
"console_welcome": "Bienvenido a la Consola de ObsidianDragon",
|
||||
"console_zoom_in": "Acercar",
|
||||
"console_zoom_out": "Alejar",
|
||||
"consolidate_desc": "Combina muchas entradas pequeñas en una sola nota blindada. Menos notas significa un archivo de cartera más pequeño y mejor privacidad.",
|
||||
"consolidate_funds_btn": "Consolidar",
|
||||
"consolidate_title": "Consolidar fondos",
|
||||
"contact_avatar": "AVATAR",
|
||||
"contact_avatar_bad_image": "No se pudo cargar esa imagen.",
|
||||
"contact_avatar_badge": "Insignia",
|
||||
@@ -407,6 +531,7 @@
|
||||
"contacts_shape_square": "Cuadrado",
|
||||
"contacts_shape_tab": "Pestaña",
|
||||
"copied": "¡Copiado!",
|
||||
"copied_to_clipboard": "Copiado al portapapeles",
|
||||
"copy": "Copiar",
|
||||
"copy_address": "Copiar Dirección Completa",
|
||||
"copy_error": "Copiar Error",
|
||||
@@ -419,11 +544,16 @@
|
||||
"daemon_bundled": "Incluido",
|
||||
"daemon_install_bundled": "Instalar integrado",
|
||||
"daemon_installed": "Instalado",
|
||||
"daemon_maintenance_label": "MANTENIMIENTO",
|
||||
"daemon_none_bundled": "ninguno en esta compilación",
|
||||
"daemon_not_installed": "no instalado",
|
||||
"daemon_status_diff": "La versión difiere",
|
||||
"daemon_status_differ": "El binario instalado difiere de la versión incluida.",
|
||||
"daemon_status_match": "El binario instalado coincide con la versión integrada.",
|
||||
"daemon_status_missing": "No hay ningún daemon instalado — instala la versión incluida.",
|
||||
"daemon_status_none": "No instalado",
|
||||
"daemon_status_ok": "Actualizado",
|
||||
"daemon_update_active_tt": "El nodo actualmente instalado (activo)",
|
||||
"daemon_update_available": "Hay una nueva versión del nodo disponible",
|
||||
"daemon_update_body": "Esta versión de la cartera incluye un nodo DragonX más reciente que el que está instalado actualmente. Al actualizar se reemplaza el dragonxd instalado (y dragonx-cli/dragonx-tx), y luego se detiene y reinicia el nodo para que la nueva versión surta efecto. Recomendado: un nodo más reciente puede añadir funciones (p. ej. compatibilidad con frase de recuperación) que el antiguo no tiene.",
|
||||
"daemon_update_browse": "Explorar todas las versiones…",
|
||||
@@ -433,18 +563,28 @@
|
||||
"daemon_update_download_install": "Descargar e instalar",
|
||||
"daemon_update_downloading": "Descargando…",
|
||||
"daemon_update_failed": "Error en la actualización",
|
||||
"daemon_update_install_this": "Instalar esta versión",
|
||||
"daemon_update_installed": "Instalado:",
|
||||
"daemon_update_installed_ok": "Nodo instalado",
|
||||
"daemon_update_installing": "Instalando…",
|
||||
"daemon_update_keep": "Mantener actual",
|
||||
"daemon_update_later": "Más tarde",
|
||||
"daemon_update_latest": "Más reciente:",
|
||||
"daemon_update_latest_badge": "más reciente",
|
||||
"daemon_update_latest_tt": "La versión más reciente disponible",
|
||||
"daemon_update_loading": "Cargando versiones…",
|
||||
"daemon_update_no_notes": "No hay notas de la versión para esta versión.",
|
||||
"daemon_update_now": "Actualizar ahora",
|
||||
"daemon_update_prerelease_tt": "Versión preliminar / de prueba",
|
||||
"daemon_update_prompt_title": "¿Actualizar el daemon del nodo?",
|
||||
"daemon_update_reinstall": "Reinstalar",
|
||||
"daemon_update_released": "Publicada:",
|
||||
"daemon_update_restart_note": "Reinicia el daemon para empezar a ejecutar la nueva versión.",
|
||||
"daemon_update_restart_now": "Reiniciar el daemon ahora",
|
||||
"daemon_update_safe": "Tu cartera, tus claves y los datos de la blockchain no se ven afectados: solo se reemplazan los archivos del programa daemon. Si ejecutas deliberadamente un nodo personalizado, elige Mantener actual.",
|
||||
"daemon_update_status_current": "Esta versión está instalada",
|
||||
"daemon_update_status_latest": "Última versión",
|
||||
"daemon_update_status_older": "Anterior a tu versión instalada",
|
||||
"daemon_update_title": "¿Actualizar el daemon del nodo?",
|
||||
"daemon_update_unavailable_body": "No hay una compilación del nodo disponible para esta plataforma.",
|
||||
"daemon_update_unavailable_title": "Actualizaciones del nodo no disponibles",
|
||||
@@ -453,12 +593,20 @@
|
||||
"daemon_update_verify_note": "La descarga se verifica frente al SHA-256 publicado de la versión y una firma ed25519 fijada antes de instalarla.",
|
||||
"daemon_update_verifying": "Verificando…",
|
||||
"daemon_update_version": "Versión:",
|
||||
"daemon_updates_label": "ACTUALIZACIONES",
|
||||
"daemon_version": "Daemon",
|
||||
"dark": "Oscuro",
|
||||
"data_stale_prefix": "Actualizado",
|
||||
"data_stale_tooltip": "El saldo puede estar desactualizado: la cartera no ha recibido una actualización reciente. Comprueba la conexión con tu nodo.",
|
||||
"date": "Fecha",
|
||||
"date_label": "Fecha:",
|
||||
"debug_gate_confirm": "Mostrar opciones de depuración",
|
||||
"debug_gate_incorrect": "PIN o contraseña incorrectos.",
|
||||
"debug_gate_pass_prompt": "Ingresa la contraseña de tu cartera para continuar:",
|
||||
"debug_gate_pin_prompt": "Ingresa tu PIN de desbloqueo para continuar:",
|
||||
"debug_gate_title": "¿Mostrar opciones de depuración?",
|
||||
"debug_gate_verifying": "Verificando…",
|
||||
"debug_gate_warning": "Estas son opciones avanzadas de diagnóstico y ajuste del nodo. Pueden exponer información sensible y cambiar el funcionamiento de tu nodo. Continúa solo si sabes lo que haces.",
|
||||
"debug_logging": "REGISTRO DE DEPURACIÓN",
|
||||
"decrypt_desc": "Se exportará el monedero, se reiniciará el daemon con un monedero nuevo sin cifrar y se reimportarán todas las claves. Esto puede tardar varios minutos según el tamaño del monedero.",
|
||||
"decrypt_error_title": "Error al descifrar",
|
||||
@@ -481,10 +629,22 @@
|
||||
"disconnected": "Desconectado",
|
||||
"dismiss": "Descartar",
|
||||
"display": "Pantalla",
|
||||
"done": "Listo",
|
||||
"download": "Descargar",
|
||||
"download_bootstrap": "Descargar Bootstrap",
|
||||
"dragonx_green": "DragonX (Verde)",
|
||||
"edit": "Editar",
|
||||
"empty_wallet_keys_suffix": "claves",
|
||||
"empty_wallet_open_manager": "Abrir administrador de carteras",
|
||||
"empty_wallet_restore": "Restaurar mi cartera",
|
||||
"empty_wallet_salvage_body": "Esta cartera está vacía porque una reparación automática anterior apartó tu cartera original como copia de seguridad. Tus monedas casi con certeza están en esa copia, no perdidas. Restáurala para volver a cargar tus fondos: no se elimina nada; primero se aparta el archivo actual.",
|
||||
"empty_wallet_salvage_headline": "Tus monedas están a salvo en un archivo de copia de seguridad.",
|
||||
"empty_wallet_salvage_title": "Es posible que tu cartera haya sido reparada",
|
||||
"empty_wallet_warning_body": "Esta cartera no tiene direcciones ni fondos, pero otro archivo de cartera en tu carpeta de DragonX contiene claves. Lo más probable es que tus monedas estén ahí, no perdidas. Abre el administrador de carteras para cambiar a la cartera que tiene tus fondos.",
|
||||
"empty_wallet_warning_dismiss": "No volver a avisar para esta cartera",
|
||||
"empty_wallet_warning_dismiss_tip": "Detiene este aviso solo para el archivo de cartera actual. Si más tarde cambias a otra cartera vacía, podría avisarte de nuevo.",
|
||||
"empty_wallet_warning_headline": "Es posible que haya abierto la cartera equivocada.",
|
||||
"empty_wallet_warning_title": "Esta cartera está vacía",
|
||||
"enc_confirm": "Confirmar:",
|
||||
"enc_desc": "Cifrar tu monedero protege tus claves privadas con una frase de contraseña. Tras el cifrado, el daemon se reiniciará.",
|
||||
"enc_encrypting": "Cifrando el monedero...",
|
||||
@@ -523,6 +683,7 @@
|
||||
"explorer_tx_outputs": "Salidas",
|
||||
"explorer_tx_size": "Tamaño",
|
||||
"explorer_txid": "TxID:",
|
||||
"explorer_urls_hdr": "URLS",
|
||||
"export": "Exportar",
|
||||
"export_csv": "Exportar CSV",
|
||||
"export_keys_btn": "Exportar Claves",
|
||||
@@ -550,6 +711,151 @@
|
||||
"export_viewing_key": "Exportar Clave de Vista",
|
||||
"failed_create_shielded": "Error al crear dirección protegida",
|
||||
"failed_create_transparent": "Error al crear dirección transparente",
|
||||
"faq": "Preguntas frecuentes",
|
||||
"faq_d_mgmt_1_a": "El nodo integrado se inicia automáticamente con la cartera y se detiene cuando corresponde, así que normalmente no necesitas administrarlo a mano. Los controles avanzados (incluido reiniciar el daemon) están en Ajustes > Nodo y seguridad.",
|
||||
"faq_d_mgmt_1_q": "¿Cómo inicio o detengo el nodo?",
|
||||
"faq_d_mgmt_2_a": "Si conectas la cartera a un nodo que iniciaste tú mismo, esta opción decide si cerrar la cartera también apaga ese nodo. Déjala desactivada si quieres que tu nodo siga funcionando después de cerrar la cartera.",
|
||||
"faq_d_mgmt_2_q": "¿Qué hace «cerrar el daemon externo al salir»?",
|
||||
"faq_d_mgmt_3_a": "Los datos del nodo y la cartera están en el directorio de datos de DragonX, dentro de tu perfil de usuario (por ejemplo, en AppData en Windows o en tu carpeta personal en Linux/macOS). La cadena de bloques es la parte más grande; conserva suficiente espacio libre en disco para que pueda crecer.",
|
||||
"faq_d_mgmt_3_q": "¿Dónde se almacenan la cadena de bloques y los datos de la cartera?",
|
||||
"faq_d_mgmt_title": "Gestión del nodo",
|
||||
"faq_d_mine_1_a": "La pestaña Minería te permite minar con tu CPU. DragonX usa el algoritmo RandomX, diseñado para procesadores normales. Define el número de hilos e inicia — las recompensas se acreditan en tu cartera.",
|
||||
"faq_d_mine_1_q": "¿Cómo mino DragonX?",
|
||||
"faq_d_mine_2_a": "La minería solo envía las recompensas directamente a ti, pero solo paga cuando encuentras un bloque, lo cual es poco frecuente a menos que tengas mucho hashrate. La minería en pool comparte el trabajo con otros para pagos más pequeños y constantes. La pestaña Minería admite ambas.",
|
||||
"faq_d_mine_2_q": "Minería solo vs. en pool — ¿cuál elijo?",
|
||||
"faq_d_mine_3_a": "La sección de pool en la pestaña Minería tiene un botón «Actualizar minero…» que descarga, verifica e instala la última versión optimizada del minero de DragonX. Igual que el actualizador del nodo, cada descarga se verifica con checksum y firma antes de instalarse.",
|
||||
"faq_d_mine_3_q": "¿Cómo actualizo el minero?",
|
||||
"faq_d_mine_4_a": "El nodo actual puede ejecutar un servidor stratum integrado para que otros mineros apunten a tu equipo. Hay un interruptor para esto en Ajustes > Nodo y seguridad. Por defecto solo escucha localmente; exponerlo a otros equipos requiere abrirlo deliberadamente.",
|
||||
"faq_d_mine_4_q": "¿Puedo alojar un pool de minería?",
|
||||
"faq_d_mine_title": "Minería y pools",
|
||||
"faq_d_net_1_a": "Justo después de iniciar, el nodo necesita un momento para encontrar pares, así que un «0 pares» breve es normal. Si persiste, comprueba tu conexión a internet y que un firewall no esté bloqueando el nodo. El nodo encuentra pares automáticamente mediante semillas integradas.",
|
||||
"faq_d_net_1_q": "La cartera no muestra pares ni conexiones — ¿qué ocurre?",
|
||||
"faq_d_net_2_a": "Sí — los nodos de DragonX se comunican entre sí mediante TLS, por lo que las conexiones entre pares están cifradas. La cartera también se comunica con su propio nodo por un canal local seguro.",
|
||||
"faq_d_net_2_q": "¿Está cifrada mi conexión a la red?",
|
||||
"faq_d_net_title": "Pares y red",
|
||||
"faq_d_node_1_a": "El demonio, dragonxd, es el nodo completo de DragonX. Descarga y verifica toda la cadena de bloques, retransmite transacciones a la red y responde a las consultas de la cartera. Ejecutar tu propio nodo significa que no tienes que confiar en el servidor de nadie más.",
|
||||
"faq_d_node_1_q": "¿Qué es el demonio (dragonxd)?",
|
||||
"faq_d_node_2_a": "Por defecto, la cartera inicia y gestiona su propio nodo integrado (embebido) — no tienes que hacer nada. Si ya ejecutas dragonxd por tu cuenta, la cartera puede conectarse a ese nodo externo en su lugar. La cartera detecta en qué situación está y actúa en consecuencia.",
|
||||
"faq_d_node_2_q": "Nodo integrado vs. externo — ¿cuál es la diferencia?",
|
||||
"faq_d_node_3_a": "Un nodo completo valida cada regla de la cadena de bloques de forma independiente, así que tu cartera confía en las matemáticas en lugar de en un tercero. También refuerza la red. La contrapartida es el espacio en disco y una sincronización inicial.",
|
||||
"faq_d_node_3_q": "¿Por qué ejecutar un nodo completo?",
|
||||
"faq_d_node_title": "El nodo completo",
|
||||
"faq_d_perf_1_a": "La cadena de bloques es el elemento más grande y crece con el tiempo, así que conserva varios gigabytes libres. El nodo usa una caché de base de datos para mayor velocidad; los valores predeterminados incluidos están ajustados para equipos típicos. Los usuarios avanzados pueden ajustar la caché de base de datos del nodo editando dbcache en su archivo de configuración (DRAGONX.conf).",
|
||||
"faq_d_perf_1_q": "¿Cuánto disco usa el nodo y puedo ajustar la memoria?",
|
||||
"faq_d_perf_2_a": "Activa la descarga de arranque (bootstrap) (ofrecida en el asistente de configuración) para obtener una copia reciente y verificada de la cadena en lugar de validar cada bloque desde cero. Un disco rápido (SSD) también ayuda mucho.",
|
||||
"faq_d_perf_2_q": "¿Cómo hago más rápida la sincronización inicial?",
|
||||
"faq_d_perf_title": "Almacenamiento y rendimiento",
|
||||
"faq_d_sync_1_a": "Al iniciar, el nodo carga y valida bloques antes de poder atender a la cartera. La pantalla «Procesando bloques / Aplicando bloques para construir el estado actual de la cadena» es ese calentamiento. Es normal; la cartera se vuelve utilizable cuando el nodo termina e informa su altura.",
|
||||
"faq_d_sync_1_q": "¿Qué significa «Procesando bloques» al iniciar?",
|
||||
"faq_d_sync_2_a": "Para tus fondos blindados, el nodo mantiene «testigos» criptográficos que te permiten gastar de forma privada. Tras ciertos reinicios o actualizaciones, reconstruye esta caché reescaneando bloques recientes. En una cartera con muchas transacciones, esto puede tardar varios minutos y usar memoria adicional.\n\nNo está atascado — deja que termine. Forzar el cierre a la mitad solo hace que la reconstrucción se reinicie la próxima vez.",
|
||||
"faq_d_sync_2_q": "¿Qué es «Reconstruyendo caché de testigos»?",
|
||||
"faq_d_sync_3_a": "Cerca de la punta de la cadena, el trabajo que escanea tu cartera (comprobando saldos y notas) compite con el nodo por el mismo bloqueo interno, lo que puede ralentizar la conexión de los últimos bloques en una cartera grande. Las versiones recientes limitan ese escaneo para que el nodo pueda ponerse al día. Quedarte en una pestaña más ligera (o simplemente esperar) permite que termine más rápido.",
|
||||
"faq_d_sync_3_q": "¿Por qué la sincronización es lenta cuando ya casi estoy al día?",
|
||||
"faq_d_sync_4_a": "La primera sincronización descarga y verifica toda la cadena y puede tardar según tu conexión y disco. Puedes acelerarla drásticamente activando la descarga de arranque (bootstrap) en el asistente de configuración, que obtiene una copia reciente y verificada de los datos de la cadena.",
|
||||
"faq_d_sync_4_q": "¿Cuánto dura la sincronización inicial?",
|
||||
"faq_d_sync_title": "Sincronización y cadena de bloques",
|
||||
"faq_d_trbl_1_a": "Normalmente no. En una cartera grande, el nodo puede pasar varios minutos reconstruyendo testigos blindados o aplicando bloques, durante lo cual parece pausado y puede tardar en responder. Dale tiempo — el registro de depuración (Ajustes) muestra progreso constante si está funcionando.\n\nEvita forzar el cierre durante esta fase; reinicia el trabajo desde cero en el próximo arranque.",
|
||||
"faq_d_trbl_1_q": "El nodo parece atascado en «Activando la mejor cadena» — ¿está congelado?",
|
||||
"faq_d_trbl_2_a": "Si un archivo de cartera se recupera pero falta parte de sus datos de claves, el nodo puede abrirlo en un modo «degradado» limitado, donde no puede derivar nuevas direcciones ni blindar fondos. La solución más segura es restaurar desde tu frase de recuperación en una cartera nueva.",
|
||||
"faq_d_trbl_2_q": "¿Qué es el «modo degradado»?",
|
||||
"faq_d_trbl_3_a": "Un reescaneo hace que la cartera vuelva a leer la cadena para redescubrir tus transacciones (útil tras importar claves). Una reindexación reconstruye la base de datos de la cadena de bloques del nodo y solo es necesaria si esa base de datos está dañada o es incompatible tras un cambio de versión. Ambas pueden tardar un rato; inícialas desde Ajustes > Nodo y seguridad.",
|
||||
"faq_d_trbl_3_q": "¿Cuándo debo reindexar o reescanear?",
|
||||
"faq_d_trbl_4_a": "El uso de memoria aumenta durante trabajo intenso, como una reconstrucción de testigos o una sincronización inicial, y vuelve a bajar al terminar. Un uso de memoria alto y sostenido en reposo es inusual — reiniciar la cartera lo libera. En un equipo con poca RAM, asegúrate de que otras aplicaciones que consumen mucha memoria no compitan por ella.",
|
||||
"faq_d_trbl_4_q": "El nodo usa mucha memoria — ¿es normal?",
|
||||
"faq_d_trbl_title": "Solución de problemas",
|
||||
"faq_d_upd_1_a": "Abre Ajustes > Nodo y seguridad > Binario del demonio y usa «Buscar actualizaciones…». La cartera descarga la última versión verificada del nodo y la instala; la nueva versión se aplica la próxima vez que se inicie el demonio.",
|
||||
"faq_d_upd_1_q": "¿Cómo actualizo el nodo?",
|
||||
"faq_d_upd_2_a": "Sí — el diálogo de actualización enumera todas las versiones para que puedas elegir una compilación específica o anterior. Ten cuidado al degradar: un nodo más antiguo puede no aceptar datos de la cadena de bloques escritos por uno más reciente y podría necesitar una reindexación.",
|
||||
"faq_d_upd_2_q": "¿Puedo instalar una versión específica del nodo?",
|
||||
"faq_d_upd_3_a": "Cada archivo de nodo descargado se verifica con un checksum y una firma criptográfica antes de instalarse. Si la verificación falla, la cartera rechaza la actualización.",
|
||||
"faq_d_upd_3_q": "¿Es segura la actualización?",
|
||||
"faq_d_upd_title": "Actualizar el nodo",
|
||||
"faq_group_daemon": "Demonio",
|
||||
"faq_group_wallet": "Cartera",
|
||||
"faq_intro": "Respuestas sobre tu cartera y el nodo de DragonX. Busca, o explora por tema abajo.",
|
||||
"faq_intro_lite": "Respuestas sobre tu cartera Lite. Busca, o explora por tema abajo.",
|
||||
"faq_l_gs_1_a": "ObsidianDragonLite es la cartera ligera de DragonX. En lugar de ejecutar un nodo completo, se conecta a un servidor lite de DragonX, así que inicia rápido y no descarga toda la cadena de bloques.\n\nGestiona tus claves, saldos y envíos igual que la cartera completa — consulta la sección «Cartera Lite» más abajo para ver cómo funciona la conexión al servidor y la contrapartida de privacidad.",
|
||||
"faq_l_gs_1_q": "¿Qué es ObsidianDragonLite?",
|
||||
"faq_l_lite_1_a": "La cartera Lite se comunica con un servidor lite de DragonX en lugar de ejecutar su propio nodo completo. Eso la hace rápida de iniciar y ligera en disco y ancho de banda, porque no descarga ni verifica toda la cadena de bloques por sí misma. Tus claves privadas siempre permanecen en tu dispositivo — nunca se envían al servidor.",
|
||||
"faq_l_lite_1_q": "¿En qué se diferencia la cartera Lite de la cartera completa?",
|
||||
"faq_l_lite_2_a": "Abre Ajustes y elige un servidor. Puedes cambiar de servidor en cualquier momento y la cartera se resincroniza desde el nuevo. Si un servidor va lento o no responde, prueba con otro.",
|
||||
"faq_l_lite_2_q": "¿Cómo elijo o cambio el servidor?",
|
||||
"faq_l_lite_3_a": "Tus claves nunca salen de tu dispositivo, y los importes protegidos permanecen cifrados. Pero el servidor lite sí ve qué direcciones consulta tu cartera y tu dirección IP, por lo que puede vincular tus direcciones entre sí — un compromiso entre comodidad y privacidad. Para la máxima privacidad, usa la cartera de full node, que verifica la cadena por sí misma y no revela tus direcciones a un servidor.",
|
||||
"faq_l_lite_3_q": "¿Es la cartera Lite tan privada como el full node?",
|
||||
"faq_l_lite_title": "Cartera Lite",
|
||||
"faq_l_sec_1_a": "Ve a Configuración > Cartera y define una contraseña en la sección de seguridad. El cifrado protege tus claves privadas en el disco, de modo que alguien con acceso a los archivos aún no podría gastar tus monedas.\n\nElige una contraseña segura y no la pierdas — no hay forma de recuperar una cartera cifrada sin ella.",
|
||||
"faq_no_results": "Sin resultados. Prueba con otro término de búsqueda.",
|
||||
"faq_open_tooltip": "Ayuda y preguntas frecuentes",
|
||||
"faq_search_hint": "Buscar en las preguntas frecuentes…",
|
||||
"faq_title": "Ayuda y preguntas frecuentes",
|
||||
"faq_w_addr_1_a": "Las direcciones transparentes (empiezan con R o t) funcionan como la mayoría de las monedas: los montos y las direcciones son públicos en la cadena de bloques.\n\nLas direcciones blindadas (direcciones z, empiezan con zs) usan criptografía de conocimiento cero: el monto, el remitente y el destinatario quedan cifrados en la cadena. Los saldos en direcciones blindadas son privados.",
|
||||
"faq_w_addr_1_q": "¿Cuál es la diferencia entre direcciones transparentes y blindadas?",
|
||||
"faq_w_addr_2_a": "Prefiere las direcciones blindadas (z) siempre que sea posible: mantienen privados tu saldo y tu historial de pagos. DragonX es una moneda de privacidad y lo blindado es la opción predeterminada para los fondos recibidos.\n\nUsa una dirección transparente solo cuando un servicio con el que interactúas no admita direcciones blindadas.",
|
||||
"faq_w_addr_2_q": "¿Qué tipo de dirección debería usar?",
|
||||
"faq_w_addr_3_a": "Para las direcciones blindadas, reutilizarlas está bien y no filtra el historial. Para las direcciones transparentes, usar una dirección nueva por cada pago mejora la privacidad. Crea nuevas direcciones desde la pestaña Recibir.",
|
||||
"faq_w_addr_3_q": "¿Debería reutilizar una dirección o crear una nueva?",
|
||||
"faq_w_addr_4_a": "Blindar mueve monedas de una dirección transparente a una blindada, haciéndolas privadas. Las monedas recién minadas llegan transparentes y se blindan automáticamente por ti.\n\nTambién puedes blindar manualmente desde la cartera; blindar es solo una transacción especial, así que tarda el tiempo de confirmación normal en completarse.",
|
||||
"faq_w_addr_4_q": "¿Qué es \"blindar\"?",
|
||||
"faq_w_addr_title": "Direcciones y privacidad",
|
||||
"faq_w_bal_1_a": "Mientras tu cartera aún se está sincronizando, tu saldo está incompleto y puede mostrar 0 — todavía no ha escaneado todas tus transacciones. Se completa cuando termina la sincronización.\n\nObserva la barra de estado en la parte inferior: muestra el estado de la conexión y la altura de bloque. Una vez que la sincronización termina, tu saldo es exacto.",
|
||||
"faq_w_bal_1_q": "¿Por qué mi saldo está en 0 o no se actualiza?",
|
||||
"faq_w_bal_2_a": "Cada bloque nuevo minado sobre el bloque que contiene tu transacción añade una confirmación. Más confirmaciones significan que el pago está más firmemente asentado. Los fondos recibidos se pueden gastar después de la primera confirmación.",
|
||||
"faq_w_bal_2_q": "¿Qué son las confirmaciones?",
|
||||
"faq_w_bal_3_a": "El total incluye fondos que aún se están confirmando o están temporalmente bloqueados (por ejemplo, monedas en proceso de blindarse). El saldo disponible es lo que puedes enviar ahora mismo. Ambos convergen a medida que las transacciones se confirman.",
|
||||
"faq_w_bal_3_q": "¿Cuál es la diferencia entre el saldo total y el disponible?",
|
||||
"faq_w_bal_title": "Saldo y sincronización",
|
||||
"faq_w_chat_1_a": "Chat es un mensajero cifrado en la cadena, integrado en la cartera. Los mensajes se envían como memos blindados privados, así que solo tú y tu contacto pueden leerlos.",
|
||||
"faq_w_chat_1_q": "¿Qué es la función de Chat?",
|
||||
"faq_w_chat_2_a": "Tu identidad de chat se deriva de la frase de recuperación de tu cartera, así que viaja con tu cartera — restaura la cartera y tu identidad vuelve. No creas una cuenta ni una contraseña aparte.",
|
||||
"faq_w_chat_2_q": "¿Cómo funciona mi identidad de chat?",
|
||||
"faq_w_chat_3_a": "Añade un contacto por su dirección en la pestaña Contactos, opcionalmente con un nombre y un avatar. Los contactos se pueden mantener privados solo para la cartera actual o compartirse entre todas las carteras que abras en este equipo.",
|
||||
"faq_w_chat_3_q": "¿Cómo funcionan los contactos?",
|
||||
"faq_w_chat_4_a": "Abre una conversación y usa los íconos de su encabezado. Ocultar (el ojo) la quita de la lista pero conserva todos los mensajes — un mensaje nuevo la vuelve a mostrar. Eliminar (la papelera) abre un diálogo: \"Eliminar\" borra el historial en este dispositivo (si esa persona te escribe de nuevo, la conversación vuelve), mientras que \"Eliminar y bloquear\" también impide sus mensajes futuros hasta que la desbloquees desde la lista \"Bloqueados\" encima de la lista de conversaciones. Todo esto es local a este dispositivo — los mensajes permanecen en la cadena de bloques y la otra persona conserva su propia copia, así que no es un \"deshacer envío\".",
|
||||
"faq_w_chat_4_q": "¿Cómo oculto, elimino o bloqueo una conversación?",
|
||||
"faq_w_chat_title": "Chat y contactos",
|
||||
"faq_w_gs_1_a": "ObsidianDragon es una cartera de nodo completo para DragonX (DRGX). Gestiona tus monedas y, en la versión de escritorio, ejecuta un nodo completo de DragonX (el daemon) en segundo plano para que tu cartera verifique la cadena de bloques por sí misma en lugar de confiar en un servidor de terceros.\n\nPiénsalo como dos partes que trabajan juntas: la cartera (claves, saldos, envíos) y el nodo (la cadena de bloques y la red). Las preguntas aquí están divididas del mismo modo — mira las pestañas Cartera y Daemon arriba.",
|
||||
"faq_w_gs_1_q": "¿Qué es ObsidianDragon?",
|
||||
"faq_w_gs_2_a": "En el primer inicio, el asistente de configuración crea una cartera por ti automáticamente. Las carteras nuevas están respaldadas por una frase de recuperación secreta (una lista de palabras). Anota esa frase y guárdala sin conexión — es la única forma de restaurar tus fondos si pierdes este equipo.\n\nPuedes revisar o respaldar la frase en cualquier momento desde Configuración > Copia de seguridad y datos > Frase de recuperación.",
|
||||
"faq_w_gs_2_q": "¿Cómo creo una nueva cartera?",
|
||||
"faq_w_gs_3_a": "Usa el flujo de restauración con frase de recuperación si tienes una frase de recuperación. (En la versión de nodo completo también puedes copiar un archivo wallet.dat existente en el directorio de datos de la cartera antes de iniciarla.)\n\nDespués de restaurar, la cartera vuelve a escanear para encontrar tus transacciones pasadas, así que tu saldo e historial pueden tardar un poco en aparecer la primera vez.",
|
||||
"faq_w_gs_3_q": "¿Cómo restauro una cartera que ya tengo?",
|
||||
"faq_w_gs_4_a": "El asistente te permite elegir una apariencia/tema, configurar el cifrado y un PIN y, en la versión de nodo completo, descargar opcionalmente un bootstrap para acelerar la primera sincronización. Puedes omitir cualquier paso y cambiar todo esto más tarde en Configuración.",
|
||||
"faq_w_gs_4_q": "¿Qué hace la configuración inicial?",
|
||||
"faq_w_gs_title": "Primeros pasos",
|
||||
"faq_w_sec_1_a": "Ve a Configuración > Nodo y seguridad y define una contraseña (la sección SEGURIDAD). El cifrado protege tus claves privadas en el disco, de modo que alguien con acceso a los archivos aún no podría gastar tus monedas.\n\nElige una contraseña segura y no la pierdas — no hay forma de recuperar una cartera cifrada sin ella.",
|
||||
"faq_w_sec_1_q": "¿Cómo cifro mi cartera?",
|
||||
"faq_w_sec_2_a": "El PIN bloquea la pantalla de la app para que los saldos y las acciones queden ocultos cuando te alejas. Es un bloqueo de conveniencia adicional al cifrado; lo configuras durante el asistente o en Configuración, después de cifrar la cartera.",
|
||||
"faq_w_sec_2_q": "¿Qué es el PIN o la pantalla de bloqueo?",
|
||||
"faq_w_sec_3_a": "No. El cifrado de la cartera no se puede eludir. Si aún conservas tu frase de recuperación, puedes restaurar la cartera a partir de ella en una cartera nueva y establecer una contraseña nueva. Sin la contraseña ni la frase de recuperación, los fondos no se pueden recuperar.",
|
||||
"faq_w_sec_3_q": "Olvidé mi contraseña — ¿se puede recuperar?",
|
||||
"faq_w_sec_title": "Seguridad y cifrado",
|
||||
"faq_w_seed_1_a": "Ve a Configuración > Copia de seguridad y datos > Frase de recuperación. Anota las palabras en papel en orden y guárdalas en un lugar seguro y sin conexión. Cualquiera que tenga la frase puede gastar tus monedas, así que nunca la guardes en una foto, un correo o una nota en la nube.",
|
||||
"faq_w_seed_1_q": "¿Cómo respaldo mi frase de recuperación?",
|
||||
"faq_w_seed_2_a": "Las carteras antiguas (heredadas) no se basaban en una frase de recuperación. La cartera puede migrar una cartera heredada a una moderna con frase de recuperación: crea una nueva cartera con frase y transfiere tus fondos a ella. Busca \"Migrar a una cartera con frase…\" en Configuración > Copia de seguridad y datos. Esta función necesita la versión actual del daemon.",
|
||||
"faq_w_seed_2_q": "Mi cartera no tiene frase de recuperación — ¿puedo añadir una?",
|
||||
"faq_w_seed_3_a": "Usa el flujo de restauración con frase de recuperación al configurar una cartera e ingresa tus palabras en orden. Tu cartera luego vuelve a escanear para reconstruir tu saldo e historial, lo cual puede tardar un poco la primera vez.",
|
||||
"faq_w_seed_3_q": "¿Cómo restauro a partir de mi frase de recuperación?",
|
||||
"faq_w_seed_4_a": "Tu frase de recuperación es el respaldo principal y basta para restaurarlo todo. Una copia de tu archivo de cartera es un respaldo secundario práctico que además conserva las etiquetas y la configuración. Mantén cualquier respaldo sin conexión y privado.",
|
||||
"faq_w_seed_4_q": "¿Debería respaldar también wallet.dat?",
|
||||
"faq_w_seed_title": "Frase de recuperación y copia de seguridad",
|
||||
"faq_w_send_1_a": "Abre la pestaña Enviar, pega la dirección del destinatario, ingresa un monto y confirma. Si la cartera está cifrada, se te pedirá desbloquearla para la transacción.\n\nLos envíos a direcciones blindadas son privados; los envíos a direcciones transparentes son públicos.",
|
||||
"faq_w_send_1_q": "¿Cómo envío fondos?",
|
||||
"faq_w_send_2_a": "Las comisiones de DragonX son muy bajas y se establecen automáticamente. La comisión se muestra antes de confirmar. Normalmente no necesitas cambiarla.",
|
||||
"faq_w_send_2_q": "¿Qué comisión pago?",
|
||||
"faq_w_send_3_a": "Sí — al enviar a una dirección blindada (z) puedes incluir un memo cifrado. Solo el destinatario puede leerlo. Las direcciones transparentes no admiten memos.",
|
||||
"faq_w_send_3_q": "¿Puedo adjuntar un mensaje a un pago?",
|
||||
"faq_w_send_4_a": "Una transacción está \"pendiente\" hasta que se mina en un bloque y obtiene confirmaciones. Esto suele tardar uno o dos minutos. Las transacciones blindadas también necesitan que tu cartera esté sincronizada para construir la prueba.\n\nSi un envío permanece pendiente durante un tiempo inusualmente largo, verifica que tu cartera esté conectada y sincronizada (mira la barra de estado en la parte inferior de la ventana).",
|
||||
"faq_w_send_4_q": "Envié un pago pero dice pendiente — ¿por qué?",
|
||||
"faq_w_send_title": "Envío y recepción",
|
||||
"faq_w_set_1_a": "Abre Ajustes > Apariencia para elegir un tema y un color de acento. Se incluyen varios estilos claros y oscuros, y los efectos como el desenfoque se pueden desactivar en equipos menos potentes.",
|
||||
"faq_w_set_1_q": "¿Cómo cambio el tema?",
|
||||
"faq_w_set_2_a": "Sí — Ajustes > Apariencia tiene un selector de idioma. La cartera incluye varias traducciones; lo que aún no esté traducido se muestra en inglés.",
|
||||
"faq_w_set_2_q": "¿Puedo cambiar el idioma?",
|
||||
"faq_w_set_3_a": "Usa la opción de escala de fuente en Ajustes > Apariencia. Escala toda la interfaz, lo cual es útil en pantallas de alta resolución (HiDPI) donde la app podría verse pequeña.",
|
||||
"faq_w_set_3_q": "El texto es demasiado pequeño (o grande) — ¿puedo escalarlo?",
|
||||
"faq_w_set_4_a": "Activa la obtención de precios en Ajustes. La cartera mostrará entonces un valor fiat aproximado junto a los saldos y puede mostrar un pequeño gráfico de mercado. Los precios son solo informativos.",
|
||||
"faq_w_set_4_q": "¿Cómo muestro precios en moneda fiat?",
|
||||
"faq_w_set_title": "Ajustes y apariencia",
|
||||
"favorite_address": "Marcar como favorito",
|
||||
"fee": "Comisión",
|
||||
"fee_high": "Alta",
|
||||
@@ -572,6 +878,63 @@
|
||||
"general": "General",
|
||||
"generating": "Generando",
|
||||
"go_to_receive": "Ir a Recibir",
|
||||
"grpa_current_block_paren": "(Actual: %d)",
|
||||
"grpa_days_ago": "hace %lld días",
|
||||
"grpa_dbg_addrman": "Seguimiento y gestión de direcciones de pares",
|
||||
"grpa_dbg_alert": "Mensajes del sistema de alertas",
|
||||
"grpa_dbg_bench": "Tiempos de referencia de las operaciones",
|
||||
"grpa_dbg_coindb": "Operaciones de lectura/escritura de la base de datos de monedas",
|
||||
"grpa_dbg_db": "Operaciones de Berkeley DB",
|
||||
"grpa_dbg_estimatefee": "Algoritmo de estimación de comisiones",
|
||||
"grpa_dbg_http": "Actividad del servidor RPC HTTP",
|
||||
"grpa_dbg_libevent": "Biblioteca de red Libevent",
|
||||
"grpa_dbg_lock": "Depuración de contención de bloqueos",
|
||||
"grpa_dbg_mempool": "Actividad del pool de memoria de transacciones",
|
||||
"grpa_dbg_net": "Conexiones y mensajes de red",
|
||||
"grpa_dbg_paymentdisclosure": "Protocolo de divulgación de pagos",
|
||||
"grpa_dbg_pow": "Actividad de minería por prueba de trabajo",
|
||||
"grpa_dbg_proxy": "Conexiones de proxy SOCKS5",
|
||||
"grpa_dbg_prune": "Operaciones de poda de bloques",
|
||||
"grpa_dbg_rand": "Generación de números aleatorios",
|
||||
"grpa_dbg_reindex": "Progreso de reindexación de la cadena de bloques",
|
||||
"grpa_dbg_rpc": "Procesamiento de comandos RPC",
|
||||
"grpa_dbg_selectcoins": "Selección de monedas para transacciones",
|
||||
"grpa_dbg_tor": "Integración de Tor e información de circuitos",
|
||||
"grpa_dbg_zmq": "Sistema de notificaciones ZeroMQ",
|
||||
"grpa_dbg_zrpc": "Operaciones RPC blindadas (z-addr)",
|
||||
"grpa_enter_private_key_to_import": "Introduce una clave privada para importar.",
|
||||
"grpa_error_prefix": "Error: ",
|
||||
"grpa_hr_ago": "hace %lld h",
|
||||
"grpa_invalid_response_from_daemon": "Respuesta no válida del daemon",
|
||||
"grpa_invalid_suffix": " (no válido)",
|
||||
"grpa_min_ago": "hace %lld min",
|
||||
"grpa_sec_ago": "hace %lld s",
|
||||
"grpa_seed_demo_chat": "Chat de demostración con semilla",
|
||||
"grpa_showing_first_100_of": "... mostrando los primeros 100 de %d",
|
||||
"grpa_tab_about": "Acerca de",
|
||||
"grpa_tab_appearance": "Apariencia",
|
||||
"grpa_tab_backup_data": "Copia de seguridad y datos",
|
||||
"grpa_tab_chat": "Chat",
|
||||
"grpa_tab_explorer": "Explorador",
|
||||
"grpa_tab_node_security": "Nodo y seguridad",
|
||||
"grpa_tab_wallet": "Cartera",
|
||||
"grpa_unexpected_getblockhash_result": "resultado inesperado de getblockhash",
|
||||
"grpb_copy": "Copiar",
|
||||
"grpb_max": "Máx.",
|
||||
"grpb_new_badge_suffix": " [NUEVO]",
|
||||
"grpb_preview_msg_payment_through": "¿Se realizó el pago? 🙂",
|
||||
"grpb_preview_msg_sending_rest": "Enviando el resto ahora 👍",
|
||||
"grpb_preview_msg_yep_confirmed": "Sí — acabo de confirmarlo ✅",
|
||||
"grpb_selected_suffix": "\n(seleccionado)",
|
||||
"grpb_tooltip_address_balance": "%s\nSaldo: %.8f %s%s",
|
||||
"grpb_undo_clear": "Deshacer borrado",
|
||||
"grpc_benchmark_inconclusive": "Prueba de rendimiento no concluyente: no se registraron muestras de tasa de hash. Comprueba la conexión con el pool e inténtalo de nuevo.",
|
||||
"grpc_benchmark_takes_secs": "La prueba de rendimiento tarda ~%ds e interrumpe la minería. Haz clic de nuevo para empezar.",
|
||||
"grpc_bootstrap_failed": "Error en el arranque inicial",
|
||||
"grpc_bootstrap_not_initialized": "Arranque inicial no inicializado",
|
||||
"grpc_hashrate_fee": "%s %s%% de comisión",
|
||||
"grpc_key_not_available": "Clave no disponible para esta dirección",
|
||||
"grpc_na": "N/D",
|
||||
"height": "Altura",
|
||||
"help": "Ayuda",
|
||||
"hidden_tag": " (oculto)",
|
||||
@@ -646,6 +1009,7 @@
|
||||
"light": "Claro",
|
||||
"lite_account_label": "Cuenta",
|
||||
"lite_action": "Acción",
|
||||
"lite_backend_unavailable": "Backend de billetera Lite no disponible",
|
||||
"lite_backup_keys": "Respaldo y claves",
|
||||
"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).",
|
||||
@@ -653,9 +1017,12 @@
|
||||
"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_start": "No se pudo iniciar la operación",
|
||||
"lite_could_not_write": "No se pudo escribir ",
|
||||
"lite_encrypt_wallet": "Cifrar cartera",
|
||||
"lite_encryption_removed": "Cifrado eliminado",
|
||||
"lite_enter_all_seed_words": "Ingrese las 24 palabras semilla para restaurar (se obtuvieron %d)",
|
||||
"lite_enter_wallet_path": "Ingrese una ruta de billetera",
|
||||
"lite_hide_wipe": "Ocultar y borrar",
|
||||
"lite_import": "Importar",
|
||||
"lite_import_key_label": "Importar clave",
|
||||
@@ -792,9 +1159,27 @@
|
||||
"memo_optional": "MEMO (OPCIONAL)",
|
||||
"memo_upper": "MEMO",
|
||||
"memo_z_only": "Nota: Los memos solo están disponibles al enviar a direcciones blindadas (z)",
|
||||
"merge_addr_created": "Dirección blindada creada.",
|
||||
"merge_advanced": "Avanzado",
|
||||
"merge_back": "Atrás",
|
||||
"merge_batch_fmt": "Fusiona hasta %d entradas por ejecución — repite para completar el resto.",
|
||||
"merge_confirm_btn": "Confirmar",
|
||||
"merge_confirm_fmt": "¿Consolidar ~%s DRGX de %d entrada(s) en %s?",
|
||||
"merge_create_zaddr": "Crear dirección blindada",
|
||||
"merge_creating": "Creando dirección…",
|
||||
"merge_description": "Fusiona múltiples UTXOs en una sola dirección protegida. Esto puede ayudar a reducir el tamaño de la cartera y mejorar la privacidad.",
|
||||
"merge_fee_hint": "Comisión de red para esta transacción.",
|
||||
"merge_funds": "Fusionar Fondos",
|
||||
"merge_max_inputs": "Máximo de entradas por lote",
|
||||
"merge_no_spendable": "Aún no hay entradas disponibles para consolidar.",
|
||||
"merge_progress": "Consolidando… esto puede tardar varios minutos. Puedes cerrar esta ventana.",
|
||||
"merge_scope_fmt": "%d entradas transparentes + %d blindadas · ~%s DRGX disponibles",
|
||||
"merge_scope_loading": "Comprobando tus entradas…",
|
||||
"merge_send_failed": "Error al fusionar: ",
|
||||
"merge_source": "Consolidar",
|
||||
"merge_src_both": "Ambas",
|
||||
"merge_src_shielded": "Blindadas",
|
||||
"merge_src_transparent": "Transparentes",
|
||||
"merge_started": "Operación de fusión iniciada",
|
||||
"merge_title": "Fusionar a Dirección",
|
||||
"mig_adopt": "Hacer que esta sea mi cartera",
|
||||
@@ -875,6 +1260,7 @@
|
||||
"mining_difficulty_copied": "Dificultad copiada",
|
||||
"mining_est_block": "Bloque Est.",
|
||||
"mining_est_daily": "Diario Est.",
|
||||
"mining_est_daily_pool_sub": "equivalente solo aproximado, antes de la comisión del pool",
|
||||
"mining_filter_all": "Todos",
|
||||
"mining_filter_tip_all": "Mostrar todas las ganancias",
|
||||
"mining_filter_tip_pool": "Mostrar solo ganancias del pool",
|
||||
@@ -903,10 +1289,12 @@
|
||||
"mining_open_in_explorer": "Abrir en explorador",
|
||||
"mining_payout_address": "Dirección de Pago",
|
||||
"mining_payout_foreign": "⚠ Esta dirección de pago no está en tu cartera actual — las recompensas minadas irían a otra cartera. Actualízala si cambiaste de cartera.",
|
||||
"mining_payout_invalid": "No es una dirección DragonX válida — corrígela antes de empezar, o se pierden las recompensas de minería.",
|
||||
"mining_payout_tooltip": "Dirección para recibir recompensas de minería",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_fee": "Comisión",
|
||||
"mining_pool_hashrate": "Hashrate del Pool",
|
||||
"mining_pool_needs_payout_tooltip": "Ingrese primero una dirección de pago (genere una dirección Z)",
|
||||
"mining_pool_url": "URL del Pool",
|
||||
"mining_pools_header": "POOLS",
|
||||
"mining_recent_blocks": "BLOQUES RECIENTES",
|
||||
@@ -936,6 +1324,9 @@
|
||||
"mining_syncing_tooltip": "El blockchain está sincronizando...",
|
||||
"mining_tag": " · Minería",
|
||||
"mining_threads": "Hilos de Minería",
|
||||
"mining_threads_input_tooltip": "Escribe un número exacto de hilos (pulsa Enter para aplicar)",
|
||||
"mining_threads_minus_tooltip": "Menos hilos",
|
||||
"mining_threads_plus_tooltip": "Más hilos",
|
||||
"mining_to_save": "para guardar",
|
||||
"mining_today": "Hoy",
|
||||
"mining_uptime": "Tiempo activo",
|
||||
@@ -1028,6 +1419,13 @@
|
||||
"peers_upper": "NODOS",
|
||||
"peers_version": "Versión",
|
||||
"pending": "Pendiente",
|
||||
"picker_dat_count": "%d archivo(s) de cartera en esta carpeta",
|
||||
"picker_dat_none": "No hay archivos de cartera directamente en esta carpeta",
|
||||
"picker_empty": "Esta carpeta no tiene subcarpetas ni archivos de cartera.",
|
||||
"picker_home": "Carpeta de inicio",
|
||||
"picker_select": "Escanear esta carpeta",
|
||||
"picker_title": "Selecciona una carpeta para escanear",
|
||||
"picker_up": "Subir un nivel",
|
||||
"pin_change_desc": "Cambia tu PIN de desbloqueo. Necesitas tu PIN actual y un nuevo PIN.",
|
||||
"pin_confirm_new_label": "Confirmar nuevo PIN:",
|
||||
"pin_current_label": "PIN actual:",
|
||||
@@ -1105,6 +1503,8 @@
|
||||
"qr_failed": "Error al generar código QR",
|
||||
"qr_title": "Código QR",
|
||||
"qr_unavailable": "QR no disponible",
|
||||
"quick_receive": "Recepción rápida",
|
||||
"quick_send": "Envío rápido",
|
||||
"ram_daemon_gb": "Daemon: %.1f GB (%s)",
|
||||
"ram_daemon_mb": "Daemon: %.0f MB (%s)",
|
||||
"ram_system_gb": "Sistema: %.1f / %.0f GB",
|
||||
@@ -1154,6 +1554,7 @@
|
||||
"rpc_connection": "Conexión RPC...",
|
||||
"rpc_host": "Host RPC",
|
||||
"rpc_pass": "Contraseña",
|
||||
"rpc_plaintext_remote_warning": "El RPC remoto está usando HTTP sin cifrar. Agregue rpctls=1 a DRAGONX.conf si su daemon admite TLS.",
|
||||
"rpc_port": "Puerto",
|
||||
"rpc_user": "Usuario",
|
||||
"save": "Guardar",
|
||||
@@ -1161,6 +1562,7 @@
|
||||
"save_z_transactions": "Guardar Z-tx en lista",
|
||||
"sb_auth_failed": "Autenticación fallida — verifique rpcuser/rpcpassword",
|
||||
"sb_block": "Bloque: %d",
|
||||
"sb_block_db_unreadable": "Base de datos de bloques ilegible — es necesario reconstruirla",
|
||||
"sb_building_witnesses": "Estableciendo testigos",
|
||||
"sb_building_witnesses_pct": "Reconstruyendo testigos %.0f%%",
|
||||
"sb_connecting_daemon": "Conectando a dragonxd...",
|
||||
@@ -1176,6 +1578,7 @@
|
||||
"sb_dragonxd_stopped": "dragonxd detenido",
|
||||
"sb_dragonxd_stopping": "Deteniendo dragonxd...",
|
||||
"sb_extracting_sapling": "Extrayendo parámetros Sapling...",
|
||||
"sb_finishing_repair": "Terminando la reparación de la cartera — reescaneando…",
|
||||
"sb_importing_keys": "Importando claves",
|
||||
"sb_loading_config": "Cargando configuración...",
|
||||
"sb_mining_hs": "%.1f H/s",
|
||||
@@ -1197,14 +1600,55 @@
|
||||
"sb_waiting_config": "Esperando configuración del daemon...",
|
||||
"sb_waiting_daemon": "Esperando a dragonxd...",
|
||||
"sb_waiting_daemon_err": "Esperando a dragonxd — %s",
|
||||
"sb_wallet_needs_recovery": "Reparación de cartera disponible",
|
||||
"sb_warming_up": "Calentando...",
|
||||
"sb_witness_cache": "Reconstruyendo testigos",
|
||||
"scale_effects": "ESCALA Y EFECTOS",
|
||||
"screenshot_open_dir": "Abrir ubicación",
|
||||
"screenshot_sweep": "Ejecutar barrido de capturas",
|
||||
"screenshot_sweep_desc": "Recorre cada tema en cada pestaña y guarda una captura de pantalla de cada una en subcarpetas por pestaña dentro de la carpeta de capturas del directorio de configuración (sobrescribiendo el barrido anterior). Se ejecuta durante unos segundos.",
|
||||
"screenshot_sweep_full": "Barrido completo de la interfaz",
|
||||
"search_icons": "Buscar iconos...",
|
||||
"search_placeholder": "Buscar...",
|
||||
"sec_changing_passphrase": "Cambiando la frase de contraseña...",
|
||||
"sec_changing_pin": "Cambiando el PIN...",
|
||||
"sec_couldnt_lock_wallet": "No se pudo bloquear la cartera; sigue desbloqueada. Comprueba la conexión con el daemon.",
|
||||
"sec_encrypted_backup_suffix": "\nCopia de seguridad cifrada: wallet.dat.encrypted.bak",
|
||||
"sec_encrypting_wallet": "Cifrando la cartera...",
|
||||
"sec_encryption_did_not_complete": "El cifrado de la cartera no se completó; tu cartera NO está cifrada. Abre Ajustes para terminar de cifrarla.",
|
||||
"sec_encryption_failed_prefix": "Error de cifrado: ",
|
||||
"sec_failed_prefix": "Error: ",
|
||||
"sec_failed_to_create_vault": "No se pudo crear la bóveda",
|
||||
"sec_importing_keys_rescanning": "Importando claves y volviendo a escanear la cadena de bloques; la cartera se puede usar mientras tanto",
|
||||
"sec_incorrect_current_pin": "PIN actual incorrecto",
|
||||
"sec_incorrect_passphrase_decrypt": "Frase de contraseña incorrecta",
|
||||
"sec_incorrect_passphrase_pin_setup": "Frase de contraseña incorrecta",
|
||||
"sec_incorrect_pin_remove": "PIN incorrecto",
|
||||
"sec_internal_error_change_pin": "Error interno",
|
||||
"sec_internal_error_remove_pin": "Error interno",
|
||||
"sec_mode_passphrase": " Frase de contraseña",
|
||||
"sec_not_connected_to_daemon": "Sin conexión con el daemon",
|
||||
"sec_not_connected_to_daemon_pin": "Sin conexión con el daemon",
|
||||
"sec_passphrase_changed_successfully": "Frase de contraseña cambiada correctamente",
|
||||
"sec_pin_changed_successfully": "PIN cambiado correctamente",
|
||||
"sec_pin_removed": "PIN eliminado",
|
||||
"sec_pin_set_successfully": "PIN configurado correctamente",
|
||||
"sec_restart_daemon_for_encryption": "Reinicia el daemon para que el cifrado surta efecto.",
|
||||
"sec_too_many_attempts_wait": "Demasiados intentos. Espera %.0f segundos...",
|
||||
"sec_total_elapsed_fmt": "Tiempo total: %dm %02ds",
|
||||
"sec_unlock_button": "Desbloquear",
|
||||
"sec_unlock_failed_prefix": "Error al desbloquear: ",
|
||||
"sec_unlocking_fmt": "Desbloqueando%s",
|
||||
"sec_use_passphrase_instead": "Usar frase de contraseña en su lugar",
|
||||
"sec_use_pin_instead": "Usar PIN en su lugar",
|
||||
"sec_verifying_passphrase": "Verificando la frase de contraseña...",
|
||||
"sec_verifying_pin": "Verificando el PIN...",
|
||||
"sec_wallet_decrypted_all_keys_imported": "¡Cartera descifrada correctamente! Todas las claves importadas.",
|
||||
"sec_wallet_encrypted_and_pin_set": "Cartera cifrada y PIN configurado",
|
||||
"sec_wallet_encrypted_but_pin_vault_failed": "Cartera cifrada, pero falló la bóveda del PIN",
|
||||
"sec_wallet_encrypted_restarting_daemon": "Cartera cifrada. Reiniciando el daemon...",
|
||||
"sec_wallet_encrypted_successfully": "Cartera cifrada correctamente",
|
||||
"sec_wallet_locked_title": "Cartera bloqueada",
|
||||
"security": "SEGURIDAD",
|
||||
"seed_backup_button": "Frase de recuperación",
|
||||
"seed_backup_close": "Cerrar",
|
||||
@@ -1228,6 +1672,7 @@
|
||||
"send_amount": "Cantidad",
|
||||
"send_amount_details": "DETALLES DE CANTIDAD",
|
||||
"send_amount_upper": "CANTIDAD",
|
||||
"send_available_note": "disponible",
|
||||
"send_cannot_send_now": "No se puede enviar ahora — revisa la conexión, la sincronización y el saldo disponible.",
|
||||
"send_clear_fields": "¿Limpiar todos los campos del formulario?",
|
||||
"send_contacts_button": "Elegir de contactos",
|
||||
@@ -1263,6 +1708,7 @@
|
||||
"send_tooltip_not_connected": "No conectado al daemon",
|
||||
"send_tooltip_select_source": "Selecciona una dirección de origen primero",
|
||||
"send_tooltip_syncing": "Espera a que se sincronice el blockchain",
|
||||
"send_tooltip_view_only": "Dirección de solo vista — sin clave de gasto, no se puede enviar",
|
||||
"send_total": "Total",
|
||||
"send_transaction": "Enviar Transacción",
|
||||
"send_tx_failed": "Error en la transacción",
|
||||
@@ -1282,16 +1728,16 @@
|
||||
"sent_filter": "Enviado",
|
||||
"sent_type": "Enviado",
|
||||
"sent_upper": "ENVIADO",
|
||||
"set_label": "Establecer Etiqueta...",
|
||||
"set_label": "Establecer Etiqueta",
|
||||
"settings": "Ajustes",
|
||||
"settings_about_text": "Una billetera de criptomonedas blindada para DragonX (DRGX), creada con Dear ImGui para una experiencia ligera y portátil.",
|
||||
"settings_acrylic_level": "Nivel de acrílico:",
|
||||
"settings_address_book": "Libreta de direcciones...",
|
||||
"settings_address_book": "Libreta de direcciones…",
|
||||
"settings_auto_detected": "Autodetectado de DRAGONX.conf",
|
||||
"settings_auto_lock": "BLOQUEO AUTOMÁTICO",
|
||||
"settings_auto_shield_desc": "Mover automáticamente fondos transparentes a direcciones blindadas",
|
||||
"settings_auto_shield_funds": "Blindar fondos transparentes automáticamente",
|
||||
"settings_backup": "Respaldo...",
|
||||
"settings_backup": "Respaldo…",
|
||||
"settings_block_explorer_urls": "URLs del explorador de bloques",
|
||||
"settings_builtin": "Integrado",
|
||||
"settings_change_passphrase": "Cambiar contraseña",
|
||||
@@ -1305,7 +1751,7 @@
|
||||
"settings_copy_diagnostics": "Copiar diagnósticos",
|
||||
"settings_copyright": "Copyright 2024-2026 Desarrolladores de DragonX | Licencia GPLv3",
|
||||
"settings_custom": "Personalizado",
|
||||
"settings_data_dir": "Dir. de datos:",
|
||||
"settings_data_dir": "Dir. de datos",
|
||||
"settings_debug_changed": "Categorías de depuración cambiadas — reinicie el daemon para aplicar",
|
||||
"settings_debug_restart_note": "Los cambios surten efecto después de reiniciar el daemon.",
|
||||
"settings_debug_select": "Seleccione categorías para habilitar el registro de depuración del daemon (flags -debug=).",
|
||||
@@ -1313,19 +1759,20 @@
|
||||
"settings_encrypt_first_pin": "Primero cifre la billetera para habilitar el PIN",
|
||||
"settings_encrypt_wallet": "Cifrar billetera",
|
||||
"settings_explorer_hint": "Las URLs deben incluir una barra final. Se añadirá el txid/dirección.",
|
||||
"settings_export_all": "Exportar todo...",
|
||||
"settings_export_csv": "Exportar CSV...",
|
||||
"settings_export_key": "Exportar clave...",
|
||||
"settings_export_all": "Exportar todo…",
|
||||
"settings_export_csv": "Exportar CSV…",
|
||||
"settings_export_key": "Exportar clave…",
|
||||
"settings_gradient_bg": "Fondo degradado",
|
||||
"settings_gradient_desc": "Reemplazar fondos con texturas por degradados suaves",
|
||||
"settings_idle_after": "después de",
|
||||
"settings_import_key": "Importar Clave Privada...",
|
||||
"settings_import_viewkey": "Importar clave de visualización...",
|
||||
"settings_import_key": "Importar Clave Privada…",
|
||||
"settings_import_viewkey": "Importar clave de visualización…",
|
||||
"settings_language_note": "Nota: Parte del texto requiere reinicio para actualizarse",
|
||||
"settings_lock_now": "Bloquear ahora",
|
||||
"settings_locked": "Bloqueado",
|
||||
"settings_merge_to_address": "Fusionar a dirección...",
|
||||
"settings_merge_to_address": "Fusionar a dirección…",
|
||||
"settings_noise_opacity": "Opacidad de ruido:",
|
||||
"settings_not_connected": "No conectado al daemon",
|
||||
"settings_not_encrypted": "Sin cifrar",
|
||||
"settings_not_found": "No encontrado",
|
||||
"settings_open_app_dir": "Abrir carpeta de la aplicación",
|
||||
@@ -1336,29 +1783,36 @@
|
||||
"settings_privacy": "Privacidad",
|
||||
"settings_quick_unlock_pin": "PIN de desbloqueo rápido",
|
||||
"settings_reduce_transparency": "Reducir transparencia",
|
||||
"settings_reloaded": "Configuración recargada desde el disco",
|
||||
"settings_remove_encryption": "Quitar cifrado",
|
||||
"settings_remove_pin": "Quitar PIN",
|
||||
"settings_request_payment": "Solicitar pago...",
|
||||
"settings_request_payment": "Solicitar pago…",
|
||||
"settings_rescan_desc": "Reescanear la cadena de bloques en busca de transacciones faltantes",
|
||||
"settings_restart_daemon": "Reiniciar daemon",
|
||||
"settings_rpc_connection": "Conexión RPC",
|
||||
"settings_rpc_error_prefix": "Error de RPC: ",
|
||||
"settings_rpc_note": "Nota: Los ajustes de conexión se detectan automáticamente desde DRAGONX.conf",
|
||||
"settings_rpc_ok": "Conexión RPC correcta",
|
||||
"settings_save_shielded_desc": "Almacena transacciones z-addr en un archivo local para visualización",
|
||||
"settings_save_shielded_local": "Guardar historial de transacciones blindadas localmente",
|
||||
"settings_saved": "Configuración guardada",
|
||||
"settings_set_pin": "Establecer PIN",
|
||||
"settings_shield_mining": "Blindar minería...",
|
||||
"settings_shield_mining": "Blindar minería…",
|
||||
"settings_solid_colors_desc": "Usar colores sólidos en lugar de efectos de desenfoque (accesibilidad)",
|
||||
"settings_theme_refreshed": "Lista de temas actualizada",
|
||||
"settings_tor_desc": "Enrutar todas las conexiones a través de Tor para mayor privacidad",
|
||||
"settings_unlocked": "Desbloqueado",
|
||||
"settings_use_tor_network": "Usar Tor para conexiones de red",
|
||||
"settings_validate_address": "Validar dirección...",
|
||||
"settings_validate_address": "Validar dirección…",
|
||||
"settings_visual_effects": "Efectos visuales",
|
||||
"settings_wallet_file_size": "Tamaño del archivo de billetera: %s",
|
||||
"settings_wallet_info": "Información de billetera",
|
||||
"settings_wallet_location": "Ubicación de billetera: %s",
|
||||
"settings_wallet_maintenance": "Mantenimiento de billetera",
|
||||
"settings_wallet_not_found": "Archivo de billetera no encontrado",
|
||||
"settings_wallet_size_label": "Tamaño de billetera:",
|
||||
"settings_wallet_size_label": "Tamaño de billetera",
|
||||
"settings_ztx_cleared": "Historial de transacciones Z borrado",
|
||||
"settings_ztx_not_found": "No se encontró archivo de historial",
|
||||
"setup_wizard": "Asistente de Configuración",
|
||||
"share": "Compartir",
|
||||
"shield_check_status": "Verificar Estado",
|
||||
@@ -1406,17 +1860,34 @@
|
||||
"status": "Estado",
|
||||
"stop_external": "Detener daemon externo",
|
||||
"stop_mining": "Detener Minería",
|
||||
"stratum_allowip_hint": "Permitir mineros desde IP o CIDR (en blanco = solo este equipo)",
|
||||
"stratum_expose_warn": "Abre un puerto de minería a la red que permitas — úsalo solo en una LAN de confianza.",
|
||||
"stratum_host": "Alojar un pool de minería (stratum)",
|
||||
"stratum_host_hint": "Los mineros se conectan a este equipo en el puerto 22769 (puerto RPC + 1000) con un minero stratum RandomX. Reinicia el demonio para aplicar el cambio.",
|
||||
"stratum_host_section": "ALOJAMIENTO DE POOL DE MINERÍA",
|
||||
"submitting_transaction": "Enviando transacción...",
|
||||
"success": "Éxito",
|
||||
"summary": "Resumen",
|
||||
"sweep_button": "Barrer",
|
||||
"sweep_caveat": "Importa la clave para firmar una transacción que mueve todos sus fondos a tu dirección. La clave permanece en tu monedero con saldo cero.",
|
||||
"sweep_current_theme_only": "Solo el tema actual",
|
||||
"sweep_dest_label": "Enviar los fondos barridos a",
|
||||
"sweep_dest_new": "Nueva dirección blindada (recomendado)",
|
||||
"sweep_done": "Listo — fondos barridos a tu dirección.",
|
||||
"sweep_to": "Barrido a:",
|
||||
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
|
||||
"sweep_tx": "Transacción:",
|
||||
"swin_connection_failed": "Error de conexión: ",
|
||||
"swin_connection_successful": "¡Conexión correcta!\nVersión de dragonxd: ",
|
||||
"swin_invalid_suffix": " (no válido)",
|
||||
"swin_no_history_file_found": "No se encontró ningún archivo de historial",
|
||||
"swin_rescan_failed": "Error en el reescaneo: ",
|
||||
"swin_rescan_started_from_block": "Reescaneo iniciado desde el bloque ",
|
||||
"swin_rescan_to": " hasta ",
|
||||
"swin_rpc_client_not_initialized": "Cliente RPC no inicializado",
|
||||
"swin_settings_saved": "Ajustes guardados",
|
||||
"swin_theme_list_refreshed": "Lista de temas actualizada",
|
||||
"swin_ztx_history_cleared": "Historial de transacciones Z borrado",
|
||||
"switch_corrupt_body": "Esta cartera parece dañada: el nodo no pudo abrirla. Restáurala desde una copia de seguridad, vuelve a crearla o intenta repararla.",
|
||||
"switch_corrupt_repair": "Intentar reparar (salvage)",
|
||||
"switch_progress_background": "Continuar en segundo plano",
|
||||
@@ -1441,6 +1912,7 @@
|
||||
"theme": "Tema",
|
||||
"theme_effects": "Efectos de tema",
|
||||
"theme_language": "TEMA E IDIOMA",
|
||||
"tile_click_to_open": "Clic para abrir",
|
||||
"time_days_ago": "hace %d días",
|
||||
"time_hours_ago": "hace %d horas",
|
||||
"time_minutes_ago": "hace %d minutos",
|
||||
@@ -1455,7 +1927,9 @@
|
||||
"to_upper": "PARA",
|
||||
"tools": "HERRAMIENTAS",
|
||||
"tools_actions": "Herramientas y Acciones...",
|
||||
"tools_actions_hdr": "HERRAMIENTAS Y ACCIONES",
|
||||
"total": "Total",
|
||||
"total_balance_label": "Saldo Total",
|
||||
"transaction_id": "ID DE TRANSACCIÓN",
|
||||
"transaction_sent": "Transacción enviada exitosamente",
|
||||
"transaction_sent_msg": "¡Transacción enviada!",
|
||||
@@ -1477,7 +1951,7 @@
|
||||
"tt_auto_shield": "Mover automáticamente el saldo transparente a direcciones blindadas para privacidad",
|
||||
"tt_backup": "Crear una copia de seguridad de su wallet.dat",
|
||||
"tt_block_explorer": "Abrir el explorador de bloques DragonX en su navegador",
|
||||
"tt_blur": "Cantidad de desenfoque (0%% = apagado, 100%% = máximo)",
|
||||
"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)",
|
||||
@@ -1540,7 +2014,7 @@
|
||||
"tt_low_spec": "Desactivar todos los efectos visuales pesados\\nAtajo: Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolidar múltiples UTXOs en una dirección",
|
||||
"tt_mine_idle": "Iniciar minería automáticamente cuando el\\nsistema esté inactivo (sin entrada de teclado/ratón)",
|
||||
"tt_noise": "Intensidad de textura granulada (0%% = apagado, 100%% = máximo)",
|
||||
"tt_noise": "Intensidad de textura granulada (0% = apagado, 100% = máximo)",
|
||||
"tt_open_app_dir": "Abrir la carpeta de ObsidianDragon (configuración, temas, registros) en el explorador de archivos",
|
||||
"tt_open_data_dir": "Abre en el gestor de archivos la carpeta con los datos de tu cartera y de la blockchain",
|
||||
"tt_open_dir": "Clic para abrir en explorador de archivos",
|
||||
@@ -1574,14 +2048,17 @@
|
||||
"tt_simple_bg": "Usar un gradiente simple para el fondo\\nAtajo: Ctrl+Up",
|
||||
"tt_simple_bg_alt": "Usar una versión degradada de la imagen de fondo del tema\\nAtajo: Ctrl+Up",
|
||||
"tt_stop_external": "Se aplica al conectarse a un daemon\\niniciado fuera de esta billetera",
|
||||
"tt_stratum_host": "Ejecuta un servidor de pool stratum RandomX en este nodo para que otros mineros RandomX puedan apuntar a este equipo. Requiere un nodo v1.3.0+ y reiniciar el demonio para aplicar el cambio.",
|
||||
"tt_sweep_current_theme_only": "Barrer solo el tema activo en lugar de recorrer todos los temas",
|
||||
"tt_test_conn": "Verificar la conexión RPC al daemon",
|
||||
"tt_theme_effects": "Brillo, resplandor, ciclo de tono por tema",
|
||||
"tt_theme_hotkey": "Atajo: Ctrl+Izquierda/Derecha para cambiar temas",
|
||||
"tt_tor": "Enrutar conexiones del daemon a través de la red Tor para anonimato",
|
||||
"tt_tx_url": "URL base para ver transacciones en un explorador de bloques",
|
||||
"tt_ui_opacity": "Opacidad de tarjetas y barra lateral (100%% = totalmente opaco, menor = más transparente)",
|
||||
"tt_ui_opacity": "Opacidad de tarjetas y barra lateral (100% = totalmente opaco, menor = más transparente)",
|
||||
"tt_validate": "Comprobar si una dirección DragonX es válida",
|
||||
"tt_verbose": "Registrar diagnósticos detallados de conexión,\\nestado del daemon e info de propietario de puerto\\nen la pestaña de Consola",
|
||||
"tt_wallet_size_warn": "Las carteras blindadas crecen con los datos de testigo de cada nota — fusionar muchas notas en una sola dirección lo reduce. Haz una copia de seguridad primero.",
|
||||
"tt_wallets_button": "Enumera tus archivos de cartera y cambia entre ellos",
|
||||
"tt_website": "Abrir el sitio web de DragonX",
|
||||
"tt_window_opacity": "Opacidad del fondo (menor = escritorio visible a través de la ventana)",
|
||||
@@ -1632,6 +2109,7 @@
|
||||
"validate_not_mine": "No es propiedad de esta cartera",
|
||||
"validate_ownership": "Propiedad:",
|
||||
"validate_results": "Resultados:",
|
||||
"validate_results_placeholder": "Los resultados aparecerán aquí",
|
||||
"validate_shielded_type": "Protegida (dirección z)",
|
||||
"validate_status": "Estado:",
|
||||
"validate_title": "Validar Dirección",
|
||||
@@ -1647,8 +2125,68 @@
|
||||
"view_on_explorer": "Ver en Explorador",
|
||||
"waiting_for_daemon": "Esperando conexión al daemon...",
|
||||
"wallet": "CARTERA",
|
||||
"wallet_degraded_notify": "Tu cartera se abrió en modo de funciones reducidas: los fondos existentes están seguros y disponibles, pero crear nuevas direcciones y blindar están desactivados. Respalda tu frase de recuperación y restáurala para reparar la cartera por completo.",
|
||||
"wallet_diagnostics_hdr": "DIAGNÓSTICOS",
|
||||
"wallet_empty": "Tu cartera está vacía",
|
||||
"wallet_empty_hint": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
|
||||
"wallet_options_hdr": "OPCIONES",
|
||||
"wallet_rebuild_failed": "La reconstrucción no produjo una cartera válida, así que no se cambió nada. Tu cartera no fue modificada.",
|
||||
"wallet_rebuild_install_failed": "No se pudo instalar la cartera reconstruida; tu cartera actual se dejó tal cual.",
|
||||
"wallet_rebuild_no_helper": "El asistente de reconstrucción de cartera no está disponible en esta compilación. Usa Restaurar, o reconstruye manualmente.",
|
||||
"wallet_rebuild_no_source": "No se pudo encontrar una cartera legible para reconstruir. No se cambió nada.",
|
||||
"wallet_rebuild_ok": "Base de datos de la cartera reconstruida — el nodo la está cargando y reescaneando para calcular tu saldo.",
|
||||
"wallet_rebuild_started": "Reparando tu archivo de cartera — por favor no cierres esta ventana.",
|
||||
"wallet_recovered_body": "No se ha eliminado nada. Tu cartera original sigue guardada en tu computadora como un archivo de respaldo con fecha, y cada opción a continuación solo copia o renombra archivos — nunca borra ninguno. Tus claves nunca se regeneran, solo se vuelven a leer.\n\nEs posible que a la copia reparada que está cargada ahora le falten algunas transacciones recientes, así que tu saldo puede verse un poco bajo hasta que termines abajo y se vuelva a escanear.",
|
||||
"wallet_recovered_dismiss": "Ahora no — mantener la copia reparada",
|
||||
"wallet_recovered_dismiss_sub": "No se cambia ningún archivo. Puedes volver a abrir esto cuando quieras desde la barra de estado; tu original queda respaldado de forma segura de todos modos.",
|
||||
"wallet_recovered_notify": "Tu archivo de cartera necesitaba una reparación — tu original se respaldó de forma segura. Abre la app para revisar tus opciones.",
|
||||
"wallet_recovered_open_folder": "Mostrarme los archivos",
|
||||
"wallet_recovered_open_folder_sub": "Abre la carpeta de datos de la cartera para que puedas inspeccionar los archivos de respaldo tú mismo — nada cambia.",
|
||||
"wallet_recovered_rebuild": "Reparar automáticamente (recomendado)",
|
||||
"wallet_recovered_rebuild_sub": "Vuelve a leer cada registro recuperable de tu archivo de cartera más completo y escribe uno nuevo limpio, luego reinicia. La opción más exhaustiva — tu archivo actual se conserva como respaldo con fecha de todos modos.",
|
||||
"wallet_recovered_restore": "Restaurar el archivo original en su lugar",
|
||||
"wallet_recovered_restore_sub": "Vuelve a poner en su lugar tu respaldo más grande sin modificar, tal cual, y luego vuelve a escanear — algo más rápido, pero solo tan completo como lo era ese archivo. Tu archivo actual se conserva como respaldo con fecha de todos modos.",
|
||||
"wallet_recovered_safety": "Tus monedas están a salvo.",
|
||||
"wallet_recovered_title": "Tu archivo de cartera necesita una reparación rápida",
|
||||
"wallet_recovered_warn": "Cuando la app se inició, encontró que tu archivo de cartera no pasó su verificación de consistencia — esto suele ocurrir después de una actualización de la app o un cierre inesperado. La app ya protegió tus datos: apartó el archivo antiguo y cargó una copia reparada para que no te quedes bloqueado.",
|
||||
"wallet_recovery_decide_later": "Decidir más tarde",
|
||||
"wallet_recovery_decide_later_tip": "Cierra esto y mantiene la copia que está cargada ahora. Nada cambia, y puedes reparar cuando quieras — la barra de estado conserva un enlace de “Reparación de cartera disponible”.",
|
||||
"wallet_recovery_details_label": "Mostrar detalles técnicos",
|
||||
"wallet_recovery_done": "Listo",
|
||||
"wallet_recovery_failure_body": "La reparación se ejecutó, pero su resultado no pasó la verificación (no se pudo leer, o no tenía direcciones), así que se descartó automáticamente antes de reemplazar nada. Tu cartera está exactamente como estaba antes de que hicieras clic — nada cambió en el disco.",
|
||||
"wallet_recovery_failure_title": "La reparación no se completó",
|
||||
"wallet_recovery_files_detail": "Tu cartera original sigue aquí — la app la renombró a un respaldo con fecha (wallet.<números>.bak) en tu carpeta de datos y no ha eliminado nada. “Reparar” lee cada registro de tu cartera más completa en un archivo nuevo y limpio. “Restaurar” copia tu respaldo más grande de vuelta exactamente como está. La copia cargada en este momento también se conserva como respaldo.",
|
||||
"wallet_recovery_files_label": "¿Qué pasa con mis archivos?",
|
||||
"wallet_recovery_notnow_short": "Ahora no",
|
||||
"wallet_recovery_other_options": "Otras opciones",
|
||||
"wallet_recovery_rebuild_card": "Reparar automáticamente",
|
||||
"wallet_recovery_rebuild_card_desc": "Lee cada registro recuperable en un archivo limpio, luego reinicia. La opción más exhaustiva.",
|
||||
"wallet_recovery_recommended": "RECOMENDADO",
|
||||
"wallet_recovery_repair_go": "Reparar",
|
||||
"wallet_recovery_rescan_body": "Volviendo a escanear la cadena de bloques para reconstruir tu saldo e historial de transacciones. Esto puede tardar varios minutos — no necesitas hacer nada, y por favor no reinicies el nodo mientras se ejecuta.",
|
||||
"wallet_recovery_rescan_elapsed": "Trabajando desde hace %s",
|
||||
"wallet_recovery_rescan_size": "La cartera reconstruida ahora pesa %s y sigue completándose",
|
||||
"wallet_recovery_rescan_slow": "Esto está tardando más de lo habitual, lo cual es normal en una cartera grande. Es seguro dejarlo en ejecución — puedes ver el progreso en Avanzado ▸ Consola.",
|
||||
"wallet_recovery_rescan_title": "Terminando la reparación de tu cartera",
|
||||
"wallet_recovery_restore_card": "Restaurar original",
|
||||
"wallet_recovery_restore_card_desc": "Vuelve a poner en su lugar tu respaldo más grande sin modificar, tal cual — solo tan completo como era ese archivo.",
|
||||
"wallet_recovery_restore_go": "Restaurar",
|
||||
"wallet_recovery_success_body": "La app cargó tu cartera reparada y está volviendo a escanear para totalizar tu saldo — esto puede tardar unos minutos. Lee cada registro que puede, pero en archivos dañados poco frecuentes puede recuperar algunas direcciones menos — u ocasionalmente más — que antes. Cuando termine el reescaneo, comprueba que tu saldo e historial se vean correctos.",
|
||||
"wallet_recovery_success_restore": "Tu archivo de respaldo más grande ya está en su lugar y cargando ahora, y la app está volviendo a escanear. Se restauró tal cual, así que es exactamente tan completo como era ese archivo — comprueba tu saldo cuando termine el reescaneo.",
|
||||
"wallet_recovery_try_other": "Probar la otra opción",
|
||||
"wallet_recovery_whats_happened": "¿Qué pasó?",
|
||||
"wallet_recovery_working_label": "Trabajando",
|
||||
"wallet_restore_bad_backup": "El archivo de cartera de respaldo parece ilegible, así que NO se restauró — tu cartera actual no cambió. Restaura desde tu propio respaldo en su lugar.",
|
||||
"wallet_restore_busy": "El nodo está ocupado reiniciándose — inténtalo de nuevo en un momento.",
|
||||
"wallet_restore_copy_failed": "No se pudo instalar la cartera de respaldo; tu cartera actual se dejó en su lugar.",
|
||||
"wallet_restore_move_failed": "No se pudo apartar la cartera actual — no se cambió nada.",
|
||||
"wallet_restore_no_backup": "No se encontró un wallet.<timestamp>.bak para restaurar. No se cambió nada.",
|
||||
"wallet_restore_no_restart": "Tu cartera original se restauró, pero el nodo no se reinició — inícialo desde Ajustes.",
|
||||
"wallet_restore_ok": "Cartera original restaurada. El nodo la está cargando ahora.",
|
||||
"wallet_restore_started": "Restaurando tu archivo original — por favor no cierres esta ventana.",
|
||||
"wallet_restore_stop_failed": "El nodo no se detuvo a tiempo, así que no se cambió nada. Inténtalo de nuevo.",
|
||||
"wallet_size_consolidate": "Consolidar notas…",
|
||||
"wallet_size_warn": "Este archivo de cartera es grande. Consolidar tus notas puede frenar un mayor crecimiento.",
|
||||
"wallets_active": "Activa",
|
||||
"wallets_add": "Agregar carpeta",
|
||||
"wallets_add_folder": "Escanear también otra carpeta en busca de archivos de cartera:",
|
||||
@@ -1691,6 +2229,7 @@
|
||||
"wallets_open_inplace_tt": "Abre esta cartera donde está, enlazándola al directorio de datos (sin copiar)",
|
||||
"wallets_remove_folder": "Dejar de escanear esta carpeta",
|
||||
"wallets_reveal": "Mostrar carpeta",
|
||||
"wallets_scan_folder": "Escanear otra carpeta en busca de carteras…",
|
||||
"wallets_scanned_folders": "Carpetas escaneadas:",
|
||||
"wallets_sort_addresses": "Direcciones",
|
||||
"wallets_sort_asc": "Ascendente (más antiguo / menos / más pequeño primero)",
|
||||
@@ -1760,18 +2299,30 @@
|
||||
"wiz_welcome_sub": "Unas cuantas opciones rápidas y tu nodo completo estará listo.",
|
||||
"wiz_welcome_title": "¡Bienvenido a ObsidianDragon!",
|
||||
"wizard_daemon_start_failed": "Error al iniciar el daemon — se reintentará automáticamente",
|
||||
"xmrig_active_tt": "El minero actualmente instalado (activo)",
|
||||
"xmrig_browse_releases": "Ver todos los lanzamientos…",
|
||||
"xmrig_checking": "Buscando el minero más reciente…",
|
||||
"xmrig_current": "Actual:",
|
||||
"xmrig_downgrade_note": "Instalar una versión de minero diferente reemplaza la actual; detén la minería primero, y se aplicará en el siguiente inicio.",
|
||||
"xmrig_download_install": "Descargar e instalar",
|
||||
"xmrig_downloading": "Descargando…",
|
||||
"xmrig_install_this": "Instalar esta versión",
|
||||
"xmrig_installed": "Instalado:",
|
||||
"xmrig_installed_ok": "Minero instalado",
|
||||
"xmrig_installing": "Instalando…",
|
||||
"xmrig_latest": "Más reciente:",
|
||||
"xmrig_latest_badge": "la más reciente",
|
||||
"xmrig_latest_tt": "Versión más reciente disponible",
|
||||
"xmrig_loading_releases": "Cargando versiones…",
|
||||
"xmrig_no_notes": "No hay notas de la versión para esta versión.",
|
||||
"xmrig_none": "ninguno",
|
||||
"xmrig_prerelease_tt": "Compilación preliminar / de pruebas",
|
||||
"xmrig_reinstall": "Reinstalar",
|
||||
"xmrig_released": "Publicado:",
|
||||
"xmrig_releases": "versiones de xmrig",
|
||||
"xmrig_status_current": "Esta versión está instalada",
|
||||
"xmrig_status_latest": "Última versión",
|
||||
"xmrig_status_older": "Anterior a tu versión instalada",
|
||||
"xmrig_stop_mining_first": "Detén la minería antes de actualizar el minero.",
|
||||
"xmrig_unavailable_body": "No hay ninguna versión del minero disponible para esta plataforma.",
|
||||
"xmrig_unavailable_title": "Actualizaciones del minero no disponibles",
|
||||
|
||||
+568
-17
@@ -19,6 +19,7 @@
|
||||
"about_license_text": "Ce logiciel est publié sous la licence publique générale GNU v3 (GPLv3). Vous êtes libre d'utiliser, de modifier et de distribuer ce logiciel selon les termes de la licence.",
|
||||
"about_peers_count": "%zu pairs",
|
||||
"about_release": "Version",
|
||||
"about_source": "Source",
|
||||
"about_title": "À propos d'ObsidianDragon",
|
||||
"about_version": "Version :",
|
||||
"about_website": "Site web",
|
||||
@@ -60,7 +61,84 @@
|
||||
"amount_label": "Montant :",
|
||||
"animate_avatars": "Animer les avatars",
|
||||
"appearance": "APPARENCE",
|
||||
"appx_back": "Retour",
|
||||
"appx_back_up_seed_phrase_title": "Sauvegardez votre phrase de récupération",
|
||||
"appx_birthday_block_height": "Naissance (hauteur de bloc) : %llu — sauvegardez-la également.",
|
||||
"appx_blockchain_data_deleted": "Données de la blockchain supprimées (%d éléments). Le démon redémarre pour se resynchroniser depuis le réseau.",
|
||||
"appx_blockchain_maintenance_in_progress": "Une opération de maintenance de la blockchain est déjà en cours.",
|
||||
"appx_blockchain_rescan_complete": "Nouvelle analyse de la blockchain terminée",
|
||||
"appx_bootstrap_complete_reconciling": "Amorçage terminé — réconciliation de votre portefeuille avec les nouvelles données de la chaîne.",
|
||||
"appx_cancel": "Annuler",
|
||||
"appx_cleaning_up": "Nettoyage...",
|
||||
"appx_confirm_your_backup": "Confirmez votre sauvegarde",
|
||||
"appx_copied_clipboard_autoclears": "Copié — le presse-papiers s'efface automatiquement dans 45 s",
|
||||
"appx_copy": "Copier",
|
||||
"appx_could_not_start_restore": "Impossible de démarrer la restauration",
|
||||
"appx_create_failed_prefix": "Échec de la création : ",
|
||||
"appx_creating_your_wallet": "Création de votre portefeuille…",
|
||||
"appx_daemon_error": "Erreur du démon",
|
||||
"appx_daemon_reinstall_in_progress": "La réinstallation du démon est déjà en cours.",
|
||||
"appx_disconnecting": "Déconnexion...",
|
||||
"appx_done": "Terminé",
|
||||
"appx_dragonxd_output": "Sortie de dragonxd",
|
||||
"appx_encrypting_wallet": "Chiffrement du portefeuille...",
|
||||
"appx_fullnode_lifecycle_unavailable_lite": "Les actions de cycle de vie du nœud complet ne sont pas disponibles dans la version allégée",
|
||||
"appx_installing_bundled_daemon": "Installation du démon intégré — le nœud va s'arrêter, se mettre à jour, puis redémarrer...",
|
||||
"appx_invalid_payment_uri_prefix": "URI de paiement invalide : ",
|
||||
"appx_ive_written_it_down": "Je les ai notés",
|
||||
"appx_keep_node_running_and_quit": "Laisser le nœud actif et quitter",
|
||||
"appx_last_block_n": "Dernier bloc : %d",
|
||||
"appx_last_used_wallet_not_found_prefix": "Votre dernier fichier de portefeuille utilisé (",
|
||||
"appx_last_used_wallet_not_found_suffix": ") est introuvable — le portefeuille par défaut a été ouvert à la place. Si vous l'avez déplacé, restaurez-le et revenez à celui-ci depuis la liste des portefeuilles.",
|
||||
"appx_low_spec_mode_disabled": "Mode faibles ressources désactivé",
|
||||
"appx_low_spec_mode_enabled": "Mode faibles ressources activé",
|
||||
"appx_miner_stopped_prefix": "Mineur arrêté : ",
|
||||
"appx_miner_stopped_unexpectedly": "Le mineur s'est arrêté de manière inattendue.",
|
||||
"appx_n_min_n_sec": "%d min %d s",
|
||||
"appx_n_seconds": "%d secondes",
|
||||
"appx_no_bundled_daemon_to_install": "Cette version ne contient aucun démon intégré à installer",
|
||||
"appx_no_embedded_daemon_to_install": "Cette version ne contient aucun démon embarqué à installer",
|
||||
"appx_node_busy_restarting": "Le nœud est occupé à redémarrer — réessayez dans un instant.",
|
||||
"appx_node_rebuilding_witness_cache": "Le nœud reconstruit son cache de témoins",
|
||||
"appx_not_next_word": " — ce n'est pas le mot suivant",
|
||||
"appx_payment_request_loaded": "Demande de paiement chargée",
|
||||
"appx_pool_miner_connected_and_hashing": "Mineur de pool connecté et en cours de hachage.",
|
||||
"appx_progress_n_of_n": "Progression : %d / %d",
|
||||
"appx_rebuilding_sapling_note_witnesses": "Reconstruction des témoins de notes Sapling…",
|
||||
"appx_rebuilding_witness_cache_blocks_left": "Reconstruction du cache de témoins %.0f%% — %d blocs restants",
|
||||
"appx_rebuilding_witness_cache_pct": "Reconstruction du cache de témoins %.0f%%",
|
||||
"appx_recovery_phrase_word_count": "La phrase de récupération doit comporter 24 mots — vous en avez %d.",
|
||||
"appx_restarting_daemon_rescan_flag": "Redémarrage du démon avec l'option -rescan...",
|
||||
"appx_restarting_daemon_zapwallettxes": "Redémarrage du démon avec -zapwallettxes=2 (réparation du portefeuille)...",
|
||||
"appx_restoring_your_wallet": "Restauration de votre portefeuille…",
|
||||
"appx_seed_backup_warning": "Ces 24 mots sont le SEUL moyen de restaurer votre portefeuille. Notez-les dans l'ordre, conservez-les hors ligne et ne les partagez jamais. Si vous les perdez, vos fonds seront perdus à jamais.",
|
||||
"appx_seed_not_backed_up_warning": "Vous n'avez pas sauvegardé votre phrase de récupération — des fonds pourraient être perdus. Ignorer quand même ?",
|
||||
"appx_sending_stop_command_to_daemon": "Envoi de la commande d'arrêt au démon...",
|
||||
"appx_setting_initial_sapling_witnesses": "Définition des témoins Sapling initiaux %.0f%%",
|
||||
"appx_shutdown_complete": "Arrêt terminé",
|
||||
"appx_simple_background_disabled": "Arrière-plan simple désactivé",
|
||||
"appx_simple_background_enabled": "Arrière-plan simple activé",
|
||||
"appx_skip": "Ignorer",
|
||||
"appx_skip_anyway": "Ignorer quand même",
|
||||
"appx_still_status_prefix": "Toujours « ",
|
||||
"appx_still_status_suffix": " » — forcer la fermeture maintenant peut corrompre les données de la chaîne.",
|
||||
"appx_stop_anyway_and_quit": "Arrêter quand même et quitter",
|
||||
"appx_stopping_daemon_deleting_blockchain": "Arrêt du démon et suppression des données de la blockchain...",
|
||||
"appx_stopping_node_discards_rebuild": "Arrêter le nœud maintenant abandonne la reconstruction en cours et la relance (plusieurs minutes) à la prochaine ouverture du portefeuille. Vous pouvez plutôt laisser le nœud en marche.",
|
||||
"appx_stopping_pool_miner": "Arrêt du mineur de pool...",
|
||||
"appx_syncing_pct_block_n_of_n": "Synchronisation %.1f%% — Bloc %d / %d",
|
||||
"appx_tap_words_in_order": "Touchez les mots dans le bon ordre pour confirmer que vous les avez enregistrés.",
|
||||
"appx_theme_effects_disabled": "Effets de thème désactivés",
|
||||
"appx_theme_effects_enabled": "Effets de thème activés",
|
||||
"appx_theme_prefix": "Thème : ",
|
||||
"appx_use_settings_restart_daemon_hint": "Utilisez Paramètres > Redémarrer le démon pour réessayer",
|
||||
"appx_waiting_for_daemon_to_encrypt_wallet": "En attente du chiffrement du portefeuille par le démon...",
|
||||
"appx_wallet_created_and_backed_up": "Portefeuille créé et sauvegardé.",
|
||||
"appx_wallet_open_failed_prefix": "Échec de l'ouverture du portefeuille : ",
|
||||
"auto_shield": "Auto-blindage du minage",
|
||||
"autoshield_by_node": "Votre nœud blinde automatiquement les coinbase minées pour vous.",
|
||||
"autoshield_off_backup_seed": "Les coinbase minées ne sont pas encore blindées automatiquement. Sauvegardez votre phrase de récupération (Paramètres → Sauvegarde et données) pour que votre nœud puisse l'activer en toute sécurité.",
|
||||
"autoshield_off_generic": "Votre nœud ne blinde pas automatiquement les coinbase minées en ce moment.",
|
||||
"av_intro": "Les logiciels de minage sont souvent signalés comme potentiellement indésirables. Suivez ces étapes pour activer le minage en pool :",
|
||||
"av_open_security": "Ouvrir Sécurité Windows",
|
||||
"av_step1": "Étape 1 : Ajouter une exclusion",
|
||||
@@ -74,6 +152,9 @@
|
||||
"av_title": "Windows Defender a bloqué le mineur",
|
||||
"available": "Disponible",
|
||||
"backup_backing_up": "Sauvegarde en cours...",
|
||||
"backup_col_backup": "SAUVEGARDE",
|
||||
"backup_col_export": "EXPORTER",
|
||||
"backup_col_import": "IMPORTER ET RESTAURER",
|
||||
"backup_create": "Créer une sauvegarde",
|
||||
"backup_created": "Sauvegarde du portefeuille créée",
|
||||
"backup_data": "SAUVEGARDE & DONNÉES",
|
||||
@@ -92,8 +173,22 @@
|
||||
"balance": "Solde",
|
||||
"balance_history_collecting": "Historique du solde — collecte des données...",
|
||||
"balance_layout": "Disposition du solde",
|
||||
"balance_layout_switched": "Disposition : %s",
|
||||
"balance_mining_rate": "Minage %s",
|
||||
"balance_shielded_fmt": "Blindé : %.8f",
|
||||
"balance_syncing_pct": "Synchronisation %.1f%%",
|
||||
"balance_transparent_fmt": "Transparent : %.8f",
|
||||
"baltab_market": "Marché",
|
||||
"baltab_market_price_4dp": "Marché : %.4f $",
|
||||
"baltab_market_price_8dp": "Marché : %.8f $",
|
||||
"baltab_pct_change_24h": "%s%.1f%% 24h",
|
||||
"baltab_pct_of_total_zaddr": "%.0f%% du total · %d Z-addr",
|
||||
"baltab_shielded": "Protégé",
|
||||
"baltab_shielded_amount": "Protégé %.8f",
|
||||
"baltab_t_addresses_count": "%d T-addresses",
|
||||
"baltab_total_balance": "Solde total",
|
||||
"baltab_transparent": "Transparent",
|
||||
"baltab_transparent_amount": "Transparent %.8f",
|
||||
"ban": "Bannir",
|
||||
"banned_peers": "Pairs bannis",
|
||||
"block": "Bloc",
|
||||
@@ -101,6 +196,12 @@
|
||||
"block_click_copy": "Cliquez pour copier",
|
||||
"block_click_next": "Cliquez pour voir le bloc suivant",
|
||||
"block_click_prev": "Cliquez pour voir le bloc précédent",
|
||||
"block_db_reindex_body": "Cela arrive généralement après qu'une mise à jour du démon change le format sur disque, ou si l'index des blocs est endommagé. Votre portefeuille et vos pièces sont en sécurité — le nœud ne peut simplement pas charger la chaîne, donc les soldes affichent zéro.\n\nLa reconstruction relit vos fichiers de blocs existants et peut prendre du temps (elle rescanne aussi votre portefeuille). Rien n'est téléchargé.",
|
||||
"block_db_reindex_confirm": "Reconstruire la base de données de blocs",
|
||||
"block_db_reindex_notify": "Le nœud ne peut pas lire sa base de données de blocs (souvent après une mise à jour du démon). Reconstruisez-la pour restaurer votre solde — voir l'invite, ou Paramètres › Nœud.",
|
||||
"block_db_reindex_started": "Reconstruction de la base de données de blocs à partir de vos blocs — cela peut prendre du temps.",
|
||||
"block_db_reindex_title": "Reconstruire la base de données de blocs ?",
|
||||
"block_db_reindex_warn": "Le nœud ne peut pas lire sa base de données de blocs.",
|
||||
"block_explorer": "Explorateur de blocs",
|
||||
"block_get_info": "Obtenir les infos du bloc",
|
||||
"block_hash": "Hash du bloc :",
|
||||
@@ -132,6 +233,7 @@
|
||||
"bootstrap_verifying": "Vérification des sommes de contrôle...",
|
||||
"bootstrap_wallet_protected": "(wallet.dat est protégé)",
|
||||
"bootstrap_warning": "Les données de blocs existantes (blocks, chainstate, notarizations) seront supprimées et remplacées. Votre wallet.dat ne sera PAS modifié ni supprimé.",
|
||||
"byte_count_fmt": "%zu / %zu octets",
|
||||
"cancel": "Annuler",
|
||||
"change_pass_confirm": "Confirmer la nouvelle :",
|
||||
"change_pass_current": "Phrase secrète actuelle :",
|
||||
@@ -147,6 +249,10 @@
|
||||
"chat_accent_theme": "Thème",
|
||||
"chat_add_contact": "Ajouter un contact",
|
||||
"chat_awaiting_key": "En attente de réponse",
|
||||
"chat_blocked_desc": "Les conversations bloquées sont supprimées et leurs messages sont rejetés — anciens et nouveaux — jusqu'à ce que vous débloquiez. Débloquer réimporte la conversation depuis la chaîne.",
|
||||
"chat_blocked_manage": "Bloqué",
|
||||
"chat_blocked_title": "Conversations bloquées",
|
||||
"chat_blocked_toast": "Conversation supprimée et bloquée",
|
||||
"chat_bubble_minimal": "Minimale",
|
||||
"chat_bubble_rounded": "Arrondie",
|
||||
"chat_bubble_square": "Carrée",
|
||||
@@ -159,6 +265,16 @@
|
||||
"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_delete": "Supprimer la conversation",
|
||||
"chat_delete_block": "Supprimer et bloquer",
|
||||
"chat_delete_body_prefix": "Supprimer votre copie locale de la conversation avec ",
|
||||
"chat_delete_body_suffix": " ?",
|
||||
"chat_delete_confirm": "Supprimer",
|
||||
"chat_delete_failed": "Impossible de supprimer la conversation — rien n'a été modifié.",
|
||||
"chat_delete_local_note": "Cela n'affecte que cet appareil — les messages restent sur la blockchain et l'autre personne conserve sa copie.",
|
||||
"chat_delete_revive_note": "« Supprimer » efface l'historique sur cet appareil. Si cette personne vous écrit à nouveau, la conversation revient.",
|
||||
"chat_delete_title": "Supprimer la conversation ?",
|
||||
"chat_deleted_toast": "Conversation supprimée",
|
||||
"chat_density_comfortable": "Confortable",
|
||||
"chat_density_compact": "Compacte",
|
||||
"chat_emoji_color": "Couleur",
|
||||
@@ -182,6 +298,7 @@
|
||||
"chat_new_button": "Nouvelle discussion",
|
||||
"chat_new_message": "Message",
|
||||
"chat_new_message_toast": "Nouveau message chiffré",
|
||||
"chat_new_needs_zaddr": "Le chat nécessite une adresse blindée (z) — les adresses transparentes (t) ne peuvent pas recevoir de messages chiffrés.",
|
||||
"chat_new_send": "Envoyer la demande",
|
||||
"chat_new_title": "Nouvelle discussion",
|
||||
"chat_new_zaddr": "Adresse Z du destinataire",
|
||||
@@ -227,6 +344,8 @@
|
||||
"chat_ts_24h": "24 heures",
|
||||
"chat_ts_global": "Suivre global",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unblock": "Débloquer",
|
||||
"chat_unblocked_toast": "Conversation débloquée",
|
||||
"chat_unhide": "Afficher",
|
||||
"chat_unmute": "Réactiver",
|
||||
"chat_verify_key": "Clé d'identité — comparez pour vérifier",
|
||||
@@ -279,6 +398,7 @@
|
||||
"console": "Console",
|
||||
"console_accents": "Accents de couleur",
|
||||
"console_app": "App",
|
||||
"console_auto_focus": "Placer le focus sur le champ de saisie à l'ouverture",
|
||||
"console_auto_scroll": "Défilement auto",
|
||||
"console_available_commands": "Commandes disponibles :",
|
||||
"console_backend_reference": "Référence des commandes du backend",
|
||||
@@ -373,11 +493,15 @@
|
||||
"console_tab_completion": "Tab pour compléter",
|
||||
"console_text_colors": "Couleurs du texte",
|
||||
"console_toggle_accents": "Basculer les accents de couleur des lignes",
|
||||
"console_toggle_auto_focus": "Placer le curseur dans la zone de commande à l'ouverture de l'onglet Console",
|
||||
"console_toggle_text_color": "Basculer les couleurs du texte des lignes",
|
||||
"console_type_help": "Tapez 'help' pour les commandes disponibles",
|
||||
"console_welcome": "Bienvenue dans la console ObsidianDragon",
|
||||
"console_zoom_in": "Agrandir",
|
||||
"console_zoom_out": "Réduire",
|
||||
"consolidate_desc": "Combinez de nombreuses petites entrées en une seule note blindée. Moins de notes signifie un fichier de portefeuille plus petit et une meilleure confidentialité.",
|
||||
"consolidate_funds_btn": "Consolider",
|
||||
"consolidate_title": "Consolider les fonds",
|
||||
"contact_avatar": "AVATAR",
|
||||
"contact_avatar_bad_image": "Cette image n'a pas pu être chargée.",
|
||||
"contact_avatar_badge": "Badge",
|
||||
@@ -407,6 +531,7 @@
|
||||
"contacts_shape_square": "Carré",
|
||||
"contacts_shape_tab": "Onglet",
|
||||
"copied": "Copié !",
|
||||
"copied_to_clipboard": "Copié dans le presse-papiers",
|
||||
"copy": "Copier",
|
||||
"copy_address": "Copier l'adresse complète",
|
||||
"copy_error": "Copier l'erreur",
|
||||
@@ -419,11 +544,16 @@
|
||||
"daemon_bundled": "Intégré",
|
||||
"daemon_install_bundled": "Installer la version intégrée",
|
||||
"daemon_installed": "Installé",
|
||||
"daemon_maintenance_label": "MAINTENANCE",
|
||||
"daemon_none_bundled": "aucun dans cette version",
|
||||
"daemon_not_installed": "non installé",
|
||||
"daemon_status_diff": "Version différente",
|
||||
"daemon_status_differ": "Le binaire installé diffère de la version intégrée.",
|
||||
"daemon_status_match": "Le binaire installé correspond à la version intégrée.",
|
||||
"daemon_status_missing": "Aucun daemon installé — installez la version intégrée.",
|
||||
"daemon_status_none": "Non installé",
|
||||
"daemon_status_ok": "À jour",
|
||||
"daemon_update_active_tt": "Le nœud actuellement installé (actif)",
|
||||
"daemon_update_available": "Une nouvelle version du nœud est disponible",
|
||||
"daemon_update_body": "Cette version du portefeuille inclut un nœud DragonX plus récent que celui actuellement installé. La mise à jour remplace le dragonxd installé (ainsi que dragonx-cli/dragonx-tx), puis arrête et redémarre le nœud pour que la nouvelle version prenne effet. Recommandé — un nœud plus récent peut ajouter des fonctionnalités (par exemple la prise en charge des phrases de récupération) que l'ancien n'a pas.",
|
||||
"daemon_update_browse": "Parcourir toutes les versions…",
|
||||
@@ -433,18 +563,28 @@
|
||||
"daemon_update_download_install": "Télécharger et installer",
|
||||
"daemon_update_downloading": "Téléchargement…",
|
||||
"daemon_update_failed": "Échec de la mise à jour",
|
||||
"daemon_update_install_this": "Installer cette version",
|
||||
"daemon_update_installed": "Installé :",
|
||||
"daemon_update_installed_ok": "Nœud installé",
|
||||
"daemon_update_installing": "Installation…",
|
||||
"daemon_update_keep": "Conserver l'actuel",
|
||||
"daemon_update_later": "Plus tard",
|
||||
"daemon_update_latest": "Dernière :",
|
||||
"daemon_update_latest_badge": "dernière",
|
||||
"daemon_update_latest_tt": "Dernière version disponible",
|
||||
"daemon_update_loading": "Chargement des versions…",
|
||||
"daemon_update_no_notes": "Aucune note de version pour cette version.",
|
||||
"daemon_update_now": "Mettre à jour",
|
||||
"daemon_update_prerelease_tt": "Version préliminaire / de test",
|
||||
"daemon_update_prompt_title": "Mettre à jour le démon du nœud ?",
|
||||
"daemon_update_reinstall": "Réinstaller",
|
||||
"daemon_update_released": "Publiée :",
|
||||
"daemon_update_restart_note": "Redémarrez le daemon pour lancer la nouvelle version.",
|
||||
"daemon_update_restart_now": "Redémarrer le daemon maintenant",
|
||||
"daemon_update_safe": "Votre portefeuille, vos clés et vos données de blockchain ne sont pas touchés — seuls les fichiers du programme daemon sont remplacés. Si vous exécutez délibérément un nœud personnalisé, choisissez Conserver l'actuel.",
|
||||
"daemon_update_status_current": "Cette version est installée",
|
||||
"daemon_update_status_latest": "Dernière version",
|
||||
"daemon_update_status_older": "Antérieure à votre version installée",
|
||||
"daemon_update_title": "Mettre à jour le daemon du nœud ?",
|
||||
"daemon_update_unavailable_body": "Aucune version du nœud n'est disponible pour cette plateforme.",
|
||||
"daemon_update_unavailable_title": "Mises à jour du nœud indisponibles",
|
||||
@@ -453,12 +593,20 @@
|
||||
"daemon_update_verify_note": "Le téléchargement est vérifié par rapport au SHA-256 publié de la version et à une signature ed25519 épinglée avant l'installation.",
|
||||
"daemon_update_verifying": "Vérification…",
|
||||
"daemon_update_version": "Version :",
|
||||
"daemon_updates_label": "MISES À JOUR",
|
||||
"daemon_version": "Daemon",
|
||||
"dark": "Sombre",
|
||||
"data_stale_prefix": "Mis à jour",
|
||||
"data_stale_tooltip": "Le solde est peut-être obsolète — le portefeuille n'a pas reçu de mise à jour récente. Vérifiez la connexion à votre nœud.",
|
||||
"date": "Date",
|
||||
"date_label": "Date :",
|
||||
"debug_gate_confirm": "Afficher les options de débogage",
|
||||
"debug_gate_incorrect": "PIN ou phrase secrète incorrect.",
|
||||
"debug_gate_pass_prompt": "Entrez la phrase secrète de votre portefeuille pour continuer :",
|
||||
"debug_gate_pin_prompt": "Entrez votre PIN de déverrouillage pour continuer :",
|
||||
"debug_gate_title": "Afficher les options de débogage ?",
|
||||
"debug_gate_verifying": "Vérification…",
|
||||
"debug_gate_warning": "Ce sont des options avancées de diagnostic et de réglage du nœud. Elles peuvent exposer des informations sensibles et modifier le fonctionnement de votre nœud. Ne continuez que si vous savez ce que vous faites.",
|
||||
"debug_logging": "JOURNALISATION DE DÉBOGAGE",
|
||||
"decrypt_desc": "Le portefeuille sera exporté, le daemon redémarré avec un nouveau portefeuille non chiffré, et toutes les clés réimportées. Cela peut prendre plusieurs minutes selon la taille du portefeuille.",
|
||||
"decrypt_error_title": "Échec du déchiffrement",
|
||||
@@ -481,10 +629,22 @@
|
||||
"disconnected": "Déconnecté",
|
||||
"dismiss": "Ignorer",
|
||||
"display": "Affichage",
|
||||
"done": "Terminé",
|
||||
"download": "Télécharger",
|
||||
"download_bootstrap": "Télécharger Bootstrap",
|
||||
"dragonx_green": "DragonX (Vert)",
|
||||
"edit": "Modifier",
|
||||
"empty_wallet_keys_suffix": "clés",
|
||||
"empty_wallet_open_manager": "Ouvrir le gestionnaire de portefeuilles",
|
||||
"empty_wallet_restore": "Restaurer mon portefeuille",
|
||||
"empty_wallet_salvage_body": "Ce portefeuille est vide car une réparation automatique antérieure a mis votre portefeuille d'origine de côté comme sauvegarde. Vos pièces se trouvent presque certainement dans cette sauvegarde, elles ne sont pas perdues. Restaurez-la pour recharger vos fonds — rien n'est supprimé ; le fichier actuel est d'abord mis de côté.",
|
||||
"empty_wallet_salvage_headline": "Vos pièces sont en sécurité dans un fichier de sauvegarde.",
|
||||
"empty_wallet_salvage_title": "Votre portefeuille a peut-être été réparé",
|
||||
"empty_wallet_warning_body": "Ce portefeuille n'a aucune adresse ni fonds, mais un autre fichier de portefeuille dans votre dossier DragonX contient des clés. Vos pièces s'y trouvent très probablement, elles ne sont pas perdues. Ouvrez le gestionnaire de portefeuilles pour passer au portefeuille qui contient vos fonds.",
|
||||
"empty_wallet_warning_dismiss": "Ne plus avertir pour ce portefeuille",
|
||||
"empty_wallet_warning_dismiss_tip": "Arrête cet avertissement uniquement pour le fichier de portefeuille actuel. Si vous passez plus tard à un autre portefeuille vide, il pourra avertir à nouveau.",
|
||||
"empty_wallet_warning_headline": "Vous avez peut-être ouvert le mauvais portefeuille.",
|
||||
"empty_wallet_warning_title": "Ce portefeuille est vide",
|
||||
"enc_confirm": "Confirmer :",
|
||||
"enc_desc": "Chiffrer votre portefeuille protège vos clés privées avec une phrase secrète. Après le chiffrement, le daemon redémarrera.",
|
||||
"enc_encrypting": "Chiffrement du portefeuille...",
|
||||
@@ -523,6 +683,7 @@
|
||||
"explorer_tx_outputs": "Sorties",
|
||||
"explorer_tx_size": "Taille",
|
||||
"explorer_txid": "TxID :",
|
||||
"explorer_urls_hdr": "URLS",
|
||||
"export": "Exporter",
|
||||
"export_csv": "Exporter en CSV",
|
||||
"export_keys_btn": "Exporter les clés",
|
||||
@@ -550,6 +711,151 @@
|
||||
"export_viewing_key": "Exporter la clé de visualisation",
|
||||
"failed_create_shielded": "Échec de la création de l'adresse blindée",
|
||||
"failed_create_transparent": "Échec de la création de l'adresse transparente",
|
||||
"faq": "FAQ",
|
||||
"faq_d_mgmt_1_a": "Le nœud intégré démarre automatiquement avec le portefeuille et s'arrête quand il le faut, donc vous n'avez normalement pas besoin de le gérer manuellement. Les contrôles avancés (y compris le redémarrage du démon) se trouvent dans Paramètres > Nœud et sécurité.",
|
||||
"faq_d_mgmt_1_q": "Comment démarrer ou arrêter le nœud ?",
|
||||
"faq_d_mgmt_2_a": "Si vous connectez le portefeuille à un nœud que vous avez démarré vous-même, cette option détermine si la fermeture du portefeuille arrête aussi ce nœud. Laissez-la désactivée si vous voulez que votre nœud continue de fonctionner après la fermeture du portefeuille.",
|
||||
"faq_d_mgmt_2_q": "À quoi sert « fermer le démon externe à la sortie » ?",
|
||||
"faq_d_mgmt_3_a": "Les données du nœud et du portefeuille se trouvent dans le répertoire de données DragonX sous votre profil utilisateur (par exemple, dans AppData sous Windows ou votre dossier personnel sous Linux/macOS). La blockchain en est la partie volumineuse ; gardez assez d'espace disque libre pour sa croissance.",
|
||||
"faq_d_mgmt_3_q": "Où sont stockées la blockchain et les données du portefeuille ?",
|
||||
"faq_d_mgmt_title": "Gestion du nœud",
|
||||
"faq_d_mine_1_a": "L'onglet Minage vous permet de miner avec votre processeur (CPU). DragonX utilise l'algorithme RandomX, conçu pour les processeurs classiques. Définissez le nombre de threads et démarrez — les récompenses s'accumulent dans votre portefeuille.",
|
||||
"faq_d_mine_1_q": "Comment miner du DragonX ?",
|
||||
"faq_d_mine_2_a": "Le minage solo vous envoie les récompenses directement, mais ne paie que lorsque vous trouvez un bloc, ce qui est peu fréquent sauf si vous avez beaucoup de puissance de calcul. Le minage en pool partage le travail avec d'autres pour des gains plus petits mais plus réguliers. L'onglet Minage prend en charge les deux.",
|
||||
"faq_d_mine_2_q": "Minage solo ou en pool — lequel choisir ?",
|
||||
"faq_d_mine_3_a": "La section pool de l'onglet Minage propose un bouton « Mettre à jour le mineur… » qui télécharge, vérifie et installe la dernière version optimisée du mineur DragonX. Comme pour la mise à jour du nœud, chaque téléchargement est vérifié par somme de contrôle et signature avant l'installation.",
|
||||
"faq_d_mine_3_q": "Comment mettre à jour le mineur ?",
|
||||
"faq_d_mine_4_a": "Le nœud actuel peut exécuter un serveur stratum intégré afin que d'autres mineurs puissent se connecter à votre machine. Une option se trouve dans Paramètres > Nœud et sécurité. Par défaut, il n'écoute qu'en local ; l'exposer à d'autres ordinateurs nécessite de l'ouvrir volontairement.",
|
||||
"faq_d_mine_4_q": "Puis-je héberger une pool de minage ?",
|
||||
"faq_d_mine_title": "Minage et pools",
|
||||
"faq_d_net_1_a": "Juste après le lancement, le nœud a besoin d'un instant pour trouver des pairs, donc afficher brièvement « 0 pair » est normal. Si cela persiste, vérifiez votre connexion internet et qu'un pare-feu ne bloque pas le nœud. Le nœud trouve automatiquement des pairs via des graines intégrées.",
|
||||
"faq_d_net_1_q": "Le portefeuille n'affiche aucun pair ni connexion — que se passe-t-il ?",
|
||||
"faq_d_net_2_a": "Oui — les nœuds DragonX communiquent entre eux via TLS, donc les connexions entre pairs sont chiffrées. Le portefeuille communique aussi avec son propre nœud via un canal local sécurisé.",
|
||||
"faq_d_net_2_q": "Ma connexion au réseau est-elle chiffrée ?",
|
||||
"faq_d_net_title": "Pairs et réseau",
|
||||
"faq_d_node_1_a": "Le démon, dragonxd, est le nœud complet de DragonX. Il télécharge et vérifie l'intégralité de la blockchain, relaie les transactions vers le réseau et répond aux requêtes du portefeuille. Exécuter votre propre nœud signifie que vous n'avez à faire confiance au serveur de personne d'autre.",
|
||||
"faq_d_node_1_q": "Qu'est-ce que le démon (dragonxd) ?",
|
||||
"faq_d_node_2_a": "Par défaut, le portefeuille lance et gère son propre nœud intégré (embarqué) — vous n'avez rien à faire. Si vous exécutez déjà dragonxd vous-même, le portefeuille peut se connecter à ce nœud externe à la place. Le portefeuille détecte la situation et se comporte en conséquence.",
|
||||
"faq_d_node_2_q": "Nœud intégré ou externe — quelle est la différence ?",
|
||||
"faq_d_node_3_a": "Un nœud complet valide indépendamment chaque règle de la blockchain, de sorte que votre portefeuille fait confiance aux mathématiques plutôt qu'à un tiers. Cela renforce aussi le réseau. La contrepartie est l'espace disque et une synchronisation initiale.",
|
||||
"faq_d_node_3_q": "Pourquoi exécuter un nœud complet ?",
|
||||
"faq_d_node_title": "Le nœud complet",
|
||||
"faq_d_perf_1_a": "La blockchain est l'élément le plus volumineux et croît avec le temps, donc gardez plusieurs gigaoctets libres. Le nœud utilise un cache de base de données pour la rapidité ; les valeurs par défaut intégrées sont adaptées aux machines courantes. Les utilisateurs avancés peuvent ajuster le cache de base de données du nœud en modifiant dbcache dans son fichier de configuration (DRAGONX.conf).",
|
||||
"faq_d_perf_1_q": "Combien d'espace disque le nœud utilise-t-il, et puis-je régler la mémoire ?",
|
||||
"faq_d_perf_2_a": "Activez le téléchargement du bootstrap (proposé dans l'assistant de configuration) pour récupérer une copie récente et vérifiée de la chaîne au lieu de valider chaque bloc depuis le début. Un disque rapide (SSD) aide aussi beaucoup.",
|
||||
"faq_d_perf_2_q": "Comment accélérer la synchronisation initiale ?",
|
||||
"faq_d_perf_title": "Stockage et performances",
|
||||
"faq_d_sync_1_a": "Au lancement, le nœud charge et valide les blocs avant de pouvoir servir le portefeuille. L'écran « Traitement des blocs / Application des blocs pour construire l'état actuel de la chaîne » correspond à cette phase de préparation. C'est normal ; le portefeuille devient utilisable une fois que le nœud a terminé et signalé sa hauteur.",
|
||||
"faq_d_sync_1_q": "Que signifie « Traitement des blocs » au démarrage ?",
|
||||
"faq_d_sync_2_a": "Pour vos fonds blindés, le nœud conserve des « témoins » cryptographiques qui vous permettent de dépenser en privé. Après certains redémarrages ou mises à jour, il reconstruit ce cache en réanalysant les blocs récents. Sur un portefeuille avec de nombreuses transactions, cela peut prendre plusieurs minutes et utiliser de la mémoire supplémentaire.\n\nCe n'est pas bloqué — laissez-le terminer. Forcer l'arrêt en plein milieu ne fait que redémarrer la reconstruction la prochaine fois.",
|
||||
"faq_d_sync_2_q": "Qu'est-ce que « Reconstruction du cache de témoins » ?",
|
||||
"faq_d_sync_3_a": "Près de la pointe de la chaîne, le travail qui analyse votre portefeuille (vérification des soldes et des notes) est en concurrence avec le nœud pour le même verrou interne, ce qui peut ralentir la connexion des derniers blocs sur un gros portefeuille. Les versions récentes limitent cette analyse pour laisser le nœud rattraper son retard. Rester sur un onglet plus léger (ou simplement attendre) permet de terminer plus vite.",
|
||||
"faq_d_sync_3_q": "Pourquoi la synchronisation est-elle lente alors que je suis presque à jour ?",
|
||||
"faq_d_sync_4_a": "La première synchronisation télécharge et vérifie toute la chaîne et peut prendre du temps selon votre connexion et votre disque. Vous pouvez l'accélérer considérablement en activant le téléchargement du bootstrap dans l'assistant de configuration, qui récupère une copie récente et vérifiée des données de la chaîne.",
|
||||
"faq_d_sync_4_q": "Combien de temps dure la synchronisation initiale ?",
|
||||
"faq_d_sync_title": "Synchronisation et blockchain",
|
||||
"faq_d_trbl_1_a": "Généralement non. Sur un gros portefeuille, le nœud peut passer plusieurs minutes à reconstruire les témoins blindés ou à appliquer des blocs, période durant laquelle il semble en pause et peut répondre lentement. Laissez-lui du temps — le journal de débogage (Paramètres) montre une progression régulière s'il fonctionne.\n\nÉvitez de forcer l'arrêt pendant cette phase ; cela redémarre le travail depuis le début au prochain lancement.",
|
||||
"faq_d_trbl_1_q": "Le nœud semble bloqué sur « Activation de la meilleure chaîne » — est-il figé ?",
|
||||
"faq_d_trbl_2_a": "Si un fichier de portefeuille est récupéré mais qu'une partie de ses données de clés est manquante, le nœud peut l'ouvrir dans un mode « dégradé » limité où il ne peut pas dériver de nouvelles adresses ni blinder des fonds. La solution la plus sûre est de restaurer à partir de votre phrase de récupération dans un nouveau portefeuille.",
|
||||
"faq_d_trbl_2_q": "Qu'est-ce que le « mode dégradé » ?",
|
||||
"faq_d_trbl_3_a": "Une réanalyse fait relire la chaîne par le portefeuille pour redécouvrir vos transactions (utile après l'import de clés). Une réindexation reconstruit la base de données blockchain du nœud et n'est nécessaire que si cette base est corrompue ou incompatible après un changement de version. Les deux peuvent prendre du temps ; lancez-les depuis Paramètres > Nœud et sécurité.",
|
||||
"faq_d_trbl_3_q": "Quand dois-je réindexer ou réanalyser ?",
|
||||
"faq_d_trbl_4_a": "L'utilisation de la mémoire augmente pendant un travail intensif comme la reconstruction des témoins ou une synchronisation initiale, puis redescend une fois terminé. Une mémoire élevée soutenue au repos est inhabituelle — redémarrer le portefeuille la libère. Sur une machine à faible RAM, vérifiez que d'autres applications gourmandes en mémoire ne sont pas en concurrence.",
|
||||
"faq_d_trbl_4_q": "Le nœud utilise beaucoup de mémoire — est-ce normal ?",
|
||||
"faq_d_trbl_title": "Dépannage",
|
||||
"faq_d_upd_1_a": "Ouvrez Paramètres > Nœud et sécurité > Binaire du démon et utilisez « Rechercher des mises à jour… ». Le portefeuille télécharge la dernière version vérifiée du nœud et l'installe ; la nouvelle version prend effet au prochain démarrage du démon.",
|
||||
"faq_d_upd_1_q": "Comment mettre à jour le nœud ?",
|
||||
"faq_d_upd_2_a": "Oui — la boîte de dialogue de mise à jour liste toutes les versions publiées afin que vous puissiez en choisir une spécifique ou plus ancienne. Soyez prudent en cas de rétrogradation : un nœud plus ancien peut ne pas accepter des données blockchain écrites par une version plus récente et pourrait nécessiter une réindexation.",
|
||||
"faq_d_upd_2_q": "Puis-je installer une version spécifique du nœud ?",
|
||||
"faq_d_upd_3_a": "Chaque archive de nœud téléchargée est vérifiée par une somme de contrôle et une signature cryptographique avant d'être installée. Si la vérification échoue, le portefeuille refuse la mise à jour.",
|
||||
"faq_d_upd_3_q": "La mise à jour est-elle sûre ?",
|
||||
"faq_d_upd_title": "Mise à jour du nœud",
|
||||
"faq_group_daemon": "Démon",
|
||||
"faq_group_wallet": "Portefeuille",
|
||||
"faq_intro": "Réponses sur votre portefeuille et le nœud DragonX. Cherchez, ou parcourez par sujet ci-dessous.",
|
||||
"faq_intro_lite": "Réponses sur votre portefeuille Lite. Cherchez, ou parcourez par sujet ci-dessous.",
|
||||
"faq_l_gs_1_a": "ObsidianDragonLite est le portefeuille DragonX allégé. Au lieu d'exécuter un nœud complet, il se connecte à un serveur DragonX lite, ce qui lui permet de démarrer rapidement et de ne pas télécharger toute la blockchain.\n\nIl gère vos clés, vos soldes et vos envois tout comme le portefeuille complet — voir la section « Portefeuille Lite » ci-dessous pour comprendre le fonctionnement de la connexion au serveur et le compromis en matière de confidentialité.",
|
||||
"faq_l_gs_1_q": "Qu'est-ce qu'ObsidianDragonLite ?",
|
||||
"faq_l_lite_1_a": "Le portefeuille Lite communique avec un serveur DragonX lite au lieu d'exécuter son propre nœud complet. Cela le rend rapide à démarrer et léger en disque et en bande passante, car il ne télécharge ni ne vérifie lui-même toute la blockchain. Vos clés privées restent toujours sur votre appareil — elles ne sont jamais envoyées au serveur.",
|
||||
"faq_l_lite_1_q": "En quoi le portefeuille Lite diffère-t-il du portefeuille complet ?",
|
||||
"faq_l_lite_2_a": "Ouvrez Paramètres et choisissez un serveur. Vous pouvez changer de serveur à tout moment et le portefeuille se resynchronise avec le nouveau. Si un serveur est lent ou inaccessible, essayez-en un autre.",
|
||||
"faq_l_lite_2_q": "Comment choisir ou changer de serveur ?",
|
||||
"faq_l_lite_3_a": "Vos clés ne quittent jamais votre appareil, et les montants blindés restent chiffrés. Mais le serveur lite voit quelles adresses votre portefeuille interroge ainsi que votre adresse IP, ce qui lui permet de relier vos adresses entre elles — un compromis entre confidentialité et commodité. Pour un maximum de confidentialité, utilisez le portefeuille à nœud complet, qui vérifie lui-même la chaîne et ne révèle pas vos adresses à un serveur.",
|
||||
"faq_l_lite_3_q": "Le portefeuille Lite est-il aussi privé que le nœud complet ?",
|
||||
"faq_l_lite_title": "Portefeuille Lite",
|
||||
"faq_l_sec_1_a": "Ouvrez Paramètres > Portefeuille et définissez une phrase secrète dans la section sécurité. Le chiffrement protège vos clés privées sur le disque, de sorte qu'une personne ayant accès aux fichiers ne puisse toujours pas dépenser vos pièces.\n\nChoisissez une phrase secrète forte et ne la perdez pas — il n'existe aucun moyen de récupérer un portefeuille chiffré sans elle.",
|
||||
"faq_no_results": "Aucun résultat. Essayez un autre terme de recherche.",
|
||||
"faq_open_tooltip": "Aide et FAQ",
|
||||
"faq_search_hint": "Rechercher dans la FAQ…",
|
||||
"faq_title": "Aide et FAQ",
|
||||
"faq_w_addr_1_a": "Les adresses transparentes (elles commencent par R ou t) fonctionnent comme la plupart des cryptomonnaies : les montants et adresses sont publics sur la blockchain.\n\nLes adresses blindées (adresses Z, commençant par zs) utilisent la cryptographie à divulgation nulle de connaissance — le montant, l'expéditeur et le destinataire sont chiffrés sur la chaîne. Les soldes détenus sur des adresses blindées sont privés.",
|
||||
"faq_w_addr_1_q": "Quelle est la différence entre les adresses transparentes et blindées ?",
|
||||
"faq_w_addr_2_a": "Privilégiez les adresses blindées (Z) autant que possible — elles préservent la confidentialité de votre solde et de votre historique de paiements. DragonX est une cryptomonnaie axée sur la confidentialité, et le blindage est le mode par défaut pour les fonds reçus.\n\nN'utilisez une adresse transparente que lorsqu'un service avec lequel vous interagissez ne peut pas gérer les adresses blindées.",
|
||||
"faq_w_addr_2_q": "Quel type d'adresse dois-je utiliser ?",
|
||||
"faq_w_addr_3_a": "Pour les adresses blindées, la réutilisation ne pose aucun problème et ne révèle pas l'historique. Pour les adresses transparentes, utiliser une nouvelle adresse à chaque paiement améliore la confidentialité. Créez de nouvelles adresses depuis l'onglet Recevoir.",
|
||||
"faq_w_addr_3_q": "Dois-je réutiliser une adresse ou en créer une nouvelle ?",
|
||||
"faq_w_addr_4_a": "Le blindage déplace des pièces d'une adresse transparente vers une adresse blindée, les rendant privées. Les pièces nouvellement minées arrivent en transparent et sont blindées automatiquement pour vous.\n\nVous pouvez aussi blinder manuellement depuis le portefeuille ; un blindage est simplement une transaction spéciale, il faut donc un temps de confirmation normal pour qu'elle se termine.",
|
||||
"faq_w_addr_4_q": "Qu'est-ce que le « blindage » ?",
|
||||
"faq_w_addr_title": "Adresses et confidentialité",
|
||||
"faq_w_bal_1_a": "Tant que votre portefeuille est encore en cours de synchronisation, votre solde est incomplet et peut afficher 0 — il n'a pas encore analysé toutes vos transactions. Il se complète une fois la synchronisation terminée.\n\nSurveillez la barre d'état en bas : elle indique l'état de la connexion et la hauteur de bloc. Une fois la synchronisation terminée, votre solde est exact.",
|
||||
"faq_w_bal_1_q": "Pourquoi mon solde est-il à 0 ou ne se met-il pas à jour ?",
|
||||
"faq_w_bal_2_a": "Chaque nouveau bloc miné au-dessus du bloc contenant votre transaction ajoute une confirmation. Plus il y a de confirmations, plus le paiement est fermement établi. Les fonds reçus deviennent dépensables après la première confirmation.",
|
||||
"faq_w_bal_2_q": "Que sont les confirmations ?",
|
||||
"faq_w_bal_3_a": "Le total inclut les fonds encore en cours de confirmation ou temporairement verrouillés (par exemple, des pièces en cours de blindage). Le solde dépensable est ce que vous pouvez envoyer immédiatement. Les deux convergent au fur et à mesure que les transactions se confirment.",
|
||||
"faq_w_bal_3_q": "Quelle est la différence entre le solde total et le solde dépensable ?",
|
||||
"faq_w_bal_title": "Solde et synchronisation",
|
||||
"faq_w_chat_1_a": "Le chat est une messagerie chiffrée intégrée au portefeuille, fonctionnant sur la chaîne. Les messages sont envoyés sous forme de mémos blindés privés, de sorte que seuls vous et votre contact pouvez les lire.",
|
||||
"faq_w_chat_1_q": "Qu'est-ce que la fonction Chat ?",
|
||||
"faq_w_chat_2_a": "Votre identité de chat est dérivée de la phrase de récupération de votre portefeuille, elle vous suit donc avec votre portefeuille — restaurez le portefeuille et votre identité revient. Vous ne créez pas de compte ou de mot de passe distinct.",
|
||||
"faq_w_chat_2_q": "Comment fonctionne mon identité de chat ?",
|
||||
"faq_w_chat_3_a": "Ajoutez un contact via son adresse dans l'onglet Contacts, avec éventuellement un nom et un avatar. Les contacts peuvent rester privés au portefeuille actuel ou être partagés entre tous les portefeuilles que vous ouvrez sur cet ordinateur.",
|
||||
"faq_w_chat_3_q": "Comment fonctionnent les contacts ?",
|
||||
"faq_w_chat_4_a": "Ouvrez une conversation et utilisez les icônes de son en-tête. Masquer (l'œil) la retire de la liste mais conserve tous les messages — un nouveau message la fait réapparaître. Supprimer (la corbeille) ouvre une boîte de dialogue : « Supprimer » efface l'historique sur cet appareil (si cette personne vous écrit à nouveau, la conversation revient), tandis que « Supprimer et bloquer » arrête aussi ses futurs messages jusqu'à ce que vous la débloquiez depuis la liste « Bloqués » au-dessus de la liste des conversations. Tout ceci est local à cet appareil — les messages restent sur la blockchain et l'autre personne conserve sa propre copie, ce n'est donc pas un « retrait de message ».",
|
||||
"faq_w_chat_4_q": "Comment masquer, supprimer ou bloquer une conversation ?",
|
||||
"faq_w_chat_title": "Chat et contacts",
|
||||
"faq_w_gs_1_a": "ObsidianDragon est un portefeuille à nœud complet pour DragonX (DRGX). Il gère vos pièces et, sur la version bureau, exécute un nœud complet DragonX (le démon) en arrière-plan afin que votre portefeuille vérifie lui-même la blockchain plutôt que de faire confiance à un serveur tiers.\n\nVoyez-le comme deux parties fonctionnant ensemble : le portefeuille (clés, soldes, envoi) et le nœud (la blockchain et le réseau). Les questions ici sont réparties de la même façon — voir les onglets Portefeuille et Démon ci-dessus.",
|
||||
"faq_w_gs_1_q": "Qu'est-ce qu'ObsidianDragon ?",
|
||||
"faq_w_gs_2_a": "Au premier lancement, l'assistant de configuration crée automatiquement un portefeuille pour vous. Les nouveaux portefeuilles sont sécurisés par une phrase de récupération secrète (une liste de mots). Notez cette phrase et conservez-la hors ligne — c'est le seul moyen de restaurer vos fonds si cet ordinateur est perdu.\n\nVous pouvez consulter ou sauvegarder la phrase à tout moment depuis Paramètres > Sauvegarde et données > Phrase de récupération.",
|
||||
"faq_w_gs_2_q": "Comment créer un nouveau portefeuille ?",
|
||||
"faq_w_gs_3_a": "Utilisez le flux de restauration par phrase de récupération si vous en avez une. (Sur la version à nœud complet, vous pouvez aussi copier un fichier wallet.dat existant dans le dossier de données du portefeuille avant le lancement.)\n\nAprès la restauration, le portefeuille se réanalyse pour retrouver vos transactions passées ; votre solde et votre historique peuvent donc mettre un moment à apparaître la première fois.",
|
||||
"faq_w_gs_3_q": "Comment restaurer un portefeuille que j'ai déjà ?",
|
||||
"faq_w_gs_4_a": "L'assistant vous permet de choisir une apparence/un thème, de configurer le chiffrement et un code PIN, et — sur la version à nœud complet — de télécharger en option un bootstrap pour accélérer la première synchronisation. Vous pouvez ignorer n'importe quelle étape et modifier tout cela plus tard dans Paramètres.",
|
||||
"faq_w_gs_4_q": "Que fait la configuration au premier lancement ?",
|
||||
"faq_w_gs_title": "Prise en main",
|
||||
"faq_w_sec_1_a": "Ouvrez Paramètres > Nœud et sécurité et définissez une phrase secrète (section SÉCURITÉ). Le chiffrement protège vos clés privées sur le disque, de sorte qu'une personne ayant accès aux fichiers ne puisse toujours pas dépenser vos pièces.\n\nChoisissez une phrase secrète forte et ne la perdez pas — il n'existe aucun moyen de récupérer un portefeuille chiffré sans elle.",
|
||||
"faq_w_sec_1_q": "Comment chiffrer mon portefeuille ?",
|
||||
"faq_w_sec_2_a": "Le code PIN verrouille l'écran de l'application afin que les soldes et actions soient masqués lorsque vous vous absentez. C'est un verrouillage de confort qui s'ajoute au chiffrement ; vous le configurez pendant l'assistant ou dans Paramètres, après avoir chiffré le portefeuille.",
|
||||
"faq_w_sec_2_q": "Qu'est-ce que le code PIN / l'écran de verrouillage ?",
|
||||
"faq_w_sec_3_a": "Non. Le chiffrement du portefeuille ne peut pas être contourné. Si vous avez encore votre phrase de récupération, vous pouvez restaurer le portefeuille à partir de celle-ci dans un nouveau portefeuille et définir une nouvelle phrase secrète. Sans la phrase secrète ni la phrase de récupération, les fonds ne peuvent pas être récupérés.",
|
||||
"faq_w_sec_3_q": "J'ai oublié ma phrase secrète — peut-elle être récupérée ?",
|
||||
"faq_w_sec_title": "Sécurité et chiffrement",
|
||||
"faq_w_seed_1_a": "Allez dans Paramètres > Sauvegarde et données > Phrase de récupération. Notez les mots sur papier dans l'ordre et conservez-les en lieu sûr et hors ligne. Toute personne possédant la phrase peut dépenser vos pièces, ne la stockez donc jamais dans une photo, un e-mail ou une note dans le cloud.",
|
||||
"faq_w_seed_1_q": "Comment sauvegarder ma phrase de récupération ?",
|
||||
"faq_w_seed_2_a": "Les anciens portefeuilles (hérités) n'étaient pas basés sur une phrase de récupération. Le portefeuille peut migrer un portefeuille hérité vers un portefeuille moderne à phrase de récupération : il crée un nouveau portefeuille à phrase et y transfère vos fonds. Cherchez « Migrer vers un portefeuille à phrase… » dans Paramètres > Sauvegarde et données. Cette fonctionnalité nécessite la version actuelle du démon.",
|
||||
"faq_w_seed_2_q": "Mon portefeuille n'a pas de phrase de récupération — puis-je en ajouter une ?",
|
||||
"faq_w_seed_3_a": "Utilisez le flux de restauration par phrase de récupération lors de la configuration d'un portefeuille et saisissez vos mots dans l'ordre. Votre portefeuille se réanalyse ensuite pour reconstruire votre solde et votre historique, ce qui peut prendre un moment la première fois.",
|
||||
"faq_w_seed_3_q": "Comment restaurer à partir de ma phrase de récupération ?",
|
||||
"faq_w_seed_4_a": "Votre phrase de récupération est la sauvegarde principale et suffit à tout restaurer. Une copie de votre fichier de portefeuille est une sauvegarde secondaire pratique qui conserve aussi les libellés et les paramètres. Conservez toute sauvegarde hors ligne et en lieu privé.",
|
||||
"faq_w_seed_4_q": "Dois-je aussi sauvegarder wallet.dat ?",
|
||||
"faq_w_seed_title": "Phrase de récupération et sauvegarde",
|
||||
"faq_w_send_1_a": "Ouvrez l'onglet Envoyer, collez l'adresse du destinataire, saisissez un montant et confirmez. Si le portefeuille est chiffré, on vous demandera de le déverrouiller pour la transaction.\n\nLes envois vers des adresses blindées sont privés ; les envois vers des adresses transparentes sont publics.",
|
||||
"faq_w_send_1_q": "Comment envoyer des fonds ?",
|
||||
"faq_w_send_2_a": "Les frais DragonX sont très faibles et définis automatiquement. Les frais sont affichés avant que vous ne confirmiez. Vous n'avez normalement pas besoin de les modifier.",
|
||||
"faq_w_send_2_q": "Quels frais dois-je payer ?",
|
||||
"faq_w_send_3_a": "Oui — lors de l'envoi vers une adresse blindée (Z), vous pouvez inclure un mémo chiffré. Seul le destinataire peut le lire. Les adresses transparentes ne prennent pas en charge les mémos.",
|
||||
"faq_w_send_3_q": "Puis-je joindre un message à un paiement ?",
|
||||
"faq_w_send_4_a": "Une transaction est « en attente » jusqu'à ce qu'elle soit minée dans un bloc et gagne des confirmations. Cela prend généralement une minute ou deux. Les transactions blindées nécessitent aussi que votre portefeuille soit synchronisé pour construire la preuve.\n\nSi un envoi reste en attente anormalement longtemps, vérifiez que votre portefeuille est connecté et synchronisé (voir la barre d'état en bas de la fenêtre).",
|
||||
"faq_w_send_4_q": "J'ai envoyé un paiement mais il indique « en attente » — pourquoi ?",
|
||||
"faq_w_send_title": "Envoi et réception",
|
||||
"faq_w_set_1_a": "Ouvrez Paramètres > Apparence pour choisir un thème et un accent. Plusieurs habillages clairs et sombres sont inclus, et des effets comme le flou peuvent être désactivés pour les machines moins puissantes.",
|
||||
"faq_w_set_1_q": "Comment changer le thème ?",
|
||||
"faq_w_set_2_a": "Oui — Paramètres > Apparence possède un sélecteur de langue. Le portefeuille est fourni avec plusieurs traductions ; ce qui n'est pas encore traduit revient à l'anglais.",
|
||||
"faq_w_set_2_q": "Puis-je changer la langue ?",
|
||||
"faq_w_set_3_a": "Utilisez l'option d'échelle de police dans Paramètres > Apparence. Elle redimensionne toute l'interface, ce qui est pratique sur les écrans haute résolution (HiDPI) où l'application pourrait sinon s'afficher trop petite.",
|
||||
"faq_w_set_3_q": "Le texte est trop petit (ou trop grand) — puis-je le redimensionner ?",
|
||||
"faq_w_set_4_a": "Activez la récupération des prix dans Paramètres. Le portefeuille affiche alors une valeur fiduciaire approximative à côté des soldes et peut afficher un petit graphique de marché. Les prix sont fournis à titre informatif uniquement.",
|
||||
"faq_w_set_4_q": "Comment afficher les prix en monnaie fiduciaire ?",
|
||||
"faq_w_set_title": "Paramètres et apparence",
|
||||
"favorite_address": "Ajouter aux favoris",
|
||||
"fee": "Frais",
|
||||
"fee_high": "Élevés",
|
||||
@@ -572,6 +878,63 @@
|
||||
"general": "Général",
|
||||
"generating": "Génération",
|
||||
"go_to_receive": "Aller à Recevoir",
|
||||
"grpa_current_block_paren": "(Actuel : %d)",
|
||||
"grpa_days_ago": "il y a %lld jours",
|
||||
"grpa_dbg_addrman": "Suivi et gestion des adresses des pairs",
|
||||
"grpa_dbg_alert": "Messages du système d'alerte",
|
||||
"grpa_dbg_bench": "Chronométrage des performances des opérations",
|
||||
"grpa_dbg_coindb": "Opérations de lecture/écriture de la base de données des pièces",
|
||||
"grpa_dbg_db": "Opérations Berkeley DB",
|
||||
"grpa_dbg_estimatefee": "Algorithme d'estimation des frais",
|
||||
"grpa_dbg_http": "Activité du serveur RPC HTTP",
|
||||
"grpa_dbg_libevent": "Bibliothèque réseau Libevent",
|
||||
"grpa_dbg_lock": "Débogage de la contention des verrous",
|
||||
"grpa_dbg_mempool": "Activité du pool mémoire des transactions",
|
||||
"grpa_dbg_net": "Connexions et messages réseau",
|
||||
"grpa_dbg_paymentdisclosure": "Protocole de divulgation de paiement",
|
||||
"grpa_dbg_pow": "Activité de minage par preuve de travail",
|
||||
"grpa_dbg_proxy": "Connexions proxy SOCKS5",
|
||||
"grpa_dbg_prune": "Opérations d'élagage des blocs",
|
||||
"grpa_dbg_rand": "Génération de nombres aléatoires",
|
||||
"grpa_dbg_reindex": "Progression de la réindexation de la blockchain",
|
||||
"grpa_dbg_rpc": "Traitement des commandes RPC",
|
||||
"grpa_dbg_selectcoins": "Sélection des pièces pour les transactions",
|
||||
"grpa_dbg_tor": "Intégration de Tor et informations de circuit",
|
||||
"grpa_dbg_zmq": "Système de notification ZeroMQ",
|
||||
"grpa_dbg_zrpc": "Opérations RPC protégées (z-addr)",
|
||||
"grpa_enter_private_key_to_import": "Saisissez une clé privée à importer.",
|
||||
"grpa_error_prefix": "Erreur : ",
|
||||
"grpa_hr_ago": "il y a %lld h",
|
||||
"grpa_invalid_response_from_daemon": "Réponse invalide du démon",
|
||||
"grpa_invalid_suffix": " (invalide)",
|
||||
"grpa_min_ago": "il y a %lld min",
|
||||
"grpa_sec_ago": "il y a %lld s",
|
||||
"grpa_seed_demo_chat": "Chat de démonstration seed",
|
||||
"grpa_showing_first_100_of": "... affichage des 100 premiers sur %d",
|
||||
"grpa_tab_about": "À propos",
|
||||
"grpa_tab_appearance": "Apparence",
|
||||
"grpa_tab_backup_data": "Sauvegarde et données",
|
||||
"grpa_tab_chat": "Chat",
|
||||
"grpa_tab_explorer": "Explorateur",
|
||||
"grpa_tab_node_security": "Nœud et sécurité",
|
||||
"grpa_tab_wallet": "Portefeuille",
|
||||
"grpa_unexpected_getblockhash_result": "résultat getblockhash inattendu",
|
||||
"grpb_copy": "Copier",
|
||||
"grpb_max": "Max",
|
||||
"grpb_new_badge_suffix": " [NOUVEAU]",
|
||||
"grpb_preview_msg_payment_through": "Le paiement est-il passé ? 🙂",
|
||||
"grpb_preview_msg_sending_rest": "J'envoie le reste maintenant 👍",
|
||||
"grpb_preview_msg_yep_confirmed": "Oui — je viens de confirmer ✅",
|
||||
"grpb_selected_suffix": "\n(sélectionné)",
|
||||
"grpb_tooltip_address_balance": "%s\nSolde : %.8f %s%s",
|
||||
"grpb_undo_clear": "Annuler l'effacement",
|
||||
"grpc_benchmark_inconclusive": "Benchmark non concluant : aucun échantillon de taux de hachage n'a été enregistré. Vérifiez la connexion au pool et réessayez.",
|
||||
"grpc_benchmark_takes_secs": "Le benchmark prend environ %d s et interrompt le minage. Cliquez de nouveau pour démarrer.",
|
||||
"grpc_bootstrap_failed": "Échec de l'amorçage",
|
||||
"grpc_bootstrap_not_initialized": "Amorçage non initialisé",
|
||||
"grpc_hashrate_fee": "%s %s%% de frais",
|
||||
"grpc_key_not_available": "Clé non disponible pour cette adresse",
|
||||
"grpc_na": "N/D",
|
||||
"height": "Hauteur",
|
||||
"help": "Aide",
|
||||
"hidden_tag": " (masqué)",
|
||||
@@ -646,6 +1009,7 @@
|
||||
"light": "Clair",
|
||||
"lite_account_label": "Compte",
|
||||
"lite_action": "Action",
|
||||
"lite_backend_unavailable": "Backend du portefeuille léger indisponible",
|
||||
"lite_backup_keys": "Sauvegarde et clés",
|
||||
"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).",
|
||||
@@ -653,9 +1017,12 @@
|
||||
"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_start": "Impossible de démarrer l'opération",
|
||||
"lite_could_not_write": "Impossible d'écrire ",
|
||||
"lite_encrypt_wallet": "Chiffrer le portefeuille",
|
||||
"lite_encryption_removed": "Chiffrement supprimé",
|
||||
"lite_enter_all_seed_words": "Entrez les 24 mots de la phrase de récupération (%d obtenus)",
|
||||
"lite_enter_wallet_path": "Entrez un chemin de portefeuille",
|
||||
"lite_hide_wipe": "Masquer et effacer",
|
||||
"lite_import": "Importer",
|
||||
"lite_import_key_label": "Importer une clé",
|
||||
@@ -792,9 +1159,27 @@
|
||||
"memo_optional": "MÉMO (OPTIONNEL)",
|
||||
"memo_upper": "MÉMO",
|
||||
"memo_z_only": "Note : Les mémos ne sont disponibles que lors de l'envoi vers des adresses blindées (z)",
|
||||
"merge_addr_created": "Adresse blindée créée.",
|
||||
"merge_advanced": "Avancé",
|
||||
"merge_back": "Retour",
|
||||
"merge_batch_fmt": "Fusionne jusqu'à %d entrées par passage — répétez pour terminer le reste.",
|
||||
"merge_confirm_btn": "Confirmer",
|
||||
"merge_confirm_fmt": "Consolider ~%s DRGX depuis %d entrée(s) vers %s ?",
|
||||
"merge_create_zaddr": "Créer une adresse blindée",
|
||||
"merge_creating": "Création de l'adresse…",
|
||||
"merge_description": "Fusionnez plusieurs UTXOs en une seule adresse blindée. Cela peut réduire la taille du portefeuille et améliorer la confidentialité.",
|
||||
"merge_fee_hint": "Frais réseau pour cette transaction.",
|
||||
"merge_funds": "Fusionner les fonds",
|
||||
"merge_max_inputs": "Nombre max d'entrées par lot",
|
||||
"merge_no_spendable": "Aucune entrée dépensable à consolider pour l'instant.",
|
||||
"merge_progress": "Consolidation en cours… cela peut prendre quelques minutes. Vous pouvez fermer cette fenêtre.",
|
||||
"merge_scope_fmt": "%d entrées transparentes + %d blindées · ~%s DRGX dépensables",
|
||||
"merge_scope_loading": "Vérification de vos entrées…",
|
||||
"merge_send_failed": "Échec de la fusion : ",
|
||||
"merge_source": "Consolider",
|
||||
"merge_src_both": "Les deux",
|
||||
"merge_src_shielded": "Blindées",
|
||||
"merge_src_transparent": "Transparentes",
|
||||
"merge_started": "Opération de fusion démarrée",
|
||||
"merge_title": "Fusionner vers une adresse",
|
||||
"mig_adopt": "Faire de ceci mon portefeuille",
|
||||
@@ -875,6 +1260,7 @@
|
||||
"mining_difficulty_copied": "Difficulté copiée",
|
||||
"mining_est_block": "Bloc est.",
|
||||
"mining_est_daily": "Est. quotidien",
|
||||
"mining_est_daily_pool_sub": "équivalent solo approximatif, avant les frais du pool",
|
||||
"mining_filter_all": "Tout",
|
||||
"mining_filter_tip_all": "Afficher tous les gains",
|
||||
"mining_filter_tip_pool": "Afficher uniquement les gains du pool",
|
||||
@@ -903,10 +1289,12 @@
|
||||
"mining_open_in_explorer": "Ouvrir dans l'explorateur",
|
||||
"mining_payout_address": "Adresse de paiement",
|
||||
"mining_payout_foreign": "⚠ Cette adresse de paiement ne fait pas partie de votre portefeuille actuel — les récompenses minées iraient vers un autre portefeuille. Mettez-la à jour si vous avez changé de portefeuille.",
|
||||
"mining_payout_invalid": "Adresse DragonX invalide — corrigez-la avant de démarrer, sinon les récompenses de minage sont perdues.",
|
||||
"mining_payout_tooltip": "Adresse pour recevoir les récompenses de minage",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_fee": "Frais",
|
||||
"mining_pool_hashrate": "Hashrate du pool",
|
||||
"mining_pool_needs_payout_tooltip": "Entrez d'abord une adresse de paiement (générez une adresse Z)",
|
||||
"mining_pool_url": "URL du pool",
|
||||
"mining_pools_header": "POOLS",
|
||||
"mining_recent_blocks": "BLOCS RÉCENTS",
|
||||
@@ -936,6 +1324,9 @@
|
||||
"mining_syncing_tooltip": "La blockchain se synchronise...",
|
||||
"mining_tag": " · Minage",
|
||||
"mining_threads": "Threads de minage",
|
||||
"mining_threads_input_tooltip": "Saisissez un nombre exact de threads (Entrée pour appliquer)",
|
||||
"mining_threads_minus_tooltip": "Moins de threads",
|
||||
"mining_threads_plus_tooltip": "Plus de threads",
|
||||
"mining_to_save": "pour enregistrer",
|
||||
"mining_today": "Aujourd'hui",
|
||||
"mining_uptime": "Temps de fonctionnement",
|
||||
@@ -1028,6 +1419,13 @@
|
||||
"peers_upper": "PAIRS",
|
||||
"peers_version": "Version",
|
||||
"pending": "En attente",
|
||||
"picker_dat_count": "%d fichier(s) de portefeuille dans ce dossier",
|
||||
"picker_dat_none": "Aucun fichier de portefeuille directement dans ce dossier",
|
||||
"picker_empty": "Ce dossier ne contient aucun sous-dossier ni fichier de portefeuille.",
|
||||
"picker_home": "Dossier personnel",
|
||||
"picker_select": "Analyser ce dossier",
|
||||
"picker_title": "Sélectionnez un dossier à analyser",
|
||||
"picker_up": "Remonter d'un niveau",
|
||||
"pin_change_desc": "Modifiez votre code PIN de déverrouillage. Vous avez besoin de votre code PIN actuel et d'un nouveau code PIN.",
|
||||
"pin_confirm_new_label": "Confirmer le nouveau code PIN :",
|
||||
"pin_current_label": "Code PIN actuel :",
|
||||
@@ -1105,6 +1503,8 @@
|
||||
"qr_failed": "Échec de la génération du code QR",
|
||||
"qr_title": "Code QR",
|
||||
"qr_unavailable": "QR indisponible",
|
||||
"quick_receive": "Réception rapide",
|
||||
"quick_send": "Envoi rapide",
|
||||
"ram_daemon_gb": "Daemon : %.1f Go (%s)",
|
||||
"ram_daemon_mb": "Daemon : %.0f Mo (%s)",
|
||||
"ram_system_gb": "Système : %.1f / %.0f Go",
|
||||
@@ -1154,6 +1554,7 @@
|
||||
"rpc_connection": "Connexion RPC...",
|
||||
"rpc_host": "Hôte RPC",
|
||||
"rpc_pass": "Mot de passe",
|
||||
"rpc_plaintext_remote_warning": "Le RPC distant utilise du HTTP en clair. Ajoutez rpctls=1 à DRAGONX.conf si votre démon prend en charge TLS.",
|
||||
"rpc_port": "Port",
|
||||
"rpc_user": "Nom d'utilisateur",
|
||||
"save": "Enregistrer",
|
||||
@@ -1161,6 +1562,7 @@
|
||||
"save_z_transactions": "Enregistrer les Z-tx dans la liste",
|
||||
"sb_auth_failed": "Authentification échouée — vérifiez rpcuser/rpcpassword",
|
||||
"sb_block": "Bloc : %d",
|
||||
"sb_block_db_unreadable": "Base de données des blocs illisible — reconstruction requise",
|
||||
"sb_building_witnesses": "Configuration des témoins",
|
||||
"sb_building_witnesses_pct": "Reconstruction des témoins %.0f%%",
|
||||
"sb_connecting_daemon": "Connexion à dragonxd...",
|
||||
@@ -1176,6 +1578,7 @@
|
||||
"sb_dragonxd_stopped": "dragonxd arrêté",
|
||||
"sb_dragonxd_stopping": "Arrêt de dragonxd...",
|
||||
"sb_extracting_sapling": "Extraction des paramètres Sapling...",
|
||||
"sb_finishing_repair": "Finalisation de la réparation du portefeuille — nouvelle analyse en cours…",
|
||||
"sb_importing_keys": "Importation des clés",
|
||||
"sb_loading_config": "Chargement de la configuration...",
|
||||
"sb_mining_hs": "%.1f H/s",
|
||||
@@ -1197,14 +1600,55 @@
|
||||
"sb_waiting_config": "En attente de la configuration du daemon...",
|
||||
"sb_waiting_daemon": "En attente de dragonxd...",
|
||||
"sb_waiting_daemon_err": "En attente de dragonxd — %s",
|
||||
"sb_wallet_needs_recovery": "Réparation du portefeuille disponible",
|
||||
"sb_warming_up": "Démarrage...",
|
||||
"sb_witness_cache": "Reconstruction des témoins",
|
||||
"scale_effects": "ÉCHELLE ET EFFETS",
|
||||
"screenshot_open_dir": "Ouvrir l'emplacement",
|
||||
"screenshot_sweep": "Lancer la capture d'écran",
|
||||
"screenshot_sweep_desc": "Parcourt chaque thème sur chaque onglet et enregistre une capture d'écran de chacun dans des sous-dossiers par onglet, sous le dossier screenshots du répertoire de configuration (en écrasant le balayage précédent). Dure quelques secondes.",
|
||||
"screenshot_sweep_full": "Balayage complet de l'interface",
|
||||
"search_icons": "Rechercher des icônes...",
|
||||
"search_placeholder": "Rechercher...",
|
||||
"sec_changing_passphrase": "Modification de la phrase secrète...",
|
||||
"sec_changing_pin": "Modification du PIN...",
|
||||
"sec_couldnt_lock_wallet": "Impossible de verrouiller le portefeuille — il reste déverrouillé. Vérifiez la connexion au démon.",
|
||||
"sec_encrypted_backup_suffix": "\nSauvegarde chiffrée : wallet.dat.encrypted.bak",
|
||||
"sec_encrypting_wallet": "Chiffrement du portefeuille...",
|
||||
"sec_encryption_did_not_complete": "Le chiffrement du portefeuille ne s'est pas terminé — votre portefeuille N'est PAS chiffré. Ouvrez les Paramètres pour terminer le chiffrement.",
|
||||
"sec_encryption_failed_prefix": "Échec du chiffrement : ",
|
||||
"sec_failed_prefix": "Échec : ",
|
||||
"sec_failed_to_create_vault": "Échec de la création du coffre",
|
||||
"sec_importing_keys_rescanning": "Importation des clés et nouvelle analyse de la blockchain — le portefeuille reste utilisable pendant l'opération",
|
||||
"sec_incorrect_current_pin": "PIN actuel incorrect",
|
||||
"sec_incorrect_passphrase_decrypt": "Phrase secrète incorrecte",
|
||||
"sec_incorrect_passphrase_pin_setup": "Phrase secrète incorrecte",
|
||||
"sec_incorrect_pin_remove": "PIN incorrect",
|
||||
"sec_internal_error_change_pin": "Erreur interne",
|
||||
"sec_internal_error_remove_pin": "Erreur interne",
|
||||
"sec_mode_passphrase": " Phrase secrète",
|
||||
"sec_not_connected_to_daemon": "Non connecté au démon",
|
||||
"sec_not_connected_to_daemon_pin": "Non connecté au démon",
|
||||
"sec_passphrase_changed_successfully": "Phrase secrète modifiée avec succès",
|
||||
"sec_pin_changed_successfully": "PIN modifié avec succès",
|
||||
"sec_pin_removed": "PIN supprimé",
|
||||
"sec_pin_set_successfully": "PIN défini avec succès",
|
||||
"sec_restart_daemon_for_encryption": "Veuillez redémarrer votre démon pour que le chiffrement prenne effet.",
|
||||
"sec_too_many_attempts_wait": "Trop de tentatives. Patientez %.0f secondes...",
|
||||
"sec_total_elapsed_fmt": "Temps total écoulé : %dm %02ds",
|
||||
"sec_unlock_button": "Déverrouiller",
|
||||
"sec_unlock_failed_prefix": "Échec du déverrouillage : ",
|
||||
"sec_unlocking_fmt": "Déverrouillage%s",
|
||||
"sec_use_passphrase_instead": "Utiliser une phrase secrète",
|
||||
"sec_use_pin_instead": "Utiliser un PIN",
|
||||
"sec_verifying_passphrase": "Vérification de la phrase secrète...",
|
||||
"sec_verifying_pin": "Vérification du PIN...",
|
||||
"sec_wallet_decrypted_all_keys_imported": "Portefeuille déchiffré avec succès ! Toutes les clés ont été importées.",
|
||||
"sec_wallet_encrypted_and_pin_set": "Portefeuille chiffré et PIN défini",
|
||||
"sec_wallet_encrypted_but_pin_vault_failed": "Portefeuille chiffré mais échec du coffre PIN",
|
||||
"sec_wallet_encrypted_restarting_daemon": "Portefeuille chiffré. Redémarrage du démon...",
|
||||
"sec_wallet_encrypted_successfully": "Portefeuille chiffré avec succès",
|
||||
"sec_wallet_locked_title": "Portefeuille verrouillé",
|
||||
"security": "SÉCURITÉ",
|
||||
"seed_backup_button": "Phrase de récupération",
|
||||
"seed_backup_close": "Fermer",
|
||||
@@ -1228,6 +1672,7 @@
|
||||
"send_amount": "Montant",
|
||||
"send_amount_details": "DÉTAILS DU MONTANT",
|
||||
"send_amount_upper": "MONTANT",
|
||||
"send_available_note": "disponible",
|
||||
"send_cannot_send_now": "Envoi impossible pour le moment — vérifiez la connexion, la synchronisation et le solde disponible.",
|
||||
"send_clear_fields": "Effacer tous les champs du formulaire ?",
|
||||
"send_contacts_button": "Choisir parmi les contacts",
|
||||
@@ -1263,6 +1708,7 @@
|
||||
"send_tooltip_not_connected": "Non connecté au daemon",
|
||||
"send_tooltip_select_source": "Sélectionnez d'abord une adresse source",
|
||||
"send_tooltip_syncing": "Attendez la synchronisation de la blockchain",
|
||||
"send_tooltip_view_only": "Adresse en lecture seule — pas de clé de dépense, envoi impossible",
|
||||
"send_total": "Total",
|
||||
"send_transaction": "Envoyer la transaction",
|
||||
"send_tx_failed": "Transaction échouée",
|
||||
@@ -1282,16 +1728,16 @@
|
||||
"sent_filter": "Envoyé",
|
||||
"sent_type": "Envoyé",
|
||||
"sent_upper": "ENVOYÉ",
|
||||
"set_label": "Définir le libellé...",
|
||||
"set_label": "Définir le libellé",
|
||||
"settings": "Paramètres",
|
||||
"settings_about_text": "Un portefeuille de cryptomonnaie blindé pour DragonX (DRGX), construit avec Dear ImGui pour une expérience légère et portable.",
|
||||
"settings_acrylic_level": "Niveau acrylique :",
|
||||
"settings_address_book": "Carnet d'adresses...",
|
||||
"settings_address_book": "Carnet d'adresses…",
|
||||
"settings_auto_detected": "Détecté automatiquement depuis DRAGONX.conf",
|
||||
"settings_auto_lock": "VERROUILLAGE AUTO",
|
||||
"settings_auto_shield_desc": "Déplacer automatiquement les fonds transparents vers des adresses blindées",
|
||||
"settings_auto_shield_funds": "Blindage automatique des fonds transparents",
|
||||
"settings_backup": "Sauvegarde...",
|
||||
"settings_backup": "Sauvegarde…",
|
||||
"settings_block_explorer_urls": "URLs de l'explorateur de blocs",
|
||||
"settings_builtin": "Intégré",
|
||||
"settings_change_passphrase": "Changer la phrase secrète",
|
||||
@@ -1305,7 +1751,7 @@
|
||||
"settings_copy_diagnostics": "Copier les diagnostics",
|
||||
"settings_copyright": "Copyright 2024-2026 Développeurs DragonX | Licence GPLv3",
|
||||
"settings_custom": "Personnalisé",
|
||||
"settings_data_dir": "Rép. de données :",
|
||||
"settings_data_dir": "Rép. de données ",
|
||||
"settings_debug_changed": "Catégories de débogage modifiées — redémarrez le daemon pour appliquer",
|
||||
"settings_debug_restart_note": "Les modifications prennent effet après le redémarrage du daemon.",
|
||||
"settings_debug_select": "Sélectionnez les catégories pour activer la journalisation de débogage du daemon (flags -debug=).",
|
||||
@@ -1313,19 +1759,20 @@
|
||||
"settings_encrypt_first_pin": "Chiffrez d'abord le portefeuille pour activer le PIN",
|
||||
"settings_encrypt_wallet": "Chiffrer le portefeuille",
|
||||
"settings_explorer_hint": "Les URLs doivent inclure une barre oblique finale. Le txid/adresse sera ajouté.",
|
||||
"settings_export_all": "Tout exporter...",
|
||||
"settings_export_csv": "Exporter CSV...",
|
||||
"settings_export_key": "Exporter la clé...",
|
||||
"settings_export_all": "Tout exporter…",
|
||||
"settings_export_csv": "Exporter CSV…",
|
||||
"settings_export_key": "Exporter la clé…",
|
||||
"settings_gradient_bg": "Fond dégradé",
|
||||
"settings_gradient_desc": "Remplacer les arrière-plans texturés par des dégradés lisses",
|
||||
"settings_idle_after": "après",
|
||||
"settings_import_key": "Importer une clé privée...",
|
||||
"settings_import_viewkey": "Importer la clé de visualisation...",
|
||||
"settings_import_key": "Importer une clé privée…",
|
||||
"settings_import_viewkey": "Importer la clé de visualisation…",
|
||||
"settings_language_note": "Remarque : Certains textes nécessitent un redémarrage pour se mettre à jour",
|
||||
"settings_lock_now": "Verrouiller maintenant",
|
||||
"settings_locked": "Verrouillé",
|
||||
"settings_merge_to_address": "Fusionner vers l'adresse...",
|
||||
"settings_merge_to_address": "Fusionner vers l'adresse…",
|
||||
"settings_noise_opacity": "Opacité du bruit :",
|
||||
"settings_not_connected": "Non connecté au démon",
|
||||
"settings_not_encrypted": "Non chiffré",
|
||||
"settings_not_found": "Non trouvé",
|
||||
"settings_open_app_dir": "Ouvrir le dossier de l'application",
|
||||
@@ -1336,29 +1783,36 @@
|
||||
"settings_privacy": "Confidentialité",
|
||||
"settings_quick_unlock_pin": "PIN de déverrouillage rapide",
|
||||
"settings_reduce_transparency": "Réduire la transparence",
|
||||
"settings_reloaded": "Paramètres rechargés depuis le disque",
|
||||
"settings_remove_encryption": "Supprimer le chiffrement",
|
||||
"settings_remove_pin": "Supprimer le PIN",
|
||||
"settings_request_payment": "Demander un paiement...",
|
||||
"settings_request_payment": "Demander un paiement…",
|
||||
"settings_rescan_desc": "Rescanner la blockchain pour les transactions manquantes",
|
||||
"settings_restart_daemon": "Redémarrer le daemon",
|
||||
"settings_rpc_connection": "Connexion RPC",
|
||||
"settings_rpc_error_prefix": "Erreur RPC : ",
|
||||
"settings_rpc_note": "Remarque : Les paramètres de connexion sont généralement détectés automatiquement depuis DRAGONX.conf",
|
||||
"settings_rpc_ok": "Connexion RPC OK",
|
||||
"settings_save_shielded_desc": "Stocke les transactions z-addr dans un fichier local pour consultation",
|
||||
"settings_save_shielded_local": "Enregistrer l'historique des transactions blindées localement",
|
||||
"settings_saved": "Paramètres enregistrés",
|
||||
"settings_set_pin": "Définir le PIN",
|
||||
"settings_shield_mining": "Blindage minage...",
|
||||
"settings_shield_mining": "Blindage minage…",
|
||||
"settings_solid_colors_desc": "Utiliser des couleurs unies au lieu des effets de flou (accessibilité)",
|
||||
"settings_theme_refreshed": "Liste des thèmes actualisée",
|
||||
"settings_tor_desc": "Acheminer toutes les connexions via Tor pour une confidentialité renforcée",
|
||||
"settings_unlocked": "Déverrouillé",
|
||||
"settings_use_tor_network": "Utiliser Tor pour les connexions réseau",
|
||||
"settings_validate_address": "Valider l'adresse...",
|
||||
"settings_validate_address": "Valider l'adresse…",
|
||||
"settings_visual_effects": "Effets visuels",
|
||||
"settings_wallet_file_size": "Taille du fichier portefeuille : %s",
|
||||
"settings_wallet_info": "Informations du portefeuille",
|
||||
"settings_wallet_location": "Emplacement du portefeuille : %s",
|
||||
"settings_wallet_maintenance": "Maintenance du portefeuille",
|
||||
"settings_wallet_not_found": "Fichier portefeuille introuvable",
|
||||
"settings_wallet_size_label": "Taille du portefeuille :",
|
||||
"settings_wallet_size_label": "Taille du portefeuille ",
|
||||
"settings_ztx_cleared": "Historique des transactions Z effacé",
|
||||
"settings_ztx_not_found": "Aucun fichier d'historique trouvé",
|
||||
"setup_wizard": "Assistant de configuration",
|
||||
"share": "Partager",
|
||||
"shield_check_status": "Vérifier le statut",
|
||||
@@ -1406,17 +1860,34 @@
|
||||
"status": "Statut",
|
||||
"stop_external": "Arrêter le daemon externe",
|
||||
"stop_mining": "Arrêter le minage",
|
||||
"stratum_allowip_hint": "Autoriser les mineurs depuis une IP ou un CIDR (vide = cet ordinateur uniquement)",
|
||||
"stratum_expose_warn": "Ouvre un port de minage vers le réseau que vous autorisez — à utiliser uniquement sur un réseau local de confiance.",
|
||||
"stratum_host": "Héberger un pool de minage (stratum)",
|
||||
"stratum_host_hint": "Les mineurs se connectent à cet ordinateur sur le port 22769 (port RPC + 1000) avec un mineur stratum RandomX. Redémarrez le démon pour appliquer.",
|
||||
"stratum_host_section": "HÉBERGEMENT DE POOL DE MINAGE",
|
||||
"submitting_transaction": "Soumission de la transaction...",
|
||||
"success": "Succès",
|
||||
"summary": "Résumé",
|
||||
"sweep_button": "Balayer",
|
||||
"sweep_caveat": "Importe la clé pour signer une transaction qui déplace tous ses fonds vers votre adresse. La clé reste dans votre portefeuille avec un solde vide.",
|
||||
"sweep_current_theme_only": "Thème actuel uniquement",
|
||||
"sweep_dest_label": "Envoyer les fonds balayés vers",
|
||||
"sweep_dest_new": "Nouvelle adresse blindée (recommandé)",
|
||||
"sweep_done": "Terminé — fonds balayés vers votre adresse.",
|
||||
"sweep_to": "Balayé vers :",
|
||||
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
|
||||
"sweep_tx": "Transaction :",
|
||||
"swin_connection_failed": "Échec de la connexion : ",
|
||||
"swin_connection_successful": "Connexion réussie !\nVersion de dragonxd : ",
|
||||
"swin_invalid_suffix": " (invalide)",
|
||||
"swin_no_history_file_found": "Aucun fichier d'historique trouvé",
|
||||
"swin_rescan_failed": "Échec de la nouvelle analyse : ",
|
||||
"swin_rescan_started_from_block": "Nouvelle analyse démarrée à partir du bloc ",
|
||||
"swin_rescan_to": " à ",
|
||||
"swin_rpc_client_not_initialized": "Client RPC non initialisé",
|
||||
"swin_settings_saved": "Paramètres enregistrés",
|
||||
"swin_theme_list_refreshed": "Liste des thèmes actualisée",
|
||||
"swin_ztx_history_cleared": "Historique des transactions Z effacé",
|
||||
"switch_corrupt_body": "Ce portefeuille semble corrompu — le nœud n'a pas pu l'ouvrir. Restaurez-le depuis une sauvegarde, recréez-le ou tentez de le réparer.",
|
||||
"switch_corrupt_repair": "Tenter une réparation (salvage)",
|
||||
"switch_progress_background": "Continuer en arrière-plan",
|
||||
@@ -1441,6 +1912,7 @@
|
||||
"theme": "Thème",
|
||||
"theme_effects": "Effets de thème",
|
||||
"theme_language": "THÈME & LANGUE",
|
||||
"tile_click_to_open": "Cliquer pour ouvrir",
|
||||
"time_days_ago": "il y a %d jours",
|
||||
"time_hours_ago": "il y a %d heures",
|
||||
"time_minutes_ago": "il y a %d minutes",
|
||||
@@ -1455,7 +1927,9 @@
|
||||
"to_upper": "À",
|
||||
"tools": "OUTILS",
|
||||
"tools_actions": "Outils & Actions...",
|
||||
"tools_actions_hdr": "OUTILS ET ACTIONS",
|
||||
"total": "Total",
|
||||
"total_balance_label": "Solde total",
|
||||
"transaction_id": "ID DE TRANSACTION",
|
||||
"transaction_sent": "Transaction envoyée avec succès",
|
||||
"transaction_sent_msg": "Transaction envoyée !",
|
||||
@@ -1477,7 +1951,7 @@
|
||||
"tt_auto_shield": "Déplacer automatiquement le solde transparent vers des adresses blindées pour la confidentialité",
|
||||
"tt_backup": "Créer une sauvegarde de votre wallet.dat",
|
||||
"tt_block_explorer": "Ouvrir l'explorateur de blocs DragonX dans votre navigateur",
|
||||
"tt_blur": "Quantité de flou (0%% = désactivé, 100%% = maximum)",
|
||||
"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)",
|
||||
@@ -1540,7 +2014,7 @@
|
||||
"tt_low_spec": "Désactiver tous les effets visuels lourds\\nRaccourci : Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolider plusieurs UTXOs vers une adresse",
|
||||
"tt_mine_idle": "Démarrer le minage automatiquement quand le\\nsystème est inactif (aucune entrée clavier/souris)",
|
||||
"tt_noise": "Intensité de texture grainée (0%% = désactivé, 100%% = maximum)",
|
||||
"tt_noise": "Intensité de texture grainée (0% = désactivé, 100% = maximum)",
|
||||
"tt_open_app_dir": "Ouvrir le dossier ObsidianDragon (paramètres, thèmes, journaux) dans le gestionnaire de fichiers",
|
||||
"tt_open_data_dir": "Ouvrir le dossier contenant les données de votre portefeuille et de la blockchain dans le gestionnaire de fichiers",
|
||||
"tt_open_dir": "Cliquer pour ouvrir dans l'explorateur de fichiers",
|
||||
@@ -1574,14 +2048,17 @@
|
||||
"tt_simple_bg": "Utiliser un dégradé simple pour l'arrière-plan\\nRaccourci : Ctrl+Up",
|
||||
"tt_simple_bg_alt": "Utiliser une version dégradée de l'image d'arrière-plan du thème\\nRaccourci : Ctrl+Up",
|
||||
"tt_stop_external": "S'applique lors de la connexion à un daemon\\ndémarré en dehors de ce portefeuille",
|
||||
"tt_stratum_host": "Exécute un serveur de pool stratum RandomX sur ce nœud afin que d'autres mineurs RandomX puissent pointer vers cet ordinateur. Nécessite un nœud v1.3.0+ et un redémarrage du démon pour appliquer.",
|
||||
"tt_sweep_current_theme_only": "Balayer uniquement le thème actif au lieu de parcourir tous les thèmes",
|
||||
"tt_test_conn": "Vérifier la connexion RPC au daemon",
|
||||
"tt_theme_effects": "Scintillement, lueur, cycle de teinte par thème",
|
||||
"tt_theme_hotkey": "Raccourci : Ctrl+Gauche/Droite pour changer de thème",
|
||||
"tt_tor": "Acheminer les connexions du daemon via le réseau Tor pour l'anonymat",
|
||||
"tt_tx_url": "URL de base pour consulter les transactions dans un explorateur de blocs",
|
||||
"tt_ui_opacity": "Opacité des cartes et de la barre latérale (100%% = entièrement opaque, plus bas = plus transparent)",
|
||||
"tt_ui_opacity": "Opacité des cartes et de la barre latérale (100% = entièrement opaque, plus bas = plus transparent)",
|
||||
"tt_validate": "Vérifier si une adresse DragonX est valide",
|
||||
"tt_verbose": "Journaliser les diagnostics de connexion détaillés,\\nl'état du daemon et les informations de propriétaire de port\\ndans l'onglet Console",
|
||||
"tt_wallet_size_warn": "Les portefeuilles blindés grandissent avec les données de témoin de chaque note — fusionner de nombreuses notes en une seule adresse réduit cette taille. Sauvegardez d'abord.",
|
||||
"tt_wallets_button": "Répertoriez vos fichiers de portefeuille et passez de l'un à l'autre",
|
||||
"tt_website": "Ouvrir le site web DragonX",
|
||||
"tt_window_opacity": "Opacité de l'arrière-plan (plus bas = bureau visible à travers la fenêtre)",
|
||||
@@ -1632,6 +2109,7 @@
|
||||
"validate_not_mine": "N'appartient pas à ce portefeuille",
|
||||
"validate_ownership": "Propriété :",
|
||||
"validate_results": "Résultats :",
|
||||
"validate_results_placeholder": "Les résultats apparaîtront ici",
|
||||
"validate_shielded_type": "Blindée (z-adresse)",
|
||||
"validate_status": "Statut :",
|
||||
"validate_title": "Valider l'adresse",
|
||||
@@ -1647,8 +2125,68 @@
|
||||
"view_on_explorer": "Voir dans l'explorateur",
|
||||
"waiting_for_daemon": "En attente de la connexion au daemon...",
|
||||
"wallet": "PORTEFEUILLE",
|
||||
"wallet_degraded_notify": "Votre portefeuille s'est ouvert en mode à fonctionnalités réduites : les fonds existants sont en sécurité et dépensables, mais la création de nouvelles adresses et le blindage sont désactivés. Sauvegardez votre phrase de récupération et restaurez-la pour réparer entièrement le portefeuille.",
|
||||
"wallet_diagnostics_hdr": "DIAGNOSTICS",
|
||||
"wallet_empty": "Votre portefeuille est vide",
|
||||
"wallet_empty_hint": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
|
||||
"wallet_options_hdr": "OPTIONS",
|
||||
"wallet_rebuild_failed": "La reconstruction n'a pas produit de portefeuille valide, donc rien n'a été modifié. Votre portefeuille est intact.",
|
||||
"wallet_rebuild_install_failed": "Impossible d'installer le portefeuille reconstruit ; votre portefeuille actuel a été laissé en place.",
|
||||
"wallet_rebuild_no_helper": "L'assistant de reconstruction du portefeuille n'est pas disponible dans cette version. Utilisez Restaurer, ou reconstruisez manuellement.",
|
||||
"wallet_rebuild_no_source": "Impossible de trouver un portefeuille lisible à reconstruire. Rien n'a été modifié.",
|
||||
"wallet_rebuild_ok": "Base de données du portefeuille reconstruite — le nœud la charge et effectue une nouvelle analyse pour retrouver votre solde.",
|
||||
"wallet_rebuild_started": "Réparation de votre fichier de portefeuille en cours — veuillez ne pas fermer cette fenêtre.",
|
||||
"wallet_recovered_body": "Rien n'a été supprimé. Votre portefeuille d'origine est toujours enregistré sur votre ordinateur sous forme de fichier de sauvegarde daté, et chaque option ci-dessous se contente de copier ou de renommer des fichiers — elle n'en efface jamais un. Vos clés ne sont jamais régénérées, seulement relues.\n\nLa copie réparée actuellement chargée peut ne pas inclure quelques transactions récentes ; votre solde peut donc paraître légèrement bas jusqu'à ce que vous terminiez ci-dessous et qu'il se re-synchronise.",
|
||||
"wallet_recovered_dismiss": "Pas maintenant — conserver la copie réparée",
|
||||
"wallet_recovered_dismiss_sub": "Aucun fichier ne change. Vous pouvez rouvrir ceci à tout moment depuis la barre d'état ; votre original reste sauvegardé dans tous les cas.",
|
||||
"wallet_recovered_notify": "Votre fichier de portefeuille avait besoin d'une réparation — votre original a été sauvegardé en toute sécurité. Ouvrez l'application pour examiner vos options.",
|
||||
"wallet_recovered_open_folder": "Montrez-moi les fichiers",
|
||||
"wallet_recovered_open_folder_sub": "Ouvre le dossier de données du portefeuille pour que vous puissiez inspecter vous-même les fichiers de sauvegarde — rien n'est modifié.",
|
||||
"wallet_recovered_rebuild": "Réparer automatiquement (recommandé)",
|
||||
"wallet_recovered_rebuild_sub": "Relit chaque enregistrement récupérable de votre fichier de portefeuille le plus complet et en écrit un nouveau propre, puis redémarre. L'option la plus complète — votre fichier actuel est conservé comme sauvegarde datée dans tous les cas.",
|
||||
"wallet_recovered_restore": "Restaurer le fichier original à la place",
|
||||
"wallet_recovered_restore_sub": "Remet en place votre plus grande sauvegarde intacte, telle quelle, puis relance l'analyse — un peu plus rapide, mais seulement aussi complet que ce fichier l'était. Votre fichier actuel est conservé comme sauvegarde datée dans tous les cas.",
|
||||
"wallet_recovered_safety": "Vos DRGX sont en sécurité.",
|
||||
"wallet_recovered_title": "Votre fichier de portefeuille a besoin d'une réparation rapide",
|
||||
"wallet_recovered_warn": "Au démarrage de l'application, elle a détecté que votre fichier de portefeuille ne passait pas son contrôle de cohérence — cela arrive généralement après une mise à jour de l'application ou un arrêt brutal. L'application a déjà protégé vos données : elle a mis de côté l'ancien fichier et chargé une copie réparée pour que vous ne soyez pas bloqué.",
|
||||
"wallet_recovery_decide_later": "Décider plus tard",
|
||||
"wallet_recovery_decide_later_tip": "Ferme ceci et conserve la copie actuellement chargée. Rien n'est modifié, et vous pouvez réparer à tout moment — la barre d'état garde un lien « Réparation du portefeuille disponible ».",
|
||||
"wallet_recovery_details_label": "Afficher les détails techniques",
|
||||
"wallet_recovery_done": "Terminé",
|
||||
"wallet_recovery_failure_body": "La réparation s'est exécutée, mais son résultat n'a pas passé la vérification (il était illisible ou ne contenait aucune adresse), il a donc été automatiquement rejeté avant de remplacer quoi que ce soit. Votre portefeuille est exactement comme avant que vous cliquiez — rien n'a changé sur le disque.",
|
||||
"wallet_recovery_failure_title": "La réparation n'a pas abouti",
|
||||
"wallet_recovery_files_detail": "Votre portefeuille original est toujours là — l'application l'a renommé en sauvegarde datée (wallet.<numbers>.bak) dans votre dossier de données et n'a rien supprimé. « Réparer » lit chaque enregistrement de votre fichier le plus complet vers un tout nouveau fichier propre. « Restaurer » recopie votre plus grande sauvegarde exactement telle quelle. La copie actuellement chargée est elle aussi conservée comme sauvegarde.",
|
||||
"wallet_recovery_files_label": "Qu'advient-il de mes fichiers ?",
|
||||
"wallet_recovery_notnow_short": "Pas maintenant",
|
||||
"wallet_recovery_other_options": "Autres options",
|
||||
"wallet_recovery_rebuild_card": "Réparer automatiquement",
|
||||
"wallet_recovery_rebuild_card_desc": "Lit chaque enregistrement récupérable vers un fichier propre, puis redémarre. L'option la plus complète.",
|
||||
"wallet_recovery_recommended": "RECOMMANDÉ",
|
||||
"wallet_recovery_repair_go": "Réparer",
|
||||
"wallet_recovery_rescan_body": "Réanalyse de la blockchain pour reconstruire votre solde et l'historique des transactions. Cela peut prendre plusieurs minutes — vous n'avez rien à faire, et merci de ne pas redémarrer le nœud pendant l'opération.",
|
||||
"wallet_recovery_rescan_elapsed": "En cours depuis %s",
|
||||
"wallet_recovery_rescan_size": "Le portefeuille reconstruit fait maintenant %s et continue de se remplir",
|
||||
"wallet_recovery_rescan_slow": "Cela prend plus de temps que d'habitude, ce qui est normal pour un grand portefeuille. Vous pouvez le laisser tourner en toute sécurité — vous pouvez suivre la progression sous Avancé ▸ Console.",
|
||||
"wallet_recovery_rescan_title": "Finalisation de la réparation de votre portefeuille",
|
||||
"wallet_recovery_restore_card": "Restaurer l'original",
|
||||
"wallet_recovery_restore_card_desc": "Remet en place votre plus grande sauvegarde intacte, telle quelle — seulement aussi complète que ce fichier l'était.",
|
||||
"wallet_recovery_restore_go": "Restaurer",
|
||||
"wallet_recovery_success_body": "L'application a chargé votre portefeuille réparé et réanalyse la blockchain pour totaliser votre solde — cela peut prendre quelques minutes. Elle lit chaque enregistrement possible, mais sur de rares fichiers endommagés, elle peut récupérer légèrement moins — ou parfois plus — d'adresses qu'avant. Une fois la réanalyse terminée, vérifiez que votre solde et votre historique semblent corrects.",
|
||||
"wallet_recovery_success_restore": "Votre plus grande sauvegarde est remise en place et se charge maintenant, et l'application réanalyse la blockchain. Elle est restaurée telle quelle, donc aussi complète que ce fichier l'était — vérifiez votre solde une fois la réanalyse terminée.",
|
||||
"wallet_recovery_try_other": "Essayer l'autre option",
|
||||
"wallet_recovery_whats_happened": "Que s'est-il passé ?",
|
||||
"wallet_recovery_working_label": "En cours",
|
||||
"wallet_restore_bad_backup": "Le fichier de sauvegarde du portefeuille semble illisible, il N'A DONC PAS été restauré — votre portefeuille actuel n'a pas changé. Restaurez plutôt à partir de votre propre sauvegarde.",
|
||||
"wallet_restore_busy": "Le nœud est occupé à redémarrer — réessayez dans un instant.",
|
||||
"wallet_restore_copy_failed": "Impossible d'installer le portefeuille de sauvegarde ; votre portefeuille actuel a été laissé en place.",
|
||||
"wallet_restore_move_failed": "Impossible de mettre de côté le portefeuille actuel — rien n'a été modifié.",
|
||||
"wallet_restore_no_backup": "Impossible de trouver un fichier wallet.<timestamp>.bak à restaurer. Rien n'a été modifié.",
|
||||
"wallet_restore_no_restart": "Votre portefeuille original a été restauré, mais le nœud n'a pas redémarré — démarrez-le depuis les Paramètres.",
|
||||
"wallet_restore_ok": "Portefeuille original restauré. Le nœud le charge maintenant.",
|
||||
"wallet_restore_started": "Restauration de votre fichier original — veuillez ne pas fermer cette fenêtre.",
|
||||
"wallet_restore_stop_failed": "Le nœud ne s'est pas arrêté à temps, donc rien n'a été modifié. Réessayez.",
|
||||
"wallet_size_consolidate": "Consolider les notes…",
|
||||
"wallet_size_warn": "Ce fichier de portefeuille est volumineux. Consolider vos notes peut freiner sa croissance.",
|
||||
"wallets_active": "Actif",
|
||||
"wallets_add": "Ajouter le dossier",
|
||||
"wallets_add_folder": "Analyser aussi un autre dossier à la recherche de fichiers de portefeuille :",
|
||||
@@ -1691,6 +2229,7 @@
|
||||
"wallets_open_inplace_tt": "Ouvre ce portefeuille à son emplacement — lié au répertoire de données (sans copie)",
|
||||
"wallets_remove_folder": "Ne plus analyser ce dossier",
|
||||
"wallets_reveal": "Afficher le dossier",
|
||||
"wallets_scan_folder": "Analyser un autre dossier pour des portefeuilles…",
|
||||
"wallets_scanned_folders": "Dossiers analysés :",
|
||||
"wallets_sort_addresses": "Adresses",
|
||||
"wallets_sort_asc": "Croissant (plus ancien / moins / plus petit d'abord)",
|
||||
@@ -1760,18 +2299,30 @@
|
||||
"wiz_welcome_sub": "Quelques choix rapides et votre nœud complet est prêt.",
|
||||
"wiz_welcome_title": "Bienvenue dans ObsidianDragon !",
|
||||
"wizard_daemon_start_failed": "Échec du démarrage du daemon — sera réessayé automatiquement",
|
||||
"xmrig_active_tt": "Le mineur actuellement installé (actif)",
|
||||
"xmrig_browse_releases": "Parcourir toutes les versions…",
|
||||
"xmrig_checking": "Recherche du dernier mineur…",
|
||||
"xmrig_current": "Actuel :",
|
||||
"xmrig_downgrade_note": "Installer une autre version du mineur remplace la version actuelle ; arrêtez d'abord le minage, l'effet prendra place au prochain démarrage.",
|
||||
"xmrig_download_install": "Télécharger et installer",
|
||||
"xmrig_downloading": "Téléchargement…",
|
||||
"xmrig_install_this": "Installer cette version",
|
||||
"xmrig_installed": "Installé :",
|
||||
"xmrig_installed_ok": "Mineur installé",
|
||||
"xmrig_installing": "Installation…",
|
||||
"xmrig_latest": "Dernière :",
|
||||
"xmrig_latest_badge": "la plus récente",
|
||||
"xmrig_latest_tt": "Version la plus récente disponible",
|
||||
"xmrig_loading_releases": "Chargement des versions…",
|
||||
"xmrig_no_notes": "Aucune note de version pour cette version.",
|
||||
"xmrig_none": "aucun",
|
||||
"xmrig_prerelease_tt": "Version préliminaire / de test",
|
||||
"xmrig_reinstall": "Réinstaller",
|
||||
"xmrig_released": "Publiée le :",
|
||||
"xmrig_releases": "versions de xmrig",
|
||||
"xmrig_status_current": "Cette version est installée",
|
||||
"xmrig_status_latest": "Dernière version",
|
||||
"xmrig_status_older": "Plus ancienne que votre version installée",
|
||||
"xmrig_stop_mining_first": "Arrêtez le minage avant de mettre à jour le mineur.",
|
||||
"xmrig_unavailable_body": "Aucune version du mineur n'est disponible pour cette plateforme.",
|
||||
"xmrig_unavailable_title": "Mises à jour du mineur indisponibles",
|
||||
|
||||
+569
-15
@@ -19,6 +19,7 @@
|
||||
"about_license_text": "本ソフトウェアはGNU General Public License v3 (GPLv3)の下で公開されています。ライセンス条項に従い、自由に使用、変更、配布できます。",
|
||||
"about_peers_count": "%zu ピア",
|
||||
"about_release": "リリース",
|
||||
"about_source": "ソース",
|
||||
"about_title": "ObsidianDragonについて",
|
||||
"about_version": "バージョン:",
|
||||
"about_website": "ウェブサイト",
|
||||
@@ -60,7 +61,84 @@
|
||||
"amount_label": "金額:",
|
||||
"animate_avatars": "アバターをアニメーション",
|
||||
"appearance": "外観",
|
||||
"appx_back": "戻る",
|
||||
"appx_back_up_seed_phrase_title": "シードフレーズをバックアップ",
|
||||
"appx_birthday_block_height": "誕生日(ブロック高): %llu — これもバックアップしてください。",
|
||||
"appx_blockchain_data_deleted": "ブロックチェーンデータを削除しました(%d件)。ネットワークから再同期するため、デーモンを再起動しています。",
|
||||
"appx_blockchain_maintenance_in_progress": "ブロックチェーンのメンテナンス操作がすでに進行中です。",
|
||||
"appx_blockchain_rescan_complete": "ブロックチェーンの再スキャンが完了しました",
|
||||
"appx_bootstrap_complete_reconciling": "ブートストラップが完了しました。新しいチェーンデータとウォレットを照合しています。",
|
||||
"appx_cancel": "キャンセル",
|
||||
"appx_cleaning_up": "クリーンアップしています...",
|
||||
"appx_confirm_your_backup": "バックアップを確認",
|
||||
"appx_copied_clipboard_autoclears": "コピーしました — クリップボードは45秒後に自動的にクリアされます",
|
||||
"appx_copy": "コピー",
|
||||
"appx_could_not_start_restore": "復元を開始できませんでした",
|
||||
"appx_create_failed_prefix": "作成に失敗しました: ",
|
||||
"appx_creating_your_wallet": "ウォレットを作成しています…",
|
||||
"appx_daemon_error": "デーモンエラー",
|
||||
"appx_daemon_reinstall_in_progress": "デーモンの再インストールがすでに進行中です。",
|
||||
"appx_disconnecting": "切断しています...",
|
||||
"appx_done": "完了",
|
||||
"appx_dragonxd_output": "dragonxd の出力",
|
||||
"appx_encrypting_wallet": "ウォレットを暗号化しています...",
|
||||
"appx_fullnode_lifecycle_unavailable_lite": "フルノードのライフサイクル操作はライトビルドでは利用できません",
|
||||
"appx_installing_bundled_daemon": "同梱デーモンをインストールしています。ノードが停止、更新、再起動されます...",
|
||||
"appx_invalid_payment_uri_prefix": "無効な支払いURI: ",
|
||||
"appx_ive_written_it_down": "書き留めました",
|
||||
"appx_keep_node_running_and_quit": "ノードを起動したまま終了",
|
||||
"appx_last_block_n": "最新ブロック: %d",
|
||||
"appx_last_used_wallet_not_found_prefix": "前回使用したウォレットファイル(",
|
||||
"appx_last_used_wallet_not_found_suffix": ")が見つかりませんでした。代わりにデフォルトのウォレットを開きました。移動した場合は、元に戻してからウォレット一覧で切り替えてください。",
|
||||
"appx_low_spec_mode_disabled": "低スペックモードを無効にしました",
|
||||
"appx_low_spec_mode_enabled": "低スペックモードを有効にしました",
|
||||
"appx_miner_stopped_prefix": "マイナーが停止しました: ",
|
||||
"appx_miner_stopped_unexpectedly": "マイナーが予期せず停止しました。",
|
||||
"appx_n_min_n_sec": "%d分 %d秒",
|
||||
"appx_n_seconds": "%d秒",
|
||||
"appx_no_bundled_daemon_to_install": "このビルドにはインストールできる同梱デーモンがありません",
|
||||
"appx_no_embedded_daemon_to_install": "このビルドにはインストールできる組み込みデーモンがありません",
|
||||
"appx_node_busy_restarting": "ノードが再起動中でビジー状態です。少し待ってから再試行してください。",
|
||||
"appx_node_rebuilding_witness_cache": "ノードがウィットネスキャッシュを再構築しています",
|
||||
"appx_not_next_word": " — 次の単語ではありません",
|
||||
"appx_payment_request_loaded": "支払いリクエストを読み込みました",
|
||||
"appx_pool_miner_connected_and_hashing": "プールマイナーが接続し、ハッシュ計算中です。",
|
||||
"appx_progress_n_of_n": "進捗: %d / %d",
|
||||
"appx_rebuilding_sapling_note_witnesses": "Saplingノートウィットネスを再構築しています…",
|
||||
"appx_rebuilding_witness_cache_blocks_left": "ウィットネスキャッシュを再構築中 %.0f%% — 残り%dブロック",
|
||||
"appx_rebuilding_witness_cache_pct": "ウィットネスキャッシュを再構築中 %.0f%%",
|
||||
"appx_recovery_phrase_word_count": "リカバリーフレーズは24単語である必要があります。現在は%d単語です。",
|
||||
"appx_restarting_daemon_rescan_flag": "-rescanフラグ付きでデーモンを再起動しています...",
|
||||
"appx_restarting_daemon_zapwallettxes": "-zapwallettxes=2(ウォレット修復)付きでデーモンを再起動しています...",
|
||||
"appx_restoring_your_wallet": "ウォレットを復元しています…",
|
||||
"appx_seed_backup_warning": "この24個の単語は、ウォレットを復元する唯一の手段です。順番どおりに書き留め、オフラインで保管し、決して他人に教えないでください。紛失すると、資金は永久に失われます。",
|
||||
"appx_seed_not_backed_up_warning": "シードをバックアップしていません。資金を失う可能性があります。それでもスキップしますか?",
|
||||
"appx_sending_stop_command_to_daemon": "デーモンに停止コマンドを送信しています...",
|
||||
"appx_setting_initial_sapling_witnesses": "初期Saplingウィットネスを設定中 %.0f%%",
|
||||
"appx_shutdown_complete": "シャットダウンが完了しました",
|
||||
"appx_simple_background_disabled": "シンプル背景を無効にしました",
|
||||
"appx_simple_background_enabled": "シンプル背景を有効にしました",
|
||||
"appx_skip": "スキップ",
|
||||
"appx_skip_anyway": "それでもスキップ",
|
||||
"appx_still_status_prefix": "まだ「",
|
||||
"appx_still_status_suffix": "」です — 今強制終了するとチェーンデータが破損する可能性があります。",
|
||||
"appx_stop_anyway_and_quit": "それでも停止して終了",
|
||||
"appx_stopping_daemon_deleting_blockchain": "デーモンを停止し、ブロックチェーンデータを削除しています...",
|
||||
"appx_stopping_node_discards_rebuild": "今ノードを停止すると、進行中の再構築が破棄され、次回ウォレットを開いたときに再度実行されます(数分かかります)。代わりにノードを起動したままにすることもできます。",
|
||||
"appx_stopping_pool_miner": "プールマイナーを停止しています...",
|
||||
"appx_syncing_pct_block_n_of_n": "同期中 %.1f%% — ブロック %d / %d",
|
||||
"appx_tap_words_in_order": "保存したことを確認するため、単語を正しい順番でタップしてください。",
|
||||
"appx_theme_effects_disabled": "テーマ効果を無効にしました",
|
||||
"appx_theme_effects_enabled": "テーマ効果を有効にしました",
|
||||
"appx_theme_prefix": "テーマ: ",
|
||||
"appx_use_settings_restart_daemon_hint": "設定 > デーモンを再起動 から再試行してください",
|
||||
"appx_waiting_for_daemon_to_encrypt_wallet": "デーモンによるウォレットの暗号化を待っています...",
|
||||
"appx_wallet_created_and_backed_up": "ウォレットを作成し、バックアップしました。",
|
||||
"appx_wallet_open_failed_prefix": "ウォレットを開けませんでした: ",
|
||||
"auto_shield": "マイニング自動シールド",
|
||||
"autoshield_by_node": "あなたのノードは、マイニングしたコインベースを自動的にシールドします。",
|
||||
"autoshield_off_backup_seed": "マイニングしたコインベースはまだ自動シールドされていません。ノードが安全に有効化できるよう、シードフレーズをバックアップしてください(設定 → バックアップとデータ)。",
|
||||
"autoshield_off_generic": "現在、あなたのノードはマイニングしたコインベースを自動シールドしていません。",
|
||||
"av_intro": "マイニングソフトウェアは、望ましくない可能性があるものとしてフラグが立てられることがよくあります。プールマイニングを有効にするには、次の手順に従ってください。",
|
||||
"av_open_security": "Windows セキュリティを開く",
|
||||
"av_step1": "ステップ1: 除外の追加",
|
||||
@@ -74,6 +152,9 @@
|
||||
"av_title": "Windows Defender がマイナーをブロックしました",
|
||||
"available": "利用可能",
|
||||
"backup_backing_up": "バックアップ中...",
|
||||
"backup_col_backup": "バックアップ",
|
||||
"backup_col_export": "エクスポート",
|
||||
"backup_col_import": "インポートと復元",
|
||||
"backup_create": "バックアップを作成",
|
||||
"backup_created": "ウォレットのバックアップを作成しました",
|
||||
"backup_data": "バックアップとデータ",
|
||||
@@ -92,8 +173,22 @@
|
||||
"balance": "残高",
|
||||
"balance_history_collecting": "残高履歴 — データを収集中...",
|
||||
"balance_layout": "残高レイアウト",
|
||||
"balance_layout_switched": "レイアウト: %s",
|
||||
"balance_mining_rate": "マイニング中 %s",
|
||||
"balance_shielded_fmt": "シールド: %.8f",
|
||||
"balance_syncing_pct": "同期中 %.1f%%",
|
||||
"balance_transparent_fmt": "透明: %.8f",
|
||||
"baltab_market": "市場",
|
||||
"baltab_market_price_4dp": "市場価格: $%.4f",
|
||||
"baltab_market_price_8dp": "市場価格: $%.8f",
|
||||
"baltab_pct_change_24h": "%s%.1f%% 24h",
|
||||
"baltab_pct_of_total_zaddr": "全体の%.0f%% · %d Z-addr",
|
||||
"baltab_shielded": "シールド",
|
||||
"baltab_shielded_amount": "シールド %.8f",
|
||||
"baltab_t_addresses_count": "%d 個のT-address",
|
||||
"baltab_total_balance": "合計残高",
|
||||
"baltab_transparent": "透明",
|
||||
"baltab_transparent_amount": "透明 %.8f",
|
||||
"ban": "ブロック",
|
||||
"banned_peers": "ブロック済みピア",
|
||||
"block": "ブロック",
|
||||
@@ -101,6 +196,12 @@
|
||||
"block_click_copy": "クリックしてコピー",
|
||||
"block_click_next": "クリックして次のブロックを表示",
|
||||
"block_click_prev": "クリックして前のブロックを表示",
|
||||
"block_db_reindex_body": "これは通常、デーモンの更新でディスク上のフォーマットが変わった後、またはブロックインデックスが破損している場合に発生します。ウォレットとコインは安全です — ノードがチェーンを読み込めないだけなので、残高は0と表示されます。\n\n再構築は既存のブロックファイルを再読み込みするもので、時間がかかることがあります(ウォレットの再スキャンも行います)。ダウンロードは発生しません。",
|
||||
"block_db_reindex_confirm": "ブロックデータベースを再構築",
|
||||
"block_db_reindex_notify": "ノードがブロックデータベースを読み込めません(デーモンの更新後によく発生します)。残高を復元するには再構築してください — プロンプトを確認するか、設定 › ノードへ。",
|
||||
"block_db_reindex_started": "保存済みのブロックからブロックデータベースを再構築しています — 時間がかかる場合があります。",
|
||||
"block_db_reindex_title": "ブロックデータベースを再構築しますか?",
|
||||
"block_db_reindex_warn": "ノードがブロックデータベースを読み込めません。",
|
||||
"block_explorer": "ブロックエクスプローラー",
|
||||
"block_get_info": "ブロック情報を取得",
|
||||
"block_hash": "ブロックハッシュ:",
|
||||
@@ -132,6 +233,7 @@
|
||||
"bootstrap_verifying": "チェックサムを検証中...",
|
||||
"bootstrap_wallet_protected": "(wallet.dat は保護されています)",
|
||||
"bootstrap_warning": "既存のブロックデータ(blocks、chainstate、notarizations)は削除され置き換えられます。wallet.dat は変更・削除されません。",
|
||||
"byte_count_fmt": "%zu / %zu バイト",
|
||||
"cancel": "キャンセル",
|
||||
"change_pass_confirm": "新しいパスフレーズ(確認):",
|
||||
"change_pass_current": "現在のパスフレーズ:",
|
||||
@@ -147,6 +249,10 @@
|
||||
"chat_accent_theme": "テーマ",
|
||||
"chat_add_contact": "連絡先に追加",
|
||||
"chat_awaiting_key": "返信待ち",
|
||||
"chat_blocked_desc": "ブロックされた会話は削除され、ブロック解除するまでメッセージ(過去・新規とも)は反映されません。ブロックを解除すると、チェーンから会話が再インポートされます。",
|
||||
"chat_blocked_manage": "ブロック中",
|
||||
"chat_blocked_title": "ブロックされた会話",
|
||||
"chat_blocked_toast": "会話を削除してブロックしました",
|
||||
"chat_bubble_minimal": "ミニマル",
|
||||
"chat_bubble_rounded": "角丸",
|
||||
"chat_bubble_square": "角ばった",
|
||||
@@ -159,6 +265,16 @@
|
||||
"chat_contact_added": "連絡先を追加しました — 連絡先で名前を変更できます",
|
||||
"chat_contact_request": "連絡リクエスト",
|
||||
"chat_copy_address_tip": "クリックしてアドレスをコピー",
|
||||
"chat_delete": "会話を削除",
|
||||
"chat_delete_block": "削除してブロック",
|
||||
"chat_delete_body_prefix": "次の相手との会話のローカルコピーを削除しますか:",
|
||||
"chat_delete_body_suffix": "?",
|
||||
"chat_delete_confirm": "削除",
|
||||
"chat_delete_failed": "会話を削除できませんでした — 変更はありません。",
|
||||
"chat_delete_local_note": "これはこの端末のみに影響します — メッセージはブロックチェーン上に残り、相手も自分のコピーを保持し続けます。",
|
||||
"chat_delete_revive_note": "「削除」はこの端末上の履歴を消去します。相手から再度メッセージが届くと、会話は復活します。",
|
||||
"chat_delete_title": "会話を削除しますか?",
|
||||
"chat_deleted_toast": "会話を削除しました",
|
||||
"chat_density_comfortable": "ゆったり",
|
||||
"chat_density_compact": "コンパクト",
|
||||
"chat_emoji_color": "カラー",
|
||||
@@ -182,6 +298,7 @@
|
||||
"chat_new_button": "新しいチャット",
|
||||
"chat_new_message": "メッセージ",
|
||||
"chat_new_message_toast": "新しい暗号化チャットメッセージ",
|
||||
"chat_new_needs_zaddr": "チャットにはシールド(z)アドレスが必要です — 透過(t)アドレスは暗号化メッセージを受信できません。",
|
||||
"chat_new_send": "リクエストを送信",
|
||||
"chat_new_title": "新しいチャット",
|
||||
"chat_new_zaddr": "宛先Zアドレス",
|
||||
@@ -227,6 +344,8 @@
|
||||
"chat_ts_24h": "24時間",
|
||||
"chat_ts_global": "全体設定に従う",
|
||||
"chat_ts_global_short": "全体",
|
||||
"chat_unblock": "ブロック解除",
|
||||
"chat_unblocked_toast": "会話のブロックを解除しました",
|
||||
"chat_unhide": "再表示",
|
||||
"chat_unmute": "ミュート解除",
|
||||
"chat_verify_key": "識別鍵 — 照合して確認",
|
||||
@@ -279,6 +398,7 @@
|
||||
"console": "コンソール",
|
||||
"console_accents": "カラーアクセント",
|
||||
"console_app": "アプリ",
|
||||
"console_auto_focus": "開いたときに入力欄へフォーカス",
|
||||
"console_auto_scroll": "自動スクロール",
|
||||
"console_available_commands": "利用可能なコマンド:",
|
||||
"console_backend_reference": "バックエンドコマンドリファレンス",
|
||||
@@ -373,11 +493,15 @@
|
||||
"console_tab_completion": "Tabで補完",
|
||||
"console_text_colors": "テキスト色",
|
||||
"console_toggle_accents": "行のカラーアクセントを切り替え",
|
||||
"console_toggle_auto_focus": "コンソールタブを開いたときにコマンド入力欄にカーソルを置きます",
|
||||
"console_toggle_text_color": "行のテキスト色を切り替え",
|
||||
"console_type_help": "'help'と入力して利用可能なコマンドを表示",
|
||||
"console_welcome": "ObsidianDragonコンソールへようこそ",
|
||||
"console_zoom_in": "拡大",
|
||||
"console_zoom_out": "縮小",
|
||||
"consolidate_desc": "多数の小さな入力を1つのシールドノートにまとめます。ノート数が減るとウォレットファイルが小さくなり、プライバシーも向上します。",
|
||||
"consolidate_funds_btn": "統合",
|
||||
"consolidate_title": "資金を統合",
|
||||
"contact_avatar": "アバター",
|
||||
"contact_avatar_bad_image": "その画像を読み込めませんでした。",
|
||||
"contact_avatar_badge": "バッジ",
|
||||
@@ -407,6 +531,7 @@
|
||||
"contacts_shape_square": "四角",
|
||||
"contacts_shape_tab": "左タブ",
|
||||
"copied": "コピーしました!",
|
||||
"copied_to_clipboard": "クリップボードにコピーしました",
|
||||
"copy": "コピー",
|
||||
"copy_address": "完全なアドレスをコピー",
|
||||
"copy_error": "エラーをコピー",
|
||||
@@ -419,11 +544,16 @@
|
||||
"daemon_bundled": "バンドル版",
|
||||
"daemon_install_bundled": "バンドル版をインストール",
|
||||
"daemon_installed": "インストール済み",
|
||||
"daemon_maintenance_label": "メンテナンス",
|
||||
"daemon_none_bundled": "このビルドにはなし",
|
||||
"daemon_not_installed": "未インストール",
|
||||
"daemon_status_diff": "バージョンが異なります",
|
||||
"daemon_status_differ": "インストール済みのバイナリはバンドル版と異なります。",
|
||||
"daemon_status_match": "インストール済みのバイナリはバンドル版と一致しています。",
|
||||
"daemon_status_missing": "デーモンがインストールされていません — バンドル版をインストールしてください。",
|
||||
"daemon_status_none": "未インストール",
|
||||
"daemon_status_ok": "最新です",
|
||||
"daemon_update_active_tt": "現在インストールされている(有効な)ノード",
|
||||
"daemon_update_available": "新しいノードバージョンが利用可能です",
|
||||
"daemon_update_body": "このウォレットビルドには、現在インストールされているものよりも新しいDragonXノードがバンドルされています。更新すると、インストール済みのdragonxd(およびdragonx-cli/dragonx-tx)が置き換えられ、その後ノードを停止して再起動し、新しいバージョンが有効になります。新しいノードには、古いものにない機能(例:シードフレーズのサポート)が追加されている場合があるため、更新を推奨します。",
|
||||
"daemon_update_browse": "すべてのリリースを表示…",
|
||||
@@ -433,18 +563,28 @@
|
||||
"daemon_update_download_install": "ダウンロードしてインストール",
|
||||
"daemon_update_downloading": "ダウンロード中…",
|
||||
"daemon_update_failed": "更新に失敗しました",
|
||||
"daemon_update_install_this": "このバージョンをインストール",
|
||||
"daemon_update_installed": "インストール済み:",
|
||||
"daemon_update_installed_ok": "ノードをインストールしました",
|
||||
"daemon_update_installing": "インストール中…",
|
||||
"daemon_update_keep": "現在のまま使用",
|
||||
"daemon_update_later": "後で",
|
||||
"daemon_update_latest": "最新:",
|
||||
"daemon_update_latest_badge": "最新",
|
||||
"daemon_update_latest_tt": "利用可能な最新バージョン",
|
||||
"daemon_update_loading": "リリースを読み込み中…",
|
||||
"daemon_update_no_notes": "このバージョンのリリースノートはありません。",
|
||||
"daemon_update_now": "今すぐ更新",
|
||||
"daemon_update_prerelease_tt": "プレリリース/テストビルド",
|
||||
"daemon_update_prompt_title": "ノードデーモンを更新しますか?",
|
||||
"daemon_update_reinstall": "再インストール",
|
||||
"daemon_update_released": "リリース日:",
|
||||
"daemon_update_restart_note": "新しいバージョンを実行するにはデーモンを再起動してください。",
|
||||
"daemon_update_restart_now": "今すぐデーモンを再起動",
|
||||
"daemon_update_safe": "ウォレット、鍵、ブロックチェーンデータには影響しません — 置き換えられるのはデーモンのプログラムファイルのみです。意図的にカスタムノードを実行している場合は、「現在のまま使用」を選択してください。",
|
||||
"daemon_update_status_current": "このバージョンはインストール済みです",
|
||||
"daemon_update_status_latest": "最新バージョン",
|
||||
"daemon_update_status_older": "インストール済みバージョンより古いです",
|
||||
"daemon_update_title": "ノードデーモンを更新しますか?",
|
||||
"daemon_update_unavailable_body": "このプラットフォーム向けのノードビルドはありません。",
|
||||
"daemon_update_unavailable_title": "ノードの更新は利用できません",
|
||||
@@ -453,12 +593,20 @@
|
||||
"daemon_update_verify_note": "ダウンロードは、インストール前にリリースで公開された SHA-256 と固定された ed25519 署名で検証されます。",
|
||||
"daemon_update_verifying": "検証中…",
|
||||
"daemon_update_version": "バージョン:",
|
||||
"daemon_updates_label": "アップデート",
|
||||
"daemon_version": "デーモン",
|
||||
"dark": "ダーク",
|
||||
"data_stale_prefix": "更新",
|
||||
"data_stale_tooltip": "残高が最新でない可能性があります。ウォレットは最近更新を受信していません。ノード接続を確認してください。",
|
||||
"date": "日付",
|
||||
"date_label": "日付:",
|
||||
"debug_gate_confirm": "デバッグオプションを表示",
|
||||
"debug_gate_incorrect": "PINまたはパスフレーズが正しくありません。",
|
||||
"debug_gate_pass_prompt": "続行するにはウォレットのパスフレーズを入力してください:",
|
||||
"debug_gate_pin_prompt": "続行するにはロック解除PINを入力してください:",
|
||||
"debug_gate_title": "デバッグオプションを表示しますか?",
|
||||
"debug_gate_verifying": "確認中…",
|
||||
"debug_gate_warning": "これらは高度な診断・ノード調整用オプションです。機密情報が露出したり、ノードの動作が変わったりする可能性があります。内容を理解している場合のみ続行してください。",
|
||||
"debug_logging": "デバッグログ",
|
||||
"decrypt_desc": "ウォレットがエクスポートされ、デーモンが新しい暗号化されていないウォレットで再起動し、すべての鍵が再インポートされます。ウォレットのサイズによっては数分かかる場合があります。",
|
||||
"decrypt_error_title": "復号に失敗しました",
|
||||
@@ -481,10 +629,22 @@
|
||||
"disconnected": "切断済み",
|
||||
"dismiss": "閉じる",
|
||||
"display": "表示",
|
||||
"done": "完了",
|
||||
"download": "ダウンロード",
|
||||
"download_bootstrap": "ブートストラップをダウンロード",
|
||||
"dragonx_green": "DragonX(グリーン)",
|
||||
"edit": "編集",
|
||||
"empty_wallet_keys_suffix": "個の鍵",
|
||||
"empty_wallet_open_manager": "ウォレットマネージャーを開く",
|
||||
"empty_wallet_restore": "ウォレットを復元",
|
||||
"empty_wallet_salvage_body": "このウォレットが空なのは、以前の自動修復によって元のウォレットがバックアップとして脇に保存されたためです。コインはほぼ確実にそのバックアップの中にあり、失われていません。復元すれば資金を再び読み込めます。何も削除されません。現在のファイルは先に脇へ保存されます。",
|
||||
"empty_wallet_salvage_headline": "コインはバックアップファイルに安全に保管されています。",
|
||||
"empty_wallet_salvage_title": "ウォレットが修復された可能性があります",
|
||||
"empty_wallet_warning_body": "このウォレットにはアドレスも資金もありませんが、DragonX フォルダー内の別のウォレットファイルに鍵が含まれています。コインはおそらくそちらにあり、失われていません。ウォレットマネージャーを開いて、資金のあるウォレットに切り替えてください。",
|
||||
"empty_wallet_warning_dismiss": "このウォレットでは今後警告しない",
|
||||
"empty_wallet_warning_dismiss_tip": "現在のウォレットファイルに対してのみこの警告を停止します。後で別の空のウォレットに切り替えると、再び警告される場合があります。",
|
||||
"empty_wallet_warning_headline": "間違ったウォレットを開いた可能性があります。",
|
||||
"empty_wallet_warning_title": "このウォレットは空です",
|
||||
"enc_confirm": "確認:",
|
||||
"enc_desc": "ウォレットを暗号化すると、パスフレーズで秘密鍵が保護されます。暗号化後、デーモンが再起動します。",
|
||||
"enc_encrypting": "ウォレットを暗号化しています...",
|
||||
@@ -523,6 +683,7 @@
|
||||
"explorer_tx_outputs": "出力",
|
||||
"explorer_tx_size": "サイズ",
|
||||
"explorer_txid": "TxID:",
|
||||
"explorer_urls_hdr": "URL",
|
||||
"export": "エクスポート",
|
||||
"export_csv": "CSVエクスポート",
|
||||
"export_keys_btn": "鍵をエクスポート",
|
||||
@@ -550,6 +711,151 @@
|
||||
"export_viewing_key": "閲覧鍵をエクスポート",
|
||||
"failed_create_shielded": "シールドアドレスの作成に失敗しました",
|
||||
"failed_create_transparent": "透明アドレスの作成に失敗しました",
|
||||
"faq": "FAQ",
|
||||
"faq_d_mgmt_1_a": "組み込みノードはウォレットと同時に自動的に起動し、適切なタイミングで停止するため、通常は手動で管理する必要はありません。高度な操作(デーモンの再起動を含む)は設定 > ノードとセキュリティにあります。",
|
||||
"faq_d_mgmt_1_q": "ノードの起動・停止はどうすればいいですか?",
|
||||
"faq_d_mgmt_2_a": "自分で起動したノードにウォレットを接続している場合、このオプションはウォレットを閉じたときにそのノードも停止するかどうかを決めます。ウォレットを閉じた後もノードを動かし続けたい場合はオフのままにしてください。",
|
||||
"faq_d_mgmt_2_q": "「終了時に外部デーモンを閉じる」とは何をする機能ですか?",
|
||||
"faq_d_mgmt_3_a": "ノードとウォレットのデータは、ユーザープロファイル配下のDragonXデータディレクトリに保存されます(例:WindowsではAppData内、Linux/macOSではホームフォルダ内)。ブロックチェーンがその大部分を占め、増え続けるので十分な空き容量を確保してください。",
|
||||
"faq_d_mgmt_3_q": "ブロックチェーンとウォレットのデータはどこに保存されますか?",
|
||||
"faq_d_mgmt_title": "ノード管理",
|
||||
"faq_d_mine_1_a": "「マイニング」タブでは、CPUを使ってマイニングできます。DragonXは、一般的なプロセッサ向けに設計されたRandomXアルゴリズムを使用します。スレッド数を設定して開始すると、報酬がウォレットに蓄積されます。",
|
||||
"faq_d_mine_1_q": "DragonXをマイニングするには?",
|
||||
"faq_d_mine_2_a": "ソロマイニングは報酬を直接受け取れますが、ブロックを見つけたときにしか支払われないため、十分なハッシュレートがない限り頻度は低くなります。プールマイニングは他の参加者と作業を分担し、小さいながらも安定した支払いを受けられます。マイニングタブは両方に対応しています。",
|
||||
"faq_d_mine_2_q": "ソロマイニングとプールマイニング、どちらを選べばいい?",
|
||||
"faq_d_mine_3_a": "マイニングタブのプールセクションにある「マイナーを更新…」ボタンから、最新の最適化されたDragonXマイナービルドをダウンロード・検証・インストールできます。ノードのアップデータと同様に、ダウンロードごとにチェックサムと署名を検証してからインストールします。",
|
||||
"faq_d_mine_3_q": "マイナーを更新するには?",
|
||||
"faq_d_mine_4_a": "現行のノードは組み込みのstratumサーバーを実行でき、他のマイナーがあなたのマシンに接続できるようになります。Settings > Node & Security に切り替えスイッチがあります。デフォルトではローカルのみで待ち受けており、他のコンピュータに公開するには明示的な設定が必要です。",
|
||||
"faq_d_mine_4_q": "マイニングプールをホストできますか?",
|
||||
"faq_d_mine_title": "マイニングとプール",
|
||||
"faq_d_net_1_a": "起動直後はノードがピアを見つけるまでに少し時間がかかるため、「0ピア」と一時的に表示されるのは正常です。長く続く場合は、インターネット接続とファイアウォールがノードをブロックしていないか確認してください。ノードは組み込みのシードを通じて自動的にピアを見つけます。",
|
||||
"faq_d_net_1_q": "ウォレットにピアや接続が表示されません。どうすればいい?",
|
||||
"faq_d_net_2_a": "はい — DragonXノード同士はTLSで通信するため、ピア接続は暗号化されています。ウォレットも自身のノードとは安全なローカルチャネルで通信します。",
|
||||
"faq_d_net_2_q": "ネットワークへの接続は暗号化されていますか?",
|
||||
"faq_d_net_title": "ピアとネットワーク",
|
||||
"faq_d_node_1_a": "デーモン(dragonxd)は、DragonXのフルノードです。ブロックチェーン全体をダウンロード・検証し、取引をネットワークに中継し、ウォレットの問い合わせに応答します。自分のノードを実行することで、他者のサーバーを信頼する必要がなくなります。",
|
||||
"faq_d_node_1_q": "デーモン(dragonxd)とは何ですか?",
|
||||
"faq_d_node_2_a": "デフォルトでは、ウォレットは同梱された独自のノード(組み込み)を起動・管理するため、あなたが何かする必要はありません。すでに自分でdragonxdを実行している場合は、ウォレットをその外部ノードに接続することもできます。ウォレットはどちらの状況かを検出し、それに応じて動作します。",
|
||||
"faq_d_node_2_q": "組み込みノードと外部ノードの違いは?",
|
||||
"faq_d_node_3_a": "フルノードはブロックチェーンのすべてのルールを独自に検証するため、あなたのウォレットは第三者ではなく数学を信頼します。ネットワーク自体の強化にもつながります。トレードオフはディスク容量と初回同期です。",
|
||||
"faq_d_node_3_q": "そもそもなぜフルノードを実行するのですか?",
|
||||
"faq_d_node_title": "フルノードについて",
|
||||
"faq_d_perf_1_a": "ブロックチェーンが最も容量を占め、時間とともに増加するため、数ギガバイトの空き容量を確保しておいてください。ノードは高速化のためにデータベースキャッシュを使用しており、同梱のデフォルト値は一般的なマシン向けに調整されています。上級者は設定ファイル(DRAGONX.conf)のdbcacheを編集して、ノードのデータベースキャッシュを調整できます。",
|
||||
"faq_d_perf_1_q": "ノードはどれくらいディスクを使いますか?メモリは調整できますか?",
|
||||
"faq_d_perf_2_a": "セットアップウィザードで提供されるブートストラップダウンロードを有効にすると、すべてのブロックをゼロから検証する代わりに、最近の検証済みチェーンのコピーを取得できます。高速なディスク(SSD)も大いに役立ちます。",
|
||||
"faq_d_perf_2_q": "初回同期を速くするには?",
|
||||
"faq_d_perf_title": "ストレージとパフォーマンス",
|
||||
"faq_d_sync_1_a": "起動時、ノードはウォレットに応答できるようになる前にブロックを読み込んで検証します。「ブロックを処理中/現在のチェーン状態を構築するためにブロックを適用中」という画面はそのウォームアップです。これは正常な動作で、ノードが処理を終えて高さを報告すると、ウォレットは使用可能になります。",
|
||||
"faq_d_sync_1_q": "起動時の「ブロックを処理中」とはどういう意味ですか?",
|
||||
"faq_d_sync_2_a": "シールドされた資金については、ノードはプライベートに送金できるよう暗号学的な「ウィットネス」を保持しています。再起動やアップデートの後、最近のブロックを再スキャンしてこのキャッシュを再構築することがあります。取引の多いウォレットでは、これに数分かかり、追加のメモリを使用することがあります。\n\nこれは停止しているわけではないので、完了するまでお待ちください。途中で強制終了すると、次回は再構築を最初からやり直すことになります。",
|
||||
"faq_d_sync_2_q": "「ウィットネスキャッシュを再構築中」とは何ですか?",
|
||||
"faq_d_sync_3_a": "チェーンの先端付近では、ウォレットをスキャンする処理(残高やノートの確認)がノードと同じ内部ロックを奪い合うため、大きなウォレットでは最後のブロックの接続が遅くなることがあります。最近のバージョンではこのスキャンを制限し、ノードが追いつけるようにしています。負荷の軽いタブにとどまる(または単に待つ)ことで、より速く完了します。",
|
||||
"faq_d_sync_3_q": "ほぼ追いついているのに同期が遅いのはなぜですか?",
|
||||
"faq_d_sync_4_a": "初回同期ではチェーン全体をダウンロード・検証するため、回線とディスクの性能によっては時間がかかることがあります。セットアップウィザードでブートストラップダウンロードを有効にすると、最近の検証済みチェーンデータのコピーを取得できるため、大幅に高速化できます。",
|
||||
"faq_d_sync_4_q": "初回同期にはどれくらい時間がかかりますか?",
|
||||
"faq_d_sync_title": "同期とブロックチェーン",
|
||||
"faq_d_trbl_1_a": "通常は問題ありません。大きなウォレットでは、ノードがシールドウィットネスの再構築やブロックの適用に数分かかることがあり、その間は一時停止しているように見え、応答が遅くなることがあります。しばらくお待ちください — 動作中であれば、デバッグログ(Settings)に着実な進捗が表示されます。\n\nこの段階での強制終了は避けてください。次回起動時に作業が最初からやり直しになります。",
|
||||
"faq_d_trbl_1_q": "ノードが「最良チェーンを有効化中」で止まっているようです。フリーズしていますか?",
|
||||
"faq_d_trbl_2_a": "ウォレットファイルが復旧されたものの、鍵データの一部が欠けている場合、ノードは新しいアドレスの生成やシールドができない限定的な「デグレード」モードで開くことがあります。最も安全な対処法は、シードリカバリーフレーズから新しいウォレットに復元することです。",
|
||||
"faq_d_trbl_2_q": "「デグレードモード」とは何ですか?",
|
||||
"faq_d_trbl_3_a": "再スキャンは、ウォレットにチェーンを再読み込みさせて取引を再検出するもので(鍵をインポートした後などに便利です)。再インデックスは、ノードのブロックチェーンデータベースを再構築するもので、そのデータベースが破損している、またはバージョン変更後に非互換になっている場合にのみ必要です。どちらも時間がかかることがあります。Settings > Node & Security から開始してください。",
|
||||
"faq_d_trbl_3_q": "再インデックスや再スキャンはいつ行うべきですか?",
|
||||
"faq_d_trbl_4_a": "ウィットネスの再構築や初回同期のような重い処理の間、メモリ使用量が急増し、完了すると元に戻ります。アイドル時にメモリ使用量が高いままなのは通常ではありません — ウォレットを再起動すると解消します。低メモリのマシンでは、他のメモリを大量に使うアプリと競合していないか確認してください。",
|
||||
"faq_d_trbl_4_q": "ノードのメモリ使用量が多いのですが、正常ですか?",
|
||||
"faq_d_trbl_title": "トラブルシューティング",
|
||||
"faq_d_upd_1_a": "Settings > Node & Security > Daemon binary を開き、「アップデートを確認…」を使用してください。ウォレットが最新の検証済みノードビルドをダウンロードしてインストールし、新しいバージョンは次回デーモン起動時に有効になります。",
|
||||
"faq_d_upd_1_q": "ノードを更新するには?",
|
||||
"faq_d_upd_2_a": "はい — アップデートダイアログにはすべてのリリースが一覧表示されるため、特定のバージョンや古いビルドを選べます。ダウングレードには注意してください。古いノードは新しいノードが書き込んだブロックチェーンデータを受け付けない場合があり、再インデックスが必要になることがあります。",
|
||||
"faq_d_upd_2_q": "特定のノードバージョンをインストールできますか?",
|
||||
"faq_d_upd_3_a": "ダウンロードされたノードアーカイブは、インストール前に必ずチェックサムと暗号署名で検証されます。検証に失敗した場合、ウォレットはアップデートを拒否します。",
|
||||
"faq_d_upd_3_q": "このアップデートは安全ですか?",
|
||||
"faq_d_upd_title": "ノードの更新",
|
||||
"faq_group_daemon": "デーモン",
|
||||
"faq_group_wallet": "ウォレット",
|
||||
"faq_intro": "ウォレットとDragonXノードに関する回答です。検索するか、下のトピックから参照してください。",
|
||||
"faq_intro_lite": "Liteウォレットに関する回答です。検索するか、下のトピックから参照してください。",
|
||||
"faq_l_gs_1_a": "ObsidianDragonLiteは軽量版のDragonXウォレットです。フルノードを実行する代わりに、DragonX Liteサーバーに接続するため、起動が速く、ブロックチェーン全体をダウンロードする必要がありません。\n\n鍵、残高、送金の管理はフルウォレットと同じように行えます。サーバー接続の仕組みとプライバシー上のトレードオフについては、下記の「Liteウォレット」セクションをご覧ください。",
|
||||
"faq_l_gs_1_q": "ObsidianDragonLiteとは何ですか?",
|
||||
"faq_l_lite_1_a": "Liteウォレットは、自身でフルノードを実行する代わりにDragonX Liteサーバーと通信します。ブロックチェーン全体を自分でダウンロード・検証しないため、起動が速く、ディスクと帯域の消費も軽くなります。秘密鍵は常にあなたのデバイス内にとどまり、サーバーに送信されることはありません。",
|
||||
"faq_l_lite_1_q": "Liteウォレットはフルウォレットとどう違いますか?",
|
||||
"faq_l_lite_2_a": "Settingsを開いてサーバーを選択してください。サーバーはいつでも切り替えられ、ウォレットは新しいサーバーから再同期します。サーバーの応答が遅い、または到達できない場合は、別のサーバーを試してください。",
|
||||
"faq_l_lite_2_q": "サーバーの選び方や変更方法は?",
|
||||
"faq_l_lite_3_a": "あなたの鍵がデバイスの外に出ることはなく、シールドされた金額は暗号化されたままです。ただし、Liteサーバーはあなたのウォレットがどのアドレスについて問い合わせているかとあなたのIPアドレスを把握できるため、あなたのアドレス同士を関連付けられる可能性があります — 利便性とプライバシーのトレードオフです。最もプライバシーを重視する場合は、チェーンを自身で検証し、アドレスをサーバーに明かさないフルノードウォレットをご利用ください。",
|
||||
"faq_l_lite_3_q": "Liteウォレットはフルノードと同じくらいプライベートですか?",
|
||||
"faq_l_lite_title": "Liteウォレット",
|
||||
"faq_l_sec_1_a": "設定 > ウォレット を開き、セキュリティのセクションでパスフレーズを設定してください。暗号化はディスク上の秘密鍵を保護するので、ファイルにアクセスできる人がいてもコインを送金することはできません。\n\n強力なパスフレーズを選び、絶対に紛失しないでください。パスフレーズなしでは暗号化されたウォレットを復元する方法はありません。",
|
||||
"faq_no_results": "該当する結果がありません。別のキーワードでお試しください。",
|
||||
"faq_open_tooltip": "ヘルプ&FAQ",
|
||||
"faq_search_hint": "FAQを検索…",
|
||||
"faq_title": "ヘルプ&FAQ",
|
||||
"faq_w_addr_1_a": "透明アドレス(RまたはtまたはR始まり)はほとんどのコインと同様に動作します。金額とアドレスはブロックチェーン上で公開されます。\n\nシールドアドレス(zアドレス、zsで始まる)はゼロ知識暗号を使用し、金額・送信者・受信者はチェーン上で暗号化されます。シールドアドレスに保有された残高は非公開です。",
|
||||
"faq_w_addr_1_q": "透明アドレスとシールドアドレスの違いは何ですか?",
|
||||
"faq_w_addr_2_a": "できる限りシールド(z)アドレスを使用してください。残高と支払い履歴を非公開に保てます。DragonXはプライバシーコインであり、受け取る資金はデフォルトでシールドされます。\n\n透明アドレスは、やり取りするサービスがシールドアドレスに対応していない場合のみ使用してください。",
|
||||
"faq_w_addr_2_q": "どちらのアドレスタイプを使うべきですか?",
|
||||
"faq_w_addr_3_a": "シールドアドレスは再利用しても問題なく、履歴が漏れることもありません。透明アドレスは支払いごとに新しいアドレスを使うとプライバシーが向上します。新しいアドレスは受信タブから作成できます。",
|
||||
"faq_w_addr_3_q": "アドレスは使い回すべきですか、それとも新しく作るべきですか?",
|
||||
"faq_w_addr_4_a": "シールド化とは、コインを透明アドレスからシールドアドレスへ移し、非公開にすることです。新しくマイニングされたコインは透明な状態で届き、自動的にシールドされます。\n\n手動でシールドすることもできます。シールドは特別な種類のトランザクションにすぎないため、完了までに通常の確認時間がかかります。",
|
||||
"faq_w_addr_4_q": "「シールド化」とは何ですか?",
|
||||
"faq_w_addr_title": "アドレスとプライバシー",
|
||||
"faq_w_bal_1_a": "ウォレットがまだ同期中の場合、残高は不完全で0と表示されることがあります。まだすべてのトランザクションをスキャンしていないためです。同期が完了すると反映されます。\n\n画面下部のステータスバーを確認してください。接続状態とブロック高が表示されます。同期が完了すれば残高は正確になります。",
|
||||
"faq_w_bal_1_q": "残高が0のまま、または更新されないのはなぜですか?",
|
||||
"faq_w_bal_2_a": "あなたのトランザクションを含むブロックの上に新しいブロックがマイニングされるたびに、確認数が1つ増えます。確認数が多いほど、支払いはより確実に確定します。受け取った資金は最初の確認後に送金可能になります。",
|
||||
"faq_w_bal_2_q": "確認数とは何ですか?",
|
||||
"faq_w_bal_3_a": "合計残高には、まだ確認中の資金や一時的にロックされている資金(例えばシールド化の途中のコイン)も含まれます。送金可能残高は今すぐ送金できる金額です。トランザクションが確認されるにつれて両者は一致していきます。",
|
||||
"faq_w_bal_3_q": "合計残高と送金可能残高の違いは何ですか?",
|
||||
"faq_w_bal_title": "残高と同期",
|
||||
"faq_w_chat_1_a": "チャットはウォレットに組み込まれた、暗号化されたオンチェーンのメッセンジャー機能です。メッセージはシールドされたメモとして送信されるため、あなたと相手だけが読むことができます。",
|
||||
"faq_w_chat_1_q": "チャット機能とは何ですか?",
|
||||
"faq_w_chat_2_a": "チャットのアイデンティティはウォレットのリカバリーフレーズから導出されるため、ウォレットと一緒に持ち運ばれます。ウォレットを復元すればアイデンティティも戻ってきます。別途アカウントやパスワードを作成する必要はありません。",
|
||||
"faq_w_chat_2_q": "チャットのアイデンティティはどのように機能しますか?",
|
||||
"faq_w_chat_3_a": "連絡先タブでアドレスを入力して連絡先を追加します。名前やアバターの設定は任意です。連絡先は現在のウォレットのみに限定することも、このコンピューター上で開くすべてのウォレットで共有することもできます。",
|
||||
"faq_w_chat_3_q": "連絡先はどのように機能しますか?",
|
||||
"faq_w_chat_4_a": "会話を開き、ヘッダーのアイコンを使用してください。非表示(目のアイコン)はリストから削除しますが、すべてのメッセージは保持されます。新しいメッセージが届くと再表示されます。削除(ゴミ箱のアイコン)はダイアログを開きます。「削除」はこの端末上の履歴を消去します(相手から再度メッセージが届くと会話は復活します)。「削除してブロック」はさらに、会話リスト上部の「ブロック済み」リストから解除するまで相手からの今後のメッセージも停止します。ここでの操作はすべてこの端末のみに影響し、メッセージはブロックチェーン上に残り、相手も自分のコピーを保持し続けるため、「送信取り消し」ではありません。",
|
||||
"faq_w_chat_4_q": "会話を非表示、削除、またはブロックするにはどうすればよいですか?",
|
||||
"faq_w_chat_title": "チャットと連絡先",
|
||||
"faq_w_gs_1_a": "ObsidianDragonはDragonX(DRGX)のフルノードウォレットです。コインを管理し、デスクトップ版ではバックグラウンドでDragonXフルノード(デーモン)を実行するため、サードパーティのサーバーを信頼することなく、ウォレット自身がブロックチェーンを検証します。\n\nウォレット(鍵、残高、送金)とノード(ブロックチェーンとネットワーク)という2つの部分が連携して動作するイメージです。ここでの質問も同様に分かれています。上部の「ウォレット」タブと「デーモン」タブをご覧ください。",
|
||||
"faq_w_gs_1_q": "ObsidianDragonとは何ですか?",
|
||||
"faq_w_gs_2_a": "初回起動時、セットアップウィザードが自動的にウォレットを作成します。新しいウォレットは秘密のリカバリーフレーズ(単語のリスト)でバックアップされます。このフレーズを書き留めてオフラインで保管してください。このコンピューターを失った場合、資金を復元する唯一の方法です。\n\nフレーズはいつでも 設定 > バックアップとデータ > シードフレーズ から確認・バックアップできます。",
|
||||
"faq_w_gs_2_q": "新しいウォレットを作成するにはどうすればよいですか?",
|
||||
"faq_w_gs_3_a": "リカバリーフレーズをお持ちの場合は、シードフレーズ復元フローを使用してください。(フルノード版では、起動前に既存の wallet.dat ファイルをウォレットのデータディレクトリにコピーすることもできます。)\n\n復元後、ウォレットは過去のトランザクションを見つけるために再スキャンを行うため、初回は残高と履歴が表示されるまで時間がかかることがあります。",
|
||||
"faq_w_gs_3_q": "すでに持っているウォレットを復元するにはどうすればよいですか?",
|
||||
"faq_w_gs_4_a": "ウィザードでは、外観/テーマの選択、暗号化とPINの設定を行うことができ、フルノード版ではさらに初回同期を高速化するためのブートストラップのダウンロードを任意で選択できます。どのステップもスキップでき、後から設定でいつでも変更できます。",
|
||||
"faq_w_gs_4_q": "初回起動時のセットアップでは何が行われますか?",
|
||||
"faq_w_gs_title": "はじめに",
|
||||
"faq_w_sec_1_a": "設定 > ノードとセキュリティ を開き、パスフレーズを設定してください(セキュリティのセクション)。暗号化はディスク上の秘密鍵を保護するので、ファイルにアクセスできる人がいてもコインを送金することはできません。\n\n強力なパスフレーズを選び、絶対に紛失しないでください。パスフレーズなしでは暗号化されたウォレットを復元する方法はありません。",
|
||||
"faq_w_sec_1_q": "ウォレットを暗号化するにはどうすればよいですか?",
|
||||
"faq_w_sec_2_a": "PINはアプリの画面をロックし、離席中に残高や操作を非表示にします。これは暗号化に加えた利便性のためのロックで、ウォレットを暗号化した後、ウィザード中または設定で設定します。",
|
||||
"faq_w_sec_2_q": "PIN/ロック画面とは何ですか?",
|
||||
"faq_w_sec_3_a": "いいえ。ウォレットの暗号化を回避する方法はありません。シードリカバリーフレーズをまだお持ちであれば、そこから新しいウォレットに復元し、新しいパスフレーズを設定できます。パスフレーズとシードフレーズのどちらも失った場合、資金を復元することはできません。",
|
||||
"faq_w_sec_3_q": "パスフレーズを忘れました — 復元できますか?",
|
||||
"faq_w_sec_title": "セキュリティと暗号化",
|
||||
"faq_w_seed_1_a": "設定 > バックアップとデータ > シードフレーズ に移動してください。単語を順番どおりに紙に書き留め、安全でオフラインの場所に保管してください。フレーズを持つ人は誰でもあなたのコインを送金できるため、写真、メール、クラウドメモには絶対に保存しないでください。",
|
||||
"faq_w_seed_1_q": "リカバリーフレーズはどうやってバックアップすればよいですか?",
|
||||
"faq_w_seed_2_a": "古い(レガシー)ウォレットはシードベースではありませんでした。このウォレットは、レガシーウォレットを最新のシードベースのウォレットへ移行できます。新しいシードウォレットを作成し、資金をそこへスイープします。設定 > バックアップとデータ の「シードウォレットへ移行…」を探してください。この機能には現在のデーモンバージョンが必要です。",
|
||||
"faq_w_seed_2_q": "ウォレットにリカバリーフレーズがありません — 追加できますか?",
|
||||
"faq_w_seed_3_a": "ウォレットのセットアップ時にシード復元フローを使用し、単語を順番どおりに入力してください。ウォレットは残高と履歴を再構築するために再スキャンを行うため、初回は時間がかかることがあります。",
|
||||
"faq_w_seed_3_q": "リカバリーフレーズから復元するにはどうすればよいですか?",
|
||||
"faq_w_seed_4_a": "シードフレーズは主要なバックアップであり、これだけですべてを復元できます。ウォレットファイルのコピーは便利な二次バックアップで、ラベルや設定も保持されます。どちらのバックアップもオフラインかつ非公開の場所に保管してください。",
|
||||
"faq_w_seed_4_q": "wallet.dat もバックアップすべきですか?",
|
||||
"faq_w_seed_title": "シードフレーズとバックアップ",
|
||||
"faq_w_send_1_a": "送金タブを開き、受取人のアドレスを貼り付け、金額を入力して確認してください。ウォレットが暗号化されている場合、トランザクションのためにロック解除を求められます。\n\nシールドアドレスへの送金は非公開、透明アドレスへの送金は公開されます。",
|
||||
"faq_w_send_1_q": "資金を送金するにはどうすればよいですか?",
|
||||
"faq_w_send_2_a": "DragonXの手数料は非常に低く、自動的に設定されます。手数料は確認前に表示されます。通常は変更する必要はありません。",
|
||||
"faq_w_send_2_q": "どのくらいの手数料がかかりますか?",
|
||||
"faq_w_send_3_a": "はい — シールド(z)アドレスへ送金する際は、暗号化されたメモを添付できます。読めるのは受取人だけです。透明アドレスはメモに対応していません。",
|
||||
"faq_w_send_3_q": "支払いにメッセージを添付できますか?",
|
||||
"faq_w_send_4_a": "トランザクションはブロックに取り込まれ、確認数を得るまで「保留中」となります。通常1~2分ほどかかります。シールドトランザクションでは、証明を生成するためにウォレットが同期済みである必要もあります。\n\n送金が異常に長く保留中のままの場合は、ウォレットが接続され、同期済みであることを確認してください(ウィンドウ下部のステータスバーを参照)。",
|
||||
"faq_w_send_4_q": "支払いを送りましたが「保留中」のままです — なぜですか?",
|
||||
"faq_w_send_title": "送受金",
|
||||
"faq_w_set_1_a": "設定 > 外観 でテーマとアクセントカラーを選べます。ライト/ダークの複数のスキンが含まれており、性能の低いマシン向けにぼかしなどのエフェクトをオフにすることもできます。",
|
||||
"faq_w_set_1_q": "テーマを変更するには?",
|
||||
"faq_w_set_2_a": "はい — 設定 > 外観 に言語選択があります。ウォレットには複数の翻訳が同梱されており、未翻訳の部分は英語のまま表示されます。",
|
||||
"faq_w_set_2_q": "言語を変更できますか?",
|
||||
"faq_w_set_3_a": "設定 > 外観 のフォントスケールオプションを使います。インターフェース全体を拡大縮小でき、高解像度(HiDPI)ディスプレイでアプリの表示が小さくなりすぎる場合に便利です。",
|
||||
"faq_w_set_3_q": "文字が小さすぎる(または大きすぎる)のですが、拡大縮小できますか?",
|
||||
"faq_w_set_4_a": "設定で価格取得を有効にしてください。残高と併せて概算の法定通貨価値が表示され、小さな相場チャートも表示できるようになります。価格は参考情報にすぎません。",
|
||||
"faq_w_set_4_q": "法定通貨での価格を表示するには?",
|
||||
"faq_w_set_title": "設定と外観",
|
||||
"favorite_address": "お気に入りに追加",
|
||||
"fee": "手数料",
|
||||
"fee_high": "高い",
|
||||
@@ -572,6 +878,63 @@
|
||||
"general": "一般",
|
||||
"generating": "生成中",
|
||||
"go_to_receive": "受信へ移動",
|
||||
"grpa_current_block_paren": "(現在: %d)",
|
||||
"grpa_days_ago": "%lld日前",
|
||||
"grpa_dbg_addrman": "ピアアドレスの追跡と管理",
|
||||
"grpa_dbg_alert": "アラートシステムのメッセージ",
|
||||
"grpa_dbg_bench": "操作のベンチマーク計測",
|
||||
"grpa_dbg_coindb": "コインデータベースの読み書き操作",
|
||||
"grpa_dbg_db": "Berkeley DB の操作",
|
||||
"grpa_dbg_estimatefee": "手数料見積もりアルゴリズム",
|
||||
"grpa_dbg_http": "HTTP RPCサーバーの動作",
|
||||
"grpa_dbg_libevent": "Libevent ネットワークライブラリ",
|
||||
"grpa_dbg_lock": "ロック競合のデバッグ",
|
||||
"grpa_dbg_mempool": "トランザクションメモリプールの動作",
|
||||
"grpa_dbg_net": "ネットワーク接続とメッセージ",
|
||||
"grpa_dbg_paymentdisclosure": "支払い開示プロトコル",
|
||||
"grpa_dbg_pow": "プルーフオブワークのマイニング動作",
|
||||
"grpa_dbg_proxy": "SOCKS5 プロキシ接続",
|
||||
"grpa_dbg_prune": "ブロックのプルーニング操作",
|
||||
"grpa_dbg_rand": "乱数生成",
|
||||
"grpa_dbg_reindex": "ブロックチェーンの再インデックス進捗",
|
||||
"grpa_dbg_rpc": "RPCコマンドの処理",
|
||||
"grpa_dbg_selectcoins": "トランザクション用のコイン選択",
|
||||
"grpa_dbg_tor": "Tor統合とサーキット情報",
|
||||
"grpa_dbg_zmq": "ZeroMQ 通知システム",
|
||||
"grpa_dbg_zrpc": "シールド(z-addr)RPC操作",
|
||||
"grpa_enter_private_key_to_import": "インポートする秘密鍵を入力してください。",
|
||||
"grpa_error_prefix": "エラー: ",
|
||||
"grpa_hr_ago": "%lld時間前",
|
||||
"grpa_invalid_response_from_daemon": "デーモンからの応答が無効です",
|
||||
"grpa_invalid_suffix": "(無効)",
|
||||
"grpa_min_ago": "%lld分前",
|
||||
"grpa_sec_ago": "%lld秒前",
|
||||
"grpa_seed_demo_chat": "デモチャットのシード",
|
||||
"grpa_showing_first_100_of": "... %d件中の最初の100件を表示",
|
||||
"grpa_tab_about": "情報",
|
||||
"grpa_tab_appearance": "外観",
|
||||
"grpa_tab_backup_data": "バックアップとデータ",
|
||||
"grpa_tab_chat": "チャット",
|
||||
"grpa_tab_explorer": "エクスプローラー",
|
||||
"grpa_tab_node_security": "ノードとセキュリティ",
|
||||
"grpa_tab_wallet": "ウォレット",
|
||||
"grpa_unexpected_getblockhash_result": "予期しない getblockhash の結果",
|
||||
"grpb_copy": "コピー",
|
||||
"grpb_max": "最大",
|
||||
"grpb_new_badge_suffix": " [新規]",
|
||||
"grpb_preview_msg_payment_through": "支払いは完了しましたか? 🙂",
|
||||
"grpb_preview_msg_sending_rest": "残りを今送金します 👍",
|
||||
"grpb_preview_msg_yep_confirmed": "はい — たった今確認できました ✅",
|
||||
"grpb_selected_suffix": "\n(選択済み)",
|
||||
"grpb_tooltip_address_balance": "%s\n残高: %.8f %s%s",
|
||||
"grpb_undo_clear": "消去を元に戻す",
|
||||
"grpc_benchmark_inconclusive": "ベンチマークの結果が不明確です: ハッシュレートのサンプルが記録されませんでした。プール接続を確認して再試行してください。",
|
||||
"grpc_benchmark_takes_secs": "ベンチマークには約%d秒かかり、マイニングが中断されます。開始するにはもう一度クリックしてください。",
|
||||
"grpc_bootstrap_failed": "ブートストラップに失敗しました",
|
||||
"grpc_bootstrap_not_initialized": "ブートストラップが初期化されていません",
|
||||
"grpc_hashrate_fee": "%s 手数料 %s%%",
|
||||
"grpc_key_not_available": "このアドレスの鍵は利用できません",
|
||||
"grpc_na": "該当なし",
|
||||
"height": "高さ",
|
||||
"help": "ヘルプ",
|
||||
"hidden_tag": " (非表示)",
|
||||
@@ -646,6 +1009,7 @@
|
||||
"light": "ライト",
|
||||
"lite_account_label": "アカウント",
|
||||
"lite_action": "アクション",
|
||||
"lite_backend_unavailable": "ライトウォレットのバックエンドが利用できません",
|
||||
"lite_backup_keys": "バックアップと鍵",
|
||||
"lite_birthday_backup": "誕生日:%llu (これもバックアップしてください)",
|
||||
"lite_birthday_hint": "スキャンを開始するブロック高。不明な場合は0のままにしてください(完全スキャンが遅くなります)。",
|
||||
@@ -653,9 +1017,12 @@
|
||||
"lite_console_backend_commands": "バックエンドコマンド:",
|
||||
"lite_console_help_passthrough": "その他の入力はライトウォレットのコンソールコマンドとして実行されます。",
|
||||
"lite_copy": "コピー",
|
||||
"lite_could_not_start": "操作を開始できませんでした",
|
||||
"lite_could_not_write": "書き込めませんでした ",
|
||||
"lite_encrypt_wallet": "ウォレットを暗号化",
|
||||
"lite_encryption_removed": "暗号化を解除しました",
|
||||
"lite_enter_all_seed_words": "復元するには24個のシードワードをすべて入力してください(現在 %d 個)",
|
||||
"lite_enter_wallet_path": "ウォレットのパスを入力してください",
|
||||
"lite_hide_wipe": "非表示にして消去",
|
||||
"lite_import": "インポート",
|
||||
"lite_import_key_label": "鍵をインポート",
|
||||
@@ -792,9 +1159,27 @@
|
||||
"memo_optional": "メモ(任意)",
|
||||
"memo_upper": "メモ",
|
||||
"memo_z_only": "注:メモはシールド (z) アドレスへの送金時のみ利用可能です",
|
||||
"merge_addr_created": "シールドアドレスを作成しました。",
|
||||
"merge_advanced": "詳細設定",
|
||||
"merge_back": "戻る",
|
||||
"merge_batch_fmt": "1回の実行で最大%d件の入力を統合します — 残りは繰り返し実行して完了させてください。",
|
||||
"merge_confirm_btn": "確認",
|
||||
"merge_confirm_fmt": "~%s DRGXを入力%d件から%sへ統合しますか?",
|
||||
"merge_create_zaddr": "シールドアドレスを作成",
|
||||
"merge_creating": "アドレスを作成中…",
|
||||
"merge_description": "複数のUTXOを単一のシールドアドレスに統合します。ウォレットサイズの縮小とプライバシーの向上に役立ちます。",
|
||||
"merge_fee_hint": "この取引にかかるネットワーク手数料です。",
|
||||
"merge_funds": "資金を統合",
|
||||
"merge_max_inputs": "バッチあたりの最大入力数",
|
||||
"merge_no_spendable": "統合できる使用可能な入力がまだありません。",
|
||||
"merge_progress": "統合中… 数分かかることがあります。このウィンドウは閉じても構いません。",
|
||||
"merge_scope_fmt": "透明%d件 + シールド%d件の入力 · 使用可能 約%s DRGX",
|
||||
"merge_scope_loading": "入力を確認中…",
|
||||
"merge_send_failed": "マージに失敗しました: ",
|
||||
"merge_source": "統合",
|
||||
"merge_src_both": "両方",
|
||||
"merge_src_shielded": "シールド",
|
||||
"merge_src_transparent": "透明",
|
||||
"merge_started": "統合操作を開始しました",
|
||||
"merge_title": "アドレスに統合",
|
||||
"mig_adopt": "これを自分のウォレットにする",
|
||||
@@ -875,6 +1260,7 @@
|
||||
"mining_difficulty_copied": "難易度をコピーしました",
|
||||
"mining_est_block": "予測ブロック",
|
||||
"mining_est_daily": "予測日収",
|
||||
"mining_est_daily_pool_sub": "おおよそのソロ換算(プール手数料前)",
|
||||
"mining_filter_all": "すべて",
|
||||
"mining_filter_tip_all": "すべての収益を表示",
|
||||
"mining_filter_tip_pool": "プール収益のみ表示",
|
||||
@@ -903,10 +1289,12 @@
|
||||
"mining_open_in_explorer": "エクスプローラーで開く",
|
||||
"mining_payout_address": "支払いアドレス",
|
||||
"mining_payout_foreign": "⚠ この支払いアドレスは現在のウォレットに含まれていません — マイニング報酬が別のウォレットに送られます。ウォレットを切り替えた場合は更新してください。",
|
||||
"mining_payout_invalid": "有効な DragonX アドレスではありません — 開始前に修正してください。さもないとマイニング報酬が失われます。",
|
||||
"mining_payout_tooltip": "マイニング報酬の受取アドレス",
|
||||
"mining_pool": "プール",
|
||||
"mining_pool_fee": "手数料",
|
||||
"mining_pool_hashrate": "プールハッシュレート",
|
||||
"mining_pool_needs_payout_tooltip": "先に支払い先アドレスを入力してください(Zアドレスを生成)",
|
||||
"mining_pool_url": "プールURL",
|
||||
"mining_pools_header": "プール",
|
||||
"mining_recent_blocks": "最近のブロック",
|
||||
@@ -936,6 +1324,9 @@
|
||||
"mining_syncing_tooltip": "ブロックチェーン同期中...",
|
||||
"mining_tag": " · マイニング",
|
||||
"mining_threads": "マイニングスレッド",
|
||||
"mining_threads_input_tooltip": "正確なスレッド数を入力(Enter で適用)",
|
||||
"mining_threads_minus_tooltip": "スレッドを減らす",
|
||||
"mining_threads_plus_tooltip": "スレッドを増やす",
|
||||
"mining_to_save": "保存する",
|
||||
"mining_today": "今日",
|
||||
"mining_uptime": "稼働時間",
|
||||
@@ -1028,6 +1419,13 @@
|
||||
"peers_upper": "ピア",
|
||||
"peers_version": "バージョン",
|
||||
"pending": "保留中",
|
||||
"picker_dat_count": "このフォルダー内のウォレットファイル:%d件",
|
||||
"picker_dat_none": "このフォルダー直下にウォレットファイルはありません",
|
||||
"picker_empty": "このフォルダーにはサブフォルダーもウォレットファイルもありません。",
|
||||
"picker_home": "ホームフォルダー",
|
||||
"picker_select": "このフォルダーをスキャン",
|
||||
"picker_title": "スキャンするフォルダーを選択",
|
||||
"picker_up": "1つ上の階層へ",
|
||||
"pin_change_desc": "ロック解除PINを変更します。現在のPINと新しいPINが必要です。",
|
||||
"pin_confirm_new_label": "新しいPINの確認:",
|
||||
"pin_current_label": "現在のPIN:",
|
||||
@@ -1105,6 +1503,8 @@
|
||||
"qr_failed": "QRコードの生成に失敗しました",
|
||||
"qr_title": "QRコード",
|
||||
"qr_unavailable": "QR利用不可",
|
||||
"quick_receive": "クイック受取",
|
||||
"quick_send": "クイック送金",
|
||||
"ram_daemon_gb": "デーモン:%.1f GB (%s)",
|
||||
"ram_daemon_mb": "デーモン:%.0f MB (%s)",
|
||||
"ram_system_gb": "システム:%.1f / %.0f GB",
|
||||
@@ -1154,6 +1554,7 @@
|
||||
"rpc_connection": "RPC接続...",
|
||||
"rpc_host": "RPCホスト",
|
||||
"rpc_pass": "パスワード",
|
||||
"rpc_plaintext_remote_warning": "リモートRPCは暗号化されていないHTTPを使用しています。デーモンがTLSに対応している場合は、DRAGONX.confにrpctls=1を追加してください。",
|
||||
"rpc_port": "ポート",
|
||||
"rpc_user": "ユーザー名",
|
||||
"save": "保存",
|
||||
@@ -1161,6 +1562,7 @@
|
||||
"save_z_transactions": "Z取引を取引リストに保存",
|
||||
"sb_auth_failed": "認証失敗 — rpcuser/rpcpassword を確認してください",
|
||||
"sb_block": "ブロック: %d",
|
||||
"sb_block_db_unreadable": "ブロックデータベースを読み込めません — 再構築が必要です",
|
||||
"sb_building_witnesses": "証人を設定中",
|
||||
"sb_building_witnesses_pct": "ウィットネスを再構築中 %.0f%%",
|
||||
"sb_connecting_daemon": "dragonxd に接続中...",
|
||||
@@ -1168,12 +1570,15 @@
|
||||
"sb_connecting_external": "外部デーモンに接続中...",
|
||||
"sb_connecting_generic": "デーモンに接続中...",
|
||||
"sb_daemon_crashed": "デーモンが %d 回クラッシュしました",
|
||||
"sb_daemon_extract_failed": "デーモンファイルの書き込みに失敗しました — 空きディスク容量と権限を確認してください。",
|
||||
"sb_daemon_files_failed": "デーモンファイルを%sに書き込めませんでした — 空きディスク容量と権限を確認してください。",
|
||||
"sb_daemon_not_found": "デーモンが見つかりません",
|
||||
"sb_daemon_start_failed": "dragonxd を起動できませんでした",
|
||||
"sb_dragonxd_running": "dragonxd 実行中",
|
||||
"sb_dragonxd_stopped": "dragonxd 停止",
|
||||
"sb_dragonxd_stopping": "dragonxd を停止中...",
|
||||
"sb_extracting_sapling": "Sapling パラメータを展開中...",
|
||||
"sb_finishing_repair": "ウォレットの修復を仕上げています — 再スキャン中…",
|
||||
"sb_importing_keys": "鍵をインポート中",
|
||||
"sb_loading_config": "設定を読み込み中...",
|
||||
"sb_mining_hs": "%.1f H/s",
|
||||
@@ -1183,6 +1588,7 @@
|
||||
"sb_net_mhs": "ネット: %.2f MH/s",
|
||||
"sb_no_conf": "DRAGONX.conf が見つかりません",
|
||||
"sb_peers": "ピア: %zu",
|
||||
"sb_plaintext_remote_blocked": "リモートホストへRPC認証情報を平文で送信することを拒否しました。許可するにはDRAGONX.confにrpcallowplaintext=1を追加するか、rpctls=1でTLSを有効にしてください。",
|
||||
"sb_rescanning": "再スキャン",
|
||||
"sb_rescanning_pct": "再スキャン %.0f%%",
|
||||
"sb_restarting_daemon": "デーモンを再起動中...",
|
||||
@@ -1194,14 +1600,55 @@
|
||||
"sb_waiting_config": "デーモン設定を待機中...",
|
||||
"sb_waiting_daemon": "dragonxd を待機中...",
|
||||
"sb_waiting_daemon_err": "dragonxd を待機中 — %s",
|
||||
"sb_wallet_needs_recovery": "ウォレットの修復が利用可能です",
|
||||
"sb_warming_up": "ウォームアップ中...",
|
||||
"sb_witness_cache": "ウィットネスを再構築中",
|
||||
"scale_effects": "スケールとエフェクト",
|
||||
"screenshot_open_dir": "場所を開く",
|
||||
"screenshot_sweep": "スクリーンショットスイープを実行",
|
||||
"screenshot_sweep_desc": "すべてのテーマをすべてのタブで巡回し、それぞれのスクリーンショットを設定ディレクトリの screenshots フォルダ内のタブごとのサブフォルダに保存します(前回のスイープを上書きします)。数秒間実行されます。",
|
||||
"screenshot_sweep_full": "UI全体スイープ",
|
||||
"search_icons": "アイコンを検索...",
|
||||
"search_placeholder": "検索...",
|
||||
"sec_changing_passphrase": "パスフレーズを変更しています...",
|
||||
"sec_changing_pin": "PINを変更しています...",
|
||||
"sec_couldnt_lock_wallet": "ウォレットをロックできませんでした。まだロック解除された状態です。デーモンの接続を確認してください。",
|
||||
"sec_encrypted_backup_suffix": "\n暗号化バックアップ: wallet.dat.encrypted.bak",
|
||||
"sec_encrypting_wallet": "ウォレットを暗号化しています...",
|
||||
"sec_encryption_did_not_complete": "ウォレットの暗号化が完了しませんでした。ウォレットは暗号化されていません。設定を開いて暗号化を完了してください。",
|
||||
"sec_encryption_failed_prefix": "暗号化に失敗しました: ",
|
||||
"sec_failed_prefix": "失敗しました: ",
|
||||
"sec_failed_to_create_vault": "ボールトの作成に失敗しました",
|
||||
"sec_importing_keys_rescanning": "鍵をインポートし、ブロックチェーンを再スキャンしています。処理中もウォレットは使用できます",
|
||||
"sec_incorrect_current_pin": "現在のPINが正しくありません",
|
||||
"sec_incorrect_passphrase_decrypt": "パスフレーズが正しくありません",
|
||||
"sec_incorrect_passphrase_pin_setup": "パスフレーズが正しくありません",
|
||||
"sec_incorrect_pin_remove": "PINが正しくありません",
|
||||
"sec_internal_error_change_pin": "内部エラー",
|
||||
"sec_internal_error_remove_pin": "内部エラー",
|
||||
"sec_mode_passphrase": " パスフレーズ",
|
||||
"sec_not_connected_to_daemon": "デーモンに接続されていません",
|
||||
"sec_not_connected_to_daemon_pin": "デーモンに接続されていません",
|
||||
"sec_passphrase_changed_successfully": "パスフレーズを変更しました",
|
||||
"sec_pin_changed_successfully": "PINを変更しました",
|
||||
"sec_pin_removed": "PINを削除しました",
|
||||
"sec_pin_set_successfully": "PINを設定しました",
|
||||
"sec_restart_daemon_for_encryption": "暗号化を有効にするには、デーモンを再起動してください。",
|
||||
"sec_too_many_attempts_wait": "試行回数が多すぎます。%.0f秒お待ちください...",
|
||||
"sec_total_elapsed_fmt": "合計経過時間: %d分 %02d秒",
|
||||
"sec_unlock_button": "ロック解除",
|
||||
"sec_unlock_failed_prefix": "ロック解除に失敗しました: ",
|
||||
"sec_unlocking_fmt": "ロック解除中%s",
|
||||
"sec_use_passphrase_instead": "代わりにパスフレーズを使用",
|
||||
"sec_use_pin_instead": "代わりにPINを使用",
|
||||
"sec_verifying_passphrase": "パスフレーズを確認しています...",
|
||||
"sec_verifying_pin": "PINを確認しています...",
|
||||
"sec_wallet_decrypted_all_keys_imported": "ウォレットの復号に成功しました。すべての鍵をインポートしました。",
|
||||
"sec_wallet_encrypted_and_pin_set": "ウォレットを暗号化し、PINを設定しました",
|
||||
"sec_wallet_encrypted_but_pin_vault_failed": "ウォレットは暗号化されましたが、PINボールトの作成に失敗しました",
|
||||
"sec_wallet_encrypted_restarting_daemon": "ウォレットを暗号化しました。デーモンを再起動しています...",
|
||||
"sec_wallet_encrypted_successfully": "ウォレットを暗号化しました",
|
||||
"sec_wallet_locked_title": "ウォレットがロックされています",
|
||||
"security": "セキュリティ",
|
||||
"seed_backup_button": "シードフレーズ",
|
||||
"seed_backup_close": "閉じる",
|
||||
@@ -1225,6 +1672,7 @@
|
||||
"send_amount": "金額",
|
||||
"send_amount_details": "金額の詳細",
|
||||
"send_amount_upper": "金額",
|
||||
"send_available_note": "利用可能",
|
||||
"send_cannot_send_now": "現在送信できません — 接続、同期、利用可能な残高を確認してください。",
|
||||
"send_clear_fields": "すべてのフォームフィールドをクリアしますか?",
|
||||
"send_contacts_button": "連絡先から選択",
|
||||
@@ -1260,6 +1708,7 @@
|
||||
"send_tooltip_not_connected": "デーモンに未接続",
|
||||
"send_tooltip_select_source": "まず送信元アドレスを選択してください",
|
||||
"send_tooltip_syncing": "ブロックチェーンの同期をお待ちください",
|
||||
"send_tooltip_view_only": "閲覧専用アドレス — 送金鍵がないため送金できません",
|
||||
"send_total": "合計",
|
||||
"send_transaction": "取引を送信",
|
||||
"send_tx_failed": "取引に失敗しました",
|
||||
@@ -1279,16 +1728,16 @@
|
||||
"sent_filter": "送信済み",
|
||||
"sent_type": "送信済み",
|
||||
"sent_upper": "送信済み",
|
||||
"set_label": "ラベルを設定...",
|
||||
"set_label": "ラベルを設定",
|
||||
"settings": "設定",
|
||||
"settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。",
|
||||
"settings_acrylic_level": "アクリルレベル:",
|
||||
"settings_address_book": "アドレス帳...",
|
||||
"settings_address_book": "アドレス帳…",
|
||||
"settings_auto_detected": "DRAGONX.conf から自動検出",
|
||||
"settings_auto_lock": "オートロック",
|
||||
"settings_auto_shield_desc": "透明資金を自動的にシールドアドレスに移動",
|
||||
"settings_auto_shield_funds": "透明資金を自動シールド",
|
||||
"settings_backup": "バックアップ...",
|
||||
"settings_backup": "バックアップ…",
|
||||
"settings_block_explorer_urls": "ブロックエクスプローラーURL",
|
||||
"settings_builtin": "内蔵",
|
||||
"settings_change_passphrase": "パスフレーズを変更",
|
||||
@@ -1310,19 +1759,20 @@
|
||||
"settings_encrypt_first_pin": "PIN を有効にするには、まずウォレットを暗号化してください",
|
||||
"settings_encrypt_wallet": "ウォレットを暗号化",
|
||||
"settings_explorer_hint": "URLには末尾のスラッシュを含めてください。txid/アドレスが追加されます。",
|
||||
"settings_export_all": "すべてエクスポート...",
|
||||
"settings_export_csv": "CSV エクスポート...",
|
||||
"settings_export_key": "鍵をエクスポート...",
|
||||
"settings_export_all": "すべてエクスポート…",
|
||||
"settings_export_csv": "CSV エクスポート…",
|
||||
"settings_export_key": "鍵をエクスポート…",
|
||||
"settings_gradient_bg": "グラデーション背景",
|
||||
"settings_gradient_desc": "テクスチャ背景を滑らかなグラデーションに置換",
|
||||
"settings_idle_after": "経過後",
|
||||
"settings_import_key": "秘密鍵をインポート...",
|
||||
"settings_import_viewkey": "閲覧鍵をインポート...",
|
||||
"settings_import_key": "秘密鍵をインポート…",
|
||||
"settings_import_viewkey": "閲覧鍵をインポート…",
|
||||
"settings_language_note": "注意:一部のテキストは更新に再起動が必要です",
|
||||
"settings_lock_now": "今すぐロック",
|
||||
"settings_locked": "ロック済み",
|
||||
"settings_merge_to_address": "アドレスにマージ...",
|
||||
"settings_merge_to_address": "アドレスにマージ…",
|
||||
"settings_noise_opacity": "ノイズ不透明度:",
|
||||
"settings_not_connected": "デーモンに接続されていません",
|
||||
"settings_not_encrypted": "暗号化されていません",
|
||||
"settings_not_found": "見つかりません",
|
||||
"settings_open_app_dir": "アプリフォルダを開く",
|
||||
@@ -1333,22 +1783,27 @@
|
||||
"settings_privacy": "プライバシー",
|
||||
"settings_quick_unlock_pin": "クイックアンロック PIN",
|
||||
"settings_reduce_transparency": "透明度を下げる",
|
||||
"settings_reloaded": "ディスクから設定を再読み込みしました",
|
||||
"settings_remove_encryption": "暗号化を解除",
|
||||
"settings_remove_pin": "PIN を削除",
|
||||
"settings_request_payment": "支払い請求...",
|
||||
"settings_request_payment": "支払い請求…",
|
||||
"settings_rescan_desc": "欠落したトランザクションのためにブロックチェーンを再スキャン",
|
||||
"settings_restart_daemon": "デーモンを再起動",
|
||||
"settings_rpc_connection": "RPC 接続",
|
||||
"settings_rpc_error_prefix": "RPCエラー: ",
|
||||
"settings_rpc_note": "注意:接続設定は通常 DRAGONX.conf から自動検出されます",
|
||||
"settings_rpc_ok": "RPC接続は正常です",
|
||||
"settings_save_shielded_desc": "z-addr トランザクションをローカルファイルに保存して表示",
|
||||
"settings_save_shielded_local": "シールドトランザクション履歴をローカルに保存",
|
||||
"settings_saved": "設定を保存しました",
|
||||
"settings_set_pin": "PIN を設定",
|
||||
"settings_shield_mining": "マイニングシールド...",
|
||||
"settings_shield_mining": "マイニングシールド…",
|
||||
"settings_solid_colors_desc": "ぼかし効果の代わりに単色を使用(アクセシビリティ)",
|
||||
"settings_theme_refreshed": "テーマ一覧を更新しました",
|
||||
"settings_tor_desc": "プライバシー向上のため全接続を Tor 経由にする",
|
||||
"settings_unlocked": "ロック解除",
|
||||
"settings_use_tor_network": "ネットワーク接続に Tor を使用",
|
||||
"settings_validate_address": "アドレス検証...",
|
||||
"settings_validate_address": "アドレス検証…",
|
||||
"settings_visual_effects": "視覚効果",
|
||||
"settings_wallet_file_size": "ウォレットファイルサイズ:%s",
|
||||
"settings_wallet_info": "ウォレット情報",
|
||||
@@ -1356,6 +1811,8 @@
|
||||
"settings_wallet_maintenance": "ウォレットメンテナンス",
|
||||
"settings_wallet_not_found": "ウォレットファイルが見つかりません",
|
||||
"settings_wallet_size_label": "ウォレットサイズ:",
|
||||
"settings_ztx_cleared": "Zトランザクション履歴を消去しました",
|
||||
"settings_ztx_not_found": "履歴ファイルが見つかりません",
|
||||
"setup_wizard": "セットアップウィザード",
|
||||
"share": "共有",
|
||||
"shield_check_status": "ステータスを確認",
|
||||
@@ -1403,17 +1860,34 @@
|
||||
"status": "ステータス",
|
||||
"stop_external": "外部デーモンを停止",
|
||||
"stop_mining": "マイニング停止",
|
||||
"stratum_allowip_hint": "マイナーの接続を許可するIPまたはCIDR(空欄=このコンピューターのみ)",
|
||||
"stratum_expose_warn": "許可したネットワークにマイニングポートを公開します — 信頼できるLANでのみ使用してください。",
|
||||
"stratum_host": "マイニングプールをホストする(stratum)",
|
||||
"stratum_host_hint": "マイナーはポート22769(RPCポート+1000)でこのコンピューターにRandomX stratumマイナーとして接続します。適用するにはデーモンを再起動してください。",
|
||||
"stratum_host_section": "マイニングプールのホスティング",
|
||||
"submitting_transaction": "取引を送信中...",
|
||||
"success": "成功",
|
||||
"summary": "概要",
|
||||
"sweep_button": "集約",
|
||||
"sweep_caveat": "鍵をインポートして、すべての資金をあなたのアドレスへ移す取引に署名します。鍵は残高ゼロでウォレットに残ります。",
|
||||
"sweep_current_theme_only": "現在のテーマのみ",
|
||||
"sweep_dest_label": "集約先",
|
||||
"sweep_dest_new": "新しいシールドアドレス(推奨)",
|
||||
"sweep_done": "完了 — 資金をあなたのアドレスに集約しました。",
|
||||
"sweep_to": "集約先:",
|
||||
"sweep_toggle": "ウォレットに集約(鍵は保持しない)",
|
||||
"sweep_tx": "取引:",
|
||||
"swin_connection_failed": "接続に失敗しました: ",
|
||||
"swin_connection_successful": "接続に成功しました!\ndragonxd バージョン: ",
|
||||
"swin_invalid_suffix": "(無効)",
|
||||
"swin_no_history_file_found": "履歴ファイルが見つかりません",
|
||||
"swin_rescan_failed": "再スキャンに失敗しました: ",
|
||||
"swin_rescan_started_from_block": "再スキャンを開始しました。開始ブロック ",
|
||||
"swin_rescan_to": " 〜 ",
|
||||
"swin_rpc_client_not_initialized": "RPCクライアントが初期化されていません",
|
||||
"swin_settings_saved": "設定を保存しました",
|
||||
"swin_theme_list_refreshed": "テーマ一覧を更新しました",
|
||||
"swin_ztx_history_cleared": "Zトランザクション履歴を消去しました",
|
||||
"switch_corrupt_body": "このウォレットは破損しているようです。ノードが開けませんでした。バックアップから復元するか、作り直すか、修復を試してください。",
|
||||
"switch_corrupt_repair": "修復を試す(salvage)",
|
||||
"switch_progress_background": "バックグラウンドで続行",
|
||||
@@ -1438,6 +1912,7 @@
|
||||
"theme": "テーマ",
|
||||
"theme_effects": "テーマ効果",
|
||||
"theme_language": "テーマと言語",
|
||||
"tile_click_to_open": "クリックして開く",
|
||||
"time_days_ago": "%d日前",
|
||||
"time_hours_ago": "%d時間前",
|
||||
"time_minutes_ago": "%d分前",
|
||||
@@ -1452,7 +1927,9 @@
|
||||
"to_upper": "宛先",
|
||||
"tools": "ツール",
|
||||
"tools_actions": "ツールとアクション...",
|
||||
"tools_actions_hdr": "ツールと操作",
|
||||
"total": "合計",
|
||||
"total_balance_label": "総残高",
|
||||
"transaction_id": "取引ID",
|
||||
"transaction_sent": "取引の送信に成功しました",
|
||||
"transaction_sent_msg": "取引を送信しました!",
|
||||
@@ -1474,7 +1951,7 @@
|
||||
"tt_auto_shield": "プライバシーのため透明残高を自動的にシールドアドレスに移動",
|
||||
"tt_backup": "wallet.dat のバックアップを作成",
|
||||
"tt_block_explorer": "ブラウザで DragonX ブロックエクスプローラーを開く",
|
||||
"tt_blur": "ぼかし量(0%% = オフ、100%% = 最大)",
|
||||
"tt_blur": "ぼかし量(0% = オフ、100% = 最大)",
|
||||
"tt_change_pass": "ウォレットの暗号化パスフレーズを変更",
|
||||
"tt_change_pin": "アンロック PIN を変更",
|
||||
"tt_chat_bubble_accent": "送信メッセージの吹き出しのアクセントカラー(または現在のテーマに従う)",
|
||||
@@ -1537,7 +2014,7 @@
|
||||
"tt_low_spec": "すべての重い視覚効果を無効化\\nホットキー:Ctrl+Shift+Down",
|
||||
"tt_merge": "複数の UTXO を一つのアドレスに統合",
|
||||
"tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始",
|
||||
"tt_noise": "グレインテクスチャ強度(0%% = オフ、100%% = 最大)",
|
||||
"tt_noise": "グレインテクスチャ強度(0% = オフ、100% = 最大)",
|
||||
"tt_open_app_dir": "ObsidianDragon フォルダ(設定、テーマ、ログ)をファイルマネージャーで開く",
|
||||
"tt_open_data_dir": "ファイルマネージャーでウォレットとブロックチェーンデータのフォルダを開きます",
|
||||
"tt_open_dir": "クリックしてファイルエクスプローラーで開く",
|
||||
@@ -1571,14 +2048,17 @@
|
||||
"tt_simple_bg": "背景にシンプルなグラデーションを使用\\nホットキー:Ctrl+Up",
|
||||
"tt_simple_bg_alt": "テーマ背景画像のグラデーション版を使用\\nホットキー:Ctrl+Up",
|
||||
"tt_stop_external": "このウォレット外で起動された\\nデーモンに接続する場合に適用",
|
||||
"tt_stratum_host": "このノード上でRandomX stratumプールサーバーを実行し、他のRandomXマイナーがこのコンピューターに接続できるようにします。v1.3.0以降のノードが必要で、適用にはデーモンの再起動が必要です。",
|
||||
"tt_sweep_current_theme_only": "全テーマを順に処理せず、有効なテーマのみをスイープします",
|
||||
"tt_test_conn": "デーモンへの RPC 接続を確認",
|
||||
"tt_theme_effects": "テーマごとのシマー、グロー、色相サイクル",
|
||||
"tt_theme_hotkey": "ホットキー:Ctrl+左/右でテーマを切り替え",
|
||||
"tt_tor": "匿名性のためにデーモン接続を Tor ネットワーク経由でルーティング",
|
||||
"tt_tx_url": "ブロックエクスプローラーでトランザクションを表示するためのベース URL",
|
||||
"tt_ui_opacity": "カードとサイドバーの不透明度(100%% = 完全不透明、低い = より透過)",
|
||||
"tt_ui_opacity": "カードとサイドバーの不透明度(100% = 完全不透明、低い = より透過)",
|
||||
"tt_validate": "DragonX アドレスが有効かどうかを確認",
|
||||
"tt_verbose": "詳細な接続診断、デーモン状態、\\nポート所有者情報をコンソールタブに記録",
|
||||
"tt_wallet_size_warn": "シールドウォレットはノートごとのウィットネスデータによって肥大化します — 多数のノートを1つのアドレスに統合するとサイズを抑えられます。事前にバックアップしてください。",
|
||||
"tt_wallets_button": "ウォレットファイルを一覧表示して切り替えます",
|
||||
"tt_website": "DragonX ウェブサイトを開く",
|
||||
"tt_window_opacity": "背景の不透明度(低い = デスクトップがウィンドウ越しに見える)",
|
||||
@@ -1629,6 +2109,7 @@
|
||||
"validate_not_mine": "このウォレットに属していません",
|
||||
"validate_ownership": "所有者:",
|
||||
"validate_results": "結果:",
|
||||
"validate_results_placeholder": "ここに結果が表示されます",
|
||||
"validate_shielded_type": "シールド(zアドレス)",
|
||||
"validate_status": "ステータス:",
|
||||
"validate_title": "アドレスを検証",
|
||||
@@ -1644,8 +2125,68 @@
|
||||
"view_on_explorer": "エクスプローラーで表示",
|
||||
"waiting_for_daemon": "デーモン接続を待機中...",
|
||||
"wallet": "ウォレット",
|
||||
"wallet_degraded_notify": "ウォレットは機能制限モードで開かれました。既存の資金は安全で使用可能ですが、新しいアドレスの作成とシールドは無効になっています。ウォレットを完全に修復するには、シードフレーズをバックアップしてそこから復元してください。",
|
||||
"wallet_diagnostics_hdr": "診断",
|
||||
"wallet_empty": "ウォレットは空です",
|
||||
"wallet_empty_hint": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
|
||||
"wallet_options_hdr": "オプション",
|
||||
"wallet_rebuild_failed": "再構築で有効なウォレットが生成されなかったため、変更は行われませんでした。ウォレットはそのままです。",
|
||||
"wallet_rebuild_install_failed": "再構築したウォレットをインストールできませんでした。現在のウォレットはそのまま残されています。",
|
||||
"wallet_rebuild_no_helper": "このビルドではウォレット再構築ヘルパーを利用できません。「復元」を使うか、手動で再構築してください。",
|
||||
"wallet_rebuild_no_source": "再構築可能な読み取り可能なウォレットが見つかりませんでした。変更は行われませんでした。",
|
||||
"wallet_rebuild_ok": "ウォレットデータベースを再構築しました — ノードが読み込み、残高の再スキャンを行っています。",
|
||||
"wallet_rebuild_started": "ウォレットファイルを修復しています — このウィンドウを閉じないでください。",
|
||||
"wallet_recovered_body": "何も削除されていません。元のウォレットは日付付きのバックアップファイルとしてこのコンピューターに保存されたままで、以下のどの選択肢もファイルをコピーまたは名前変更するだけで、削除することはありません。鍵が再生成されることはなく、再読み込みされるだけです。\n\n現在読み込まれている修復済みコピーには、直近の一部の取引が含まれていない可能性があるため、以下を完了して再スキャンが終わるまでは残高が少し低く見えることがあります。",
|
||||
"wallet_recovered_dismiss": "今はしない — 修復済みコピーのまま使う",
|
||||
"wallet_recovered_dismiss_sub": "ファイルは変更されません。ステータスバーからいつでもこの画面を再度開けます。どちらを選んでも、オリジナルは安全にバックアップされたままです。",
|
||||
"wallet_recovered_notify": "ウォレットファイルの修復が必要でした — オリジナルは安全にバックアップされています。アプリを開いてオプションを確認してください。",
|
||||
"wallet_recovered_open_folder": "ファイルを表示",
|
||||
"wallet_recovered_open_folder_sub": "ウォレットデータフォルダを開き、バックアップファイルを自分で確認できます — 何も変更されません。",
|
||||
"wallet_recovered_rebuild": "自動修復する(推奨)",
|
||||
"wallet_recovered_rebuild_sub": "最も内容の多いウォレットファイルから復元可能なすべてのレコードを読み込み、新しいクリーンなファイルを作成してから再起動します。最も徹底したオプションです — いずれにせよ現在のファイルは日付付きバックアップとして保存されます。",
|
||||
"wallet_recovered_restore": "代わりにオリジナルファイルを復元する",
|
||||
"wallet_recovered_restore_sub": "手つかずの最も内容の多いバックアップをそのまま元に戻し、再スキャンします — やや高速ですが、そのファイルにあった範囲までしか復元されません。いずれにせよ現在のファイルは日付付きバックアップとして保存されます。",
|
||||
"wallet_recovered_safety": "コインは安全です。",
|
||||
"wallet_recovered_title": "ウォレットファイルの簡単な修復が必要です",
|
||||
"wallet_recovered_warn": "アプリの起動時に、ウォレットファイルが整合性チェックに合格しないことが判明しました — これはアプリの更新後や、正常終了しなかった場合によく起こります。アプリはすでにデータを保護しています。古いファイルは退避され、修復済みのコピーを読み込んだので、行き詰まることはありません。",
|
||||
"wallet_recovery_decide_later": "後で決める",
|
||||
"wallet_recovery_decide_later_tip": "この画面を閉じ、現在読み込まれているコピーをそのまま使用します。何も変更されず、いつでも修復できます — ステータスバーに「ウォレットの修復が可能」というリンクが表示され続けます。",
|
||||
"wallet_recovery_details_label": "技術的な詳細を表示",
|
||||
"wallet_recovery_done": "完了",
|
||||
"wallet_recovery_failure_body": "修復は実行されましたが、その結果が検証に合格しませんでした(読み込めなかった、またはアドレスがありませんでした)。そのため何かを置き換える前に自動的に破棄されました。ウォレットはクリックする前とまったく同じ状態です — ディスク上には何も変更されていません。",
|
||||
"wallet_recovery_failure_title": "修復は完了しませんでした",
|
||||
"wallet_recovery_files_detail": "オリジナルのウォレットはまだここにあります — アプリはそれをデータフォルダ内の日付付きバックアップ(wallet.<番号>.bak)にリネームしただけで、何も削除していません。「修復」は最も内容の多いウォレットからすべてのレコードを読み込み、まったく新しいクリーンなファイルを作成します。「復元」は最も内容の多いバックアップをそのままコピーして元に戻します。現在読み込まれているコピーもバックアップとして保存されます。",
|
||||
"wallet_recovery_files_label": "自分のファイルはどうなりますか?",
|
||||
"wallet_recovery_notnow_short": "後で",
|
||||
"wallet_recovery_other_options": "他のオプション",
|
||||
"wallet_recovery_rebuild_card": "自動修復する",
|
||||
"wallet_recovery_rebuild_card_desc": "復元可能なすべてのレコードをクリーンなファイルに読み込んでから再起動します。最も徹底したオプションです。",
|
||||
"wallet_recovery_recommended": "推奨",
|
||||
"wallet_recovery_repair_go": "修復",
|
||||
"wallet_recovery_rescan_body": "ブロックチェーンを再スキャンして残高と取引履歴を再構築しています。数分かかる場合があります — 何もする必要はありません。実行中はノードを再起動しないでください。",
|
||||
"wallet_recovery_rescan_elapsed": "%s 経過",
|
||||
"wallet_recovery_rescan_size": "再構築されたウォレットは現在 %s で、まだ増加中です",
|
||||
"wallet_recovery_rescan_slow": "通常より時間がかかっていますが、大きなウォレットでは正常です。実行したままにしておいて問題ありません — 進捗は「詳細設定 ▸ コンソール」で確認できます。",
|
||||
"wallet_recovery_rescan_title": "ウォレットの修復を完了しています",
|
||||
"wallet_recovery_restore_card": "オリジナルを復元",
|
||||
"wallet_recovery_restore_card_desc": "手つかずの最も内容の多いバックアップをそのまま元に戻します — そのファイルにあった範囲までしか復元されません。",
|
||||
"wallet_recovery_restore_go": "復元",
|
||||
"wallet_recovery_success_body": "アプリは修復済みのウォレットを読み込み、残高を集計するために再スキャンしています — 数分かかる場合があります。読み込める限りすべてのレコードを読み込みますが、まれに破損したファイルでは、以前よりわずかに少ない(または多い)アドレスが復元されることがあります。再スキャンが完了したら、残高と履歴が正しいか確認してください。",
|
||||
"wallet_recovery_success_restore": "最も内容の多いバックアップファイルが元の場所に戻され、現在読み込み中です。アプリは再スキャンしています。そのまま復元されているため、そのファイルにあった範囲まで正確です — 再スキャンが完了したら残高を確認してください。",
|
||||
"wallet_recovery_try_other": "別のオプションを試す",
|
||||
"wallet_recovery_whats_happened": "何が起きましたか?",
|
||||
"wallet_recovery_working_label": "処理中",
|
||||
"wallet_restore_bad_backup": "バックアップのウォレットファイルは読み込めないようです。そのため復元されませんでした — 現在のウォレットは変更されていません。ご自身のバックアップから復元してください。",
|
||||
"wallet_restore_busy": "ノードは再起動中でビジー状態です — しばらくしてからもう一度お試しください。",
|
||||
"wallet_restore_copy_failed": "バックアップウォレットをインストールできませんでした。現在のウォレットはそのまま残されました。",
|
||||
"wallet_restore_move_failed": "現在のウォレットを退避できませんでした — 何も変更されていません。",
|
||||
"wallet_restore_no_backup": "復元できる wallet.<timestamp>.bak が見つかりませんでした。何も変更されていません。",
|
||||
"wallet_restore_no_restart": "オリジナルのウォレットは復元されましたが、ノードは再起動しませんでした — 設定から起動してください。",
|
||||
"wallet_restore_ok": "オリジナルのウォレットを復元しました。ノードが読み込み中です。",
|
||||
"wallet_restore_started": "オリジナルファイルを復元しています — このウィンドウを閉じないでください。",
|
||||
"wallet_restore_stop_failed": "ノードが時間内に停止しなかったため、何も変更されていません。もう一度お試しください。",
|
||||
"wallet_size_consolidate": "ノートを統合…",
|
||||
"wallet_size_warn": "このウォレットファイルは大きくなっています。ノートを統合すると、さらなる増加を抑えられます。",
|
||||
"wallets_active": "アクティブ",
|
||||
"wallets_add": "フォルダを追加",
|
||||
"wallets_add_folder": "ウォレットファイルを検索する別のフォルダを追加:",
|
||||
@@ -1688,6 +2229,7 @@
|
||||
"wallets_open_inplace_tt": "このウォレットをその場で開きます — データディレクトリにリンク(コピーなし)",
|
||||
"wallets_remove_folder": "このフォルダーのスキャンを停止",
|
||||
"wallets_reveal": "フォルダを開く",
|
||||
"wallets_scan_folder": "別のフォルダをスキャンしてウォレットを検索…",
|
||||
"wallets_scanned_folders": "スキャン対象フォルダー:",
|
||||
"wallets_sort_addresses": "アドレス",
|
||||
"wallets_sort_asc": "昇順(古い/少ない/小さい順)",
|
||||
@@ -1757,18 +2299,30 @@
|
||||
"wiz_welcome_sub": "いくつか選ぶだけでフルノードの準備が整います。",
|
||||
"wiz_welcome_title": "ObsidianDragon へようこそ!",
|
||||
"wizard_daemon_start_failed": "デーモンの起動に失敗しました — 自動的に再試行されます",
|
||||
"xmrig_active_tt": "現在インストールされている(有効な)マイナー",
|
||||
"xmrig_browse_releases": "すべてのリリースを表示…",
|
||||
"xmrig_checking": "最新のマイナーを確認中…",
|
||||
"xmrig_current": "現在:",
|
||||
"xmrig_downgrade_note": "別のバージョンのマイナーをインストールすると、現在のものが置き換わります。まずマイニングを停止してください。変更は次回の起動時に反映されます。",
|
||||
"xmrig_download_install": "ダウンロードしてインストール",
|
||||
"xmrig_downloading": "ダウンロード中…",
|
||||
"xmrig_install_this": "このバージョンをインストール",
|
||||
"xmrig_installed": "インストール済み:",
|
||||
"xmrig_installed_ok": "マイナーをインストールしました",
|
||||
"xmrig_installing": "インストール中…",
|
||||
"xmrig_latest": "最新:",
|
||||
"xmrig_latest_badge": "最新",
|
||||
"xmrig_latest_tt": "利用可能な最新バージョン",
|
||||
"xmrig_loading_releases": "リリースを読み込み中…",
|
||||
"xmrig_no_notes": "このバージョンのリリースノートはありません。",
|
||||
"xmrig_none": "なし",
|
||||
"xmrig_prerelease_tt": "プレリリース/テストビルド",
|
||||
"xmrig_reinstall": "再インストール",
|
||||
"xmrig_released": "リリース日:",
|
||||
"xmrig_releases": "xmrig リリース",
|
||||
"xmrig_status_current": "このバージョンはインストール済みです",
|
||||
"xmrig_status_latest": "最新バージョン",
|
||||
"xmrig_status_older": "インストール済みのバージョンより古い",
|
||||
"xmrig_stop_mining_first": "マイナーを更新する前にマイニングを停止してください。",
|
||||
"xmrig_unavailable_body": "このプラットフォーム向けのマイナービルドは利用できません。",
|
||||
"xmrig_unavailable_title": "マイナーの更新は利用できません",
|
||||
|
||||
+569
-17
@@ -19,6 +19,7 @@
|
||||
"about_license_text": "본 소프트웨어는 GNU General Public License v3 (GPLv3) 하에 배포됩니다. 라이선스 조건에 따라 자유롭게 사용, 수정 및 배포할 수 있습니다.",
|
||||
"about_peers_count": "%zu 피어",
|
||||
"about_release": "릴리스",
|
||||
"about_source": "소스",
|
||||
"about_title": "ObsidianDragon 정보",
|
||||
"about_version": "버전:",
|
||||
"about_website": "웹사이트",
|
||||
@@ -60,7 +61,84 @@
|
||||
"amount_label": "금액:",
|
||||
"animate_avatars": "아바타 애니메이션",
|
||||
"appearance": "외관",
|
||||
"appx_back": "뒤로",
|
||||
"appx_back_up_seed_phrase_title": "시드 문구 백업",
|
||||
"appx_birthday_block_height": "생성 시점(블록 높이): %llu — 이것도 함께 백업하세요.",
|
||||
"appx_blockchain_data_deleted": "블록체인 데이터를 삭제했습니다(%d개 항목). 네트워크에서 다시 동기화하기 위해 데몬을 재시작합니다.",
|
||||
"appx_blockchain_maintenance_in_progress": "블록체인 유지 관리 작업이 이미 진행 중입니다.",
|
||||
"appx_blockchain_rescan_complete": "블록체인 다시 스캔 완료",
|
||||
"appx_bootstrap_complete_reconciling": "부트스트랩 완료 — 새 체인 데이터와 지갑을 대조하는 중입니다.",
|
||||
"appx_cancel": "취소",
|
||||
"appx_cleaning_up": "정리하는 중...",
|
||||
"appx_confirm_your_backup": "백업 확인",
|
||||
"appx_copied_clipboard_autoclears": "복사됨 — 클립보드가 45초 후 자동으로 지워집니다",
|
||||
"appx_copy": "복사",
|
||||
"appx_could_not_start_restore": "복구를 시작할 수 없습니다",
|
||||
"appx_create_failed_prefix": "생성 실패: ",
|
||||
"appx_creating_your_wallet": "지갑을 생성하는 중…",
|
||||
"appx_daemon_error": "데몬 오류",
|
||||
"appx_daemon_reinstall_in_progress": "데몬 재설치가 이미 진행 중입니다.",
|
||||
"appx_disconnecting": "연결 해제 중...",
|
||||
"appx_done": "완료",
|
||||
"appx_dragonxd_output": "dragonxd 출력",
|
||||
"appx_encrypting_wallet": "지갑 암호화 중...",
|
||||
"appx_fullnode_lifecycle_unavailable_lite": "풀노드 수명 주기 작업은 라이트 빌드에서 사용할 수 없습니다",
|
||||
"appx_installing_bundled_daemon": "번들 데몬을 설치하는 중 — 노드가 중지, 업데이트 후 다시 시작됩니다...",
|
||||
"appx_invalid_payment_uri_prefix": "잘못된 결제 URI: ",
|
||||
"appx_ive_written_it_down": "적어 두었습니다",
|
||||
"appx_keep_node_running_and_quit": "노드 유지하고 종료",
|
||||
"appx_last_block_n": "마지막 블록: %d",
|
||||
"appx_last_used_wallet_not_found_prefix": "마지막으로 사용한 지갑 파일(",
|
||||
"appx_last_used_wallet_not_found_suffix": ")을 찾을 수 없어 기본 지갑을 대신 열었습니다. 옮기셨다면 복원한 후 지갑 목록에서 다시 전환하세요.",
|
||||
"appx_low_spec_mode_disabled": "저사양 모드 비활성화됨",
|
||||
"appx_low_spec_mode_enabled": "저사양 모드 활성화됨",
|
||||
"appx_miner_stopped_prefix": "채굴기 중지됨: ",
|
||||
"appx_miner_stopped_unexpectedly": "채굴기가 예기치 않게 중지되었습니다.",
|
||||
"appx_n_min_n_sec": "%d분 %d초",
|
||||
"appx_n_seconds": "%d초",
|
||||
"appx_no_bundled_daemon_to_install": "이 빌드에는 설치할 번들 데몬이 없습니다",
|
||||
"appx_no_embedded_daemon_to_install": "이 빌드에는 설치할 내장 데몬이 없습니다",
|
||||
"appx_node_busy_restarting": "노드가 재시작 중입니다 — 잠시 후 다시 시도하세요.",
|
||||
"appx_node_rebuilding_witness_cache": "노드가 위트니스 캐시를 재구성하는 중입니다",
|
||||
"appx_not_next_word": " — 다음 단어가 아닙니다",
|
||||
"appx_payment_request_loaded": "결제 요청을 불러왔습니다",
|
||||
"appx_pool_miner_connected_and_hashing": "풀 채굴기가 연결되어 해싱 중입니다.",
|
||||
"appx_progress_n_of_n": "진행 상황: %d / %d",
|
||||
"appx_rebuilding_sapling_note_witnesses": "Sapling 노트 위트니스 재구성 중…",
|
||||
"appx_rebuilding_witness_cache_blocks_left": "위트니스 캐시 재구성 중 %.0f%% — %d개 블록 남음",
|
||||
"appx_rebuilding_witness_cache_pct": "위트니스 캐시 재구성 중 %.0f%%",
|
||||
"appx_recovery_phrase_word_count": "복구 문구는 24개 단어여야 합니다 — 현재 %d개입니다.",
|
||||
"appx_restarting_daemon_rescan_flag": "-rescan 플래그로 데몬을 다시 시작하는 중...",
|
||||
"appx_restarting_daemon_zapwallettxes": "-zapwallettxes=2로 데몬을 다시 시작하는 중(지갑 복구)...",
|
||||
"appx_restoring_your_wallet": "지갑을 복구하는 중…",
|
||||
"appx_seed_backup_warning": "이 24개 단어는 지갑을 복구할 수 있는 유일한 방법입니다. 순서대로 적어 오프라인에 보관하고 절대 공유하지 마세요. 잃어버리면 자금을 영원히 되찾을 수 없습니다.",
|
||||
"appx_seed_not_backed_up_warning": "시드를 아직 백업하지 않았습니다 — 자금을 잃을 수 있습니다. 그래도 건너뛰시겠습니까?",
|
||||
"appx_sending_stop_command_to_daemon": "데몬에 중지 명령을 보내는 중...",
|
||||
"appx_setting_initial_sapling_witnesses": "초기 Sapling 위트니스 설정 중 %.0f%%",
|
||||
"appx_shutdown_complete": "종료 완료",
|
||||
"appx_simple_background_disabled": "단순 배경 비활성화됨",
|
||||
"appx_simple_background_enabled": "단순 배경 활성화됨",
|
||||
"appx_skip": "건너뛰기",
|
||||
"appx_skip_anyway": "그래도 건너뛰기",
|
||||
"appx_still_status_prefix": "아직 \"",
|
||||
"appx_still_status_suffix": "\" 상태입니다 — 지금 강제 종료하면 체인 데이터가 손상될 수 있습니다.",
|
||||
"appx_stop_anyway_and_quit": "그래도 중지하고 종료",
|
||||
"appx_stopping_daemon_deleting_blockchain": "데몬을 중지하고 블록체인 데이터를 삭제하는 중...",
|
||||
"appx_stopping_node_discards_rebuild": "지금 노드를 중지하면 진행 중인 재구성이 취소되며, 다음에 지갑을 열 때 다시 시작됩니다(몇 분 소요). 대신 노드를 계속 실행할 수 있습니다.",
|
||||
"appx_stopping_pool_miner": "풀 채굴기를 중지하는 중...",
|
||||
"appx_syncing_pct_block_n_of_n": "동기화 중 %.1f%% — 블록 %d / %d",
|
||||
"appx_tap_words_in_order": "저장한 내용을 확인하도록 단어를 올바른 순서대로 탭하세요.",
|
||||
"appx_theme_effects_disabled": "테마 효과 비활성화됨",
|
||||
"appx_theme_effects_enabled": "테마 효과 활성화됨",
|
||||
"appx_theme_prefix": "테마: ",
|
||||
"appx_use_settings_restart_daemon_hint": "설정 > 데몬 다시 시작을 사용해 다시 시도하세요",
|
||||
"appx_waiting_for_daemon_to_encrypt_wallet": "데몬이 지갑을 암호화하기를 기다리는 중...",
|
||||
"appx_wallet_created_and_backed_up": "지갑이 생성되고 백업되었습니다.",
|
||||
"appx_wallet_open_failed_prefix": "지갑 열기 실패: ",
|
||||
"auto_shield": "채굴 자동 차폐",
|
||||
"autoshield_by_node": "노드가 채굴한 코인베이스를 자동으로 보호 처리합니다.",
|
||||
"autoshield_off_backup_seed": "채굴한 코인베이스가 아직 자동으로 보호 처리되지 않고 있습니다. 노드가 안전하게 이를 활성화할 수 있도록 시드 문구를 백업하세요 (설정 → 백업 & 데이터).",
|
||||
"autoshield_off_generic": "현재 노드가 채굴한 코인베이스를 자동으로 보호 처리하지 않고 있습니다.",
|
||||
"av_intro": "채굴 소프트웨어는 종종 잠재적으로 원치 않는 항목으로 표시됩니다. 풀 채굴을 활성화하려면 다음 단계를 따르세요:",
|
||||
"av_open_security": "Windows 보안 열기",
|
||||
"av_step1": "1단계: 제외 추가",
|
||||
@@ -74,6 +152,9 @@
|
||||
"av_title": "Windows Defender가 채굴기를 차단했습니다",
|
||||
"available": "사용 가능",
|
||||
"backup_backing_up": "백업 중...",
|
||||
"backup_col_backup": "백업",
|
||||
"backup_col_export": "내보내기",
|
||||
"backup_col_import": "가져오기 및 복원",
|
||||
"backup_create": "백업 생성",
|
||||
"backup_created": "지갑 백업이 생성되었습니다",
|
||||
"backup_data": "백업 및 데이터",
|
||||
@@ -92,8 +173,22 @@
|
||||
"balance": "잔액",
|
||||
"balance_history_collecting": "잔액 내역 — 데이터 수집 중...",
|
||||
"balance_layout": "잔액 레이아웃",
|
||||
"balance_layout_switched": "레이아웃: %s",
|
||||
"balance_mining_rate": "채굴 중 %s",
|
||||
"balance_shielded_fmt": "차폐: %.8f",
|
||||
"balance_syncing_pct": "동기화 중 %.1f%%",
|
||||
"balance_transparent_fmt": "투명: %.8f",
|
||||
"baltab_market": "시세",
|
||||
"baltab_market_price_4dp": "시세: $%.4f",
|
||||
"baltab_market_price_8dp": "시세: $%.8f",
|
||||
"baltab_pct_change_24h": "%s%.1f%% 24시간",
|
||||
"baltab_pct_of_total_zaddr": "전체의 %.0f%% · Z-addr %d개",
|
||||
"baltab_shielded": "실드됨",
|
||||
"baltab_shielded_amount": "실드됨 %.8f",
|
||||
"baltab_t_addresses_count": "T-address %d개",
|
||||
"baltab_total_balance": "총 잔액",
|
||||
"baltab_transparent": "투명",
|
||||
"baltab_transparent_amount": "투명 %.8f",
|
||||
"ban": "차단",
|
||||
"banned_peers": "차단된 피어",
|
||||
"block": "블록",
|
||||
@@ -101,6 +196,12 @@
|
||||
"block_click_copy": "복사하려면 클릭",
|
||||
"block_click_next": "클릭하여 다음 블록 보기",
|
||||
"block_click_prev": "클릭하여 이전 블록 보기",
|
||||
"block_db_reindex_body": "이 현상은 보통 데몬 업데이트로 디스크 저장 형식이 바뀌었거나, 블록 색인이 손상된 경우에 발생합니다. 지갑과 코인은 안전합니다 — 노드가 체인을 불러오지 못할 뿐이라 잔액이 0으로 표시됩니다.\n\n재구축은 기존 블록 파일을 다시 읽어 들이며 시간이 다소 걸릴 수 있습니다(지갑도 다시 스캔합니다). 아무것도 다운로드하지 않습니다.",
|
||||
"block_db_reindex_confirm": "블록 데이터베이스 재구축",
|
||||
"block_db_reindex_notify": "노드가 블록 데이터베이스를 읽을 수 없습니다(흔히 데몬 업데이트 후 발생). 잔액을 복구하려면 재구축하세요 — 안내창을 확인하거나 설정 › 노드로 이동하세요.",
|
||||
"block_db_reindex_started": "블록에서 블록 데이터베이스를 재구축하는 중입니다 — 시간이 다소 걸릴 수 있습니다.",
|
||||
"block_db_reindex_title": "블록 데이터베이스를 재구축할까요?",
|
||||
"block_db_reindex_warn": "노드가 블록 데이터베이스를 읽을 수 없습니다.",
|
||||
"block_explorer": "블록 탐색기",
|
||||
"block_get_info": "블록 정보 조회",
|
||||
"block_hash": "블록 해시:",
|
||||
@@ -132,6 +233,7 @@
|
||||
"bootstrap_verifying": "체크섬 확인 중...",
|
||||
"bootstrap_wallet_protected": "(wallet.dat 보호됨)",
|
||||
"bootstrap_warning": "기존 블록 데이터(blocks, chainstate, notarizations)가 삭제되고 교체됩니다. wallet.dat는 수정되거나 삭제되지 않습니다.",
|
||||
"byte_count_fmt": "%zu / %zu 바이트",
|
||||
"cancel": "취소",
|
||||
"change_pass_confirm": "새 암호 확인:",
|
||||
"change_pass_current": "현재 암호:",
|
||||
@@ -147,6 +249,10 @@
|
||||
"chat_accent_theme": "테마",
|
||||
"chat_add_contact": "연락처 추가",
|
||||
"chat_awaiting_key": "답장 대기 중",
|
||||
"chat_blocked_desc": "차단된 대화는 삭제되며, 차단을 해제하기 전까지 기존 및 새 메시지가 모두 무시됩니다. 차단을 해제하면 체인에서 대화를 다시 가져옵니다.",
|
||||
"chat_blocked_manage": "차단됨",
|
||||
"chat_blocked_title": "차단된 대화",
|
||||
"chat_blocked_toast": "대화를 삭제하고 차단했습니다",
|
||||
"chat_bubble_minimal": "미니멀",
|
||||
"chat_bubble_rounded": "둥근",
|
||||
"chat_bubble_square": "각진",
|
||||
@@ -159,6 +265,16 @@
|
||||
"chat_contact_added": "연락처 추가됨 — 연락처에서 이름을 변경하세요",
|
||||
"chat_contact_request": "연락 요청",
|
||||
"chat_copy_address_tip": "클릭하여 주소 복사",
|
||||
"chat_delete": "대화 삭제",
|
||||
"chat_delete_block": "삭제 및 차단",
|
||||
"chat_delete_body_prefix": "다음 상대와의 대화 로컬 사본을 삭제할까요: ",
|
||||
"chat_delete_body_suffix": "?",
|
||||
"chat_delete_confirm": "삭제",
|
||||
"chat_delete_failed": "대화를 삭제하지 못했습니다 — 변경된 내용이 없습니다.",
|
||||
"chat_delete_local_note": "이 기기에만 영향을 줍니다 — 메시지는 블록체인에 남아 있고 상대방도 자신의 사본을 계속 보관합니다.",
|
||||
"chat_delete_revive_note": "\"삭제\"는 이 기기의 기록만 지웁니다. 상대방이 다시 메시지를 보내면 대화가 되살아납니다.",
|
||||
"chat_delete_title": "대화를 삭제할까요?",
|
||||
"chat_deleted_toast": "대화를 삭제했습니다",
|
||||
"chat_density_comfortable": "편안하게",
|
||||
"chat_density_compact": "촘촘하게",
|
||||
"chat_emoji_color": "컬러",
|
||||
@@ -182,6 +298,7 @@
|
||||
"chat_new_button": "새 채팅",
|
||||
"chat_new_message": "메시지",
|
||||
"chat_new_message_toast": "새 암호화 채팅 메시지",
|
||||
"chat_new_needs_zaddr": "채팅에는 보호 (z) 주소가 필요합니다 — 투명 (t) 주소는 암호화된 메시지를 받을 수 없습니다.",
|
||||
"chat_new_send": "요청 보내기",
|
||||
"chat_new_title": "새 채팅",
|
||||
"chat_new_zaddr": "받는 사람 z-주소",
|
||||
@@ -227,6 +344,8 @@
|
||||
"chat_ts_24h": "24시간",
|
||||
"chat_ts_global": "전역 설정 따르기",
|
||||
"chat_ts_global_short": "전역",
|
||||
"chat_unblock": "차단 해제",
|
||||
"chat_unblocked_toast": "대화 차단을 해제했습니다",
|
||||
"chat_unhide": "다시 표시",
|
||||
"chat_unmute": "음소거 해제",
|
||||
"chat_verify_key": "신원 키 — 비교하여 확인",
|
||||
@@ -279,6 +398,7 @@
|
||||
"console": "콘솔",
|
||||
"console_accents": "색상 강조",
|
||||
"console_app": "앱",
|
||||
"console_auto_focus": "열 때 입력창에 포커스",
|
||||
"console_auto_scroll": "자동 스크롤",
|
||||
"console_available_commands": "사용 가능한 명령어:",
|
||||
"console_backend_reference": "백엔드 명령어 참조",
|
||||
@@ -373,11 +493,15 @@
|
||||
"console_tab_completion": "Tab으로 자동 완성",
|
||||
"console_text_colors": "텍스트 색상",
|
||||
"console_toggle_accents": "줄 색상 강조 전환",
|
||||
"console_toggle_auto_focus": "콘솔 탭을 열 때 명령 입력창에 커서를 놓습니다",
|
||||
"console_toggle_text_color": "줄 텍스트 색상 전환",
|
||||
"console_type_help": "'help'를 입력하여 사용 가능한 명령어 보기",
|
||||
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
|
||||
"console_zoom_in": "확대",
|
||||
"console_zoom_out": "축소",
|
||||
"consolidate_desc": "여러 개의 작은 입력을 하나의 보호된 노트로 합칩니다. 노트 수가 줄면 지갑 파일이 작아지고 프라이버시도 향상됩니다.",
|
||||
"consolidate_funds_btn": "통합",
|
||||
"consolidate_title": "자금 통합",
|
||||
"contact_avatar": "아바타",
|
||||
"contact_avatar_bad_image": "그 이미지를 불러올 수 없습니다.",
|
||||
"contact_avatar_badge": "배지",
|
||||
@@ -407,6 +531,7 @@
|
||||
"contacts_shape_square": "사각형",
|
||||
"contacts_shape_tab": "왼쪽 탭",
|
||||
"copied": "복사됨!",
|
||||
"copied_to_clipboard": "클립보드에 복사됨",
|
||||
"copy": "복사",
|
||||
"copy_address": "전체 주소 복사",
|
||||
"copy_error": "오류 복사",
|
||||
@@ -419,11 +544,16 @@
|
||||
"daemon_bundled": "번들",
|
||||
"daemon_install_bundled": "번들 버전 설치",
|
||||
"daemon_installed": "설치됨",
|
||||
"daemon_maintenance_label": "유지 관리",
|
||||
"daemon_none_bundled": "이 빌드에 없음",
|
||||
"daemon_not_installed": "설치되지 않음",
|
||||
"daemon_status_diff": "버전이 다름",
|
||||
"daemon_status_differ": "설치된 바이너리가 번들 버전과 다릅니다.",
|
||||
"daemon_status_match": "설치된 바이너리가 번들 버전과 일치합니다.",
|
||||
"daemon_status_missing": "설치된 데몬 없음 — 번들 버전을 설치하세요.",
|
||||
"daemon_status_none": "설치되지 않음",
|
||||
"daemon_status_ok": "최신 상태",
|
||||
"daemon_update_active_tt": "현재 설치되어(활성화되어) 있는 노드",
|
||||
"daemon_update_available": "새 노드 버전이 있습니다",
|
||||
"daemon_update_body": "이 지갑 빌드에는 현재 설치된 것보다 새로운 DragonX 노드가 번들로 포함되어 있습니다. 업데이트하면 설치된 dragonxd(및 dragonx-cli/dragonx-tx)가 교체된 후, 새 버전이 적용되도록 노드를 중지했다가 다시 시작합니다. 새 노드는 기존 노드에 없는 기능(예: 시드 문구 지원)을 추가할 수 있으므로 권장합니다.",
|
||||
"daemon_update_browse": "모든 릴리스 보기…",
|
||||
@@ -433,18 +563,28 @@
|
||||
"daemon_update_download_install": "다운로드 및 설치",
|
||||
"daemon_update_downloading": "다운로드 중…",
|
||||
"daemon_update_failed": "업데이트 실패",
|
||||
"daemon_update_install_this": "이 버전 설치",
|
||||
"daemon_update_installed": "설치됨:",
|
||||
"daemon_update_installed_ok": "노드가 설치되었습니다",
|
||||
"daemon_update_installing": "설치 중…",
|
||||
"daemon_update_keep": "현재 버전 유지",
|
||||
"daemon_update_later": "나중에",
|
||||
"daemon_update_latest": "최신:",
|
||||
"daemon_update_latest_badge": "최신",
|
||||
"daemon_update_latest_tt": "사용 가능한 최신 버전",
|
||||
"daemon_update_loading": "릴리스 불러오는 중…",
|
||||
"daemon_update_no_notes": "이 버전에는 릴리스 노트가 없습니다.",
|
||||
"daemon_update_now": "지금 업데이트",
|
||||
"daemon_update_prerelease_tt": "사전 릴리스 / 테스트 빌드",
|
||||
"daemon_update_prompt_title": "노드 데몬을 업데이트하시겠습니까?",
|
||||
"daemon_update_reinstall": "다시 설치",
|
||||
"daemon_update_released": "출시일:",
|
||||
"daemon_update_restart_note": "새 버전을 실행하려면 데몬을 재시작하세요.",
|
||||
"daemon_update_restart_now": "지금 데몬 재시작",
|
||||
"daemon_update_safe": "지갑, 키, 블록체인 데이터는 그대로이며 — 데몬 프로그램 파일만 교체됩니다. 커스텀 노드를 일부러 실행 중이라면 현재 버전 유지를 선택하세요.",
|
||||
"daemon_update_status_current": "이 버전이 설치되어 있습니다",
|
||||
"daemon_update_status_latest": "최신 버전",
|
||||
"daemon_update_status_older": "설치된 버전보다 이전 버전입니다",
|
||||
"daemon_update_title": "노드 데몬을 업데이트할까요?",
|
||||
"daemon_update_unavailable_body": "이 플랫폼에 사용할 수 있는 노드 빌드가 없습니다.",
|
||||
"daemon_update_unavailable_title": "노드 업데이트를 사용할 수 없음",
|
||||
@@ -453,12 +593,20 @@
|
||||
"daemon_update_verify_note": "다운로드는 설치 전에 릴리스에 게시된 SHA-256과 고정된 ed25519 서명으로 검증됩니다.",
|
||||
"daemon_update_verifying": "확인 중…",
|
||||
"daemon_update_version": "버전:",
|
||||
"daemon_updates_label": "업데이트",
|
||||
"daemon_version": "데몬",
|
||||
"dark": "다크",
|
||||
"data_stale_prefix": "업데이트",
|
||||
"data_stale_tooltip": "잔액이 오래되었을 수 있습니다 — 지갑이 최근에 업데이트를 받지 못했습니다. 노드 연결을 확인하세요.",
|
||||
"date": "날짜",
|
||||
"date_label": "날짜:",
|
||||
"debug_gate_confirm": "디버그 옵션 표시",
|
||||
"debug_gate_incorrect": "PIN 또는 암호가 올바르지 않습니다.",
|
||||
"debug_gate_pass_prompt": "계속하려면 지갑 암호를 입력하세요:",
|
||||
"debug_gate_pin_prompt": "계속하려면 잠금 해제 PIN을 입력하세요:",
|
||||
"debug_gate_title": "디버그 옵션을 표시할까요?",
|
||||
"debug_gate_verifying": "확인 중…",
|
||||
"debug_gate_warning": "이 옵션들은 고급 진단 및 노드 튜닝 옵션입니다. 민감한 정보를 노출하거나 노드 동작 방식을 변경할 수 있습니다. 무엇을 하는지 정확히 알고 있을 때만 계속하세요.",
|
||||
"debug_logging": "디버그 로깅",
|
||||
"decrypt_desc": "지갑을 내보내고 데몬을 새 암호화되지 않은 지갑으로 다시 시작한 후 모든 키를 다시 가져옵니다. 지갑 크기에 따라 몇 분 정도 걸릴 수 있습니다.",
|
||||
"decrypt_error_title": "복호화 실패",
|
||||
@@ -481,10 +629,22 @@
|
||||
"disconnected": "연결 끊김",
|
||||
"dismiss": "닫기",
|
||||
"display": "디스플레이",
|
||||
"done": "완료",
|
||||
"download": "다운로드",
|
||||
"download_bootstrap": "부트스트랩 다운로드",
|
||||
"dragonx_green": "DragonX(그린)",
|
||||
"edit": "편집",
|
||||
"empty_wallet_keys_suffix": "개 키",
|
||||
"empty_wallet_open_manager": "지갑 관리자 열기",
|
||||
"empty_wallet_restore": "내 지갑 복원",
|
||||
"empty_wallet_salvage_body": "이 지갑이 비어 있는 것은 이전의 자동 복구가 원본 지갑을 백업으로 따로 보관했기 때문입니다. 코인은 거의 확실히 그 백업에 있으며 사라지지 않았습니다. 복원하면 자금을 다시 불러올 수 있습니다. 아무것도 삭제되지 않으며, 현재 파일은 먼저 따로 보관됩니다.",
|
||||
"empty_wallet_salvage_headline": "코인은 백업 파일에 안전하게 보관되어 있습니다.",
|
||||
"empty_wallet_salvage_title": "지갑이 복구되었을 수 있습니다",
|
||||
"empty_wallet_warning_body": "이 지갑에는 주소도 자금도 없지만, DragonX 폴더의 다른 지갑 파일에 키가 들어 있습니다. 코인은 대부분 그 안에 있으며 사라진 것이 아닙니다. 지갑 관리자를 열어 자금이 있는 지갑으로 전환하세요.",
|
||||
"empty_wallet_warning_dismiss": "이 지갑에 대해 다시 경고하지 않기",
|
||||
"empty_wallet_warning_dismiss_tip": "현재 지갑 파일에 대해서만 이 경고를 중지합니다. 나중에 다른 빈 지갑으로 전환하면 다시 경고할 수 있습니다.",
|
||||
"empty_wallet_warning_headline": "잘못된 지갑을 열었을 수 있습니다.",
|
||||
"empty_wallet_warning_title": "이 지갑은 비어 있습니다",
|
||||
"enc_confirm": "확인:",
|
||||
"enc_desc": "지갑을 암호화하면 암호로 개인 키를 보호합니다. 암호화 후 데몬이 다시 시작됩니다.",
|
||||
"enc_encrypting": "지갑을 암호화하는 중...",
|
||||
@@ -523,6 +683,7 @@
|
||||
"explorer_tx_outputs": "출력",
|
||||
"explorer_tx_size": "크기",
|
||||
"explorer_txid": "TxID:",
|
||||
"explorer_urls_hdr": "URL",
|
||||
"export": "내보내기",
|
||||
"export_csv": "CSV 내보내기",
|
||||
"export_keys_btn": "키 내보내기",
|
||||
@@ -550,6 +711,151 @@
|
||||
"export_viewing_key": "조회 키 내보내기",
|
||||
"failed_create_shielded": "차폐 주소 생성 실패",
|
||||
"failed_create_transparent": "투명 주소 생성 실패",
|
||||
"faq": "FAQ",
|
||||
"faq_d_mgmt_1_a": "내장 노드는 지갑과 함께 자동으로 시작되고 적절한 시점에 중지되므로, 보통 직접 관리할 필요가 없습니다. 고급 제어 기능(데몬 재시작 포함)은 설정 > 노드 및 보안에 있습니다.",
|
||||
"faq_d_mgmt_1_q": "노드를 시작하거나 중지하려면 어떻게 하나요?",
|
||||
"faq_d_mgmt_2_a": "직접 실행한 노드에 지갑을 연결한 경우, 이 옵션은 지갑을 닫을 때 해당 노드도 함께 종료할지를 결정합니다. 지갑을 닫은 후에도 노드를 계속 실행하고 싶다면 꺼두세요.",
|
||||
"faq_d_mgmt_2_q": "'종료 시 외부 데몬 닫기'는 무엇을 하나요?",
|
||||
"faq_d_mgmt_3_a": "노드와 지갑 데이터는 사용자 프로필 아래 DragonX 데이터 디렉터리에 저장됩니다(예: Windows에서는 AppData, Linux/macOS에서는 홈 폴더). 블록체인이 가장 많은 용량을 차지하므로, 성장할 만큼 충분한 여유 디스크 공간을 확보하세요.",
|
||||
"faq_d_mgmt_3_q": "블록체인과 지갑 데이터는 어디에 저장되나요?",
|
||||
"faq_d_mgmt_title": "노드 관리",
|
||||
"faq_d_mine_1_a": "마이닝 탭에서 CPU로 채굴할 수 있습니다. DragonX는 일반 프로세서용으로 설계된 RandomX 알고리즘을 사용합니다. 스레드 수를 설정하고 시작하면 보상이 지갑에 쌓입니다.",
|
||||
"faq_d_mine_1_q": "DragonX는 어떻게 채굴하나요?",
|
||||
"faq_d_mine_2_a": "솔로 채굴은 보상을 바로 받지만 해시레이트가 크지 않으면 드뭅니다. 풀 채굴은 다른 사람들과 작업을 나누어 더 작지만 꾸준한 보상을 받습니다. 마이닝 탭은 두 방식 모두 지원합니다.",
|
||||
"faq_d_mine_2_q": "솔로 채굴과 풀 채굴, 무엇을 선택해야 하나요?",
|
||||
"faq_d_mine_3_a": "마이닝 탭의 풀 섹션에 있는 '마이너 업데이트…' 버튼은 최신 최적화된 DragonX 마이너 빌드를 다운로드, 검증, 설치합니다. 노드 업데이트와 마찬가지로 각 다운로드는 설치 전 체크섬과 서명 검증을 거칩니다.",
|
||||
"faq_d_mine_3_q": "마이너는 어떻게 업데이트하나요?",
|
||||
"faq_d_mine_4_a": "현재 노드는 내장 stratum 서버를 실행할 수 있어 다른 채굴자가 이 컴퓨터를 가리킬 수 있습니다. 설정 > 노드 및 보안에 관련 토글이 있습니다. 기본적으로 로컬에서만 대기하며, 다른 컴퓨터에 노출하려면 직접 열어야 합니다.",
|
||||
"faq_d_mine_4_q": "마이닝 풀을 호스팅할 수 있나요?",
|
||||
"faq_d_mine_title": "채굴 및 풀",
|
||||
"faq_d_net_1_a": "실행 직후에는 노드가 피어를 찾는 데 잠시 시간이 걸리므로 '피어 0'이 잠깐 나타나는 것은 정상입니다. 계속되면 인터넷 연결과 방화벽이 노드를 차단하고 있지 않은지 확인하세요. 노드는 내장된 시드를 통해 자동으로 피어를 찾습니다.",
|
||||
"faq_d_net_1_q": "지갑에 피어나 연결이 표시되지 않습니다 — 무엇이 문제인가요?",
|
||||
"faq_d_net_2_a": "예 — DragonX 노드는 TLS를 통해 서로 통신하므로 피어 연결이 암호화됩니다. 지갑도 자신의 노드와 안전한 로컬 채널로 통신합니다.",
|
||||
"faq_d_net_2_q": "네트워크 연결이 암호화되어 있나요?",
|
||||
"faq_d_net_title": "피어 및 네트워크",
|
||||
"faq_d_node_1_a": "데몬 dragonxd는 DragonX 풀 노드입니다. 전체 블록체인을 다운로드하고 검증하며, 트랜잭션을 네트워크에 중계하고, 지갑의 요청에 응답합니다. 자신의 노드를 운영하면 다른 서버를 신뢰할 필요가 없습니다.",
|
||||
"faq_d_node_1_q": "데몬(dragonxd)이란 무엇인가요?",
|
||||
"faq_d_node_2_a": "기본적으로 지갑은 번들된 자체 노드를 실행하고 관리합니다(내장형) — 별도로 할 일은 없습니다. 이미 직접 dragonxd를 실행 중이라면 지갑이 그 외부 노드에 연결할 수도 있습니다. 지갑은 현재 상황을 감지해 그에 맞게 동작합니다.",
|
||||
"faq_d_node_2_q": "내장 노드와 외부 노드의 차이는 무엇인가요?",
|
||||
"faq_d_node_3_a": "풀 노드는 블록체인의 모든 규칙을 독립적으로 검증하므로 지갑이 제3자가 아닌 수학을 신뢰하게 됩니다. 네트워크도 강화합니다. 대신 디스크 공간과 초기 동기화가 필요합니다.",
|
||||
"faq_d_node_3_q": "왜 굳이 풀 노드를 운영해야 하나요?",
|
||||
"faq_d_node_title": "풀 노드",
|
||||
"faq_d_perf_1_a": "블록체인이 가장 큰 부분이며 시간이 지나면서 커지므로 여유 공간을 몇 기가바이트 남겨두세요. 노드는 속도를 위해 데이터베이스 캐시를 사용하며, 번들된 기본값은 일반적인 기기에 맞춰져 있습니다. 고급 사용자는 설정 파일(DRAGONX.conf)에서 dbcache를 수정해 노드의 데이터베이스 캐시를 조정할 수 있습니다.",
|
||||
"faq_d_perf_1_q": "노드가 디스크를 얼마나 사용하나요? 메모리를 조정할 수 있나요?",
|
||||
"faq_d_perf_2_a": "모든 블록을 처음부터 검증하는 대신 설정 마법사에서 제공되는 부트스트랩 다운로드를 활성화하면 최근의 검증된 체인 사본을 받아올 수 있습니다. 빠른 디스크(SSD)도 큰 도움이 됩니다.",
|
||||
"faq_d_perf_2_q": "초기 동기화를 더 빠르게 하려면 어떻게 하나요?",
|
||||
"faq_d_perf_title": "저장 공간 및 성능",
|
||||
"faq_d_sync_1_a": "실행 시 노드는 지갑에 서비스를 제공하기 전에 블록을 불러오고 검증합니다. '블록 처리 중 / 현재 체인 상태를 구성하기 위해 블록 적용 중' 화면이 바로 이 준비 과정입니다. 정상적인 동작이며, 노드가 완료되고 높이를 보고하면 지갑을 사용할 수 있게 됩니다.",
|
||||
"faq_d_sync_1_q": "시작 시 나타나는 '블록 처리 중'은 무슨 의미인가요?",
|
||||
"faq_d_sync_2_a": "보호 자금을 위해 노드는 비공개로 송금할 수 있게 해주는 암호학적 '증인(witness)'을 유지합니다. 일부 재시작이나 업데이트 후에는 최근 블록을 다시 스캔하여 이 캐시를 재구축합니다. 트랜잭션이 많은 지갑에서는 몇 분이 걸리고 추가 메모리를 사용할 수 있습니다.\n\n멈춘 것이 아니니 완료될 때까지 기다리세요. 도중에 강제 종료하면 다음 실행 시 재구축이 처음부터 다시 시작될 뿐입니다.",
|
||||
"faq_d_sync_2_q": "'증인 캐시 재구축 중'이란 무엇인가요?",
|
||||
"faq_d_sync_3_a": "체인 끝부분에 가까워지면 지갑을 스캔하는 작업(잔액과 노트 확인)이 노드와 동일한 내부 잠금을 두고 경쟁하게 되어, 대형 지갑에서는 마지막 블록 연결 속도가 느려질 수 있습니다. 최근 버전은 이 스캔을 제한해 노드가 따라잡을 수 있게 합니다. 가벼운 탭에 머물거나 그냥 기다리면 더 빨리 끝납니다.",
|
||||
"faq_d_sync_3_q": "거의 다 따라잡았는데 왜 동기화가 느린가요?",
|
||||
"faq_d_sync_4_a": "첫 동기화는 전체 체인을 다운로드하고 검증하며, 연결 상태와 디스크에 따라 시간이 걸릴 수 있습니다. 설정 마법사에서 부트스트랩 다운로드를 활성화하면 최근의 검증된 체인 데이터 사본을 받아와 크게 단축할 수 있습니다.",
|
||||
"faq_d_sync_4_q": "초기 동기화는 얼마나 걸리나요?",
|
||||
"faq_d_sync_title": "동기화 및 블록체인",
|
||||
"faq_d_trbl_1_a": "대개는 아닙니다. 대형 지갑에서는 노드가 보호 증인을 재구축하거나 블록을 적용하는 데 몇 분이 걸릴 수 있으며, 이 동안 멈춘 것처럼 보이거나 응답이 느릴 수 있습니다. 시간을 두고 기다려 주세요 — 디버그 로그(설정)를 보면 정상 작동 중이라면 꾸준한 진행이 보입니다.\n\n이 단계에서 강제 종료하지 마세요. 다음 실행 시 작업이 처음부터 다시 시작됩니다.",
|
||||
"faq_d_trbl_1_q": "노드가 '최적 체인 활성화 중'에서 멈춘 것 같습니다 — 멈춘 건가요?",
|
||||
"faq_d_trbl_2_a": "지갑 파일이 복구되었지만 키 데이터 일부가 없으면 노드가 새 주소를 생성하거나 자금을 보호할 수 없는 제한된 '저하 모드'로 열 수 있습니다. 가장 안전한 해결책은 시드 복구 문구로 새 지갑을 복원하는 것입니다.",
|
||||
"faq_d_trbl_2_q": "'저하 모드'란 무엇인가요?",
|
||||
"faq_d_trbl_3_a": "재스캔은 지갑이 체인을 다시 읽어 트랜잭션을 재발견하게 합니다(키를 가져온 후 유용). 재색인은 노드의 블록체인 데이터베이스를 재구축하며, 해당 데이터베이스가 손상되었거나 버전 변경 후 호환되지 않을 때만 필요합니다. 둘 다 시간이 걸릴 수 있으며, 설정 > 노드 및 보안에서 시작할 수 있습니다.",
|
||||
"faq_d_trbl_3_q": "언제 재색인이나 재스캔을 해야 하나요?",
|
||||
"faq_d_trbl_4_a": "증인 재구축이나 초기 동기화 같은 무거운 작업 중에는 메모리 사용량이 급증했다가 완료되면 다시 낮아집니다. 유휴 상태에서 계속 높은 메모리 사용은 이례적이며, 지갑을 재시작하면 해소됩니다. 메모리가 적은 기기에서는 다른 메모리 사용량이 많은 앱과 경쟁하지 않는지 확인하세요.",
|
||||
"faq_d_trbl_4_q": "노드가 메모리를 많이 사용하고 있습니다 — 정상인가요?",
|
||||
"faq_d_trbl_title": "문제 해결",
|
||||
"faq_d_upd_1_a": "설정 > 노드 및 보안 > 데몬 바이너리를 열고 '업데이트 확인…'을 사용하세요. 지갑이 최신 검증된 노드 빌드를 다운로드해 설치하며, 새 버전은 데몬이 다음에 시작될 때 적용됩니다.",
|
||||
"faq_d_upd_1_q": "노드는 어떻게 업데이트하나요?",
|
||||
"faq_d_upd_2_a": "예 — 업데이트 대화상자에 모든 릴리스가 나열되어 특정 버전이나 이전 빌드를 선택할 수 있습니다. 다운그레이드는 주의하세요. 이전 노드는 더 새로운 노드가 작성한 블록체인 데이터를 받아들이지 못해 재색인이 필요할 수 있습니다.",
|
||||
"faq_d_upd_2_q": "특정 노드 버전을 설치할 수 있나요?",
|
||||
"faq_d_upd_3_a": "다운로드된 모든 노드 아카이브는 설치 전에 체크섬과 암호화 서명으로 검증됩니다. 검증에 실패하면 지갑이 업데이트를 거부합니다.",
|
||||
"faq_d_upd_3_q": "업데이트는 안전한가요?",
|
||||
"faq_d_upd_title": "노드 업데이트",
|
||||
"faq_group_daemon": "데몬",
|
||||
"faq_group_wallet": "지갑",
|
||||
"faq_intro": "지갑과 DragonX 노드에 관한 답변입니다. 검색하거나 아래 주제별로 살펴보세요.",
|
||||
"faq_intro_lite": "Lite 지갑에 관한 답변입니다. 검색하거나 아래 주제별로 살펴보세요.",
|
||||
"faq_l_gs_1_a": "ObsidianDragonLite는 경량 DragonX 지갑입니다. 풀 노드를 실행하는 대신 DragonX lite 서버에 연결하므로 빠르게 시작되고 전체 블록체인을 다운로드하지 않습니다.\n\n일반 지갑과 마찬가지로 키, 잔액, 송금을 관리합니다 — 서버 연결 방식과 프라이버시 절충에 대해서는 아래의 'Lite 지갑' 섹션을 참고하세요.",
|
||||
"faq_l_gs_1_q": "ObsidianDragonLite란 무엇인가요?",
|
||||
"faq_l_lite_1_a": "Lite 지갑은 자체 풀 노드를 실행하는 대신 DragonX lite 서버와 통신합니다. 전체 블록체인을 직접 다운로드하거나 검증하지 않으므로 빠르게 시작되고 디스크와 대역폭 사용이 적습니다. 개인 키는 항상 기기에 남아 있으며 서버로 전송되지 않습니다.",
|
||||
"faq_l_lite_1_q": "Lite 지갑은 풀 지갑과 어떻게 다른가요?",
|
||||
"faq_l_lite_2_a": "설정을 열고 서버를 선택하세요. 언제든 서버를 전환할 수 있으며 지갑이 새 서버에서 다시 동기화합니다. 서버가 느리거나 연결되지 않으면 다른 서버를 시도해 보세요.",
|
||||
"faq_l_lite_2_q": "서버는 어떻게 선택하거나 변경하나요?",
|
||||
"faq_l_lite_3_a": "키는 절대 기기를 벗어나지 않으며 보호된 금액은 암호화된 상태로 유지됩니다. 다만 lite 서버는 지갑이 조회하는 주소와 IP 주소를 볼 수 있어 주소들을 서로 연결 지을 수 있습니다 — 편의성과 프라이버시를 맞바꾸는 셈입니다. 최고 수준의 프라이버시를 원한다면 체인을 직접 검증하고 서버에 주소를 노출하지 않는 풀 노드 지갑을 사용하세요.",
|
||||
"faq_l_lite_3_q": "Lite 지갑은 풀 노드만큼 프라이버시가 보장되나요?",
|
||||
"faq_l_lite_title": "라이트 지갑",
|
||||
"faq_l_sec_1_a": "설정 > 지갑으로 이동해 보안 섹션에서 암호를 설정하세요. 암호화는 디스크에 저장된 개인 키를 보호하므로, 파일에 접근할 수 있는 사람이라도 코인을 사용할 수 없습니다.\n\n강력한 암호를 선택하고 잊어버리지 마세요 — 암호 없이는 암호화된 지갑을 복구할 방법이 없습니다.",
|
||||
"faq_no_results": "결과가 없습니다. 다른 검색어를 시도해 보세요.",
|
||||
"faq_open_tooltip": "도움말 및 FAQ",
|
||||
"faq_search_hint": "FAQ 검색…",
|
||||
"faq_title": "도움말 및 FAQ",
|
||||
"faq_w_addr_1_a": "투명 주소(R 또는 t로 시작)는 대부분의 코인처럼 작동합니다. 금액과 주소가 블록체인에 공개됩니다.\n\n보호 주소(zs로 시작하는 z-주소)는 영지식 암호화를 사용합니다 — 금액, 발신자, 수신자가 체인상에서 암호화됩니다. 보호 주소에 보관된 잔액은 비공개입니다.",
|
||||
"faq_w_addr_1_q": "투명 주소와 보호 주소의 차이는 무엇인가요?",
|
||||
"faq_w_addr_2_a": "가능하면 항상 보호(z) 주소를 사용하세요 — 잔액과 결제 내역이 비공개로 유지됩니다. DragonX는 프라이버시 코인이며, 수신 자금의 기본값은 보호 주소입니다.\n\n상대방 서비스가 보호 주소를 처리할 수 없는 경우에만 투명 주소를 사용하세요.",
|
||||
"faq_w_addr_2_q": "어떤 주소 유형을 사용해야 하나요?",
|
||||
"faq_w_addr_3_a": "보호 주소는 재사용해도 괜찮으며 내역이 노출되지 않습니다. 투명 주소는 결제마다 새 주소를 사용하는 것이 프라이버시에 좋습니다. 수신 탭에서 새 주소를 만들 수 있습니다.",
|
||||
"faq_w_addr_3_q": "주소를 재사용해야 하나요, 아니면 새로 만들어야 하나요?",
|
||||
"faq_w_addr_4_a": "보호(shielding)는 코인을 투명 주소에서 보호 주소로 옮겨 비공개로 만드는 것입니다. 새로 채굴된 코인은 투명 상태로 도착하며 자동으로 보호 처리됩니다.\n\n지갑에서 수동으로 보호 처리할 수도 있습니다. 보호는 일반 트랜잭션의 일종이므로 완료까지 일반적인 확인 시간이 걸립니다.",
|
||||
"faq_w_addr_4_q": "'보호(shielding)'란 무엇인가요?",
|
||||
"faq_w_addr_title": "주소 및 프라이버시",
|
||||
"faq_w_bal_1_a": "지갑이 아직 동기화 중일 때는 잔액이 불완전하며 0으로 표시될 수 있습니다 — 아직 모든 트랜잭션을 스캔하지 않았기 때문입니다. 동기화가 끝나면 채워집니다.\n\n하단 상태 표시줄을 확인하세요. 연결 상태와 블록 높이가 표시됩니다. 동기화가 완료되면 잔액이 정확해집니다.",
|
||||
"faq_w_bal_1_q": "잔액이 0이거나 업데이트되지 않는 이유는 무엇인가요?",
|
||||
"faq_w_bal_2_a": "트랜잭션이 포함된 블록 위에 새 블록이 채굴될 때마다 확인이 하나씩 늘어납니다. 확인 수가 많을수록 결제가 더 확실하게 확정된 것입니다. 수신 자금은 첫 확인 후 사용할 수 있게 됩니다.",
|
||||
"faq_w_bal_2_q": "확인이란 무엇인가요?",
|
||||
"faq_w_bal_3_a": "총 잔액에는 아직 확인 중이거나 일시적으로 잠긴 자금(예: 보호 처리 중인 코인)이 포함됩니다. 사용 가능 잔액은 지금 바로 보낼 수 있는 금액입니다. 트랜잭션이 확인되면 두 값이 같아집니다.",
|
||||
"faq_w_bal_3_q": "총 잔액과 사용 가능 잔액의 차이는 무엇인가요?",
|
||||
"faq_w_bal_title": "잔액 및 동기화",
|
||||
"faq_w_chat_1_a": "채팅은 지갑에 내장된 암호화 온체인 메신저입니다. 메시지는 비공개 보호 메모 형태로 전송되므로 본인과 상대방만 읽을 수 있습니다.",
|
||||
"faq_w_chat_1_q": "채팅 기능이란 무엇인가요?",
|
||||
"faq_w_chat_2_a": "채팅 아이디는 지갑의 복구 문구에서 파생되므로 지갑과 함께 이동합니다 — 지갑을 복원하면 아이디도 함께 돌아옵니다. 별도의 계정이나 비밀번호를 만들 필요가 없습니다.",
|
||||
"faq_w_chat_2_q": "채팅 아이디는 어떻게 작동하나요?",
|
||||
"faq_w_chat_3_a": "연락처 탭에서 주소로 연락처를 추가할 수 있으며, 선택적으로 이름과 아바타를 지정할 수 있습니다. 연락처는 현재 지갑에만 비공개로 유지하거나 이 컴퓨터에서 여는 모든 지갑과 공유할 수 있습니다.",
|
||||
"faq_w_chat_3_q": "연락처는 어떻게 작동하나요?",
|
||||
"faq_w_chat_4_a": "대화를 열고 헤더의 아이콘을 사용하세요. 숨기기(눈 모양)는 목록에서 제거하지만 모든 메시지는 유지됩니다 — 새 메시지가 오면 다시 표시됩니다. 삭제(휴지통)는 대화상자를 엽니다. '삭제'는 이 기기의 내역만 지웁니다(상대방이 다시 메시지를 보내면 대화가 다시 나타납니다). '삭제 및 차단'은 상대방 목록 위의 '차단됨' 목록에서 차단을 해제할 때까지 향후 메시지도 막습니다. 이 모든 작업은 이 기기에서만 이루어집니다 — 메시지는 블록체인에 그대로 남아 있고 상대방은 자신의 사본을 유지하므로 '전송 취소'는 아닙니다.",
|
||||
"faq_w_chat_4_q": "대화를 숨기거나 삭제하거나 차단하려면 어떻게 하나요?",
|
||||
"faq_w_chat_title": "채팅 및 연락처",
|
||||
"faq_w_gs_1_a": "ObsidianDragon은 DragonX(DRGX)용 풀노드 지갑입니다. 코인을 관리하며, 데스크톱 빌드에서는 백그라운드에서 DragonX 풀노드(데몬)를 실행하여 지갑이 제3자 서버를 신뢰하는 대신 블록체인을 직접 검증합니다.\n\n지갑(키, 잔액, 전송)과 노드(블록체인, 네트워크)라는 두 부분이 함께 작동한다고 생각하면 됩니다. 여기 질문들도 같은 방식으로 나뉘어 있습니다 — 위의 지갑 및 데몬 탭을 참고하세요.",
|
||||
"faq_w_gs_1_q": "ObsidianDragon이란 무엇인가요?",
|
||||
"faq_w_gs_2_a": "처음 실행하면 설정 마법사가 자동으로 지갑을 만들어 줍니다. 새 지갑은 비밀 복구 문구(단어 목록)로 보호됩니다. 이 문구를 적어 오프라인에 안전하게 보관하세요 — 이 컴퓨터를 잃어버렸을 때 자금을 복원할 수 있는 유일한 방법입니다.\n\n설정 > 백업 및 데이터 > 시드 문구에서 언제든지 문구를 확인하거나 백업할 수 있습니다.",
|
||||
"faq_w_gs_2_q": "새 지갑은 어떻게 만드나요?",
|
||||
"faq_w_gs_3_a": "복구 문구가 있다면 시드 문구 복원 절차를 사용하세요. (풀노드 빌드에서는 실행 전에 기존 wallet.dat 파일을 지갑의 데이터 디렉터리에 복사할 수도 있습니다.)\n\n복원 후 지갑이 과거 트랜잭션을 찾기 위해 다시 스캔하므로, 처음에는 잔액과 내역이 표시되기까지 시간이 걸릴 수 있습니다.",
|
||||
"faq_w_gs_3_q": "이미 가지고 있는 지갑은 어떻게 복원하나요?",
|
||||
"faq_w_gs_4_a": "마법사에서는 모양/테마를 선택하고, 암호화와 PIN을 설정하며, 풀노드 빌드에서는 선택적으로 부트스트랩을 다운로드해 첫 동기화 속도를 높일 수 있습니다. 어떤 단계든 건너뛸 수 있으며 나중에 설정에서 모두 변경할 수 있습니다.",
|
||||
"faq_w_gs_4_q": "최초 실행 설정은 무엇을 하나요?",
|
||||
"faq_w_gs_title": "시작하기",
|
||||
"faq_w_sec_1_a": "설정 > 노드 및 보안으로 이동해 암호를 설정하세요(보안 섹션). 암호화는 디스크에 저장된 개인 키를 보호하므로, 파일에 접근할 수 있는 사람이라도 코인을 사용할 수 없습니다.\n\n강력한 암호를 선택하고 잊어버리지 마세요 — 암호 없이는 암호화된 지갑을 복구할 방법이 없습니다.",
|
||||
"faq_w_sec_1_q": "지갑을 어떻게 암호화하나요?",
|
||||
"faq_w_sec_2_a": "PIN은 자리를 비웠을 때 잔액과 작업이 노출되지 않도록 앱 화면을 잠급니다. 암호화 위에 추가된 편의용 잠금이며, 마법사 중이나 지갑을 암호화한 후 설정에서 설정할 수 있습니다.",
|
||||
"faq_w_sec_2_q": "PIN/잠금 화면이란 무엇인가요?",
|
||||
"faq_w_sec_3_a": "아니요. 지갑 암호화는 우회할 수 없습니다. 시드 복구 문구가 있다면 새 지갑으로 복원하고 새 암호를 설정할 수 있습니다. 암호와 시드 문구 둘 다 없으면 자금을 복구할 수 없습니다.",
|
||||
"faq_w_sec_3_q": "암호를 잊어버렸어요 — 복구할 수 있나요?",
|
||||
"faq_w_sec_title": "보안 및 암호화",
|
||||
"faq_w_seed_1_a": "설정 > 백업 및 데이터 > 시드 문구로 이동하세요. 단어를 순서대로 종이에 적어 안전한 오프라인 장소에 보관하세요. 이 문구를 가진 사람은 누구든 코인을 사용할 수 있으므로 사진, 이메일, 클라우드 메모에는 절대 저장하지 마세요.",
|
||||
"faq_w_seed_1_q": "복구 문구는 어떻게 백업하나요?",
|
||||
"faq_w_seed_2_a": "이전(레거시) 지갑은 시드 기반이 아니었습니다. 지갑은 레거시 지갑을 최신 시드 기반 지갑으로 이전할 수 있습니다. 새 시드 지갑을 만들고 자금을 그곳으로 쓸어 담습니다. 설정 > 백업 및 데이터에서 '시드 지갑으로 이전…'을 찾으세요. 이 기능은 현재 버전의 데몬이 필요합니다.",
|
||||
"faq_w_seed_2_q": "제 지갑에는 복구 문구가 없어요 — 추가할 수 있나요?",
|
||||
"faq_w_seed_3_a": "지갑을 설정할 때 시드 복원 절차를 사용해 단어를 순서대로 입력하세요. 그러면 지갑이 다시 스캔해 잔액과 내역을 재구성하며, 처음에는 시간이 걸릴 수 있습니다.",
|
||||
"faq_w_seed_3_q": "복구 문구로 어떻게 복원하나요?",
|
||||
"faq_w_seed_4_a": "시드 문구는 기본 백업이며 모든 것을 복원하기에 충분합니다. 지갑 파일 사본은 라벨과 설정까지 보존하는 편리한 보조 백업입니다. 모든 백업은 오프라인에 비공개로 보관하세요.",
|
||||
"faq_w_seed_4_q": "wallet.dat도 백업해야 하나요?",
|
||||
"faq_w_seed_title": "시드 문구 및 백업",
|
||||
"faq_w_send_1_a": "전송 탭을 열고 받는 사람의 주소를 붙여넣은 다음 금액을 입력하고 확인하세요. 지갑이 암호화되어 있다면 트랜잭션을 위해 잠금 해제를 요청받습니다.\n\n보호 주소로의 전송은 비공개이며, 투명 주소로의 전송은 공개됩니다.",
|
||||
"faq_w_send_1_q": "자금은 어떻게 보내나요?",
|
||||
"faq_w_send_2_a": "DragonX 수수료는 매우 낮으며 자동으로 설정됩니다. 확인 전에 수수료가 표시되며, 보통 변경할 필요는 없습니다.",
|
||||
"faq_w_send_2_q": "어떤 수수료를 내나요?",
|
||||
"faq_w_send_3_a": "네 — 보호(z) 주소로 전송할 때 암호화된 메모를 첨부할 수 있습니다. 받는 사람만 읽을 수 있습니다. 투명 주소는 메모를 지원하지 않습니다.",
|
||||
"faq_w_send_3_q": "결제에 메시지를 첨부할 수 있나요?",
|
||||
"faq_w_send_4_a": "트랜잭션은 블록에 채굴되어 확인을 얻기 전까지 '대기 중'입니다. 보통 1~2분 정도 걸립니다. 보호 트랜잭션은 증명을 생성하기 위해 지갑이 동기화되어 있어야 합니다.\n\n전송이 유난히 오래 대기 중이라면 지갑이 연결되고 동기화되어 있는지 확인하세요(창 하단의 상태 표시줄 참고).",
|
||||
"faq_w_send_4_q": "결제를 보냈는데 대기 중이라고 나와요 — 왜 그런가요?",
|
||||
"faq_w_send_title": "송금 및 수신",
|
||||
"faq_w_set_1_a": "설정 > 화면에서 테마와 강조 색상을 선택하세요. 여러 밝은/어두운 스킨이 포함되어 있으며, 저사양 기기에서는 블러 같은 효과를 끌 수 있습니다.",
|
||||
"faq_w_set_1_q": "테마를 어떻게 바꾸나요?",
|
||||
"faq_w_set_2_a": "네 — 설정 > 화면에 언어 선택 항목이 있습니다. 지갑은 여러 언어로 번역되어 있으며, 아직 번역되지 않은 항목은 영어로 표시됩니다.",
|
||||
"faq_w_set_2_q": "언어를 바꿀 수 있나요?",
|
||||
"faq_w_set_3_a": "설정 > 화면의 글꼴 배율 옵션을 사용하세요. 전체 인터페이스 크기를 조절하며, 고해상도(HiDPI) 디스플레이에서 앱이 작게 표시될 때 유용합니다.",
|
||||
"faq_w_set_3_q": "글자가 너무 작거나(또는 커요) — 크기를 조절할 수 있나요?",
|
||||
"faq_w_set_4_a": "설정에서 가격 조회를 활성화하세요. 그러면 잔액과 함께 대략적인 법정화폐 가치가 표시되고, 작은 시세 차트도 볼 수 있습니다. 가격은 참고용일 뿐입니다.",
|
||||
"faq_w_set_4_q": "법정화폐 가격을 어떻게 표시하나요?",
|
||||
"faq_w_set_title": "설정 및 화면",
|
||||
"favorite_address": "즐겨찾기에 추가",
|
||||
"fee": "수수료",
|
||||
"fee_high": "높음",
|
||||
@@ -572,6 +878,63 @@
|
||||
"general": "일반",
|
||||
"generating": "생성 중",
|
||||
"go_to_receive": "수신으로 이동",
|
||||
"grpa_current_block_paren": "(현재: %d)",
|
||||
"grpa_days_ago": "%lld일 전",
|
||||
"grpa_dbg_addrman": "피어 주소 추적 및 관리",
|
||||
"grpa_dbg_alert": "경고 시스템 메시지",
|
||||
"grpa_dbg_bench": "작업 벤치마크 시간 측정",
|
||||
"grpa_dbg_coindb": "코인 데이터베이스 읽기/쓰기 작업",
|
||||
"grpa_dbg_db": "Berkeley DB 작업",
|
||||
"grpa_dbg_estimatefee": "수수료 추정 알고리즘",
|
||||
"grpa_dbg_http": "HTTP RPC 서버 활동",
|
||||
"grpa_dbg_libevent": "Libevent 네트워킹 라이브러리",
|
||||
"grpa_dbg_lock": "락 경합 디버깅",
|
||||
"grpa_dbg_mempool": "트랜잭션 메모리 풀 활동",
|
||||
"grpa_dbg_net": "네트워크 연결 및 메시지",
|
||||
"grpa_dbg_paymentdisclosure": "결제 공개 프로토콜",
|
||||
"grpa_dbg_pow": "작업 증명 채굴 활동",
|
||||
"grpa_dbg_proxy": "SOCKS5 프록시 연결",
|
||||
"grpa_dbg_prune": "블록 정리 작업",
|
||||
"grpa_dbg_rand": "난수 생성",
|
||||
"grpa_dbg_reindex": "블록체인 재색인 진행 상황",
|
||||
"grpa_dbg_rpc": "RPC 명령 처리",
|
||||
"grpa_dbg_selectcoins": "트랜잭션용 코인 선택",
|
||||
"grpa_dbg_tor": "Tor 연동 및 회로 정보",
|
||||
"grpa_dbg_zmq": "ZeroMQ 알림 시스템",
|
||||
"grpa_dbg_zrpc": "실드(z-addr) RPC 작업",
|
||||
"grpa_enter_private_key_to_import": "가져올 개인 키를 입력하세요.",
|
||||
"grpa_error_prefix": "오류: ",
|
||||
"grpa_hr_ago": "%lld시간 전",
|
||||
"grpa_invalid_response_from_daemon": "데몬으로부터 잘못된 응답",
|
||||
"grpa_invalid_suffix": " (유효하지 않음)",
|
||||
"grpa_min_ago": "%lld분 전",
|
||||
"grpa_sec_ago": "%lld초 전",
|
||||
"grpa_seed_demo_chat": "시드 데모 채팅",
|
||||
"grpa_showing_first_100_of": "... %d개 중 처음 100개 표시",
|
||||
"grpa_tab_about": "정보",
|
||||
"grpa_tab_appearance": "화면 표시",
|
||||
"grpa_tab_backup_data": "백업 및 데이터",
|
||||
"grpa_tab_chat": "채팅",
|
||||
"grpa_tab_explorer": "탐색기",
|
||||
"grpa_tab_node_security": "노드 및 보안",
|
||||
"grpa_tab_wallet": "지갑",
|
||||
"grpa_unexpected_getblockhash_result": "예기치 않은 getblockhash 결과",
|
||||
"grpb_copy": "복사",
|
||||
"grpb_max": "최대",
|
||||
"grpb_new_badge_suffix": " [신규]",
|
||||
"grpb_preview_msg_payment_through": "결제가 완료됐나요? 🙂",
|
||||
"grpb_preview_msg_sending_rest": "지금 나머지를 보낼게요 👍",
|
||||
"grpb_preview_msg_yep_confirmed": "네 — 방금 확인했어요 ✅",
|
||||
"grpb_selected_suffix": "\n(선택됨)",
|
||||
"grpb_tooltip_address_balance": "%s\n잔액: %.8f %s%s",
|
||||
"grpb_undo_clear": "지우기 취소",
|
||||
"grpc_benchmark_inconclusive": "벤치마크 결과가 불확실합니다: 해시레이트 샘플이 기록되지 않았습니다. 풀 연결을 확인한 후 다시 시도하세요.",
|
||||
"grpc_benchmark_takes_secs": "벤치마크는 약 %d초가 걸리며 채굴을 중단합니다. 시작하려면 다시 클릭하세요.",
|
||||
"grpc_bootstrap_failed": "부트스트랩 실패",
|
||||
"grpc_bootstrap_not_initialized": "부트스트랩이 초기화되지 않았습니다",
|
||||
"grpc_hashrate_fee": "%s 수수료 %s%%",
|
||||
"grpc_key_not_available": "이 주소에 대한 키를 사용할 수 없습니다",
|
||||
"grpc_na": "해당 없음",
|
||||
"height": "높이",
|
||||
"help": "도움말",
|
||||
"hidden_tag": " (숨김)",
|
||||
@@ -646,6 +1009,7 @@
|
||||
"light": "라이트",
|
||||
"lite_account_label": "계정",
|
||||
"lite_action": "작업",
|
||||
"lite_backend_unavailable": "라이트 지갑 백엔드를 사용할 수 없습니다",
|
||||
"lite_backup_keys": "백업 및 키",
|
||||
"lite_birthday_backup": "생성 블록: %llu (이 값도 백업하세요)",
|
||||
"lite_birthday_hint": "스캔을 시작할 블록 높이입니다. 모르면 0으로 두세요(전체 스캔이 느려짐).",
|
||||
@@ -653,9 +1017,12 @@
|
||||
"lite_console_backend_commands": "백엔드 명령:",
|
||||
"lite_console_help_passthrough": "그 외 입력은 라이트 지갑 콘솔 명령으로 실행됩니다.",
|
||||
"lite_copy": "복사",
|
||||
"lite_could_not_start": "작업을 시작할 수 없습니다",
|
||||
"lite_could_not_write": "쓸 수 없습니다: ",
|
||||
"lite_encrypt_wallet": "지갑 암호화",
|
||||
"lite_encryption_removed": "암호화가 제거되었습니다",
|
||||
"lite_enter_all_seed_words": "복구하려면 24개의 시드 단어를 모두 입력하세요 (현재 %d개)",
|
||||
"lite_enter_wallet_path": "지갑 경로를 입력하세요",
|
||||
"lite_hide_wipe": "숨기고 삭제",
|
||||
"lite_import": "가져오기",
|
||||
"lite_import_key_label": "키 가져오기",
|
||||
@@ -741,6 +1108,7 @@
|
||||
"loading": "로딩 중...",
|
||||
"loading_addresses": "주소 로딩 중...",
|
||||
"loading_stall_body": "데몬이 %.0f초 동안 초기화 중입니다. 업데이트 후나 첫 실행 시(블록 인덱스 로드 또는 재스캔)에는 정상일 수 있습니다. 준비되면 자동으로 연결됩니다.",
|
||||
"loading_stall_hint": "멈췄나요? 설정 → 데몬 재시작을 시도하거나 콘솔을 확인하세요.",
|
||||
"loading_stall_title": "예상보다 오래 걸리고 있습니다",
|
||||
"loading_transactions": "거래를 불러오는 중",
|
||||
"local_hashrate": "로컬 해시레이트",
|
||||
@@ -791,9 +1159,27 @@
|
||||
"memo_optional": "메모 (선택)",
|
||||
"memo_upper": "메모",
|
||||
"memo_z_only": "참고: 메모는 차폐 (z) 주소로 전송할 때만 사용할 수 있습니다",
|
||||
"merge_addr_created": "보호 주소가 생성되었습니다.",
|
||||
"merge_advanced": "고급",
|
||||
"merge_back": "뒤로",
|
||||
"merge_batch_fmt": "실행당 최대 %d개의 입력을 병합합니다 — 나머지를 마치려면 반복하세요.",
|
||||
"merge_confirm_btn": "확인",
|
||||
"merge_confirm_fmt": "~%s DRGX를 입력 %d개에서 %s(으)로 통합할까요?",
|
||||
"merge_create_zaddr": "보호 주소 생성",
|
||||
"merge_creating": "주소 생성 중…",
|
||||
"merge_description": "여러 UTXO를 단일 차폐 주소로 통합합니다. 지갑 크기를 줄이고 프라이버시를 향상시킵니다.",
|
||||
"merge_fee_hint": "이 거래에 대한 네트워크 수수료입니다.",
|
||||
"merge_funds": "자금 통합",
|
||||
"merge_max_inputs": "배치당 최대 입력 수",
|
||||
"merge_no_spendable": "아직 통합할 사용 가능한 입력이 없습니다.",
|
||||
"merge_progress": "통합 중… 몇 분 정도 걸릴 수 있습니다. 이 창을 닫아도 됩니다.",
|
||||
"merge_scope_fmt": "투명 입력 %d개 + 보호 입력 %d개 · 사용 가능 약 %s DRGX",
|
||||
"merge_scope_loading": "입력을 확인하는 중…",
|
||||
"merge_send_failed": "통합 실패: ",
|
||||
"merge_source": "통합",
|
||||
"merge_src_both": "둘 다",
|
||||
"merge_src_shielded": "보호",
|
||||
"merge_src_transparent": "투명",
|
||||
"merge_started": "통합 작업이 시작되었습니다",
|
||||
"merge_title": "주소로 통합",
|
||||
"mig_adopt": "이 지갑을 내 지갑으로 사용",
|
||||
@@ -874,6 +1260,7 @@
|
||||
"mining_difficulty_copied": "난이도가 복사되었습니다",
|
||||
"mining_est_block": "예상 블록",
|
||||
"mining_est_daily": "예상 일일 수익",
|
||||
"mining_est_daily_pool_sub": "대략적인 솔로 환산, 풀 수수료 전",
|
||||
"mining_filter_all": "전체",
|
||||
"mining_filter_tip_all": "모든 수익 표시",
|
||||
"mining_filter_tip_pool": "풀 수익만 표시",
|
||||
@@ -902,10 +1289,12 @@
|
||||
"mining_open_in_explorer": "탐색기에서 열기",
|
||||
"mining_payout_address": "지급 주소",
|
||||
"mining_payout_foreign": "⚠ 이 지급 주소는 현재 지갑에 없습니다 — 채굴한 보상이 다른 지갑으로 전송됩니다. 지갑을 전환했다면 주소를 업데이트하세요.",
|
||||
"mining_payout_invalid": "유효한 DragonX 주소가 아닙니다 — 시작하기 전에 수정하세요. 그렇지 않으면 채굴 보상이 사라집니다.",
|
||||
"mining_payout_tooltip": "채굴 보상 수신 주소",
|
||||
"mining_pool": "풀",
|
||||
"mining_pool_fee": "수수료",
|
||||
"mining_pool_hashrate": "풀 해시레이트",
|
||||
"mining_pool_needs_payout_tooltip": "먼저 지급 주소를 입력하세요 (Z 주소를 생성하세요)",
|
||||
"mining_pool_url": "풀 URL",
|
||||
"mining_pools_header": "풀",
|
||||
"mining_recent_blocks": "최근 블록",
|
||||
@@ -935,6 +1324,9 @@
|
||||
"mining_syncing_tooltip": "블록체인 동기화 중...",
|
||||
"mining_tag": " · 채굴",
|
||||
"mining_threads": "채굴 스레드",
|
||||
"mining_threads_input_tooltip": "정확한 스레드 수 입력 (Enter로 적용)",
|
||||
"mining_threads_minus_tooltip": "스레드 줄이기",
|
||||
"mining_threads_plus_tooltip": "스레드 늘리기",
|
||||
"mining_to_save": "저장하려면",
|
||||
"mining_today": "오늘",
|
||||
"mining_uptime": "가동 시간",
|
||||
@@ -1027,6 +1419,13 @@
|
||||
"peers_upper": "피어",
|
||||
"peers_version": "버전",
|
||||
"pending": "대기 중",
|
||||
"picker_dat_count": "이 폴더에 지갑 파일 %d개",
|
||||
"picker_dat_none": "이 폴더에 직접 있는 지갑 파일이 없습니다",
|
||||
"picker_empty": "이 폴더에는 하위 폴더나 지갑 파일이 없습니다.",
|
||||
"picker_home": "홈 폴더",
|
||||
"picker_select": "이 폴더 검색",
|
||||
"picker_title": "검색할 폴더 선택",
|
||||
"picker_up": "한 단계 위로",
|
||||
"pin_change_desc": "잠금 해제 PIN을 변경합니다. 현재 PIN과 새 PIN이 필요합니다.",
|
||||
"pin_confirm_new_label": "새 PIN 확인:",
|
||||
"pin_current_label": "현재 PIN:",
|
||||
@@ -1104,6 +1503,8 @@
|
||||
"qr_failed": "QR 코드 생성 실패",
|
||||
"qr_title": "QR 코드",
|
||||
"qr_unavailable": "QR 사용 불가",
|
||||
"quick_receive": "빠른 받기",
|
||||
"quick_send": "빠른 보내기",
|
||||
"ram_daemon_gb": "데몬: %.1f GB (%s)",
|
||||
"ram_daemon_mb": "데몬: %.0f MB (%s)",
|
||||
"ram_system_gb": "시스템: %.1f / %.0f GB",
|
||||
@@ -1153,6 +1554,7 @@
|
||||
"rpc_connection": "RPC 연결...",
|
||||
"rpc_host": "RPC 호스트",
|
||||
"rpc_pass": "비밀번호",
|
||||
"rpc_plaintext_remote_warning": "원격 RPC가 암호화되지 않은 HTTP를 사용하고 있습니다. 데몬이 TLS를 지원하면 DRAGONX.conf에 rpctls=1을 추가하세요.",
|
||||
"rpc_port": "포트",
|
||||
"rpc_user": "사용자명",
|
||||
"save": "저장",
|
||||
@@ -1160,6 +1562,7 @@
|
||||
"save_z_transactions": "Z 거래를 거래 목록에 저장",
|
||||
"sb_auth_failed": "인증 실패 — rpcuser/rpcpassword를 확인하세요",
|
||||
"sb_block": "블록: %d",
|
||||
"sb_block_db_unreadable": "블록 데이터베이스를 읽을 수 없음 — 재구축 필요",
|
||||
"sb_building_witnesses": "증인 설정 중",
|
||||
"sb_building_witnesses_pct": "위트니스 재생성 중 %.0f%%",
|
||||
"sb_connecting_daemon": "dragonxd에 연결 중...",
|
||||
@@ -1175,6 +1578,7 @@
|
||||
"sb_dragonxd_stopped": "dragonxd 중지됨",
|
||||
"sb_dragonxd_stopping": "dragonxd 중지 중...",
|
||||
"sb_extracting_sapling": "Sapling 매개변수 추출 중...",
|
||||
"sb_finishing_repair": "지갑 복구 마무리 중 — 다시 스캔하는 중…",
|
||||
"sb_importing_keys": "키 가져오기 중",
|
||||
"sb_loading_config": "설정 불러오는 중...",
|
||||
"sb_mining_hs": "%.1f H/s",
|
||||
@@ -1196,14 +1600,55 @@
|
||||
"sb_waiting_config": "데몬 설정 대기 중...",
|
||||
"sb_waiting_daemon": "dragonxd 대기 중...",
|
||||
"sb_waiting_daemon_err": "dragonxd 대기 중 — %s",
|
||||
"sb_wallet_needs_recovery": "지갑 복구 가능",
|
||||
"sb_warming_up": "워밍업 중...",
|
||||
"sb_witness_cache": "증인 재구축 중",
|
||||
"scale_effects": "배율 및 효과",
|
||||
"screenshot_open_dir": "위치 열기",
|
||||
"screenshot_sweep": "스크린샷 스윕 실행",
|
||||
"screenshot_sweep_desc": "모든 탭에 대해 모든 테마를 순회하며 각각의 스크린샷을 설정 디렉터리의 screenshots 폴더 아래 탭별 하위 폴더에 저장합니다(이전 스윕을 덮어씀). 몇 초 동안 실행됩니다.",
|
||||
"screenshot_sweep_full": "전체 UI 스윕",
|
||||
"search_icons": "아이콘 검색...",
|
||||
"search_placeholder": "검색...",
|
||||
"sec_changing_passphrase": "암호문 변경 중...",
|
||||
"sec_changing_pin": "PIN 변경 중...",
|
||||
"sec_couldnt_lock_wallet": "지갑을 잠글 수 없습니다 — 아직 잠금 해제 상태입니다. 데몬 연결을 확인하세요.",
|
||||
"sec_encrypted_backup_suffix": "\n암호화된 백업: wallet.dat.encrypted.bak",
|
||||
"sec_encrypting_wallet": "지갑 암호화 중...",
|
||||
"sec_encryption_did_not_complete": "지갑 암호화가 완료되지 않았습니다 — 지갑이 암호화되지 않은 상태입니다. 설정을 열어 암호화를 완료하세요.",
|
||||
"sec_encryption_failed_prefix": "암호화 실패: ",
|
||||
"sec_failed_prefix": "실패: ",
|
||||
"sec_failed_to_create_vault": "볼트 생성에 실패했습니다",
|
||||
"sec_importing_keys_rescanning": "키를 가져오고 블록체인을 다시 스캔하는 중 — 진행 중에도 지갑을 사용할 수 있습니다",
|
||||
"sec_incorrect_current_pin": "현재 PIN이 올바르지 않습니다",
|
||||
"sec_incorrect_passphrase_decrypt": "잘못된 암호문",
|
||||
"sec_incorrect_passphrase_pin_setup": "잘못된 암호문",
|
||||
"sec_incorrect_pin_remove": "잘못된 PIN",
|
||||
"sec_internal_error_change_pin": "내부 오류",
|
||||
"sec_internal_error_remove_pin": "내부 오류",
|
||||
"sec_mode_passphrase": " 암호문",
|
||||
"sec_not_connected_to_daemon": "데몬에 연결되지 않음",
|
||||
"sec_not_connected_to_daemon_pin": "데몬에 연결되지 않음",
|
||||
"sec_passphrase_changed_successfully": "암호문이 성공적으로 변경되었습니다",
|
||||
"sec_pin_changed_successfully": "PIN이 성공적으로 변경되었습니다",
|
||||
"sec_pin_removed": "PIN이 제거되었습니다",
|
||||
"sec_pin_set_successfully": "PIN이 성공적으로 설정되었습니다",
|
||||
"sec_restart_daemon_for_encryption": "암호화를 적용하려면 데몬을 다시 시작하세요.",
|
||||
"sec_too_many_attempts_wait": "시도 횟수가 너무 많습니다. %.0f초 기다리세요...",
|
||||
"sec_total_elapsed_fmt": "총 경과 시간: %d분 %02d초",
|
||||
"sec_unlock_button": "잠금 해제",
|
||||
"sec_unlock_failed_prefix": "잠금 해제 실패: ",
|
||||
"sec_unlocking_fmt": "잠금 해제 중%s",
|
||||
"sec_use_passphrase_instead": "대신 암호문 사용",
|
||||
"sec_use_pin_instead": "대신 PIN 사용",
|
||||
"sec_verifying_passphrase": "암호문 확인 중...",
|
||||
"sec_verifying_pin": "PIN 확인 중...",
|
||||
"sec_wallet_decrypted_all_keys_imported": "지갑 복호화 완료! 모든 키를 가져왔습니다.",
|
||||
"sec_wallet_encrypted_and_pin_set": "지갑 암호화 및 PIN 설정 완료",
|
||||
"sec_wallet_encrypted_but_pin_vault_failed": "지갑은 암호화되었으나 PIN 볼트 설정에 실패했습니다",
|
||||
"sec_wallet_encrypted_restarting_daemon": "지갑이 암호화되었습니다. 데몬을 다시 시작하는 중...",
|
||||
"sec_wallet_encrypted_successfully": "지갑이 성공적으로 암호화되었습니다",
|
||||
"sec_wallet_locked_title": "지갑 잠김",
|
||||
"security": "보안",
|
||||
"seed_backup_button": "시드 문구",
|
||||
"seed_backup_close": "닫기",
|
||||
@@ -1227,6 +1672,7 @@
|
||||
"send_amount": "금액",
|
||||
"send_amount_details": "금액 상세",
|
||||
"send_amount_upper": "금액",
|
||||
"send_available_note": "사용 가능",
|
||||
"send_cannot_send_now": "지금 보낼 수 없습니다 — 연결, 동기화, 사용 가능한 잔액을 확인하세요.",
|
||||
"send_clear_fields": "모든 양식 필드를 지우시겠습니까?",
|
||||
"send_contacts_button": "연락처에서 선택",
|
||||
@@ -1262,6 +1708,7 @@
|
||||
"send_tooltip_not_connected": "데몬에 연결되지 않음",
|
||||
"send_tooltip_select_source": "먼저 보낼 주소를 선택하세요",
|
||||
"send_tooltip_syncing": "블록체인 동기화를 기다려 주세요",
|
||||
"send_tooltip_view_only": "조회 전용 주소 — 지출 키가 없어 보낼 수 없습니다",
|
||||
"send_total": "합계",
|
||||
"send_transaction": "거래 전송",
|
||||
"send_tx_failed": "거래 실패",
|
||||
@@ -1281,16 +1728,16 @@
|
||||
"sent_filter": "전송됨",
|
||||
"sent_type": "전송됨",
|
||||
"sent_upper": "전송됨",
|
||||
"set_label": "라벨 설정...",
|
||||
"set_label": "라벨 설정",
|
||||
"settings": "설정",
|
||||
"settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.",
|
||||
"settings_acrylic_level": "아크릴 레벨:",
|
||||
"settings_address_book": "주소록...",
|
||||
"settings_address_book": "주소록…",
|
||||
"settings_auto_detected": "DRAGONX.conf에서 자동 감지",
|
||||
"settings_auto_lock": "자동 잠금",
|
||||
"settings_auto_shield_desc": "투명 자금을 자동으로 차폐 주소로 이동",
|
||||
"settings_auto_shield_funds": "투명 자금 자동 차폐",
|
||||
"settings_backup": "백업...",
|
||||
"settings_backup": "백업…",
|
||||
"settings_block_explorer_urls": "블록 탐색기 URL",
|
||||
"settings_builtin": "내장",
|
||||
"settings_change_passphrase": "비밀번호 변경",
|
||||
@@ -1304,7 +1751,7 @@
|
||||
"settings_copy_diagnostics": "진단 정보 복사",
|
||||
"settings_copyright": "Copyright 2024-2026 DragonX 개발자 | GPLv3 라이선스",
|
||||
"settings_custom": "사용자 지정",
|
||||
"settings_data_dir": "데이터 디렉터리:",
|
||||
"settings_data_dir": "데이터 디렉터리",
|
||||
"settings_debug_changed": "디버그 카테고리가 변경되었습니다 — 데몬을 재시작하여 적용",
|
||||
"settings_debug_restart_note": "변경 사항은 데몬을 다시 시작한 후에 적용됩니다.",
|
||||
"settings_debug_select": "데몬 디버그 로깅을 활성화할 카테고리를 선택하세요 (-debug= 플래그).",
|
||||
@@ -1312,19 +1759,20 @@
|
||||
"settings_encrypt_first_pin": "PIN을 활성화하려면 먼저 지갑을 암호화하세요",
|
||||
"settings_encrypt_wallet": "지갑 암호화",
|
||||
"settings_explorer_hint": "URL에 후행 슬래시를 포함해야 합니다. txid/주소가 추가됩니다.",
|
||||
"settings_export_all": "모두 내보내기...",
|
||||
"settings_export_csv": "CSV 내보내기...",
|
||||
"settings_export_key": "키 내보내기...",
|
||||
"settings_export_all": "모두 내보내기…",
|
||||
"settings_export_csv": "CSV 내보내기…",
|
||||
"settings_export_key": "키 내보내기…",
|
||||
"settings_gradient_bg": "그라데이션 배경",
|
||||
"settings_gradient_desc": "텍스처 배경을 부드러운 그라데이션으로 교체",
|
||||
"settings_idle_after": "후",
|
||||
"settings_import_key": "개인 키 가져오기...",
|
||||
"settings_import_viewkey": "조회 키 가져오기...",
|
||||
"settings_import_key": "개인 키 가져오기…",
|
||||
"settings_import_viewkey": "조회 키 가져오기…",
|
||||
"settings_language_note": "참고: 일부 텍스트는 업데이트하려면 다시 시작해야 합니다",
|
||||
"settings_lock_now": "지금 잠금",
|
||||
"settings_locked": "잠김",
|
||||
"settings_merge_to_address": "주소로 병합...",
|
||||
"settings_merge_to_address": "주소로 병합…",
|
||||
"settings_noise_opacity": "노이즈 불투명도:",
|
||||
"settings_not_connected": "데몬에 연결되지 않음",
|
||||
"settings_not_encrypted": "암호화되지 않음",
|
||||
"settings_not_found": "찾을 수 없음",
|
||||
"settings_open_app_dir": "앱 폴더 열기",
|
||||
@@ -1335,29 +1783,36 @@
|
||||
"settings_privacy": "개인 정보",
|
||||
"settings_quick_unlock_pin": "빠른 잠금 해제 PIN",
|
||||
"settings_reduce_transparency": "투명도 줄이기",
|
||||
"settings_reloaded": "디스크에서 설정을 다시 불러왔습니다",
|
||||
"settings_remove_encryption": "암호화 제거",
|
||||
"settings_remove_pin": "PIN 제거",
|
||||
"settings_request_payment": "결제 요청...",
|
||||
"settings_request_payment": "결제 요청…",
|
||||
"settings_rescan_desc": "누락된 거래를 찾기 위해 블록체인 재스캔",
|
||||
"settings_restart_daemon": "데몬 재시작",
|
||||
"settings_rpc_connection": "RPC 연결",
|
||||
"settings_rpc_error_prefix": "RPC 오류: ",
|
||||
"settings_rpc_note": "참고: 연결 설정은 보통 DRAGONX.conf에서 자동 감지됩니다",
|
||||
"settings_rpc_ok": "RPC 연결 정상",
|
||||
"settings_save_shielded_desc": "z-addr 거래를 로컬 파일에 저장하여 조회",
|
||||
"settings_save_shielded_local": "차폐 거래 기록을 로컬에 저장",
|
||||
"settings_saved": "설정이 저장되었습니다",
|
||||
"settings_set_pin": "PIN 설정",
|
||||
"settings_shield_mining": "채굴 차폐...",
|
||||
"settings_shield_mining": "채굴 차폐…",
|
||||
"settings_solid_colors_desc": "블러 효과 대신 단색 사용 (접근성)",
|
||||
"settings_theme_refreshed": "테마 목록을 새로고침했습니다",
|
||||
"settings_tor_desc": "향상된 개인 정보 보호를 위해 모든 연결을 Tor를 통해 라우팅",
|
||||
"settings_unlocked": "잠금 해제",
|
||||
"settings_use_tor_network": "네트워크 연결에 Tor 사용",
|
||||
"settings_validate_address": "주소 확인...",
|
||||
"settings_validate_address": "주소 확인…",
|
||||
"settings_visual_effects": "시각 효과",
|
||||
"settings_wallet_file_size": "지갑 파일 크기: %s",
|
||||
"settings_wallet_info": "지갑 정보",
|
||||
"settings_wallet_location": "지갑 위치: %s",
|
||||
"settings_wallet_maintenance": "지갑 유지보수",
|
||||
"settings_wallet_not_found": "지갑 파일을 찾을 수 없음",
|
||||
"settings_wallet_size_label": "지갑 크기:",
|
||||
"settings_wallet_size_label": "지갑 크기",
|
||||
"settings_ztx_cleared": "Z-거래 내역이 삭제되었습니다",
|
||||
"settings_ztx_not_found": "내역 파일을 찾을 수 없습니다",
|
||||
"setup_wizard": "설정 마법사",
|
||||
"share": "공유",
|
||||
"shield_check_status": "상태 확인",
|
||||
@@ -1405,17 +1860,34 @@
|
||||
"status": "상태",
|
||||
"stop_external": "외부 데몬 중지",
|
||||
"stop_mining": "채굴 중지",
|
||||
"stratum_allowip_hint": "채굴자를 허용할 IP 또는 CIDR (비워두면 이 컴퓨터만 허용)",
|
||||
"stratum_expose_warn": "허용한 네트워크로 채굴 포트를 엽니다 — 신뢰할 수 있는 LAN에서만 사용하세요.",
|
||||
"stratum_host": "마이닝 풀 호스팅 (stratum)",
|
||||
"stratum_host_hint": "채굴자는 RandomX stratum 마이너로 포트 22769(RPC 포트 + 1000)에서 이 컴퓨터에 연결합니다. 적용하려면 데몬을 재시작하세요.",
|
||||
"stratum_host_section": "마이닝 풀 호스팅",
|
||||
"submitting_transaction": "거래 제출 중...",
|
||||
"success": "성공",
|
||||
"summary": "요약",
|
||||
"sweep_button": "쓸어담기",
|
||||
"sweep_caveat": "키를 가져와 모든 자금을 내 주소로 옮기는 거래에 서명합니다. 키는 잔액이 0인 상태로 지갑에 남습니다.",
|
||||
"sweep_current_theme_only": "현재 테마만",
|
||||
"sweep_dest_label": "쓸어담은 자금을 보낼 주소",
|
||||
"sweep_dest_new": "새 차폐 주소 (권장)",
|
||||
"sweep_done": "완료 — 자금을 내 주소로 쓸어담았습니다.",
|
||||
"sweep_to": "쓸어담은 주소:",
|
||||
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
|
||||
"sweep_tx": "거래:",
|
||||
"swin_connection_failed": "연결 실패: ",
|
||||
"swin_connection_successful": "연결에 성공했습니다!\ndragonxd 버전: ",
|
||||
"swin_invalid_suffix": " (유효하지 않음)",
|
||||
"swin_no_history_file_found": "기록 파일을 찾을 수 없습니다",
|
||||
"swin_rescan_failed": "다시 스캔 실패: ",
|
||||
"swin_rescan_started_from_block": "다시 스캔이 시작된 블록 ",
|
||||
"swin_rescan_to": " ~ ",
|
||||
"swin_rpc_client_not_initialized": "RPC 클라이언트가 초기화되지 않았습니다",
|
||||
"swin_settings_saved": "설정이 저장되었습니다",
|
||||
"swin_theme_list_refreshed": "테마 목록을 새로고침했습니다",
|
||||
"swin_ztx_history_cleared": "Z-트랜잭션 기록이 삭제되었습니다",
|
||||
"switch_corrupt_body": "이 지갑이 손상된 것 같습니다. 노드가 열 수 없습니다. 백업에서 복원하거나 다시 만들거나 복구를 시도하세요.",
|
||||
"switch_corrupt_repair": "복구 시도(salvage)",
|
||||
"switch_progress_background": "백그라운드에서 계속",
|
||||
@@ -1440,6 +1912,7 @@
|
||||
"theme": "테마",
|
||||
"theme_effects": "테마 효과",
|
||||
"theme_language": "테마 및 언어",
|
||||
"tile_click_to_open": "클릭하여 열기",
|
||||
"time_days_ago": "%d일 전",
|
||||
"time_hours_ago": "%d시간 전",
|
||||
"time_minutes_ago": "%d분 전",
|
||||
@@ -1454,7 +1927,9 @@
|
||||
"to_upper": "받는 곳",
|
||||
"tools": "도구",
|
||||
"tools_actions": "도구 및 작업...",
|
||||
"tools_actions_hdr": "도구 및 작업",
|
||||
"total": "합계",
|
||||
"total_balance_label": "총 잔액",
|
||||
"transaction_id": "거래 ID",
|
||||
"transaction_sent": "거래 전송 성공",
|
||||
"transaction_sent_msg": "거래가 전송되었습니다!",
|
||||
@@ -1476,7 +1951,7 @@
|
||||
"tt_auto_shield": "개인 정보 보호를 위해 투명 잔액을 자동으로 차폐 주소로 이동",
|
||||
"tt_backup": "wallet.dat 백업 만들기",
|
||||
"tt_block_explorer": "브라우저에서 DragonX 블록 탐색기 열기",
|
||||
"tt_blur": "블러 양 (0%% = 끔, 100%% = 최대)",
|
||||
"tt_blur": "블러 양 (0% = 끔, 100% = 최대)",
|
||||
"tt_change_pass": "지갑 암호화 비밀번호 변경",
|
||||
"tt_change_pin": "잠금 해제 PIN 변경",
|
||||
"tt_chat_bubble_accent": "보내는 메시지 말풍선의 강조 색상(또는 현재 테마를 따름)",
|
||||
@@ -1539,7 +2014,7 @@
|
||||
"tt_low_spec": "모든 고부하 시각 효과 비활성화\\n단축키: Ctrl+Shift+Down",
|
||||
"tt_merge": "여러 UTXO를 하나의 주소로 통합",
|
||||
"tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작",
|
||||
"tt_noise": "그레인 텍스처 강도 (0%% = 끔, 100%% = 최대)",
|
||||
"tt_noise": "그레인 텍스처 강도 (0% = 끔, 100% = 최대)",
|
||||
"tt_open_app_dir": "파일 관리자에서 ObsidianDragon 폴더(설정, 테마, 로그)를 엽니다",
|
||||
"tt_open_data_dir": "지갑 및 블록체인 데이터가 있는 폴더를 파일 탐색기에서 엽니다",
|
||||
"tt_open_dir": "파일 탐색기에서 열려면 클릭",
|
||||
@@ -1573,14 +2048,17 @@
|
||||
"tt_simple_bg": "배경에 단순 그라데이션 사용\\n단축키: Ctrl+Up",
|
||||
"tt_simple_bg_alt": "테마 배경 이미지의 그라데이션 버전 사용\\n단축키: Ctrl+Up",
|
||||
"tt_stop_external": "이 지갑 외부에서 시작된\\n데몬에 연결할 때 적용",
|
||||
"tt_stratum_host": "이 노드에서 RandomX stratum 풀 서버를 실행하여 다른 RandomX 채굴자가 이 컴퓨터를 사용할 수 있게 합니다. v1.3.0 이상 노드와 적용을 위한 데몬 재시작이 필요합니다.",
|
||||
"tt_sweep_current_theme_only": "모든 테마를 순회하는 대신 활성 테마만 스윕합니다",
|
||||
"tt_test_conn": "데몬에 대한 RPC 연결 확인",
|
||||
"tt_theme_effects": "테마별 반짝임, 글로우, 색조 순환",
|
||||
"tt_theme_hotkey": "단축키: Ctrl+왼쪽/오른쪽으로 테마 전환",
|
||||
"tt_tor": "익명성을 위해 데몬 연결을 Tor 네트워크를 통해 라우팅",
|
||||
"tt_tx_url": "블록 탐색기에서 거래를 보기 위한 기본 URL",
|
||||
"tt_ui_opacity": "카드 및 사이드바 불투명도 (100%% = 완전 불투명, 낮을수록 더 투명)",
|
||||
"tt_ui_opacity": "카드 및 사이드바 불투명도 (100% = 완전 불투명, 낮을수록 더 투명)",
|
||||
"tt_validate": "DragonX 주소가 유효한지 확인",
|
||||
"tt_verbose": "콘솔 탭에 상세 연결 진단,\\n데몬 상태 및 포트 소유자 정보 기록",
|
||||
"tt_wallet_size_warn": "보호 지갑은 각 노트의 위트니스 데이터로 인해 커집니다 — 여러 노트를 하나의 주소로 병합하면 크기를 줄일 수 있습니다. 먼저 백업하세요.",
|
||||
"tt_wallets_button": "지갑 파일 목록을 보고 전환합니다",
|
||||
"tt_website": "DragonX 웹사이트 열기",
|
||||
"tt_window_opacity": "배경 불투명도 (낮을수록 = 창을 통해 바탕 화면이 보임)",
|
||||
@@ -1631,6 +2109,7 @@
|
||||
"validate_not_mine": "이 지갑에 속하지 않음",
|
||||
"validate_ownership": "소유자:",
|
||||
"validate_results": "결과:",
|
||||
"validate_results_placeholder": "결과가 여기에 표시됩니다",
|
||||
"validate_shielded_type": "차폐 (z 주소)",
|
||||
"validate_status": "상태:",
|
||||
"validate_title": "주소 검증",
|
||||
@@ -1646,8 +2125,68 @@
|
||||
"view_on_explorer": "탐색기에서 보기",
|
||||
"waiting_for_daemon": "데몬 연결 대기 중...",
|
||||
"wallet": "지갑",
|
||||
"wallet_degraded_notify": "지갑이 기능 제한 모드로 열렸습니다. 기존 자금은 안전하며 사용할 수 있지만, 새 주소 생성과 보호 처리는 비활성화되어 있습니다. 시드 문구를 백업하고 이를 통해 복원하면 지갑을 완전히 복구할 수 있습니다.",
|
||||
"wallet_diagnostics_hdr": "진단",
|
||||
"wallet_empty": "지갑이 비어 있습니다",
|
||||
"wallet_empty_hint": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
|
||||
"wallet_options_hdr": "옵션",
|
||||
"wallet_rebuild_failed": "재구축 결과가 유효한 지갑이 아니어서 아무것도 변경되지 않았습니다. 지갑은 그대로입니다.",
|
||||
"wallet_rebuild_install_failed": "재구축된 지갑을 설치하지 못했습니다. 기존 지갑은 그대로 유지되었습니다.",
|
||||
"wallet_rebuild_no_helper": "이 빌드에서는 지갑 재구축 도우미를 사용할 수 없습니다. 복원을 사용하거나 수동으로 재구축하세요.",
|
||||
"wallet_rebuild_no_source": "재구축할 읽을 수 있는 지갑을 찾지 못했습니다. 아무것도 변경되지 않았습니다.",
|
||||
"wallet_rebuild_ok": "지갑 데이터베이스가 재구축되었습니다 — 노드가 이를 불러오고 잔액을 다시 스캔하는 중입니다.",
|
||||
"wallet_rebuild_started": "지갑 파일을 복구하는 중입니다 — 이 창을 닫지 마세요.",
|
||||
"wallet_recovered_body": "삭제된 것은 없습니다. 원본 지갑은 날짜가 표시된 백업 파일로 컴퓨터에 그대로 저장되어 있으며, 아래의 모든 옵션은 파일을 복사하거나 이름을 바꿀 뿐 절대 삭제하지 않습니다. 키는 다시 생성되지 않고 다시 읽히기만 합니다.\n\n지금 불러온 복구본에는 최근 거래 몇 건이 누락되었을 수 있어, 아래 작업을 마치고 다시 스캔할 때까지 잔액이 약간 적게 보일 수 있습니다.",
|
||||
"wallet_recovered_dismiss": "나중에 — 복구본 계속 사용",
|
||||
"wallet_recovered_dismiss_sub": "파일은 변경되지 않습니다. 상태 표시줄에서 언제든 다시 열 수 있으며, 원본은 어떤 경우에도 안전하게 백업된 상태로 남습니다.",
|
||||
"wallet_recovered_notify": "지갑 파일을 복구해야 했습니다 — 원본은 안전하게 백업되었습니다. 앱을 열어 옵션을 확인하세요.",
|
||||
"wallet_recovered_open_folder": "파일 보기",
|
||||
"wallet_recovered_open_folder_sub": "지갑 데이터 폴더를 열어 백업 파일을 직접 확인할 수 있습니다 — 아무것도 변경되지 않습니다.",
|
||||
"wallet_recovered_rebuild": "자동으로 복구 (권장)",
|
||||
"wallet_recovered_rebuild_sub": "가장 온전한 지갑 파일에서 복구 가능한 모든 기록을 다시 읽어 깨끗한 새 파일로 작성한 다음 재시작합니다. 가장 철저한 방법이며 — 현재 파일은 어떤 경우에도 날짜가 표시된 백업으로 보관됩니다.",
|
||||
"wallet_recovered_restore": "대신 원본 파일 복원",
|
||||
"wallet_recovered_restore_sub": "가장 큰 원본 백업을 그대로 제자리에 넣은 다음 재스캔합니다 — 조금 더 빠르지만 그 파일이 담고 있던 만큼만 완전합니다. 현재 파일은 어떤 경우에도 날짜가 표시된 백업으로 보관됩니다.",
|
||||
"wallet_recovered_safety": "코인은 안전합니다.",
|
||||
"wallet_recovered_title": "지갑 파일에 간단한 복구가 필요합니다",
|
||||
"wallet_recovered_warn": "앱이 시작될 때 지갑 파일이 무결성 검사를 통과하지 못한 것을 발견했습니다 — 보통 앱 업데이트 후나 비정상 종료 후에 발생합니다. 앱은 이미 데이터를 보호했습니다. 기존 파일을 따로 보관하고 복구된 사본을 불러와서 지갑을 계속 사용할 수 있게 했습니다.",
|
||||
"wallet_recovery_decide_later": "나중에 결정",
|
||||
"wallet_recovery_decide_later_tip": "이 창을 닫고 지금 불러온 사본을 그대로 사용합니다. 아무것도 변경되지 않으며, 언제든 복구할 수 있습니다 — 상태 표시줄에 “지갑 복구 가능” 링크가 계속 표시됩니다.",
|
||||
"wallet_recovery_details_label": "기술 세부정보 표시",
|
||||
"wallet_recovery_done": "완료",
|
||||
"wallet_recovery_failure_body": "복구가 실행되었지만 결과가 검증을 통과하지 못했습니다 (읽을 수 없거나 주소가 없음). 그래서 무엇도 교체하기 전에 자동으로 폐기되었습니다. 지갑은 클릭하기 전과 정확히 동일하며 — 디스크에는 아무 변경도 없습니다.",
|
||||
"wallet_recovery_failure_title": "복구가 완료되지 않았습니다",
|
||||
"wallet_recovery_files_detail": "원본 지갑은 그대로 있습니다 — 앱이 데이터 폴더에서 원본 이름을 날짜가 표시된 백업(wallet.<숫자>.bak)으로 변경했을 뿐, 아무것도 삭제하지 않았습니다. “복구”는 가장 온전한 지갑의 모든 기록을 읽어 완전히 새로운 깨끗한 파일로 만듭니다. “복원”은 가장 큰 백업을 있는 그대로 정확히 복사합니다. 지금 불러온 사본 역시 백업으로 보관됩니다.",
|
||||
"wallet_recovery_files_label": "내 파일은 어떻게 되나요?",
|
||||
"wallet_recovery_notnow_short": "나중에",
|
||||
"wallet_recovery_other_options": "다른 옵션",
|
||||
"wallet_recovery_rebuild_card": "자동으로 복구",
|
||||
"wallet_recovery_rebuild_card_desc": "복구 가능한 모든 기록을 깨끗한 파일로 읽어 들인 다음 재시작합니다. 가장 철저한 방법입니다.",
|
||||
"wallet_recovery_recommended": "권장",
|
||||
"wallet_recovery_repair_go": "복구",
|
||||
"wallet_recovery_rescan_body": "잔액과 거래 내역을 재구성하기 위해 블록체인을 재스캔하고 있습니다. 몇 분 정도 걸릴 수 있습니다 — 아무 작업도 할 필요 없으며, 실행 중에는 노드를 재시작하지 마세요.",
|
||||
"wallet_recovery_rescan_elapsed": "%s 동안 진행 중",
|
||||
"wallet_recovery_rescan_size": "복구된 지갑 크기는 현재 %s이며 계속 채워지는 중입니다",
|
||||
"wallet_recovery_rescan_slow": "평소보다 오래 걸리고 있으며, 지갑이 클 경우 정상입니다. 그대로 실행되도록 두어도 안전합니다 — 고급 ▸ 콘솔에서 진행 상황을 확인할 수 있습니다.",
|
||||
"wallet_recovery_rescan_title": "지갑 복구 마무리 중",
|
||||
"wallet_recovery_restore_card": "원본 복원",
|
||||
"wallet_recovery_restore_card_desc": "가장 큰 원본 백업을 그대로 되돌려 놓습니다 — 그 파일이 담고 있던 만큼만 완전합니다.",
|
||||
"wallet_recovery_restore_go": "복원",
|
||||
"wallet_recovery_success_body": "앱이 복구된 지갑을 불러왔으며 잔액을 합산하기 위해 재스캔하고 있습니다 — 몇 분 정도 걸릴 수 있습니다. 가능한 모든 기록을 읽어 들이지만, 드물게 손상된 파일에서는 이전보다 주소가 조금 적게 — 또는 가끔 더 많이 — 복구될 수 있습니다. 재스캔이 끝나면 잔액과 내역이 올바른지 확인하세요.",
|
||||
"wallet_recovery_success_restore": "가장 큰 백업 파일이 제자리로 돌아와 지금 불러오는 중이며, 앱이 재스캔하고 있습니다. 있는 그대로 복원되었으므로 그 파일이 담고 있던 만큼만 완전합니다 — 재스캔이 끝나면 잔액을 확인하세요.",
|
||||
"wallet_recovery_try_other": "다른 옵션 시도",
|
||||
"wallet_recovery_whats_happened": "무슨 일이 있었나요?",
|
||||
"wallet_recovery_working_label": "작업 중",
|
||||
"wallet_restore_bad_backup": "백업 지갑 파일을 읽을 수 없어 복원되지 않았습니다 — 현재 지갑은 변경되지 않았습니다. 대신 직접 보관한 백업에서 복원하세요.",
|
||||
"wallet_restore_busy": "노드가 재시작 중이라 바쁩니다 — 잠시 후 다시 시도하세요.",
|
||||
"wallet_restore_copy_failed": "백업 지갑을 설치하지 못했습니다. 현재 지갑은 그대로 유지되었습니다.",
|
||||
"wallet_restore_move_failed": "현재 지갑을 따로 보관하지 못했습니다 — 아무것도 변경되지 않았습니다.",
|
||||
"wallet_restore_no_backup": "복원할 wallet.<타임스탬프>.bak을 찾을 수 없습니다. 아무것도 변경되지 않았습니다.",
|
||||
"wallet_restore_no_restart": "원본 지갑은 복원되었지만 노드가 재시작되지 않았습니다 — 설정에서 노드를 시작하세요.",
|
||||
"wallet_restore_ok": "원본 지갑이 복원되었습니다. 노드가 지금 불러오고 있습니다.",
|
||||
"wallet_restore_started": "원본 파일을 복원하는 중입니다 — 이 창을 닫지 마세요.",
|
||||
"wallet_restore_stop_failed": "노드가 제시간에 멈추지 않아 아무것도 변경되지 않았습니다. 다시 시도하세요.",
|
||||
"wallet_size_consolidate": "노트 통합…",
|
||||
"wallet_size_warn": "이 지갑 파일이 큽니다. 노트를 통합하면 추가 증가를 억제할 수 있습니다.",
|
||||
"wallets_active": "활성",
|
||||
"wallets_add": "폴더 추가",
|
||||
"wallets_add_folder": "지갑 파일을 검색할 폴더를 추가로 지정:",
|
||||
@@ -1690,6 +2229,7 @@
|
||||
"wallets_open_inplace_tt": "이 지갑을 있는 자리에서 엽니다 — 데이터 디렉터리에 링크 (복사 없음)",
|
||||
"wallets_remove_folder": "이 폴더 스캔 중지",
|
||||
"wallets_reveal": "폴더 열기",
|
||||
"wallets_scan_folder": "다른 폴더에서 지갑 검색…",
|
||||
"wallets_scanned_folders": "스캔한 폴더:",
|
||||
"wallets_sort_addresses": "주소",
|
||||
"wallets_sort_asc": "오름차순 (오래된/적은/작은 순)",
|
||||
@@ -1759,18 +2299,30 @@
|
||||
"wiz_welcome_sub": "몇 가지만 선택하면 풀 노드가 준비됩니다.",
|
||||
"wiz_welcome_title": "ObsidianDragon에 오신 것을 환영합니다!",
|
||||
"wizard_daemon_start_failed": "데몬 시작 실패 — 자동으로 재시도됩니다",
|
||||
"xmrig_active_tt": "현재 설치된 (사용 중인) 채굴기",
|
||||
"xmrig_browse_releases": "모든 릴리스 보기…",
|
||||
"xmrig_checking": "최신 채굴기를 확인하는 중…",
|
||||
"xmrig_current": "현재:",
|
||||
"xmrig_downgrade_note": "다른 버전의 채굴기를 설치하면 현재 버전을 대체합니다. 먼저 채굴을 중지하면 다음 시작 시 적용됩니다.",
|
||||
"xmrig_download_install": "다운로드 및 설치",
|
||||
"xmrig_downloading": "다운로드 중…",
|
||||
"xmrig_install_this": "이 버전 설치",
|
||||
"xmrig_installed": "설치됨:",
|
||||
"xmrig_installed_ok": "채굴기가 설치되었습니다",
|
||||
"xmrig_installing": "설치 중…",
|
||||
"xmrig_latest": "최신:",
|
||||
"xmrig_latest_badge": "최신",
|
||||
"xmrig_latest_tt": "사용 가능한 최신 버전",
|
||||
"xmrig_loading_releases": "릴리스를 불러오는 중…",
|
||||
"xmrig_no_notes": "이 버전에 대한 릴리스 노트가 없습니다.",
|
||||
"xmrig_none": "없음",
|
||||
"xmrig_prerelease_tt": "사전 릴리스 / 테스트 빌드",
|
||||
"xmrig_reinstall": "재설치",
|
||||
"xmrig_released": "출시일:",
|
||||
"xmrig_releases": "xmrig 릴리스",
|
||||
"xmrig_status_current": "이 버전이 설치되어 있습니다",
|
||||
"xmrig_status_latest": "최신 버전",
|
||||
"xmrig_status_older": "설치된 버전보다 이전 버전입니다",
|
||||
"xmrig_stop_mining_first": "채굴기를 업데이트하기 전에 채굴을 중지하세요.",
|
||||
"xmrig_unavailable_body": "이 플랫폼에서 사용 가능한 채굴기 빌드가 없습니다.",
|
||||
"xmrig_unavailable_title": "채굴기 업데이트를 사용할 수 없습니다",
|
||||
|
||||
+568
-17
@@ -19,6 +19,7 @@
|
||||
"about_license_text": "Este software é disponibilizado sob a Licença Pública Geral GNU v3 (GPLv3). Você é livre para usar, modificar e distribuir este software sob os termos da licença.",
|
||||
"about_peers_count": "%zu pares",
|
||||
"about_release": "Versão",
|
||||
"about_source": "Origem",
|
||||
"about_title": "Sobre o ObsidianDragon",
|
||||
"about_version": "Versão:",
|
||||
"about_website": "Website",
|
||||
@@ -60,7 +61,84 @@
|
||||
"amount_label": "Valor:",
|
||||
"animate_avatars": "Animar avatares",
|
||||
"appearance": "APARÊNCIA",
|
||||
"appx_back": "Voltar",
|
||||
"appx_back_up_seed_phrase_title": "Faça o backup da sua frase-semente",
|
||||
"appx_birthday_block_height": "Nascimento (altura do bloco): %llu — faça o backup disto também.",
|
||||
"appx_blockchain_data_deleted": "Dados da blockchain excluídos (%d itens). O daemon está reiniciando para ressincronizar com a rede.",
|
||||
"appx_blockchain_maintenance_in_progress": "Uma operação de manutenção da blockchain já está em andamento.",
|
||||
"appx_blockchain_rescan_complete": "Reescaneamento da blockchain concluído",
|
||||
"appx_bootstrap_complete_reconciling": "Bootstrap concluído — reconciliando sua carteira com os novos dados da chain.",
|
||||
"appx_cancel": "Cancelar",
|
||||
"appx_cleaning_up": "Limpando...",
|
||||
"appx_confirm_your_backup": "Confirme seu backup",
|
||||
"appx_copied_clipboard_autoclears": "Copiado — a área de transferência será limpa em 45s",
|
||||
"appx_copy": "Copiar",
|
||||
"appx_could_not_start_restore": "Não foi possível iniciar a restauração",
|
||||
"appx_create_failed_prefix": "Falha na criação: ",
|
||||
"appx_creating_your_wallet": "Criando sua carteira…",
|
||||
"appx_daemon_error": "Erro do Daemon",
|
||||
"appx_daemon_reinstall_in_progress": "A reinstalação do daemon já está em andamento.",
|
||||
"appx_disconnecting": "Desconectando...",
|
||||
"appx_done": "Concluído",
|
||||
"appx_dragonxd_output": "saída do dragonxd",
|
||||
"appx_encrypting_wallet": "Criptografando a carteira...",
|
||||
"appx_fullnode_lifecycle_unavailable_lite": "As ações de ciclo de vida do nó completo não estão disponíveis na versão lite",
|
||||
"appx_installing_bundled_daemon": "Instalando o daemon incluído — o nó vai parar, atualizar e reiniciar...",
|
||||
"appx_invalid_payment_uri_prefix": "URI de pagamento inválida: ",
|
||||
"appx_ive_written_it_down": "Já anotei",
|
||||
"appx_keep_node_running_and_quit": "Manter o nó em execução e sair",
|
||||
"appx_last_block_n": "Último bloco: %d",
|
||||
"appx_last_used_wallet_not_found_prefix": "Seu último arquivo de carteira usado (",
|
||||
"appx_last_used_wallet_not_found_suffix": ") não foi encontrado — a carteira padrão foi aberta em seu lugar. Se você o moveu, restaure-o e volte a ele pela lista de carteiras.",
|
||||
"appx_low_spec_mode_disabled": "Modo de baixo desempenho desativado",
|
||||
"appx_low_spec_mode_enabled": "Modo de baixo desempenho ativado",
|
||||
"appx_miner_stopped_prefix": "Minerador parado: ",
|
||||
"appx_miner_stopped_unexpectedly": "O minerador parou inesperadamente.",
|
||||
"appx_n_min_n_sec": "%d min %d s",
|
||||
"appx_n_seconds": "%d segundos",
|
||||
"appx_no_bundled_daemon_to_install": "Esta versão não tem daemon incluído para instalar",
|
||||
"appx_no_embedded_daemon_to_install": "Esta versão não tem daemon embutido para instalar",
|
||||
"appx_node_busy_restarting": "O nó está ocupado reiniciando — tente novamente em instantes.",
|
||||
"appx_node_rebuilding_witness_cache": "O nó está reconstruindo seu cache de testemunhas",
|
||||
"appx_not_next_word": " — essa não é a próxima palavra",
|
||||
"appx_payment_request_loaded": "Solicitação de pagamento carregada",
|
||||
"appx_pool_miner_connected_and_hashing": "Minerador de pool conectado e processando hashes.",
|
||||
"appx_progress_n_of_n": "Progresso: %d / %d",
|
||||
"appx_rebuilding_sapling_note_witnesses": "Reconstruindo testemunhas de notas Sapling…",
|
||||
"appx_rebuilding_witness_cache_blocks_left": "Reconstruindo cache de testemunhas %.0f%% — %d blocos restantes",
|
||||
"appx_rebuilding_witness_cache_pct": "Reconstruindo cache de testemunhas %.0f%%",
|
||||
"appx_recovery_phrase_word_count": "A frase de recuperação deve ter 24 palavras — você tem %d.",
|
||||
"appx_restarting_daemon_rescan_flag": "Reiniciando o daemon com a flag -rescan...",
|
||||
"appx_restarting_daemon_zapwallettxes": "Reiniciando o daemon com -zapwallettxes=2 (reparo da carteira)...",
|
||||
"appx_restoring_your_wallet": "Restaurando sua carteira…",
|
||||
"appx_seed_backup_warning": "Estas 24 palavras são a ÚNICA forma de restaurar sua carteira. Anote-as na ordem, guarde-as offline e nunca as compartilhe. Se perdê-las, seus fundos desaparecem para sempre.",
|
||||
"appx_seed_not_backed_up_warning": "Você não fez o backup da sua semente — os fundos podem ser perdidos. Pular mesmo assim?",
|
||||
"appx_sending_stop_command_to_daemon": "Enviando comando de parada ao daemon...",
|
||||
"appx_setting_initial_sapling_witnesses": "Definindo testemunhas Sapling iniciais %.0f%%",
|
||||
"appx_shutdown_complete": "Encerramento concluído",
|
||||
"appx_simple_background_disabled": "Fundo simples desativado",
|
||||
"appx_simple_background_enabled": "Fundo simples ativado",
|
||||
"appx_skip": "Pular",
|
||||
"appx_skip_anyway": "Pular mesmo assim",
|
||||
"appx_still_status_prefix": "Ainda \"",
|
||||
"appx_still_status_suffix": "\" — forçar o encerramento agora pode corromper os dados da chain.",
|
||||
"appx_stop_anyway_and_quit": "Parar mesmo assim e sair",
|
||||
"appx_stopping_daemon_deleting_blockchain": "Parando o daemon e excluindo os dados da blockchain...",
|
||||
"appx_stopping_node_discards_rebuild": "Parar o nó agora descarta a reconstrução em andamento e a reinicia (vários minutos) na próxima vez que você abrir a carteira. Em vez disso, você pode manter o nó em execução.",
|
||||
"appx_stopping_pool_miner": "Parando o minerador de pool...",
|
||||
"appx_syncing_pct_block_n_of_n": "Sincronizando %.1f%% — Bloco %d / %d",
|
||||
"appx_tap_words_in_order": "Toque nas palavras na ordem correta para confirmar que você as salvou.",
|
||||
"appx_theme_effects_disabled": "Efeitos de tema desativados",
|
||||
"appx_theme_effects_enabled": "Efeitos de tema ativados",
|
||||
"appx_theme_prefix": "Tema: ",
|
||||
"appx_use_settings_restart_daemon_hint": "Use Configurações > Reiniciar Daemon para tentar novamente",
|
||||
"appx_waiting_for_daemon_to_encrypt_wallet": "Aguardando o daemon criptografar a carteira...",
|
||||
"appx_wallet_created_and_backed_up": "Carteira criada e com backup feito.",
|
||||
"appx_wallet_open_failed_prefix": "Falha ao abrir a carteira: ",
|
||||
"auto_shield": "Auto-blindar mineração",
|
||||
"autoshield_by_node": "Seu node blinda automaticamente o coinbase minerado para você.",
|
||||
"autoshield_off_backup_seed": "O coinbase minerado ainda não está sendo blindado automaticamente. Faça backup da sua frase de recuperação (Configurações → Backup e Dados) para que seu node possa ativar isso com segurança.",
|
||||
"autoshield_off_generic": "Seu node não está blindando automaticamente o coinbase minerado no momento.",
|
||||
"av_intro": "Softwares de mineração costumam ser sinalizados como potencialmente indesejados. Siga estes passos para habilitar a mineração em pool:",
|
||||
"av_open_security": "Abrir Segurança do Windows",
|
||||
"av_step1": "Passo 1: Adicionar exclusão",
|
||||
@@ -74,6 +152,9 @@
|
||||
"av_title": "Windows Defender bloqueou o minerador",
|
||||
"available": "Disponível",
|
||||
"backup_backing_up": "Fazendo backup...",
|
||||
"backup_col_backup": "BACKUP",
|
||||
"backup_col_export": "EXPORTAR",
|
||||
"backup_col_import": "IMPORTAR E RESTAURAR",
|
||||
"backup_create": "Criar Backup",
|
||||
"backup_created": "Backup da carteira criado",
|
||||
"backup_data": "BACKUP & DADOS",
|
||||
@@ -92,8 +173,22 @@
|
||||
"balance": "Saldo",
|
||||
"balance_history_collecting": "Histórico de saldo — coletando dados...",
|
||||
"balance_layout": "Layout do Saldo",
|
||||
"balance_layout_switched": "Layout: %s",
|
||||
"balance_mining_rate": "Minerando %s",
|
||||
"balance_shielded_fmt": "Blindado: %.8f",
|
||||
"balance_syncing_pct": "Sincronizando %.1f%%",
|
||||
"balance_transparent_fmt": "Transparente: %.8f",
|
||||
"baltab_market": "Mercado",
|
||||
"baltab_market_price_4dp": "Mercado: $%.4f",
|
||||
"baltab_market_price_8dp": "Mercado: $%.8f",
|
||||
"baltab_pct_change_24h": "%s%.1f%% 24h",
|
||||
"baltab_pct_of_total_zaddr": "%.0f%% do total · %d Z-addr",
|
||||
"baltab_shielded": "Blindado",
|
||||
"baltab_shielded_amount": "Blindado %.8f",
|
||||
"baltab_t_addresses_count": "%d T-addresses",
|
||||
"baltab_total_balance": "Saldo Total",
|
||||
"baltab_transparent": "Transparente",
|
||||
"baltab_transparent_amount": "Transparente %.8f",
|
||||
"ban": "Banir",
|
||||
"banned_peers": "Pares Banidos",
|
||||
"block": "Bloco",
|
||||
@@ -101,6 +196,12 @@
|
||||
"block_click_copy": "Clique para copiar",
|
||||
"block_click_next": "Clique para ver o próximo bloco",
|
||||
"block_click_prev": "Clique para ver o bloco anterior",
|
||||
"block_db_reindex_body": "Isso geralmente acontece após uma atualização do daemon alterar o formato em disco, ou se o índice de blocos estiver danificado. Sua carteira e suas moedas estão seguras — o node apenas não consegue carregar a chain, então os saldos aparecem como zero.\n\nReconstruir relê seus arquivos de blocos existentes e pode levar um tempo (também refaz o rescan da sua carteira). Nada é baixado.",
|
||||
"block_db_reindex_confirm": "Reconstruir banco de dados de blocos",
|
||||
"block_db_reindex_notify": "O node não consegue ler seu banco de dados de blocos (geralmente após uma atualização do daemon). Reconstrua-o para restaurar seu saldo — veja o aviso, ou Configurações › Node.",
|
||||
"block_db_reindex_started": "Reconstruindo o banco de dados de blocos a partir dos seus blocos — isso pode levar um tempo.",
|
||||
"block_db_reindex_title": "Reconstruir banco de dados de blocos?",
|
||||
"block_db_reindex_warn": "O node não consegue ler seu banco de dados de blocos.",
|
||||
"block_explorer": "Explorador de Blocos",
|
||||
"block_get_info": "Obter Info do Bloco",
|
||||
"block_hash": "Hash do Bloco:",
|
||||
@@ -132,6 +233,7 @@
|
||||
"bootstrap_verifying": "Verificando somas de verificação...",
|
||||
"bootstrap_wallet_protected": "(wallet.dat está protegido)",
|
||||
"bootstrap_warning": "Os dados de blocos existentes (blocks, chainstate, notarizations) serão excluídos e substituídos. Seu wallet.dat NÃO será modificado ou excluído.",
|
||||
"byte_count_fmt": "%zu / %zu bytes",
|
||||
"cancel": "Cancelar",
|
||||
"change_pass_confirm": "Confirmar nova:",
|
||||
"change_pass_current": "Senha atual:",
|
||||
@@ -147,6 +249,10 @@
|
||||
"chat_accent_theme": "Tema",
|
||||
"chat_add_contact": "Adicionar contato",
|
||||
"chat_awaiting_key": "Aguardando resposta",
|
||||
"chat_blocked_desc": "Conversas bloqueadas são removidas e suas mensagens são descartadas — antigas e novas — até você desbloquear. Desbloquear reimporta a conversa a partir da chain.",
|
||||
"chat_blocked_manage": "Bloqueadas",
|
||||
"chat_blocked_title": "Conversas bloqueadas",
|
||||
"chat_blocked_toast": "Conversa excluída e bloqueada",
|
||||
"chat_bubble_minimal": "Mínimo",
|
||||
"chat_bubble_rounded": "Arredondado",
|
||||
"chat_bubble_square": "Quadrado",
|
||||
@@ -159,6 +265,16 @@
|
||||
"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_delete": "Excluir conversa",
|
||||
"chat_delete_block": "Excluir e bloquear",
|
||||
"chat_delete_body_prefix": "Excluir sua cópia local da conversa com ",
|
||||
"chat_delete_body_suffix": "?",
|
||||
"chat_delete_confirm": "Excluir",
|
||||
"chat_delete_failed": "Não foi possível excluir a conversa — nada foi alterado.",
|
||||
"chat_delete_local_note": "Isso afeta apenas este dispositivo — as mensagens permanecem na blockchain e a outra pessoa mantém sua cópia.",
|
||||
"chat_delete_revive_note": "\"Excluir\" limpa o histórico neste dispositivo. Se essa pessoa enviar uma mensagem novamente, a conversa volta.",
|
||||
"chat_delete_title": "Excluir conversa?",
|
||||
"chat_deleted_toast": "Conversa excluída",
|
||||
"chat_density_comfortable": "Confortável",
|
||||
"chat_density_compact": "Compacta",
|
||||
"chat_emoji_color": "Colorido",
|
||||
@@ -182,6 +298,7 @@
|
||||
"chat_new_button": "Novo chat",
|
||||
"chat_new_message": "Mensagem",
|
||||
"chat_new_message_toast": "Nova mensagem de chat criptografada",
|
||||
"chat_new_needs_zaddr": "O chat precisa de um endereço blindado (z) — endereços transparentes (t) não podem receber mensagens criptografadas.",
|
||||
"chat_new_send": "Enviar solicitação",
|
||||
"chat_new_title": "Novo chat",
|
||||
"chat_new_zaddr": "Endereço-z do destinatário",
|
||||
@@ -227,6 +344,8 @@
|
||||
"chat_ts_24h": "24 horas",
|
||||
"chat_ts_global": "Seguir global",
|
||||
"chat_ts_global_short": "Global",
|
||||
"chat_unblock": "Desbloquear",
|
||||
"chat_unblocked_toast": "Conversa desbloqueada",
|
||||
"chat_unhide": "Mostrar",
|
||||
"chat_unmute": "Reativar som",
|
||||
"chat_verify_key": "Chave de identidade — compare para verificar",
|
||||
@@ -279,6 +398,7 @@
|
||||
"console": "Console",
|
||||
"console_accents": "Destaques de cor",
|
||||
"console_app": "App",
|
||||
"console_auto_focus": "Focar entrada ao abrir",
|
||||
"console_auto_scroll": "Rolagem automática",
|
||||
"console_available_commands": "Comandos disponíveis:",
|
||||
"console_backend_reference": "Referência de Comandos do Backend",
|
||||
@@ -373,11 +493,15 @@
|
||||
"console_tab_completion": "Tab para completar",
|
||||
"console_text_colors": "Cores do texto",
|
||||
"console_toggle_accents": "Alternar destaques de cor das linhas",
|
||||
"console_toggle_auto_focus": "Colocar o cursor na caixa de comando ao abrir a aba Console",
|
||||
"console_toggle_text_color": "Alternar cores do texto das linhas",
|
||||
"console_type_help": "Digite 'help' para comandos disponíveis",
|
||||
"console_welcome": "Bem-vindo ao Console ObsidianDragon",
|
||||
"console_zoom_in": "Aumentar zoom",
|
||||
"console_zoom_out": "Diminuir zoom",
|
||||
"consolidate_desc": "Combine muitas entradas pequenas em uma única nota blindada. Menos notas significa um arquivo de carteira menor e mais privacidade.",
|
||||
"consolidate_funds_btn": "Consolidar",
|
||||
"consolidate_title": "Consolidar fundos",
|
||||
"contact_avatar": "AVATAR",
|
||||
"contact_avatar_bad_image": "Não foi possível carregar essa imagem.",
|
||||
"contact_avatar_badge": "Selo",
|
||||
@@ -407,6 +531,7 @@
|
||||
"contacts_shape_square": "Quadrado",
|
||||
"contacts_shape_tab": "Aba",
|
||||
"copied": "Copiado!",
|
||||
"copied_to_clipboard": "Copiado para a área de transferência",
|
||||
"copy": "Copiar",
|
||||
"copy_address": "Copiar Endereço Completo",
|
||||
"copy_error": "Copiar Erro",
|
||||
@@ -419,11 +544,16 @@
|
||||
"daemon_bundled": "Empacotado",
|
||||
"daemon_install_bundled": "Instalar incluído",
|
||||
"daemon_installed": "Instalado",
|
||||
"daemon_maintenance_label": "MANUTENÇÃO",
|
||||
"daemon_none_bundled": "nenhum nesta build",
|
||||
"daemon_not_installed": "não instalado",
|
||||
"daemon_status_diff": "Versão diferente",
|
||||
"daemon_status_differ": "O binário instalado difere da versão empacotada.",
|
||||
"daemon_status_match": "O binário instalado corresponde à versão incluída.",
|
||||
"daemon_status_missing": "Nenhum daemon instalado — instale a versão incluída.",
|
||||
"daemon_status_none": "Não instalado",
|
||||
"daemon_status_ok": "Atualizado",
|
||||
"daemon_update_active_tt": "O node atualmente instalado (ativo)",
|
||||
"daemon_update_available": "Uma nova versão do nó está disponível",
|
||||
"daemon_update_body": "Esta versão da carteira inclui um nó DragonX mais recente do que o atualmente instalado. A atualização substitui o dragonxd instalado (e o dragonx-cli/dragonx-tx), depois para e reinicia o nó para que a nova versão entre em vigor. Recomendado — um nó mais recente pode adicionar recursos (por exemplo, suporte a frase de recuperação) que o antigo não tem.",
|
||||
"daemon_update_browse": "Ver todos os lançamentos…",
|
||||
@@ -433,18 +563,28 @@
|
||||
"daemon_update_download_install": "Baixar e instalar",
|
||||
"daemon_update_downloading": "Baixando…",
|
||||
"daemon_update_failed": "Falha na atualização",
|
||||
"daemon_update_install_this": "Instalar esta versão",
|
||||
"daemon_update_installed": "Instalado:",
|
||||
"daemon_update_installed_ok": "Nó instalado",
|
||||
"daemon_update_installing": "Instalando…",
|
||||
"daemon_update_keep": "Manter atual",
|
||||
"daemon_update_later": "Depois",
|
||||
"daemon_update_latest": "Mais recente:",
|
||||
"daemon_update_latest_badge": "mais recente",
|
||||
"daemon_update_latest_tt": "Versão mais recente disponível",
|
||||
"daemon_update_loading": "Carregando versões…",
|
||||
"daemon_update_no_notes": "Sem notas de versão para esta versão.",
|
||||
"daemon_update_now": "Atualizar agora",
|
||||
"daemon_update_prerelease_tt": "Build de pré-lançamento / teste",
|
||||
"daemon_update_prompt_title": "Atualizar o daemon do node?",
|
||||
"daemon_update_reinstall": "Reinstalar",
|
||||
"daemon_update_released": "Lançado:",
|
||||
"daemon_update_restart_note": "Reinicie o daemon para começar a executar a nova versão.",
|
||||
"daemon_update_restart_now": "Reiniciar daemon agora",
|
||||
"daemon_update_safe": "Sua carteira, chaves e dados da blockchain não são tocados — apenas os arquivos de programa do daemon são substituídos. Se você executa deliberadamente um nó personalizado, escolha Manter atual.",
|
||||
"daemon_update_status_current": "Esta versão está instalada",
|
||||
"daemon_update_status_latest": "Versão mais recente",
|
||||
"daemon_update_status_older": "Mais antiga que sua versão instalada",
|
||||
"daemon_update_title": "Atualizar o daemon do nó?",
|
||||
"daemon_update_unavailable_body": "Nenhuma versão do nó está disponível para esta plataforma.",
|
||||
"daemon_update_unavailable_title": "Atualizações do nó indisponíveis",
|
||||
@@ -453,12 +593,20 @@
|
||||
"daemon_update_verify_note": "O download é verificado contra o SHA-256 publicado do lançamento e uma assinatura ed25519 fixada antes da instalação.",
|
||||
"daemon_update_verifying": "Verificando…",
|
||||
"daemon_update_version": "Versão:",
|
||||
"daemon_updates_label": "ATUALIZAÇÕES",
|
||||
"daemon_version": "Daemon",
|
||||
"dark": "Escuro",
|
||||
"data_stale_prefix": "Atualizado",
|
||||
"data_stale_tooltip": "O saldo pode estar desatualizado — a carteira não recebeu uma atualização recente. Verifique a conexão com o seu nó.",
|
||||
"date": "Data",
|
||||
"date_label": "Data:",
|
||||
"debug_gate_confirm": "Mostrar opções de depuração",
|
||||
"debug_gate_incorrect": "PIN ou senha incorretos.",
|
||||
"debug_gate_pass_prompt": "Digite a senha da sua carteira para continuar:",
|
||||
"debug_gate_pin_prompt": "Digite seu PIN de desbloqueio para continuar:",
|
||||
"debug_gate_title": "Mostrar opções de depuração?",
|
||||
"debug_gate_verifying": "Verificando…",
|
||||
"debug_gate_warning": "Estas são opções avançadas de diagnóstico e ajuste do node. Elas podem expor informações sensíveis e alterar o funcionamento do seu node. Só continue se souber o que está fazendo.",
|
||||
"debug_logging": "REGISTRO DE DEPURAÇÃO",
|
||||
"decrypt_desc": "A carteira será exportada, o daemon reiniciado com uma nova carteira sem criptografia e todas as chaves reimportadas. Isso pode levar alguns minutos, dependendo do tamanho da carteira.",
|
||||
"decrypt_error_title": "Falha na descriptografia",
|
||||
@@ -481,10 +629,22 @@
|
||||
"disconnected": "Desconectado",
|
||||
"dismiss": "Dispensar",
|
||||
"display": "Exibição",
|
||||
"done": "Concluído",
|
||||
"download": "Baixar",
|
||||
"download_bootstrap": "Baixar Bootstrap",
|
||||
"dragonx_green": "DragonX (Verde)",
|
||||
"edit": "Editar",
|
||||
"empty_wallet_keys_suffix": "chaves",
|
||||
"empty_wallet_open_manager": "Abrir gerenciador de carteiras",
|
||||
"empty_wallet_restore": "Restaurar minha carteira",
|
||||
"empty_wallet_salvage_body": "Esta carteira está vazia porque um reparo automático anterior colocou sua carteira original de lado como backup. Suas moedas quase certamente estão nesse backup, não perdidas. Restaure-o para carregar seus fundos novamente — nada é excluído; o arquivo atual é guardado primeiro.",
|
||||
"empty_wallet_salvage_headline": "Suas moedas estão seguras em um arquivo de backup.",
|
||||
"empty_wallet_salvage_title": "Sua carteira pode ter sido reparada",
|
||||
"empty_wallet_warning_body": "Esta carteira não tem endereços nem fundos, mas outro arquivo de carteira na sua pasta do DragonX contém chaves. Suas moedas provavelmente estão nele, não perdidas. Abra o gerenciador de carteiras para mudar para a carteira que contém seus fundos.",
|
||||
"empty_wallet_warning_dismiss": "Não avisar novamente para esta carteira",
|
||||
"empty_wallet_warning_dismiss_tip": "Interrompe este aviso apenas para o arquivo de carteira atual. Se você mudar para outra carteira vazia mais tarde, poderá avisar novamente.",
|
||||
"empty_wallet_warning_headline": "Você pode ter aberto a carteira errada.",
|
||||
"empty_wallet_warning_title": "Esta carteira está vazia",
|
||||
"enc_confirm": "Confirmar:",
|
||||
"enc_desc": "Criptografar sua carteira protege suas chaves privadas com uma senha. Após a criptografia, o daemon será reiniciado.",
|
||||
"enc_encrypting": "Criptografando a carteira...",
|
||||
@@ -523,6 +683,7 @@
|
||||
"explorer_tx_outputs": "Saídas",
|
||||
"explorer_tx_size": "Tamanho",
|
||||
"explorer_txid": "TxID:",
|
||||
"explorer_urls_hdr": "URLS",
|
||||
"export": "Exportar",
|
||||
"export_csv": "Exportar CSV",
|
||||
"export_keys_btn": "Exportar Chaves",
|
||||
@@ -550,6 +711,151 @@
|
||||
"export_viewing_key": "Exportar Chave de Visualização",
|
||||
"failed_create_shielded": "Falha ao criar endereço blindado",
|
||||
"failed_create_transparent": "Falha ao criar endereço transparente",
|
||||
"faq": "Perguntas frequentes",
|
||||
"faq_d_mgmt_1_a": "O node incorporado inicia automaticamente com a carteira e para quando apropriado, então normalmente você não precisa gerenciá-lo manualmente. Os controles avançados (incluindo reiniciar o daemon) ficam em Configurações > Node e Segurança.",
|
||||
"faq_d_mgmt_1_q": "Como inicio ou paro o node?",
|
||||
"faq_d_mgmt_2_a": "Se você conectar a carteira a um node que você mesmo iniciou, esta opção decide se fechar a carteira também encerra esse node. Deixe desativado se quiser que seu node continue rodando após fechar a carteira.",
|
||||
"faq_d_mgmt_2_q": "O que faz \"fechar daemon externo ao sair\"?",
|
||||
"faq_d_mgmt_3_a": "Os dados do node e da carteira ficam no diretório de dados do DragonX dentro do seu perfil de usuário (por exemplo, em AppData no Windows ou na sua pasta pessoal no Linux/macOS). A blockchain é a parte grande; mantenha espaço livre em disco suficiente para ela crescer.",
|
||||
"faq_d_mgmt_3_q": "Onde ficam armazenados a blockchain e os dados da carteira?",
|
||||
"faq_d_mgmt_title": "Gestão do nó",
|
||||
"faq_d_mine_1_a": "A aba Mineração permite minerar com sua CPU. O DragonX usa o algoritmo RandomX, projetado para processadores comuns. Defina o número de threads e inicie — as recompensas se acumulam na sua carteira.",
|
||||
"faq_d_mine_1_q": "Como faço para minerar DragonX?",
|
||||
"faq_d_mine_2_a": "A mineração solo envia recompensas diretamente para você, mas só paga quando você encontra um bloco, o que é raro a menos que você tenha muito hashrate. A mineração em pool compartilha o trabalho com outros para pagamentos menores e mais constantes. A aba Mineração suporta ambos.",
|
||||
"faq_d_mine_2_q": "Solo ou pool — qual devo escolher?",
|
||||
"faq_d_mine_3_a": "A seção de pool da aba Mineração tem um botão 'Atualizar minerador…' que baixa, verifica e instala a versão mais recente do minerador DragonX otimizado. Assim como o atualizador do nó, cada download é verificado por checksum e assinatura antes da instalação.",
|
||||
"faq_d_mine_3_q": "Como faço para atualizar o minerador?",
|
||||
"faq_d_mine_4_a": "O nó atual pode executar um servidor stratum embutido para que outros mineradores possam apontar para sua máquina. Há uma opção para isso em Configurações > Nó & Segurança. Por padrão, ele escuta apenas localmente; expô-lo a outros computadores exige abertura deliberada.",
|
||||
"faq_d_mine_4_q": "Posso hospedar um pool de mineração?",
|
||||
"faq_d_mine_title": "Mineração e Pools",
|
||||
"faq_d_net_1_a": "Logo após a inicialização, o nó precisa de um momento para encontrar peers, então um breve '0 peers' é normal. Se persistir, verifique sua conexão com a internet e se um firewall não está bloqueando o nó. O nó encontra peers automaticamente através de seeds embutidos.",
|
||||
"faq_d_net_1_q": "A carteira mostra nenhum peer ou conexão — o que está errado?",
|
||||
"faq_d_net_2_a": "Sim — os nós DragonX se comunicam entre si via TLS, então as conexões entre peers são criptografadas. A carteira também se comunica com seu próprio nó por um canal local seguro.",
|
||||
"faq_d_net_2_q": "Minha conexão com a rede é criptografada?",
|
||||
"faq_d_net_title": "Peers e Rede",
|
||||
"faq_d_node_1_a": "O daemon, dragonxd, é o nó completo do DragonX. Ele baixa e verifica toda a blockchain, retransmite transações para a rede e responde às consultas da carteira. Rodar seu próprio nó significa que você não precisa confiar no servidor de outra pessoa.",
|
||||
"faq_d_node_1_q": "O que é o daemon (dragonxd)?",
|
||||
"faq_d_node_2_a": "Por padrão, a carteira inicia e gerencia seu próprio nó embutido (embedded) — você não precisa fazer nada. Se você já roda o dragonxd por conta própria, a carteira pode se conectar a esse nó externo. A carteira detecta em qual situação está e se comporta de acordo.",
|
||||
"faq_d_node_2_q": "Nó embutido vs externo — qual a diferença?",
|
||||
"faq_d_node_3_a": "Um nó completo valida independentemente cada regra da blockchain, então sua carteira confia em matemática em vez de em terceiros. Isso também fortalece a rede. O custo é espaço em disco e uma sincronização inicial.",
|
||||
"faq_d_node_3_q": "Por que rodar um nó completo?",
|
||||
"faq_d_node_title": "O Nó Completo",
|
||||
"faq_d_perf_1_a": "A blockchain é o maior item e cresce com o tempo, então mantenha vários gigabytes livres. O nó usa um cache de banco de dados para velocidade; os padrões incluídos são ajustados para máquinas comuns. Usuários avançados podem ajustar o cache de banco de dados do nó editando dbcache no arquivo de configuração (DRAGONX.conf).",
|
||||
"faq_d_perf_1_q": "Quanto disco o nó usa, e posso ajustar a memória?",
|
||||
"faq_d_perf_2_a": "Ative o download de bootstrap (oferecido no assistente de configuração) para obter uma cópia recente e verificada da chain em vez de validar cada bloco do zero. Um disco rápido (SSD) também ajuda bastante.",
|
||||
"faq_d_perf_2_q": "Como faço para acelerar a sincronização inicial?",
|
||||
"faq_d_perf_title": "Armazenamento e Desempenho",
|
||||
"faq_d_sync_1_a": "Na inicialização, o nó carrega e valida blocos antes de poder atender a carteira. A tela 'Processando blocos / Aplicando blocos para construir o estado atual da chain' é esse aquecimento. É normal; a carteira fica utilizável assim que o nó termina e reporta sua altura.",
|
||||
"faq_d_sync_1_q": "O que significa 'Processando blocos' na inicialização?",
|
||||
"faq_d_sync_2_a": "Para seus fundos blindados, o nó mantém 'testemunhas' (witnesses) criptográficas que permitem gastar de forma privada. Após certas reinicializações ou atualizações, ele reconstrói esse cache reescaneando blocos recentes. Em uma carteira com muitas transações isso pode levar vários minutos e usar memória extra.\n\nNão está travado — deixe terminar. Forçar o encerramento no meio disso só faz com que a reconstrução reinicie do zero na próxima vez.",
|
||||
"faq_d_sync_2_q": "O que é 'Reconstruindo cache de testemunhas'?",
|
||||
"faq_d_sync_3_a": "Perto do topo da chain, o trabalho que escaneia sua carteira (verificando saldos e notas) disputa com o nó o mesmo bloqueio interno, o que pode retardar a conexão dos últimos blocos em uma carteira grande. Versões recentes limitam esse escaneamento para que o nó possa se atualizar. Ficar em uma aba mais leve (ou apenas esperar) permite que termine mais rápido.",
|
||||
"faq_d_sync_3_q": "Por que a sincronização está lenta quando estou quase em dia?",
|
||||
"faq_d_sync_4_a": "A primeira sincronização baixa e verifica toda a chain e pode levar um tempo dependendo da sua conexão e disco. Você pode acelerá-la drasticamente ativando o download de bootstrap no assistente de configuração, que obtém uma cópia recente e verificada dos dados da chain.",
|
||||
"faq_d_sync_4_q": "Quanto tempo leva a sincronização inicial?",
|
||||
"faq_d_sync_title": "Sincronização e Blockchain",
|
||||
"faq_d_trbl_1_a": "Geralmente não. Em uma carteira grande, o nó pode passar vários minutos reconstruindo testemunhas blindadas ou aplicando blocos, durante os quais parece pausado e pode demorar para responder. Dê um tempo — o log de depuração (Configurações) mostra progresso constante se estiver funcionando.\n\nEvite forçar o encerramento durante essa fase; isso reinicia o trabalho do zero na próxima inicialização.",
|
||||
"faq_d_trbl_1_q": "O nó parece travado em 'Ativando a melhor chain' — está congelado?",
|
||||
"faq_d_trbl_2_a": "Se um arquivo de carteira for recuperado mas parte de seus dados de chave estiver faltando, o nó pode abri-lo em um modo 'degradado' limitado, no qual não pode derivar novos endereços ou blindar fundos. A correção mais segura é restaurar a partir da sua frase de recuperação para uma carteira nova.",
|
||||
"faq_d_trbl_2_q": "O que é o 'modo degradado'?",
|
||||
"faq_d_trbl_3_a": "Um re-scan faz a carteira reler a chain para redescobrir suas transações (útil após importar chaves). Um re-index reconstrói o banco de dados da blockchain do nó e só é necessário se esse banco de dados estiver corrompido ou incompatível após uma mudança de versão. Ambos podem levar um tempo; inicie-os em Configurações > Nó & Segurança.",
|
||||
"faq_d_trbl_3_q": "Quando devo fazer re-index ou re-scan?",
|
||||
"faq_d_trbl_4_a": "O uso de memória aumenta durante trabalho pesado, como uma reconstrução de testemunhas ou uma sincronização inicial, e depois cai novamente quando termina. Uso de memória alto e sustentado em repouso é incomum — reiniciar a carteira o limpa. Em uma máquina com pouca RAM, certifique-se de que outros aplicativos pesados em memória não estejam competindo.",
|
||||
"faq_d_trbl_4_q": "O nó está usando muita memória — isso é normal?",
|
||||
"faq_d_trbl_title": "Solução de Problemas",
|
||||
"faq_d_upd_1_a": "Abra Configurações > Nó & Segurança > Binário do daemon e use 'Verificar atualizações…'. A carteira baixa a versão mais recente e verificada do nó e a instala; a nova versão entra em vigor na próxima vez que o daemon iniciar.",
|
||||
"faq_d_upd_1_q": "Como faço para atualizar o nó?",
|
||||
"faq_d_upd_2_a": "Sim — a caixa de diálogo de atualização lista todas as versões para que você possa escolher uma específica ou mais antiga. Tenha cuidado ao fazer downgrade: um nó mais antigo pode não aceitar dados da blockchain gravados por um mais novo e pode precisar de re-index.",
|
||||
"faq_d_upd_2_q": "Posso instalar uma versão específica do nó?",
|
||||
"faq_d_upd_3_a": "Todo arquivo do nó baixado é verificado por checksum e assinatura criptográfica antes de ser instalado. Se a verificação falhar, a carteira recusa a atualização.",
|
||||
"faq_d_upd_3_q": "A atualização é segura?",
|
||||
"faq_d_upd_title": "Atualizando o Nó",
|
||||
"faq_group_daemon": "Daemon",
|
||||
"faq_group_wallet": "Carteira",
|
||||
"faq_intro": "Respostas sobre sua carteira e o nó DragonX. Pesquise, ou navegue por tópico abaixo.",
|
||||
"faq_intro_lite": "Respostas sobre sua carteira Lite. Pesquise, ou navegue por tópico abaixo.",
|
||||
"faq_l_gs_1_a": "O ObsidianDragonLite é a carteira leve do DragonX. Em vez de rodar um nó completo, ele se conecta a um servidor lite do DragonX, então inicia rápido e não baixa toda a blockchain.\n\nEle gerencia suas chaves, saldos e envios assim como a carteira completa — veja a seção 'Carteira Lite' abaixo para saber como funciona a conexão com o servidor e a troca envolvendo privacidade.",
|
||||
"faq_l_gs_1_q": "O que é o ObsidianDragonLite?",
|
||||
"faq_l_lite_1_a": "A carteira Lite se comunica com um servidor lite do DragonX em vez de rodar seu próprio nó completo. Isso a torna rápida para iniciar e leve em disco e largura de banda, pois ela não baixa nem verifica toda a blockchain sozinha. Suas chaves privadas sempre permanecem no seu dispositivo — nunca são enviadas ao servidor.",
|
||||
"faq_l_lite_1_q": "Qual a diferença entre a carteira Lite e a carteira completa?",
|
||||
"faq_l_lite_2_a": "Abra Configurações e escolha um servidor. Você pode trocar de servidor a qualquer momento e a carteira ressincroniza a partir do novo. Se um servidor estiver lento ou inacessível, tente outro.",
|
||||
"faq_l_lite_2_q": "Como escolho ou altero o servidor?",
|
||||
"faq_l_lite_3_a": "Suas chaves nunca saem do seu dispositivo, e os valores blindados permanecem criptografados. Mas o servidor lite vê quais endereços sua carteira consulta e seu endereço IP, então ele pode vincular seus endereços entre si — uma troca de conveniência por privacidade. Para máxima privacidade, use a carteira de nó completo, que verifica a chain por conta própria e não revela seus endereços a um servidor.",
|
||||
"faq_l_lite_3_q": "A carteira Lite é tão privada quanto o nó completo?",
|
||||
"faq_l_lite_title": "Carteira Lite",
|
||||
"faq_l_sec_1_a": "Vá em Configurações > Carteira e defina uma senha na seção de segurança. A encriptação protege suas chaves privadas em disco, então quem tiver acesso aos arquivos ainda assim não poderá gastar suas moedas.\n\nEscolha uma senha forte e não a perca — não há como recuperar uma carteira encriptada sem ela.",
|
||||
"faq_no_results": "Nenhum resultado. Tente outro termo de busca.",
|
||||
"faq_open_tooltip": "Ajuda e FAQ",
|
||||
"faq_search_hint": "Buscar no FAQ…",
|
||||
"faq_title": "Ajuda e FAQ",
|
||||
"faq_w_addr_1_a": "Endereços transparentes (começam com R ou t) funcionam como a maioria das moedas: os valores e endereços são públicos na blockchain.\n\nEndereços blindados (endereços-z, começam com zs) usam criptografia de conhecimento zero — o valor, remetente e destinatário são encriptados on-chain. Saldos em endereços blindados são privados.",
|
||||
"faq_w_addr_1_q": "Qual a diferença entre endereços transparentes e blindados?",
|
||||
"faq_w_addr_2_a": "Prefira endereços blindados (z) sempre que possível — eles mantêm seu saldo e histórico de pagamentos privados. DragonX é uma moeda de privacidade, e blindado é o padrão para fundos recebidos.\n\nUse um endereço transparente apenas quando um serviço com o qual você interage não conseguir lidar com endereços blindados.",
|
||||
"faq_w_addr_2_q": "Qual tipo de endereço devo usar?",
|
||||
"faq_w_addr_3_a": "Para endereços blindados, reutilizar é tranquilo e não expõe o histórico. Para endereços transparentes, usar um endereço novo a cada pagamento melhora a privacidade. Crie novos endereços na aba Receber.",
|
||||
"faq_w_addr_3_q": "Devo reutilizar um endereço ou criar um novo?",
|
||||
"faq_w_addr_4_a": "Blindar move moedas de um endereço transparente para um blindado, tornando-as privadas. Moedas recém-mineradas chegam transparentes e são blindadas automaticamente para você.\n\nVocê também pode blindar manualmente pela carteira; um blindamento é apenas uma transação especial, então leva o tempo normal de confirmação para ser concluído.",
|
||||
"faq_w_addr_4_q": "O que é \"blindar\"?",
|
||||
"faq_w_addr_title": "Endereços e Privacidade",
|
||||
"faq_w_bal_1_a": "Enquanto sua carteira ainda está sincronizando, seu saldo está incompleto e pode aparecer como 0 — ainda não foram escaneadas todas as suas transações. Ele se completa assim que a sincronização terminar.\n\nObserve a barra de status na parte inferior: ela mostra o estado da conexão e a altura do bloco. Quando a sincronização estiver completa, seu saldo estará correto.",
|
||||
"faq_w_bal_1_q": "Por que meu saldo está em 0 ou não atualiza?",
|
||||
"faq_w_bal_2_a": "Cada novo bloco minerado sobre o bloco que contém sua transação adiciona uma confirmação. Mais confirmações significam que o pagamento está mais firmemente consolidado. Fundos recebidos ficam disponíveis para gasto após a primeira confirmação.",
|
||||
"faq_w_bal_2_q": "O que são confirmações?",
|
||||
"faq_w_bal_3_a": "O total inclui fundos que ainda estão confirmando ou estão temporariamente bloqueados (por exemplo, moedas no meio do processo de blindagem). O disponível é o que você pode enviar agora mesmo. Eles convergem à medida que as transações são confirmadas.",
|
||||
"faq_w_bal_3_q": "Qual a diferença entre saldo total e saldo disponível?",
|
||||
"faq_w_bal_title": "Saldo e Sincronização",
|
||||
"faq_w_chat_1_a": "O Chat é um mensageiro encriptado, on-chain, integrado à carteira. As mensagens são enviadas como memos blindados privados, então só você e seu contato podem lê-las.",
|
||||
"faq_w_chat_1_q": "O que é o recurso de Chat?",
|
||||
"faq_w_chat_2_a": "Sua identidade de chat é derivada da frase de recuperação da sua carteira, então ela acompanha sua carteira — restaure a carteira e sua identidade volta junto. Você não cria uma conta ou senha separada.",
|
||||
"faq_w_chat_2_q": "Como funciona minha identidade de chat?",
|
||||
"faq_w_chat_3_a": "Adicione um contato pelo endereço na aba Contatos, opcionalmente com nome e avatar. Os contatos podem ficar privados apenas para a carteira atual ou compartilhados entre todas as carteiras que você abrir neste computador.",
|
||||
"faq_w_chat_3_q": "Como funcionam os contatos?",
|
||||
"faq_w_chat_4_a": "Abra uma conversa e use os ícones no cabeçalho dela. Ocultar (o olho) remove a conversa da lista, mas mantém todas as mensagens — uma nova mensagem a torna visível novamente. Excluir (a lixeira) abre um diálogo: \"Excluir\" limpa o histórico neste dispositivo (se essa pessoa te enviar mensagem de novo, a conversa volta), enquanto \"Excluir e bloquear\" também impede mensagens futuras dela até você desbloqueá-la na lista \"Bloqueados\" acima da lista de conversas. Tudo isso é local a este dispositivo — as mensagens permanecem na blockchain e a outra pessoa mantém sua própria cópia, então não é um \"desenvio\".",
|
||||
"faq_w_chat_4_q": "Como eu oculto, excluo ou bloqueio uma conversa?",
|
||||
"faq_w_chat_title": "Chat e Contatos",
|
||||
"faq_w_gs_1_a": "ObsidianDragon é uma carteira de nó completo para DragonX (DRGX). Ela gerencia suas moedas e, na versão desktop, executa um nó completo DragonX (o daemon) em segundo plano, para que sua carteira verifique a blockchain por conta própria em vez de confiar em um servidor de terceiros.\n\nPense nisso como duas partes trabalhando juntas: a carteira (chaves, saldos, envios) e o nó (a blockchain e a rede). As perguntas aqui estão divididas da mesma forma — veja as abas Carteira e Daemon acima.",
|
||||
"faq_w_gs_1_q": "O que é o ObsidianDragon?",
|
||||
"faq_w_gs_2_a": "Na primeira execução, o assistente de configuração cria uma carteira para você automaticamente. Novas carteiras são protegidas por uma frase de recuperação secreta (uma lista de palavras). Anote essa frase e guarde-a offline — é a única forma de restaurar seus fundos se este computador for perdido.\n\nVocê pode revisar ou fazer backup da frase a qualquer momento em Configurações > Backup e Dados > Frase de recuperação.",
|
||||
"faq_w_gs_2_q": "Como eu crio uma nova carteira?",
|
||||
"faq_w_gs_3_a": "Use o fluxo de restauração por frase de recuperação se você tiver uma. (Na versão de nó completo você também pode copiar um arquivo wallet.dat existente para o diretório de dados da carteira antes de iniciá-la.)\n\nApós restaurar, a carteira reescaneia para encontrar suas transações passadas, então seu saldo e histórico podem levar um tempo para aparecer na primeira vez.",
|
||||
"faq_w_gs_3_q": "Como eu restauro uma carteira que já tenho?",
|
||||
"faq_w_gs_4_a": "O assistente permite escolher uma aparência/tema, configurar encriptação e um PIN e — na versão de nó completo — opcionalmente baixar um bootstrap para acelerar a primeira sincronização. Você pode pular qualquer etapa e alterar tudo isso depois em Configurações.",
|
||||
"faq_w_gs_4_q": "O que o assistente de primeira execução faz?",
|
||||
"faq_w_gs_title": "Primeiros Passos",
|
||||
"faq_w_sec_1_a": "Vá em Configurações > Nó e Segurança e defina uma senha (seção SEGURANÇA). A encriptação protege suas chaves privadas em disco, então quem tiver acesso aos arquivos ainda assim não poderá gastar suas moedas.\n\nEscolha uma senha forte e não a perca — não há como recuperar uma carteira encriptada sem ela.",
|
||||
"faq_w_sec_1_q": "Como eu encripto minha carteira?",
|
||||
"faq_w_sec_2_a": "O PIN bloqueia a tela do app para que saldos e ações fiquem ocultos quando você se afasta. É um bloqueio de conveniência sobre a encriptação; você o configura durante o assistente ou em Configurações, depois de encriptar a carteira.",
|
||||
"faq_w_sec_2_q": "O que é o PIN / tela de bloqueio?",
|
||||
"faq_w_sec_3_a": "Não. A encriptação da carteira não pode ser contornada. Se você ainda tiver sua frase de recuperação, pode restaurar a carteira a partir dela em uma carteira nova e definir uma nova senha. Sem a senha ou a frase de recuperação, os fundos não podem ser recuperados.",
|
||||
"faq_w_sec_3_q": "Esqueci minha senha — ela pode ser recuperada?",
|
||||
"faq_w_sec_title": "Segurança e Encriptação",
|
||||
"faq_w_seed_1_a": "Vá em Configurações > Backup e Dados > Frase de recuperação. Anote as palavras no papel, em ordem, e guarde-as em um lugar seguro e offline. Qualquer pessoa com a frase pode gastar suas moedas, então nunca a guarde em uma foto, e-mail ou nota na nuvem.",
|
||||
"faq_w_seed_1_q": "Como eu faço backup da minha frase de recuperação?",
|
||||
"faq_w_seed_2_a": "Carteiras mais antigas (legadas) não eram baseadas em frase semente. A carteira pode migrar uma carteira legada para uma moderna, baseada em frase semente: ela cria uma nova carteira semente e transfere seus fundos para ela. Procure por \"Migrar para carteira semente…\" em Configurações > Backup e Dados. Esse recurso requer a versão atual do daemon.",
|
||||
"faq_w_seed_2_q": "Minha carteira não tem frase de recuperação — posso adicionar uma?",
|
||||
"faq_w_seed_3_a": "Use o fluxo de restauração por frase semente ao configurar uma carteira e digite suas palavras em ordem. Sua carteira então reescaneia para reconstruir seu saldo e histórico, o que pode levar um tempo na primeira vez.",
|
||||
"faq_w_seed_3_q": "Como eu restauro a partir da minha frase de recuperação?",
|
||||
"faq_w_seed_4_a": "Sua frase de recuperação é o backup principal e é suficiente para restaurar tudo. Uma cópia do arquivo da sua carteira é um backup secundário conveniente que também preserva rótulos e configurações. Mantenha qualquer backup offline e privado.",
|
||||
"faq_w_seed_4_q": "Devo também fazer backup do wallet.dat?",
|
||||
"faq_w_seed_title": "Frase de Recuperação e Backup",
|
||||
"faq_w_send_1_a": "Abra a aba Enviar, cole o endereço do destinatário, informe um valor e confirme. Se a carteira estiver encriptada, você será solicitado a desbloqueá-la para a transação.\n\nEnvios para endereços blindados são privados; envios para endereços transparentes são públicos.",
|
||||
"faq_w_send_1_q": "Como eu envio fundos?",
|
||||
"faq_w_send_2_a": "As taxas do DragonX são muito baixas e definidas automaticamente. A taxa é mostrada antes de você confirmar. Normalmente você não precisa alterá-la.",
|
||||
"faq_w_send_2_q": "Qual taxa eu pago?",
|
||||
"faq_w_send_3_a": "Sim — ao enviar para um endereço blindado (z) você pode incluir um memo encriptado. Só o destinatário pode lê-lo. Endereços transparentes não suportam memos.",
|
||||
"faq_w_send_3_q": "Posso anexar uma mensagem a um pagamento?",
|
||||
"faq_w_send_4_a": "Uma transação fica \"pendente\" até ser minerada em um bloco e ganhar confirmações. Isso costuma levar um ou dois minutos. Transações blindadas também precisam que sua carteira esteja sincronizada para gerar a prova.\n\nSe um envio permanecer pendente por tempo incomumente longo, verifique se sua carteira está conectada e sincronizada (veja a barra de status na parte inferior da janela).",
|
||||
"faq_w_send_4_q": "Enviei um pagamento, mas ele diz pendente — por quê?",
|
||||
"faq_w_send_title": "Envio e recebimento",
|
||||
"faq_w_set_1_a": "Abra Configurações > Aparência para escolher um tema e um destaque. Vários skins claros e escuros estão incluídos, e efeitos como o desfoque podem ser desativados em máquinas menos potentes.",
|
||||
"faq_w_set_1_q": "Como mudo o tema?",
|
||||
"faq_w_set_2_a": "Sim — Configurações > Aparência tem um seletor de idioma. A carteira já vem com várias traduções; o que ainda não foi traduzido volta para o inglês.",
|
||||
"faq_w_set_2_q": "Posso mudar o idioma?",
|
||||
"faq_w_set_3_a": "Use a opção de escala de fonte em Configurações > Aparência. Ela redimensiona toda a interface, o que é útil em telas de alta resolução (HiDPI), onde o app pode renderizar pequeno demais.",
|
||||
"faq_w_set_3_q": "O texto está pequeno demais (ou grande demais) — dá para redimensionar?",
|
||||
"faq_w_set_4_a": "Ative a busca de preços em Configurações. A carteira passa a mostrar um valor aproximado em moeda fiduciária ao lado dos saldos e pode exibir um pequeno gráfico de mercado. Os preços são apenas informativos.",
|
||||
"faq_w_set_4_q": "Como exibo preços em moeda fiduciária?",
|
||||
"faq_w_set_title": "Configurações e aparência",
|
||||
"favorite_address": "Adicionar aos favoritos",
|
||||
"fee": "Taxa",
|
||||
"fee_high": "Alta",
|
||||
@@ -572,6 +878,63 @@
|
||||
"general": "Geral",
|
||||
"generating": "Gerando",
|
||||
"go_to_receive": "Ir para Receber",
|
||||
"grpa_current_block_paren": "(Atual: %d)",
|
||||
"grpa_days_ago": "há %lld dias",
|
||||
"grpa_dbg_addrman": "Rastreamento e gerenciamento de endereços de pares",
|
||||
"grpa_dbg_alert": "Mensagens do sistema de alertas",
|
||||
"grpa_dbg_bench": "Medições de tempo de benchmark das operações",
|
||||
"grpa_dbg_coindb": "Operações de leitura/gravação do banco de dados de moedas",
|
||||
"grpa_dbg_db": "Operações do Berkeley DB",
|
||||
"grpa_dbg_estimatefee": "Algoritmo de estimativa de taxa",
|
||||
"grpa_dbg_http": "Atividade do servidor RPC HTTP",
|
||||
"grpa_dbg_libevent": "Biblioteca de rede Libevent",
|
||||
"grpa_dbg_lock": "Depuração de contenção de bloqueios",
|
||||
"grpa_dbg_mempool": "Atividade do pool de memória de transações",
|
||||
"grpa_dbg_net": "Conexões e mensagens de rede",
|
||||
"grpa_dbg_paymentdisclosure": "Protocolo de divulgação de pagamento",
|
||||
"grpa_dbg_pow": "Atividade de mineração de prova de trabalho",
|
||||
"grpa_dbg_proxy": "Conexões de proxy SOCKS5",
|
||||
"grpa_dbg_prune": "Operações de poda de blocos",
|
||||
"grpa_dbg_rand": "Geração de números aleatórios",
|
||||
"grpa_dbg_reindex": "Progresso da reindexação da blockchain",
|
||||
"grpa_dbg_rpc": "Processamento de comandos RPC",
|
||||
"grpa_dbg_selectcoins": "Seleção de moedas para transações",
|
||||
"grpa_dbg_tor": "Integração com Tor e informações de circuito",
|
||||
"grpa_dbg_zmq": "Sistema de notificações ZeroMQ",
|
||||
"grpa_dbg_zrpc": "Operações RPC blindadas (z-address)",
|
||||
"grpa_enter_private_key_to_import": "Insira uma chave privada para importar.",
|
||||
"grpa_error_prefix": "Erro: ",
|
||||
"grpa_hr_ago": "há %lld h",
|
||||
"grpa_invalid_response_from_daemon": "Resposta inválida do daemon",
|
||||
"grpa_invalid_suffix": " (inválido)",
|
||||
"grpa_min_ago": "há %lld min",
|
||||
"grpa_sec_ago": "há %lld s",
|
||||
"grpa_seed_demo_chat": "Chat de demonstração da semente",
|
||||
"grpa_showing_first_100_of": "... mostrando os primeiros 100 de %d",
|
||||
"grpa_tab_about": "Sobre",
|
||||
"grpa_tab_appearance": "Aparência",
|
||||
"grpa_tab_backup_data": "Backup e Dados",
|
||||
"grpa_tab_chat": "Chat",
|
||||
"grpa_tab_explorer": "Explorer",
|
||||
"grpa_tab_node_security": "Nó e Segurança",
|
||||
"grpa_tab_wallet": "Carteira",
|
||||
"grpa_unexpected_getblockhash_result": "resultado inesperado de getblockhash",
|
||||
"grpb_copy": "Copiar",
|
||||
"grpb_max": "Máx",
|
||||
"grpb_new_badge_suffix": " [NOVO]",
|
||||
"grpb_preview_msg_payment_through": "O pagamento foi concluído? 🙂",
|
||||
"grpb_preview_msg_sending_rest": "Enviando o restante agora 👍",
|
||||
"grpb_preview_msg_yep_confirmed": "Sim — acabei de confirmar ✅",
|
||||
"grpb_selected_suffix": "\n(selecionado)",
|
||||
"grpb_tooltip_address_balance": "%s\nSaldo: %.8f %s%s",
|
||||
"grpb_undo_clear": "Desfazer Limpar",
|
||||
"grpc_benchmark_inconclusive": "Benchmark inconclusivo: nenhuma amostra de taxa de hash foi registrada. Verifique a conexão com a pool e tente novamente.",
|
||||
"grpc_benchmark_takes_secs": "O benchmark leva cerca de %ds e interrompe a mineração. Clique novamente para iniciar.",
|
||||
"grpc_bootstrap_failed": "Falha no bootstrap",
|
||||
"grpc_bootstrap_not_initialized": "Bootstrap não inicializado",
|
||||
"grpc_hashrate_fee": "%s %s%% de taxa",
|
||||
"grpc_key_not_available": "Chave não disponível para este endereço",
|
||||
"grpc_na": "N/D",
|
||||
"height": "Altura",
|
||||
"help": "Ajuda",
|
||||
"hidden_tag": " (oculto)",
|
||||
@@ -646,6 +1009,7 @@
|
||||
"light": "Claro",
|
||||
"lite_account_label": "Conta",
|
||||
"lite_action": "Ação",
|
||||
"lite_backend_unavailable": "Backend da carteira lite indisponível",
|
||||
"lite_backup_keys": "Backup e chaves",
|
||||
"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).",
|
||||
@@ -653,9 +1017,12 @@
|
||||
"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_start": "Não foi possível iniciar a operação",
|
||||
"lite_could_not_write": "Não foi possível gravar ",
|
||||
"lite_encrypt_wallet": "Criptografar carteira",
|
||||
"lite_encryption_removed": "Criptografia removida",
|
||||
"lite_enter_all_seed_words": "Informe todas as 24 palavras-semente para restaurar (obtidas %d)",
|
||||
"lite_enter_wallet_path": "Informe um caminho para a carteira",
|
||||
"lite_hide_wipe": "Ocultar e apagar",
|
||||
"lite_import": "Importar",
|
||||
"lite_import_key_label": "Importar chave",
|
||||
@@ -792,9 +1159,27 @@
|
||||
"memo_optional": "MEMO (OPCIONAL)",
|
||||
"memo_upper": "MEMO",
|
||||
"memo_z_only": "Nota: Memos só estão disponíveis ao enviar para endereços blindados (z)",
|
||||
"merge_addr_created": "Endereço blindado criado.",
|
||||
"merge_advanced": "Avançado",
|
||||
"merge_back": "Voltar",
|
||||
"merge_batch_fmt": "Mescla até %d entradas por execução — repita para concluir o restante.",
|
||||
"merge_confirm_btn": "Confirmar",
|
||||
"merge_confirm_fmt": "Consolidar ~%s DRGX de %d entrada(s) em %s?",
|
||||
"merge_create_zaddr": "Criar endereço blindado",
|
||||
"merge_creating": "Criando endereço…",
|
||||
"merge_description": "Fundir múltiplos UTXOs em um único endereço blindado. Isso pode ajudar a reduzir o tamanho da carteira e melhorar a privacidade.",
|
||||
"merge_fee_hint": "Taxa de rede para esta transação.",
|
||||
"merge_funds": "Fundir Fundos",
|
||||
"merge_max_inputs": "Máx. de entradas por lote",
|
||||
"merge_no_spendable": "Ainda não há entradas disponíveis para consolidar.",
|
||||
"merge_progress": "Consolidando… isso pode levar alguns minutos. Você pode fechar esta janela.",
|
||||
"merge_scope_fmt": "%d entradas transparentes + %d blindadas · ~%s DRGX disponíveis",
|
||||
"merge_scope_loading": "Verificando suas entradas…",
|
||||
"merge_send_failed": "Falha na fusão: ",
|
||||
"merge_source": "Consolidar",
|
||||
"merge_src_both": "Ambos",
|
||||
"merge_src_shielded": "Blindado",
|
||||
"merge_src_transparent": "Transparente",
|
||||
"merge_started": "Operação de fusão iniciada",
|
||||
"merge_title": "Fundir para Endereço",
|
||||
"mig_adopt": "Tornar esta minha carteira",
|
||||
@@ -875,6 +1260,7 @@
|
||||
"mining_difficulty_copied": "Dificuldade copiada",
|
||||
"mining_est_block": "Bloco Est.",
|
||||
"mining_est_daily": "Est. Diário",
|
||||
"mining_est_daily_pool_sub": "equivalente solo aproximado, antes da taxa do pool",
|
||||
"mining_filter_all": "Todos",
|
||||
"mining_filter_tip_all": "Mostrar todos os ganhos",
|
||||
"mining_filter_tip_pool": "Mostrar apenas ganhos do pool",
|
||||
@@ -903,10 +1289,12 @@
|
||||
"mining_open_in_explorer": "Abrir no explorador",
|
||||
"mining_payout_address": "Endereço de Pagamento",
|
||||
"mining_payout_foreign": "⚠ Este endereço de pagamento não está na sua carteira atual — as recompensas mineradas iriam para uma carteira diferente. Atualize-o se você trocou de carteira.",
|
||||
"mining_payout_invalid": "Endereço DragonX inválido — corrija antes de iniciar, ou as recompensas de mineração serão perdidas.",
|
||||
"mining_payout_tooltip": "Endereço para receber recompensas de mineração",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_fee": "Taxa",
|
||||
"mining_pool_hashrate": "Hashrate do Pool",
|
||||
"mining_pool_needs_payout_tooltip": "Informe primeiro um endereço de pagamento (gere um endereço Z)",
|
||||
"mining_pool_url": "URL do Pool",
|
||||
"mining_pools_header": "POOLS",
|
||||
"mining_recent_blocks": "BLOCOS RECENTES",
|
||||
@@ -936,6 +1324,9 @@
|
||||
"mining_syncing_tooltip": "Blockchain está sincronizando...",
|
||||
"mining_tag": " · Mineração",
|
||||
"mining_threads": "Threads de Mineração",
|
||||
"mining_threads_input_tooltip": "Digite um número exato de threads (Enter para aplicar)",
|
||||
"mining_threads_minus_tooltip": "Menos threads",
|
||||
"mining_threads_plus_tooltip": "Mais threads",
|
||||
"mining_to_save": "para salvar",
|
||||
"mining_today": "Hoje",
|
||||
"mining_uptime": "Tempo Ativo",
|
||||
@@ -1028,6 +1419,13 @@
|
||||
"peers_upper": "PARES",
|
||||
"peers_version": "Versão",
|
||||
"pending": "Pendente",
|
||||
"picker_dat_count": "%d arquivo(s) de carteira nesta pasta",
|
||||
"picker_dat_none": "Nenhum arquivo de carteira diretamente nesta pasta",
|
||||
"picker_empty": "Esta pasta não tem subpastas nem arquivos de carteira.",
|
||||
"picker_home": "Pasta pessoal",
|
||||
"picker_select": "Escanear esta pasta",
|
||||
"picker_title": "Selecione uma pasta para escanear",
|
||||
"picker_up": "Subir um nível",
|
||||
"pin_change_desc": "Altere seu PIN de desbloqueio. Você precisa do seu PIN atual e de um novo PIN.",
|
||||
"pin_confirm_new_label": "Confirmar novo PIN:",
|
||||
"pin_current_label": "PIN atual:",
|
||||
@@ -1105,6 +1503,8 @@
|
||||
"qr_failed": "Falha ao gerar código QR",
|
||||
"qr_title": "Código QR",
|
||||
"qr_unavailable": "QR indisponível",
|
||||
"quick_receive": "Recebimento rápido",
|
||||
"quick_send": "Envio rápido",
|
||||
"ram_daemon_gb": "Daemon: %.1f GB (%s)",
|
||||
"ram_daemon_mb": "Daemon: %.0f MB (%s)",
|
||||
"ram_system_gb": "Sistema: %.1f / %.0f GB",
|
||||
@@ -1154,6 +1554,7 @@
|
||||
"rpc_connection": "Conexão RPC...",
|
||||
"rpc_host": "Host RPC",
|
||||
"rpc_pass": "Senha",
|
||||
"rpc_plaintext_remote_warning": "O RPC remoto está usando HTTP em texto simples. Adicione rpctls=1 ao DRAGONX.conf se o seu daemon suportar TLS.",
|
||||
"rpc_port": "Porta",
|
||||
"rpc_user": "Usuário",
|
||||
"save": "Salvar",
|
||||
@@ -1161,6 +1562,7 @@
|
||||
"save_z_transactions": "Salvar Z-tx na lista de tx",
|
||||
"sb_auth_failed": "Autenticação falhou — verifique rpcuser/rpcpassword",
|
||||
"sb_block": "Bloco: %d",
|
||||
"sb_block_db_unreadable": "Banco de dados de blocos ilegível — reconstrução necessária",
|
||||
"sb_building_witnesses": "Definindo testemunhas",
|
||||
"sb_building_witnesses_pct": "Reconstruindo testemunhas %.0f%%",
|
||||
"sb_connecting_daemon": "Conectando ao dragonxd...",
|
||||
@@ -1176,6 +1578,7 @@
|
||||
"sb_dragonxd_stopped": "dragonxd parado",
|
||||
"sb_dragonxd_stopping": "Parando dragonxd...",
|
||||
"sb_extracting_sapling": "Extraindo parâmetros Sapling...",
|
||||
"sb_finishing_repair": "Concluindo o reparo da carteira — reescaneando…",
|
||||
"sb_importing_keys": "Importando chaves",
|
||||
"sb_loading_config": "Carregando configuração...",
|
||||
"sb_mining_hs": "%.1f H/s",
|
||||
@@ -1197,14 +1600,55 @@
|
||||
"sb_waiting_config": "Aguardando configuração do daemon...",
|
||||
"sb_waiting_daemon": "Aguardando dragonxd...",
|
||||
"sb_waiting_daemon_err": "Aguardando dragonxd — %s",
|
||||
"sb_wallet_needs_recovery": "Reparo de carteira disponível",
|
||||
"sb_warming_up": "Aquecendo...",
|
||||
"sb_witness_cache": "Reconstruindo testemunhas",
|
||||
"scale_effects": "ESCALA E EFEITOS",
|
||||
"screenshot_open_dir": "Abrir local",
|
||||
"screenshot_sweep": "Executar varredura de capturas de tela",
|
||||
"screenshot_sweep_desc": "Percorre cada tema em cada aba e salva uma captura de tela de cada um em subpastas por aba dentro da pasta de capturas de tela do diretório de configuração (sobrescrevendo a varredura anterior). É executado por alguns segundos.",
|
||||
"screenshot_sweep_full": "Varredura completa da interface",
|
||||
"search_icons": "Pesquisar ícones...",
|
||||
"search_placeholder": "Pesquisar...",
|
||||
"sec_changing_passphrase": "Alterando a frase-senha...",
|
||||
"sec_changing_pin": "Alterando o PIN...",
|
||||
"sec_couldnt_lock_wallet": "Não foi possível bloquear a carteira — ela continua desbloqueada. Verifique a conexão com o daemon.",
|
||||
"sec_encrypted_backup_suffix": "\nBackup criptografado: wallet.dat.encrypted.bak",
|
||||
"sec_encrypting_wallet": "Criptografando a carteira...",
|
||||
"sec_encryption_did_not_complete": "A criptografia da carteira não foi concluída — sua carteira NÃO está criptografada. Abra as Configurações para terminar de criptografá-la.",
|
||||
"sec_encryption_failed_prefix": "Falha na criptografia: ",
|
||||
"sec_failed_prefix": "Falhou: ",
|
||||
"sec_failed_to_create_vault": "Falha ao criar o cofre",
|
||||
"sec_importing_keys_rescanning": "Importando chaves e reescaneando a blockchain — a carteira pode ser usada durante o processo",
|
||||
"sec_incorrect_current_pin": "PIN atual incorreto",
|
||||
"sec_incorrect_passphrase_decrypt": "Frase-senha incorreta",
|
||||
"sec_incorrect_passphrase_pin_setup": "Frase-senha incorreta",
|
||||
"sec_incorrect_pin_remove": "PIN incorreto",
|
||||
"sec_internal_error_change_pin": "Erro interno",
|
||||
"sec_internal_error_remove_pin": "Erro interno",
|
||||
"sec_mode_passphrase": " Frase-senha",
|
||||
"sec_not_connected_to_daemon": "Não conectado ao daemon",
|
||||
"sec_not_connected_to_daemon_pin": "Não conectado ao daemon",
|
||||
"sec_passphrase_changed_successfully": "Frase-senha alterada com sucesso",
|
||||
"sec_pin_changed_successfully": "PIN alterado com sucesso",
|
||||
"sec_pin_removed": "PIN removido",
|
||||
"sec_pin_set_successfully": "PIN definido com sucesso",
|
||||
"sec_restart_daemon_for_encryption": "Reinicie o daemon para que a criptografia entre em vigor.",
|
||||
"sec_too_many_attempts_wait": "Tentativas em excesso. Aguarde %.0f segundos...",
|
||||
"sec_total_elapsed_fmt": "Tempo total: %dm %02ds",
|
||||
"sec_unlock_button": "Desbloquear",
|
||||
"sec_unlock_failed_prefix": "Falha ao desbloquear: ",
|
||||
"sec_unlocking_fmt": "Desbloqueando%s",
|
||||
"sec_use_passphrase_instead": "Usar frase-senha",
|
||||
"sec_use_pin_instead": "Usar PIN",
|
||||
"sec_verifying_passphrase": "Verificando a frase-senha...",
|
||||
"sec_verifying_pin": "Verificando o PIN...",
|
||||
"sec_wallet_decrypted_all_keys_imported": "Carteira descriptografada com sucesso! Todas as chaves importadas.",
|
||||
"sec_wallet_encrypted_and_pin_set": "Carteira criptografada e PIN definido",
|
||||
"sec_wallet_encrypted_but_pin_vault_failed": "Carteira criptografada, mas o cofre do PIN falhou",
|
||||
"sec_wallet_encrypted_restarting_daemon": "Carteira criptografada. Reiniciando o daemon...",
|
||||
"sec_wallet_encrypted_successfully": "Carteira criptografada com sucesso",
|
||||
"sec_wallet_locked_title": "Carteira Bloqueada",
|
||||
"security": "SEGURANÇA",
|
||||
"seed_backup_button": "Frase de recuperação",
|
||||
"seed_backup_close": "Fechar",
|
||||
@@ -1228,6 +1672,7 @@
|
||||
"send_amount": "Valor",
|
||||
"send_amount_details": "DETALHES DO VALOR",
|
||||
"send_amount_upper": "VALOR",
|
||||
"send_available_note": "disponível",
|
||||
"send_cannot_send_now": "Não é possível enviar agora — verifique a conexão, a sincronização e o saldo disponível.",
|
||||
"send_clear_fields": "Limpar todos os campos do formulário?",
|
||||
"send_contacts_button": "Escolher dos contatos",
|
||||
@@ -1263,6 +1708,7 @@
|
||||
"send_tooltip_not_connected": "Não conectado ao daemon",
|
||||
"send_tooltip_select_source": "Selecione primeiro um endereço de origem",
|
||||
"send_tooltip_syncing": "Aguarde a sincronização da blockchain",
|
||||
"send_tooltip_view_only": "Endereço somente visualização — sem chave de gasto, não é possível enviar",
|
||||
"send_total": "Total",
|
||||
"send_transaction": "Enviar Transação",
|
||||
"send_tx_failed": "Transação falhou",
|
||||
@@ -1282,16 +1728,16 @@
|
||||
"sent_filter": "Enviado",
|
||||
"sent_type": "Enviado",
|
||||
"sent_upper": "ENVIADO",
|
||||
"set_label": "Definir Rótulo...",
|
||||
"set_label": "Definir Rótulo",
|
||||
"settings": "Ajustes",
|
||||
"settings_about_text": "Uma carteira de criptomoeda blindada para DragonX (DRGX), criada com Dear ImGui para uma experiência leve e portátil.",
|
||||
"settings_acrylic_level": "Nível acrílico:",
|
||||
"settings_address_book": "Livro de endereços...",
|
||||
"settings_address_book": "Livro de endereços…",
|
||||
"settings_auto_detected": "Detectado automaticamente de DRAGONX.conf",
|
||||
"settings_auto_lock": "BLOQUEIO AUTOMÁTICO",
|
||||
"settings_auto_shield_desc": "Mover automaticamente fundos transparentes para endereços blindados",
|
||||
"settings_auto_shield_funds": "Blindar fundos transparentes automaticamente",
|
||||
"settings_backup": "Backup...",
|
||||
"settings_backup": "Backup…",
|
||||
"settings_block_explorer_urls": "URLs do explorador de blocos",
|
||||
"settings_builtin": "Integrado",
|
||||
"settings_change_passphrase": "Alterar frase secreta",
|
||||
@@ -1305,7 +1751,7 @@
|
||||
"settings_copy_diagnostics": "Copiar diagnósticos",
|
||||
"settings_copyright": "Copyright 2024-2026 Desenvolvedores DragonX | Licença GPLv3",
|
||||
"settings_custom": "Personalizado",
|
||||
"settings_data_dir": "Dir. de dados:",
|
||||
"settings_data_dir": "Dir. de dados",
|
||||
"settings_debug_changed": "Categorias de depuração alteradas — reinicie o daemon para aplicar",
|
||||
"settings_debug_restart_note": "As alterações entram em vigor após reiniciar o daemon.",
|
||||
"settings_debug_select": "Selecione categorias para ativar o registro de depuração do daemon (flags -debug=).",
|
||||
@@ -1313,19 +1759,20 @@
|
||||
"settings_encrypt_first_pin": "Encripte a carteira primeiro para ativar o PIN",
|
||||
"settings_encrypt_wallet": "Encriptar carteira",
|
||||
"settings_explorer_hint": "As URLs devem incluir uma barra final. O txid/endereço será adicionado.",
|
||||
"settings_export_all": "Exportar tudo...",
|
||||
"settings_export_csv": "Exportar CSV...",
|
||||
"settings_export_key": "Exportar chave...",
|
||||
"settings_export_all": "Exportar tudo…",
|
||||
"settings_export_csv": "Exportar CSV…",
|
||||
"settings_export_key": "Exportar chave…",
|
||||
"settings_gradient_bg": "Fundo gradiente",
|
||||
"settings_gradient_desc": "Substituir fundos texturizados por gradientes suaves",
|
||||
"settings_idle_after": "após",
|
||||
"settings_import_key": "Importar Chave Privada...",
|
||||
"settings_import_viewkey": "Importar chave de visualização...",
|
||||
"settings_import_key": "Importar Chave Privada…",
|
||||
"settings_import_viewkey": "Importar chave de visualização…",
|
||||
"settings_language_note": "Nota: Alguns textos requerem reinício para atualizar",
|
||||
"settings_lock_now": "Bloquear agora",
|
||||
"settings_locked": "Bloqueado",
|
||||
"settings_merge_to_address": "Fundir para endereço...",
|
||||
"settings_merge_to_address": "Fundir para endereço…",
|
||||
"settings_noise_opacity": "Opacidade do ruído:",
|
||||
"settings_not_connected": "Não conectado ao daemon",
|
||||
"settings_not_encrypted": "Não encriptado",
|
||||
"settings_not_found": "Não encontrado",
|
||||
"settings_open_app_dir": "Abrir pasta do aplicativo",
|
||||
@@ -1336,29 +1783,36 @@
|
||||
"settings_privacy": "Privacidade",
|
||||
"settings_quick_unlock_pin": "PIN de desbloqueio rápido",
|
||||
"settings_reduce_transparency": "Reduzir transparência",
|
||||
"settings_reloaded": "Configurações recarregadas do disco",
|
||||
"settings_remove_encryption": "Remover encriptação",
|
||||
"settings_remove_pin": "Remover PIN",
|
||||
"settings_request_payment": "Solicitar pagamento...",
|
||||
"settings_request_payment": "Solicitar pagamento…",
|
||||
"settings_rescan_desc": "Reescanear a blockchain em busca de transações ausentes",
|
||||
"settings_restart_daemon": "Reiniciar daemon",
|
||||
"settings_rpc_connection": "Conexão RPC",
|
||||
"settings_rpc_error_prefix": "Erro de RPC: ",
|
||||
"settings_rpc_note": "Nota: As configurações de conexão são normalmente detectadas automaticamente do DRAGONX.conf",
|
||||
"settings_rpc_ok": "Conexão RPC OK",
|
||||
"settings_save_shielded_desc": "Armazena transações z-addr em um arquivo local para visualização",
|
||||
"settings_save_shielded_local": "Salvar histórico de transações blindadas localmente",
|
||||
"settings_saved": "Configurações salvas",
|
||||
"settings_set_pin": "Definir PIN",
|
||||
"settings_shield_mining": "Blindar mineração...",
|
||||
"settings_shield_mining": "Blindar mineração…",
|
||||
"settings_solid_colors_desc": "Usar cores sólidas em vez de efeitos de desfoque (acessibilidade)",
|
||||
"settings_theme_refreshed": "Lista de temas atualizada",
|
||||
"settings_tor_desc": "Rotear todas as conexões através do Tor para maior privacidade",
|
||||
"settings_unlocked": "Desbloqueado",
|
||||
"settings_use_tor_network": "Usar Tor para conexões de rede",
|
||||
"settings_validate_address": "Validar endereço...",
|
||||
"settings_validate_address": "Validar endereço…",
|
||||
"settings_visual_effects": "Efeitos visuais",
|
||||
"settings_wallet_file_size": "Tamanho do arquivo da carteira: %s",
|
||||
"settings_wallet_info": "Informações da carteira",
|
||||
"settings_wallet_location": "Localização da carteira: %s",
|
||||
"settings_wallet_maintenance": "Manutenção da carteira",
|
||||
"settings_wallet_not_found": "Arquivo da carteira não encontrado",
|
||||
"settings_wallet_size_label": "Tamanho da carteira:",
|
||||
"settings_wallet_size_label": "Tamanho da carteira",
|
||||
"settings_ztx_cleared": "Histórico de transações Z limpo",
|
||||
"settings_ztx_not_found": "Nenhum arquivo de histórico encontrado",
|
||||
"setup_wizard": "Assistente de Configuração",
|
||||
"share": "Compartilhar",
|
||||
"shield_check_status": "Verificar Status",
|
||||
@@ -1406,17 +1860,34 @@
|
||||
"status": "Status",
|
||||
"stop_external": "Parar daemon externo",
|
||||
"stop_mining": "Parar Mineração",
|
||||
"stratum_allowip_hint": "Permitir mineradores a partir de IP ou CIDR (em branco = apenas este computador)",
|
||||
"stratum_expose_warn": "Abre uma porta de mineração para a rede que você permitir — use apenas em uma LAN confiável.",
|
||||
"stratum_host": "Hospedar um pool de mineração (stratum)",
|
||||
"stratum_host_hint": "Os mineradores se conectam a este computador na porta 22769 (porta RPC + 1000) com um minerador stratum RandomX. Reinicie o daemon para aplicar.",
|
||||
"stratum_host_section": "HOSPEDAGEM DE POOL DE MINERAÇÃO",
|
||||
"submitting_transaction": "Enviando transação...",
|
||||
"success": "Sucesso",
|
||||
"summary": "Resumo",
|
||||
"sweep_button": "Varrer",
|
||||
"sweep_caveat": "Importa a chave para assinar uma transação que move todos os seus fundos para o seu endereço. A chave permanece na carteira com saldo zero.",
|
||||
"sweep_current_theme_only": "Apenas o tema atual",
|
||||
"sweep_dest_label": "Enviar fundos varridos para",
|
||||
"sweep_dest_new": "Novo endereço blindado (recomendado)",
|
||||
"sweep_done": "Concluído — fundos varridos para o seu endereço.",
|
||||
"sweep_to": "Varrido para:",
|
||||
"sweep_toggle": "Varrer para minha carteira (não manter a chave)",
|
||||
"sweep_tx": "Transação:",
|
||||
"swin_connection_failed": "Falha na conexão: ",
|
||||
"swin_connection_successful": "Conexão bem-sucedida!\nversão do dragonxd: ",
|
||||
"swin_invalid_suffix": " (inválido)",
|
||||
"swin_no_history_file_found": "Nenhum arquivo de histórico encontrado",
|
||||
"swin_rescan_failed": "Falha no reescaneamento: ",
|
||||
"swin_rescan_started_from_block": "Reescaneamento iniciado a partir do bloco ",
|
||||
"swin_rescan_to": " até ",
|
||||
"swin_rpc_client_not_initialized": "Cliente RPC não inicializado",
|
||||
"swin_settings_saved": "Configurações salvas",
|
||||
"swin_theme_list_refreshed": "Lista de temas atualizada",
|
||||
"swin_ztx_history_cleared": "Histórico de transações Z limpo",
|
||||
"switch_corrupt_body": "Esta carteira parece corrompida — o nó não conseguiu abri-la. Restaure de um backup, recrie-a ou tente repará-la.",
|
||||
"switch_corrupt_repair": "Tentar reparar (salvage)",
|
||||
"switch_progress_background": "Continuar em segundo plano",
|
||||
@@ -1441,6 +1912,7 @@
|
||||
"theme": "Tema",
|
||||
"theme_effects": "Efeitos de tema",
|
||||
"theme_language": "TEMA E IDIOMA",
|
||||
"tile_click_to_open": "Clique para abrir",
|
||||
"time_days_ago": "há %d dias",
|
||||
"time_hours_ago": "há %d horas",
|
||||
"time_minutes_ago": "há %d minutos",
|
||||
@@ -1455,7 +1927,9 @@
|
||||
"to_upper": "PARA",
|
||||
"tools": "FERRAMENTAS",
|
||||
"tools_actions": "Ferramentas e Ações...",
|
||||
"tools_actions_hdr": "FERRAMENTAS E AÇÕES",
|
||||
"total": "Total",
|
||||
"total_balance_label": "Saldo Total",
|
||||
"transaction_id": "ID DA TRANSAÇÃO",
|
||||
"transaction_sent": "Transação enviada com sucesso",
|
||||
"transaction_sent_msg": "Transação enviada!",
|
||||
@@ -1477,7 +1951,7 @@
|
||||
"tt_auto_shield": "Mover automaticamente o saldo transparente para endereços blindados para privacidade",
|
||||
"tt_backup": "Criar um backup do seu wallet.dat",
|
||||
"tt_block_explorer": "Abrir o explorador de blocos DragonX no seu navegador",
|
||||
"tt_blur": "Quantidade de desfoque (0%% = desligado, 100%% = máximo)",
|
||||
"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)",
|
||||
@@ -1540,7 +2014,7 @@
|
||||
"tt_low_spec": "Desativar todos os efeitos visuais pesados\\nAtalho: Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolidar múltiplos UTXOs em um endereço",
|
||||
"tt_mine_idle": "Iniciar mineração automaticamente quando o\\nsistema estiver ocioso (sem entrada de teclado/mouse)",
|
||||
"tt_noise": "Intensidade de textura granulada (0%% = desligado, 100%% = máximo)",
|
||||
"tt_noise": "Intensidade de textura granulada (0% = desligado, 100% = máximo)",
|
||||
"tt_open_app_dir": "Abrir a pasta ObsidianDragon (configurações, temas, logs) no gerenciador de arquivos",
|
||||
"tt_open_data_dir": "Abrir a pasta com os dados da sua carteira e da blockchain no gerenciador de arquivos",
|
||||
"tt_open_dir": "Clique para abrir no explorador de arquivos",
|
||||
@@ -1574,14 +2048,17 @@
|
||||
"tt_simple_bg": "Usar um gradiente simples para o fundo\\nAtalho: Ctrl+Up",
|
||||
"tt_simple_bg_alt": "Usar uma versão gradiente da imagem de fundo do tema\\nAtalho: Ctrl+Up",
|
||||
"tt_stop_external": "Aplica-se ao conectar a um daemon\\niniciado fora desta carteira",
|
||||
"tt_stratum_host": "Executa um servidor de pool stratum RandomX neste nó para que outros mineradores RandomX possam apontar para este computador. Requer um nó v1.3.0+ e reinício do daemon para aplicar.",
|
||||
"tt_sweep_current_theme_only": "Varrer apenas o tema ativo em vez de percorrer todos os temas",
|
||||
"tt_test_conn": "Verificar a conexão RPC ao daemon",
|
||||
"tt_theme_effects": "Brilho, luminescência, ciclo de matiz por tema",
|
||||
"tt_theme_hotkey": "Atalho: Ctrl+Esquerda/Direita para alternar temas",
|
||||
"tt_tor": "Rotear conexões do daemon através da rede Tor para anonimato",
|
||||
"tt_tx_url": "URL base para visualizar transações em um explorador de blocos",
|
||||
"tt_ui_opacity": "Opacidade de cartões e barra lateral (100%% = totalmente opaco, menor = mais transparente)",
|
||||
"tt_ui_opacity": "Opacidade de cartões e barra lateral (100% = totalmente opaco, menor = mais transparente)",
|
||||
"tt_validate": "Verificar se um endereço DragonX é válido",
|
||||
"tt_verbose": "Registrar diagnósticos detalhados de conexão,\\nestado do daemon e info de proprietário de porta\\nna aba Console",
|
||||
"tt_wallet_size_warn": "Carteiras blindadas crescem com os dados de testemunha de cada nota — mesclar várias notas em um único endereço reduz esse tamanho. Faça backup antes.",
|
||||
"tt_wallets_button": "Liste os arquivos de carteira e alterne entre eles",
|
||||
"tt_website": "Abrir o site do DragonX",
|
||||
"tt_window_opacity": "Opacidade do fundo (menor = área de trabalho visível através da janela)",
|
||||
@@ -1632,6 +2109,7 @@
|
||||
"validate_not_mine": "Não pertence a esta carteira",
|
||||
"validate_ownership": "Propriedade:",
|
||||
"validate_results": "Resultados:",
|
||||
"validate_results_placeholder": "Os resultados aparecerão aqui",
|
||||
"validate_shielded_type": "Blindado (z-endereço)",
|
||||
"validate_status": "Status:",
|
||||
"validate_title": "Validar Endereço",
|
||||
@@ -1647,8 +2125,68 @@
|
||||
"view_on_explorer": "Ver no Explorador",
|
||||
"waiting_for_daemon": "Aguardando conexão com o daemon...",
|
||||
"wallet": "CARTEIRA",
|
||||
"wallet_degraded_notify": "Sua carteira foi aberta em modo de função reduzida: os fundos existentes estão seguros e disponíveis para gasto, mas a criação de novos endereços e o blindamento estão desativados. Faça backup da sua frase de recuperação e restaure-a para reparar totalmente a carteira.",
|
||||
"wallet_diagnostics_hdr": "DIAGNÓSTICOS",
|
||||
"wallet_empty": "Sua carteira está vazia",
|
||||
"wallet_empty_hint": "Mude para Receber para obter seu endereço e começar a receber fundos.",
|
||||
"wallet_options_hdr": "OPÇÕES",
|
||||
"wallet_rebuild_failed": "A reconstrução não produziu uma carteira válida, então nada foi alterado. Sua carteira permanece intacta.",
|
||||
"wallet_rebuild_install_failed": "Não foi possível instalar a carteira reconstruída; sua carteira atual foi mantida.",
|
||||
"wallet_rebuild_no_helper": "O assistente de reconstrução de carteira não está disponível nesta build. Use Restaurar, ou reconstrua manualmente.",
|
||||
"wallet_rebuild_no_source": "Não foi possível encontrar uma carteira legível para reconstruir. Nada foi alterado.",
|
||||
"wallet_rebuild_ok": "Banco de dados da carteira reconstruído — o nó está carregando-o e reescaneando para encontrar seu saldo.",
|
||||
"wallet_rebuild_started": "Reparando seu arquivo de carteira — não feche esta janela.",
|
||||
"wallet_recovered_body": "Nada foi excluído. Sua carteira original continua salva no seu computador como um arquivo de backup datado, e cada opção abaixo apenas copia ou renomeia arquivos — nunca apaga nenhum. Suas chaves nunca são regeneradas, apenas relidas.\n\nA cópia reparada carregada agora pode estar sem algumas transações recentes, então seu saldo pode parecer um pouco baixo até você concluir abaixo e ele reescanear.",
|
||||
"wallet_recovered_dismiss": "Agora não — manter a cópia reparada",
|
||||
"wallet_recovered_dismiss_sub": "Nenhum arquivo é alterado. Você pode reabrir isto a qualquer momento na barra de status; seu original permanece guardado com segurança de qualquer forma.",
|
||||
"wallet_recovered_notify": "Seu arquivo de carteira precisou de reparo — seu original foi guardado com segurança. Abra o app para ver suas opções.",
|
||||
"wallet_recovered_open_folder": "Mostrar os arquivos",
|
||||
"wallet_recovered_open_folder_sub": "Abre a pasta de dados da carteira para você inspecionar os arquivos de backup — nada é alterado.",
|
||||
"wallet_recovered_rebuild": "Reparar automaticamente (recomendado)",
|
||||
"wallet_recovered_rebuild_sub": "Relê cada registro recuperável do seu arquivo de carteira mais completo e grava um novo arquivo limpo, depois reinicia. A opção mais completa — seu arquivo atual é mantido como backup datado de qualquer forma.",
|
||||
"wallet_recovered_restore": "Restaurar o arquivo original em vez disso",
|
||||
"wallet_recovered_restore_sub": "Coloca de volta seu maior backup intocado, palavra por palavra, depois faz uma nova varredura — um pouco mais rápido, mas só tão completo quanto aquele arquivo era. Seu arquivo atual é mantido como backup datado de qualquer forma.",
|
||||
"wallet_recovered_safety": "Suas moedas estão seguras.",
|
||||
"wallet_recovered_title": "Seu arquivo de carteira precisa de um reparo rápido",
|
||||
"wallet_recovered_warn": "Ao iniciar, o app encontrou que seu arquivo de carteira não passou na verificação de consistência — isso costuma acontecer após uma atualização do app ou um desligamento incorreto. O app já protegeu seus dados: guardou o arquivo antigo à parte e carregou uma cópia reparada para você não ficar preso.",
|
||||
"wallet_recovery_decide_later": "Decidir depois",
|
||||
"wallet_recovery_decide_later_tip": "Fecha isto e mantém a cópia carregada agora. Nada é alterado, e você pode reparar quando quiser — a barra de status mantém um link de “Reparo de carteira disponível”.",
|
||||
"wallet_recovery_details_label": "Mostrar detalhes técnicos",
|
||||
"wallet_recovery_done": "Concluído",
|
||||
"wallet_recovery_failure_body": "O reparo foi executado, mas o resultado não passou na verificação (não pôde ser lido, ou não tinha endereços), então foi descartado automaticamente antes de substituir qualquer coisa. Sua carteira está exatamente como estava antes de você clicar — nada no disco mudou.",
|
||||
"wallet_recovery_failure_title": "O reparo não foi concluído",
|
||||
"wallet_recovery_files_detail": "Sua carteira original ainda está aqui — o app a renomeou para um backup datado (wallet.<números>.bak) na sua pasta de dados e não apagou nada. “Reparar” lê cada registro do seu arquivo de carteira mais completo em um novo arquivo limpo. “Restaurar” copia seu maior backup de volta exatamente como está. A cópia carregada agora também é mantida como backup.",
|
||||
"wallet_recovery_files_label": "O que acontece com meus arquivos?",
|
||||
"wallet_recovery_notnow_short": "Agora não",
|
||||
"wallet_recovery_other_options": "Outras opções",
|
||||
"wallet_recovery_rebuild_card": "Reparar automaticamente",
|
||||
"wallet_recovery_rebuild_card_desc": "Lê cada registro recuperável em um arquivo limpo, depois reinicia. A opção mais completa.",
|
||||
"wallet_recovery_recommended": "RECOMENDADO",
|
||||
"wallet_recovery_repair_go": "Reparar",
|
||||
"wallet_recovery_rescan_body": "Refazendo a varredura da blockchain para reconstruir seu saldo e histórico de transações. Isso pode levar vários minutos — você não precisa fazer nada, e por favor não reinicie o node enquanto estiver em execução.",
|
||||
"wallet_recovery_rescan_elapsed": "Trabalhando há %s",
|
||||
"wallet_recovery_rescan_size": "A carteira reconstruída já está em %s e ainda está sendo preenchida",
|
||||
"wallet_recovery_rescan_slow": "Isso está demorando mais que o normal, o que é comum em uma carteira grande. É seguro deixar em execução — você pode acompanhar o progresso em Avançado ▸ Console.",
|
||||
"wallet_recovery_rescan_title": "Finalizando o reparo da sua carteira",
|
||||
"wallet_recovery_restore_card": "Restaurar original",
|
||||
"wallet_recovery_restore_card_desc": "Coloca de volta seu maior backup intocado, palavra por palavra — só tão completo quanto aquele arquivo era.",
|
||||
"wallet_recovery_restore_go": "Restaurar",
|
||||
"wallet_recovery_success_body": "O app carregou sua carteira reparada e está refazendo a varredura para totalizar seu saldo — isso pode levar alguns minutos. Ele lê cada registro que consegue, mas em arquivos danificados raros pode recuperar um pouco menos — ou ocasionalmente mais — endereços do que antes. Depois que a nova varredura terminar, confira se seu saldo e histórico estão corretos.",
|
||||
"wallet_recovery_success_restore": "Seu maior arquivo de backup foi colocado de volta e está carregando agora, e o app está refazendo a varredura. Foi restaurado palavra por palavra, então está exatamente tão completo quanto aquele arquivo era — confira seu saldo depois que a nova varredura terminar.",
|
||||
"wallet_recovery_try_other": "Tentar a outra opção",
|
||||
"wallet_recovery_whats_happened": "O que aconteceu?",
|
||||
"wallet_recovery_working_label": "Trabalhando",
|
||||
"wallet_restore_bad_backup": "O arquivo de backup da carteira parece ilegível, então NÃO foi restaurado — sua carteira atual está inalterada. Restaure a partir do seu próprio backup.",
|
||||
"wallet_restore_busy": "O node está ocupado reiniciando — tente novamente em instantes.",
|
||||
"wallet_restore_copy_failed": "Não foi possível instalar a carteira de backup; sua carteira atual foi mantida.",
|
||||
"wallet_restore_move_failed": "Não foi possível colocar a carteira atual de lado — nada foi alterado.",
|
||||
"wallet_restore_no_backup": "Não foi possível encontrar um wallet.<timestamp>.bak para restaurar. Nada foi alterado.",
|
||||
"wallet_restore_no_restart": "Sua carteira original foi restaurada, mas o node não reiniciou — inicie-o em Configurações.",
|
||||
"wallet_restore_ok": "Carteira original restaurada. O node está carregando-a agora.",
|
||||
"wallet_restore_started": "Restaurando seu arquivo original — por favor não feche esta janela.",
|
||||
"wallet_restore_stop_failed": "O node não parou a tempo, então nada foi alterado. Tente novamente.",
|
||||
"wallet_size_consolidate": "Consolidar notas…",
|
||||
"wallet_size_warn": "Este arquivo de carteira está grande. Consolidar suas notas pode conter o crescimento.",
|
||||
"wallets_active": "Ativa",
|
||||
"wallets_add": "Adicionar pasta",
|
||||
"wallets_add_folder": "Verificar também outra pasta em busca de arquivos de carteira:",
|
||||
@@ -1691,6 +2229,7 @@
|
||||
"wallets_open_inplace_tt": "Abre esta carteira onde está — vinculada ao diretório de dados (sem cópia)",
|
||||
"wallets_remove_folder": "Parar de verificar esta pasta",
|
||||
"wallets_reveal": "Mostrar pasta",
|
||||
"wallets_scan_folder": "Escanear outra pasta em busca de carteiras…",
|
||||
"wallets_scanned_folders": "Pastas verificadas:",
|
||||
"wallets_sort_addresses": "Endereços",
|
||||
"wallets_sort_asc": "Crescente (mais antigo / menos / menor primeiro)",
|
||||
@@ -1760,18 +2299,30 @@
|
||||
"wiz_welcome_sub": "Algumas escolhas rápidas e o seu full node estará pronto.",
|
||||
"wiz_welcome_title": "Bem-vindo ao ObsidianDragon!",
|
||||
"wizard_daemon_start_failed": "Falha ao iniciar o daemon — será tentado novamente automaticamente",
|
||||
"xmrig_active_tt": "O minerador atualmente instalado (ativo)",
|
||||
"xmrig_browse_releases": "Navegar por todos os lançamentos…",
|
||||
"xmrig_checking": "Verificando o minerador mais recente…",
|
||||
"xmrig_current": "Atual:",
|
||||
"xmrig_downgrade_note": "Instalar uma versão diferente do minerador substitui a atual; pare de minerar primeiro, e a mudança terá efeito na próxima inicialização.",
|
||||
"xmrig_download_install": "Baixar e instalar",
|
||||
"xmrig_downloading": "Baixando…",
|
||||
"xmrig_install_this": "Instalar esta versão",
|
||||
"xmrig_installed": "Instalado:",
|
||||
"xmrig_installed_ok": "Minerador instalado",
|
||||
"xmrig_installing": "Instalando…",
|
||||
"xmrig_latest": "Mais recente:",
|
||||
"xmrig_latest_badge": "mais recente",
|
||||
"xmrig_latest_tt": "Versão mais recente disponível",
|
||||
"xmrig_loading_releases": "Carregando versões…",
|
||||
"xmrig_no_notes": "Sem notas de lançamento para esta versão.",
|
||||
"xmrig_none": "nenhum",
|
||||
"xmrig_prerelease_tt": "Build de pré-lançamento / teste",
|
||||
"xmrig_reinstall": "Reinstalar",
|
||||
"xmrig_released": "Lançado em:",
|
||||
"xmrig_releases": "versões do xmrig",
|
||||
"xmrig_status_current": "Esta versão está instalada",
|
||||
"xmrig_status_latest": "Versão mais recente",
|
||||
"xmrig_status_older": "Mais antiga que sua versão instalada",
|
||||
"xmrig_stop_mining_first": "Pare a mineração antes de atualizar o minerador.",
|
||||
"xmrig_unavailable_body": "Nenhuma versão do minerador está disponível para esta plataforma.",
|
||||
"xmrig_unavailable_title": "Atualizações do minerador indisponíveis",
|
||||
|
||||
+568
-17
@@ -19,6 +19,7 @@
|
||||
"about_license_text": "Это программное обеспечение выпущено под лицензией GNU General Public License v3 (GPLv3). Вы можете свободно использовать, изменять и распространять это ПО в соответствии с условиями лицензии.",
|
||||
"about_peers_count": "%zu узлов",
|
||||
"about_release": "Релиз",
|
||||
"about_source": "Источник",
|
||||
"about_title": "Об ObsidianDragon",
|
||||
"about_version": "Версия:",
|
||||
"about_website": "Веб-сайт",
|
||||
@@ -60,7 +61,84 @@
|
||||
"amount_label": "Сумма:",
|
||||
"animate_avatars": "Анимировать аватары",
|
||||
"appearance": "ВНЕШНИЙ ВИД",
|
||||
"appx_back": "Назад",
|
||||
"appx_back_up_seed_phrase_title": "Сохраните seed-фразу",
|
||||
"appx_birthday_block_height": "Дата создания (высота блока): %llu — сохраните и её.",
|
||||
"appx_blockchain_data_deleted": "Данные блокчейна удалены (%d элементов). Демон перезапускается для повторной синхронизации с сетью.",
|
||||
"appx_blockchain_maintenance_in_progress": "Операция обслуживания блокчейна уже выполняется.",
|
||||
"appx_blockchain_rescan_complete": "Повторное сканирование блокчейна завершено",
|
||||
"appx_bootstrap_complete_reconciling": "Начальная загрузка завершена — сверка кошелька с новыми данными цепи.",
|
||||
"appx_cancel": "Отмена",
|
||||
"appx_cleaning_up": "Очистка...",
|
||||
"appx_confirm_your_backup": "Подтвердите резервную копию",
|
||||
"appx_copied_clipboard_autoclears": "Скопировано — буфер обмена очистится через 45 с",
|
||||
"appx_copy": "Копировать",
|
||||
"appx_could_not_start_restore": "Не удалось начать восстановление",
|
||||
"appx_create_failed_prefix": "Ошибка создания: ",
|
||||
"appx_creating_your_wallet": "Создание кошелька…",
|
||||
"appx_daemon_error": "Ошибка демона",
|
||||
"appx_daemon_reinstall_in_progress": "Переустановка демона уже выполняется.",
|
||||
"appx_disconnecting": "Отключение...",
|
||||
"appx_done": "Готово",
|
||||
"appx_dragonxd_output": "вывод dragonxd",
|
||||
"appx_encrypting_wallet": "Шифрование кошелька...",
|
||||
"appx_fullnode_lifecycle_unavailable_lite": "Действия жизненного цикла полного узла недоступны в lite-сборке",
|
||||
"appx_installing_bundled_daemon": "Установка встроенного демона — узел остановится, обновится и перезапустится...",
|
||||
"appx_invalid_payment_uri_prefix": "Неверный платёжный URI: ",
|
||||
"appx_ive_written_it_down": "Я записал её",
|
||||
"appx_keep_node_running_and_quit": "Оставить узел и выйти",
|
||||
"appx_last_block_n": "Последний блок: %d",
|
||||
"appx_last_used_wallet_not_found_prefix": "Ваш последний использованный файл кошелька (",
|
||||
"appx_last_used_wallet_not_found_suffix": ") не найден — вместо него открыт кошелёк по умолчанию. Если вы его переместили, восстановите файл и переключитесь обратно из списка кошельков.",
|
||||
"appx_low_spec_mode_disabled": "Режим слабого оборудования выключен",
|
||||
"appx_low_spec_mode_enabled": "Режим слабого оборудования включён",
|
||||
"appx_miner_stopped_prefix": "Майнер остановлен: ",
|
||||
"appx_miner_stopped_unexpectedly": "Майнер неожиданно остановился.",
|
||||
"appx_n_min_n_sec": "%d мин %d сек",
|
||||
"appx_n_seconds": "%d сек",
|
||||
"appx_no_bundled_daemon_to_install": "В этой сборке нет встроенного демона для установки",
|
||||
"appx_no_embedded_daemon_to_install": "В этой сборке нет встроенного демона для установки",
|
||||
"appx_node_busy_restarting": "Узел занят перезапуском — попробуйте ещё раз через мгновение.",
|
||||
"appx_node_rebuilding_witness_cache": "Узел перестраивает кэш свидетелей",
|
||||
"appx_not_next_word": " — это не следующее слово",
|
||||
"appx_payment_request_loaded": "Запрос на оплату загружен",
|
||||
"appx_pool_miner_connected_and_hashing": "Пул-майнер подключён и вычисляет хеши.",
|
||||
"appx_progress_n_of_n": "Прогресс: %d / %d",
|
||||
"appx_rebuilding_sapling_note_witnesses": "Перестройка свидетелей заметок Sapling…",
|
||||
"appx_rebuilding_witness_cache_blocks_left": "Перестройка кэша свидетелей %.0f%% — осталось блоков: %d",
|
||||
"appx_rebuilding_witness_cache_pct": "Перестройка кэша свидетелей %.0f%%",
|
||||
"appx_recovery_phrase_word_count": "Фраза восстановления должна содержать 24 слова — у вас %d.",
|
||||
"appx_restarting_daemon_rescan_flag": "Перезапуск демона с флагом -rescan...",
|
||||
"appx_restarting_daemon_zapwallettxes": "Перезапуск демона с -zapwallettxes=2 (восстановление кошелька)...",
|
||||
"appx_restoring_your_wallet": "Восстановление кошелька…",
|
||||
"appx_seed_backup_warning": "Эти 24 слова — ЕДИНСТВЕННЫЙ способ восстановить кошелёк. Запишите их по порядку, храните офлайн и никому не сообщайте. Если вы их потеряете, средства пропадут навсегда.",
|
||||
"appx_seed_not_backed_up_warning": "Вы не сохранили seed-фразу — средства могут быть потеряны. Всё равно пропустить?",
|
||||
"appx_sending_stop_command_to_daemon": "Отправка команды остановки демону...",
|
||||
"appx_setting_initial_sapling_witnesses": "Установка начальных свидетелей Sapling %.0f%%",
|
||||
"appx_shutdown_complete": "Завершение работы выполнено",
|
||||
"appx_simple_background_disabled": "Простой фон выключен",
|
||||
"appx_simple_background_enabled": "Простой фон включён",
|
||||
"appx_skip": "Пропустить",
|
||||
"appx_skip_anyway": "Всё равно пропустить",
|
||||
"appx_still_status_prefix": "Всё ещё \"",
|
||||
"appx_still_status_suffix": "\" — принудительный выход сейчас может повредить данные цепи.",
|
||||
"appx_stop_anyway_and_quit": "Всё равно остановить и выйти",
|
||||
"appx_stopping_daemon_deleting_blockchain": "Остановка демона и удаление данных блокчейна...",
|
||||
"appx_stopping_node_discards_rebuild": "Остановка узла сейчас отменит текущую перестройку и запустит её заново (несколько минут) при следующем открытии кошелька. Вместо этого можно оставить узел работать.",
|
||||
"appx_stopping_pool_miner": "Остановка пул-майнера...",
|
||||
"appx_syncing_pct_block_n_of_n": "Синхронизация %.1f%% — Блок %d / %d",
|
||||
"appx_tap_words_in_order": "Нажимайте слова в правильном порядке, чтобы подтвердить, что вы их сохранили.",
|
||||
"appx_theme_effects_disabled": "Эффекты темы выключены",
|
||||
"appx_theme_effects_enabled": "Эффекты темы включены",
|
||||
"appx_theme_prefix": "Тема: ",
|
||||
"appx_use_settings_restart_daemon_hint": "Откройте Настройки > Перезапустить демон, чтобы повторить попытку",
|
||||
"appx_waiting_for_daemon_to_encrypt_wallet": "Ожидание шифрования кошелька демоном...",
|
||||
"appx_wallet_created_and_backed_up": "Кошелёк создан, резервная копия сохранена.",
|
||||
"appx_wallet_open_failed_prefix": "Не удалось открыть кошелёк: ",
|
||||
"auto_shield": "Авто-экранирование майнинга",
|
||||
"autoshield_by_node": "Ваш узел автоматически защищает добытый coinbase за вас.",
|
||||
"autoshield_off_backup_seed": "Добытый coinbase пока не защищается автоматически. Сделайте резервную копию сид-фразы (Настройки → Резервное копирование и данные), чтобы узел мог безопасно включить эту функцию.",
|
||||
"autoshield_off_generic": "Ваш узел сейчас не защищает добытый coinbase автоматически.",
|
||||
"av_intro": "Программы для майнинга часто помечаются как потенциально нежелательные. Выполните эти шаги, чтобы включить пул-майнинг:",
|
||||
"av_open_security": "Открыть Безопасность Windows",
|
||||
"av_step1": "Шаг 1: Добавить исключение",
|
||||
@@ -74,6 +152,9 @@
|
||||
"av_title": "Windows Defender заблокировал майнер",
|
||||
"available": "Доступно",
|
||||
"backup_backing_up": "Создание резервной копии...",
|
||||
"backup_col_backup": "РЕЗЕРВНАЯ КОПИЯ",
|
||||
"backup_col_export": "ЭКСПОРТ",
|
||||
"backup_col_import": "ИМПОРТ И ВОССТАНОВЛЕНИЕ",
|
||||
"backup_create": "Создать резервную копию",
|
||||
"backup_created": "Резервная копия кошелька создана",
|
||||
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
|
||||
@@ -92,8 +173,22 @@
|
||||
"balance": "Баланс",
|
||||
"balance_history_collecting": "История баланса — сбор данных...",
|
||||
"balance_layout": "Макет баланса",
|
||||
"balance_layout_switched": "Раскладка: %s",
|
||||
"balance_mining_rate": "Майнинг %s",
|
||||
"balance_shielded_fmt": "Экранировано: %.8f",
|
||||
"balance_syncing_pct": "Синхронизация %.1f%%",
|
||||
"balance_transparent_fmt": "Прозрачный: %.8f",
|
||||
"baltab_market": "Рынок",
|
||||
"baltab_market_price_4dp": "Рынок: $%.4f",
|
||||
"baltab_market_price_8dp": "Рынок: $%.8f",
|
||||
"baltab_pct_change_24h": "%s%.1f%% за 24ч",
|
||||
"baltab_pct_of_total_zaddr": "%.0f%% от общего · %d Z-addr",
|
||||
"baltab_shielded": "Скрытый",
|
||||
"baltab_shielded_amount": "Скрытый %.8f",
|
||||
"baltab_t_addresses_count": "%d T-адресов",
|
||||
"baltab_total_balance": "Общий баланс",
|
||||
"baltab_transparent": "Прозрачный",
|
||||
"baltab_transparent_amount": "Прозрачный %.8f",
|
||||
"ban": "Заблокировать",
|
||||
"banned_peers": "Заблокированные узлы",
|
||||
"block": "Блок",
|
||||
@@ -101,6 +196,12 @@
|
||||
"block_click_copy": "Нажмите для копирования",
|
||||
"block_click_next": "Нажмите для следующего блока",
|
||||
"block_click_prev": "Нажмите для предыдущего блока",
|
||||
"block_db_reindex_body": "Обычно это происходит после обновления демона, изменившего формат данных на диске, или при повреждении индекса блоков. Ваш кошелёк и монеты в безопасности — узел просто не может загрузить цепочку, поэтому баланс показывается как нулевой.\n\nПересборка заново считывает имеющиеся файлы блоков и может занять некоторое время (также пересканируется кошелёк). Ничего не загружается.",
|
||||
"block_db_reindex_confirm": "Пересобрать базу данных блоков",
|
||||
"block_db_reindex_notify": "Узел не может прочитать свою базу данных блоков (часто после обновления демона). Пересоберите её, чтобы восстановить баланс — см. подсказку или Настройки › Узел.",
|
||||
"block_db_reindex_started": "Пересборка базы данных блоков из ваших блоков — это может занять некоторое время.",
|
||||
"block_db_reindex_title": "Пересобрать базу данных блоков?",
|
||||
"block_db_reindex_warn": "Узел не может прочитать свою базу данных блоков.",
|
||||
"block_explorer": "Обозреватель блоков",
|
||||
"block_get_info": "Получить информацию о блоке",
|
||||
"block_hash": "Хэш блока:",
|
||||
@@ -132,6 +233,7 @@
|
||||
"bootstrap_verifying": "Проверка контрольных сумм...",
|
||||
"bootstrap_wallet_protected": "(wallet.dat защищён)",
|
||||
"bootstrap_warning": "Существующие данные блоков (blocks, chainstate, notarizations) будут удалены и заменены. Ваш wallet.dat НЕ будет изменён или удалён.",
|
||||
"byte_count_fmt": "%zu / %zu байт",
|
||||
"cancel": "Отмена",
|
||||
"change_pass_confirm": "Подтвердите новый:",
|
||||
"change_pass_current": "Текущий пароль:",
|
||||
@@ -147,6 +249,10 @@
|
||||
"chat_accent_theme": "Тема",
|
||||
"chat_add_contact": "Добавить контакт",
|
||||
"chat_awaiting_key": "Ожидание ответа",
|
||||
"chat_blocked_desc": "Заблокированные разговоры удаляются, а их сообщения — старые и новые — отбрасываются, пока вы не разблокируете. Разблокировка заново импортирует разговор из блокчейна.",
|
||||
"chat_blocked_manage": "Заблокированные",
|
||||
"chat_blocked_title": "Заблокированные разговоры",
|
||||
"chat_blocked_toast": "Разговор удалён и заблокирован",
|
||||
"chat_bubble_minimal": "Минимальный",
|
||||
"chat_bubble_rounded": "Скруглённый",
|
||||
"chat_bubble_square": "Прямоугольный",
|
||||
@@ -159,6 +265,16 @@
|
||||
"chat_contact_added": "Контакт добавлен — переименуйте его в Контактах",
|
||||
"chat_contact_request": "запрос контакта",
|
||||
"chat_copy_address_tip": "Нажмите, чтобы скопировать адрес",
|
||||
"chat_delete": "Удалить разговор",
|
||||
"chat_delete_block": "Удалить и заблокировать",
|
||||
"chat_delete_body_prefix": "Удалить локальную копию разговора с ",
|
||||
"chat_delete_body_suffix": "?",
|
||||
"chat_delete_confirm": "Удалить",
|
||||
"chat_delete_failed": "Не удалось удалить разговор — ничего не изменено.",
|
||||
"chat_delete_local_note": "Это влияет только на это устройство — сообщения остаются в блокчейне, и у собеседника сохранится своя копия.",
|
||||
"chat_delete_revive_note": "«Удалить» очищает историю на этом устройстве. Если вам снова напишут, разговор появится снова.",
|
||||
"chat_delete_title": "Удалить разговор?",
|
||||
"chat_deleted_toast": "Разговор удалён",
|
||||
"chat_density_comfortable": "Свободная",
|
||||
"chat_density_compact": "Компактная",
|
||||
"chat_emoji_color": "Цветной",
|
||||
@@ -182,6 +298,7 @@
|
||||
"chat_new_button": "Новый чат",
|
||||
"chat_new_message": "Сообщение",
|
||||
"chat_new_message_toast": "Новое зашифрованное сообщение",
|
||||
"chat_new_needs_zaddr": "Для чата нужен защищённый (z) адрес — прозрачные (t) адреса не могут принимать зашифрованные сообщения.",
|
||||
"chat_new_send": "Отправить запрос",
|
||||
"chat_new_title": "Новый чат",
|
||||
"chat_new_zaddr": "Z-адрес получателя",
|
||||
@@ -227,6 +344,8 @@
|
||||
"chat_ts_24h": "24 часа",
|
||||
"chat_ts_global": "Как глобально",
|
||||
"chat_ts_global_short": "Общий",
|
||||
"chat_unblock": "Разблокировать",
|
||||
"chat_unblocked_toast": "Разговор разблокирован",
|
||||
"chat_unhide": "Показать",
|
||||
"chat_unmute": "Включить уведомления",
|
||||
"chat_verify_key": "Ключ личности — сравните для проверки",
|
||||
@@ -279,6 +398,7 @@
|
||||
"console": "Консоль",
|
||||
"console_accents": "Цветовые акценты",
|
||||
"console_app": "Прил.",
|
||||
"console_auto_focus": "Фокус на поле ввода при открытии",
|
||||
"console_auto_scroll": "Авто-прокрутка",
|
||||
"console_available_commands": "Доступные команды:",
|
||||
"console_backend_reference": "Справочник команд бэкенда",
|
||||
@@ -373,11 +493,15 @@
|
||||
"console_tab_completion": "Tab для дополнения",
|
||||
"console_text_colors": "Цвета текста",
|
||||
"console_toggle_accents": "Переключить цветовые акценты строк",
|
||||
"console_toggle_auto_focus": "Помещать курсор в поле команд при открытии вкладки Консоль",
|
||||
"console_toggle_text_color": "Переключить цвета текста строк",
|
||||
"console_type_help": "Введите 'help' для списка команд",
|
||||
"console_welcome": "Добро пожаловать в консоль ObsidianDragon",
|
||||
"console_zoom_in": "Увеличить",
|
||||
"console_zoom_out": "Уменьшить",
|
||||
"consolidate_desc": "Объедините множество мелких входов в одну защищённую заметку. Меньше заметок означает файл кошелька меньшего размера и лучшую приватность.",
|
||||
"consolidate_funds_btn": "Объединить",
|
||||
"consolidate_title": "Объединение средств",
|
||||
"contact_avatar": "АВАТАР",
|
||||
"contact_avatar_bad_image": "Не удалось загрузить это изображение.",
|
||||
"contact_avatar_badge": "Значок",
|
||||
@@ -407,6 +531,7 @@
|
||||
"contacts_shape_square": "Квадрат",
|
||||
"contacts_shape_tab": "Вкладка",
|
||||
"copied": "Скопировано!",
|
||||
"copied_to_clipboard": "Скопировано в буфер обмена",
|
||||
"copy": "Копировать",
|
||||
"copy_address": "Копировать полный адрес",
|
||||
"copy_error": "Копировать ошибку",
|
||||
@@ -419,11 +544,16 @@
|
||||
"daemon_bundled": "Встроенный",
|
||||
"daemon_install_bundled": "Установить встроенную",
|
||||
"daemon_installed": "Установлено",
|
||||
"daemon_maintenance_label": "ОБСЛУЖИВАНИЕ",
|
||||
"daemon_none_bundled": "нет в этой сборке",
|
||||
"daemon_not_installed": "не установлен",
|
||||
"daemon_status_diff": "Версия отличается",
|
||||
"daemon_status_differ": "Установленный бинарный файл отличается от встроенной версии.",
|
||||
"daemon_status_match": "Установленный бинарник соответствует встроенной версии.",
|
||||
"daemon_status_missing": "Демон не установлен — установите встроенную версию.",
|
||||
"daemon_status_none": "Не установлен",
|
||||
"daemon_status_ok": "Актуальная версия",
|
||||
"daemon_update_active_tt": "Текущий установленный (активный) узел",
|
||||
"daemon_update_available": "Доступна новая версия узла",
|
||||
"daemon_update_body": "В эту сборку кошелька встроена более новая версия узла DragonX, чем установленная сейчас. При обновлении установленный dragonxd (а также dragonx-cli/dragonx-tx) заменяется, после чего узел останавливается и перезапускается, чтобы новая версия вступила в силу. Рекомендуется — более новый узел может добавить возможности (например, поддержку сид-фразы), которых нет в старом.",
|
||||
"daemon_update_browse": "Просмотреть все релизы…",
|
||||
@@ -433,18 +563,28 @@
|
||||
"daemon_update_download_install": "Скачать и установить",
|
||||
"daemon_update_downloading": "Загрузка…",
|
||||
"daemon_update_failed": "Ошибка обновления",
|
||||
"daemon_update_install_this": "Установить эту версию",
|
||||
"daemon_update_installed": "Установлено:",
|
||||
"daemon_update_installed_ok": "Узел установлен",
|
||||
"daemon_update_installing": "Установка…",
|
||||
"daemon_update_keep": "Оставить текущий",
|
||||
"daemon_update_later": "Позже",
|
||||
"daemon_update_latest": "Последняя:",
|
||||
"daemon_update_latest_badge": "новейшая",
|
||||
"daemon_update_latest_tt": "Новейшая доступная версия",
|
||||
"daemon_update_loading": "Загрузка релизов…",
|
||||
"daemon_update_no_notes": "Нет заметок о выпуске для этой версии.",
|
||||
"daemon_update_now": "Обновить сейчас",
|
||||
"daemon_update_prerelease_tt": "Предварительная / тестовая сборка",
|
||||
"daemon_update_prompt_title": "Обновить демон узла?",
|
||||
"daemon_update_reinstall": "Переустановить",
|
||||
"daemon_update_released": "Выпущено:",
|
||||
"daemon_update_restart_note": "Перезапустите daemon, чтобы начать работу с новой версией.",
|
||||
"daemon_update_restart_now": "Перезапустить демон сейчас",
|
||||
"daemon_update_safe": "Ваш кошелёк, ключи и данные блокчейна не затрагиваются — заменяются только программные файлы демона. Если вы намеренно используете собственный узел, выберите «Оставить текущий».",
|
||||
"daemon_update_status_current": "Эта версия установлена",
|
||||
"daemon_update_status_latest": "Последняя версия",
|
||||
"daemon_update_status_older": "Старее установленной версии",
|
||||
"daemon_update_title": "Обновить демон узла?",
|
||||
"daemon_update_unavailable_body": "Для этой платформы нет доступной сборки узла.",
|
||||
"daemon_update_unavailable_title": "Обновления узла недоступны",
|
||||
@@ -453,12 +593,20 @@
|
||||
"daemon_update_verify_note": "Перед установкой загрузка проверяется по опубликованному для релиза SHA-256 и закреплённой подписи ed25519.",
|
||||
"daemon_update_verifying": "Проверка…",
|
||||
"daemon_update_version": "Версия:",
|
||||
"daemon_updates_label": "ОБНОВЛЕНИЯ",
|
||||
"daemon_version": "Демон",
|
||||
"dark": "Тёмная",
|
||||
"data_stale_prefix": "Обновлено",
|
||||
"data_stale_tooltip": "Баланс может быть устаревшим — кошелёк давно не получал обновлений. Проверьте подключение к узлу.",
|
||||
"date": "Дата",
|
||||
"date_label": "Дата:",
|
||||
"debug_gate_confirm": "Показать отладочные параметры",
|
||||
"debug_gate_incorrect": "Неверный PIN или пароль.",
|
||||
"debug_gate_pass_prompt": "Введите пароль кошелька, чтобы продолжить:",
|
||||
"debug_gate_pin_prompt": "Введите PIN разблокировки, чтобы продолжить:",
|
||||
"debug_gate_title": "Показать отладочные параметры?",
|
||||
"debug_gate_verifying": "Проверка…",
|
||||
"debug_gate_warning": "Это расширенные диагностические и настроечные параметры узла. Они могут раскрыть конфиденциальную информацию и изменить работу вашего узла. Продолжайте, только если понимаете, что делаете.",
|
||||
"debug_logging": "ЖУРНАЛ ОТЛАДКИ",
|
||||
"decrypt_desc": "Кошелёк будет экспортирован, демон перезапущен с новым незашифрованным кошельком, и все ключи будут повторно импортированы. Это может занять несколько минут в зависимости от размера кошелька.",
|
||||
"decrypt_error_title": "Не удалось расшифровать",
|
||||
@@ -481,10 +629,22 @@
|
||||
"disconnected": "Отключено",
|
||||
"dismiss": "Отклонить",
|
||||
"display": "Отображение",
|
||||
"done": "Готово",
|
||||
"download": "Скачать",
|
||||
"download_bootstrap": "Скачать бутстрап",
|
||||
"dragonx_green": "DragonX (Зелёная)",
|
||||
"edit": "Редактировать",
|
||||
"empty_wallet_keys_suffix": "ключей",
|
||||
"empty_wallet_open_manager": "Открыть менеджер кошельков",
|
||||
"empty_wallet_restore": "Восстановить мой кошелёк",
|
||||
"empty_wallet_salvage_body": "Этот кошелёк пуст, потому что предыдущее автоматическое восстановление отложило ваш исходный кошелёк в качестве резервной копии. Ваши монеты почти наверняка находятся в этой копии и не потеряны. Восстановите её, чтобы снова загрузить средства — ничего не удаляется; текущий файл сначала откладывается в сторону.",
|
||||
"empty_wallet_salvage_headline": "Ваши монеты в безопасности в файле резервной копии.",
|
||||
"empty_wallet_salvage_title": "Возможно, ваш кошелёк был восстановлен",
|
||||
"empty_wallet_warning_body": "В этом кошельке нет адресов и средств, но другой файл кошелька в вашей папке DragonX содержит ключи. Ваши монеты, скорее всего, находятся в нём и не потеряны. Откройте менеджер кошельков, чтобы переключиться на кошелёк с вашими средствами.",
|
||||
"empty_wallet_warning_dismiss": "Больше не предупреждать для этого кошелька",
|
||||
"empty_wallet_warning_dismiss_tip": "Останавливает это предупреждение только для текущего файла кошелька. Если позже вы переключитесь на другой пустой кошелёк, предупреждение может появиться снова.",
|
||||
"empty_wallet_warning_headline": "Возможно, вы открыли не тот кошелёк.",
|
||||
"empty_wallet_warning_title": "Этот кошелёк пуст",
|
||||
"enc_confirm": "Подтвердите:",
|
||||
"enc_desc": "Шифрование кошелька защищает ваши приватные ключи паролем. После шифрования демон перезапустится.",
|
||||
"enc_encrypting": "Шифрование кошелька...",
|
||||
@@ -523,6 +683,7 @@
|
||||
"explorer_tx_outputs": "Выходы",
|
||||
"explorer_tx_size": "Размер",
|
||||
"explorer_txid": "TxID:",
|
||||
"explorer_urls_hdr": "URL-АДРЕСА",
|
||||
"export": "Экспорт",
|
||||
"export_csv": "Экспорт в CSV",
|
||||
"export_keys_btn": "Экспорт ключей",
|
||||
@@ -550,6 +711,151 @@
|
||||
"export_viewing_key": "Экспорт ключа просмотра",
|
||||
"failed_create_shielded": "Не удалось создать экранированный адрес",
|
||||
"failed_create_transparent": "Не удалось создать прозрачный адрес",
|
||||
"faq": "ЧаВо",
|
||||
"faq_d_mgmt_1_a": "Встроенный узел запускается автоматически вместе с кошельком и останавливается, когда это уместно, поэтому обычно управлять им вручную не нужно. Расширенные элементы управления (включая перезапуск демона) находятся в Настройки > Узел и безопасность.",
|
||||
"faq_d_mgmt_1_q": "Как запустить или остановить узел?",
|
||||
"faq_d_mgmt_2_a": "Если вы подключаете кошелёк к узлу, который запустили сами, этот параметр определяет, останавливается ли этот узел при закрытии кошелька. Оставьте его выключенным, если хотите, чтобы узел продолжал работать после закрытия кошелька.",
|
||||
"faq_d_mgmt_2_q": "Что делает параметр «закрывать внешний демон при выходе»?",
|
||||
"faq_d_mgmt_3_a": "Данные узла и кошелька хранятся в каталоге данных DragonX в профиле вашего пользователя (например, в AppData в Windows или в домашней папке в Linux/macOS). Блокчейн занимает большую часть; оставьте достаточно свободного места на диске для его роста.",
|
||||
"faq_d_mgmt_3_q": "Где хранятся данные блокчейна и кошелька?",
|
||||
"faq_d_mgmt_title": "Управление узлом",
|
||||
"faq_d_mine_1_a": "Вкладка «Майнинг» позволяет майнить на вашем процессоре (CPU). DragonX использует алгоритм RandomX, разработанный для обычных процессоров. Задайте количество потоков и запустите — вознаграждения начисляются на ваш кошелёк.",
|
||||
"faq_d_mine_1_q": "Как майнить DragonX?",
|
||||
"faq_d_mine_2_a": "Соло-майнинг отправляет награду прямо вам, но платит только тогда, когда вы находите блок — а это редкость, если у вас нет большого хешрейта. Пул-майнинг делит работу с другими ради меньших, но более стабильных выплат. Вкладка «Майнинг» поддерживает оба варианта.",
|
||||
"faq_d_mine_2_q": "Соло или пул-майнинг — что выбрать?",
|
||||
"faq_d_mine_3_a": "В разделе пула на вкладке «Майнинг» есть кнопка «Обновить майнер…», которая скачивает, проверяет и устанавливает последнюю оптимизированную сборку майнера DragonX. Как и в случае с обновлением узла, каждая загрузка проверяется по контрольной сумме и цифровой подписи перед установкой.",
|
||||
"faq_d_mine_3_q": "Как обновить майнер?",
|
||||
"faq_d_mine_4_a": "Текущий узел может запускать встроенный stratum-сервер, чтобы другие майнеры могли подключаться к вашему компьютеру. Переключатель для этого находится в Настройки > Узел и безопасность. По умолчанию он слушает только локально; чтобы открыть его для других компьютеров, это нужно включить намеренно.",
|
||||
"faq_d_mine_4_q": "Можно ли разместить у себя майнинг-пул?",
|
||||
"faq_d_mine_title": "Майнинг и пулы",
|
||||
"faq_d_net_1_a": "Сразу после запуска узлу нужно немного времени, чтобы найти пиров, поэтому кратковременное «0 пиров» — это нормально. Если это продолжается долго, проверьте подключение к интернету и убедитесь, что брандмауэр не блокирует узел. Узел находит пиров автоматически через встроенные сиды.",
|
||||
"faq_d_net_1_q": "Кошелёк показывает отсутствие пиров или подключений — что не так?",
|
||||
"faq_d_net_2_a": "Да — узлы DragonX общаются друг с другом по TLS, поэтому соединения с пирами зашифрованы. Кошелёк также общается со своим узлом по защищённому локальному каналу.",
|
||||
"faq_d_net_2_q": "Зашифровано ли моё соединение с сетью?",
|
||||
"faq_d_net_title": "Пиры и сеть",
|
||||
"faq_d_node_1_a": "Демон, dragonxd, — это полный узел DragonX. Он скачивает и проверяет весь блокчейн, передаёт транзакции в сеть и отвечает на запросы кошелька. Запуск собственного узла означает, что вам не нужно доверять чужому серверу.",
|
||||
"faq_d_node_1_q": "Что такое демон (dragonxd)?",
|
||||
"faq_d_node_2_a": "По умолчанию кошелёк запускает и управляет собственным встроенным узлом (embedded) — вам не нужно ничего делать. Если у вас уже запущен свой dragonxd, кошелёк может подключиться к этому внешнему узлу вместо своего. Кошелёк определяет, какая ситуация имеет место, и действует соответственно.",
|
||||
"faq_d_node_2_q": "В чём разница между встроенным и внешним узлом?",
|
||||
"faq_d_node_3_a": "Полный узел независимо проверяет каждое правило блокчейна, поэтому ваш кошелёк доверяет математике, а не третьей стороне. Это также укрепляет сеть. Плата за это — место на диске и первоначальная синхронизация.",
|
||||
"faq_d_node_3_q": "Зачем вообще запускать полный узел?",
|
||||
"faq_d_node_title": "Полный узел",
|
||||
"faq_d_perf_1_a": "Блокчейн — самая большая часть, и он растёт со временем, поэтому держите свободными несколько гигабайт. Узел использует кеш базы данных для скорости; встроенные значения по умолчанию настроены для типичных машин. Продвинутые пользователи могут изменить размер кеша базы данных узла, отредактировав dbcache в файле конфигурации (DRAGONX.conf).",
|
||||
"faq_d_perf_1_q": "Сколько места на диске использует узел и можно ли настроить память?",
|
||||
"faq_d_perf_2_a": "Включите загрузку bootstrap (предлагается в мастере настройки), чтобы получить свежую проверенную копию цепочки вместо проверки каждого блока с нуля. Быстрый диск (SSD) также сильно помогает.",
|
||||
"faq_d_perf_2_q": "Как ускорить первоначальную синхронизацию?",
|
||||
"faq_d_perf_title": "Хранилище и производительность",
|
||||
"faq_d_sync_1_a": "При запуске узел загружает и проверяет блоки, прежде чем сможет обслуживать кошелёк. Экран «Обработка блоков / Применение блоков для построения текущего состояния цепочки» — это и есть такой разогрев. Это нормально; кошелёк становится доступен, как только узел закончит и сообщит свою высоту.",
|
||||
"faq_d_sync_1_q": "Что означает «Обработка блоков» при запуске?",
|
||||
"faq_d_sync_2_a": "Для ваших защищённых средств узел хранит криптографические «свидетели» (witnesses), которые позволяют тратить их приватно. После некоторых перезапусков или обновлений он перестраивает этот кеш, повторно сканируя недавние блоки. На кошельке со множеством транзакций это может занять несколько минут и потребовать дополнительной памяти.\n\nЭто не зависание — дайте процессу завершиться. Принудительное закрытие в середине процесса просто заставит его перезапустить перестроение в следующий раз.",
|
||||
"faq_d_sync_2_q": "Что такое «Перестроение кеша свидетелей»?",
|
||||
"faq_d_sync_3_a": "Вблизи вершины цепочки работа по сканированию кошелька (проверка балансов и заметок) конкурирует с узлом за одну и ту же внутреннюю блокировку, что может замедлить подключение последних блоков на крупном кошельке. Последние версии ограничивают это сканирование, чтобы узел мог наверстать упущенное. Если оставаться на более лёгкой вкладке (или просто подождать), процесс завершится быстрее.",
|
||||
"faq_d_sync_3_q": "Почему синхронизация медленная, когда я почти догнал сеть?",
|
||||
"faq_d_sync_4_a": "Первая синхронизация скачивает и проверяет всю цепочку и может занять некоторое время в зависимости от вашего соединения и диска. Вы можете значительно ускорить её, включив загрузку bootstrap в мастере настройки — она получает свежую проверенную копию данных цепочки.",
|
||||
"faq_d_sync_4_q": "Сколько занимает первоначальная синхронизация?",
|
||||
"faq_d_sync_title": "Синхронизация и блокчейн",
|
||||
"faq_d_trbl_1_a": "Обычно нет. На крупном кошельке узел может тратить несколько минут на перестроение защищённых свидетелей или применение блоков, в течение которых он выглядит приостановленным и может медленно отвечать. Дайте ему время — журнал отладки (в настройках) показывает стабильный прогресс, если всё работает.\n\nИзбегайте принудительного закрытия на этом этапе — иначе работа перезапустится с нуля при следующем запуске.",
|
||||
"faq_d_trbl_1_q": "Узел словно застрял на «Активация лучшей цепочки» — он завис?",
|
||||
"faq_d_trbl_2_a": "Если файл кошелька был восстановлен, но часть его ключевых данных отсутствует, узел может открыть его в ограниченном «пониженном» режиме, в котором нельзя создавать новые адреса или защищать средства. Самое надёжное решение — восстановить кошелёк из сид-фразы в новый кошелёк.",
|
||||
"faq_d_trbl_2_q": "Что такое «пониженный режим»?",
|
||||
"faq_d_trbl_3_a": "Пересканирование заставляет кошелёк заново прочитать цепочку, чтобы заново обнаружить ваши транзакции (полезно после импорта ключей). Переиндексация перестраивает базу данных блокчейна узла и нужна только в том случае, если эта база повреждена или несовместима после смены версии. Оба процесса могут занять некоторое время; запускайте их из Настройки > Узел и безопасность.",
|
||||
"faq_d_trbl_3_q": "Когда нужно переиндексировать или пересканировать?",
|
||||
"faq_d_trbl_4_a": "Использование памяти резко возрастает во время тяжёлой работы, например перестроения свидетелей или первоначальной синхронизации, а затем снижается после завершения. Постоянно высокое потребление памяти в состоянии простоя — это необычно; перезапуск кошелька сбрасывает его. На машине с малым объёмом ОЗУ убедитесь, что другие требовательные к памяти приложения не конкурируют за ресурсы.",
|
||||
"faq_d_trbl_4_q": "Узел использует много памяти — это нормально?",
|
||||
"faq_d_trbl_title": "Устранение неполадок",
|
||||
"faq_d_upd_1_a": "Откройте Настройки > Узел и безопасность > Бинарник демона и нажмите «Проверить обновления…». Кошелёк скачивает последнюю проверенную сборку узла и устанавливает её; новая версия вступает в силу при следующем запуске демона.",
|
||||
"faq_d_upd_1_q": "Как обновить узел?",
|
||||
"faq_d_upd_2_a": "Да — диалог обновления перечисляет все релизы, чтобы вы могли выбрать конкретную или более старую сборку. Будьте осторожны при откате на более старую версию: старый узел может не принять данные блокчейна, записанные более новым, и может потребоваться переиндексация.",
|
||||
"faq_d_upd_2_q": "Можно ли установить конкретную версию узла?",
|
||||
"faq_d_upd_3_a": "Каждый скачанный архив узла проверяется по контрольной сумме и криптографической подписи перед установкой. Если проверка не проходит, кошелёк отклоняет обновление.",
|
||||
"faq_d_upd_3_q": "Безопасно ли обновление?",
|
||||
"faq_d_upd_title": "Обновление узла",
|
||||
"faq_group_daemon": "Демон",
|
||||
"faq_group_wallet": "Кошелёк",
|
||||
"faq_intro": "Ответы о вашем кошельке и узле DragonX. Ищите или просматривайте темы ниже.",
|
||||
"faq_intro_lite": "Ответы о вашем Lite-кошельке. Ищите или просматривайте темы ниже.",
|
||||
"faq_l_gs_1_a": "ObsidianDragonLite — это облегчённый кошелёк DragonX. Вместо запуска полного узла он подключается к lite-серверу DragonX, поэтому запускается быстро и не скачивает весь блокчейн.\n\nОн управляет вашими ключами, балансами и отправкой так же, как и полный кошелёк — см. раздел «Lite-кошелёк» ниже о том, как работает подключение к серверу, и о компромиссе с приватностью.",
|
||||
"faq_l_gs_1_q": "Что такое ObsidianDragonLite?",
|
||||
"faq_l_lite_1_a": "Lite-кошелёк общается с lite-сервером DragonX вместо запуска собственного полного узла. Это делает его быстрым в запуске и лёгким по нагрузке на диск и трафик, поскольку он сам не скачивает и не проверяет весь блокчейн. Ваши приватные ключи всегда остаются на вашем устройстве — они никогда не отправляются на сервер.",
|
||||
"faq_l_lite_1_q": "Чем Lite-кошелёк отличается от полного кошелька?",
|
||||
"faq_l_lite_2_a": "Откройте настройки и выберите сервер. Вы можете сменить сервер в любой момент, и кошелёк заново синхронизируется с новым. Если сервер медленный или недоступен, попробуйте другой.",
|
||||
"faq_l_lite_2_q": "Как выбрать или сменить сервер?",
|
||||
"faq_l_lite_3_a": "Ваши ключи никогда не покидают устройство, а защищённые суммы остаются зашифрованными. Но lite-сервер видит, какими адресами интересуется ваш кошелёк, и ваш IP-адрес, поэтому он может связать ваши адреса между собой — это компромисс удобства ради приватности. Для максимальной приватности используйте кошелёк с полным узлом, который сам проверяет цепочку и не раскрывает ваши адреса серверу.",
|
||||
"faq_l_lite_3_q": "Так же ли приватен Lite-кошелёк, как полный узел?",
|
||||
"faq_l_lite_title": "Lite-кошелёк",
|
||||
"faq_l_sec_1_a": "Откройте Настройки > Кошелёк и задайте пароль в разделе безопасности. Шифрование защищает ваши приватные ключи на диске, поэтому тот, кто получит доступ к файлам, всё равно не сможет потратить ваши монеты.\n\nВыберите надёжный пароль и не теряйте его — восстановить зашифрованный кошелёк без него невозможно.",
|
||||
"faq_no_results": "Ничего не найдено. Попробуйте другой запрос.",
|
||||
"faq_open_tooltip": "Справка и FAQ",
|
||||
"faq_search_hint": "Поиск по FAQ…",
|
||||
"faq_title": "Справка и FAQ",
|
||||
"faq_w_addr_1_a": "Прозрачные адреса (начинаются с R или t) работают как в большинстве монет: суммы и адреса видны в блокчейне.\n\nЗащищённые адреса (Z-адреса, начинаются с zs) используют криптографию с нулевым разглашением — сумма, отправитель и получатель зашифрованы в цепочке. Балансы на защищённых адресах приватны.",
|
||||
"faq_w_addr_1_q": "В чём разница между прозрачными и защищёнными адресами?",
|
||||
"faq_w_addr_2_a": "По возможности используйте защищённые (Z) адреса — они сохраняют приватность вашего баланса и истории платежей. DragonX — приватная монета, и защищённый адрес используется по умолчанию для получения средств.\n\nПрозрачный адрес стоит использовать только если сервис, с которым вы работаете, не поддерживает защищённые адреса.",
|
||||
"faq_w_addr_2_q": "Какой тип адреса мне использовать?",
|
||||
"faq_w_addr_3_a": "Для защищённых адресов повторное использование допустимо и не раскрывает историю. Для прозрачных адресов использование нового адреса для каждого платежа повышает приватность. Создавайте новые адреса на вкладке «Получить».",
|
||||
"faq_w_addr_3_q": "Использовать один адрес повторно или создавать новый?",
|
||||
"faq_w_addr_4_a": "Экранирование перемещает монеты с прозрачного адреса на защищённый, делая их приватными. Новые добытые монеты поступают как прозрачные и экранируются автоматически.\n\nВы также можете экранировать монеты вручную из кошелька — экранирование — это обычная транзакция, поэтому она подтверждается за обычное время.",
|
||||
"faq_w_addr_4_q": "Что такое «экранирование»?",
|
||||
"faq_w_addr_title": "Адреса и приватность",
|
||||
"faq_w_bal_1_a": "Пока кошелёк синхронизируется, ваш баланс неполный и может отображаться как 0 — не все транзакции ещё просканированы. Он заполнится после завершения синхронизации.\n\nСледите за строкой состояния внизу окна: она показывает состояние соединения и высоту блока. После завершения синхронизации баланс будет точным.",
|
||||
"faq_w_bal_1_q": "Почему мой баланс 0 или не обновляется?",
|
||||
"faq_w_bal_2_a": "Каждый новый блок, добытый поверх блока с вашей транзакцией, добавляет одно подтверждение. Чем больше подтверждений, тем надёжнее закреплён платёж. Полученные средства становятся доступны для отправки после первого подтверждения.",
|
||||
"faq_w_bal_2_q": "Что такое подтверждения?",
|
||||
"faq_w_bal_3_a": "Общий баланс включает средства, которые ещё подтверждаются или временно заблокированы (например, монеты в процессе экранирования). Доступный баланс — это то, что можно отправить прямо сейчас. Они сравниваются по мере подтверждения транзакций.",
|
||||
"faq_w_bal_3_q": "В чём разница между общим и доступным балансом?",
|
||||
"faq_w_bal_title": "Баланс и синхронизация",
|
||||
"faq_w_chat_1_a": "Чат — это зашифрованный мессенджер на основе блокчейна, встроенный в кошелёк. Сообщения отправляются как приватные защищённые заметки, поэтому прочитать их могут только вы и ваш собеседник.",
|
||||
"faq_w_chat_1_q": "Что такое функция чата?",
|
||||
"faq_w_chat_2_a": "Ваша личность в чате формируется из сид-фразы кошелька, поэтому она всегда следует за кошельком — восстановите кошелёк, и ваша личность вернётся. Отдельный аккаунт или пароль не нужны.",
|
||||
"faq_w_chat_2_q": "Как работает моя личность в чате?",
|
||||
"faq_w_chat_3_a": "Добавьте контакт по его адресу на вкладке «Контакты», при желании указав имя и аватар. Контакты можно хранить только для текущего кошелька или использовать во всех кошельках, которые вы открываете на этом компьютере.",
|
||||
"faq_w_chat_3_q": "Как работают контакты?",
|
||||
"faq_w_chat_4_a": "Откройте разговор и используйте значки в его заголовке. «Скрыть» (глаз) убирает разговор из списка, но сохраняет все сообщения — новое сообщение снова покажет разговор. «Удалить» (корзина) открывает диалог: «Удалить» очищает историю на этом устройстве (если собеседник снова напишет вам, разговор появится вновь), а «Удалить и заблокировать» также останавливает будущие сообщения от него, пока вы не разблокируете его в списке «Заблокированные» над списком разговоров. Всё это действует только на этом устройстве — сообщения остаются в блокчейне, и у собеседника сохраняется своя копия, поэтому это не «отмена отправки».",
|
||||
"faq_w_chat_4_q": "Как скрыть, удалить или заблокировать разговор?",
|
||||
"faq_w_chat_title": "Чат и контакты",
|
||||
"faq_w_gs_1_a": "ObsidianDragon — это полноузловой кошелёк для DragonX (DRGX). Он управляет вашими монетами и в десктопной сборке запускает полный узел DragonX (демон) в фоне, поэтому кошелёк сам проверяет блокчейн, а не доверяет стороннему серверу.\n\nПредставьте это как две части, работающие вместе: кошелёк (ключи, балансы, отправка) и узел (блокчейн и сеть). Вопросы здесь разделены так же — см. вкладки «Кошелёк» и «Демон» выше.",
|
||||
"faq_w_gs_1_q": "Что такое ObsidianDragon?",
|
||||
"faq_w_gs_2_a": "При первом запуске мастер настройки автоматически создаёт для вас кошелёк. Новые кошельки защищены секретной сид-фразой (списком слов). Запишите эту фразу и храните её офлайн — это единственный способ восстановить средства, если этот компьютер будет потерян.\n\nПросмотреть или сохранить фразу можно в любой момент в Настройки > Резервное копирование и данные > Сид-фраза.",
|
||||
"faq_w_gs_2_q": "Как создать новый кошелёк?",
|
||||
"faq_w_gs_3_a": "Используйте восстановление по сид-фразе, если у вас есть сид-фраза. (В полноузловой сборке вы также можете скопировать существующий файл wallet.dat в папку данных кошелька перед запуском.)\n\nПосле восстановления кошелёк повторно сканирует блокчейн, чтобы найти ваши прошлые транзакции, поэтому баланс и история могут появиться не сразу.",
|
||||
"faq_w_gs_3_q": "Как восстановить уже имеющийся кошелёк?",
|
||||
"faq_w_gs_4_a": "Мастер позволяет выбрать оформление/тему, настроить шифрование и PIN, а также — в полноузловой сборке — при желании загрузить bootstrap для ускорения первой синхронизации. Любой шаг можно пропустить и изменить позже в Настройках.",
|
||||
"faq_w_gs_4_q": "Что делает первоначальная настройка?",
|
||||
"faq_w_gs_title": "Начало работы",
|
||||
"faq_w_sec_1_a": "Откройте Настройки > Узел и безопасность и задайте пароль (раздел БЕЗОПАСНОСТЬ). Шифрование защищает ваши приватные ключи на диске, поэтому тот, кто получит доступ к файлам, всё равно не сможет потратить ваши монеты.\n\nВыберите надёжный пароль и не теряйте его — восстановить зашифрованный кошелёк без него невозможно.",
|
||||
"faq_w_sec_1_q": "Как зашифровать свой кошелёк?",
|
||||
"faq_w_sec_2_a": "PIN блокирует экран приложения, чтобы скрыть балансы и действия, когда вы отходите. Это дополнительная блокировка поверх шифрования; вы настраиваете её в мастере настройки или в Настройках после шифрования кошелька.",
|
||||
"faq_w_sec_2_q": "Что такое PIN / экран блокировки?",
|
||||
"faq_w_sec_3_a": "Нет. Шифрование кошелька невозможно обойти. Если у вас сохранилась сид-фраза восстановления, вы можете восстановить кошелёк из неё в новый кошелёк и задать новый пароль. Без пароля или сид-фразы средства восстановить нельзя.",
|
||||
"faq_w_sec_3_q": "Я забыл пароль — можно ли его восстановить?",
|
||||
"faq_w_sec_title": "Безопасность и шифрование",
|
||||
"faq_w_seed_1_a": "Перейдите в Настройки > Резервное копирование и данные > Сид-фраза. Запишите слова на бумаге по порядку и храните их в надёжном месте офлайн. Любой, у кого есть эта фраза, может потратить ваши монеты, поэтому никогда не храните её в фото, письме или облачной заметке.",
|
||||
"faq_w_seed_1_q": "Как сделать резервную копию сид-фразы?",
|
||||
"faq_w_seed_2_a": "Старые (устаревшие) кошельки не были основаны на сид-фразе. Кошелёк умеет переносить устаревший кошелёк в современный кошелёк с сид-фразой: он создаёт новый сид-кошелёк и переводит в него ваши средства. Ищите «Перейти на сид-кошелёк…» в Настройки > Резервное копирование и данные. Для этой функции нужна текущая версия демона.",
|
||||
"faq_w_seed_2_q": "У моего кошелька нет сид-фразы — можно ли её добавить?",
|
||||
"faq_w_seed_3_a": "При настройке кошелька используйте восстановление по сид-фразе и введите слова по порядку. Затем кошелёк повторно сканирует блокчейн, чтобы восстановить баланс и историю, что при первом запуске может занять некоторое время.",
|
||||
"faq_w_seed_3_q": "Как восстановить кошелёк по сид-фразе?",
|
||||
"faq_w_seed_4_a": "Ваша сид-фраза — это основная резервная копия, и её достаточно для полного восстановления. Копия файла кошелька — удобная дополнительная резервная копия, которая также сохраняет метки и настройки. Храните любые резервные копии офлайн и в тайне.",
|
||||
"faq_w_seed_4_q": "Нужно ли также сохранять резервную копию wallet.dat?",
|
||||
"faq_w_seed_title": "Сид-фраза и резервное копирование",
|
||||
"faq_w_send_1_a": "Откройте вкладку «Отправить», вставьте адрес получателя, укажите сумму и подтвердите. Если кошелёк зашифрован, вам предложат разблокировать его для транзакции.\n\nОтправка на защищённые адреса приватна; отправка на прозрачные адреса публична.",
|
||||
"faq_w_send_1_q": "Как отправить средства?",
|
||||
"faq_w_send_2_a": "Комиссии DragonX очень низкие и устанавливаются автоматически. Комиссия показывается перед подтверждением. Обычно менять её не нужно.",
|
||||
"faq_w_send_2_q": "Какую комиссию я плачу?",
|
||||
"faq_w_send_3_a": "Да — при отправке на защищённый (Z) адрес можно приложить зашифрованную заметку. Прочитать её сможет только получатель. Прозрачные адреса не поддерживают заметки.",
|
||||
"faq_w_send_3_q": "Можно ли прикрепить сообщение к платежу?",
|
||||
"faq_w_send_4_a": "Транзакция остаётся «в ожидании», пока не будет добыта в блок и не получит подтверждения. Обычно это занимает минуту-две. Защищённым транзакциям также нужно, чтобы кошелёк был синхронизирован, для построения доказательства.\n\nЕсли отправка необычно долго остаётся в ожидании, проверьте, что кошелёк подключён и синхронизирован (см. строку состояния внизу окна).",
|
||||
"faq_w_send_4_q": "Я отправил платёж, но он в статусе «в ожидании» — почему?",
|
||||
"faq_w_send_title": "Отправка и получение",
|
||||
"faq_w_set_1_a": "Откройте Настройки > Внешний вид, чтобы выбрать тему и акцентный цвет. Доступно несколько светлых и тёмных оформлений, а эффекты вроде размытия можно отключить на маломощных компьютерах.",
|
||||
"faq_w_set_1_q": "Как изменить тему?",
|
||||
"faq_w_set_2_a": "Да — в Настройки > Внешний вид есть выбор языка. В кошелёк встроено несколько переводов; всё, что ещё не переведено, отображается на английском.",
|
||||
"faq_w_set_2_q": "Можно ли изменить язык?",
|
||||
"faq_w_set_3_a": "Используйте параметр масштаба шрифта в Настройки > Внешний вид. Он масштабирует весь интерфейс — это удобно на дисплеях высокого разрешения (HiDPI), где приложение иначе может выглядеть слишком мелким.",
|
||||
"faq_w_set_3_q": "Текст слишком мелкий (или слишком крупный) — можно ли его масштабировать?",
|
||||
"faq_w_set_4_a": "Включите получение курса в Настройках. После этого кошелёк будет показывать приблизительную стоимость в фиатной валюте рядом с балансами и сможет отображать небольшой график рынка. Цены приведены только для справки.",
|
||||
"faq_w_set_4_q": "Как показать цены в фиатной валюте?",
|
||||
"faq_w_set_title": "Настройки и внешний вид",
|
||||
"favorite_address": "Добавить в избранное",
|
||||
"fee": "Комиссия",
|
||||
"fee_high": "Высокая",
|
||||
@@ -572,6 +878,63 @@
|
||||
"general": "Общие",
|
||||
"generating": "Генерация",
|
||||
"go_to_receive": "Перейти к получению",
|
||||
"grpa_current_block_paren": "(Текущий: %d)",
|
||||
"grpa_days_ago": "%lld дн назад",
|
||||
"grpa_dbg_addrman": "Отслеживание и управление адресами узлов",
|
||||
"grpa_dbg_alert": "Сообщения системы оповещений",
|
||||
"grpa_dbg_bench": "Замеры производительности операций",
|
||||
"grpa_dbg_coindb": "Операции чтения/записи базы монет",
|
||||
"grpa_dbg_db": "Операции Berkeley DB",
|
||||
"grpa_dbg_estimatefee": "Алгоритм оценки комиссии",
|
||||
"grpa_dbg_http": "Активность HTTP RPC-сервера",
|
||||
"grpa_dbg_libevent": "Сетевая библиотека Libevent",
|
||||
"grpa_dbg_lock": "Отладка конкуренции блокировок",
|
||||
"grpa_dbg_mempool": "Активность пула транзакций в памяти",
|
||||
"grpa_dbg_net": "Сетевые соединения и сообщения",
|
||||
"grpa_dbg_paymentdisclosure": "Протокол раскрытия платежей",
|
||||
"grpa_dbg_pow": "Активность майнинга proof-of-work",
|
||||
"grpa_dbg_proxy": "Соединения через прокси SOCKS5",
|
||||
"grpa_dbg_prune": "Операции обрезки блоков",
|
||||
"grpa_dbg_rand": "Генерация случайных чисел",
|
||||
"grpa_dbg_reindex": "Прогресс переиндексации блокчейна",
|
||||
"grpa_dbg_rpc": "Обработка RPC-команд",
|
||||
"grpa_dbg_selectcoins": "Выбор монет для транзакций",
|
||||
"grpa_dbg_tor": "Интеграция Tor и сведения о цепочках",
|
||||
"grpa_dbg_zmq": "Система уведомлений ZeroMQ",
|
||||
"grpa_dbg_zrpc": "Скрытые (z-addr) RPC-операции",
|
||||
"grpa_enter_private_key_to_import": "Введите приватный ключ для импорта.",
|
||||
"grpa_error_prefix": "Ошибка: ",
|
||||
"grpa_hr_ago": "%lld ч назад",
|
||||
"grpa_invalid_response_from_daemon": "Неверный ответ от демона",
|
||||
"grpa_invalid_suffix": " (неверный)",
|
||||
"grpa_min_ago": "%lld мин назад",
|
||||
"grpa_sec_ago": "%lld сек назад",
|
||||
"grpa_seed_demo_chat": "Демо-чат seed",
|
||||
"grpa_showing_first_100_of": "... показаны первые 100 из %d",
|
||||
"grpa_tab_about": "О программе",
|
||||
"grpa_tab_appearance": "Внешний вид",
|
||||
"grpa_tab_backup_data": "Резервное копирование и данные",
|
||||
"grpa_tab_chat": "Чат",
|
||||
"grpa_tab_explorer": "Обозреватель",
|
||||
"grpa_tab_node_security": "Узел и безопасность",
|
||||
"grpa_tab_wallet": "Кошелёк",
|
||||
"grpa_unexpected_getblockhash_result": "неожиданный результат getblockhash",
|
||||
"grpb_copy": "Копировать",
|
||||
"grpb_max": "Макс",
|
||||
"grpb_new_badge_suffix": " [НОВОЕ]",
|
||||
"grpb_preview_msg_payment_through": "Платёж прошёл? 🙂",
|
||||
"grpb_preview_msg_sending_rest": "Отправляю остаток 👍",
|
||||
"grpb_preview_msg_yep_confirmed": "Да — только что подтвердил ✅",
|
||||
"grpb_selected_suffix": "\n(выбрано)",
|
||||
"grpb_tooltip_address_balance": "%s\nБаланс: %.8f %s%s",
|
||||
"grpb_undo_clear": "Отменить очистку",
|
||||
"grpc_benchmark_inconclusive": "Бенчмарк не дал результата: не записано ни одной выборки хешрейта. Проверьте соединение с пулом и повторите попытку.",
|
||||
"grpc_benchmark_takes_secs": "Бенчмарк занимает ~%dс и прерывает майнинг. Нажмите ещё раз для запуска.",
|
||||
"grpc_bootstrap_failed": "Ошибка начальной загрузки",
|
||||
"grpc_bootstrap_not_initialized": "Начальная загрузка не инициализирована",
|
||||
"grpc_hashrate_fee": "%s комиссия %s%%",
|
||||
"grpc_key_not_available": "Ключ недоступен для этого адреса",
|
||||
"grpc_na": "Н/Д",
|
||||
"height": "Высота",
|
||||
"help": "Справка",
|
||||
"hidden_tag": " (скрыт)",
|
||||
@@ -646,6 +1009,7 @@
|
||||
"light": "Светлая",
|
||||
"lite_account_label": "Аккаунт",
|
||||
"lite_action": "Действие",
|
||||
"lite_backend_unavailable": "Бэкенд облегчённого кошелька недоступен",
|
||||
"lite_backup_keys": "Резервная копия и ключи",
|
||||
"lite_birthday_backup": "Дата рождения: %llu (сохраните её тоже)",
|
||||
"lite_birthday_hint": "Высота блока, с которой начинать сканирование. Оставьте 0, если неизвестно (медленное полное сканирование).",
|
||||
@@ -653,9 +1017,12 @@
|
||||
"lite_console_backend_commands": "Команды бэкенда:",
|
||||
"lite_console_help_passthrough": "Любой другой ввод выполняется как команда консоли лайт-кошелька.",
|
||||
"lite_copy": "Копировать",
|
||||
"lite_could_not_start": "Не удалось запустить операцию",
|
||||
"lite_could_not_write": "Не удалось записать ",
|
||||
"lite_encrypt_wallet": "Зашифровать кошелёк",
|
||||
"lite_encryption_removed": "Шифрование удалено",
|
||||
"lite_enter_all_seed_words": "Введите все 24 слова seed-фразы для восстановления (введено %d)",
|
||||
"lite_enter_wallet_path": "Введите путь к кошельку",
|
||||
"lite_hide_wipe": "Скрыть и стереть",
|
||||
"lite_import": "Импорт",
|
||||
"lite_import_key_label": "Импортировать ключ",
|
||||
@@ -792,9 +1159,27 @@
|
||||
"memo_optional": "ЗАМЕТКА (НЕОБЯЗАТЕЛЬНО)",
|
||||
"memo_upper": "ЗАМЕТКА",
|
||||
"memo_z_only": "Примечание: Заметки доступны только при отправке на экранированные (z) адреса",
|
||||
"merge_addr_created": "Защищённый адрес создан.",
|
||||
"merge_advanced": "Дополнительно",
|
||||
"merge_back": "Назад",
|
||||
"merge_batch_fmt": "За один запуск объединяется до %d входов — повторите, чтобы завершить остальные.",
|
||||
"merge_confirm_btn": "Подтвердить",
|
||||
"merge_confirm_fmt": "Объединить ~%s DRGX из %d входов в %s?",
|
||||
"merge_create_zaddr": "Создать защищённый адрес",
|
||||
"merge_creating": "Создание адреса…",
|
||||
"merge_description": "Объедините несколько UTXO в один экранированный адрес. Это может уменьшить размер кошелька и улучшить конфиденциальность.",
|
||||
"merge_fee_hint": "Сетевая комиссия за эту транзакцию.",
|
||||
"merge_funds": "Объединить средства",
|
||||
"merge_max_inputs": "Макс. входов за партию",
|
||||
"merge_no_spendable": "Пока нет доступных для расходования входов для объединения.",
|
||||
"merge_progress": "Объединение… это может занять несколько минут. Вы можете закрыть это окно.",
|
||||
"merge_scope_fmt": "%d прозрачных + %d защищённых входов · ~%s DRGX доступно",
|
||||
"merge_scope_loading": "Проверка ваших входов…",
|
||||
"merge_send_failed": "Ошибка объединения: ",
|
||||
"merge_source": "Объединить",
|
||||
"merge_src_both": "Оба типа",
|
||||
"merge_src_shielded": "Защищённые",
|
||||
"merge_src_transparent": "Прозрачные",
|
||||
"merge_started": "Операция объединения начата",
|
||||
"merge_title": "Объединить на адрес",
|
||||
"mig_adopt": "Сделать это моим кошельком",
|
||||
@@ -875,6 +1260,7 @@
|
||||
"mining_difficulty_copied": "Сложность скопирована",
|
||||
"mining_est_block": "Расч. блок",
|
||||
"mining_est_daily": "Расч. за день",
|
||||
"mining_est_daily_pool_sub": "примерный соло-эквивалент, до комиссии пула",
|
||||
"mining_filter_all": "Все",
|
||||
"mining_filter_tip_all": "Показать все доходы",
|
||||
"mining_filter_tip_pool": "Показать только доходы пула",
|
||||
@@ -903,10 +1289,12 @@
|
||||
"mining_open_in_explorer": "Открыть в обозревателе",
|
||||
"mining_payout_address": "Адрес выплат",
|
||||
"mining_payout_foreign": "⚠ Этот адрес выплат отсутствует в вашем текущем кошельке — намайненные вознаграждения будут отправлены в другой кошелёк. Обновите его, если вы сменили кошелёк.",
|
||||
"mining_payout_invalid": "Недействительный адрес DragonX — исправьте перед запуском, иначе награды за майнинг будут потеряны.",
|
||||
"mining_payout_tooltip": "Адрес для получения вознаграждений за майнинг",
|
||||
"mining_pool": "Пул",
|
||||
"mining_pool_fee": "Комиссия",
|
||||
"mining_pool_hashrate": "Хешрейт пула",
|
||||
"mining_pool_needs_payout_tooltip": "Сначала введите адрес для выплат (создайте Z-адрес)",
|
||||
"mining_pool_url": "URL пула",
|
||||
"mining_pools_header": "ПУЛЫ",
|
||||
"mining_recent_blocks": "ПОСЛЕДНИЕ БЛОКИ",
|
||||
@@ -936,6 +1324,9 @@
|
||||
"mining_syncing_tooltip": "Блокчейн синхронизируется...",
|
||||
"mining_tag": " · Майнинг",
|
||||
"mining_threads": "Потоки майнинга",
|
||||
"mining_threads_input_tooltip": "Введите точное число потоков (Enter для применения)",
|
||||
"mining_threads_minus_tooltip": "Меньше потоков",
|
||||
"mining_threads_plus_tooltip": "Больше потоков",
|
||||
"mining_to_save": "для сохранения",
|
||||
"mining_today": "Сегодня",
|
||||
"mining_uptime": "Время работы",
|
||||
@@ -1028,6 +1419,13 @@
|
||||
"peers_upper": "УЗЛЫ",
|
||||
"peers_version": "Версия",
|
||||
"pending": "Ожидание",
|
||||
"picker_dat_count": "Файлов кошелька в этой папке: %d",
|
||||
"picker_dat_none": "В этой папке нет файлов кошелька",
|
||||
"picker_empty": "В этой папке нет подпапок или файлов кошелька.",
|
||||
"picker_home": "Домашняя папка",
|
||||
"picker_select": "Сканировать эту папку",
|
||||
"picker_title": "Выберите папку для сканирования",
|
||||
"picker_up": "На уровень вверх",
|
||||
"pin_change_desc": "Измените PIN-код разблокировки. Вам понадобится текущий PIN-код и новый PIN-код.",
|
||||
"pin_confirm_new_label": "Подтвердите новый PIN-код:",
|
||||
"pin_current_label": "Текущий PIN-код:",
|
||||
@@ -1105,6 +1503,8 @@
|
||||
"qr_failed": "Не удалось сгенерировать QR-код",
|
||||
"qr_title": "QR-код",
|
||||
"qr_unavailable": "QR недоступен",
|
||||
"quick_receive": "Быстрый приём",
|
||||
"quick_send": "Быстрая отправка",
|
||||
"ram_daemon_gb": "Демон: %.1f ГБ (%s)",
|
||||
"ram_daemon_mb": "Демон: %.0f МБ (%s)",
|
||||
"ram_system_gb": "Система: %.1f / %.0f ГБ",
|
||||
@@ -1154,6 +1554,7 @@
|
||||
"rpc_connection": "RPC-подключение...",
|
||||
"rpc_host": "RPC-хост",
|
||||
"rpc_pass": "Пароль",
|
||||
"rpc_plaintext_remote_warning": "Удалённый RPC использует незашифрованный HTTP. Добавьте rpctls=1 в DRAGONX.conf, если ваш демон поддерживает TLS.",
|
||||
"rpc_port": "Порт",
|
||||
"rpc_user": "Имя пользователя",
|
||||
"save": "Сохранить",
|
||||
@@ -1161,6 +1562,7 @@
|
||||
"save_z_transactions": "Сохранять Z-tx в списке транзакций",
|
||||
"sb_auth_failed": "Ошибка авторизации — проверьте rpcuser/rpcpassword",
|
||||
"sb_block": "Блок: %d",
|
||||
"sb_block_db_unreadable": "База данных блоков нечитаема — требуется пересборка",
|
||||
"sb_building_witnesses": "Установка свидетелей",
|
||||
"sb_building_witnesses_pct": "Перестроение свидетелей %.0f%%",
|
||||
"sb_connecting_daemon": "Подключение к dragonxd...",
|
||||
@@ -1176,6 +1578,7 @@
|
||||
"sb_dragonxd_stopped": "dragonxd остановлен",
|
||||
"sb_dragonxd_stopping": "Остановка dragonxd...",
|
||||
"sb_extracting_sapling": "Извлечение параметров Sapling...",
|
||||
"sb_finishing_repair": "Завершение восстановления кошелька — повторное сканирование…",
|
||||
"sb_importing_keys": "Импорт ключей",
|
||||
"sb_loading_config": "Загрузка конфигурации...",
|
||||
"sb_mining_hs": "%.1f H/s",
|
||||
@@ -1197,14 +1600,55 @@
|
||||
"sb_waiting_config": "Ожидание конфигурации демона...",
|
||||
"sb_waiting_daemon": "Ожидание dragonxd...",
|
||||
"sb_waiting_daemon_err": "Ожидание dragonxd — %s",
|
||||
"sb_wallet_needs_recovery": "Доступно восстановление кошелька",
|
||||
"sb_warming_up": "Прогрев...",
|
||||
"sb_witness_cache": "Перестроение свидетелей",
|
||||
"scale_effects": "МАСШТАБ И ЭФФЕКТЫ",
|
||||
"screenshot_open_dir": "Открыть расположение",
|
||||
"screenshot_sweep": "Запустить прогон скриншотов",
|
||||
"screenshot_sweep_desc": "Перебирает каждую тему по всем вкладкам и сохраняет скриншот каждой в подпапки по вкладкам внутри папки screenshots в каталоге конфигурации (перезаписывая предыдущий проход). Выполняется несколько секунд.",
|
||||
"screenshot_sweep_full": "Полный обход интерфейса",
|
||||
"search_icons": "Поиск значков...",
|
||||
"search_placeholder": "Поиск...",
|
||||
"sec_changing_passphrase": "Смена парольной фразы...",
|
||||
"sec_changing_pin": "Смена PIN...",
|
||||
"sec_couldnt_lock_wallet": "Не удалось заблокировать кошелёк — он всё ещё разблокирован. Проверьте соединение с демоном.",
|
||||
"sec_encrypted_backup_suffix": "\nЗашифрованная резервная копия: wallet.dat.encrypted.bak",
|
||||
"sec_encrypting_wallet": "Шифрование кошелька...",
|
||||
"sec_encryption_did_not_complete": "Шифрование кошелька не завершено — ваш кошелёк НЕ зашифрован. Откройте настройки, чтобы завершить шифрование.",
|
||||
"sec_encryption_failed_prefix": "Ошибка шифрования: ",
|
||||
"sec_failed_prefix": "Ошибка: ",
|
||||
"sec_failed_to_create_vault": "Не удалось создать хранилище",
|
||||
"sec_importing_keys_rescanning": "Импорт ключей и повторное сканирование блокчейна — кошелёк доступен во время выполнения",
|
||||
"sec_incorrect_current_pin": "Неверный текущий PIN",
|
||||
"sec_incorrect_passphrase_decrypt": "Неверная парольная фраза",
|
||||
"sec_incorrect_passphrase_pin_setup": "Неверная парольная фраза",
|
||||
"sec_incorrect_pin_remove": "Неверный PIN",
|
||||
"sec_internal_error_change_pin": "Внутренняя ошибка",
|
||||
"sec_internal_error_remove_pin": "Внутренняя ошибка",
|
||||
"sec_mode_passphrase": " Парольная фраза",
|
||||
"sec_not_connected_to_daemon": "Нет соединения с демоном",
|
||||
"sec_not_connected_to_daemon_pin": "Нет соединения с демоном",
|
||||
"sec_passphrase_changed_successfully": "Парольная фраза успешно изменена",
|
||||
"sec_pin_changed_successfully": "PIN успешно изменён",
|
||||
"sec_pin_removed": "PIN удалён",
|
||||
"sec_pin_set_successfully": "PIN успешно задан",
|
||||
"sec_restart_daemon_for_encryption": "Перезапустите демон, чтобы шифрование вступило в силу.",
|
||||
"sec_too_many_attempts_wait": "Слишком много попыток. Подождите %.0f сек...",
|
||||
"sec_total_elapsed_fmt": "Всего прошло: %dм %02dс",
|
||||
"sec_unlock_button": "Разблокировать",
|
||||
"sec_unlock_failed_prefix": "Ошибка разблокировки: ",
|
||||
"sec_unlocking_fmt": "Разблокировка%s",
|
||||
"sec_use_passphrase_instead": "Использовать парольную фразу",
|
||||
"sec_use_pin_instead": "Использовать PIN",
|
||||
"sec_verifying_passphrase": "Проверка парольной фразы...",
|
||||
"sec_verifying_pin": "Проверка PIN...",
|
||||
"sec_wallet_decrypted_all_keys_imported": "Кошелёк успешно расшифрован! Все ключи импортированы.",
|
||||
"sec_wallet_encrypted_and_pin_set": "Кошелёк зашифрован, PIN задан",
|
||||
"sec_wallet_encrypted_but_pin_vault_failed": "Кошелёк зашифрован, но не удалось создать хранилище PIN",
|
||||
"sec_wallet_encrypted_restarting_daemon": "Кошелёк зашифрован. Перезапуск демона...",
|
||||
"sec_wallet_encrypted_successfully": "Кошелёк успешно зашифрован",
|
||||
"sec_wallet_locked_title": "Кошелёк заблокирован",
|
||||
"security": "БЕЗОПАСНОСТЬ",
|
||||
"seed_backup_button": "Сид-фраза",
|
||||
"seed_backup_close": "Закрыть",
|
||||
@@ -1228,6 +1672,7 @@
|
||||
"send_amount": "Сумма",
|
||||
"send_amount_details": "ДЕТАЛИ СУММЫ",
|
||||
"send_amount_upper": "СУММА",
|
||||
"send_available_note": "доступно",
|
||||
"send_cannot_send_now": "Сейчас отправить нельзя — проверьте соединение, синхронизацию и доступный баланс.",
|
||||
"send_clear_fields": "Очистить все поля формы?",
|
||||
"send_contacts_button": "Выбрать из контактов",
|
||||
@@ -1263,6 +1708,7 @@
|
||||
"send_tooltip_not_connected": "Не подключено к daemon",
|
||||
"send_tooltip_select_source": "Сначала выберите адрес-источник",
|
||||
"send_tooltip_syncing": "Дождитесь синхронизации блокчейна",
|
||||
"send_tooltip_view_only": "Адрес только для просмотра — нет ключа расходования, отправка невозможна",
|
||||
"send_total": "Итого",
|
||||
"send_transaction": "Отправить транзакцию",
|
||||
"send_tx_failed": "Транзакция не удалась",
|
||||
@@ -1282,16 +1728,16 @@
|
||||
"sent_filter": "Отправлено",
|
||||
"sent_type": "Отправлено",
|
||||
"sent_upper": "ОТПРАВЛЕНО",
|
||||
"set_label": "Установить метку...",
|
||||
"set_label": "Установить метку",
|
||||
"settings": "Настройки",
|
||||
"settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.",
|
||||
"settings_acrylic_level": "Уровень акрила:",
|
||||
"settings_address_book": "Адресная книга...",
|
||||
"settings_address_book": "Адресная книга…",
|
||||
"settings_auto_detected": "Автоопределено из DRAGONX.conf",
|
||||
"settings_auto_lock": "АВТОБЛОКИРОВКА",
|
||||
"settings_auto_shield_desc": "Автоматически перемещать прозрачные средства на экранированные адреса",
|
||||
"settings_auto_shield_funds": "Автоматически экранировать прозрачные средства",
|
||||
"settings_backup": "Резервная копия...",
|
||||
"settings_backup": "Резервная копия…",
|
||||
"settings_block_explorer_urls": "URL-адреса обозревателя блоков",
|
||||
"settings_builtin": "Встроенные",
|
||||
"settings_change_passphrase": "Сменить пароль",
|
||||
@@ -1305,7 +1751,7 @@
|
||||
"settings_copy_diagnostics": "Копировать диагностику",
|
||||
"settings_copyright": "Copyright 2024-2026 Разработчики DragonX | Лицензия GPLv3",
|
||||
"settings_custom": "Пользовательские",
|
||||
"settings_data_dir": "Каталог данных:",
|
||||
"settings_data_dir": "Каталог данных",
|
||||
"settings_debug_changed": "Категории отладки изменены — перезапустите демон для применения",
|
||||
"settings_debug_restart_note": "Изменения вступают в силу после перезапуска демона.",
|
||||
"settings_debug_select": "Выберите категории для включения журнала отладки демона (флаги -debug=).",
|
||||
@@ -1313,19 +1759,20 @@
|
||||
"settings_encrypt_first_pin": "Сначала зашифруйте кошелёк, чтобы включить PIN",
|
||||
"settings_encrypt_wallet": "Зашифровать кошелёк",
|
||||
"settings_explorer_hint": "URL-адреса должны заканчиваться косой чертой. Txid/адрес будет добавлен.",
|
||||
"settings_export_all": "Экспортировать все...",
|
||||
"settings_export_csv": "Экспорт CSV...",
|
||||
"settings_export_key": "Экспортировать ключ...",
|
||||
"settings_export_all": "Экспортировать все…",
|
||||
"settings_export_csv": "Экспорт CSV…",
|
||||
"settings_export_key": "Экспортировать ключ…",
|
||||
"settings_gradient_bg": "Градиент фона",
|
||||
"settings_gradient_desc": "Заменить текстурные фоны плавными градиентами",
|
||||
"settings_idle_after": "через",
|
||||
"settings_import_key": "Импорт приватного ключа...",
|
||||
"settings_import_viewkey": "Импортировать ключ просмотра...",
|
||||
"settings_import_key": "Импорт приватного ключа…",
|
||||
"settings_import_viewkey": "Импортировать ключ просмотра…",
|
||||
"settings_language_note": "Примечание: Некоторый текст требует перезапуска для обновления",
|
||||
"settings_lock_now": "Заблокировать сейчас",
|
||||
"settings_locked": "Заблокирован",
|
||||
"settings_merge_to_address": "Объединить на адрес...",
|
||||
"settings_merge_to_address": "Объединить на адрес…",
|
||||
"settings_noise_opacity": "Непрозрачность шума:",
|
||||
"settings_not_connected": "Нет соединения с демоном",
|
||||
"settings_not_encrypted": "Не зашифрован",
|
||||
"settings_not_found": "Не найден",
|
||||
"settings_open_app_dir": "Открыть папку приложения",
|
||||
@@ -1336,29 +1783,36 @@
|
||||
"settings_privacy": "Конфиденциальность",
|
||||
"settings_quick_unlock_pin": "Быстрый PIN-код разблокировки",
|
||||
"settings_reduce_transparency": "Уменьшить прозрачность",
|
||||
"settings_reloaded": "Настройки перезагружены с диска",
|
||||
"settings_remove_encryption": "Удалить шифрование",
|
||||
"settings_remove_pin": "Удалить PIN",
|
||||
"settings_request_payment": "Запросить платёж...",
|
||||
"settings_request_payment": "Запросить платёж…",
|
||||
"settings_rescan_desc": "Пересканировать блокчейн для поиска пропущенных транзакций",
|
||||
"settings_restart_daemon": "Перезапустить демон",
|
||||
"settings_rpc_connection": "RPC-соединение",
|
||||
"settings_rpc_error_prefix": "Ошибка RPC: ",
|
||||
"settings_rpc_note": "Примечание: Настройки подключения обычно определяются автоматически из DRAGONX.conf",
|
||||
"settings_rpc_ok": "RPC-соединение в порядке",
|
||||
"settings_save_shielded_desc": "Сохраняет z-addr транзакции в локальном файле для просмотра",
|
||||
"settings_save_shielded_local": "Сохранять историю защищённых транзакций локально",
|
||||
"settings_saved": "Настройки сохранены",
|
||||
"settings_set_pin": "Установить PIN",
|
||||
"settings_shield_mining": "Экранировать майнинг...",
|
||||
"settings_shield_mining": "Экранировать майнинг…",
|
||||
"settings_solid_colors_desc": "Использовать сплошные цвета вместо эффектов размытия (доступность)",
|
||||
"settings_theme_refreshed": "Список тем обновлён",
|
||||
"settings_tor_desc": "Маршрутизировать все соединения через Tor для повышения конфиденциальности",
|
||||
"settings_unlocked": "Разблокирован",
|
||||
"settings_use_tor_network": "Использовать Tor для сетевых подключений",
|
||||
"settings_validate_address": "Проверить адрес...",
|
||||
"settings_validate_address": "Проверить адрес…",
|
||||
"settings_visual_effects": "Визуальные эффекты",
|
||||
"settings_wallet_file_size": "Размер файла кошелька: %s",
|
||||
"settings_wallet_info": "Информация о кошельке",
|
||||
"settings_wallet_location": "Расположение кошелька: %s",
|
||||
"settings_wallet_maintenance": "Обслуживание кошелька",
|
||||
"settings_wallet_not_found": "Файл кошелька не найден",
|
||||
"settings_wallet_size_label": "Размер кошелька:",
|
||||
"settings_wallet_size_label": "Размер кошелька",
|
||||
"settings_ztx_cleared": "История Z-транзакций очищена",
|
||||
"settings_ztx_not_found": "Файл истории не найден",
|
||||
"setup_wizard": "Мастер настройки",
|
||||
"share": "Поделиться",
|
||||
"shield_check_status": "Проверить статус",
|
||||
@@ -1406,17 +1860,34 @@
|
||||
"status": "Статус",
|
||||
"stop_external": "Остановить внешний daemon",
|
||||
"stop_mining": "Остановить майнинг",
|
||||
"stratum_allowip_hint": "Разрешить майнерам подключаться с IP или CIDR (пусто = только этот компьютер)",
|
||||
"stratum_expose_warn": "Открывает порт для майнинга в разрешённую вами сеть — используйте только в доверенной локальной сети.",
|
||||
"stratum_host": "Разместить майнинг-пул (stratum)",
|
||||
"stratum_host_hint": "Майнеры подключаются к этому компьютеру на порт 22769 (RPC-порт + 1000) с помощью stratum-майнера RandomX. Для применения перезапустите демон.",
|
||||
"stratum_host_section": "РАЗМЕЩЕНИЕ МАЙНИНГ-ПУЛА",
|
||||
"submitting_transaction": "Отправка транзакции...",
|
||||
"success": "Успешно",
|
||||
"summary": "Итоги",
|
||||
"sweep_button": "Перевести",
|
||||
"sweep_caveat": "Импортирует ключ, чтобы подписать одну транзакцию, переводящую все его средства на ваш адрес. Ключ остаётся в кошельке с нулевым балансом.",
|
||||
"sweep_current_theme_only": "Только текущая тема",
|
||||
"sweep_dest_label": "Отправить переведённые средства на",
|
||||
"sweep_dest_new": "Новый экранированный адрес (рекомендуется)",
|
||||
"sweep_done": "Готово — средства переведены на ваш адрес.",
|
||||
"sweep_to": "Переведено на:",
|
||||
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
|
||||
"sweep_tx": "Транзакция:",
|
||||
"swin_connection_failed": "Ошибка соединения: ",
|
||||
"swin_connection_successful": "Соединение установлено!\nВерсия dragonxd: ",
|
||||
"swin_invalid_suffix": " (неверный)",
|
||||
"swin_no_history_file_found": "Файл истории не найден",
|
||||
"swin_rescan_failed": "Ошибка повторного сканирования: ",
|
||||
"swin_rescan_started_from_block": "Повторное сканирование начато с блока ",
|
||||
"swin_rescan_to": " до ",
|
||||
"swin_rpc_client_not_initialized": "RPC-клиент не инициализирован",
|
||||
"swin_settings_saved": "Настройки сохранены",
|
||||
"swin_theme_list_refreshed": "Список тем обновлён",
|
||||
"swin_ztx_history_cleared": "История Z-транзакций очищена",
|
||||
"switch_corrupt_body": "Похоже, этот кошелёк повреждён — узел не смог его открыть. Восстановите из резервной копии, создайте заново или попробуйте восстановить.",
|
||||
"switch_corrupt_repair": "Попробовать восстановить (salvage)",
|
||||
"switch_progress_background": "Продолжить в фоне",
|
||||
@@ -1441,6 +1912,7 @@
|
||||
"theme": "Тема",
|
||||
"theme_effects": "Эффекты темы",
|
||||
"theme_language": "ТЕМА И ЯЗЫК",
|
||||
"tile_click_to_open": "Нажмите, чтобы открыть",
|
||||
"time_days_ago": "%d дней назад",
|
||||
"time_hours_ago": "%d часов назад",
|
||||
"time_minutes_ago": "%d минут назад",
|
||||
@@ -1455,7 +1927,9 @@
|
||||
"to_upper": "КОМУ",
|
||||
"tools": "УТИЛИТЫ",
|
||||
"tools_actions": "Инструменты и действия...",
|
||||
"tools_actions_hdr": "ИНСТРУМЕНТЫ И ДЕЙСТВИЯ",
|
||||
"total": "Итого",
|
||||
"total_balance_label": "Общий баланс",
|
||||
"transaction_id": "ID ТРАНЗАКЦИИ",
|
||||
"transaction_sent": "Транзакция успешно отправлена",
|
||||
"transaction_sent_msg": "Транзакция отправлена!",
|
||||
@@ -1477,7 +1951,7 @@
|
||||
"tt_auto_shield": "Автоматически перемещать прозрачный баланс на экранированные адреса для конфиденциальности",
|
||||
"tt_backup": "Создать резервную копию вашего wallet.dat",
|
||||
"tt_block_explorer": "Открыть обозреватель блоков DragonX в браузере",
|
||||
"tt_blur": "Степень размытия (0%% = выкл., 100%% = максимум)",
|
||||
"tt_blur": "Степень размытия (0% = выкл., 100% = максимум)",
|
||||
"tt_change_pass": "Сменить пароль шифрования кошелька",
|
||||
"tt_change_pin": "Изменить PIN-код разблокировки",
|
||||
"tt_chat_bubble_accent": "Акцентный цвет для ваших исходящих пузырьков сообщений (или следовать текущей теме)",
|
||||
@@ -1540,7 +2014,7 @@
|
||||
"tt_low_spec": "Отключить все тяжёлые визуальные эффекты\\nГорячая клавиша: Ctrl+Shift+Down",
|
||||
"tt_merge": "Объединить несколько UTXO в один адрес",
|
||||
"tt_mine_idle": "Автоматически начать майнинг при\\nпростое системы (нет ввода с клавиатуры/мыши)",
|
||||
"tt_noise": "Интенсивность зернистой текстуры (0%% = выкл., 100%% = максимум)",
|
||||
"tt_noise": "Интенсивность зернистой текстуры (0% = выкл., 100% = максимум)",
|
||||
"tt_open_app_dir": "Открыть папку ObsidianDragon (настройки, темы, логи) в файловом менеджере",
|
||||
"tt_open_data_dir": "Открыть в файловом менеджере папку с данными кошелька и блокчейна",
|
||||
"tt_open_dir": "Нажмите, чтобы открыть в проводнике",
|
||||
@@ -1574,14 +2048,17 @@
|
||||
"tt_simple_bg": "Использовать простой градиент для фона\\nГорячая клавиша: Ctrl+Up",
|
||||
"tt_simple_bg_alt": "Использовать градиентную версию фонового изображения темы\\nГорячая клавиша: Ctrl+Up",
|
||||
"tt_stop_external": "Применяется при подключении к демону,\\nзапущенному вне этого кошелька",
|
||||
"tt_stratum_host": "Запускает на этом узле сервер stratum-пула RandomX, чтобы другие майнеры RandomX могли подключаться к этому компьютеру. Требуется узел версии v1.3.0+ и перезапуск демона для применения.",
|
||||
"tt_sweep_current_theme_only": "Применить только к активной теме вместо перебора всех тем",
|
||||
"tt_test_conn": "Проверить RPC-подключение к демону",
|
||||
"tt_theme_effects": "Мерцание, свечение, циклическая смена оттенка по теме",
|
||||
"tt_theme_hotkey": "Горячая клавиша: Ctrl+Влево/Вправо для переключения тем",
|
||||
"tt_tor": "Маршрутизировать подключения демона через сеть Tor для анонимности",
|
||||
"tt_tx_url": "Базовый URL для просмотра транзакций в обозревателе блоков",
|
||||
"tt_ui_opacity": "Непрозрачность карточек и боковой панели (100%% = полностью непрозрачно, ниже = прозрачнее)",
|
||||
"tt_ui_opacity": "Непрозрачность карточек и боковой панели (100% = полностью непрозрачно, ниже = прозрачнее)",
|
||||
"tt_validate": "Проверить, действителен ли адрес DragonX",
|
||||
"tt_verbose": "Записывать подробную диагностику подключений,\\nсостояние демона и информацию о владельце порта\\nна вкладке Консоль",
|
||||
"tt_wallet_size_warn": "Защищённые кошельки растут за счёт данных witness каждой записи — объединение множества записей в один адрес уменьшает его размер. Сначала сделайте резервную копию.",
|
||||
"tt_wallets_button": "Показать файлы кошельков и переключаться между ними",
|
||||
"tt_website": "Открыть сайт DragonX",
|
||||
"tt_window_opacity": "Непрозрачность фона (ниже = рабочий стол виден сквозь окно)",
|
||||
@@ -1632,6 +2109,7 @@
|
||||
"validate_not_mine": "Не принадлежит этому кошельку",
|
||||
"validate_ownership": "Принадлежность:",
|
||||
"validate_results": "Результаты:",
|
||||
"validate_results_placeholder": "Результаты появятся здесь",
|
||||
"validate_shielded_type": "Экранированный (z-адрес)",
|
||||
"validate_status": "Статус:",
|
||||
"validate_title": "Проверить адрес",
|
||||
@@ -1647,8 +2125,68 @@
|
||||
"view_on_explorer": "Посмотреть в обозревателе",
|
||||
"waiting_for_daemon": "Ожидание подключения к daemon...",
|
||||
"wallet": "КОШЕЛЁК",
|
||||
"wallet_degraded_notify": "Ваш кошелёк открыт в режиме с ограниченной функциональностью: существующие средства в безопасности и доступны для расходования, но создание новых адресов и защита средств отключены. Сделайте резервную копию сид-фразы и восстановите из неё кошелёк, чтобы полностью его исправить.",
|
||||
"wallet_diagnostics_hdr": "ДИАГНОСТИКА",
|
||||
"wallet_empty": "Ваш кошелёк пуст",
|
||||
"wallet_empty_hint": "Перейдите к получению, чтобы получить свой адрес и начать получать средства.",
|
||||
"wallet_options_hdr": "ПАРАМЕТРЫ",
|
||||
"wallet_rebuild_failed": "Пересборка не дала исправный кошелёк, поэтому ничего не изменилось. Ваш кошелёк не затронут.",
|
||||
"wallet_rebuild_install_failed": "Не удалось установить пересобранный кошелёк; текущий кошелёк оставлен без изменений.",
|
||||
"wallet_rebuild_no_helper": "Помощник пересборки кошелька недоступен в этой сборке. Используйте Восстановление или пересоберите вручную.",
|
||||
"wallet_rebuild_no_source": "Не удалось найти читаемый кошелёк для пересборки. Ничего не изменилось.",
|
||||
"wallet_rebuild_ok": "База данных кошелька пересобрана — узел загружает её и повторно сканирует для определения вашего баланса.",
|
||||
"wallet_rebuild_started": "Восстановление файла кошелька — пожалуйста, не закрывайте это окно.",
|
||||
"wallet_recovered_body": "Ничего не удалено. Ваш исходный кошелёк по-прежнему сохранён на компьютере как резервная копия с датой, а каждый вариант ниже только копирует или переименовывает файлы — и никогда не стирает их. Ваши ключи никогда не создаются заново, только перечитываются.\n\nВ восстановленной копии, загруженной сейчас, может не хватать нескольких последних транзакций, поэтому баланс может выглядеть немного заниженным, пока вы не завершите процесс ниже и не пройдёт повторное сканирование.",
|
||||
"wallet_recovered_dismiss": "Не сейчас — оставить восстановленную копию",
|
||||
"wallet_recovered_dismiss_sub": "Файлы не изменяются. Вы можете открыть это снова в любой момент из строки состояния; ваш оригинал в любом случае остаётся в безопасной резервной копии.",
|
||||
"wallet_recovered_notify": "Файл вашего кошелька потребовал восстановления — оригинал был безопасно сохранён в резервной копии. Откройте приложение, чтобы рассмотреть варианты.",
|
||||
"wallet_recovered_open_folder": "Показать мне файлы",
|
||||
"wallet_recovered_open_folder_sub": "Открывает папку данных кошелька, чтобы вы могли сами просмотреть файлы резервных копий — ничего не изменяется.",
|
||||
"wallet_recovered_rebuild": "Восстановить автоматически (рекомендуется)",
|
||||
"wallet_recovered_rebuild_sub": "Перечитывает каждую восстанавливаемую запись из вашего самого полного файла кошелька и записывает чистый новый файл, затем перезапускается. Самый тщательный вариант — ваш текущий файл в любом случае сохраняется как резервная копия с датой.",
|
||||
"wallet_recovered_restore": "Восстановить исходный файл вместо этого",
|
||||
"wallet_recovered_restore_sub": "Возвращает на место вашу самую большую нетронутую резервную копию, дословно, затем выполняет повторное сканирование — немного быстрее, но настолько полно, насколько был полон этот файл. Ваш текущий файл в любом случае сохраняется как резервная копия с датой.",
|
||||
"wallet_recovered_safety": "Ваши монеты в безопасности.",
|
||||
"wallet_recovered_title": "Файл вашего кошелька требует быстрого восстановления",
|
||||
"wallet_recovered_warn": "При запуске приложение обнаружило, что файл вашего кошелька не прошёл проверку целостности — обычно это происходит после обновления приложения или некорректного завершения работы. Приложение уже защитило ваши данные: оно отложило старый файл и загрузило восстановленную копию, чтобы вы не остались без доступа.",
|
||||
"wallet_recovery_decide_later": "Решить позже",
|
||||
"wallet_recovery_decide_later_tip": "Закрывает это окно и оставляет загруженной текущую копию. Ничего не меняется, и вы можете выполнить восстановление в любое время — в строке состояния остаётся ссылка «Доступно восстановление кошелька».",
|
||||
"wallet_recovery_details_label": "Показать технические подробности",
|
||||
"wallet_recovery_done": "Готово",
|
||||
"wallet_recovery_failure_body": "Восстановление было выполнено, но его результат не прошёл проверку (файл не удалось прочитать или в нём не оказалось адресов), поэтому он был автоматически отклонён, прежде чем что-либо заменить. Ваш кошелёк остался точно таким же, как до нажатия — на диске ничего не изменилось.",
|
||||
"wallet_recovery_failure_title": "Восстановление не удалось",
|
||||
"wallet_recovery_files_detail": "Ваш исходный кошелёк по-прежнему здесь — приложение переименовало его в резервную копию с датой (wallet.<числа>.bak) в папке данных и ничего не удалило. «Восстановить» считывает каждую запись из вашего самого полного кошелька в совершенно новый чистый файл. «Вернуть» копирует вашу самую большую резервную копию обратно точно в том виде, в каком она есть. Копия, загруженная прямо сейчас, тоже сохраняется как резервная копия.",
|
||||
"wallet_recovery_files_label": "Что происходит с моими файлами?",
|
||||
"wallet_recovery_notnow_short": "Не сейчас",
|
||||
"wallet_recovery_other_options": "Другие варианты",
|
||||
"wallet_recovery_rebuild_card": "Восстановить автоматически",
|
||||
"wallet_recovery_rebuild_card_desc": "Считывает каждую восстанавливаемую запись в чистый файл, затем перезапускается. Самый тщательный вариант.",
|
||||
"wallet_recovery_recommended": "РЕКОМЕНДУЕТСЯ",
|
||||
"wallet_recovery_repair_go": "Восстановить",
|
||||
"wallet_recovery_rescan_body": "Выполняется повторное сканирование блокчейна для восстановления вашего баланса и истории транзакций. Это может занять несколько минут — вам не нужно ничего делать, и, пожалуйста, не перезапускайте узел, пока идёт процесс.",
|
||||
"wallet_recovery_rescan_elapsed": "Выполняется уже %s",
|
||||
"wallet_recovery_rescan_size": "Восстановленный кошелёк сейчас занимает %s и всё ещё заполняется",
|
||||
"wallet_recovery_rescan_slow": "Это занимает больше времени, чем обычно, что нормально для крупного кошелька. Оставить процесс работать безопасно — вы можете наблюдать за прогрессом в разделе Дополнительно ▸ Консоль.",
|
||||
"wallet_recovery_rescan_title": "Завершение восстановления кошелька",
|
||||
"wallet_recovery_restore_card": "Восстановить оригинал",
|
||||
"wallet_recovery_restore_card_desc": "Возвращает на место вашу самую большую нетронутую резервную копию, дословно — настолько полную, насколько был полон этот файл.",
|
||||
"wallet_recovery_restore_go": "Вернуть",
|
||||
"wallet_recovery_success_body": "Приложение загрузило ваш восстановленный кошелёк и выполняет повторное сканирование для подсчёта баланса — это может занять несколько минут. Оно считывает каждую запись, какую может, но в редких случаях повреждённых файлов может восстановить чуть меньше — или иногда чуть больше — адресов, чем было раньше. По завершении повторного сканирования проверьте, что баланс и история выглядят правильно.",
|
||||
"wallet_recovery_success_restore": "Ваша самая большая резервная копия возвращена на место и сейчас загружается, приложение выполняет повторное сканирование. Она восстановлена дословно, поэтому настолько полна, насколько был полон этот файл — проверьте баланс по завершении повторного сканирования.",
|
||||
"wallet_recovery_try_other": "Попробовать другой вариант",
|
||||
"wallet_recovery_whats_happened": "Что произошло?",
|
||||
"wallet_recovery_working_label": "Выполняется",
|
||||
"wallet_restore_bad_backup": "Файл резервной копии кошелька выглядит нечитаемым, поэтому он НЕ был восстановлен — ваш текущий кошелёк не изменён. Восстановите из собственной резервной копии.",
|
||||
"wallet_restore_busy": "Узел занят перезапуском — повторите попытку через момент.",
|
||||
"wallet_restore_copy_failed": "Не удалось установить резервную копию кошелька; ваш текущий кошелёк оставлен без изменений.",
|
||||
"wallet_restore_move_failed": "Не удалось отложить текущий кошелёк — ничего не изменено.",
|
||||
"wallet_restore_no_backup": "Не удалось найти wallet.<timestamp>.bak для восстановления. Ничего не изменено.",
|
||||
"wallet_restore_no_restart": "Ваш исходный кошелёк восстановлен, но узел не перезапустился — запустите его из Настроек.",
|
||||
"wallet_restore_ok": "Исходный кошелёк восстановлен. Узел сейчас его загружает.",
|
||||
"wallet_restore_started": "Восстановление вашего исходного файла — пожалуйста, не закрывайте это окно.",
|
||||
"wallet_restore_stop_failed": "Узел не остановился вовремя, поэтому ничего не изменено. Попробуйте снова.",
|
||||
"wallet_size_consolidate": "Объединить заметки…",
|
||||
"wallet_size_warn": "Файл этого кошелька велик. Объединение ваших заметок может сдержать дальнейший рост.",
|
||||
"wallets_active": "Активен",
|
||||
"wallets_add": "Добавить папку",
|
||||
"wallets_add_folder": "Также сканировать другую папку на наличие файлов кошельков:",
|
||||
@@ -1691,6 +2229,7 @@
|
||||
"wallets_open_inplace_tt": "Открывает этот кошелёк на месте — по ссылке в каталоге данных (без копирования)",
|
||||
"wallets_remove_folder": "Больше не сканировать эту папку",
|
||||
"wallets_reveal": "Открыть папку",
|
||||
"wallets_scan_folder": "Сканировать другую папку на наличие кошельков…",
|
||||
"wallets_scanned_folders": "Просканированные папки:",
|
||||
"wallets_sort_addresses": "Адреса",
|
||||
"wallets_sort_asc": "По возрастанию (сначала старые / меньше / меньший)",
|
||||
@@ -1760,18 +2299,30 @@
|
||||
"wiz_welcome_sub": "Несколько быстрых настроек — и ваш полный узел готов.",
|
||||
"wiz_welcome_title": "Добро пожаловать в ObsidianDragon!",
|
||||
"wizard_daemon_start_failed": "Не удалось запустить демон — будет повторено автоматически",
|
||||
"xmrig_active_tt": "Текущий установленный (активный) майнер",
|
||||
"xmrig_browse_releases": "Просмотреть все релизы…",
|
||||
"xmrig_checking": "Проверка наличия последней версии майнера…",
|
||||
"xmrig_current": "Текущая:",
|
||||
"xmrig_downgrade_note": "Установка другой версии майнера заменяет текущую; сначала остановите майнинг, изменения вступят в силу при следующем запуске.",
|
||||
"xmrig_download_install": "Скачать и установить",
|
||||
"xmrig_downloading": "Загрузка…",
|
||||
"xmrig_install_this": "Установить эту версию",
|
||||
"xmrig_installed": "Установлено:",
|
||||
"xmrig_installed_ok": "Майнер установлен",
|
||||
"xmrig_installing": "Установка…",
|
||||
"xmrig_latest": "Последняя:",
|
||||
"xmrig_latest_badge": "последняя",
|
||||
"xmrig_latest_tt": "Новейшая доступная версия",
|
||||
"xmrig_loading_releases": "Загрузка релизов…",
|
||||
"xmrig_no_notes": "Для этой версии нет заметок о выпуске.",
|
||||
"xmrig_none": "нет",
|
||||
"xmrig_prerelease_tt": "Предварительная / тестовая сборка",
|
||||
"xmrig_reinstall": "Переустановить",
|
||||
"xmrig_released": "Выпущено:",
|
||||
"xmrig_releases": "релизы xmrig",
|
||||
"xmrig_status_current": "Эта версия установлена",
|
||||
"xmrig_status_latest": "Последняя версия",
|
||||
"xmrig_status_older": "Старее вашей установленной версии",
|
||||
"xmrig_stop_mining_first": "Остановите майнинг перед обновлением майнера.",
|
||||
"xmrig_unavailable_body": "Для этой платформы нет доступной сборки майнера.",
|
||||
"xmrig_unavailable_title": "Обновления майнера недоступны",
|
||||
|
||||
+568
-15
@@ -19,6 +19,7 @@
|
||||
"about_license_text": "本软件根据 GNU 通用公共许可证 v3 (GPLv3) 发布。您可以根据许可证条款自由使用、修改和分发本软件。",
|
||||
"about_peers_count": "%zu 个节点",
|
||||
"about_release": "发布版",
|
||||
"about_source": "源代码",
|
||||
"about_title": "关于 ObsidianDragon",
|
||||
"about_version": "版本:",
|
||||
"about_website": "网站",
|
||||
@@ -60,7 +61,84 @@
|
||||
"amount_label": "金额:",
|
||||
"animate_avatars": "动画头像",
|
||||
"appearance": "外观",
|
||||
"appx_back": "返回",
|
||||
"appx_back_up_seed_phrase_title": "备份你的助记词",
|
||||
"appx_birthday_block_height": "生日(区块高度):%llu — 也请一并备份。",
|
||||
"appx_blockchain_data_deleted": "区块链数据已删除(%d 项)。守护进程正在重启,以从网络重新同步。",
|
||||
"appx_blockchain_maintenance_in_progress": "已有区块链维护操作正在进行中。",
|
||||
"appx_blockchain_rescan_complete": "区块链重新扫描完成",
|
||||
"appx_bootstrap_complete_reconciling": "引导数据完成——正在将你的钱包与新的链数据进行核对。",
|
||||
"appx_cancel": "取消",
|
||||
"appx_cleaning_up": "正在清理…",
|
||||
"appx_confirm_your_backup": "确认你的备份",
|
||||
"appx_copied_clipboard_autoclears": "已复制——剪贴板将在 45 秒后自动清空",
|
||||
"appx_copy": "复制",
|
||||
"appx_could_not_start_restore": "无法开始恢复",
|
||||
"appx_create_failed_prefix": "创建失败:",
|
||||
"appx_creating_your_wallet": "正在创建你的钱包…",
|
||||
"appx_daemon_error": "守护进程错误",
|
||||
"appx_daemon_reinstall_in_progress": "守护进程重新安装已在进行中。",
|
||||
"appx_disconnecting": "正在断开连接…",
|
||||
"appx_done": "完成",
|
||||
"appx_dragonxd_output": "dragonxd 输出",
|
||||
"appx_encrypting_wallet": "正在加密钱包…",
|
||||
"appx_fullnode_lifecycle_unavailable_lite": "全节点生命周期操作在轻量版中不可用",
|
||||
"appx_installing_bundled_daemon": "正在安装捆绑守护进程——节点将停止、更新并重启…",
|
||||
"appx_invalid_payment_uri_prefix": "无效的支付 URI:",
|
||||
"appx_ive_written_it_down": "我已抄写完毕",
|
||||
"appx_keep_node_running_and_quit": "保持节点运行并退出",
|
||||
"appx_last_block_n": "最新区块:%d",
|
||||
"appx_last_used_wallet_not_found_prefix": "找不到你上次使用的钱包文件(",
|
||||
"appx_last_used_wallet_not_found_suffix": ")——已改为打开默认钱包。如果你移动了它,请恢复该文件,然后从钱包列表中切换回来。",
|
||||
"appx_low_spec_mode_disabled": "已禁用低配模式",
|
||||
"appx_low_spec_mode_enabled": "已启用低配模式",
|
||||
"appx_miner_stopped_prefix": "矿工已停止:",
|
||||
"appx_miner_stopped_unexpectedly": "矿工意外停止。",
|
||||
"appx_n_min_n_sec": "%d 分 %d 秒",
|
||||
"appx_n_seconds": "%d 秒",
|
||||
"appx_no_bundled_daemon_to_install": "此版本没有可安装的捆绑守护进程",
|
||||
"appx_no_embedded_daemon_to_install": "此版本没有可安装的内嵌守护进程",
|
||||
"appx_node_busy_restarting": "节点正忙于重启——请稍后再试。",
|
||||
"appx_node_rebuilding_witness_cache": "节点正在重建其见证缓存",
|
||||
"appx_not_next_word": " — 这不是下一个词",
|
||||
"appx_payment_request_loaded": "支付请求已加载",
|
||||
"appx_pool_miner_connected_and_hashing": "矿池矿工已连接并正在计算哈希。",
|
||||
"appx_progress_n_of_n": "进度:%d / %d",
|
||||
"appx_rebuilding_sapling_note_witnesses": "正在重建 Sapling 票据见证…",
|
||||
"appx_rebuilding_witness_cache_blocks_left": "正在重建见证缓存 %.0f%% —— 剩余 %d 个区块",
|
||||
"appx_rebuilding_witness_cache_pct": "正在重建见证缓存 %.0f%%",
|
||||
"appx_recovery_phrase_word_count": "助记词应为 24 个——你输入了 %d 个。",
|
||||
"appx_restarting_daemon_rescan_flag": "正在以 -rescan 标志重启守护进程…",
|
||||
"appx_restarting_daemon_zapwallettxes": "正在以 -zapwallettxes=2 重启守护进程(钱包修复)…",
|
||||
"appx_restoring_your_wallet": "正在恢复你的钱包…",
|
||||
"appx_seed_backup_warning": "这 24 个词是恢复钱包的唯一方式。请按顺序抄写、离线保存,切勿泄露给他人。一旦丢失,你的资金将永远无法找回。",
|
||||
"appx_seed_not_backed_up_warning": "你尚未备份助记词——资金可能丢失。仍要跳过吗?",
|
||||
"appx_sending_stop_command_to_daemon": "正在向守护进程发送停止命令…",
|
||||
"appx_setting_initial_sapling_witnesses": "正在设置初始 Sapling 见证 %.0f%%",
|
||||
"appx_shutdown_complete": "关闭完成",
|
||||
"appx_simple_background_disabled": "已禁用简约背景",
|
||||
"appx_simple_background_enabled": "已启用简约背景",
|
||||
"appx_skip": "跳过",
|
||||
"appx_skip_anyway": "仍要跳过",
|
||||
"appx_still_status_prefix": "仍处于“",
|
||||
"appx_still_status_suffix": "”——现在强制退出可能损坏链数据。",
|
||||
"appx_stop_anyway_and_quit": "仍要停止并退出",
|
||||
"appx_stopping_daemon_deleting_blockchain": "正在停止守护进程并删除区块链数据…",
|
||||
"appx_stopping_node_discards_rebuild": "现在停止节点会丢弃正在进行的重建,下次打开钱包时将重新开始(需要几分钟)。你也可以让节点继续运行。",
|
||||
"appx_stopping_pool_miner": "正在停止矿池矿工…",
|
||||
"appx_syncing_pct_block_n_of_n": "正在同步 %.1f%% —— 区块 %d / %d",
|
||||
"appx_tap_words_in_order": "按正确顺序点击这些词,以确认你已保存。",
|
||||
"appx_theme_effects_disabled": "已禁用主题特效",
|
||||
"appx_theme_effects_enabled": "已启用主题特效",
|
||||
"appx_theme_prefix": "主题:",
|
||||
"appx_use_settings_restart_daemon_hint": "请使用 设置 > 重启守护进程 重试",
|
||||
"appx_waiting_for_daemon_to_encrypt_wallet": "正在等待守护进程加密钱包…",
|
||||
"appx_wallet_created_and_backed_up": "钱包已创建并完成备份。",
|
||||
"appx_wallet_open_failed_prefix": "钱包打开失败:",
|
||||
"auto_shield": "自动屏蔽挖矿",
|
||||
"autoshield_by_node": "您的节点会自动为您屏蔽挖出的 coinbase。",
|
||||
"autoshield_off_backup_seed": "挖出的 coinbase 尚未自动屏蔽。请备份您的助记词(设置 → 备份与数据),以便节点可以安全启用此功能。",
|
||||
"autoshield_off_generic": "您的节点目前未自动屏蔽挖出的 coinbase。",
|
||||
"av_intro": "挖矿软件经常被标记为潜在有害程序。请按照以下步骤启用矿池挖矿:",
|
||||
"av_open_security": "打开 Windows 安全中心",
|
||||
"av_step1": "第 1 步:添加排除项",
|
||||
@@ -74,6 +152,9 @@
|
||||
"av_title": "Windows Defender 已阻止矿工程序",
|
||||
"available": "可用",
|
||||
"backup_backing_up": "正在备份...",
|
||||
"backup_col_backup": "备份",
|
||||
"backup_col_export": "导出",
|
||||
"backup_col_import": "导入与恢复",
|
||||
"backup_create": "创建备份",
|
||||
"backup_created": "钱包备份已创建",
|
||||
"backup_data": "备份与数据",
|
||||
@@ -92,8 +173,22 @@
|
||||
"balance": "余额",
|
||||
"balance_history_collecting": "余额历史——正在收集数据…",
|
||||
"balance_layout": "余额布局",
|
||||
"balance_layout_switched": "布局:%s",
|
||||
"balance_mining_rate": "挖矿中 %s",
|
||||
"balance_shielded_fmt": "屏蔽:%.8f",
|
||||
"balance_syncing_pct": "同步中 %.1f%%",
|
||||
"balance_transparent_fmt": "透明:%.8f",
|
||||
"baltab_market": "市价",
|
||||
"baltab_market_price_4dp": "市价:$%.4f",
|
||||
"baltab_market_price_8dp": "市价:$%.8f",
|
||||
"baltab_pct_change_24h": "%s%.1f%% 24小时",
|
||||
"baltab_pct_of_total_zaddr": "占总额 %.0f%% · %d 个 Z-addr",
|
||||
"baltab_shielded": "隐蔽",
|
||||
"baltab_shielded_amount": "隐蔽 %.8f",
|
||||
"baltab_t_addresses_count": "%d 个 T-address",
|
||||
"baltab_total_balance": "总余额",
|
||||
"baltab_transparent": "透明",
|
||||
"baltab_transparent_amount": "透明 %.8f",
|
||||
"ban": "封禁",
|
||||
"banned_peers": "已封禁节点",
|
||||
"block": "区块",
|
||||
@@ -101,6 +196,12 @@
|
||||
"block_click_copy": "点击复制",
|
||||
"block_click_next": "点击查看下一个区块",
|
||||
"block_click_prev": "点击查看上一个区块",
|
||||
"block_db_reindex_body": "这通常发生在守护进程更新改变了磁盘格式,或区块索引已损坏之后。您的钱包和币是安全的——节点只是无法加载链,因此余额显示为零。\n\n重建会重新读取您现有的区块文件,可能需要一段时间(同时也会重新扫描您的钱包)。不会下载任何数据。",
|
||||
"block_db_reindex_confirm": "重建区块数据库",
|
||||
"block_db_reindex_notify": "节点无法读取其区块数据库(通常发生在守护进程更新之后)。重建它以恢复您的余额——请查看提示,或前往 设置 › 节点。",
|
||||
"block_db_reindex_started": "正在根据您的区块文件重建区块数据库——这可能需要一段时间。",
|
||||
"block_db_reindex_title": "重建区块数据库?",
|
||||
"block_db_reindex_warn": "节点无法读取其区块数据库。",
|
||||
"block_explorer": "区块浏览器",
|
||||
"block_get_info": "获取区块信息",
|
||||
"block_hash": "区块哈希:",
|
||||
@@ -132,6 +233,7 @@
|
||||
"bootstrap_verifying": "正在验证校验和...",
|
||||
"bootstrap_wallet_protected": "(wallet.dat 已受保护)",
|
||||
"bootstrap_warning": "现有区块数据(blocks、chainstate、notarizations)将被删除并替换。您的 wallet.dat 不会被修改或删除。",
|
||||
"byte_count_fmt": "%zu / %zu 字节",
|
||||
"cancel": "取消",
|
||||
"change_pass_confirm": "确认新密码:",
|
||||
"change_pass_current": "当前密码短语:",
|
||||
@@ -147,6 +249,10 @@
|
||||
"chat_accent_theme": "主题",
|
||||
"chat_add_contact": "添加联系人",
|
||||
"chat_awaiting_key": "等待回复",
|
||||
"chat_blocked_desc": "被屏蔽的会话会被移除,其消息(无论新旧)都会被丢弃,直至您取消屏蔽。取消屏蔽会从链上重新导入该会话。",
|
||||
"chat_blocked_manage": "已屏蔽",
|
||||
"chat_blocked_title": "已屏蔽的会话",
|
||||
"chat_blocked_toast": "会话已删除并屏蔽",
|
||||
"chat_bubble_minimal": "极简",
|
||||
"chat_bubble_rounded": "圆角",
|
||||
"chat_bubble_square": "方形",
|
||||
@@ -159,6 +265,16 @@
|
||||
"chat_contact_added": "已添加联系人——可在联系人中重命名",
|
||||
"chat_contact_request": "联系人请求",
|
||||
"chat_copy_address_tip": "点击复制地址",
|
||||
"chat_delete": "删除会话",
|
||||
"chat_delete_block": "删除并屏蔽",
|
||||
"chat_delete_body_prefix": "删除与以下用户会话的本地副本:",
|
||||
"chat_delete_body_suffix": "?",
|
||||
"chat_delete_confirm": "删除",
|
||||
"chat_delete_failed": "无法删除会话——未做任何更改。",
|
||||
"chat_delete_local_note": "此操作仅影响此设备——消息仍保留在区块链上,对方也仍保有自己的副本。",
|
||||
"chat_delete_revive_note": "“删除”会清除此设备上的记录。如果对方再次给您发消息,会话会重新出现。",
|
||||
"chat_delete_title": "删除会话?",
|
||||
"chat_deleted_toast": "会话已删除",
|
||||
"chat_density_comfortable": "宽松",
|
||||
"chat_density_compact": "紧凑",
|
||||
"chat_emoji_color": "彩色",
|
||||
@@ -182,6 +298,7 @@
|
||||
"chat_new_button": "新聊天",
|
||||
"chat_new_message": "消息",
|
||||
"chat_new_message_toast": "新的加密聊天消息",
|
||||
"chat_new_needs_zaddr": "聊天需要隐私(z)地址——透明(t)地址无法接收加密消息。",
|
||||
"chat_new_send": "发送请求",
|
||||
"chat_new_title": "新聊天",
|
||||
"chat_new_zaddr": "收款方 z 地址",
|
||||
@@ -227,6 +344,8 @@
|
||||
"chat_ts_24h": "24小时",
|
||||
"chat_ts_global": "跟随全局",
|
||||
"chat_ts_global_short": "全局",
|
||||
"chat_unblock": "取消屏蔽",
|
||||
"chat_unblocked_toast": "会话已取消屏蔽",
|
||||
"chat_unhide": "取消隐藏",
|
||||
"chat_unmute": "取消静音",
|
||||
"chat_verify_key": "身份密钥 — 对比以验证",
|
||||
@@ -279,6 +398,7 @@
|
||||
"console": "控制台",
|
||||
"console_accents": "颜色强调",
|
||||
"console_app": "应用",
|
||||
"console_auto_focus": "打开时聚焦输入框",
|
||||
"console_auto_scroll": "自动滚动",
|
||||
"console_available_commands": "可用命令:",
|
||||
"console_backend_reference": "后端命令参考",
|
||||
@@ -373,11 +493,15 @@
|
||||
"console_tab_completion": "Tab 补全",
|
||||
"console_text_colors": "文本颜色",
|
||||
"console_toggle_accents": "切换行颜色强调",
|
||||
"console_toggle_auto_focus": "打开控制台标签页时将光标置于命令框中",
|
||||
"console_toggle_text_color": "切换行文本颜色",
|
||||
"console_type_help": "输入 'help' 查看可用命令",
|
||||
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
|
||||
"console_zoom_in": "放大",
|
||||
"console_zoom_out": "缩小",
|
||||
"consolidate_desc": "将多个小额输入合并为单个隐私票据。票据越少,钱包文件越小,隐私性也越好。",
|
||||
"consolidate_funds_btn": "合并",
|
||||
"consolidate_title": "合并资金",
|
||||
"contact_avatar": "头像",
|
||||
"contact_avatar_bad_image": "无法加载该图片。",
|
||||
"contact_avatar_badge": "标记",
|
||||
@@ -407,6 +531,7 @@
|
||||
"contacts_shape_square": "方形",
|
||||
"contacts_shape_tab": "左标签",
|
||||
"copied": "已复制!",
|
||||
"copied_to_clipboard": "已复制到剪贴板",
|
||||
"copy": "复制",
|
||||
"copy_address": "复制完整地址",
|
||||
"copy_error": "复制错误",
|
||||
@@ -419,11 +544,16 @@
|
||||
"daemon_bundled": "内置",
|
||||
"daemon_install_bundled": "安装内置版本",
|
||||
"daemon_installed": "已安装",
|
||||
"daemon_maintenance_label": "维护",
|
||||
"daemon_none_bundled": "此版本未内置",
|
||||
"daemon_not_installed": "未安装",
|
||||
"daemon_status_diff": "版本不一致",
|
||||
"daemon_status_differ": "已安装的程序文件与内置版本不同。",
|
||||
"daemon_status_match": "已安装的程序与内置版本一致。",
|
||||
"daemon_status_missing": "未安装守护进程——请安装内置版本。",
|
||||
"daemon_status_none": "未安装",
|
||||
"daemon_status_ok": "已是最新",
|
||||
"daemon_update_active_tt": "当前已安装(正在使用)的节点",
|
||||
"daemon_update_available": "有新的节点版本可用",
|
||||
"daemon_update_body": "此钱包版本内置的 DragonX 节点比当前已安装的更新。更新会替换已安装的 dragonxd(以及 dragonx-cli/dragonx-tx),然后停止并重启节点以使新版本生效。推荐更新——更新的节点可能增加旧版本所缺少的功能(例如助记词支持)。",
|
||||
"daemon_update_browse": "浏览所有版本…",
|
||||
@@ -433,18 +563,28 @@
|
||||
"daemon_update_download_install": "下载并安装",
|
||||
"daemon_update_downloading": "正在下载…",
|
||||
"daemon_update_failed": "更新失败",
|
||||
"daemon_update_install_this": "安装此版本",
|
||||
"daemon_update_installed": "已安装:",
|
||||
"daemon_update_installed_ok": "节点已安装",
|
||||
"daemon_update_installing": "正在安装…",
|
||||
"daemon_update_keep": "保持当前版本",
|
||||
"daemon_update_later": "稍后",
|
||||
"daemon_update_latest": "最新:",
|
||||
"daemon_update_latest_badge": "最新",
|
||||
"daemon_update_latest_tt": "可用的最新版本",
|
||||
"daemon_update_loading": "正在加载版本…",
|
||||
"daemon_update_no_notes": "此版本没有发行说明。",
|
||||
"daemon_update_now": "立即更新",
|
||||
"daemon_update_prerelease_tt": "预发布 / 测试版本",
|
||||
"daemon_update_prompt_title": "是否更新节点守护进程?",
|
||||
"daemon_update_reinstall": "重新安装",
|
||||
"daemon_update_released": "发布时间:",
|
||||
"daemon_update_restart_note": "重启守护进程以开始运行新版本。",
|
||||
"daemon_update_restart_now": "立即重启守护进程",
|
||||
"daemon_update_safe": "您的钱包、密钥和区块链数据不会被改动——仅替换守护进程的程序文件。如果您是有意运行自定义节点,请选择“保持当前版本”。",
|
||||
"daemon_update_status_current": "此版本已安装",
|
||||
"daemon_update_status_latest": "最新版本",
|
||||
"daemon_update_status_older": "早于您已安装的版本",
|
||||
"daemon_update_title": "更新节点守护进程?",
|
||||
"daemon_update_unavailable_body": "此平台没有可用的节点版本。",
|
||||
"daemon_update_unavailable_title": "节点更新不可用",
|
||||
@@ -453,12 +593,20 @@
|
||||
"daemon_update_verify_note": "在安装前,会根据该版本发布的 SHA-256 和固定的 ed25519 签名对下载内容进行校验。",
|
||||
"daemon_update_verifying": "正在验证…",
|
||||
"daemon_update_version": "版本:",
|
||||
"daemon_updates_label": "更新",
|
||||
"daemon_version": "守护进程",
|
||||
"dark": "深色",
|
||||
"data_stale_prefix": "更新于",
|
||||
"data_stale_tooltip": "余额可能已过时 — 钱包最近未收到更新。请检查您的节点连接。",
|
||||
"date": "日期",
|
||||
"date_label": "日期:",
|
||||
"debug_gate_confirm": "显示调试选项",
|
||||
"debug_gate_incorrect": "PIN 或密码不正确。",
|
||||
"debug_gate_pass_prompt": "请输入您的钱包密码以继续:",
|
||||
"debug_gate_pin_prompt": "请输入您的解锁 PIN 以继续:",
|
||||
"debug_gate_title": "显示调试选项?",
|
||||
"debug_gate_verifying": "验证中…",
|
||||
"debug_gate_warning": "这些是高级诊断和节点调优选项,可能暴露敏感信息并改变节点的运行方式。仅在您了解自己在做什么的情况下继续。",
|
||||
"debug_logging": "调试日志",
|
||||
"decrypt_desc": "钱包将被导出,守护进程将使用全新的未加密钱包重新启动,并重新导入所有密钥。根据钱包大小,这可能需要几分钟。",
|
||||
"decrypt_error_title": "解密失败",
|
||||
@@ -481,10 +629,22 @@
|
||||
"disconnected": "已断开",
|
||||
"dismiss": "关闭",
|
||||
"display": "显示",
|
||||
"done": "完成",
|
||||
"download": "下载",
|
||||
"download_bootstrap": "下载引导程序",
|
||||
"dragonx_green": "DragonX(绿色)",
|
||||
"edit": "编辑",
|
||||
"empty_wallet_keys_suffix": "个密钥",
|
||||
"empty_wallet_open_manager": "打开钱包管理器",
|
||||
"empty_wallet_restore": "恢复我的钱包",
|
||||
"empty_wallet_salvage_body": "此钱包为空,因为先前的一次自动修复已将您的原始钱包作为备份保存到一旁。您的币几乎肯定在该备份中,并未丢失。恢复它即可重新加载您的资金——不会删除任何内容;当前文件会先被保存到一旁。",
|
||||
"empty_wallet_salvage_headline": "您的币安全地存放在备份文件中。",
|
||||
"empty_wallet_salvage_title": "您的钱包可能已被修复",
|
||||
"empty_wallet_warning_body": "此钱包没有地址也没有资金,但您的 DragonX 文件夹中的另一个钱包文件包含密钥。您的币很可能在其中,并未丢失。打开钱包管理器以切换到持有您资金的钱包。",
|
||||
"empty_wallet_warning_dismiss": "不再为此钱包提示",
|
||||
"empty_wallet_warning_dismiss_tip": "仅对当前钱包文件停止此提示。如果您以后切换到另一个空钱包,可能会再次提示。",
|
||||
"empty_wallet_warning_headline": "您可能打开了错误的钱包。",
|
||||
"empty_wallet_warning_title": "此钱包为空",
|
||||
"enc_confirm": "确认:",
|
||||
"enc_desc": "加密钱包会用密码短语保护您的私钥。加密后,守护进程将重新启动。",
|
||||
"enc_encrypting": "正在加密钱包...",
|
||||
@@ -523,6 +683,7 @@
|
||||
"explorer_tx_outputs": "输出",
|
||||
"explorer_tx_size": "大小",
|
||||
"explorer_txid": "TxID:",
|
||||
"explorer_urls_hdr": "网址",
|
||||
"export": "导出",
|
||||
"export_csv": "导出 CSV",
|
||||
"export_keys_btn": "导出密钥",
|
||||
@@ -550,6 +711,151 @@
|
||||
"export_viewing_key": "导出查看密钥",
|
||||
"failed_create_shielded": "无法创建屏蔽地址",
|
||||
"failed_create_transparent": "无法创建透明地址",
|
||||
"faq": "常见问题",
|
||||
"faq_d_mgmt_1_a": "内置节点会随钱包自动启动,并在适当时机停止,因此通常无需手动管理。高级控制选项(包括重启守护进程)位于 设置 > 节点与安全。",
|
||||
"faq_d_mgmt_1_q": "如何启动或停止节点?",
|
||||
"faq_d_mgmt_2_a": "如果您将钱包连接到自己启动的节点,此选项决定关闭钱包时是否也关闭该节点。如果希望关闭钱包后节点继续运行,请保持关闭此选项。",
|
||||
"faq_d_mgmt_2_q": "“退出时关闭外部守护进程”是做什么的?",
|
||||
"faq_d_mgmt_3_a": "节点和钱包数据存储在用户配置文件下的 DragonX 数据目录中(例如 Windows 上的 AppData,或 Linux/macOS 上的主文件夹)。区块链占用的空间最大,请为其增长预留足够的磁盘空间。",
|
||||
"faq_d_mgmt_3_q": "区块链和钱包数据存储在哪里?",
|
||||
"faq_d_mgmt_title": "节点管理",
|
||||
"faq_d_mine_1_a": "挖矿标签页可让您使用 CPU 挖矿。DragonX 使用 RandomX 算法,专为普通处理器设计。设置线程数并开始——奖励将累积到您的钱包中。",
|
||||
"faq_d_mine_1_q": "如何挖 DragonX?",
|
||||
"faq_d_mine_2_a": "独立挖矿的奖励直接归您所有,但只有在您挖到区块时才会获得,除非您有很大算力,否则并不频繁。矿池挖矿与他人共享算力,可获得较小但更稳定的收益。挖矿标签页同时支持这两种方式。",
|
||||
"faq_d_mine_2_q": "独立挖矿还是矿池挖矿——我该选哪个?",
|
||||
"faq_d_mine_3_a": "挖矿标签页的矿池部分有一个“更新矿工…”按钮,可下载、验证并安装最新的优化版 DragonX 矿工程序。与节点更新程序一样,每次下载在安装前都会进行校验和与签名验证。",
|
||||
"faq_d_mine_3_q": "如何更新矿工程序?",
|
||||
"faq_d_mine_4_a": "当前节点可以运行内置的 stratum 服务器,让其他矿工连接到您的机器。在“设置 > 节点与安全”下有相应的开关。默认情况下它只监听本地连接;要向其他计算机开放,需要您主动配置。",
|
||||
"faq_d_mine_4_q": "我可以搭建矿池吗?",
|
||||
"faq_d_mine_title": "挖矿与矿池",
|
||||
"faq_d_net_1_a": "节点启动后需要一点时间来寻找对等节点,因此短暂出现“0 个对等节点”是正常现象。如果持续存在,请检查您的网络连接以及防火墙是否阻挡了节点。节点会通过内置种子自动寻找对等节点。",
|
||||
"faq_d_net_1_q": "钱包显示没有对等节点或连接——出了什么问题?",
|
||||
"faq_d_net_2_a": "是的——DragonX 节点之间通过 TLS 通信,因此对等节点连接是加密的。钱包与自己的节点之间也通过安全的本地通道通信。",
|
||||
"faq_d_net_2_q": "我与网络的连接是加密的吗?",
|
||||
"faq_d_net_title": "对等节点与网络",
|
||||
"faq_d_node_1_a": "守护进程 dragonxd 是 DragonX 的全节点。它下载并验证整条区块链,向网络中继交易,并响应钱包的查询。运行自己的节点意味着您无需信任任何其他服务器。",
|
||||
"faq_d_node_1_q": "什么是守护进程(dragonxd)?",
|
||||
"faq_d_node_2_a": "默认情况下,钱包会启动并管理自己内置的节点(内嵌节点)——您无需做任何操作。如果您已经自行运行了 dragonxd,钱包可以改为连接到该外部节点。钱包会检测当前所处的情况并相应地运作。",
|
||||
"faq_d_node_2_q": "内嵌节点与外部节点有什么区别?",
|
||||
"faq_d_node_3_a": "全节点独立验证区块链的每一条规则,因此您的钱包信任的是数学运算,而非第三方。这也增强了网络的健壮性。代价是磁盘空间和一次初始同步。",
|
||||
"faq_d_node_3_q": "为什么要运行全节点?",
|
||||
"faq_d_node_title": "全节点",
|
||||
"faq_d_perf_1_a": "区块链是占用空间最大的部分,且会随时间增长,因此请预留几个 GB 的空闲空间。节点使用数据库缓存来提升速度;内置默认值已针对常见机器进行了调优。高级用户可以通过编辑配置文件(DRAGONX.conf)中的 dbcache 来调整节点的数据库缓存。",
|
||||
"faq_d_perf_1_q": "节点占用多少磁盘空间?可以调整内存吗?",
|
||||
"faq_d_perf_2_a": "启用引导包下载(在设置向导中提供),可获取一份最新的、已验证的链数据副本,而无需从头验证每一个区块。使用高速磁盘(SSD)也有很大帮助。",
|
||||
"faq_d_perf_2_q": "如何加快初始同步速度?",
|
||||
"faq_d_perf_title": "存储与性能",
|
||||
"faq_d_sync_1_a": "启动时,节点会先加载并验证区块,然后才能为钱包提供服务。“正在处理区块 / 正在应用区块以构建当前链状态”界面就是这个预热过程。这是正常现象;节点完成并报告其高度后,钱包即可使用。",
|
||||
"faq_d_sync_1_q": "启动时的“正在处理区块”是什么意思?",
|
||||
"faq_d_sync_2_a": "对于您的隐私资金,节点会保留加密“见证数据”,让您能够进行隐私花费。在某些重启或更新之后,节点会通过重新扫描最近的区块来重建这个缓存。在交易较多的钱包上,这可能需要几分钟并占用额外内存。\n\n这并非卡死——请让它完成。中途强制退出只会导致下次重新开始重建。",
|
||||
"faq_d_sync_2_q": "“正在重建见证缓存”是什么意思?",
|
||||
"faq_d_sync_3_a": "接近链顶时,扫描钱包的工作(检查余额和票据)会与节点争用同一个内部锁,这可能会拖慢大钱包连接最后几个区块的速度。较新版本会限制此类扫描,以便节点能够赶上进度。停留在较轻量的标签页(或耐心等待)可以让它更快完成。",
|
||||
"faq_d_sync_3_q": "为什么快追上进度时同步反而变慢了?",
|
||||
"faq_d_sync_4_a": "首次同步会下载并验证整条链,根据您的网络和磁盘情况可能需要一段时间。在设置向导中启用引导包下载可以大幅加快速度,它会获取一份最新的、已验证的链数据副本。",
|
||||
"faq_d_sync_4_q": "初始同步需要多长时间?",
|
||||
"faq_d_sync_title": "同步与区块链",
|
||||
"faq_d_trbl_1_a": "通常不是。在大钱包上,节点重建隐私见证数据或应用区块可能需要几分钟,此期间它看起来像是暂停了,响应也可能较慢。请给它一些时间——调试日志(设置中)会显示持续的进度,如果它正在正常工作。\n\n在此阶段请避免强制退出,否则下次启动会从头开始这项工作。",
|
||||
"faq_d_trbl_1_q": "节点似乎卡在“正在激活最长链”——是卡死了吗?",
|
||||
"faq_d_trbl_2_a": "如果钱包文件被恢复,但部分密钥数据丢失,节点可能会以受限的“降级”模式打开它,此时无法生成新地址或进行隐私保护。最安全的解决方法是使用您的助记词恢复短语,将钱包恢复到一个全新的钱包中。",
|
||||
"faq_d_trbl_2_q": "什么是“降级模式”?",
|
||||
"faq_d_trbl_3_a": "重新扫描会让钱包重新读取链上数据,以重新发现您的交易记录(在导入密钥后很有用)。重新索引会重建节点的区块链数据库,仅在该数据库损坏或因版本变更而不兼容时才需要。这两个过程都可能需要一段时间;可从“设置 > 节点与安全”中启动它们。",
|
||||
"faq_d_trbl_3_q": "什么时候应该重新索引或重新扫描?",
|
||||
"faq_d_trbl_4_a": "在重建见证数据或初始同步等繁重工作期间,内存使用会激增,完成后会回落。空闲时持续高内存占用并不正常——重启钱包可以清除它。在低内存机器上,请确保没有其他占用大量内存的应用与之争抢资源。",
|
||||
"faq_d_trbl_4_q": "节点占用了大量内存——这正常吗?",
|
||||
"faq_d_trbl_title": "故障排除",
|
||||
"faq_d_upd_1_a": "打开“设置 > 节点与安全 > 守护进程二进制文件”,使用“检查更新…”。钱包会下载最新的已验证节点版本并安装;新版本会在守护进程下次启动时生效。",
|
||||
"faq_d_upd_1_q": "如何更新节点?",
|
||||
"faq_d_upd_2_a": "是的——更新对话框会列出每一个发行版本,您可以选择特定或较旧的版本。降级时请谨慎:较旧的节点可能无法接受较新节点写入的区块链数据,可能需要重新索引。",
|
||||
"faq_d_upd_2_q": "我可以安装特定版本的节点吗?",
|
||||
"faq_d_upd_3_a": "每个下载的节点压缩包在安装前都会通过校验和与加密签名进行验证。如果验证失败,钱包会拒绝该更新。",
|
||||
"faq_d_upd_3_q": "这次更新安全吗?",
|
||||
"faq_d_upd_title": "更新节点",
|
||||
"faq_group_daemon": "守护进程",
|
||||
"faq_group_wallet": "钱包",
|
||||
"faq_intro": "关于您的钱包和 DragonX 节点的解答。请搜索,或在下方按主题浏览。",
|
||||
"faq_intro_lite": "关于您的轻量钱包的解答。请搜索,或在下方按主题浏览。",
|
||||
"faq_l_gs_1_a": "ObsidianDragonLite 是轻量版 DragonX 钱包。它不运行全节点,而是连接到 DragonX 轻量服务器,因此启动速度快,也无需下载整条区块链。\n\n它像完整版钱包一样管理您的密钥、余额和转账——请参阅下方的“轻量钱包”部分,了解服务器连接的工作原理及隐私权衡。",
|
||||
"faq_l_gs_1_q": "什么是 ObsidianDragonLite?",
|
||||
"faq_l_lite_1_a": "轻量钱包连接到 DragonX 轻量服务器,而不是运行自己的全节点。这使它启动迅速,占用的磁盘和带宽也更少,因为它自身不下载或验证整条区块链。您的私钥始终保存在您的设备上——绝不会发送到服务器。",
|
||||
"faq_l_lite_1_q": "轻量钱包与完整版钱包有什么区别?",
|
||||
"faq_l_lite_2_a": "打开设置并选择一个服务器。您可以随时切换服务器,钱包会从新服务器重新同步。如果某个服务器响应慢或无法访问,请尝试其他服务器。",
|
||||
"faq_l_lite_2_q": "如何选择或更改服务器?",
|
||||
"faq_l_lite_3_a": "您的密钥永远不会离开您的设备,隐私金额也始终保持加密。但轻量服务器确实能看到您的钱包查询了哪些地址以及您的 IP 地址,因此它可以将您的地址关联起来——这是以隐私换取便利的权衡。若追求最高隐私性,请使用全节点钱包,它会自行验证链上数据,不会向服务器暴露您的地址。",
|
||||
"faq_l_lite_3_q": "轻量钱包的隐私性和全节点一样吗?",
|
||||
"faq_l_lite_title": "轻量钱包",
|
||||
"faq_l_sec_1_a": "打开 设置 > 钱包,在安全部分设置密码。加密可保护磁盘上的私钥,这样即使有人拿到了文件,也无法花费您的币。\n\n请选择一个强密码,并妥善保管——一旦丢失,加密钱包将无法恢复。",
|
||||
"faq_no_results": "没有结果。请尝试其他搜索词。",
|
||||
"faq_open_tooltip": "帮助与常见问题",
|
||||
"faq_search_hint": "搜索常见问题…",
|
||||
"faq_title": "帮助与常见问题",
|
||||
"faq_w_addr_1_a": "透明地址(以 R 或 t 开头)的工作方式与大多数币种相同:金额和地址在区块链上是公开的。\n\n隐私地址(z 地址,以 zs 开头)使用零知识加密技术——金额、发送方和接收方都在链上加密。存放在隐私地址中的余额是私密的。",
|
||||
"faq_w_addr_1_q": "透明地址和隐私地址有什么区别?",
|
||||
"faq_w_addr_2_a": "请尽量使用隐私(z)地址——它们能保护您的余额和支付记录不被公开。DragonX 是一种隐私币,收款默认使用隐私地址。\n\n只有在您使用的服务无法处理隐私地址时,才使用透明地址。",
|
||||
"faq_w_addr_2_q": "应该使用哪种地址类型?",
|
||||
"faq_w_addr_3_a": "对于隐私地址,重复使用没有问题,也不会泄露历史记录。对于透明地址,每次付款使用新地址可以提升隐私性。可以在“接收”标签页创建新地址。",
|
||||
"faq_w_addr_3_q": "应该重复使用地址,还是新建一个?",
|
||||
"faq_w_addr_4_a": "屏蔽会将币从透明地址转移到隐私地址,使其变为私密。新挖出的币到账时是透明的,会自动为您屏蔽。\n\n您也可以手动屏蔽;屏蔽只是一种特殊交易,因此需要正常的确认时间才能完成。",
|
||||
"faq_w_addr_4_q": "什么是“屏蔽”?",
|
||||
"faq_w_addr_title": "地址与隐私",
|
||||
"faq_w_bal_1_a": "在钱包同步完成之前,您的余额是不完整的,可能显示为 0——因为它还没有扫描完您的所有交易。同步完成后余额会自动补齐。\n\n请留意窗口底部的状态栏:它显示连接状态和区块高度。同步完成后,余额即为准确值。",
|
||||
"faq_w_bal_1_q": "为什么我的余额是 0 或不更新?",
|
||||
"faq_w_bal_2_a": "在包含您交易的区块之上每挖出一个新区块,就会增加一个确认。确认数越多,代表付款结算得越牢固。收到的资金在获得第一个确认后即可使用。",
|
||||
"faq_w_bal_2_q": "什么是确认数?",
|
||||
"faq_w_bal_3_a": "总余额包括仍在确认中或暂时被锁定的资金(例如正在屏蔽过程中的币)。可用余额是您现在可以发送的部分。随着交易确认,两者会趋于一致。",
|
||||
"faq_w_bal_3_q": "总余额和可用余额有什么区别?",
|
||||
"faq_w_bal_title": "余额与同步",
|
||||
"faq_w_chat_1_a": "聊天是内置于钱包中的加密链上通讯功能。消息以私密的隐私备注形式发送,只有您和对方能够读取。",
|
||||
"faq_w_chat_1_q": "什么是聊天功能?",
|
||||
"faq_w_chat_2_a": "您的聊天身份是从钱包的助记词派生而来的,因此它会随钱包一起保留——恢复钱包,身份也会随之恢复。您无需另外创建账户或密码。",
|
||||
"faq_w_chat_2_q": "我的聊天身份是如何工作的?",
|
||||
"faq_w_chat_3_a": "在“联系人”标签页通过地址添加联系人,可选填写名称和头像。联系人可以仅在当前钱包中保留私有,也可以在您在此电脑上打开的所有钱包间共享。",
|
||||
"faq_w_chat_3_q": "联系人是如何工作的?",
|
||||
"faq_w_chat_4_a": "打开一个会话,使用其顶部的图标进行操作。隐藏(眼睛图标)会将其从列表中移除但保留全部消息——收到新消息会自动取消隐藏。删除(垃圾桶图标)会打开一个对话框:“删除”会清除本设备上的记录(如果对方再次给您发消息,会话会重新出现),而“删除并屏蔽”还会阻止对方之后的消息,直到您在会话列表上方的“已屏蔽”列表中取消屏蔽为止。这里的所有操作仅在本设备本地生效——消息仍保留在区块链上,对方也保留着自己的副本,因此这并非“撤回”。",
|
||||
"faq_w_chat_4_q": "如何隐藏、删除或屏蔽一个会话?",
|
||||
"faq_w_chat_title": "聊天与联系人",
|
||||
"faq_w_gs_1_a": "ObsidianDragon 是 DragonX(DRGX)的全节点钱包。它管理您的币,并且在桌面版本中会在后台运行 DragonX 全节点(守护进程),使您的钱包能够自行验证区块链,而无需信任第三方服务器。\n\n可以把它想象成两部分协同工作:钱包(密钥、余额、发送)和节点(区块链与网络)。这里的问题也按同样的方式划分——请参见上方的“钱包”和“守护进程”标签页。",
|
||||
"faq_w_gs_1_q": "什么是 ObsidianDragon?",
|
||||
"faq_w_gs_2_a": "首次启动时,设置向导会自动为您创建一个钱包。新钱包由一个助记词(一组单词)作为备份。请把这些词写下来并妥善离线保管——如果这台电脑丢失,这是恢复您资金的唯一方式。\n\n您可以随时在 设置 > 备份与数据 > 助记词 中查看或备份该助记词。",
|
||||
"faq_w_gs_2_q": "如何创建新钱包?",
|
||||
"faq_w_gs_3_a": "如果您已有助记词,请使用助记词恢复流程。(在全节点版本中,您也可以在启动前将现有的 wallet.dat 文件复制到钱包的数据目录中。)\n\n恢复后,钱包会重新扫描以找回您以往的交易,因此首次显示余额和历史记录可能需要一些时间。",
|
||||
"faq_w_gs_3_q": "如何恢复我已有的钱包?",
|
||||
"faq_w_gs_4_a": "向导可让您选择外观/主题、设置加密和 PIN 码,在全节点版本中还可以选择下载引导数据以加快首次同步。您可以跳过任意步骤,之后随时在设置中更改这些内容。",
|
||||
"faq_w_gs_4_q": "首次运行的设置向导做了什么?",
|
||||
"faq_w_gs_title": "新手入门",
|
||||
"faq_w_sec_1_a": "打开 设置 > 节点与安全,在安全部分设置密码。加密可保护磁盘上的私钥,这样即使有人拿到了文件,也无法花费您的币。\n\n请选择一个强密码,并妥善保管——一旦丢失,加密钱包将无法恢复。",
|
||||
"faq_w_sec_1_q": "如何加密我的钱包?",
|
||||
"faq_w_sec_2_a": "PIN 码用于锁定应用界面,让您离开时余额和操作被隐藏。这是加密之上的一层便捷锁定,可以在向导中或在完成钱包加密后于设置中设置。",
|
||||
"faq_w_sec_2_q": "什么是 PIN 码/锁屏?",
|
||||
"faq_w_sec_3_a": "不能。钱包加密无法被绕过。如果您仍保存着助记词,可以用它将钱包恢复到一个新钱包并设置新密码。如果密码和助记词都没有,资金将无法找回。",
|
||||
"faq_w_sec_3_q": "我忘记密码了——能找回吗?",
|
||||
"faq_w_sec_title": "安全与加密",
|
||||
"faq_w_seed_1_a": "前往 设置 > 备份与数据 > 助记词。按顺序把这些单词抄写在纸上,并存放在安全的离线地点。任何持有该助记词的人都能花费您的币,因此切勿将其保存在照片、邮件或云笔记中。",
|
||||
"faq_w_seed_1_q": "如何备份我的助记词?",
|
||||
"faq_w_seed_2_a": "较旧(传统)的钱包不是基于助记词的。钱包可以将传统钱包迁移为现代的助记词钱包:创建一个新的助记词钱包,并将您的资金转移过去。请在 设置 > 备份与数据 中查找“迁移到助记词钱包…”。此功能需要当前版本的守护进程。",
|
||||
"faq_w_seed_2_q": "我的钱包没有助记词——可以添加吗?",
|
||||
"faq_w_seed_3_a": "设置钱包时使用助记词恢复流程,并按顺序输入您的单词。之后钱包会重新扫描以重建余额和交易历史,首次可能需要一些时间。",
|
||||
"faq_w_seed_3_q": "如何用助记词恢复钱包?",
|
||||
"faq_w_seed_4_a": "助记词是主要备份,足以恢复一切。钱包文件的副本是一个便捷的次要备份,还能保留标签和设置。请将所有备份妥善离线私密保管。",
|
||||
"faq_w_seed_4_q": "我还需要备份 wallet.dat 吗?",
|
||||
"faq_w_seed_title": "助记词与备份",
|
||||
"faq_w_send_1_a": "打开“发送”标签页,粘贴收款方地址,输入金额并确认。如果钱包已加密,系统会要求您解锁以完成交易。\n\n发送到隐私地址是私密的;发送到透明地址是公开的。",
|
||||
"faq_w_send_1_q": "如何发送资金?",
|
||||
"faq_w_send_2_a": "DragonX 的手续费非常低,并且是自动设置的。手续费会在您确认前显示。通常无需手动更改。",
|
||||
"faq_w_send_2_q": "我需要支付多少手续费?",
|
||||
"faq_w_send_3_a": "可以——向隐私(z)地址发送时,您可以附加一条加密备注。只有收款方能够读取它。透明地址不支持备注。",
|
||||
"faq_w_send_3_q": "我可以在付款时附加消息吗?",
|
||||
"faq_w_send_4_a": "交易在被打包进区块并获得确认之前处于「待处理」状态。这通常需要一两分钟。屏蔽交易还需要您的钱包已完成同步才能生成证明。\n\n如果一笔发款长时间处于待处理状态,请检查您的钱包是否已连接并同步(见窗口底部的状态栏)。",
|
||||
"faq_w_send_4_q": "我发送了一笔付款,但显示待处理——为什么?",
|
||||
"faq_w_send_title": "发送与接收",
|
||||
"faq_w_set_1_a": "打开 设置 → 外观 以选择主题和强调色。内置多种明暗皮肤,模糊等特效也可在低性能设备上关闭。",
|
||||
"faq_w_set_1_q": "如何更改主题?",
|
||||
"faq_w_set_2_a": "可以——设置 → 外观 中有语言选择器。钱包内置多种翻译;尚未翻译的部分会回退为英文。",
|
||||
"faq_w_set_2_q": "可以更改语言吗?",
|
||||
"faq_w_set_3_a": "使用设置 → 外观 中的字体缩放选项。它会缩放整个界面,在高分辨率(HiDPI)显示器上非常有用,否则应用可能显示得过小。",
|
||||
"faq_w_set_3_q": "文字太小(或太大)——可以缩放吗?",
|
||||
"faq_w_set_4_a": "在设置中启用价格获取。钱包会在余额旁显示大致的法币价值,并可显示简易行情图。价格仅供参考。",
|
||||
"faq_w_set_4_q": "如何显示法币价格?",
|
||||
"faq_w_set_title": "设置与外观",
|
||||
"favorite_address": "收藏地址",
|
||||
"fee": "手续费",
|
||||
"fee_high": "高",
|
||||
@@ -572,6 +878,63 @@
|
||||
"general": "常规",
|
||||
"generating": "正在生成",
|
||||
"go_to_receive": "前往接收",
|
||||
"grpa_current_block_paren": "(当前:%d)",
|
||||
"grpa_days_ago": "%lld 天前",
|
||||
"grpa_dbg_addrman": "对等节点地址追踪与管理",
|
||||
"grpa_dbg_alert": "警报系统消息",
|
||||
"grpa_dbg_bench": "操作的基准计时",
|
||||
"grpa_dbg_coindb": "币数据库读写操作",
|
||||
"grpa_dbg_db": "Berkeley DB 操作",
|
||||
"grpa_dbg_estimatefee": "手续费估算算法",
|
||||
"grpa_dbg_http": "HTTP RPC 服务器活动",
|
||||
"grpa_dbg_libevent": "Libevent 网络库",
|
||||
"grpa_dbg_lock": "锁竞争调试",
|
||||
"grpa_dbg_mempool": "交易内存池活动",
|
||||
"grpa_dbg_net": "网络连接与消息",
|
||||
"grpa_dbg_paymentdisclosure": "支付披露协议",
|
||||
"grpa_dbg_pow": "工作量证明挖矿活动",
|
||||
"grpa_dbg_proxy": "SOCKS5 代理连接",
|
||||
"grpa_dbg_prune": "区块修剪操作",
|
||||
"grpa_dbg_rand": "随机数生成",
|
||||
"grpa_dbg_reindex": "区块链重新索引进度",
|
||||
"grpa_dbg_rpc": "RPC 命令处理",
|
||||
"grpa_dbg_selectcoins": "交易的币选择",
|
||||
"grpa_dbg_tor": "Tor 集成与线路信息",
|
||||
"grpa_dbg_zmq": "ZeroMQ 通知系统",
|
||||
"grpa_dbg_zrpc": "隐蔽(z-addr)RPC 操作",
|
||||
"grpa_enter_private_key_to_import": "请输入要导入的私钥。",
|
||||
"grpa_error_prefix": "错误:",
|
||||
"grpa_hr_ago": "%lld 小时前",
|
||||
"grpa_invalid_response_from_daemon": "守护进程返回了无效响应",
|
||||
"grpa_invalid_suffix": "(无效)",
|
||||
"grpa_min_ago": "%lld 分钟前",
|
||||
"grpa_sec_ago": "%lld 秒前",
|
||||
"grpa_seed_demo_chat": "种子演示聊天",
|
||||
"grpa_showing_first_100_of": "…显示 %d 项中的前 100 项",
|
||||
"grpa_tab_about": "关于",
|
||||
"grpa_tab_appearance": "外观",
|
||||
"grpa_tab_backup_data": "备份与数据",
|
||||
"grpa_tab_chat": "聊天",
|
||||
"grpa_tab_explorer": "浏览器",
|
||||
"grpa_tab_node_security": "节点与安全",
|
||||
"grpa_tab_wallet": "钱包",
|
||||
"grpa_unexpected_getblockhash_result": "意外的 getblockhash 结果",
|
||||
"grpb_copy": "复制",
|
||||
"grpb_max": "最大",
|
||||
"grpb_new_badge_suffix": " [新]",
|
||||
"grpb_preview_msg_payment_through": "付款到账了吗?🙂",
|
||||
"grpb_preview_msg_sending_rest": "现在把剩下的发过去 👍",
|
||||
"grpb_preview_msg_yep_confirmed": "到了——刚刚确认 ✅",
|
||||
"grpb_selected_suffix": "\n(已选)",
|
||||
"grpb_tooltip_address_balance": "%s\n余额:%.8f %s%s",
|
||||
"grpb_undo_clear": "撤销清除",
|
||||
"grpc_benchmark_inconclusive": "基准测试无结果:未记录到任何算力样本。请检查矿池连接后重试。",
|
||||
"grpc_benchmark_takes_secs": "基准测试约需 %d 秒并会中断挖矿。再次点击以开始。",
|
||||
"grpc_bootstrap_failed": "引导失败",
|
||||
"grpc_bootstrap_not_initialized": "引导未初始化",
|
||||
"grpc_hashrate_fee": "%s %s%% 手续费",
|
||||
"grpc_key_not_available": "此地址无可用密钥",
|
||||
"grpc_na": "不适用",
|
||||
"height": "高度",
|
||||
"help": "帮助",
|
||||
"hidden_tag": " (已隐藏)",
|
||||
@@ -646,6 +1009,7 @@
|
||||
"light": "浅色",
|
||||
"lite_account_label": "账户",
|
||||
"lite_action": "操作",
|
||||
"lite_backend_unavailable": "轻量钱包后端不可用",
|
||||
"lite_backup_keys": "备份与密钥",
|
||||
"lite_birthday_backup": "生日区块:%llu (也请一并备份)",
|
||||
"lite_birthday_hint": "开始扫描的区块高度。如未知请保留 0(完整扫描更慢)。",
|
||||
@@ -653,9 +1017,12 @@
|
||||
"lite_console_backend_commands": "后端命令:",
|
||||
"lite_console_help_passthrough": "其他任何输入都将作为轻钱包控制台命令运行。",
|
||||
"lite_copy": "复制",
|
||||
"lite_could_not_start": "无法启动该操作",
|
||||
"lite_could_not_write": "无法写入 ",
|
||||
"lite_encrypt_wallet": "加密钱包",
|
||||
"lite_encryption_removed": "已移除加密",
|
||||
"lite_enter_all_seed_words": "请输入全部 24 个助记词以恢复(已输入 %d 个)",
|
||||
"lite_enter_wallet_path": "请输入钱包路径",
|
||||
"lite_hide_wipe": "隐藏并清除",
|
||||
"lite_import": "导入",
|
||||
"lite_import_key_label": "导入密钥",
|
||||
@@ -741,6 +1108,7 @@
|
||||
"loading": "加载中...",
|
||||
"loading_addresses": "正在加载地址...",
|
||||
"loading_stall_body": "守护进程已初始化 %.0f 秒。更新后或首次启动时(加载区块索引或重新扫描)这可能是正常现象——就绪后会自动连接。",
|
||||
"loading_stall_hint": "卡住了?前往 设置 → 重启节点,或查看控制台。",
|
||||
"loading_stall_title": "耗时超出预期",
|
||||
"loading_transactions": "正在加载交易",
|
||||
"local_hashrate": "本地算力",
|
||||
@@ -791,9 +1159,27 @@
|
||||
"memo_optional": "备注(可选)",
|
||||
"memo_upper": "备注",
|
||||
"memo_z_only": "注意:备注仅在发送到屏蔽 (z) 地址时可用",
|
||||
"merge_addr_created": "屏蔽地址已创建。",
|
||||
"merge_advanced": "高级",
|
||||
"merge_back": "返回",
|
||||
"merge_batch_fmt": "每次最多合并 %d 笔输入——重复执行以完成剩余部分。",
|
||||
"merge_confirm_btn": "确认",
|
||||
"merge_confirm_fmt": "将约 %s DRGX 从 %d 笔输入合并到 %s?",
|
||||
"merge_create_zaddr": "创建屏蔽地址",
|
||||
"merge_creating": "正在创建地址…",
|
||||
"merge_description": "将多个 UTXO 合并到一个屏蔽地址。这可以帮助减小钱包大小并提高隐私性。",
|
||||
"merge_fee_hint": "此交易的网络手续费。",
|
||||
"merge_funds": "合并资金",
|
||||
"merge_max_inputs": "每批最大输入数",
|
||||
"merge_no_spendable": "暂无可合并的可用输入。",
|
||||
"merge_progress": "正在合并…这可能需要几分钟。您可以关闭此窗口。",
|
||||
"merge_scope_fmt": "%d 笔透明输入 + %d 笔屏蔽输入 ・ 约 %s DRGX 可用",
|
||||
"merge_scope_loading": "正在检查您的输入…",
|
||||
"merge_send_failed": "合并失败:",
|
||||
"merge_source": "合并",
|
||||
"merge_src_both": "两者",
|
||||
"merge_src_shielded": "屏蔽",
|
||||
"merge_src_transparent": "透明",
|
||||
"merge_started": "合并操作已开始",
|
||||
"merge_title": "合并到地址",
|
||||
"mig_adopt": "将其设为我的钱包",
|
||||
@@ -874,6 +1260,7 @@
|
||||
"mining_difficulty_copied": "难度已复制",
|
||||
"mining_est_block": "预计区块",
|
||||
"mining_est_daily": "预计日收益",
|
||||
"mining_est_daily_pool_sub": "粗略的单人挖矿等值,扣除矿池费用前",
|
||||
"mining_filter_all": "全部",
|
||||
"mining_filter_tip_all": "显示所有收益",
|
||||
"mining_filter_tip_pool": "仅显示矿池收益",
|
||||
@@ -902,10 +1289,12 @@
|
||||
"mining_open_in_explorer": "在浏览器中打开",
|
||||
"mining_payout_address": "支付地址",
|
||||
"mining_payout_foreign": "⚠ 此支付地址不在您当前的钱包中——挖矿奖励将进入另一个钱包。如果您切换过钱包,请更新它。",
|
||||
"mining_payout_invalid": "不是有效的 DragonX 地址——启动前请更正,否则挖矿奖励将丢失。",
|
||||
"mining_payout_tooltip": "接收挖矿奖励的地址",
|
||||
"mining_pool": "矿池",
|
||||
"mining_pool_fee": "费用",
|
||||
"mining_pool_hashrate": "矿池算力",
|
||||
"mining_pool_needs_payout_tooltip": "请先输入收款地址(生成一个 Z 地址)",
|
||||
"mining_pool_url": "矿池 URL",
|
||||
"mining_pools_header": "矿池",
|
||||
"mining_recent_blocks": "最近区块",
|
||||
@@ -935,6 +1324,9 @@
|
||||
"mining_syncing_tooltip": "区块链同步中...",
|
||||
"mining_tag": " · 挖矿",
|
||||
"mining_threads": "挖矿线程",
|
||||
"mining_threads_input_tooltip": "输入精确的线程数(按 Enter 应用)",
|
||||
"mining_threads_minus_tooltip": "减少线程",
|
||||
"mining_threads_plus_tooltip": "增加线程",
|
||||
"mining_to_save": "保存",
|
||||
"mining_today": "今天",
|
||||
"mining_uptime": "运行时间",
|
||||
@@ -1027,6 +1419,13 @@
|
||||
"peers_upper": "节点",
|
||||
"peers_version": "版本",
|
||||
"pending": "待处理",
|
||||
"picker_dat_count": "此文件夹中有 %d 个钱包文件",
|
||||
"picker_dat_none": "此文件夹中没有直接存放的钱包文件",
|
||||
"picker_empty": "此文件夹没有子文件夹或钱包文件。",
|
||||
"picker_home": "主目录",
|
||||
"picker_select": "扫描此文件夹",
|
||||
"picker_title": "选择要扫描的文件夹",
|
||||
"picker_up": "上一级",
|
||||
"pin_change_desc": "更改您的解锁 PIN 码。您需要当前 PIN 码和新 PIN 码。",
|
||||
"pin_confirm_new_label": "确认新 PIN 码:",
|
||||
"pin_current_label": "当前 PIN 码:",
|
||||
@@ -1104,6 +1503,8 @@
|
||||
"qr_failed": "无法生成二维码",
|
||||
"qr_title": "二维码",
|
||||
"qr_unavailable": "二维码不可用",
|
||||
"quick_receive": "快速接收",
|
||||
"quick_send": "快速发送",
|
||||
"ram_daemon_gb": "守护进程:%.1f GB (%s)",
|
||||
"ram_daemon_mb": "守护进程:%.0f MB (%s)",
|
||||
"ram_system_gb": "系统:%.1f / %.0f GB",
|
||||
@@ -1153,6 +1554,7 @@
|
||||
"rpc_connection": "RPC 连接...",
|
||||
"rpc_host": "RPC 主机",
|
||||
"rpc_pass": "密码",
|
||||
"rpc_plaintext_remote_warning": "远程 RPC 正在使用明文 HTTP。如果您的守护进程支持 TLS,请在 DRAGONX.conf 中添加 rpctls=1。",
|
||||
"rpc_port": "端口",
|
||||
"rpc_user": "用户名",
|
||||
"save": "保存",
|
||||
@@ -1160,6 +1562,7 @@
|
||||
"save_z_transactions": "将 Z 交易保存到列表",
|
||||
"sb_auth_failed": "认证失败 — 请检查 rpcuser/rpcpassword",
|
||||
"sb_block": "区块: %d",
|
||||
"sb_block_db_unreadable": "区块数据库无法读取——需要重建",
|
||||
"sb_building_witnesses": "正在设置见证",
|
||||
"sb_building_witnesses_pct": "正在重建见证 %.0f%%",
|
||||
"sb_connecting_daemon": "正在连接 dragonxd...",
|
||||
@@ -1175,6 +1578,7 @@
|
||||
"sb_dragonxd_stopped": "dragonxd 已停止",
|
||||
"sb_dragonxd_stopping": "正在停止 dragonxd...",
|
||||
"sb_extracting_sapling": "正在提取 Sapling 参数...",
|
||||
"sb_finishing_repair": "正在完成钱包修复——重新扫描中…",
|
||||
"sb_importing_keys": "正在导入密钥",
|
||||
"sb_loading_config": "正在加载配置...",
|
||||
"sb_mining_hs": "%.1f H/s",
|
||||
@@ -1184,6 +1588,7 @@
|
||||
"sb_net_mhs": "网络: %.2f MH/s",
|
||||
"sb_no_conf": "未找到 DRAGONX.conf",
|
||||
"sb_peers": "节点: %zu",
|
||||
"sb_plaintext_remote_blocked": "拒绝以明文形式向远程主机发送 RPC 凭据。请在 DRAGONX.conf 中添加 rpcallowplaintext=1 以允许此操作,或启用 rpctls=1 使用 TLS。",
|
||||
"sb_rescanning": "重新扫描",
|
||||
"sb_rescanning_pct": "重新扫描 %.0f%%",
|
||||
"sb_restarting_daemon": "正在重启守护进程...",
|
||||
@@ -1195,14 +1600,55 @@
|
||||
"sb_waiting_config": "等待守护进程配置...",
|
||||
"sb_waiting_daemon": "等待 dragonxd 启动...",
|
||||
"sb_waiting_daemon_err": "等待 dragonxd — %s",
|
||||
"sb_wallet_needs_recovery": "可进行钱包修复",
|
||||
"sb_warming_up": "正在预热...",
|
||||
"sb_witness_cache": "正在重建见证",
|
||||
"scale_effects": "缩放与效果",
|
||||
"screenshot_open_dir": "打开位置",
|
||||
"screenshot_sweep": "运行截图批处理",
|
||||
"screenshot_sweep_desc": "遍历每个标签页的每一种主题,并将每一个的截图保存到配置目录 screenshots 文件夹下的各标签页子文件夹中(覆盖上一次的遍历)。运行几秒钟。",
|
||||
"screenshot_sweep_full": "完整 UI 遍历",
|
||||
"search_icons": "搜索图标...",
|
||||
"search_placeholder": "搜索...",
|
||||
"sec_changing_passphrase": "正在更改密码短语…",
|
||||
"sec_changing_pin": "正在更改 PIN…",
|
||||
"sec_couldnt_lock_wallet": "无法锁定钱包——它仍处于解锁状态。请检查守护进程连接。",
|
||||
"sec_encrypted_backup_suffix": "\n加密备份:wallet.dat.encrypted.bak",
|
||||
"sec_encrypting_wallet": "正在加密钱包…",
|
||||
"sec_encryption_did_not_complete": "钱包加密未完成——你的钱包尚未加密。请打开设置以完成加密。",
|
||||
"sec_encryption_failed_prefix": "加密失败:",
|
||||
"sec_failed_prefix": "失败:",
|
||||
"sec_failed_to_create_vault": "创建保险库失败",
|
||||
"sec_importing_keys_rescanning": "正在导入密钥并重新扫描区块链——期间钱包仍可使用",
|
||||
"sec_incorrect_current_pin": "当前 PIN 错误",
|
||||
"sec_incorrect_passphrase_decrypt": "密码短语错误",
|
||||
"sec_incorrect_passphrase_pin_setup": "密码短语错误",
|
||||
"sec_incorrect_pin_remove": "PIN 错误",
|
||||
"sec_internal_error_change_pin": "内部错误",
|
||||
"sec_internal_error_remove_pin": "内部错误",
|
||||
"sec_mode_passphrase": " 密码短语",
|
||||
"sec_not_connected_to_daemon": "未连接到守护进程",
|
||||
"sec_not_connected_to_daemon_pin": "未连接到守护进程",
|
||||
"sec_passphrase_changed_successfully": "密码短语更改成功",
|
||||
"sec_pin_changed_successfully": "PIN 更改成功",
|
||||
"sec_pin_removed": "PIN 已移除",
|
||||
"sec_pin_set_successfully": "PIN 设置成功",
|
||||
"sec_restart_daemon_for_encryption": "请重启守护进程以使加密生效。",
|
||||
"sec_too_many_attempts_wait": "尝试次数过多。请等待 %.0f 秒…",
|
||||
"sec_total_elapsed_fmt": "总耗时:%d 分 %02d 秒",
|
||||
"sec_unlock_button": "解锁",
|
||||
"sec_unlock_failed_prefix": "解锁失败:",
|
||||
"sec_unlocking_fmt": "正在解锁%s",
|
||||
"sec_use_passphrase_instead": "改用密码短语",
|
||||
"sec_use_pin_instead": "改用 PIN",
|
||||
"sec_verifying_passphrase": "正在验证密码短语…",
|
||||
"sec_verifying_pin": "正在验证 PIN…",
|
||||
"sec_wallet_decrypted_all_keys_imported": "钱包解密成功!所有密钥已导入。",
|
||||
"sec_wallet_encrypted_and_pin_set": "钱包已加密并已设置 PIN",
|
||||
"sec_wallet_encrypted_but_pin_vault_failed": "钱包已加密,但 PIN 保险库创建失败",
|
||||
"sec_wallet_encrypted_restarting_daemon": "钱包已加密。正在重启守护进程…",
|
||||
"sec_wallet_encrypted_successfully": "钱包加密成功",
|
||||
"sec_wallet_locked_title": "钱包已锁定",
|
||||
"security": "安全",
|
||||
"seed_backup_button": "助记词",
|
||||
"seed_backup_close": "关闭",
|
||||
@@ -1226,6 +1672,7 @@
|
||||
"send_amount": "金额",
|
||||
"send_amount_details": "金额详情",
|
||||
"send_amount_upper": "金额",
|
||||
"send_available_note": "可用",
|
||||
"send_cannot_send_now": "现在无法发送 — 请检查连接、同步和可用余额。",
|
||||
"send_clear_fields": "清除所有表单字段?",
|
||||
"send_contacts_button": "从联系人中选择",
|
||||
@@ -1261,6 +1708,7 @@
|
||||
"send_tooltip_not_connected": "未连接到守护进程",
|
||||
"send_tooltip_select_source": "请先选择来源地址",
|
||||
"send_tooltip_syncing": "请等待区块链同步",
|
||||
"send_tooltip_view_only": "仅查看地址 — 无花费密钥,无法发送",
|
||||
"send_total": "合计",
|
||||
"send_transaction": "发送交易",
|
||||
"send_tx_failed": "交易失败",
|
||||
@@ -1280,16 +1728,16 @@
|
||||
"sent_filter": "已发送",
|
||||
"sent_type": "已发送",
|
||||
"sent_upper": "已发送",
|
||||
"set_label": "设置标签...",
|
||||
"set_label": "设置标签",
|
||||
"settings": "设置",
|
||||
"settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。",
|
||||
"settings_acrylic_level": "亚克力级别:",
|
||||
"settings_address_book": "地址簿...",
|
||||
"settings_address_book": "地址簿…",
|
||||
"settings_auto_detected": "从 DRAGONX.conf 自动检测",
|
||||
"settings_auto_lock": "自动锁定",
|
||||
"settings_auto_shield_desc": "自动将透明资金转移到屏蔽地址",
|
||||
"settings_auto_shield_funds": "自动屏蔽透明资金",
|
||||
"settings_backup": "备份...",
|
||||
"settings_backup": "备份…",
|
||||
"settings_block_explorer_urls": "区块浏览器网址",
|
||||
"settings_builtin": "内置",
|
||||
"settings_change_passphrase": "更改密码",
|
||||
@@ -1311,19 +1759,20 @@
|
||||
"settings_encrypt_first_pin": "请先加密钱包以启用 PIN",
|
||||
"settings_encrypt_wallet": "加密钱包",
|
||||
"settings_explorer_hint": "URL 应包含尾部斜杠。将自动附加 txid/地址。",
|
||||
"settings_export_all": "全部导出...",
|
||||
"settings_export_csv": "导出 CSV...",
|
||||
"settings_export_key": "导出密钥...",
|
||||
"settings_export_all": "全部导出…",
|
||||
"settings_export_csv": "导出 CSV…",
|
||||
"settings_export_key": "导出密钥…",
|
||||
"settings_gradient_bg": "渐变背景",
|
||||
"settings_gradient_desc": "用平滑渐变替换纹理背景",
|
||||
"settings_idle_after": "之后",
|
||||
"settings_import_key": "导入私钥...",
|
||||
"settings_import_viewkey": "导入查看密钥...",
|
||||
"settings_import_key": "导入私钥…",
|
||||
"settings_import_viewkey": "导入查看密钥…",
|
||||
"settings_language_note": "注意:部分文本需要重启才能更新",
|
||||
"settings_lock_now": "立即锁定",
|
||||
"settings_locked": "已锁定",
|
||||
"settings_merge_to_address": "合并到地址...",
|
||||
"settings_merge_to_address": "合并到地址…",
|
||||
"settings_noise_opacity": "噪点不透明度:",
|
||||
"settings_not_connected": "未连接到守护进程",
|
||||
"settings_not_encrypted": "未加密",
|
||||
"settings_not_found": "未找到",
|
||||
"settings_open_app_dir": "打开应用文件夹",
|
||||
@@ -1334,22 +1783,27 @@
|
||||
"settings_privacy": "隐私",
|
||||
"settings_quick_unlock_pin": "快速解锁 PIN",
|
||||
"settings_reduce_transparency": "降低透明度",
|
||||
"settings_reloaded": "已从磁盘重新加载设置",
|
||||
"settings_remove_encryption": "移除加密",
|
||||
"settings_remove_pin": "移除 PIN",
|
||||
"settings_request_payment": "请求付款...",
|
||||
"settings_request_payment": "请求付款…",
|
||||
"settings_rescan_desc": "重新扫描区块链以查找丢失的交易",
|
||||
"settings_restart_daemon": "重启守护进程",
|
||||
"settings_rpc_connection": "RPC 连接",
|
||||
"settings_rpc_error_prefix": "RPC 错误:",
|
||||
"settings_rpc_note": "注意:连接设置通常从 DRAGONX.conf 自动检测",
|
||||
"settings_rpc_ok": "RPC 连接正常",
|
||||
"settings_save_shielded_desc": "将 z-addr 交易存储在本地文件中以供查看",
|
||||
"settings_save_shielded_local": "将屏蔽交易历史保存到本地",
|
||||
"settings_saved": "设置已保存",
|
||||
"settings_set_pin": "设置 PIN",
|
||||
"settings_shield_mining": "屏蔽挖矿...",
|
||||
"settings_shield_mining": "屏蔽挖矿…",
|
||||
"settings_solid_colors_desc": "使用纯色代替模糊效果(无障碍功能)",
|
||||
"settings_theme_refreshed": "主题列表已刷新",
|
||||
"settings_tor_desc": "通过 Tor 路由所有连接以增强隐私",
|
||||
"settings_unlocked": "已解锁",
|
||||
"settings_use_tor_network": "使用 Tor 进行网络连接",
|
||||
"settings_validate_address": "验证地址...",
|
||||
"settings_validate_address": "验证地址…",
|
||||
"settings_visual_effects": "视觉效果",
|
||||
"settings_wallet_file_size": "钱包文件大小:%s",
|
||||
"settings_wallet_info": "钱包信息",
|
||||
@@ -1357,6 +1811,8 @@
|
||||
"settings_wallet_maintenance": "钱包维护",
|
||||
"settings_wallet_not_found": "未找到钱包文件",
|
||||
"settings_wallet_size_label": "钱包大小:",
|
||||
"settings_ztx_cleared": "Z 交易历史记录已清除",
|
||||
"settings_ztx_not_found": "未找到历史记录文件",
|
||||
"setup_wizard": "设置向导",
|
||||
"share": "分享",
|
||||
"shield_check_status": "检查状态",
|
||||
@@ -1404,17 +1860,34 @@
|
||||
"status": "状态",
|
||||
"stop_external": "停止外部守护进程",
|
||||
"stop_mining": "停止挖矿",
|
||||
"stratum_allowip_hint": "允许来自指定 IP 或 CIDR 的矿工连接(留空 = 仅限本机)",
|
||||
"stratum_expose_warn": "会向您允许的网络开放挖矿端口——仅在受信任的局域网中使用。",
|
||||
"stratum_host": "托管矿池(stratum)",
|
||||
"stratum_host_hint": "矿工可通过 RandomX stratum 矿机连接本机的 22769 端口(RPC 端口 + 1000)。需重启节点以生效。",
|
||||
"stratum_host_section": "矿池托管",
|
||||
"submitting_transaction": "正在提交交易...",
|
||||
"success": "成功",
|
||||
"summary": "摘要",
|
||||
"sweep_button": "归集",
|
||||
"sweep_caveat": "导入密钥以签署一笔将其所有资金转移到您地址的交易。密钥将保留在钱包中,余额为零。",
|
||||
"sweep_current_theme_only": "仅当前主题",
|
||||
"sweep_dest_label": "将归集资金发送到",
|
||||
"sweep_dest_new": "新的屏蔽地址(推荐)",
|
||||
"sweep_done": "完成 — 资金已归集到您的地址。",
|
||||
"sweep_to": "归集到:",
|
||||
"sweep_toggle": "归集到我的钱包(不保留密钥)",
|
||||
"sweep_tx": "交易:",
|
||||
"swin_connection_failed": "连接失败:",
|
||||
"swin_connection_successful": "连接成功!\ndragonxd 版本:",
|
||||
"swin_invalid_suffix": "(无效)",
|
||||
"swin_no_history_file_found": "未找到历史文件",
|
||||
"swin_rescan_failed": "重新扫描失败:",
|
||||
"swin_rescan_started_from_block": "重新扫描已开始,起始区块 ",
|
||||
"swin_rescan_to": " 至 ",
|
||||
"swin_rpc_client_not_initialized": "RPC 客户端未初始化",
|
||||
"swin_settings_saved": "设置已保存",
|
||||
"swin_theme_list_refreshed": "主题列表已刷新",
|
||||
"swin_ztx_history_cleared": "Z 交易历史已清除",
|
||||
"switch_corrupt_body": "此钱包似乎已损坏——节点无法打开它。请从备份恢复、重新创建,或尝试修复。",
|
||||
"switch_corrupt_repair": "尝试修复(salvage)",
|
||||
"switch_progress_background": "在后台继续",
|
||||
@@ -1439,6 +1912,7 @@
|
||||
"theme": "主题",
|
||||
"theme_effects": "主题效果",
|
||||
"theme_language": "主题与语言",
|
||||
"tile_click_to_open": "点击打开",
|
||||
"time_days_ago": "%d 天前",
|
||||
"time_hours_ago": "%d 小时前",
|
||||
"time_minutes_ago": "%d 分钟前",
|
||||
@@ -1453,7 +1927,9 @@
|
||||
"to_upper": "至",
|
||||
"tools": "工具",
|
||||
"tools_actions": "工具与操作...",
|
||||
"tools_actions_hdr": "工具与操作",
|
||||
"total": "合计",
|
||||
"total_balance_label": "总余额",
|
||||
"transaction_id": "交易 ID",
|
||||
"transaction_sent": "交易发送成功",
|
||||
"transaction_sent_msg": "交易已发送!",
|
||||
@@ -1475,7 +1951,7 @@
|
||||
"tt_auto_shield": "自动将透明余额转移到屏蔽地址以增强隐私",
|
||||
"tt_backup": "创建 wallet.dat 的备份",
|
||||
"tt_block_explorer": "在浏览器中打开 DragonX 区块浏览器",
|
||||
"tt_blur": "模糊程度(0%% = 关闭,100%% = 最大)",
|
||||
"tt_blur": "模糊程度(0% = 关闭,100% = 最大)",
|
||||
"tt_change_pass": "更改钱包加密密码",
|
||||
"tt_change_pin": "更改您的解锁 PIN",
|
||||
"tt_chat_bubble_accent": "你发出的消息气泡的强调色(或跟随当前主题)",
|
||||
@@ -1538,7 +2014,7 @@
|
||||
"tt_low_spec": "禁用所有重度视觉效果\\n快捷键:Ctrl+Shift+Down",
|
||||
"tt_merge": "将多个 UTXO 合并到一个地址",
|
||||
"tt_mine_idle": "系统空闲时自动开始挖矿\\n(无键盘/鼠标输入)",
|
||||
"tt_noise": "颗粒纹理强度(0%% = 关闭,100%% = 最大)",
|
||||
"tt_noise": "颗粒纹理强度(0% = 关闭,100% = 最大)",
|
||||
"tt_open_app_dir": "在文件管理器中打开 ObsidianDragon 文件夹(设置、主题、日志)",
|
||||
"tt_open_data_dir": "在文件管理器中打开包含您钱包和区块链数据的文件夹",
|
||||
"tt_open_dir": "点击在文件管理器中打开",
|
||||
@@ -1572,14 +2048,17 @@
|
||||
"tt_simple_bg": "使用简单渐变作为背景\\n快捷键:Ctrl+Up",
|
||||
"tt_simple_bg_alt": "使用主题背景图像的渐变版本\\n快捷键:Ctrl+Up",
|
||||
"tt_stop_external": "适用于连接到在此钱包\\n外部启动的守护进程",
|
||||
"tt_stratum_host": "在此节点上运行 RandomX stratum 矿池服务器,让其他 RandomX 矿工可以连接到本机。需要 v1.3.0 及以上版本的节点,并需重启节点以生效。",
|
||||
"tt_sweep_current_theme_only": "仅扫描当前主题,而不是遍历每个主题",
|
||||
"tt_test_conn": "验证与守护进程的 RPC 连接",
|
||||
"tt_theme_effects": "每个主题的闪烁、发光、色调循环",
|
||||
"tt_theme_hotkey": "快捷键:Ctrl+左/右箭头切换主题",
|
||||
"tt_tor": "通过 Tor 网络路由守护进程连接以实现匿名",
|
||||
"tt_tx_url": "在区块浏览器中查看交易的基础 URL",
|
||||
"tt_ui_opacity": "卡片和侧边栏不透明度(100%% = 完全不透明,越低越透明)",
|
||||
"tt_ui_opacity": "卡片和侧边栏不透明度(100% = 完全不透明,越低越透明)",
|
||||
"tt_validate": "检查 DragonX 地址是否有效",
|
||||
"tt_verbose": "将详细连接诊断、守护进程状态\\n和端口所有者信息记录到控制台选项卡",
|
||||
"tt_wallet_size_warn": "屏蔽钱包会随每个票据的见证数据增长——将多个票据合并到一个地址可以缩小体积。请先备份。",
|
||||
"tt_wallets_button": "列出您的钱包文件并在它们之间切换",
|
||||
"tt_website": "打开 DragonX 网站",
|
||||
"tt_window_opacity": "背景不透明度(越低 = 桌面透过窗口可见)",
|
||||
@@ -1630,6 +2109,7 @@
|
||||
"validate_not_mine": "不属于此钱包",
|
||||
"validate_ownership": "所有权:",
|
||||
"validate_results": "结果:",
|
||||
"validate_results_placeholder": "结果将显示在此处",
|
||||
"validate_shielded_type": "屏蔽(z 地址)",
|
||||
"validate_status": "状态:",
|
||||
"validate_title": "验证地址",
|
||||
@@ -1645,8 +2125,68 @@
|
||||
"view_on_explorer": "在浏览器中查看",
|
||||
"waiting_for_daemon": "等待守护进程连接...",
|
||||
"wallet": "钱包",
|
||||
"wallet_degraded_notify": "您的钱包以受限功能模式打开:现有资金安全且可用,但无法创建新地址或进行屏蔽操作。请备份助记词并用它恢复钱包以完全修复。",
|
||||
"wallet_diagnostics_hdr": "诊断",
|
||||
"wallet_empty": "您的钱包是空的",
|
||||
"wallet_empty_hint": "切换到接收页面获取您的地址并开始接收资金。",
|
||||
"wallet_options_hdr": "选项",
|
||||
"wallet_rebuild_failed": "重建未能生成有效的钱包,因此未做任何更改。您的钱包未受影响。",
|
||||
"wallet_rebuild_install_failed": "无法安装重建后的钱包;您当前的钱包已保留原样。",
|
||||
"wallet_rebuild_no_helper": "此版本不提供钱包重建助手。请使用还原功能,或手动重建。",
|
||||
"wallet_rebuild_no_source": "找不到可读取的钱包用于重建。未做任何更改。",
|
||||
"wallet_rebuild_ok": "钱包数据库已重建——节点正在加载并重新扫描以恢复您的余额。",
|
||||
"wallet_rebuild_started": "正在修复您的钱包文件——请不要关闭此窗口。",
|
||||
"wallet_recovered_body": "没有任何内容被删除。您的原始钱包仍以带日期的备份文件形式保存在您的电脑上,下面的每个选项都只会复制或重命名文件——绝不会删除文件。您的密钥永远不会重新生成,只会被重新读取。\n\n现在加载的修复副本可能会缺少最近的一些交易,因此在您完成下方操作并重新扫描之前,余额可能会显示偏低。",
|
||||
"wallet_recovered_dismiss": "暂不处理——保留修复副本",
|
||||
"wallet_recovered_dismiss_sub": "不会更改任何文件。您可以随时从状态栏重新打开此提示;无论如何,您的原始文件都会安全地保留备份。",
|
||||
"wallet_recovered_notify": "您的钱包文件需要修复——原始文件已安全备份。打开应用查看您的选项。",
|
||||
"wallet_recovered_open_folder": "显示文件",
|
||||
"wallet_recovered_open_folder_sub": "打开钱包数据文件夹,方便您自行查看备份文件——不会更改任何内容。",
|
||||
"wallet_recovered_rebuild": "自动修复(推荐)",
|
||||
"wallet_recovered_rebuild_sub": "从您最完整的钱包文件中重新读取每条可恢复的记录,并写入一个全新的干净文件,然后重启。这是最彻底的选项——无论如何,您当前的文件都会保留为带日期的备份。",
|
||||
"wallet_recovered_restore": "改为还原原始文件",
|
||||
"wallet_recovered_restore_sub": "将您最大的未改动备份原样放回,然后重新扫描——速度稍快,但完整程度仅限于该文件本身。无论如何,您当前的文件都会保留为带日期的备份。",
|
||||
"wallet_recovered_safety": "您的币是安全的。",
|
||||
"wallet_recovered_title": "您的钱包文件需要快速修复",
|
||||
"wallet_recovered_warn": "应用启动时发现您的钱包文件未通过一致性检查——这通常发生在应用更新或非正常关闭之后。应用已保护您的数据:它将旧文件另存一旁,并加载了一个修复副本,因此您不会被卡住。",
|
||||
"wallet_recovery_decide_later": "稍后决定",
|
||||
"wallet_recovery_decide_later_tip": "关闭此提示并保留当前已加载的副本。不会更改任何内容,您可以随时进行修复——状态栏会保留“可修复钱包”链接。",
|
||||
"wallet_recovery_details_label": "显示技术细节",
|
||||
"wallet_recovery_done": "完成",
|
||||
"wallet_recovery_failure_body": "修复已运行,但结果未通过验证(无法读取,或没有地址),因此在替换任何内容之前已被自动丢弃。您的钱包与点击前完全一样——磁盘上没有任何改动。",
|
||||
"wallet_recovery_failure_title": "修复未成功",
|
||||
"wallet_recovery_files_detail": "您的原始钱包仍在这里——应用已将其重命名为数据文件夹中带日期的备份(wallet.<数字>.bak),未删除任何内容。“修复”会从您最完整的钱包中读取每条记录到一个全新的干净文件中。“还原”会将您最大的备份原样复制回来。当前加载的副本也会保留为备份。",
|
||||
"wallet_recovery_files_label": "我的文件会怎样?",
|
||||
"wallet_recovery_notnow_short": "暂不",
|
||||
"wallet_recovery_other_options": "其他选项",
|
||||
"wallet_recovery_rebuild_card": "自动修复",
|
||||
"wallet_recovery_rebuild_card_desc": "将每条可恢复的记录读取到一个干净的文件中,然后重启。这是最彻底的选项。",
|
||||
"wallet_recovery_recommended": "推荐",
|
||||
"wallet_recovery_repair_go": "修复",
|
||||
"wallet_recovery_rescan_body": "正在重新扫描区块链以重建您的余额和交易历史。这可能需要几分钟——您无需进行任何操作,请勿在此过程中重启节点。",
|
||||
"wallet_recovery_rescan_elapsed": "已运行 %s",
|
||||
"wallet_recovery_rescan_size": "已重建的钱包目前为 %s,且仍在填充中",
|
||||
"wallet_recovery_rescan_slow": "这比平时耗时更长,对于较大的钱包这是正常现象。可以放心让它继续运行——您可以在“高级 ▸ 控制台”下查看进度。",
|
||||
"wallet_recovery_rescan_title": "正在完成钱包修复",
|
||||
"wallet_recovery_restore_card": "还原原始文件",
|
||||
"wallet_recovery_restore_card_desc": "将您最大的未改动备份原样放回——完整程度仅限于该文件本身。",
|
||||
"wallet_recovery_restore_go": "还原",
|
||||
"wallet_recovery_success_body": "应用已加载您修复后的钱包,正在重新扫描以统计余额——这可能需要几分钟。它会读取所能读取的每条记录,但在极少数损坏文件的情况下,恢复的地址可能会比之前略少——或偶尔略多。重新扫描完成后,请检查您的余额和历史记录是否正确。",
|
||||
"wallet_recovery_success_restore": "您最大的备份文件已放回原位并正在加载,应用正在重新扫描。它是原样还原的,因此完整程度与该文件本身一致——重新扫描完成后请检查您的余额。",
|
||||
"wallet_recovery_try_other": "尝试其他选项",
|
||||
"wallet_recovery_whats_happened": "发生了什么?",
|
||||
"wallet_recovery_working_label": "处理中",
|
||||
"wallet_restore_bad_backup": "备份钱包文件似乎无法读取,因此未进行还原——您当前的钱包未被更改。请改用您自己的备份进行还原。",
|
||||
"wallet_restore_busy": "节点正忙于重启——请稍后再试。",
|
||||
"wallet_restore_copy_failed": "无法安装备份钱包;您当前的钱包保持不变。",
|
||||
"wallet_restore_move_failed": "无法将当前钱包移至一旁——未做任何更改。",
|
||||
"wallet_restore_no_backup": "未找到可还原的 wallet.<时间戳>.bak 文件。未做任何更改。",
|
||||
"wallet_restore_no_restart": "您的原始钱包已还原,但节点未重启——请从设置中启动节点。",
|
||||
"wallet_restore_ok": "原始钱包已还原。节点正在加载中。",
|
||||
"wallet_restore_started": "正在还原您的原始文件——请勿关闭此窗口。",
|
||||
"wallet_restore_stop_failed": "节点未能及时停止,因此未做任何更改。请重试。",
|
||||
"wallet_size_consolidate": "合并笔记…",
|
||||
"wallet_size_warn": "此钱包文件较大。合并您的笔记可以抑制其进一步增长。",
|
||||
"wallets_active": "活跃",
|
||||
"wallets_add": "添加文件夹",
|
||||
"wallets_add_folder": "同时扫描另一个文件夹中的钱包文件:",
|
||||
@@ -1689,6 +2229,7 @@
|
||||
"wallets_open_inplace_tt": "在原位置打开此钱包 — 链接到数据目录(不复制)",
|
||||
"wallets_remove_folder": "停止扫描此文件夹",
|
||||
"wallets_reveal": "打开文件夹",
|
||||
"wallets_scan_folder": "扫描其他文件夹以查找钱包…",
|
||||
"wallets_scanned_folders": "已扫描的文件夹:",
|
||||
"wallets_sort_addresses": "地址",
|
||||
"wallets_sort_asc": "升序(最早/最少/最小优先)",
|
||||
@@ -1758,18 +2299,30 @@
|
||||
"wiz_welcome_sub": "只需几项快速设置,即可启用您的全节点。",
|
||||
"wiz_welcome_title": "欢迎使用 ObsidianDragon!",
|
||||
"wizard_daemon_start_failed": "启动守护进程失败 — 将自动重试",
|
||||
"xmrig_active_tt": "当前已安装(使用中)的矿工程序",
|
||||
"xmrig_browse_releases": "浏览所有版本…",
|
||||
"xmrig_checking": "正在检查最新的矿工程序…",
|
||||
"xmrig_current": "当前:",
|
||||
"xmrig_downgrade_note": "安装不同版本的矿工程序会替换当前版本;请先停止挖矿,更改将在下次启动时生效。",
|
||||
"xmrig_download_install": "下载并安装",
|
||||
"xmrig_downloading": "正在下载…",
|
||||
"xmrig_install_this": "安装此版本",
|
||||
"xmrig_installed": "已安装:",
|
||||
"xmrig_installed_ok": "矿工程序已安装",
|
||||
"xmrig_installing": "正在安装…",
|
||||
"xmrig_latest": "最新:",
|
||||
"xmrig_latest_badge": "最新",
|
||||
"xmrig_latest_tt": "可用的最新版本",
|
||||
"xmrig_loading_releases": "正在加载发行版…",
|
||||
"xmrig_no_notes": "此版本没有发行说明。",
|
||||
"xmrig_none": "无",
|
||||
"xmrig_prerelease_tt": "预发布/测试版本",
|
||||
"xmrig_reinstall": "重新安装",
|
||||
"xmrig_released": "发布日期:",
|
||||
"xmrig_releases": "xmrig 版本",
|
||||
"xmrig_status_current": "此版本已安装",
|
||||
"xmrig_status_latest": "最新版本",
|
||||
"xmrig_status_older": "早于您已安装的版本",
|
||||
"xmrig_stop_mining_first": "更新矿工程序前请先停止挖矿。",
|
||||
"xmrig_unavailable_body": "此平台没有可用的矿工构建版本。",
|
||||
"xmrig_unavailable_title": "矿工更新不可用",
|
||||
|
||||
@@ -965,6 +965,11 @@ edition-label = { position = 120 }
|
||||
link-button = { width = 100, font = "button-sm" }
|
||||
close-button = { width = 120, font = "button", align = "center" }
|
||||
|
||||
[dialogs.faq]
|
||||
width = 860.0
|
||||
height = 660.0
|
||||
window = { width = 860, height = 660 }
|
||||
|
||||
[dialogs.settings]
|
||||
width = 600.0
|
||||
height = 550.0
|
||||
|
||||
+1236
-338
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <deque>
|
||||
#include <condition_variable>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "data/address_book.h"
|
||||
@@ -71,14 +72,28 @@ enum class EncryptDialogPhase {
|
||||
Done // Finished — close dialog
|
||||
};
|
||||
|
||||
// A status string written by a background/worker thread and read every frame by the UI thread. Its
|
||||
// operator= locks, so all the plain `x = "..."` assignment sites stay unchanged; readers call get()
|
||||
// for a consistent per-frame snapshot instead of racing a non-atomic std::string. (M-05, L-06)
|
||||
class GuardedStatus {
|
||||
public:
|
||||
GuardedStatus() = default;
|
||||
GuardedStatus& operator=(std::string v) { std::lock_guard<std::mutex> lk(m_); v_ = std::move(v); return *this; }
|
||||
std::string get() const { std::lock_guard<std::mutex> lk(m_); return v_; }
|
||||
private:
|
||||
mutable std::mutex m_;
|
||||
std::string v_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Main application class
|
||||
*
|
||||
*
|
||||
* Manages application state, RPC connection, and coordinates UI rendering.
|
||||
*/
|
||||
class App {
|
||||
public:
|
||||
App();
|
||||
void wipeSecrets(); // scrub all resident secret buffers; called from ~App() AND the forced-exit path (L-05)
|
||||
~App();
|
||||
|
||||
// Non-copyable
|
||||
@@ -148,6 +163,14 @@ public:
|
||||
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
|
||||
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
|
||||
|
||||
// Daemon (v1.3.0+) coinbase auto-shield status, from z_autoshieldstatus. "Not probed" / all-false on
|
||||
// pre-1.3.0 daemons (no such RPC) — callers treat that as "the wallet handles auto-shield itself".
|
||||
bool daemonAutoShieldProbed() const { return daemon_autoshield_probed_; }
|
||||
bool daemonAutoShieldActive() const { return daemon_autoshield_active_; }
|
||||
const std::string& daemonAutoShieldAddress() const { return daemon_autoshield_address_; }
|
||||
const std::string& daemonAutoShieldDisabledReason() const { return daemon_autoshield_disabled_reason_; }
|
||||
bool daemonAutoShieldSeedRecoverable() const { return daemon_autoshield_seed_recoverable_; }
|
||||
|
||||
// W7 QoL: a plaintext support-diagnostics snapshot (version, variant, daemon/RPC/wallet/log state)
|
||||
// for the "Copy diagnostics" action. Contains no secrets.
|
||||
std::string buildDiagnosticsReport();
|
||||
@@ -160,6 +183,21 @@ public:
|
||||
*/
|
||||
void renderShutdownScreen();
|
||||
|
||||
/**
|
||||
* @brief Tail the last N lines of the daemon's debug.log (best-effort, reads only the file tail).
|
||||
* Fallback for the shutdown screen when we have no captured stdout — e.g. an external daemon we
|
||||
* attached to rather than spawned — so the user can still see the node flushing/exiting.
|
||||
*/
|
||||
std::vector<std::string> tailDaemonDebugLog(int maxLines) const;
|
||||
|
||||
// True when the daemon's debug.log shows an in-progress Sapling witness-cache rebuild (best-effort
|
||||
// heuristic). Stopping the daemon during one discards it and forces a multi-minute redo next launch.
|
||||
bool daemonWitnessRebuildActive() const;
|
||||
// Whether beginShutdown() should pause and confirm before stopping the daemon (rebuild in progress).
|
||||
bool shouldConfirmDaemonStop() const;
|
||||
// The "node is rebuilding — stop anyway / keep running / cancel" modal, rendered from render().
|
||||
void renderDaemonStopConfirm();
|
||||
|
||||
/**
|
||||
* @brief Render loading overlay in content area while daemon is starting/syncing
|
||||
* @param contentH Height of the content area child window
|
||||
@@ -229,7 +267,7 @@ public:
|
||||
bool isConnected() const { return state_.connected; }
|
||||
int getBlockHeight() const { return state_.sync.blocks; }
|
||||
const std::string& getConnectionStatus() const { return connection_status_; }
|
||||
const std::string& getDaemonStatus() const { return daemon_status_; }
|
||||
std::string getDaemonStatus() const; // thread-safe copy (daemon_status_ is written off-thread)
|
||||
|
||||
// Balance info (convenience wrappers)
|
||||
double getShieldedBalance() const { return state_.shielded_balance; }
|
||||
@@ -356,6 +394,10 @@ public:
|
||||
|
||||
// Force refresh
|
||||
void refreshNow();
|
||||
// Called on window restore: drop the daemon-output backlog that accumulated while minimized (the
|
||||
// per-frame update loop was paused), so a background witness rebuild that started+finished during
|
||||
// the minimize isn't parsed in one batch and mistaken for a completed rescan (spurious toast).
|
||||
void skipDaemonOutputBacklog();
|
||||
void refreshMiningInfo();
|
||||
void refreshPeerInfo();
|
||||
void refreshMarketData();
|
||||
@@ -393,6 +435,9 @@ public:
|
||||
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
|
||||
void startFullUiSweep();
|
||||
std::string screenshotFullDir() const;
|
||||
// Debug option: restrict either sweep to just the currently-active theme instead of cycling all.
|
||||
bool sweepCurrentThemeOnly() const { return sweep_current_theme_only_; }
|
||||
void setSweepCurrentThemeOnly(bool v) { sweep_current_theme_only_ = v; }
|
||||
bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
|
||||
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
|
||||
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
|
||||
@@ -418,6 +463,7 @@ public:
|
||||
return 0;
|
||||
}
|
||||
void showAboutDialog() { show_about_ = true; }
|
||||
void showFaqDialog() { show_faq_ = true; }
|
||||
|
||||
// Legacy tab compat — maps int to NavPage
|
||||
void setCurrentTab(int tab);
|
||||
@@ -715,6 +761,22 @@ private:
|
||||
// wallets. In-memory only (resets on app restart).
|
||||
std::map<std::string, std::int64_t> chat_seen_watermark_;
|
||||
|
||||
// ── Per-frame render caches (avoid O(N) recompute every frame; see the respective call sites) ──
|
||||
// Chat nav-badge unread count — recomputed only when the store revision changes or after a short
|
||||
// interval (mute/hide/seen changes don't bump the store). See App::chatUnreadCount().
|
||||
mutable std::uint64_t chat_unread_rev_ = ~0ull; // ~0 forces the first compute
|
||||
mutable int chat_unread_cached_ = 0;
|
||||
mutable double chat_unread_computed_at_ = 0.0;
|
||||
// Sidebar unconfirmed-tx badge — recomputed only when the tx list changes (keyed on last_tx_update +
|
||||
// size), not every frame. See App::render().
|
||||
std::int64_t sb_unconf_key_ts_ = -1;
|
||||
std::size_t sb_unconf_key_n_ = 0;
|
||||
int sb_unconf_count_ = 0;
|
||||
// Daemon-memory probe is expensive (/proc scan on Linux, popen on macOS); throttle it to ~1.5s so the
|
||||
// Mining tab's per-frame read doesn't hammer the OS. See App::getDaemonMemoryUsageMB().
|
||||
mutable double daemon_mem_cached_mb_ = 0.0;
|
||||
mutable double daemon_mem_probe_at_ = 0.0;
|
||||
|
||||
// ── 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
|
||||
@@ -756,6 +818,7 @@ private:
|
||||
// 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)
|
||||
double chat_note_scan_ms_ = 0.0; // measured cost of the last note scan (adaptive back-off)
|
||||
// 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.
|
||||
@@ -765,6 +828,9 @@ private:
|
||||
// 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
|
||||
// Feeds the SAME chat send-budget from an already-collected z_listunspent (the address refresh),
|
||||
// so the dedicated scan above is skipped while the address refresh is active (dedup — see #3).
|
||||
void updateChatNoteBudgetFromUnspent(const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes);
|
||||
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
|
||||
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||
@@ -786,6 +852,9 @@ public:
|
||||
int chatUnreadCount() const;
|
||||
// Mark a conversation read up to latestTs (called by the Chat tab while a thread is displayed).
|
||||
void markChatConversationSeen(const std::string& cid, std::int64_t latestTs);
|
||||
// Drop the seen-watermark for a conversation (used on revive-delete so a re-imported message — even one
|
||||
// whose stamped time predates the deleted thread's last message — still badges as unread).
|
||||
void forgetChatConversationSeen(const std::string& cid);
|
||||
private:
|
||||
// Provision the chat identity once the wallet seed is reachable+unlocked (per-tick, both
|
||||
// variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the
|
||||
@@ -798,6 +867,11 @@ private:
|
||||
// One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
|
||||
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
|
||||
void maybeRemindSeedBackup();
|
||||
void maybeWarnEmptyWalletWithFundedSiblings(); // full-node: empty active wallet + a funded sibling → warn once
|
||||
void maybeWarnLargeWallet(); // full-node: wallet.dat past bloat threshold → one-time toast + clickable alert
|
||||
void scanFundedSiblingsAsync(); // off-UI-thread probe of sibling wallet files
|
||||
static void scrubAndRemoveExport(const std::string& path); // zero + delete a plaintext key export (H-02)
|
||||
void sweepStaleDecryptExports(); // startup net: purge stale obsidiandecryptexport* files (H-02)
|
||||
|
||||
// Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
|
||||
void beginCreateSeedWallet(); // starts the isolated create on a background thread
|
||||
@@ -810,6 +884,7 @@ private:
|
||||
std::function<void(bool, const std::string&)> makeSweepCompletionCallback(bool resumed);
|
||||
void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest
|
||||
void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance
|
||||
void locateSweepTxid(); // recover a lost sweep txid (send to dest) -> Confirming gate
|
||||
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
|
||||
void provisionChatIdentityFromSecret(std::string secret);
|
||||
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr — chat IDENTITY (reply-to)
|
||||
@@ -828,6 +903,8 @@ 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)
|
||||
double chat_fast_scan_last_ = 0.0; // ImGui time of the last 0-conf fast scan (adaptive back-off)
|
||||
double chat_fast_scan_ms_ = 0.0; // measured cost of the last fast scan (z_listreceivedbyaddress)
|
||||
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;
|
||||
@@ -837,6 +914,16 @@ private:
|
||||
bool lite_startup_lock_checked_ = false;
|
||||
std::unique_ptr<daemon::DaemonController> daemon_controller_;
|
||||
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
||||
// Serialized async mining-control queue: xmrig start/stop (SIGTERM->SIGKILL->join, up to ~3s) run on
|
||||
// this dedicated FIFO thread instead of the render thread, so the UI never blocks and stop/start
|
||||
// ordering is preserved across the ~13 call sites. (M-03/L-06/L-08/L-09/L-13)
|
||||
std::thread mining_ctl_thread_;
|
||||
std::mutex mining_ctl_mutex_;
|
||||
std::condition_variable mining_ctl_cv_;
|
||||
std::deque<std::function<void()>> mining_ctl_queue_;
|
||||
bool mining_ctl_stop_ = false;
|
||||
void postMiningControl(std::function<void()> job); // enqueue a blocking xmrig op onto the FIFO thread
|
||||
void stopMiningControlThread(); // signal + join the control thread (shutdown)
|
||||
// Auto-balance runtime state (pool mining, full-node only). The service fetches
|
||||
// pool hashrates off-thread; the RNG drives the weighted-random pick.
|
||||
util::PoolStatsService pool_stats_service_;
|
||||
@@ -869,8 +956,13 @@ private:
|
||||
std::atomic<bool> shutting_down_{false};
|
||||
std::atomic<bool> shutdown_complete_{false};
|
||||
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
||||
std::string shutdown_status_;
|
||||
GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05)
|
||||
std::thread shutdown_thread_;
|
||||
// Confirm-before-stopping-daemon-mid-witness-rebuild guard (see beginShutdown / renderDaemonStopConfirm)
|
||||
bool pending_shutdown_confirm_ = false; // a quit is deferred, waiting to open the confirm modal
|
||||
bool daemon_stop_confirm_open_ = false; // the confirm modal is currently showing
|
||||
bool shutdown_confirmed_ = false; // user chose to proceed — bypass the guard on re-entry
|
||||
bool shutdown_keep_daemon_override_ = false; // user chose "keep node running" for this shutdown only
|
||||
float shutdown_timer_ = 0.0f;
|
||||
bool force_quit_confirm_ = false;
|
||||
std::chrono::steady_clock::time_point shutdown_start_time_;
|
||||
@@ -905,7 +997,33 @@ private:
|
||||
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
|
||||
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
||||
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
||||
bool wallet_degraded_ = false; // v1.3.0+ opened the wallet in DEGRADED mode (no new HD keys)
|
||||
bool wallet_degraded_warned_ = false; // guard: surface the degraded-mode notice once per session
|
||||
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
||||
// Complementary on-disk safety net for a salvage we DIDN'T witness this launch (happened on a prior
|
||||
// run, or under an external daemon whose startup output we never captured): if the active wallet loads
|
||||
// empty while a sibling wallet file in the datadir still holds keys, warn once so the user's funds
|
||||
// (likely in a wallet.<ts>.bak) aren't mistaken for loss. See maybeWarnEmptyWalletWithFundedSiblings().
|
||||
struct FundedSibling { std::string fileName; int transparentKeys = 0; int shieldedKeys = 0; };
|
||||
bool show_empty_wallet_warning_ = false; // auto-shown warning modal
|
||||
bool empty_wallet_warn_checked_ = false; // evaluated this wallet-open already (reset in onConnected)
|
||||
bool empty_wallet_scan_in_flight_ = false; // a sibling scan is running (main-thread only)
|
||||
bool empty_wallet_has_salvage_bak_ = false; // modal variant: a funded salvage .bak → offer Restore
|
||||
std::vector<FundedSibling> empty_wallet_funded_siblings_; // scan result (main-thread only)
|
||||
// The recovery dialog is the ONE authoritative surface: it stays open through the async rebuild/
|
||||
// restore, driven Offer → Working → Done/Failed (pumpWalletRestore sets the outcome). Presentation
|
||||
// only — the fund-safety file ops in rebuildWalletDatabase()/restoreOriginalWallet() are unchanged.
|
||||
enum class RecoveryPhase { Offer, Working, Done, Failed };
|
||||
RecoveryPhase recovery_phase_ = RecoveryPhase::Offer;
|
||||
int recovery_outcome_sev_ = 0; // 0 ok / 1 warn / 2 error, set at Done/Failed
|
||||
std::string recovery_outcome_msg_; // honest result string for the Done/Failed body
|
||||
bool recovery_last_action_rebuild_ = false; // which handler ran (for "try the other option")
|
||||
// After a successful repair the daemon restarts with a full rescan — minutes long, and it won't
|
||||
// answer RPC yet. This makes the loading overlay show a calm "finishing your wallet repair" screen
|
||||
// (instead of the generic "daemon stuck / RPC timeout / restart daemon" text) and suppresses the
|
||||
// daemon-crash toast. Set on repair success; cleared on connect (onConnected).
|
||||
bool post_recovery_rescan_ = false;
|
||||
double post_recovery_rescan_since_ = 0.0; // stamped on first overlay frame (ImGui::GetTime)
|
||||
// "Restore original wallet" background op: worker sets these under the mutex, pumpWalletRestore()
|
||||
// (main thread) shows the result. 0 = success, 1 = warning, 2 = error.
|
||||
std::mutex wallet_restore_mutex_;
|
||||
@@ -938,6 +1056,7 @@ private:
|
||||
bool show_demo_window_ = false;
|
||||
bool show_settings_ = false;
|
||||
bool show_about_ = false;
|
||||
bool show_faq_ = false;
|
||||
bool show_import_key_ = false;
|
||||
bool show_export_key_ = false;
|
||||
bool show_backup_ = false;
|
||||
@@ -951,6 +1070,7 @@ private:
|
||||
bool seed_backup_loading_ = false;
|
||||
bool seed_backup_no_mnemonic_ = false;
|
||||
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
|
||||
bool large_wallet_checked_ = false; // gate: stat wallet.dat for the bloat nudge once per launch
|
||||
|
||||
// Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
|
||||
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
|
||||
@@ -1004,7 +1124,13 @@ private:
|
||||
|
||||
// Embedded daemon state
|
||||
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
|
||||
std::string daemon_status_;
|
||||
// Written by the DaemonController state callback (which fires on EmbeddedDaemon's monitor thread)
|
||||
// AND read on the UI thread — so all access goes through set/getDaemonStatus() under this mutex.
|
||||
// getDaemonStatus() returns a COPY: a returned reference could dangle if the monitor thread
|
||||
// reassigned the string mid-read (data race / use-after-free).
|
||||
mutable std::mutex daemon_status_mutex_;
|
||||
std::string daemon_status_; // guarded by daemon_status_mutex_ — use set/getDaemonStatus()
|
||||
void setDaemonStatus(std::string s);
|
||||
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
|
||||
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
|
||||
|
||||
@@ -1044,6 +1170,22 @@ private:
|
||||
bool daemon_start_error_shown_ = false;
|
||||
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
|
||||
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
|
||||
// Sync-settle hysteresis + adaptive balance-poll throttle. Balance polling (z_gettotalbalance) is
|
||||
// O(mapWallet) and holds the daemon's cs_main, which starves block connection on a large shielded
|
||||
// wallet — so we keep the low-impact profile briefly after catching up, and back the balance poll
|
||||
// off in proportion to its own measured cost. See effectivelySyncing() / balanceRefreshDue().
|
||||
bool was_core_syncing_ = false; // previous Core-refresh sync state, to detect the caught-up edge
|
||||
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
|
||||
double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan
|
||||
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
|
||||
// Same adaptive back-off applied to the other two O(mapWallet) scans that hold the daemon's cs_main:
|
||||
// the address scan (z_listunspent) and the history scan (z_listreceivedbyaddress). Without this, a
|
||||
// large shielded wallet re-scans them every tab cadence (~seconds each), saturating cs_main and
|
||||
// starving block connection near the tip (where effectivelySyncing() reads false). See
|
||||
// addressRefreshDue() / txRefreshDue(); only the routine periodic poll is throttled — explicit
|
||||
// refreshes (tab switch, dirty set, in-flight send) call the refresh directly and bypass this.
|
||||
double last_address_scan_ms_ = 0.0; // measured cost of the last address scan
|
||||
double last_tx_scan_ms_ = 0.0; // measured cost of the last history scan
|
||||
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
|
||||
std::uint64_t clipboard_secret_hash_ = 0;
|
||||
double clipboard_clear_deadline_ = 0.0;
|
||||
@@ -1061,6 +1203,7 @@ private:
|
||||
|
||||
// Debug screenshot sweep state.
|
||||
bool screenshot_sweep_active_ = false;
|
||||
bool sweep_current_theme_only_ = false; // Debug Options: sweep only the active theme
|
||||
bool sweep_capture_this_frame_ = false;
|
||||
int sweep_skin_idx_ = 0;
|
||||
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
|
||||
@@ -1154,6 +1297,16 @@ private:
|
||||
|
||||
// Auto-shield guard (prevents concurrent auto-shield operations)
|
||||
std::atomic<bool> auto_shield_pending_{false};
|
||||
// v1.3.0+ daemons auto-shield coinbase themselves; probe z_autoshieldstatus once per connection and
|
||||
// defer the wallet's own client-side auto-shield when the daemon is doing it (otherwise both race for
|
||||
// the same coinbase UTXOs and split funds across different z-addresses). Fail-closed: a pre-1.3.0
|
||||
// daemon lacks the RPC → active stays false → the wallet keeps shielding client-side (no regression).
|
||||
bool daemon_autoshield_probed_ = false;
|
||||
bool daemon_autoshield_active_ = false;
|
||||
std::atomic<bool> daemon_autoshield_probe_inflight_{false};
|
||||
std::string daemon_autoshield_address_; // z_autoshieldstatus fields (O1); empty on old daemons
|
||||
std::string daemon_autoshield_disabled_reason_; // daemon's reason auto-shield is off (e.g. seed not recoverable)
|
||||
bool daemon_autoshield_seed_recoverable_ = false;
|
||||
|
||||
// P4: Incremental transaction cache
|
||||
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
||||
@@ -1192,6 +1345,11 @@ private:
|
||||
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
|
||||
// the whole scan and would block them); completion is signalled by the rescan RPC callback.
|
||||
bool runtime_rescan_active_ = false;
|
||||
// True only for a rescan the WALLET/USER initiated (the Rescan button, a -rescan/salvage/zap/reindex
|
||||
// restart, key import, seed migration) — not an autonomous background witness rebuild the daemon does
|
||||
// on its own. Gates the "Blockchain rescan complete" toast so background rebuilds don't fire it;
|
||||
// cleared when the toast is shown.
|
||||
std::atomic<bool> user_initiated_rescan_{false}; // atomic: some rescan triggers run on worker threads
|
||||
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
|
||||
// that reconciles the preserved wallet.dat against the freshly-imported chain.
|
||||
bool post_bootstrap_rescan_pending_ = false;
|
||||
@@ -1250,8 +1408,8 @@ private:
|
||||
services::WalletSecurityWorkflow wallet_security_workflow_;
|
||||
|
||||
// Wizard: stopping an external daemon before bootstrap
|
||||
bool wizard_stopping_external_ = false;
|
||||
std::string wizard_stop_status_;
|
||||
std::atomic<bool> wizard_stopping_external_{false}; // written by the stop worker, read by the UI (L-06)
|
||||
GuardedStatus wizard_stop_status_; // thread-safe: written by the stop worker, read by the UI (L-06)
|
||||
|
||||
// PIN vault
|
||||
std::unique_ptr<util::SecureVault> vault_;
|
||||
@@ -1344,7 +1502,9 @@ private:
|
||||
void renderBlockDbReindexDialog(); // offer to rebuild an unreadable block database (-reindex)
|
||||
void reindexBlockDatabase(); // restart the daemon with -reindex to rebuild the block DB
|
||||
void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat
|
||||
void renderEmptyWalletWarningDialog();// warn that the active wallet is empty while a sibling holds funds
|
||||
void detectWalletAutoRecovery(); // scan daemon output for a salvage; fire the warning once/session
|
||||
void detectWalletDegraded(); // scan daemon output for a DEGRADED-mode open; warn once/session
|
||||
void restoreOriginalWallet(); // swap the wallet.<ts>.bak back over the salvaged copy + restart
|
||||
void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result
|
||||
void rebuildWalletDatabase(); // rebuild a BDB-inconsistent wallet into a loadable one (helper)
|
||||
@@ -1379,6 +1539,13 @@ private:
|
||||
void refreshPrice();
|
||||
void refreshWalletEncryptionState();
|
||||
void applyRefreshPolicy(ui::NavPage page);
|
||||
bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis)
|
||||
bool balanceRefreshDue() const; // adaptive: enough time elapsed given the last balance-scan cost?
|
||||
bool addressRefreshDue() const; // same adaptive back-off for the address scan (z_listunspent)
|
||||
bool txRefreshDue() const; // same adaptive back-off for the history scan (z_listreceivedbyaddress)
|
||||
// Shared duty-cycle rule: a scan may resume only once (lastScanMs / kScanDutyCycle) has elapsed since
|
||||
// lastUpdate, so any single O(mapWallet) scan can occupy at most ~kScanDutyCycle of wall-clock.
|
||||
bool scanRefreshDue(std::int64_t lastUpdate, double lastScanMs) const;
|
||||
bool currentPageNeedsWalletDataRefresh() const;
|
||||
bool shouldRunWalletTransactionRefresh() const;
|
||||
bool shouldRefreshTransactions() const;
|
||||
|
||||
+609
-85
File diff suppressed because it is too large
Load Diff
+129
-72
@@ -238,6 +238,41 @@ private:
|
||||
// daemon off the main thread (to avoid stalling the UI), or ask the user to
|
||||
// restart an external daemon. Shared by encryptWalletWithPassphrase() and
|
||||
// processDeferredEncryption(); must be called on the main thread.
|
||||
// Zero (overwrite) then delete a plaintext key export so a full cleartext dump of every private key is
|
||||
// never left readable on disk. Idempotent + error-tolerant (safe on a missing/locked file). (H-02)
|
||||
void App::scrubAndRemoveExport(const std::string& path)
|
||||
{
|
||||
if (path.empty()) return;
|
||||
std::error_code ec;
|
||||
const auto sz = std::filesystem::file_size(path, ec);
|
||||
if (!ec && sz > 0) {
|
||||
std::fstream scrub(path, std::ios::binary | std::ios::in | std::ios::out);
|
||||
if (scrub) {
|
||||
const std::vector<char> zeros(static_cast<size_t>(sz), 0);
|
||||
scrub.write(zeros.data(), static_cast<std::streamsize>(sz));
|
||||
scrub.flush();
|
||||
}
|
||||
}
|
||||
std::filesystem::remove(path, ec);
|
||||
}
|
||||
|
||||
// Startup net for H-02: a crash/kill/early-return between exporting the cleartext keys and scrubbing them
|
||||
// could leave an obsidiandecryptexport* file behind. Purge any found in the data dir on launch.
|
||||
void App::sweepStaleDecryptExports()
|
||||
{
|
||||
std::error_code ec;
|
||||
const std::string dir = util::Platform::getDragonXDataDir();
|
||||
std::filesystem::directory_iterator it(dir, ec), end;
|
||||
for (; it != end; it.increment(ec)) {
|
||||
if (ec) break;
|
||||
const std::string name = it->path().filename().string();
|
||||
if (name.rfind("obsidiandecryptexport", 0) == 0) {
|
||||
scrubAndRemoveExport(it->path().string());
|
||||
DEBUG_LOGF("[decrypt] swept stale plaintext key export: %s\n", name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus) {
|
||||
if (isUsingEmbeddedDaemon()) {
|
||||
if (announceRestartStatus) {
|
||||
@@ -265,14 +300,14 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
|
||||
});
|
||||
} else {
|
||||
ui::Notifications::instance().warning(
|
||||
"Please restart your daemon for encryption to take effect.");
|
||||
TR("sec_restart_daemon_for_encryption"));
|
||||
}
|
||||
}
|
||||
|
||||
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
if (!rpc_ || !rpc_->isConnected()) return;
|
||||
encrypt_in_progress_ = true;
|
||||
encrypt_status_ = "Encrypting wallet...";
|
||||
encrypt_status_ = TR("sec_encrypting_wallet");
|
||||
|
||||
if (worker_) {
|
||||
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
|
||||
@@ -282,7 +317,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
if (result.encrypted) {
|
||||
return [this]() {
|
||||
encrypt_in_progress_ = false;
|
||||
encrypt_status_ = "Wallet encrypted. Restarting daemon...";
|
||||
encrypt_status_ = TR("sec_wallet_encrypted_restarting_daemon");
|
||||
DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");
|
||||
|
||||
// Immediately update local encryption state so the
|
||||
@@ -301,7 +336,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
}
|
||||
|
||||
ui::Notifications::instance().info(
|
||||
"Wallet encrypted successfully", 5.0f);
|
||||
TR("sec_wallet_encrypted_successfully"), 5.0f);
|
||||
|
||||
// The daemon shuts itself down after encryptwallet.
|
||||
// Update connection_status_ so the loading overlay
|
||||
@@ -313,11 +348,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
std::string err = result.error;
|
||||
return [this, err]() {
|
||||
encrypt_in_progress_ = false;
|
||||
encrypt_status_ = "Encryption failed: " + err;
|
||||
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
|
||||
DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());
|
||||
|
||||
ui::Notifications::instance().error(
|
||||
"Encryption failed: " + err);
|
||||
std::string(TR("sec_encryption_failed_prefix")) + err);
|
||||
|
||||
// Return to passphrase entry on failure
|
||||
if (show_encrypt_dialog_ &&
|
||||
@@ -358,7 +393,7 @@ void App::processDeferredEncryption() {
|
||||
std::string pin = std::move(deferredEncryption.pin);
|
||||
|
||||
encrypt_in_progress_ = true;
|
||||
encrypt_status_ = "Encrypting wallet...";
|
||||
encrypt_status_ = TR("sec_encrypting_wallet");
|
||||
|
||||
if (worker_) {
|
||||
worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
|
||||
@@ -378,13 +413,13 @@ void App::processDeferredEncryption() {
|
||||
if (result.pinStored) {
|
||||
settings_->setPinEnabled(true);
|
||||
settings_->save();
|
||||
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
|
||||
ui::Notifications::instance().info(TR("sec_wallet_encrypted_and_pin_set"), 5.0f);
|
||||
} else {
|
||||
ui::Notifications::instance().warning(
|
||||
"Wallet encrypted but PIN vault failed");
|
||||
TR("sec_wallet_encrypted_but_pin_vault_failed"));
|
||||
}
|
||||
} else {
|
||||
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
|
||||
ui::Notifications::instance().info(TR("sec_wallet_encrypted_successfully"), 5.0f);
|
||||
}
|
||||
|
||||
wallet_security_.clearDeferredEncryption();
|
||||
@@ -397,9 +432,9 @@ void App::processDeferredEncryption() {
|
||||
std::string err = result.error;
|
||||
return [this, err]() {
|
||||
encrypt_in_progress_ = false;
|
||||
encrypt_status_ = "Encryption failed: " + err;
|
||||
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
|
||||
DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
|
||||
ui::Notifications::instance().error("Encryption failed: " + err);
|
||||
ui::Notifications::instance().error(std::string(TR("sec_encryption_failed_prefix")) + err);
|
||||
wallet_security_.clearDeferredEncryption();
|
||||
};
|
||||
}
|
||||
@@ -496,7 +531,7 @@ void App::lockWallet() {
|
||||
if (!lock_failure_warned_) {
|
||||
lock_failure_warned_ = true;
|
||||
ui::Notifications::instance().warning(
|
||||
"Couldn't lock the wallet — it is still unlocked. Check the daemon connection.", 12.0f);
|
||||
TR("sec_couldnt_lock_wallet"), 12.0f);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -506,7 +541,7 @@ void App::lockWallet() {
|
||||
void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
|
||||
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
||||
encrypt_in_progress_ = true;
|
||||
encrypt_status_ = "Changing passphrase...";
|
||||
encrypt_status_ = TR("sec_changing_passphrase");
|
||||
|
||||
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
||||
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||
@@ -539,9 +574,9 @@ void App::changePassphrase(const std::string& oldPass, const std::string& newPas
|
||||
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
|
||||
unlockTransactionHistoryCacheWithPassphrase(newPass);
|
||||
storeTransactionHistoryCacheIfAvailable();
|
||||
ui::Notifications::instance().info("Passphrase changed successfully");
|
||||
ui::Notifications::instance().info(TR("sec_passphrase_changed_successfully"));
|
||||
} else {
|
||||
encrypt_status_ = "Failed: " + err_msg;
|
||||
encrypt_status_ = std::string(TR("sec_failed_prefix")) + err_msg;
|
||||
}
|
||||
util::SecureVault::secureZero(newPass.data(), newPass.size());
|
||||
};
|
||||
@@ -602,8 +637,7 @@ void App::refreshWalletEncryptionState() {
|
||||
!encryption_incomplete_warned_) {
|
||||
encryption_incomplete_warned_ = true;
|
||||
ui::Notifications::instance().warning(
|
||||
"Wallet encryption did not complete — your wallet is NOT encrypted. "
|
||||
"Open Settings to finish encrypting it.", 30.0f);
|
||||
TR("sec_encryption_did_not_complete"), 30.0f);
|
||||
}
|
||||
if (state_.transactions.empty()) {
|
||||
loadTransactionHistoryCacheIfAvailable();
|
||||
@@ -724,6 +758,10 @@ void App::checkIdleMining() {
|
||||
// Resolve auto values: active defaults to half, idle defaults to all
|
||||
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
|
||||
if (idleThreads <= 0) idleThreads = maxThreads;
|
||||
// Clamp to [1, logical cores] before these reach setgenerate / startPoolMining — a settings field
|
||||
// could otherwise carry an arbitrary count straight past every bound. (M-06)
|
||||
activeThreads = std::clamp(activeThreads, 1, maxThreads);
|
||||
idleThreads = std::clamp(idleThreads, 1, maxThreads);
|
||||
|
||||
if (systemIdle) {
|
||||
// System is idle — scale up to idle thread count
|
||||
@@ -912,7 +950,7 @@ void App::renderLockScreen() {
|
||||
ImU32 textCol = ui::material::OnSurface();
|
||||
|
||||
{
|
||||
const char* title = "Wallet Locked";
|
||||
const char* title = TR("sec_wallet_locked_title");
|
||||
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
|
||||
dl->AddText(titleFont, titleFont->LegacySize,
|
||||
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
|
||||
@@ -925,7 +963,7 @@ void App::renderLockScreen() {
|
||||
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
|
||||
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), "Too many attempts. Wait %.0f seconds...", lock_lockout_timer_);
|
||||
snprintf(msg, sizeof(msg), TR("sec_too_many_attempts_wait"), lock_lockout_timer_);
|
||||
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
|
||||
@@ -938,10 +976,10 @@ void App::renderLockScreen() {
|
||||
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
|
||||
if (hasPinVault) {
|
||||
const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD;
|
||||
const char* modeText = lock_use_pin_ ? " PIN" : " Passphrase";
|
||||
const char* modeText = lock_use_pin_ ? " PIN" : TR("sec_mode_passphrase");
|
||||
const char* switchLabel = lock_use_pin_
|
||||
? "Use passphrase instead"
|
||||
: "Use PIN instead";
|
||||
? TR("sec_use_passphrase_instead")
|
||||
: TR("sec_use_pin_instead");
|
||||
|
||||
// Current mode indicator — icon with icon font, text with caption font
|
||||
ImFont* iconFont = ui::material::Type().iconSmall();
|
||||
@@ -1043,7 +1081,7 @@ void App::renderLockScreen() {
|
||||
if (lock_unlock_in_progress_) {
|
||||
// Animated spinner dots
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "Unlocking%s", ui::material::LoadingDots());
|
||||
snprintf(msg, sizeof(msg), TR("sec_unlocking_fmt"), ui::material::LoadingDots());
|
||||
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
|
||||
@@ -1067,7 +1105,7 @@ void App::renderLockScreen() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!canSubmit);
|
||||
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
|
||||
bool btnClicked = ui::material::TactileButton(TR("sec_unlock_button"), ImVec2(unlockW, unlockH));
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
@@ -1121,7 +1159,7 @@ void App::renderLockScreen() {
|
||||
r->call("walletpassphrase", {passphrase, timeout});
|
||||
rpcOk = true;
|
||||
} else {
|
||||
rpcErr = "Not connected to daemon";
|
||||
rpcErr = TR("sec_not_connected_to_daemon");
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
rpcErr = e.what();
|
||||
@@ -1137,7 +1175,7 @@ void App::renderLockScreen() {
|
||||
// so route through applyUnlockFailure so the lockout curve applies
|
||||
// (this path previously bumped the counter but skipped the lockout math).
|
||||
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
|
||||
applyUnlockFailure("Unlock failed: " + rpcErr);
|
||||
applyUnlockFailure(std::string(TR("sec_unlock_failed_prefix")) + rpcErr);
|
||||
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
|
||||
};
|
||||
}
|
||||
@@ -1226,7 +1264,7 @@ void App::renderEncryptWalletDialog() {
|
||||
else if (tier == 1) { strengthLabel = TR("wiz_strength_fair"); strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
|
||||
|
||||
float barW = ImGui::GetContentRegionAvail().x;
|
||||
float barH = 4.0f;
|
||||
float barH = 4.0f * ui::Layout::dpiScale();
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
||||
@@ -1276,7 +1314,7 @@ void App::renderEncryptWalletDialog() {
|
||||
// Indeterminate progress bar
|
||||
{
|
||||
float barW = ImGui::GetContentRegionAvail().x;
|
||||
float barH = 6.0f;
|
||||
float barH = 6.0f * ui::Layout::dpiScale();
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
||||
@@ -1344,9 +1382,13 @@ void App::renderEncryptWalletDialog() {
|
||||
enc_dlg_pin_status_.clear();
|
||||
std::string savedPass = enc_dlg_saved_passphrase_;
|
||||
if (worker_ && vault_) {
|
||||
worker_->post([this, pinStr, savedPass]() -> rpc::RPCWorker::MainCb {
|
||||
worker_->post([this, pinStr, savedPass]() mutable -> rpc::RPCWorker::MainCb {
|
||||
// Argon2id runs here (worker thread)
|
||||
bool ok = vault_->store(pinStr, savedPass);
|
||||
// Scrub the captured PIN + passphrase copies (they live in the worker's task
|
||||
// queue until this runs); the source member is scrubbed in the MainCb. (L-03)
|
||||
if (!savedPass.empty()) util::SecureVault::secureZero(&savedPass[0], savedPass.size());
|
||||
if (!pinStr.empty()) util::SecureVault::secureZero(&pinStr[0], pinStr.size());
|
||||
return [this, ok]() {
|
||||
if (ok) {
|
||||
settings_->setPinEnabled(true);
|
||||
@@ -1407,6 +1449,11 @@ void App::renderEncryptWalletDialog() {
|
||||
ov.cardWidth = 460.0f; ov.idSuffix = "changepass";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
// Same fund-loss consequence as Encrypt/Remove Encryption if the new
|
||||
// passphrase is lost — reuse their warning string/header for consistency.
|
||||
DialogWarningHeader(TR("wiz_encrypt_warning"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextUnformatted(TR("change_pass_current"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
|
||||
@@ -1433,12 +1480,22 @@ void App::renderEncryptWalletDialog() {
|
||||
bool valid = strlen(change_old_pass_buf_) > 0 &&
|
||||
strlen(change_new_pass_buf_) >= 8 &&
|
||||
strcmp(change_new_pass_buf_, change_confirm_buf_) == 0;
|
||||
|
||||
// Two-button footer (primary + Cancel) to match the encrypt/decrypt siblings.
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
||||
if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(btnW, 40))) {
|
||||
changePassphrase(std::string(change_old_pass_buf_),
|
||||
std::string(change_new_pass_buf_));
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
// Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by
|
||||
// the !show_change_passphrase_ cleanup block below.
|
||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
||||
show_change_passphrase_ = false;
|
||||
}
|
||||
EndOverlayDialog();
|
||||
}
|
||||
|
||||
@@ -1521,7 +1578,7 @@ void App::renderDecryptWalletDialog() {
|
||||
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
|
||||
if (!unlock.ok) {
|
||||
return [this]() {
|
||||
wallet_security_workflow_.failEntry("Incorrect passphrase");
|
||||
wallet_security_workflow_.failEntry(TR("sec_incorrect_passphrase_decrypt"));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1588,6 +1645,11 @@ void App::renderDecryptWalletDialog() {
|
||||
std::chrono::steady_clock::now());
|
||||
|
||||
auto restartAndImport = [this, exportPath](const util::AsyncTaskManager::Token& token) {
|
||||
// Scrub + delete the plaintext key export (obsidiandecryptexport…) on EVERY exit path —
|
||||
// success, a restart-failure early return, or an exception. A full cleartext dump of all
|
||||
// private keys must never outlive this step. The startup sweep is a further net for a
|
||||
// crash/kill mid-flight. (H-02)
|
||||
struct ExportScrub { std::string p; ~ExportScrub() { App::scrubAndRemoveExport(p); } } exportScrub{exportPath};
|
||||
WalletSecurityDaemonAdapter daemonAdapter(*this, token);
|
||||
WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
|
||||
[this](rpc::RPCClient& client, const char* context) {
|
||||
@@ -1632,7 +1694,7 @@ void App::renderDecryptWalletDialog() {
|
||||
});
|
||||
|
||||
ui::Notifications::instance().info(
|
||||
"Importing keys & rescanning blockchain — wallet is usable while this runs",
|
||||
TR("sec_importing_keys_rescanning"),
|
||||
8.0f);
|
||||
};
|
||||
});
|
||||
@@ -1641,26 +1703,7 @@ void App::renderDecryptWalletDialog() {
|
||||
WalletSecurityImportRpcAdapter importAdapter(rpc_.get(), saved_config_);
|
||||
auto importResult = services::WalletSecurityWorkflowExecutor::importWallet(
|
||||
importAdapter, exportPath);
|
||||
|
||||
// The plaintext key export (obsidiandecryptexport…) has served its purpose now
|
||||
// that the import attempt has resolved — scrub and remove it so a full cleartext
|
||||
// dump of every private key isn't left on disk forever. Recovery, if ever needed,
|
||||
// is the encrypted backup (wallet.dat.encrypted.bak), never this file.
|
||||
{
|
||||
std::error_code delEc;
|
||||
const auto sz = std::filesystem::file_size(exportPath, delEc);
|
||||
if (!delEc && sz > 0) {
|
||||
std::fstream scrub(exportPath,
|
||||
std::ios::binary | std::ios::in | std::ios::out);
|
||||
if (scrub) {
|
||||
const std::vector<char> zeros(static_cast<size_t>(sz), 0);
|
||||
scrub.write(zeros.data(), static_cast<std::streamsize>(sz));
|
||||
scrub.flush();
|
||||
}
|
||||
}
|
||||
std::filesystem::remove(exportPath, delEc);
|
||||
DEBUG_LOGF("[decrypt] removed plaintext key export after import\n");
|
||||
}
|
||||
// (exportScrub scrubs + deletes the plaintext key export on scope exit — H-02)
|
||||
|
||||
if (!importResult.ok) {
|
||||
std::string err = importResult.error;
|
||||
@@ -1670,7 +1713,7 @@ void App::renderDecryptWalletDialog() {
|
||||
wallet_security_workflow_.finishImport();
|
||||
ui::Notifications::instance().error(
|
||||
err +
|
||||
"\nEncrypted backup: wallet.dat.encrypted.bak",
|
||||
TR("sec_encrypted_backup_suffix"),
|
||||
12.0f);
|
||||
};
|
||||
});
|
||||
@@ -1696,7 +1739,7 @@ void App::renderDecryptWalletDialog() {
|
||||
refreshPeerInfo();
|
||||
|
||||
ui::Notifications::instance().success(
|
||||
"Wallet decrypted successfully! All keys imported.",
|
||||
TR("sec_wallet_decrypted_all_keys_imported"),
|
||||
8.0f);
|
||||
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
|
||||
};
|
||||
@@ -1786,7 +1829,7 @@ void App::renderDecryptWalletDialog() {
|
||||
// Indeterminate progress bar
|
||||
{
|
||||
float barW = ImGui::GetContentRegionAvail().x;
|
||||
float barH = 6.0f;
|
||||
float barH = 6.0f * ui::Layout::dpiScale();
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
||||
@@ -1818,7 +1861,7 @@ void App::renderDecryptWalletDialog() {
|
||||
int tMins = (int)(totalElapsed / 60);
|
||||
int tSecs = (int)(totalElapsed % 60);
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs);
|
||||
ImGui::TextDisabled(TR("sec_total_elapsed_fmt"), tMins, tSecs);
|
||||
}
|
||||
|
||||
// ---- Phase 2: Success ----
|
||||
@@ -1916,10 +1959,12 @@ void App::renderPinDialogs() {
|
||||
util::SecureVault::isValidPin(pinStr) &&
|
||||
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
||||
|
||||
// Two-button footer (primary + Cancel) to match the encrypt/decrypt siblings.
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Verifying passphrase...";
|
||||
pin_status_ = TR("sec_verifying_passphrase");
|
||||
|
||||
// Verify passphrase + store vault on worker thread to avoid
|
||||
// blocking the UI with Argon2id key derivation.
|
||||
@@ -1930,20 +1975,25 @@ void App::renderPinDialogs() {
|
||||
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
|
||||
|
||||
if (rpc_ && rpc_->isConnected() && worker_) {
|
||||
worker_->post([this, passphrase, pin]() -> rpc::RPCWorker::MainCb {
|
||||
worker_->post([this, passphrase, pin]() mutable -> rpc::RPCWorker::MainCb {
|
||||
// Verify passphrase via RPC (worker thread)
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Security / PIN setup");
|
||||
rpc_->call("walletpassphrase", {passphrase, 5});
|
||||
} catch (const std::exception& e) {
|
||||
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
|
||||
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
|
||||
return [this]() {
|
||||
pin_status_ = "Incorrect passphrase";
|
||||
pin_status_ = TR("sec_incorrect_passphrase_pin_setup");
|
||||
pin_in_progress_ = false;
|
||||
};
|
||||
}
|
||||
|
||||
// Passphrase correct — store in vault (Argon2id, worker thread)
|
||||
bool storeOk = vault_ && vault_->store(pin, passphrase);
|
||||
// Captured passphrase + PIN are no longer needed — scrub the worker-queue copies. (M-01)
|
||||
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
|
||||
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
|
||||
|
||||
// Lock wallet back
|
||||
try {
|
||||
@@ -1958,19 +2008,26 @@ void App::renderPinDialogs() {
|
||||
pin_status_.clear();
|
||||
pin_in_progress_ = false;
|
||||
show_pin_setup_ = false;
|
||||
ui::Notifications::instance().info("PIN set successfully");
|
||||
ui::Notifications::instance().info(TR("sec_pin_set_successfully"));
|
||||
} else {
|
||||
pin_status_ = "Failed to create vault";
|
||||
pin_status_ = TR("sec_failed_to_create_vault");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
pin_status_ = "Not connected to daemon";
|
||||
pin_status_ = TR("sec_not_connected_to_daemon_pin");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
// Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by
|
||||
// the !show_pin_setup_ cleanup block below.
|
||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
||||
show_pin_setup_ = false;
|
||||
}
|
||||
EndOverlayDialog();
|
||||
}
|
||||
// Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc /
|
||||
@@ -2024,7 +2081,7 @@ void App::renderPinDialogs() {
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Changing PIN...";
|
||||
pin_status_ = TR("sec_changing_pin");
|
||||
std::string oldPin(pin_old_buf_);
|
||||
std::string newPinCopy = newPin;
|
||||
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
||||
@@ -2040,15 +2097,15 @@ void App::renderPinDialogs() {
|
||||
pin_status_.clear();
|
||||
pin_in_progress_ = false;
|
||||
show_pin_change_ = false;
|
||||
ui::Notifications::instance().info("PIN changed successfully");
|
||||
ui::Notifications::instance().info(TR("sec_pin_changed_successfully"));
|
||||
} else {
|
||||
pin_status_ = "Incorrect current PIN";
|
||||
pin_status_ = TR("sec_incorrect_current_pin");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
pin_status_ = "Internal error";
|
||||
pin_status_ = TR("sec_internal_error_change_pin");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
}
|
||||
@@ -2089,7 +2146,7 @@ void App::renderPinDialogs() {
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Verifying PIN...";
|
||||
pin_status_ = TR("sec_verifying_pin");
|
||||
std::string oldPin(pin_old_buf_);
|
||||
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
||||
|
||||
@@ -2109,15 +2166,15 @@ void App::renderPinDialogs() {
|
||||
pin_status_.clear();
|
||||
pin_in_progress_ = false;
|
||||
show_pin_remove_ = false;
|
||||
ui::Notifications::instance().info("PIN removed");
|
||||
ui::Notifications::instance().info(TR("sec_pin_removed"));
|
||||
} else {
|
||||
pin_status_ = "Incorrect PIN";
|
||||
pin_status_ = TR("sec_incorrect_pin_remove");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
pin_status_ = "Internal error";
|
||||
pin_status_ = TR("sec_internal_error_remove_pin");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-2
@@ -170,10 +170,10 @@ void App::installDemoWalletData()
|
||||
}
|
||||
|
||||
auto zaddr = [](const char* a, double bal, const char* label) {
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i;
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.spendableBalance = bal; i.type = "shielded"; i.label = label; return i;
|
||||
};
|
||||
auto taddr = [](const char* a, double bal, const char* label) {
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "transparent"; i.label = label; return i;
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.spendableBalance = bal; i.type = "transparent"; i.label = label; return i;
|
||||
};
|
||||
state_.z_addresses = {
|
||||
zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"),
|
||||
@@ -310,6 +310,8 @@ void App::buildSweepCatalog()
|
||||
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
|
||||
add("modal-backup", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
||||
add("modal-faq", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_faq_ = true; }, [](App& a) { a.show_faq_ = false; });
|
||||
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
|
||||
add("modal-encrypt", ui::NavPage::Settings,
|
||||
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
|
||||
@@ -704,9 +706,26 @@ void App::startSweepImpl(bool full)
|
||||
if (sk.valid) sweep_skins_.push_back(sk.id);
|
||||
if (sweep_skins_.empty()) return;
|
||||
|
||||
// Debug Options "Current theme only": sweep just the active skin instead of cycling every theme.
|
||||
if (sweep_current_theme_only_)
|
||||
sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId());
|
||||
|
||||
// DEV/TEST hook (dormant unless the env is set): DRAGONX_SWEEP_ONLY="send,receive" restricts the
|
||||
// sweep to the named surfaces and the dark skin, so a slow large-window run captures just the tab
|
||||
// under review instead of all surfaces x every skin.
|
||||
const char* sweepOnly = std::getenv("DRAGONX_SWEEP_ONLY");
|
||||
std::string sweepOnlyStr = sweepOnly ? sweepOnly : "";
|
||||
if (!sweepOnlyStr.empty()) sweep_skins_.assign(1, std::string("dark"));
|
||||
|
||||
sweep_full_ = full;
|
||||
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
||||
buildSweepCatalog();
|
||||
if (!sweepOnlyStr.empty()) {
|
||||
std::vector<SweepTarget> keep;
|
||||
for (const auto& t : sweep_targets_)
|
||||
if (sweepOnlyStr.find(t.name) != std::string::npos) keep.push_back(t);
|
||||
sweep_targets_.swap(keep);
|
||||
}
|
||||
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
|
||||
|
||||
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
|
||||
|
||||
+105
-24
@@ -177,8 +177,36 @@ void App::renderFirstRunWizard() {
|
||||
// DPI scale factor — multiply all pixel constants by dp
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
|
||||
// Vertical scroll: the wizard cards are hand-drawn at absolute Y offsets and grow ~1.5x with the
|
||||
// font-scale setting, so at high scale the focused card's primary button (Continue / Encrypt & Continue
|
||||
// / Skip) can fall below the fixed window. Offset the whole layout by a wheel-driven scroll, clamped to
|
||||
// last frame's measured content height, so every control stays reachable. The window keeps
|
||||
// NoScrollWithMouse, so ImGui doesn't consume the wheel — we read the raw delta and apply our own offset.
|
||||
static float s_wizScroll = 0.0f, s_wizContentH = 0.0f;
|
||||
if (ImGui::IsWindowAppearing()) s_wizScroll = 0.0f;
|
||||
const float wizMaxScroll = std::max(0.0f, s_wizContentH - winSize.y);
|
||||
// Don't steal the wheel from an open combo popup (e.g. the 9-item Language dropdown, which is a
|
||||
// scrollable popup): NoPopupHierarchy stops the popup counting as hovering the wizard, and the
|
||||
// IsPopupOpen guard ensures no wheel is consumed for the whole wizard while any popup is showing.
|
||||
const bool wizPopupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
|
||||
if (wizMaxScroll > 0.0f && !wizPopupOpen &&
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_NoPopupHierarchy)) {
|
||||
float wheel = ImGui::GetIO().MouseWheel;
|
||||
if (wheel != 0.0f) s_wizScroll -= wheel * 60.0f * dp;
|
||||
}
|
||||
s_wizScroll = std::max(0.0f, std::min(s_wizScroll, wizMaxScroll));
|
||||
const float scrollY = s_wizScroll;
|
||||
|
||||
// --- Header: Logo + Welcome ---
|
||||
float headerCy = winPos.y + 20.0f * dp;
|
||||
// Vertically center the content when it fits (mirrors the horizontal centering below): on a tall
|
||||
// monitor top-anchoring leaves a large void under the cards. Using last frame's measured block
|
||||
// height, when the content fits inside the window (and we're NOT overflowing, so this doesn't
|
||||
// fight the scroll), push everything down by half the leftover space. No-op once content
|
||||
// fills/exceeds the window (s_wizContentH >= winSize.y ⇒ wizMaxScroll > 0 ⇒ vCenter skipped).
|
||||
float vCenter = 0.0f;
|
||||
if (wizMaxScroll == 0.0f && s_wizContentH > 0.0f && s_wizContentH < winSize.y)
|
||||
vCenter = std::max(0.0f, (winSize.y - s_wizContentH) * 0.5f);
|
||||
float headerCy = winPos.y - scrollY + 20.0f * dp + vCenter;
|
||||
float logoSize = S.drawElement("screens.first-run", "logo").sizeOr(56.0f);
|
||||
if (logo_tex_ != 0) {
|
||||
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
|
||||
@@ -266,29 +294,43 @@ void App::renderFirstRunWizard() {
|
||||
{
|
||||
int state = cardState(0);
|
||||
bool isFocused = (state == 1);
|
||||
bool isCollapsed = (state == 2); // Completed: minimize to a compact pill (mirrors Card 1)
|
||||
float cx = leftX + cardPad;
|
||||
float cy = card0Top + cardPad;
|
||||
float contentW = colW - 2 * cardPad;
|
||||
|
||||
// Step indicator
|
||||
{
|
||||
// Step indicator + title (inline when collapsed)
|
||||
if (isCollapsed) {
|
||||
// Compact single-line: check icon + "Step 1" + "Appearance"
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step1"));
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
float labelX = cx + iconW + 4.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, TR("wiz_step1"));
|
||||
float step1W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, TR("wiz_step1")).x;
|
||||
float titleX = labelX + step1W + 12.0f * dp;
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, TR("wiz_appearance"));
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
} else {
|
||||
// Step indicator
|
||||
{
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step1"));
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
|
||||
// Title
|
||||
{
|
||||
const char* t = TR("wiz_appearance");
|
||||
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
||||
cy += titleFont->LegacySize + 10.0f * dp;
|
||||
}
|
||||
// Title
|
||||
{
|
||||
const char* t = TR("wiz_appearance");
|
||||
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
||||
cy += titleFont->LegacySize + 10.0f * dp;
|
||||
}
|
||||
|
||||
// Separator
|
||||
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
|
||||
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
|
||||
cy += 14.0f * dp;
|
||||
// Separator
|
||||
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
|
||||
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
|
||||
cy += 14.0f * dp;
|
||||
}
|
||||
|
||||
float& wiz_blur_amount = wizardUi.blur_amount;
|
||||
bool& wiz_theme_effects = wizardUi.theme_effects;
|
||||
@@ -324,6 +366,9 @@ void App::renderFirstRunWizard() {
|
||||
wiz_appearance_init = true;
|
||||
}
|
||||
|
||||
// Controls: rendered for the focused and upcoming states so content is visible under
|
||||
// the dim overlay; skipped entirely once completed so the card shrinks to a compact pill.
|
||||
if (!isCollapsed) {
|
||||
// Render controls always so content is visible under the dim
|
||||
// overlay when not focused; disable interaction when not active.
|
||||
ImGui::BeginDisabled(!isFocused);
|
||||
@@ -640,13 +685,21 @@ void App::renderFirstRunWizard() {
|
||||
cy += btnH;
|
||||
}
|
||||
|
||||
cy += cardPad;
|
||||
// Lock card height to the tallest content ever seen
|
||||
float& card0MaxH = wizardUi.card0_max_h;
|
||||
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
||||
card0Bot = card0Top + card0MaxH;
|
||||
} // if (!isCollapsed)
|
||||
|
||||
// Card 0 finalization deferred until after cards 1+2 are sized
|
||||
cy += cardPad;
|
||||
// Lock card height to the tallest content ever seen (but not when collapsed)
|
||||
float& card0MaxH = wizardUi.card0_max_h;
|
||||
if (isCollapsed) {
|
||||
// Completed: finalize immediately as a compact pill (do not stretch to the
|
||||
// right column height, and skip the deferred stretch below).
|
||||
card0Bot = card0Top + (cy - card0Top);
|
||||
finalizeCard(leftX, colW, card0Top, card0Bot, state);
|
||||
} else {
|
||||
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
||||
card0Bot = card0Top + card0MaxH;
|
||||
// Card 0 finalization deferred until after cards 1+2 are sized
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -889,8 +942,9 @@ void App::renderFirstRunWizard() {
|
||||
}
|
||||
|
||||
if (wizard_stopping_external_) {
|
||||
const std::string ws = wizard_stop_status_.get();
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol,
|
||||
wizard_stop_status_.c_str());
|
||||
ws.c_str());
|
||||
cy += captionFont->LegacySize + 8.0f * dp;
|
||||
} else {
|
||||
float stopW = 150.0f * dp;
|
||||
@@ -1376,6 +1430,13 @@ void App::renderFirstRunWizard() {
|
||||
encrypt_status_ = TR("wiz_skip_confirm");
|
||||
} else {
|
||||
s_skipEncConfirm = false;
|
||||
// Skipping leaves the wallet UNENCRYPTED — wipe the passphrase/PIN the user may have
|
||||
// typed so it doesn't linger in these process-lifetime buffers (only the Encrypt
|
||||
// path cleared them before). (L-07)
|
||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
||||
memset(wizard_pin_buf_, 0, sizeof(wizard_pin_buf_));
|
||||
memset(wizard_pin_confirm_buf_, 0, sizeof(wizard_pin_confirm_buf_));
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
settings_->setWizardCompleted(true);
|
||||
settings_->save();
|
||||
@@ -1411,7 +1472,9 @@ void App::renderFirstRunWizard() {
|
||||
}
|
||||
|
||||
// --- Deferred Card 0 finalization: match right column total height ---
|
||||
{
|
||||
// Only for the focused/upcoming Appearance card; a completed one was already finalized
|
||||
// above as a compact pill and must not be re-stretched.
|
||||
if (cardState(0) != 2) {
|
||||
float rightColBot = card2Bot;
|
||||
if (rightColBot > card0Bot) card0Bot = rightColBot;
|
||||
finalizeCard(leftX, colW, card0Top, card0Bot, cardState(0));
|
||||
@@ -1420,6 +1483,24 @@ void App::renderFirstRunWizard() {
|
||||
// Merge channels: backgrounds → content → overlays
|
||||
dl->ChannelsMerge();
|
||||
|
||||
// Measure this frame's content height (feeds next frame's scroll clamp) and, when it overflows the
|
||||
// window, draw a slim scroll indicator so the off-screen content is discoverable.
|
||||
{
|
||||
float contentBottom = std::max(card0Bot, std::max(card1Bot, card2Bot));
|
||||
// Subtract vCenter back out: everything below the header was shifted down by it, so the raw
|
||||
// span includes it. We want s_wizContentH to be the true (un-centered) content height, or the
|
||||
// vertical-centering above would feed on itself and oscillate frame-to-frame.
|
||||
s_wizContentH = (contentBottom - winPos.y + scrollY - vCenter) + 24.0f * dp;
|
||||
if (wizMaxScroll > 0.0f && s_wizContentH > 0.0f) {
|
||||
float trackH = winSize.y - 8.0f * dp;
|
||||
float thumbH = std::min(trackH, std::max(32.0f * dp, trackH * (winSize.y / s_wizContentH)));
|
||||
float thumbY = winPos.y + 4.0f * dp + (trackH - thumbH) * (scrollY / wizMaxScroll);
|
||||
float barX = winPos.x + winSize.x - 6.0f * dp;
|
||||
dl->AddRectFilled(ImVec2(barX, thumbY), ImVec2(barX + 3.0f * dp, thumbY + thumbH),
|
||||
ui::material::WithAlpha(ui::material::OnSurface(), 55), 1.5f * dp);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ bool ChatDatabase::unlockWithSecret(const std::string& secret)
|
||||
lock();
|
||||
return false;
|
||||
}
|
||||
loadTombstones();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -97,11 +98,13 @@ void ChatDatabase::lock()
|
||||
sodium_memzero(key_.data(), key_.size());
|
||||
key_ready_ = false;
|
||||
wallet_tag_.clear();
|
||||
tombstones_.clear();
|
||||
}
|
||||
|
||||
bool ChatDatabase::append(const ChatMessage& message)
|
||||
{
|
||||
if (!key_ready_ || !ensureOpen()) return false;
|
||||
if (isTombstoned(message)) return false; // locally deleted — don't re-persist on a chain re-scan
|
||||
|
||||
std::vector<unsigned char> nonce;
|
||||
std::vector<unsigned char> cipher;
|
||||
@@ -193,13 +196,79 @@ std::vector<ChatMessage> ChatDatabase::load()
|
||||
void ChatDatabase::clearWallet()
|
||||
{
|
||||
if (wallet_tag_.empty() || !ensureOpen()) return;
|
||||
for (const char* sql : {"DELETE FROM chat_messages WHERE wallet_tag = ?",
|
||||
"DELETE FROM chat_deleted WHERE wallet_tag = ?"}) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) continue;
|
||||
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
tombstones_.clear();
|
||||
}
|
||||
|
||||
bool ChatDatabase::deleteMessages(const std::vector<ChatMessage>& messages, bool tombstone)
|
||||
{
|
||||
if (!key_ready_ || !ensureOpen()) return false;
|
||||
if (messages.empty()) return true;
|
||||
|
||||
if (!exec("BEGIN")) return false;
|
||||
bool ok = true;
|
||||
for (const auto& m : messages) {
|
||||
const std::string dedup = dedupHash(m.txid, m.payload_position);
|
||||
|
||||
sqlite3_stmt* del = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, "DELETE FROM chat_messages WHERE wallet_tag = ? AND dedup_hash = ?",
|
||||
-1, &del, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_text(del, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(del, 2, dedup.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(del) != SQLITE_DONE) ok = false;
|
||||
sqlite3_finalize(del);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (tombstone) {
|
||||
sqlite3_stmt* ins = nullptr;
|
||||
if (sqlite3_prepare_v2(db_,
|
||||
"INSERT OR IGNORE INTO chat_deleted (wallet_tag, dedup_hash) VALUES (?, ?)",
|
||||
-1, &ins, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_text(ins, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(ins, 2, dedup.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(ins) != SQLITE_DONE) ok = false;
|
||||
sqlite3_finalize(ins);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!exec(ok ? "COMMIT" : "ROLLBACK")) ok = false;
|
||||
// Only reflect the tombstones in the in-memory cache once they are durably committed.
|
||||
if (ok && tombstone)
|
||||
for (const auto& m : messages) tombstones_.insert(dedupHash(m.txid, m.payload_position));
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ChatDatabase::isTombstoned(const ChatMessage& message) const
|
||||
{
|
||||
if (!key_ready_ || tombstones_.empty()) return false;
|
||||
return tombstones_.count(dedupHash(message.txid, message.payload_position)) > 0;
|
||||
}
|
||||
|
||||
void ChatDatabase::loadTombstones()
|
||||
{
|
||||
tombstones_.clear();
|
||||
if (!key_ready_ || !ensureOpen()) return;
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, "DELETE FROM chat_messages WHERE wallet_tag = ?", -1, &stmt, nullptr)
|
||||
!= SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db_, "SELECT dedup_hash FROM chat_deleted WHERE wallet_tag = ?",
|
||||
-1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return;
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const auto* h = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
||||
if (h) tombstones_.insert(h);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
@@ -260,11 +329,18 @@ bool ChatDatabase::exec(const char* sql)
|
||||
|
||||
bool ChatDatabase::createSchema()
|
||||
{
|
||||
return exec("CREATE TABLE IF NOT EXISTS chat_messages ("
|
||||
if (!exec("CREATE TABLE IF NOT EXISTS chat_messages ("
|
||||
"wallet_tag TEXT NOT NULL, "
|
||||
"dedup_hash TEXT NOT NULL, "
|
||||
"nonce BLOB NOT NULL, "
|
||||
"payload BLOB NOT NULL, "
|
||||
"PRIMARY KEY (wallet_tag, dedup_hash))"))
|
||||
return false;
|
||||
// Tombstones for locally-deleted messages (dedup_hash only — the same keyed, non-revealing hash the
|
||||
// message rows use). A chain re-scan checks this so a deleted message never re-imports.
|
||||
return exec("CREATE TABLE IF NOT EXISTS chat_deleted ("
|
||||
"wallet_tag TEXT NOT NULL, "
|
||||
"dedup_hash TEXT NOT NULL, "
|
||||
"nonce BLOB NOT NULL, "
|
||||
"payload BLOB NOT NULL, "
|
||||
"PRIMARY KEY (wallet_tag, dedup_hash))");
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
struct sqlite3;
|
||||
@@ -54,8 +55,20 @@ public:
|
||||
|
||||
void clearWallet(); // delete the unlocked wallet's rows
|
||||
|
||||
// Per-conversation local delete. Removes the given messages' rows; when `tombstone` is true it also
|
||||
// records their (txid,position) dedup keys so a chain re-scan never re-imports them — this backs the
|
||||
// "delete, but a NEW message revives the thread" path. With `tombstone` false the rows are simply
|
||||
// removed (used by "delete & block", where a settings-level cid block suppresses re-import until the
|
||||
// user unblocks, at which point the history re-imports from chain). Atomic; no-op while locked.
|
||||
bool deleteMessages(const std::vector<ChatMessage>& messages, bool tombstone);
|
||||
|
||||
// True if this message's (txid,position) was locally deleted with a tombstone. Checked against an
|
||||
// in-memory cache loaded on unlock — O(1), no SQL. False while locked.
|
||||
bool isTombstoned(const ChatMessage& message) const;
|
||||
|
||||
private:
|
||||
bool ensureOpen();
|
||||
void loadTombstones(); // populate tombstones_ from chat_deleted for the unlocked wallet
|
||||
bool exec(const char* sql);
|
||||
bool createSchema();
|
||||
std::string dedupHash(const std::string& txid, std::size_t position) const;
|
||||
@@ -74,6 +87,7 @@ private:
|
||||
std::array<unsigned char, 32> key_{}; // AEAD storage key (seed-derived)
|
||||
std::string wallet_tag_; // seed-derived row partition (a keyed hash, hex)
|
||||
bool key_ready_ = false;
|
||||
std::unordered_set<std::string> tombstones_; // dedup_hash cache of locally-deleted messages
|
||||
};
|
||||
|
||||
} // namespace dragonx::chat
|
||||
|
||||
@@ -28,6 +28,10 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
||||
std::int64_t fallbackTimestamp,
|
||||
std::vector<std::string>* newIncomingCids) {
|
||||
if (!has_identity_) return 0;
|
||||
// Persistence is attached but not unlocked (e.g. the DB failed to open with the seed): we can't
|
||||
// consult tombstones, so ingesting now would resurface locally-deleted messages into the store.
|
||||
// Skip until the DB is usable — chat is degraded anyway without its store.
|
||||
if (db_ && !db_->hasKey()) return 0;
|
||||
|
||||
const std::string myPubKey = chatIdentityPublicKeyHex(identity_);
|
||||
|
||||
@@ -62,6 +66,13 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
||||
}
|
||||
message.payload_position = meta.payload_position;
|
||||
|
||||
// Suppress locally-removed conversations before the (relatively costly) decrypt. A blocked cid
|
||||
// is dropped outright (old + future messages) until unblocked; a tombstoned (txid,position) was
|
||||
// deleted with "revive on new message", so only that exact message is skipped — a new message
|
||||
// in the same conversation has a different txid and flows through normally.
|
||||
if (blocked_pred_ && blocked_pred_(message.conversation_id)) continue;
|
||||
if (db_ && db_->isTombstoned(message)) continue;
|
||||
|
||||
if (meta.type == HushChatHeaderType::ContactRequest) {
|
||||
message.kind = ChatMessageKind::ContactRequest;
|
||||
message.body = meta.payload_memo; // plaintext request text
|
||||
@@ -92,10 +103,28 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
||||
void ChatService::loadFromDatabase() {
|
||||
if (!db_) return;
|
||||
for (const auto& message : db_->load()) {
|
||||
// Never surface a blocked conversation, even if a prior "delete & block" failed to remove its
|
||||
// rows (defense-in-depth): the ingest guard already drops live scans, this covers the reload path.
|
||||
if (blocked_pred_ && blocked_pred_(message.conversation_id)) continue;
|
||||
store_.append(message);
|
||||
}
|
||||
}
|
||||
|
||||
bool ChatService::deleteConversation(const std::string& conversationId, bool block) {
|
||||
// Delete the persisted rows FIRST and only mutate the in-memory view if that succeeds. Doing it the
|
||||
// other way round means a failed DB write (disk full / locked) would empty the store while the rows
|
||||
// survive — and on the next reload the conversation silently reappears with no tombstone.
|
||||
// Revive-on-new-message => tombstone the removed rows so a re-scan won't re-import them.
|
||||
// Block => remove the rows without a tombstone; the caller's blocked predicate suppresses re-import
|
||||
// until unblocked, at which point the conversation re-imports from chain.
|
||||
if (db_) {
|
||||
std::vector<ChatMessage> msgs = store_.conversation(conversationId); // snapshot (copy)
|
||||
if (!db_->deleteMessages(msgs, /*tombstone=*/!block)) return false;
|
||||
}
|
||||
store_.eraseConversation(conversationId);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string ChatService::identityPublicKeyHex() const {
|
||||
if (!has_identity_) return {};
|
||||
return chatIdentityPublicKeyHex(identity_);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "chat_store.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -55,6 +56,19 @@ public:
|
||||
// the seed-derived key) into the in-memory store. No-op without an unlocked database.
|
||||
void loadFromDatabase();
|
||||
|
||||
// Set a predicate returning true for a BLOCKED conversation id. ingest() drops those messages
|
||||
// entirely (they are never stored or persisted) until the predicate stops returning true — this is
|
||||
// how "delete & block" suppresses old and future messages. Typically wired to Settings::isChatBlocked.
|
||||
void setBlockedPredicate(std::function<bool(const std::string&)> pred) { blocked_pred_ = std::move(pred); }
|
||||
|
||||
// Locally delete a conversation: remove its messages from the store and the database. When `block`
|
||||
// is false, the removed messages are tombstoned so a chain re-scan won't re-import them, but a
|
||||
// genuinely NEW message (new txid) revives the thread. When `block` is true, the rows are removed
|
||||
// WITHOUT a tombstone (the caller also records the cid as blocked via the predicate above); unblocking
|
||||
// later lets the conversation re-import from chain. Returns false (leaving the store untouched) if the
|
||||
// persisted rows couldn't be removed, so the caller can avoid a store/DB divergence.
|
||||
bool deleteConversation(const std::string& conversationId, bool block);
|
||||
|
||||
// --- Outgoing (compose) ---
|
||||
// My chat public key (hex), or "" without an identity — goes in an outgoing header's "p".
|
||||
std::string identityPublicKeyHex() const;
|
||||
@@ -90,6 +104,7 @@ private:
|
||||
bool has_identity_ = false;
|
||||
ChatStore store_;
|
||||
ChatDatabase* db_ = nullptr; // optional; not owned
|
||||
std::function<bool(const std::string&)> blocked_pred_; // true => cid is blocked (drop its messages)
|
||||
};
|
||||
|
||||
} // namespace dragonx::chat
|
||||
|
||||
@@ -13,6 +13,7 @@ std::string ChatStore::dedupKey(const ChatMessage& message) {
|
||||
bool ChatStore::append(const ChatMessage& message) {
|
||||
if (!seen_.insert(dedupKey(message)).second) return false;
|
||||
messages_.push_back(message);
|
||||
++revision_;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -38,12 +39,24 @@ const ChatMessage* ChatStore::updateDelivery(const std::string& txid, ChatDelive
|
||||
for (auto& message : messages_) {
|
||||
if (message.txid == txid) {
|
||||
message.delivery = delivery;
|
||||
++revision_;
|
||||
return &message;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int ChatStore::countUnread(const std::function<bool(const std::string&)>& excluded,
|
||||
const std::function<std::int64_t(const std::string&)>& seenFor) const {
|
||||
int unread = 0;
|
||||
for (const auto& m : messages_) {
|
||||
if (m.direction != ChatDirection::Incoming) continue;
|
||||
if (excluded && excluded(m.conversation_id)) continue;
|
||||
if (m.timestamp > seenFor(m.conversation_id)) ++unread;
|
||||
}
|
||||
return unread;
|
||||
}
|
||||
|
||||
std::vector<std::string> ChatStore::conversationIds() const {
|
||||
std::vector<std::string> ids;
|
||||
std::unordered_set<std::string> seenIds;
|
||||
@@ -53,9 +66,27 @@ std::vector<std::string> ChatStore::conversationIds() const {
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::vector<ChatMessage> ChatStore::eraseConversation(const std::string& conversationId) {
|
||||
std::vector<ChatMessage> removed;
|
||||
std::vector<ChatMessage> kept;
|
||||
kept.reserve(messages_.size());
|
||||
for (auto& m : messages_) {
|
||||
if (m.conversation_id == conversationId) {
|
||||
seen_.erase(dedupKey(m));
|
||||
removed.push_back(std::move(m));
|
||||
} else {
|
||||
kept.push_back(std::move(m));
|
||||
}
|
||||
}
|
||||
messages_.swap(kept);
|
||||
if (!removed.empty()) ++revision_;
|
||||
return removed;
|
||||
}
|
||||
|
||||
void ChatStore::clear() {
|
||||
messages_.clear();
|
||||
seen_.clear();
|
||||
++revision_;
|
||||
}
|
||||
|
||||
} // namespace dragonx::chat
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include "chat_message.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
@@ -38,15 +40,32 @@ public:
|
||||
return out;
|
||||
}
|
||||
|
||||
// Remove every message in a conversation from the in-memory view and return the removed messages
|
||||
// (so the caller can delete/tombstone their persisted rows). Re-appends are prevented by the ingest
|
||||
// guard (blocked-cid predicate / DB tombstone), not here.
|
||||
std::vector<ChatMessage> eraseConversation(const std::string& conversationId);
|
||||
|
||||
std::size_t size() const { return messages_.size(); }
|
||||
bool empty() const { return messages_.empty(); }
|
||||
void clear();
|
||||
|
||||
// Monotonic counter bumped on every mutation (append / updateDelivery change / eraseConversation /
|
||||
// clear). Callers memoize expensive per-frame reads (conversation-list build, unread count) against it
|
||||
// so they only rebuild when the store actually changed.
|
||||
std::uint64_t revision() const { return revision_; }
|
||||
|
||||
// Count unread incoming messages in a SINGLE pass over the store: an incoming message counts when its
|
||||
// cid is not `excluded` and its timestamp is newer than `seenFor(cid)`. Avoids the per-conversation
|
||||
// copy+sort that conversation() does — the count doesn't need ordering.
|
||||
int countUnread(const std::function<bool(const std::string&)>& excluded,
|
||||
const std::function<std::int64_t(const std::string&)>& seenFor) const;
|
||||
|
||||
private:
|
||||
static std::string dedupKey(const ChatMessage& message);
|
||||
|
||||
std::vector<ChatMessage> messages_;
|
||||
std::unordered_set<std::string> seen_;
|
||||
std::uint64_t revision_ = 0;
|
||||
};
|
||||
|
||||
} // namespace dragonx::chat
|
||||
|
||||
@@ -108,6 +108,20 @@ void loadClamped(const json& j, const char* key, T& field, T lo, T hi)
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<Settings::LiteServerPreference> Settings::defaultLiteServers()
|
||||
{
|
||||
return {
|
||||
{"https://lite.dragonx.is", "DragonX Lite", true},
|
||||
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
|
||||
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
|
||||
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
|
||||
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
|
||||
{"https://lite5.dragonx.is", "DragonX Lite 5", true},
|
||||
{"https://lite6.dragonx.is", "DragonX Lite 6", true},
|
||||
{"https://lite7.dragonx.is", "DragonX Lite 7", true}
|
||||
};
|
||||
}
|
||||
|
||||
std::string Settings::getDefaultPath()
|
||||
{
|
||||
// Single per-platform, per-variant config dir (util::Platform::getConfigDir handles the
|
||||
@@ -157,6 +171,13 @@ 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>());
|
||||
}
|
||||
if (j.contains("blocked_chat_convs") && j["blocked_chat_convs"].is_array()) {
|
||||
blocked_chat_convs_.clear();
|
||||
for (const auto& c : j["blocked_chat_convs"])
|
||||
if (c.is_object() && c.contains("cid") && c["cid"].is_string())
|
||||
blocked_chat_convs_.push_back({c["cid"].get<std::string>(),
|
||||
c.value("name", 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_);
|
||||
@@ -204,6 +225,7 @@ bool Settings::load(const std::string& path)
|
||||
loadScalar(j, "console_text_color", console_text_color_);
|
||||
loadScalar(j, "console_zoom", console_zoom_);
|
||||
if (!(console_zoom_ >= 0.25f && console_zoom_ <= 4.0f)) console_zoom_ = 1.0f; // guard bad/NaN
|
||||
loadScalar(j, "console_auto_focus", console_auto_focus_);
|
||||
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
||||
hidden_addresses_.clear();
|
||||
for (const auto& a : j["hidden_addresses"])
|
||||
@@ -231,6 +253,12 @@ bool Settings::load(const std::string& path)
|
||||
}
|
||||
loadScalar(j, "wizard_completed", wizard_completed_);
|
||||
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
||||
loadScalar(j, "large_wallet_warned", large_wallet_warned_);
|
||||
if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
|
||||
empty_wallet_warning_acked_.clear();
|
||||
for (const auto& w : j["empty_wallet_warning_acked"])
|
||||
if (w.is_string()) empty_wallet_warning_acked_.insert(w.get<std::string>());
|
||||
}
|
||||
loadScalar(j, "encryption_pending", encryption_pending_);
|
||||
loadScalar(j, "daemon_update_prompted_size", daemon_update_prompted_size_);
|
||||
loadScalar(j, "active_wallet_file", active_wallet_file_);
|
||||
@@ -239,12 +267,15 @@ bool Settings::load(const std::string& path)
|
||||
loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
|
||||
loadScalar(j, "seed_migration_sweep_txid", seed_migration_sweep_txid_);
|
||||
loadScalar(j, "seed_migration_sweep_opid", seed_migration_sweep_opid_);
|
||||
loadScalar(j, "seed_migration_sweep_submitted", seed_migration_sweep_submitted_);
|
||||
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
|
||||
loadScalar(j, "unlock_duration", unlock_duration_);
|
||||
loadScalar(j, "pin_enabled", pin_enabled_);
|
||||
loadScalar(j, "keep_daemon_running", keep_daemon_running_);
|
||||
loadScalar(j, "stop_external_daemon", stop_external_daemon_);
|
||||
loadScalar(j, "max_connections", max_connections_);
|
||||
loadScalar(j, "stratum_host_enabled", stratum_host_enabled_);
|
||||
loadScalar(j, "stratum_allowip", stratum_allowip_);
|
||||
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
|
||||
const auto& lite = j["lite_wallet"];
|
||||
if (lite.contains("server_selection_mode")) {
|
||||
@@ -291,6 +322,16 @@ bool Settings::load(const std::string& path)
|
||||
}
|
||||
lite_servers_.push_back(preference);
|
||||
}
|
||||
// Surface newly-shipped default servers (e.g. lite6/lite7) on an existing
|
||||
// install whose saved list predates them. Servers are hidden (see
|
||||
// lite_hidden_servers_), never deleted, so appending a missing default won't
|
||||
// resurrect one the user intentionally removed.
|
||||
for (const auto& def : defaultLiteServers()) {
|
||||
bool present = false;
|
||||
for (const auto& s : lite_servers_)
|
||||
if (s.url == def.url) { present = true; break; }
|
||||
if (!present) lite_servers_.push_back(def);
|
||||
}
|
||||
}
|
||||
if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
|
||||
const auto v = lite["rollout_override"].get<std::string>();
|
||||
@@ -452,6 +493,13 @@ 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["blocked_chat_convs"] = json::array();
|
||||
for (const auto& b : blocked_chat_convs_) {
|
||||
json o;
|
||||
o["cid"] = b.cid;
|
||||
o["name"] = b.name;
|
||||
j["blocked_chat_convs"].push_back(o);
|
||||
}
|
||||
j["chat_emoji_color"] = chat_emoji_color_;
|
||||
j["chat_poll_rate_sec"] = chat_poll_rate_sec_;
|
||||
j["chat_bubble_style"] = chat_bubble_style_;
|
||||
@@ -478,6 +526,7 @@ bool Settings::save(const std::string& path)
|
||||
j["console_line_accents"] = console_line_accents_;
|
||||
j["console_text_color"] = console_text_color_;
|
||||
j["console_zoom"] = console_zoom_;
|
||||
j["console_auto_focus"] = console_auto_focus_;
|
||||
j["hidden_addresses"] = json::array();
|
||||
for (const auto& addr : hidden_addresses_)
|
||||
j["hidden_addresses"].push_back(addr);
|
||||
@@ -499,6 +548,10 @@ bool Settings::save(const std::string& path)
|
||||
}
|
||||
j["wizard_completed"] = wizard_completed_;
|
||||
j["seed_backup_reminded"] = seed_backup_reminded_;
|
||||
j["large_wallet_warned"] = large_wallet_warned_;
|
||||
j["empty_wallet_warning_acked"] = json::array();
|
||||
for (const auto& w : empty_wallet_warning_acked_)
|
||||
j["empty_wallet_warning_acked"].push_back(w);
|
||||
j["encryption_pending"] = encryption_pending_;
|
||||
j["daemon_update_prompted_size"] = daemon_update_prompted_size_;
|
||||
j["active_wallet_file"] = active_wallet_file_;
|
||||
@@ -507,12 +560,15 @@ bool Settings::save(const std::string& path)
|
||||
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
|
||||
j["seed_migration_sweep_txid"] = seed_migration_sweep_txid_;
|
||||
j["seed_migration_sweep_opid"] = seed_migration_sweep_opid_;
|
||||
j["seed_migration_sweep_submitted"] = seed_migration_sweep_submitted_;
|
||||
j["auto_lock_timeout"] = auto_lock_timeout_;
|
||||
j["unlock_duration"] = unlock_duration_;
|
||||
j["pin_enabled"] = pin_enabled_;
|
||||
j["keep_daemon_running"] = keep_daemon_running_;
|
||||
j["stop_external_daemon"] = stop_external_daemon_;
|
||||
j["max_connections"] = max_connections_;
|
||||
j["stratum_host_enabled"] = stratum_host_enabled_;
|
||||
j["stratum_allowip"] = stratum_allowip_;
|
||||
{
|
||||
json lite = json::object();
|
||||
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
|
||||
|
||||
+57
-8
@@ -144,6 +144,26 @@ public:
|
||||
hidden_chat_cids_.end());
|
||||
}
|
||||
|
||||
// Blocked chat conversations (by cid). Unlike hide (reversible, keeps messages), block DELETES the
|
||||
// local history AND suppresses every message for the cid — old and future — until you unblock, at
|
||||
// which point the conversation re-imports from the chain. The last-known peer name is kept so the
|
||||
// "Blocked" list can label the entry (its messages are gone from the local store).
|
||||
struct BlockedChatConv { std::string cid; std::string name; };
|
||||
bool isChatBlocked(const std::string& cid) const {
|
||||
for (const auto& b : blocked_chat_convs_) if (b.cid == cid) return true;
|
||||
return false;
|
||||
}
|
||||
void setChatBlocked(const std::string& cid, const std::string& name, bool blocked) {
|
||||
const bool already = isChatBlocked(cid);
|
||||
if (blocked && !already) blocked_chat_convs_.push_back({cid, name});
|
||||
else if (!blocked && already)
|
||||
blocked_chat_convs_.erase(
|
||||
std::remove_if(blocked_chat_convs_.begin(), blocked_chat_convs_.end(),
|
||||
[&](const BlockedChatConv& b) { return b.cid == cid; }),
|
||||
blocked_chat_convs_.end());
|
||||
}
|
||||
const std::vector<BlockedChatConv>& blockedChatConversations() const { return blocked_chat_convs_; }
|
||||
|
||||
// ── Chat-tab customization (chat settings modal + Settings → Chat & Contacts) ──────
|
||||
bool getChatEmojiColor() const { return chat_emoji_color_; }
|
||||
void setChatEmojiColor(bool v) { chat_emoji_color_ = v; }
|
||||
@@ -256,6 +276,9 @@ public:
|
||||
void setConsoleTextColor(bool v) { console_text_color_ = v; }
|
||||
float getConsoleZoom() const { return console_zoom_; }
|
||||
void setConsoleZoom(float v) { console_zoom_ = v; }
|
||||
// Auto-place the text cursor in the command box when the Console tab is opened.
|
||||
bool getConsoleAutoFocus() const { return console_auto_focus_; }
|
||||
void setConsoleAutoFocus(bool v) { console_auto_focus_ = v; }
|
||||
|
||||
// Hidden addresses (addresses hidden from the UI by the user)
|
||||
const std::set<std::string>& getHiddenAddresses() const { return hidden_addresses_; }
|
||||
@@ -327,6 +350,18 @@ public:
|
||||
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
|
||||
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
|
||||
|
||||
// One-time nudge when wallet.dat grows past the bloat threshold (re-armed if it shrinks back).
|
||||
bool getLargeWalletWarned() const { return large_wallet_warned_; }
|
||||
void setLargeWalletWarned(bool v) { large_wallet_warned_ = v; }
|
||||
|
||||
// Wallet filenames for which the one-time "this wallet is empty but a sibling holds funds"
|
||||
// warning has been dismissed. Keyed per active wallet file so switching to a different empty
|
||||
// wallet can warn again (see App::maybeWarnEmptyWalletWithFundedSiblings).
|
||||
bool isEmptyWalletWarnAcked(const std::string& walletFile) const {
|
||||
return empty_wallet_warning_acked_.count(walletFile) > 0;
|
||||
}
|
||||
void ackEmptyWalletWarn(const std::string& walletFile) { empty_wallet_warning_acked_.insert(walletFile); }
|
||||
|
||||
// Persisted the moment deferred (wizard) encryption is requested; cleared only once the wallet is
|
||||
// observed to be actually encrypted. Lets a quit/crash/failed-connect before it applies be detected
|
||||
// and surfaced (W2-2). NEVER stores the passphrase — only the fact that encryption was requested.
|
||||
@@ -361,6 +396,12 @@ public:
|
||||
// in the same write that persists the txid, so the txid always outranks it (see [[decideSeedMigrationResume]]).
|
||||
std::string getSeedMigrationSweepOpid() const { return seed_migration_sweep_opid_; }
|
||||
void setSeedMigrationSweepOpid(const std::string& v) { seed_migration_sweep_opid_ = v; }
|
||||
// Set TRUE synchronously when a sweep is broadcast; survives the stale-opid resume fallback so the
|
||||
// Sweep step knows a ~0 legacy balance may be an UNCONFIRMED in-flight sweep (funds not yet safe)
|
||||
// and must NOT short-circuit to adopt/swap wallet.dat. Cleared on confirmed-adopt / reset / a
|
||||
// failed submit. See [[decideSeedMigrationResume]] and App::renderSeedMigrationDialog (fund-safety).
|
||||
bool getSeedMigrationSweepSubmitted() const { return seed_migration_sweep_submitted_; }
|
||||
void setSeedMigrationSweepSubmitted(bool v) { seed_migration_sweep_submitted_ = v; }
|
||||
|
||||
// Security — auto-lock timeout (seconds; 0 = disabled)
|
||||
int getAutoLockTimeout() const { return auto_lock_timeout_; }
|
||||
@@ -385,6 +426,11 @@ public:
|
||||
// Daemon — maximum peer connections (0 = daemon default)
|
||||
int getMaxConnections() const { return max_connections_; }
|
||||
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
|
||||
// Host a RandomX stratum pool from the node (v1.3.0+ daemons). Empty allow-IP = loopback only (safe).
|
||||
bool getStratumHost() const { return stratum_host_enabled_; }
|
||||
void setStratumHost(bool v) { stratum_host_enabled_ = v; }
|
||||
const std::string& getStratumAllowIp() const { return stratum_allowip_; }
|
||||
void setStratumAllowIp(const std::string& v) { stratum_allowip_ = v; }
|
||||
|
||||
// Lite wallet server selection
|
||||
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
|
||||
@@ -399,6 +445,9 @@ public:
|
||||
void setLitePersistSelectedServer(bool persist) { lite_persist_selected_server_ = persist; }
|
||||
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
|
||||
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
|
||||
// Servers shipped as defaults. Also merged into an existing install's saved list on load
|
||||
// (see Settings::load), so upgrades surface newly-added servers (e.g. lite6/lite7).
|
||||
static std::vector<LiteServerPreference> defaultLiteServers();
|
||||
|
||||
// User-defined portfolio entries (Market tab). "All funds" is implicit, not stored here.
|
||||
const std::vector<PortfolioEntry>& getPortfolioEntries() const { return portfolio_entries_; }
|
||||
@@ -539,6 +588,7 @@ 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
|
||||
std::vector<BlockedChatConv> blocked_chat_convs_; // blocked chat conversations (cid + last-known name)
|
||||
// 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)
|
||||
@@ -580,11 +630,14 @@ private:
|
||||
bool console_line_accents_ = true; // left color accent bars in console output
|
||||
bool console_text_color_ = true; // per-channel text coloring in console output
|
||||
float console_zoom_ = 1.0f; // console output font zoom factor
|
||||
bool console_auto_focus_ = false; // focus the command input when the Console tab is opened (opt-in)
|
||||
std::set<std::string> hidden_addresses_;
|
||||
std::set<std::string> favorite_addresses_;
|
||||
std::map<std::string, AddressMeta> address_meta_;
|
||||
bool wizard_completed_ = false;
|
||||
bool seed_backup_reminded_ = false;
|
||||
bool large_wallet_warned_ = false;
|
||||
std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
|
||||
bool encryption_pending_ = false;
|
||||
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
|
||||
std::string active_wallet_file_ = "wallet.dat"; // -wallet=<name> the daemon loads (multi-wallet)
|
||||
@@ -593,12 +646,15 @@ private:
|
||||
std::string seed_migration_temp_dir_;
|
||||
std::string seed_migration_sweep_txid_;
|
||||
std::string seed_migration_sweep_opid_;
|
||||
bool seed_migration_sweep_submitted_ = false;
|
||||
int auto_lock_timeout_ = 900; // 15 minutes
|
||||
int unlock_duration_ = 600; // 10 minutes
|
||||
bool pin_enabled_ = false;
|
||||
bool keep_daemon_running_ = false;
|
||||
bool stop_external_daemon_ = false;
|
||||
int max_connections_ = 0; // 0 = daemon default
|
||||
bool stratum_host_enabled_ = false; // host a RandomX stratum pool from the node (v1.3.0+ daemons)
|
||||
std::string stratum_allowip_; // -stratumallowip filter (empty = daemon default: loopback only)
|
||||
|
||||
// Lite wallet server preferences. These are user/server settings only;
|
||||
// wallet secrets, wallet files, and lifecycle state are never stored here.
|
||||
@@ -609,14 +665,7 @@ private:
|
||||
bool lite_persist_selected_server_ = true;
|
||||
std::string lite_rollout_override_ = "auto"; // auto|force_on|force_off
|
||||
std::string lite_install_id_; // random local-only id; rollout-bucket source
|
||||
std::vector<LiteServerPreference> lite_servers_ = {
|
||||
{"https://lite.dragonx.is", "DragonX Lite", true},
|
||||
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
|
||||
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
|
||||
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
|
||||
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
|
||||
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
|
||||
};
|
||||
std::vector<LiteServerPreference> lite_servers_ = defaultLiteServers();
|
||||
std::set<std::string> lite_hidden_servers_; // server URLs hidden from the Network tab
|
||||
|
||||
std::vector<PortfolioEntry> portfolio_entries_; // Market tab custom portfolio groups
|
||||
|
||||
@@ -26,6 +26,7 @@ void DaemonController::syncSettings(const config::Settings* settings)
|
||||
if (!settings) return;
|
||||
daemon_->setDebugCategories(settings->getDebugCategories());
|
||||
daemon_->setMaxConnections(settings->getMaxConnections());
|
||||
daemon_->setStratumHosting(settings->getStratumHost(), settings->getStratumAllowIp());
|
||||
|
||||
std::string walletFile = settings->getActiveWalletFile();
|
||||
// The Wallets dialog opens an out-of-datadir wallet by linking it into the datadir under a
|
||||
@@ -71,9 +72,11 @@ DaemonController::State DaemonController::state() const
|
||||
return daemon_->getState();
|
||||
}
|
||||
|
||||
const std::string& DaemonController::lastError() const
|
||||
std::string DaemonController::lastError() const
|
||||
{
|
||||
return daemon_->getLastError();
|
||||
// By value — getLastError() now returns a mutex-locked COPY, so forwarding it by reference would
|
||||
// dangle (bind a reference to that temporary). (M-04 follow-through)
|
||||
return daemon_ ? daemon_->getLastError() : std::string();
|
||||
}
|
||||
|
||||
int DaemonController::crashCount() const
|
||||
|
||||
@@ -95,7 +95,7 @@ public:
|
||||
bool externalDaemonDetected() const;
|
||||
void clearExternalDaemonDetected();
|
||||
State state() const;
|
||||
const std::string& lastError() const;
|
||||
std::string lastError() const; // by value: EmbeddedDaemon::getLastError() returns a locked copy (M-04)
|
||||
int crashCount() const;
|
||||
int lastBlockHeight() const;
|
||||
double memoryUsageMB() const;
|
||||
|
||||
@@ -49,6 +49,16 @@ inline bool walletAutoRecovered(const std::string& out)
|
||||
&& out.find(".bak") != std::string::npos); // Recover moved wallet.dat aside
|
||||
}
|
||||
|
||||
// True when dragonxd (v1.3.0+) opened the wallet in DEGRADED mode: a wallet.dat that lost its hdchain
|
||||
// record (e.g. an old `-salvagewallet` output) now OPENS — existing keys stay intact and spendable —
|
||||
// instead of aborting, but the daemon can no longer derive NEW HD keys, so z_getnewaddress /
|
||||
// z_shieldcoinbase / a t->z z_sendmany fail with "HD seed not found". The only signal is a startup log
|
||||
// line; pre-1.3.0 daemons never emit it, so this classifier is naturally a no-op against them.
|
||||
inline bool walletOpenedDegraded(const std::string& out)
|
||||
{
|
||||
return out.find("Wallet opened in DEGRADED mode") != std::string::npos;
|
||||
}
|
||||
|
||||
// If `name` is a daemon salvage backup "wallet.<unixtime>.bak", return its timestamp; else -1.
|
||||
inline long long parseWalletSalvageBakTs(const std::string& name)
|
||||
{
|
||||
|
||||
@@ -205,11 +205,13 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
||||
"-ac_reward=300000000",
|
||||
"-ac_blocktime=36",
|
||||
"-ac_private=1",
|
||||
"-addnode=node.dragonx.is",
|
||||
// Seeds: seed.dragonx.is is a round-robin A record over the live seed set (self-updates without
|
||||
// a wallet release), with node1/node5 as static fallbacks — mirrors the daemon's own vSeeds.
|
||||
// Plain -addnode hostname resolution works on EVERY daemon version, and is load-bearing for
|
||||
// pre-1.3.0 daemons whose built-in peer discovery was broken (they rely on these to find peers).
|
||||
"-addnode=seed.dragonx.is",
|
||||
"-addnode=node1.dragonx.is",
|
||||
"-addnode=node2.dragonx.is",
|
||||
"-addnode=node3.dragonx.is",
|
||||
"-addnode=node4.dragonx.is",
|
||||
"-addnode=node5.dragonx.is",
|
||||
"-experimentalfeatures",
|
||||
"-developerencryptwallet",
|
||||
// Create fresh wallets from a BIP39 mnemonic so their 24-word phrase can be
|
||||
@@ -224,12 +226,11 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
||||
void EmbeddedDaemon::setState(State s, const std::string& message)
|
||||
{
|
||||
state_ = s;
|
||||
if (!message.empty()) {
|
||||
if (s == State::Error) {
|
||||
last_error_ = message;
|
||||
}
|
||||
if (!message.empty() && s == State::Error) {
|
||||
std::lock_guard<std::mutex> lk(error_mutex_); // dedicated mutex — never taken with output_mutex_ held
|
||||
last_error_ = message;
|
||||
}
|
||||
|
||||
|
||||
if (state_callback_) {
|
||||
state_callback_(s, message);
|
||||
}
|
||||
@@ -544,6 +545,15 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
||||
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
||||
}
|
||||
|
||||
// Host a RandomX stratum pool from this node (-stratum). Only v1.3.0+ daemons implement it; older
|
||||
// ones ignore the unknown flag (no fatal arg check), and the Settings toggle is gated on daemon
|
||||
// version, so this is only enabled against a daemon that supports it. Without -stratumallowip the
|
||||
// daemon serves loopback only (safe default); a subnet opens it to that LAN.
|
||||
if (stratum_enabled_) {
|
||||
args.push_back("-stratum");
|
||||
if (!stratum_allowip_.empty()) args.push_back("-stratumallowip=" + stratum_allowip_);
|
||||
}
|
||||
|
||||
// Active wallet file (multi-wallet). The daemon loads <datadir>/<name>. Only pass it for a
|
||||
// non-default name so the common case's command line is unchanged; skip during an isolated
|
||||
// start (seed migration manages its own throwaway wallet).
|
||||
@@ -621,12 +631,28 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
||||
// Forward declaration — defined after startProcess
|
||||
static DWORD findProcessByName(const char* name);
|
||||
|
||||
// Quote a single argument per the CommandLineToArgvW rules (MSDN) so a value containing a space or a
|
||||
// quote is delivered as ONE argv token to the daemon instead of splitting/corrupting argv (L-02).
|
||||
static std::string quoteWinArg(const std::string& arg) {
|
||||
if (!arg.empty() && arg.find_first_of(" \t\n\v\"") == std::string::npos) return arg;
|
||||
std::string out = "\"";
|
||||
for (size_t i = 0; ; ++i) {
|
||||
size_t nbs = 0;
|
||||
while (i < arg.size() && arg[i] == '\\') { ++nbs; ++i; }
|
||||
if (i == arg.size()) { out.append(nbs * 2, '\\'); break; }
|
||||
if (arg[i] == '"') { out.append(nbs * 2 + 1, '\\'); out.push_back('"'); }
|
||||
else { out.append(nbs, '\\'); out.push_back(arg[i]); }
|
||||
}
|
||||
out.push_back('"');
|
||||
return out;
|
||||
}
|
||||
|
||||
bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vector<std::string>& args)
|
||||
{
|
||||
// Build command line
|
||||
// Build command line (binary path always quoted; each arg quoted/escaped per Windows rules — L-02)
|
||||
std::string cmd = "\"" + binary_path + "\"";
|
||||
for (const auto& arg : args) {
|
||||
cmd += " " + arg;
|
||||
cmd += " " + quoteWinArg(arg);
|
||||
}
|
||||
DEBUG_LOGF("[INFO] Starting daemon: %s\n", cmd.c_str());
|
||||
|
||||
@@ -674,7 +700,10 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
||||
debug_log_path_.c_str(), debug_log_offset_);
|
||||
}
|
||||
|
||||
// Launch daemon with CREATE_NEW_CONSOLE (hidden via SW_HIDE).
|
||||
// Launch daemon windowless. Use CREATE_NO_WINDOW (NOT CREATE_NEW_CONSOLE): CREATE_NEW_CONSOLE
|
||||
// allocates a console window that briefly flashes on screen before SW_HIDE can hide it, which is
|
||||
// visible as a console-window flash on wallet launch. CREATE_NO_WINDOW gives the console child no
|
||||
// window at all (same approach as the xmrig launcher). The daemon logs to debug.log, not a console.
|
||||
// The daemon binary must NOT be in the data directory (%APPDATA%\Hush\DRAGONX)
|
||||
// — it must be in <exe_dir>/dragonx/ to avoid conflicts with lock files and data.
|
||||
STARTUPINFOA si;
|
||||
@@ -684,7 +713,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
||||
si.dwFlags = STARTF_USESHOWWINDOW;
|
||||
si.wShowWindow = SW_HIDE;
|
||||
ZeroMemory(&pi, sizeof(pi));
|
||||
|
||||
|
||||
char* cmd_line = _strdup(cmd.c_str());
|
||||
BOOL success = CreateProcessA(
|
||||
NULL,
|
||||
@@ -692,7 +721,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
||||
NULL,
|
||||
NULL,
|
||||
FALSE,
|
||||
CREATE_NEW_CONSOLE,
|
||||
CREATE_NO_WINDOW,
|
||||
NULL,
|
||||
work_dir.c_str(),
|
||||
&si,
|
||||
@@ -787,11 +816,21 @@ void EmbeddedDaemon::drainOutput()
|
||||
}
|
||||
|
||||
size_t currentSize = static_cast<size_t>(fileSize.QuadPart);
|
||||
// Truncation / rotation detection. dragonxd shrinks debug.log on startup (ShrinkDebugFile keeps
|
||||
// only the tail once it has grown large), so the file can become SMALLER than where we left off.
|
||||
// Our offset was set to the PRE-shrink size at spawn, so without this reset it stays stranded ahead
|
||||
// of the freshly-truncated file and we read NOTHING for the whole session — no block height (status
|
||||
// bar shows "Block: 0") and, worse, no witness-rebuild progress, since the "Setting Initial Sapling
|
||||
// Witness …" lines the warmup progress bar parses only exist in debug.log on Windows. On a shrink,
|
||||
// restart from the new beginning; the parser is monotonic and converges as it reaches current output.
|
||||
if (currentSize < debug_log_offset_) {
|
||||
debug_log_offset_ = 0;
|
||||
}
|
||||
if (currentSize <= debug_log_offset_) {
|
||||
CloseHandle(hFile);
|
||||
return; // No new data
|
||||
}
|
||||
|
||||
|
||||
// Seek to where we left off
|
||||
LARGE_INTEGER seekPos;
|
||||
seekPos.QuadPart = static_cast<LONGLONG>(debug_log_offset_);
|
||||
|
||||
@@ -79,7 +79,9 @@ public:
|
||||
/**
|
||||
* @brief Get last error message
|
||||
*/
|
||||
const std::string& getLastError() const { return last_error_; }
|
||||
// Copy under lock: last_error_ is written from the monitor thread (setState on an unexpected exit)
|
||||
// while the UI thread reads it — a reference would be a torn-read / use-after-free race (M-04).
|
||||
std::string getLastError() const { std::lock_guard<std::mutex> lk(error_mutex_); return last_error_; }
|
||||
|
||||
/**
|
||||
* @brief Get dragonxd process output (thread-safe copy)
|
||||
@@ -180,6 +182,7 @@ public:
|
||||
* @brief Set maximum peer connections (0 = use daemon default)
|
||||
*/
|
||||
void setMaxConnections(int v) { max_connections_ = v; }
|
||||
void setStratumHosting(bool enabled, const std::string& allowIp) { stratum_enabled_ = enabled; stratum_allowip_ = allowIp; }
|
||||
|
||||
/**
|
||||
* @brief Request a blockchain rescan on the next daemon start
|
||||
@@ -286,6 +289,7 @@ private:
|
||||
std::atomic<State> state_{State::Stopped};
|
||||
std::atomic<bool> external_daemon_detected_{false};
|
||||
std::string last_error_;
|
||||
mutable std::mutex error_mutex_; // protects last_error_ (written by main + monitor threads)
|
||||
mutable std::mutex output_mutex_; // protects process_output_
|
||||
std::string process_output_;
|
||||
StateCallback state_callback_;
|
||||
@@ -308,6 +312,8 @@ private:
|
||||
std::atomic<bool> should_stop_{false};
|
||||
std::set<std::string> debug_categories_;
|
||||
int max_connections_ = 0; // 0 = daemon default
|
||||
bool stratum_enabled_ = false; // -stratum: host a RandomX pool (v1.3.0+; older daemons ignore it)
|
||||
std::string stratum_allowip_; // -stratumallowip subnet (empty = daemon default: loopback only)
|
||||
std::string wallet_file_; // -wallet=<name> for the active wallet; empty/"wallet.dat" = default
|
||||
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
||||
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
||||
|
||||
+107
-66
@@ -23,6 +23,7 @@
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include "../util/logger.h"
|
||||
#include "../util/platform.h"
|
||||
#include "../util/pool_registry.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -89,8 +90,32 @@ static std::string getConfigDir() {
|
||||
// libcurl write callback
|
||||
static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) {
|
||||
auto* s = static_cast<std::string*>(userdata);
|
||||
s->append(static_cast<char*>(ptr), sz * n);
|
||||
return sz * n;
|
||||
const size_t add = sz * n;
|
||||
// Stats JSON (local xmrig HTTP API + pool API) is tiny; refuse an unbounded body from a hostile or
|
||||
// MITM'd endpoint so it can't grow this string until OOM. Returning < add aborts the transfer. (L-02)
|
||||
constexpr size_t kMaxStatsBytes = 1u << 20; // 1 MiB
|
||||
if (s->size() + add > kMaxStatsBytes) return 0;
|
||||
s->append(static_cast<char*>(ptr), add);
|
||||
return add;
|
||||
}
|
||||
|
||||
// True if `host` (already stripped of scheme+port) is a loopback/private/link-local/single-label target
|
||||
// that a public mining pool would never be — used to refuse a background stats GET to it (M-09).
|
||||
static bool hostLooksInternal(const std::string& host) {
|
||||
if (host.empty() || host == "localhost") return true;
|
||||
if (host.rfind("127.", 0) == 0 || host.rfind("10.", 0) == 0 ||
|
||||
host.rfind("192.168.", 0) == 0 || host.rfind("169.254.", 0) == 0) return true;
|
||||
if (host.rfind("172.", 0) == 0) { // 172.16.0.0 - 172.31.255.255
|
||||
const int second = std::atoi(host.c_str() + 4);
|
||||
if (second >= 16 && second <= 31) return true;
|
||||
}
|
||||
if (host.find(':') != std::string::npos) { // IPv6 literal: loopback / ULA / link-local
|
||||
if (host == "::1" || host.rfind("fc", 0) == 0 || host.rfind("fd", 0) == 0 ||
|
||||
host.rfind("fe80", 0) == 0) return true;
|
||||
}
|
||||
if (host.size() >= 6 && host.compare(host.size() - 6, 6, ".local") == 0) return true;
|
||||
if (host.find('.') == std::string::npos) return true; // bare single-label name = LAN/hosts, not a pool
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -100,9 +125,14 @@ static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) {
|
||||
XmrigManager::XmrigManager() = default;
|
||||
|
||||
XmrigManager::~XmrigManager() {
|
||||
should_stop_ = true;
|
||||
if (isRunning()) {
|
||||
stop(3000);
|
||||
}
|
||||
// Join a monitor thread left joinable by an unexpected xmrig exit (State::Error, so isRunning() is
|
||||
// false and stop() above was skipped) — std::thread's destructor would otherwise std::terminate(). (M-04)
|
||||
if (monitor_thread_.joinable())
|
||||
monitor_thread_.join();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -116,32 +146,18 @@ std::string XmrigManager::findXmrigBinary() {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Fallback: system PATH
|
||||
// Fallback: system PATH — windowless so it never flashes a console.
|
||||
#ifdef _WIN32
|
||||
FILE* f = _popen("where xmrig.exe 2>nul", "r");
|
||||
std::string out = util::Platform::runHiddenCapture("where xmrig.exe");
|
||||
#else
|
||||
FILE* f = popen("which xmrig 2>/dev/null", "r");
|
||||
#endif
|
||||
if (f) {
|
||||
char line[512];
|
||||
if (fgets(line, sizeof(line), f)) {
|
||||
std::string s(line);
|
||||
while (!s.empty() && (s.back() == '\n' || s.back() == '\r'))
|
||||
s.pop_back();
|
||||
if (!s.empty() && fs::exists(s)) {
|
||||
#ifdef _WIN32
|
||||
_pclose(f);
|
||||
#else
|
||||
pclose(f);
|
||||
#endif
|
||||
return s;
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
_pclose(f);
|
||||
#else
|
||||
pclose(f);
|
||||
std::string out = util::Platform::runHiddenCapture("which xmrig");
|
||||
#endif
|
||||
{
|
||||
std::string s = out;
|
||||
const auto nl = s.find_first_of("\r\n"); // first line only
|
||||
if (nl != std::string::npos) s.erase(nl);
|
||||
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.pop_back();
|
||||
if (!s.empty() && fs::exists(s)) return s;
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -208,23 +224,43 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
|
||||
|
||||
try {
|
||||
fs::create_directories(fs::path(outPath).parent_path());
|
||||
std::ofstream ofs(outPath, std::ios::trunc);
|
||||
if (!ofs.is_open()) {
|
||||
last_error_ = "Cannot write xmrig config: " + outPath;
|
||||
const std::string dumped = j.dump(4);
|
||||
#ifndef _WIN32
|
||||
// Create the config 0600 AT CREATION (open with mode) so the API token + wallet address are never
|
||||
// in a world/group-readable file — even for a local attacker who opened it in the old
|
||||
// create-then-chmod window and held the fd open across the chmod. (L-01)
|
||||
int fd = ::open(outPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd < 0) {
|
||||
setLastError("Cannot write xmrig config: " + outPath);
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
// 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);
|
||||
size_t off = 0;
|
||||
bool wrote = true;
|
||||
while (off < dumped.size()) {
|
||||
ssize_t nw = ::write(fd, dumped.data() + off, dumped.size() - off);
|
||||
if (nw <= 0) { wrote = false; break; }
|
||||
off += static_cast<size_t>(nw);
|
||||
}
|
||||
::close(fd);
|
||||
if (!wrote) {
|
||||
setLastError("Cannot write xmrig config: " + outPath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
std::ofstream ofs(outPath, std::ios::trunc);
|
||||
if (!ofs.is_open()) {
|
||||
setLastError("Cannot write xmrig config: " + outPath);
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
ofs << dumped;
|
||||
ofs.close();
|
||||
return true;
|
||||
#endif
|
||||
} catch (const std::exception& e) {
|
||||
last_error_ = std::string("Config write error: ") + e.what();
|
||||
setLastError(std::string("Config write error: ") + e.what());
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -236,19 +272,22 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
|
||||
|
||||
bool XmrigManager::start(const Config& cfg) {
|
||||
if (state_ == State::Running || state_ == State::Starting) {
|
||||
last_error_ = "Already running";
|
||||
setLastError("Already running");
|
||||
DEBUG_LOGF("[WARN] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
state_ = State::Starting;
|
||||
should_stop_ = false;
|
||||
last_error_.clear();
|
||||
setLastError(std::string());
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
process_output_.clear();
|
||||
}
|
||||
stats_ = PoolStats{};
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||
stats_ = PoolStats{};
|
||||
}
|
||||
|
||||
// Extract pool hostname for stats API queries
|
||||
{
|
||||
@@ -265,7 +304,7 @@ bool XmrigManager::start(const Config& cfg) {
|
||||
// Find binary
|
||||
std::string binary = findXmrigBinary();
|
||||
if (binary.empty()) {
|
||||
last_error_ = "xmrig binary not found";
|
||||
setLastError("xmrig binary not found");
|
||||
state_ = State::Error;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: xmrig binary not found\n");
|
||||
return false;
|
||||
@@ -293,7 +332,11 @@ bool XmrigManager::start(const Config& cfg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start monitor thread
|
||||
// Join a prior monitor thread before move-assigning: if xmrig exited unexpectedly, monitorProcess set
|
||||
// State::Error and returned, leaving monitor_thread_ joinable — move-assigning over a joinable
|
||||
// std::thread calls std::terminate() and aborts the whole wallet. (M-04)
|
||||
if (monitor_thread_.joinable())
|
||||
monitor_thread_.join();
|
||||
monitor_thread_ = std::thread(&XmrigManager::monitorProcess, this);
|
||||
state_ = State::Running;
|
||||
DEBUG_LOGF("[INFO] XmrigManager: started\n");
|
||||
@@ -368,7 +411,7 @@ bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string&
|
||||
|
||||
HANDLE hRead = nullptr, hWrite = nullptr;
|
||||
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) {
|
||||
last_error_ = "CreatePipe failed";
|
||||
setLastError("CreatePipe failed");
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -400,7 +443,7 @@ bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string&
|
||||
char errBuf[256];
|
||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
NULL, err, 0, errBuf, sizeof(errBuf), NULL);
|
||||
last_error_ = "CreateProcess failed for xmrig (error " + std::to_string(err) + "): " + errBuf;
|
||||
setLastError("CreateProcess failed for xmrig (error " + std::to_string(err) + "): " + errBuf);
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\nCommand: %s\n", last_error_.c_str(), cmdLine.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -451,14 +494,14 @@ void XmrigManager::drainOutput() {
|
||||
bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads) {
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) != 0) {
|
||||
last_error_ = "pipe() failed";
|
||||
setLastError("pipe() failed");
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
last_error_ = "fork() failed";
|
||||
setLastError("fork() failed");
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
@@ -629,7 +672,7 @@ void XmrigManager::monitorProcess() {
|
||||
if (GetExitCodeProcess(process_handle_, &exitCode) && exitCode != STILL_ACTIVE) {
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: process exited (code %lu)\n", exitCode);
|
||||
state_ = State::Error;
|
||||
last_error_ = "xmrig process exited unexpectedly";
|
||||
setLastError("xmrig process exited unexpectedly");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -640,7 +683,7 @@ void XmrigManager::monitorProcess() {
|
||||
if (ret == process_pid_ || ret < 0) {
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: process exited (waitpid=%d)\n", ret);
|
||||
state_ = State::Error;
|
||||
last_error_ = "xmrig process exited unexpectedly";
|
||||
setLastError("xmrig process exited unexpectedly");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -780,6 +823,11 @@ void XmrigManager::fetchPoolApiStats() {
|
||||
// own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore
|
||||
// /api/pools); unknown/custom hosts fall back to the .is convention.
|
||||
const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_);
|
||||
// SSRF guard: for an UNKNOWN (user-typed) pool host, don't let the wallet issue a background GET to a
|
||||
// loopback/private/link-local/single-label target — those aren't public mining pools, and a
|
||||
// paste-a-pool-config lure could otherwise point us at an internal host. Known pools use their trusted
|
||||
// registry statsUrl and are exempt. (M-09)
|
||||
if (!known && hostLooksInternal(pool_host_)) return;
|
||||
const std::string url = known ? known->statsUrl
|
||||
: ("https://" + pool_host_ + "/api/stats");
|
||||
|
||||
@@ -858,25 +906,18 @@ void XmrigManager::startVersionDetection()
|
||||
std::thread([]() {
|
||||
const std::string bin = findXmrigBinary();
|
||||
std::string ver;
|
||||
if (!bin.empty()) {
|
||||
const std::string cmd = "\"" + bin + "\" --version 2>&1";
|
||||
#ifdef _WIN32
|
||||
FILE* fp = _popen(cmd.c_str(), "r");
|
||||
#else
|
||||
FILE* fp = popen(cmd.c_str(), "r");
|
||||
#endif
|
||||
if (fp) {
|
||||
std::string out;
|
||||
char buf[256];
|
||||
size_t n;
|
||||
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n);
|
||||
#ifdef _WIN32
|
||||
_pclose(fp);
|
||||
#else
|
||||
pclose(fp);
|
||||
#endif
|
||||
ver = parseMinerVersion(out);
|
||||
}
|
||||
// Don't hand a path containing shell/cmd metacharacters to popen()'s shell — bin is normally an
|
||||
// app-controlled path, but this closes command injection if it ever isn't. (M-10)
|
||||
// Reject only chars that stay shell-special INSIDE the double-quotes we wrap bin in ("\"" + bin + "\"")
|
||||
// on cmd.exe or /bin/sh. Parens are inert when quoted, so they're excluded — otherwise common Windows
|
||||
// paths like "C:\Program Files (x86)\..." would be rejected and version detection would silently fail. (M-10)
|
||||
const bool binShellSafe =
|
||||
!bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos;
|
||||
if (binShellSafe) {
|
||||
// Windowless capture (mergeStderr: xmrig may print --version to stderr) — never flashes.
|
||||
const std::string cmd = "\"" + bin + "\" --version";
|
||||
const std::string out = util::Platform::runHiddenCapture(cmd, /*mergeStderr=*/true);
|
||||
if (!out.empty()) ver = parseMinerVersion(out);
|
||||
}
|
||||
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
||||
g_installed_ver = ver;
|
||||
|
||||
@@ -86,8 +86,10 @@ public:
|
||||
bool isRunning() const;
|
||||
State getState() const { return state_.load(std::memory_order_relaxed); }
|
||||
|
||||
const PoolStats& getStats() const { return stats_; }
|
||||
const std::string& getLastError() const { return last_error_; }
|
||||
// Return COPIES under lock: stats_ and last_error_ are mutated by the monitor thread while the UI
|
||||
// thread reads them, so handing out a reference is a torn-read / use-after-free race (M-03, M-04).
|
||||
PoolStats getStats() const { std::lock_guard<std::mutex> lk(stats_mutex_); return stats_; }
|
||||
std::string getLastError() const { std::lock_guard<std::mutex> lk(error_mutex_); return last_error_; }
|
||||
|
||||
/// Thread count requested at start() — available immediately, unlike
|
||||
/// PoolStats::threads_active which requires an API response.
|
||||
@@ -156,11 +158,14 @@ private:
|
||||
void monitorProcess();
|
||||
void drainOutput();
|
||||
void appendOutput(const char* data, size_t len);
|
||||
// Set last_error_ under error_mutex_ (writers run on both the main thread and the monitor thread).
|
||||
void setLastError(std::string e) { std::lock_guard<std::mutex> lk(error_mutex_); last_error_ = std::move(e); }
|
||||
void fetchStatsHttp(); // Blocking HTTP call — runs on monitor thread only
|
||||
void fetchPoolApiStats(); // Fetch pool-side stats (hashrate) from pool HTTP API
|
||||
|
||||
std::atomic<State> state_{State::Stopped};
|
||||
std::string last_error_;
|
||||
mutable std::mutex error_mutex_; // guards last_error_ (written by main + monitor threads)
|
||||
|
||||
mutable std::mutex output_mutex_;
|
||||
std::string process_output_;
|
||||
|
||||
@@ -68,8 +68,9 @@ bool AddressBook::load()
|
||||
}
|
||||
|
||||
DEBUG_LOGF("Address book loaded: %zu entries\n", entries_.size());
|
||||
++revision_;
|
||||
return true;
|
||||
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Error loading address book: %s\n", e.what());
|
||||
return false;
|
||||
@@ -121,6 +122,7 @@ bool AddressBook::addEntry(const AddressBookEntry& entry)
|
||||
}
|
||||
|
||||
entries_.push_back(entry);
|
||||
++revision_;
|
||||
return save();
|
||||
}
|
||||
|
||||
@@ -136,6 +138,7 @@ bool AddressBook::updateEntry(size_t index, const AddressBookEntry& entry)
|
||||
}
|
||||
|
||||
entries_[index] = entry;
|
||||
++revision_;
|
||||
return save();
|
||||
}
|
||||
|
||||
@@ -146,6 +149,7 @@ bool AddressBook::removeEntry(size_t index)
|
||||
}
|
||||
|
||||
entries_.erase(entries_.begin() + index);
|
||||
++revision_;
|
||||
return save();
|
||||
}
|
||||
|
||||
@@ -159,7 +163,7 @@ int AddressBook::reattachLegacyScopes(const std::string& scopeId)
|
||||
e.scope = scopeId;
|
||||
++rescoped;
|
||||
}
|
||||
if (rescoped > 0) save();
|
||||
if (rescoped > 0) { ++revision_; save(); }
|
||||
return rescoped;
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -4,6 +4,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -121,11 +122,18 @@ public:
|
||||
*/
|
||||
size_t size() const { return entries_.size(); }
|
||||
|
||||
/**
|
||||
* @brief Monotonic counter bumped on every content change (add/update/remove/load/sweep). Consumers
|
||||
* (e.g. the chat conversation-list memo) key their caches off this so an IN-PLACE edit — a rename or
|
||||
* address change that keeps size() constant — still invalidates them.
|
||||
*/
|
||||
std::uint64_t revision() const { return revision_; }
|
||||
|
||||
/**
|
||||
* @brief UI-sweep ONLY: replace the in-memory entries WITHOUT persisting to disk, so the sweep can
|
||||
* seed demo contacts and restore the real book without a disk write. Do not use outside the sweep.
|
||||
*/
|
||||
void sweepSetEntries(std::vector<AddressBookEntry> e) { entries_ = std::move(e); }
|
||||
void sweepSetEntries(std::vector<AddressBookEntry> e) { entries_ = std::move(e); ++revision_; }
|
||||
|
||||
/**
|
||||
* @brief Check if empty
|
||||
@@ -134,6 +142,7 @@ public:
|
||||
|
||||
private:
|
||||
std::vector<AddressBookEntry> entries_;
|
||||
std::uint64_t revision_ = 0;
|
||||
std::string file_path_;
|
||||
};
|
||||
|
||||
|
||||
@@ -34,4 +34,12 @@ inline MigrationResume decideSeedMigrationResume(bool pending,
|
||||
return MigrationResume::SweepGate;
|
||||
}
|
||||
|
||||
// At the Sweep step with a ~0 legacy balance, may we offer the direct "no funds -> adopt" short-cut
|
||||
// (swap wallet.dat immediately)? Only when NO sweep has been submitted — a genuinely empty wallet.
|
||||
// If a sweep WAS submitted, a ~0 balance is ambiguous: the sweep tx may be unconfirmed in the mempool
|
||||
// and could revert on a drop/reorg, so adopting now can strand the funds in the moved-aside backup.
|
||||
// In that case adopt must go through the confirmation-gated path (>=1 conf) instead — never swap
|
||||
// wallet.dat on an unconfirmed sweep. (fund-safety: audit)
|
||||
inline bool sweepGateMayAdoptEmptyWallet(bool sweepSubmitted) { return !sweepSubmitted; }
|
||||
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -19,12 +19,13 @@ std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>
|
||||
for (size_t i = 0; i < addresses.size(); ++i) {
|
||||
const auto& address = addresses[i];
|
||||
if (!address.isSpendable()) continue;
|
||||
if (requirePositiveBalance && address.balance <= 0.0) continue;
|
||||
// Rank/filter by the CONFIRMED balance — an address holding only 0-conf change can't be sent from.
|
||||
if (requirePositiveBalance && address.spendableBalance <= 0.0) continue;
|
||||
indices.push_back(i);
|
||||
}
|
||||
|
||||
std::sort(indices.begin(), indices.end(), [&](size_t lhs, size_t rhs) {
|
||||
return addresses[lhs].balance > addresses[rhs].balance;
|
||||
return addresses[lhs].spendableBalance > addresses[rhs].spendableBalance;
|
||||
});
|
||||
return indices;
|
||||
}
|
||||
@@ -34,8 +35,8 @@ int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses)
|
||||
int bestIndex = -1;
|
||||
double bestBalance = 0.0;
|
||||
for (size_t i = 0; i < addresses.size(); ++i) {
|
||||
if (addresses[i].isSpendable() && addresses[i].balance > bestBalance) {
|
||||
bestBalance = addresses[i].balance;
|
||||
if (addresses[i].isSpendable() && addresses[i].spendableBalance > bestBalance) {
|
||||
bestBalance = addresses[i].spendableBalance;
|
||||
bestIndex = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
|
||||
+26
-8
@@ -21,13 +21,17 @@ namespace dragonx {
|
||||
*/
|
||||
struct AddressInfo {
|
||||
std::string address;
|
||||
double balance = 0.0;
|
||||
double balance = 0.0; // DISPLAY total incl. pending 0-conf change (minconf=0)
|
||||
std::string type; // "shielded" or "transparent"
|
||||
bool has_spending_key = true; // false for view-only (imported via z_importviewingkey)
|
||||
|
||||
// For display
|
||||
std::string label;
|
||||
|
||||
|
||||
// CONFIRMED balance (minconf>=1) — what z_sendmany can actually spend now. Kept last so positional
|
||||
// brace-init of the leading fields (used in tests) still compiles.
|
||||
double spendableBalance = 0.0;
|
||||
|
||||
// Derived
|
||||
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
|
||||
bool isShielded() const { return type == "shielded"; }
|
||||
@@ -145,7 +149,13 @@ struct SyncInfo {
|
||||
float witness_progress = 0.0f; // 0.0 - 1.0, within the current sub-phase
|
||||
int witness_remaining = 0; // blocks left in the cache walk (0 if unknown / phase 1)
|
||||
|
||||
bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
|
||||
// True only when the daemon reports a positive peer-derived tip (longestchain > 0). A node with
|
||||
// no peers reports longestchain == 0, so `blocks` is then only the LOCAL best height and cannot be
|
||||
// trusted as "at the network tip". isSynced() gates on this so a peerless / cold-started node is
|
||||
// never reported synced (which would enable Send against a stale balance). (audit: peerless-synced)
|
||||
bool tip_known = false;
|
||||
|
||||
bool isSynced() const { return tip_known && !syncing && blocks > 0 && blocks >= headers - 2; }
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -252,12 +262,18 @@ struct WalletState {
|
||||
// Sync status
|
||||
SyncInfo sync;
|
||||
|
||||
// Balances (named to match UI usage)
|
||||
double privateBalance = 0.0; // shielded balance
|
||||
// Balances (named to match UI usage). These are the DISPLAY totals — minconf=0, so they include the
|
||||
// user's own pending change and don't crater during an unconfirmed send.
|
||||
double privateBalance = 0.0; // shielded balance (display, incl. pending change)
|
||||
double transparentBalance = 0.0;
|
||||
double totalBalance = 0.0;
|
||||
double unconfirmedBalance = 0.0;
|
||||
|
||||
double unconfirmedBalance = 0.0; // = totalBalance - spendableTotalBalance (the pending portion)
|
||||
// CONFIRMED / spendable totals (minconf>=1) — what can actually be sent right now. z_sendmany runs at
|
||||
// minconf=1, so the Send form / Max / spend validation must size off these, never the display totals.
|
||||
double spendablePrivateBalance = 0.0;
|
||||
double spendableTransparentBalance = 0.0;
|
||||
double spendableTotalBalance = 0.0;
|
||||
|
||||
// Aliases for backward compatibility
|
||||
double& shielded_balance = privateBalance;
|
||||
double& transparent_balance = transparentBalance;
|
||||
@@ -302,6 +318,7 @@ struct WalletState {
|
||||
|
||||
// Timestamps for refresh logic
|
||||
int64_t last_balance_update = 0;
|
||||
int64_t last_address_update = 0; // set when an address-list refresh applies; 0 = never loaded yet
|
||||
int64_t last_tx_update = 0;
|
||||
int64_t last_peer_update = 0;
|
||||
int64_t last_mining_update = 0;
|
||||
@@ -325,6 +342,7 @@ struct WalletState {
|
||||
sync = SyncInfo{};
|
||||
privateBalance = transparentBalance = totalBalance = 0.0;
|
||||
unconfirmedBalance = 0.0;
|
||||
spendablePrivateBalance = spendableTransparentBalance = spendableTotalBalance = 0.0;
|
||||
encrypted = false;
|
||||
locked = false;
|
||||
unlocked_until = 0;
|
||||
@@ -343,7 +361,7 @@ struct WalletState {
|
||||
// refresh" stamps. Otherwise the pre-teardown time survives and, on reconnect, the staleness
|
||||
// badge (and any "updated X ago" reader) briefly reports it as current until the first refresh
|
||||
// re-stamps it. All readers treat 0 as "never" (formatTimeAgoShort/timeAgo return "").
|
||||
last_balance_update = last_tx_update = last_peer_update = last_mining_update = 0;
|
||||
last_balance_update = last_address_update = last_tx_update = last_peer_update = last_mining_update = 0;
|
||||
}
|
||||
|
||||
// Rebuild combined addresses list from z/t lists
|
||||
|
||||
+55
-5
@@ -1263,6 +1263,36 @@ int main(int argc, char* argv[])
|
||||
SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale));
|
||||
}
|
||||
|
||||
// DEV/TEST hook (dormant unless the env is set): DRAGONX_WIN_GEOM="WxH" forces an exact window
|
||||
// size, placing it on the largest display that can hold it and bypassing the primary-monitor
|
||||
// clamp. Lets a headless WSLg sweep render at sizes wider than the 1280 primary (e.g. 2560x1440
|
||||
// on the mirrored 4K/1440p outputs). Needs the x11 backend so absolute positioning takes effect.
|
||||
int wantW = 0, wantH = 0;
|
||||
if (const char* geom = std::getenv("DRAGONX_WIN_GEOM"))
|
||||
sscanf(geom, "%dx%d", &wantW, &wantH);
|
||||
if (wantW > 0 && wantH > 0) {
|
||||
int count = 0;
|
||||
SDL_DisplayID* disp = SDL_GetDisplays(&count);
|
||||
SDL_DisplayID best = 0; SDL_Rect bestUsable{0, 0, 0, 0};
|
||||
for (int i = 0; i < count; ++i) {
|
||||
SDL_Rect u;
|
||||
if (!SDL_GetDisplayUsableBounds(disp[i], &u)) continue;
|
||||
bool fits = (u.w >= wantW && u.h >= wantH);
|
||||
bool bestFits = (bestUsable.w >= wantW && bestUsable.h >= wantH);
|
||||
// Prefer a display that fits; among those, the smallest; else the largest available.
|
||||
if ((fits && !bestFits) ||
|
||||
(fits && bestFits && (long)u.w * u.h < (long)bestUsable.w * bestUsable.h) ||
|
||||
(!fits && !bestFits && (long)u.w * u.h > (long)bestUsable.w * bestUsable.h)) {
|
||||
bestUsable = u; best = disp[i];
|
||||
}
|
||||
}
|
||||
if (disp) SDL_free(disp);
|
||||
if (best) {
|
||||
SDL_SetWindowMinimumSize(window, 320, 240);
|
||||
SDL_SetWindowPosition(window, bestUsable.x + 10, bestUsable.y + 10);
|
||||
SDL_SetWindowSize(window, wantW, wantH);
|
||||
}
|
||||
} else {
|
||||
// Clamp to the current display's work area — runs on EVERY startup (this clamp used to live
|
||||
// inside the HiDPI branch, so a size saved on a larger/disconnected monitor could open the
|
||||
// window off-screen or bigger than the screen on a same-DPI cold start).
|
||||
@@ -1281,6 +1311,7 @@ int main(int argc, char* argv[])
|
||||
DEBUG_LOGF("Startup: window fitted %dx%d -> %dx%d (scale %.2f)\n",
|
||||
curW, curH, newW, newH, currentDpiScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
winlog("STARTUP savedSize=%dx%d currentDpiScale=%.3f", savedWinW, savedWinH, currentDpiScale);
|
||||
@@ -1469,6 +1500,7 @@ int main(int argc, char* argv[])
|
||||
// WINDOW_RESIZED events during the transition can't corrupt savedSizeForScale / lastKnownW/H.
|
||||
int dpiSettleFrames = 0;
|
||||
SDL_DisplayID lastLoggedDisplay = 0; // [WINLOG] throttle: log MOVED only when the display changes
|
||||
Uint64 minimizedLastTickMs = 0; // real-clock tick for the minimized "keep syncing" update
|
||||
{
|
||||
float s = dragonx::ui::material::Typography::instance().getDpiScale();
|
||||
int w = 0, h = 0;
|
||||
@@ -1552,6 +1584,7 @@ int main(int argc, char* argv[])
|
||||
// Window restored from minimized — trigger immediate data refresh
|
||||
if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED &&
|
||||
waitEvent.window.windowID == SDL_GetWindowID(window)) {
|
||||
app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
|
||||
app.refreshNow();
|
||||
}
|
||||
// Handle DPI change that arrived while idle (same logic as poll loop)
|
||||
@@ -1681,6 +1714,7 @@ int main(int argc, char* argv[])
|
||||
// Window restored from minimized — trigger immediate data refresh
|
||||
if (event.type == SDL_EVENT_WINDOW_RESTORED &&
|
||||
event.window.windowID == SDL_GetWindowID(window)) {
|
||||
app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
|
||||
app.refreshNow();
|
||||
}
|
||||
// Handle DPI/display scale changes (e.g. window dragged to a
|
||||
@@ -1701,13 +1735,28 @@ int main(int argc, char* argv[])
|
||||
|
||||
// Check if window is minimized
|
||||
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
|
||||
// Still check shouldQuit while minimized to avoid hang
|
||||
if (app.shouldQuit()) {
|
||||
running = false;
|
||||
}
|
||||
SDL_Delay(10);
|
||||
// Keep the wallet syncing while minimized: run the logic update (drains RPC results, ticks the
|
||||
// refresh scheduler, keeps the daemon connection/reconnect + sync status live) but skip the
|
||||
// ImGui frame + GPU present since nothing is visible. app.update() only reads
|
||||
// GetIO()/GetTime()/IsAnyItemActive() — all valid outside a frame — so it's safe without a
|
||||
// NewFrame; feed it a real-clock DeltaTime (NewFrame, which normally sets it, is skipped) and
|
||||
// let app.update() clamp it. Throttled to ~5 Hz so CPU stays near-idle (refresh cadences are
|
||||
// seconds-scale). shouldQuit is still checked so a quit request never hangs behind minimize.
|
||||
Uint64 nowMs = SDL_GetTicks();
|
||||
float minDelta = (minimizedLastTickMs == 0) ? 0.001f
|
||||
: (float)(nowMs - minimizedLastTickMs) / 1000.0f;
|
||||
minimizedLastTickMs = nowMs;
|
||||
ImGui::GetIO().DeltaTime = (minDelta > 0.0f) ? minDelta : 0.001f;
|
||||
try {
|
||||
app.update();
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("[Main] minimized app.update() threw: %s\n", e.what());
|
||||
} catch (...) {}
|
||||
if (app.shouldQuit()) running = false;
|
||||
SDL_Delay(200);
|
||||
continue;
|
||||
}
|
||||
minimizedLastTickMs = 0; // visible again — reset the minimized clock
|
||||
|
||||
// --- PerfLog: begin frame ---
|
||||
dragonx::util::PerfLog::instance().beginFrame();
|
||||
@@ -2127,6 +2176,7 @@ int main(int argc, char* argv[])
|
||||
// deadlocks waiting for detached pthreads. On Linux, static
|
||||
// destructors and atexit handlers can also block. _Exit() bypasses
|
||||
// all of that.
|
||||
app.wipeSecrets(); // _Exit() below bypasses ~App(), so scrub secret buffers here (L-05)
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
_Exit(0);
|
||||
|
||||
@@ -40,6 +40,9 @@ static const EmbeddedResource s_resources[] = {
|
||||
{ g_dragonx_cli_exe_data, g_dragonx_cli_exe_size, RESOURCE_DRAGONX_CLI },
|
||||
{ g_dragonx_tx_exe_data, g_dragonx_tx_exe_size, RESOURCE_DRAGONX_TX },
|
||||
#endif
|
||||
#ifdef HAS_EMBEDDED_WALLET_REBUILD
|
||||
{ g_dragonx_wallet_rebuild_exe_data, g_dragonx_wallet_rebuild_exe_size, RESOURCE_DRAGONX_WALLET_REBUILD },
|
||||
#endif
|
||||
#ifdef HAS_EMBEDDED_XMRIG
|
||||
{ g_xmrig_exe_data, g_xmrig_exe_size, RESOURCE_XMRIG },
|
||||
#endif
|
||||
@@ -436,6 +439,24 @@ bool extractEmbeddedResources()
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAS_EMBEDDED_WALLET_REBUILD
|
||||
// Offline wallet-rebuild recovery helper — extracted next to the daemon so a bare, self-extracting
|
||||
// ObsidianDragon.exe still offers "Repair automatically" (findWalletRebuildHelper() checks this dir).
|
||||
const EmbeddedResource* rebuildRes = getEmbeddedResource(RESOURCE_DRAGONX_WALLET_REBUILD);
|
||||
if (rebuildRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_WALLET_REBUILD;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting dragonx-wallet-rebuild (%zu MB)...\n", rebuildRes->size / (1024*1024));
|
||||
if (!extractResource(rebuildRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
else { chmod(dest.c_str(), 0755); }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Best-effort cleanup of any ".old" binaries left behind by a previous in-use replacement.
|
||||
// Once the old daemon/xmrig process has exited, the file is no longer locked and removes cleanly;
|
||||
// if it's still running, the remove fails harmlessly and we retry on the next startup.
|
||||
@@ -450,6 +471,32 @@ bool extractEmbeddedResources()
|
||||
return success;
|
||||
}
|
||||
|
||||
std::string ensureWalletRebuildHelperExtracted()
|
||||
{
|
||||
#ifdef HAS_EMBEDDED_WALLET_REBUILD
|
||||
const EmbeddedResource* res = getEmbeddedResource(RESOURCE_DRAGONX_WALLET_REBUILD);
|
||||
if (!res || res->size == 0) return {};
|
||||
#ifdef _WIN32
|
||||
const char sep = '\\';
|
||||
#else
|
||||
const char sep = '/';
|
||||
#endif
|
||||
const std::string dir = getDaemonDirectory();
|
||||
const std::string dest = dir + sep + RESOURCE_DRAGONX_WALLET_REBUILD;
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(dest, ec)) return dest; // already extracted
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
if (!extractResource(res, dest)) return {};
|
||||
#ifndef _WIN32
|
||||
chmod(dest.c_str(), 0755);
|
||||
#endif
|
||||
DEBUG_LOGF("[INFO] Extracted wallet-rebuild helper on demand: %s\n", dest.c_str());
|
||||
return dest;
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string getDaemonDirectory()
|
||||
{
|
||||
// Daemon binaries live in %APPDATA%/ObsidianDragon/dragonx/ (Windows) or
|
||||
|
||||
@@ -55,6 +55,12 @@ BundledDaemonInfo getBundledDaemonInfo();
|
||||
// caller should stop the daemon first. Returns true if all present resources were written.
|
||||
bool reextractBundledDaemon();
|
||||
|
||||
// Ensure the embedded offline wallet-rebuild recovery helper is extracted to the daemon dir, and
|
||||
// return its path ("" if not embedded in this build or extraction failed). Idempotent — extracts only
|
||||
// when missing. Unlike the first-run extractEmbeddedResources() (gated on needsParamsExtraction()),
|
||||
// this runs on demand so recovery works from a self-contained exe on ANY run, not just the first.
|
||||
std::string ensureWalletRebuildHelperExtracted();
|
||||
|
||||
// Resource names
|
||||
constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params";
|
||||
constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params";
|
||||
@@ -62,6 +68,7 @@ constexpr const char* RESOURCE_ASMAP = "asmap.dat";
|
||||
constexpr const char* RESOURCE_DRAGONXD = "dragonxd.exe";
|
||||
constexpr const char* RESOURCE_DRAGONX_CLI = "dragonx-cli.exe";
|
||||
constexpr const char* RESOURCE_DRAGONX_TX = "dragonx-tx.exe";
|
||||
constexpr const char* RESOURCE_DRAGONX_WALLET_REBUILD = "dragonx-wallet-rebuild.exe";
|
||||
constexpr const char* RESOURCE_XMRIG = "xmrig.exe";
|
||||
constexpr const char* RESOURCE_DARK_GRADIENT = "dark_gradient.png";
|
||||
constexpr const char* RESOURCE_LOGO = "logo_ObsidianDragon_dark.png";
|
||||
|
||||
@@ -456,11 +456,11 @@ bool Connection::createDefaultConfig(const std::string& path)
|
||||
file << "exportdir=" << dataDir << "\n";
|
||||
file << "experimentalfeatures=1\n";
|
||||
file << "developerencryptwallet=1\n";
|
||||
file << "addnode=node.dragonx.is\n";
|
||||
// Round-robin DNS seed (self-updating) + static fallbacks; mirrors the daemon's vSeeds and keeps
|
||||
// pre-1.3.0 daemons (broken peer discovery) able to find peers. Works on every daemon version.
|
||||
file << "addnode=seed.dragonx.is\n";
|
||||
file << "addnode=node1.dragonx.is\n";
|
||||
file << "addnode=node2.dragonx.is\n";
|
||||
file << "addnode=node3.dragonx.is\n";
|
||||
file << "addnode=node4.dragonx.is\n";
|
||||
file << "addnode=node5.dragonx.is\n";
|
||||
|
||||
file.close();
|
||||
|
||||
|
||||
@@ -146,7 +146,11 @@ RPCClient::RPCClient() : impl_(std::make_unique<Impl>())
|
||||
{
|
||||
}
|
||||
|
||||
RPCClient::~RPCClient() = default;
|
||||
RPCClient::~RPCClient() {
|
||||
// Scrub the persistent Basic-auth secret on destruction (disconnect() may not have run). impl_ is
|
||||
// still destroyed normally afterward (curl cleanup unchanged). (L-04)
|
||||
if (!auth_.empty()) sodium_memzero(auth_.data(), auth_.size());
|
||||
}
|
||||
|
||||
bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||
const std::string& user, const std::string& password)
|
||||
@@ -166,6 +170,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||
// Create Basic auth header with proper base64 encoding, then wipe the plaintext
|
||||
// "user:password" temporary (std::string does not zero its buffer on destruction).
|
||||
std::string credentials = user + ":" + password;
|
||||
if (!auth_.empty()) sodium_memzero(auth_.data(), auth_.size()); // wipe any prior secret before overwrite (L-04)
|
||||
auth_ = util::base64_encode(credentials);
|
||||
if (!credentials.empty()) sodium_memzero(credentials.data(), credentials.size());
|
||||
|
||||
@@ -193,6 +198,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||
impl_->headers = curl_slist_append(nullptr, "Content-Type: text/plain");
|
||||
std::string auth_header = "Authorization: Basic " + auth_;
|
||||
impl_->headers = curl_slist_append(impl_->headers, auth_header.c_str());
|
||||
if (!auth_header.empty()) sodium_memzero(auth_header.data(), auth_header.size()); // curl copied it (L-04)
|
||||
|
||||
// Configure curl
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_URL, impl_->url.c_str());
|
||||
@@ -299,6 +305,7 @@ void RPCClient::disconnect()
|
||||
curl_slist_free_all(impl_->headers);
|
||||
impl_->headers = nullptr;
|
||||
}
|
||||
if (!auth_.empty()) { sodium_memzero(auth_.data(), auth_.size()); auth_.clear(); } // scrub Basic-auth secret (L-04)
|
||||
}
|
||||
|
||||
json RPCClient::makePayload(const std::string& method, const json& params)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <map>
|
||||
@@ -37,16 +38,27 @@ void applyBalancesFromUnspent(std::vector<AddressInfo>& addresses, const json& u
|
||||
{
|
||||
if (!unspent.is_array()) return;
|
||||
|
||||
std::map<std::string, double> balances;
|
||||
// Partition each note/utxo by its per-entry "confirmations": `total` (minconf=0 — DISPLAY, includes
|
||||
// the user's own pending 0-conf change) vs `spendable` (confirmations>=1 — what z_sendmany, run at
|
||||
// minconf=1, can actually spend). This lets a single z_listunspent(0)/listunspent(0) feed both.
|
||||
std::map<std::string, double> total;
|
||||
std::map<std::string, double> spendable;
|
||||
for (const auto& output : unspent) {
|
||||
auto address = readOptional<std::string>(output, "address");
|
||||
auto amount = readOptional<double>(output, "amount");
|
||||
if (address && amount) balances[*address] += *amount;
|
||||
auto amount = readOptional<double>(output, "amount");
|
||||
if (!address || !amount) continue;
|
||||
total[*address] += *amount;
|
||||
auto conf = readOptional<int>(output, "confirmations");
|
||||
if (conf && *conf >= 1) spendable[*address] += *amount;
|
||||
}
|
||||
|
||||
// The address lists are rebuilt fresh (default 0) each refresh, so hard-set both — an address with no
|
||||
// notes in this set is 0, and spendableBalance is always a subset sum of balance.
|
||||
for (auto& info : addresses) {
|
||||
auto balance = balances.find(info.address);
|
||||
if (balance != balances.end()) info.balance = balance->second;
|
||||
auto t = total.find(info.address);
|
||||
auto s = spendable.find(info.address);
|
||||
info.balance = (t != total.end()) ? t->second : 0.0;
|
||||
info.spendableBalance = (s != spendable.end()) ? s->second : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +261,7 @@ NetworkRefreshService::ConnectionInitResult NetworkRefreshService::collectConnec
|
||||
}
|
||||
|
||||
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefreshResult(
|
||||
const json& totalBalance, bool balanceOk, const json& blockInfo, bool blockOk)
|
||||
const json& totalBalance, const json& spendableBalance, bool balanceOk, const json& blockInfo, bool blockOk)
|
||||
{
|
||||
CoreRefreshResult result;
|
||||
result.balanceOk = balanceOk && totalBalance.is_object();
|
||||
@@ -258,6 +270,11 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefresh
|
||||
result.transparentBalance = readBalanceString(totalBalance, "transparent");
|
||||
result.totalBalance = readBalanceString(totalBalance, "total");
|
||||
}
|
||||
if (spendableBalance.is_object()) { // confirmed totals (minconf=1); left unset on old daemons
|
||||
result.spendableShieldedBalance = readBalanceString(spendableBalance, "private");
|
||||
result.spendableTransparentBalance = readBalanceString(spendableBalance, "transparent");
|
||||
result.spendableTotalBalance = readBalanceString(spendableBalance, "total");
|
||||
}
|
||||
|
||||
result.blockchainOk = blockOk && blockInfo.is_object();
|
||||
if (result.blockchainOk) {
|
||||
@@ -274,19 +291,17 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefresh
|
||||
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefreshResult(RefreshRpcGateway& rpc, bool includeBalance)
|
||||
{
|
||||
json totalBalance;
|
||||
json spendableBalance;
|
||||
json blockInfo;
|
||||
bool balanceOk = false;
|
||||
bool blockOk = false;
|
||||
double balanceScanMs = 0.0;
|
||||
|
||||
if (includeBalance) {
|
||||
try {
|
||||
totalBalance = rpc.call("z_gettotalbalance", json::array());
|
||||
balanceOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Balance error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// getblockchaininfo FIRST — it's cheap, and the sync state it returns gates everything else
|
||||
// (the balance/address/tx throttles and kSyncProfile). Running it BEFORE the O(mapWallet) balance
|
||||
// scan keeps sync detection from being delayed behind (or, under contention, starved by) that
|
||||
// scan — the failure mode where the wallet kept reading "synced" while actually falling behind,
|
||||
// so kSyncProfile never engaged. See effectivelySyncing()'s sticky-behind latch.
|
||||
try {
|
||||
blockInfo = rpc.call("getblockchaininfo", json::array());
|
||||
blockOk = true;
|
||||
@@ -294,7 +309,39 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
|
||||
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||
}
|
||||
|
||||
return parseCoreRefreshResult(totalBalance, balanceOk, blockInfo, blockOk);
|
||||
// If getblockchaininfo shows we're behind (same 2-block tolerance as applyCoreRefreshResult), skip
|
||||
// the balance scan this cycle regardless of includeBalance: the balance is incomplete mid-sync
|
||||
// anyway, and skipping it lets the sync-state update — and hence kSyncProfile — take effect at the
|
||||
// end of THIS (now-cheap) task instead of being delayed ~20s behind the scan. This is what lets the
|
||||
// sticky-behind latch engage promptly the first time the node falls behind.
|
||||
bool behind = false;
|
||||
if (blockOk && blockInfo.is_object()) {
|
||||
const long long b = blockInfo.value("blocks", 0LL);
|
||||
const long long lc = blockInfo.value("longestchain", 0LL);
|
||||
if (lc > 0 && b < lc - 2) behind = true;
|
||||
}
|
||||
|
||||
if (includeBalance && !behind) {
|
||||
// z_gettotalbalance is O(mapWallet) and holds the daemon's cs_main for its whole duration —
|
||||
// seconds on a large shielded wallet. Time it so the caller can throttle how often it polls
|
||||
// (balanceRefreshDue()), keeping balance scans from starving block connection.
|
||||
const auto balanceStart = std::chrono::steady_clock::now();
|
||||
try { // DISPLAY total: minconf=0 — includes the user's own pending change so it doesn't crater
|
||||
totalBalance = rpc.call("z_gettotalbalance", json::array({0}));
|
||||
balanceOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Balance error: %s\n", e.what());
|
||||
}
|
||||
try { // SPENDABLE total: minconf=1 (confirmed). If absent, spendable degrades to display in apply.
|
||||
spendableBalance = rpc.call("z_gettotalbalance", json::array({1}));
|
||||
} catch (...) {}
|
||||
balanceScanMs = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - balanceStart).count();
|
||||
}
|
||||
|
||||
auto result = parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk);
|
||||
result.balanceScanMs = balanceScanMs;
|
||||
return result;
|
||||
}
|
||||
|
||||
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult(
|
||||
@@ -573,6 +620,10 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
const AddressRefreshSnapshot& snapshot)
|
||||
{
|
||||
AddressRefreshResult result;
|
||||
// Time the whole scan — z_listunspent (and per-address z_getbalance fallback) hold the daemon's
|
||||
// cs_main for the duration, seconds on a large shielded wallet. The measured cost feeds the
|
||||
// caller's adaptive throttle so the address poll can't starve block connection.
|
||||
const auto scanStart = std::chrono::steady_clock::now();
|
||||
|
||||
try {
|
||||
json zList = rpc.call("z_listaddresses", json::array());
|
||||
@@ -608,18 +659,39 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_listaddresses error: %s\n", e.what());
|
||||
result.addressListOk = false; // enumeration failed → the shielded list may be falsely short
|
||||
}
|
||||
|
||||
try {
|
||||
json unspent = rpc.call("z_listunspent", json::array());
|
||||
json unspent = rpc.call("z_listunspent", json::array({0, 9999999, false})); // minconf=0 → include 0-conf change
|
||||
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent);
|
||||
// Retain a minimal view so a downstream consumer (chat note-budget) can reuse this scan instead
|
||||
// of issuing its own z_listunspent. rawconfirmations is the TRUE depth; `confirmations` is
|
||||
// dPoW-clamped to 1 and understates it.
|
||||
if (unspent.is_array()) {
|
||||
result.unspentNotes.reserve(unspent.size());
|
||||
for (const auto& nz : unspent) {
|
||||
if (!nz.is_object()) continue;
|
||||
UnspentNoteLite lite;
|
||||
lite.amount = nz.value("amount", 0.0);
|
||||
lite.locked = nz.value("locked", false);
|
||||
lite.confirmations = (nz.contains("rawconfirmations") && nz["rawconfirmations"].is_number_integer())
|
||||
? nz["rawconfirmations"].get<int>()
|
||||
: nz.value("confirmations", 0);
|
||||
result.unspentNotes.push_back(lite);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
||||
for (auto& info : result.shieldedAddresses) {
|
||||
try {
|
||||
json balance = rpc.call("z_getbalance", json::array({info.address}));
|
||||
if (!balance.is_null()) info.balance = balance.get<double>();
|
||||
try { // display total (minconf=0, includes pending change)
|
||||
json total = rpc.call("z_getbalance", json::array({info.address, 0}));
|
||||
if (!total.is_null()) info.balance = total.get<double>();
|
||||
} catch (...) {}
|
||||
try { // spendable (minconf=1); degrade to the display value on old daemons
|
||||
json conf = rpc.call("z_getbalance", json::array({info.address, 1}));
|
||||
info.spendableBalance = (!conf.is_null()) ? conf.get<double>() : info.balance;
|
||||
} catch (...) { info.spendableBalance = info.balance; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,15 +700,18 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
result.transparentAddresses = parseTransparentAddressList(tList);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what());
|
||||
result.addressListOk = false; // enumeration failed → the transparent list may be falsely short
|
||||
}
|
||||
|
||||
try {
|
||||
json unspent = rpc.call("listunspent", json::array());
|
||||
json unspent = rpc.call("listunspent", json::array({0})); // minconf=0 → include 0-conf change
|
||||
applyTransparentBalancesFromUnspent(result.transparentAddresses, unspent);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
||||
}
|
||||
|
||||
result.scanMs = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - scanStart).count();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -778,8 +853,11 @@ void NetworkRefreshService::appendViewTransactionOutputs(std::vector<Transaction
|
||||
for (const auto& out : entry.outgoing_outputs) {
|
||||
bool alreadyTracked = false;
|
||||
for (const auto& existing : transactions) {
|
||||
// Key on address as well as amount: two outgoing outputs of EQUAL value to DIFFERENT
|
||||
// recipients in one tx are distinct rows — an amount-only match would drop the second.
|
||||
if (existing.txid == txid && existing.type == "send" &&
|
||||
std::abs(existing.amount + out.value) < 0.00000001) {
|
||||
std::abs(existing.amount + out.value) < 0.00000001 &&
|
||||
existing.address == out.address) {
|
||||
alreadyTracked = true;
|
||||
break;
|
||||
}
|
||||
@@ -810,6 +888,55 @@ void NetworkRefreshService::appendViewTransactionOutputs(std::vector<Transaction
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkRefreshService::appendSentTransparentOutputs(std::vector<TransactionInfo>& transactions,
|
||||
const std::string& txid,
|
||||
const nlohmann::json& sentEntry)
|
||||
{
|
||||
if (!sentEntry.is_object() || !sentEntry.contains("sends") || !sentEntry["sends"].is_object())
|
||||
return;
|
||||
const auto& sends = sentEntry["sends"];
|
||||
auto it = sends.find("transparentSends");
|
||||
if (it == sends.end() || !it->is_array()) return;
|
||||
|
||||
// Per-tx metadata for the synthesized rows.
|
||||
int confirmations = 0;
|
||||
std::int64_t timestamp = 0;
|
||||
if (auto v = readOptional<int>(sentEntry, "confirmations")) confirmations = *v;
|
||||
if (auto v = readOptional<std::int64_t>(sentEntry, "blocktime")) timestamp = *v;
|
||||
if (timestamp == 0) {
|
||||
if (auto v = readOptional<std::int64_t>(sentEntry, "time")) timestamp = *v;
|
||||
}
|
||||
|
||||
for (const auto& out : *it) {
|
||||
auto amount = readOptional<double>(out, "amount");
|
||||
if (!amount) continue;
|
||||
const double debit = -std::abs(*amount); // the daemon reports -nValue; a send is a debit
|
||||
std::string address;
|
||||
if (auto a = readOptional<std::string>(out, "address")) address = *a;
|
||||
|
||||
// Dedup against rows already present — notably listtransactions-derived t->t sends, which
|
||||
// this whole-wallet payload ALSO reports. Same txid+address+amount == the same output.
|
||||
bool exists = false;
|
||||
for (const auto& e : transactions) {
|
||||
if (e.txid == txid && e.type == "send" && e.address == address &&
|
||||
std::abs(e.amount - debit) < 0.00000001) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (exists) continue;
|
||||
|
||||
TransactionInfo info;
|
||||
info.txid = txid;
|
||||
info.type = "send";
|
||||
info.address = address;
|
||||
info.amount = debit;
|
||||
info.confirmations = confirmations;
|
||||
info.timestamp = timestamp;
|
||||
transactions.push_back(std::move(info));
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkRefreshService::sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions)
|
||||
{
|
||||
std::sort(transactions.begin(), transactions.end(),
|
||||
@@ -828,6 +955,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
result.blockHeight = currentBlockHeight;
|
||||
result.shieldedAddressCount = snapshot.shieldedAddresses.size();
|
||||
result.shieldedScanHeights = snapshot.shieldedScanHeights;
|
||||
// Time the whole scan — the per-address z_listreceivedbyaddress pass is O(mapWallet) and holds the
|
||||
// daemon's cs_main. The measured cost feeds the caller's adaptive throttle (txRefreshDue()) so the
|
||||
// routine full history rescan can't starve block connection on a large wallet.
|
||||
const auto scanStart = std::chrono::steady_clock::now();
|
||||
|
||||
std::set<std::string> knownTxids;
|
||||
HushChatMemoOutputMap hushChatReceivedOutputs;
|
||||
@@ -906,6 +1037,45 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
result.shieldedScanComplete = !pendingShieldedIndex.has_value();
|
||||
result.nextShieldedScanStartIndex = pendingShieldedIndex.value_or(0);
|
||||
|
||||
// Discover outgoing shielded (z->z) sends. A pure z->z send moves no value in or out of the
|
||||
// transparent pool (valueBalance == 0), so the daemon's `listtransactions` never reports it,
|
||||
// and — being a send rather than a receive — it never shows up in `z_listreceivedbyaddress`
|
||||
// either. Its txid therefore never reaches `knownTxids`, so the z_viewtransaction enrichment
|
||||
// below is never run for it and the send is invisible in History (the reported bug).
|
||||
//
|
||||
// `z_listsentbyaddress` scans the whole wallet for sends — the address argument is NOT used to
|
||||
// filter (the daemon iterates every wallet tx against "*"), so a single call yields all send
|
||||
// txids. Two legs need handling:
|
||||
// * SHIELDED (z->z) outgoing legs are surfaced by z_viewtransaction, so we fold the txid into
|
||||
// `knownTxids` and let the z_viewtransaction / appendViewTransactionOutputs path build them.
|
||||
// * TRANSPARENT outgoing legs (z->t deshields) are NOT surfaced by z_viewtransaction (it
|
||||
// enumerates only Sapling outputs), and for a z-originated send listtransactions also omits
|
||||
// the external t-vout (GetAmounts' isFromMyTaddr is false when inputs are shielded). So we
|
||||
// build those rows directly from the payload's `sends.transparentSends[]` here. They are
|
||||
// deduped (by txid+address+amount) against listtransactions-derived t->t sends above, so a
|
||||
// purely-transparent (t->t) send — which the payload ALSO reports — is not double-counted.
|
||||
//
|
||||
// Gated on shieldedScanComplete so it adds no cost during the multi-cycle initial received
|
||||
// scan: it is one more O(mapWallet) pass holding cs_main (comparable to the listtransactions
|
||||
// pass), and the caller's adaptive throttle (txRefreshDue) already accounts for it via scanMs.
|
||||
if (result.shieldedScanComplete && !snapshot.shieldedAddresses.empty()) {
|
||||
try {
|
||||
json sent = rpc.call("z_listsentbyaddress",
|
||||
json::array({snapshot.shieldedAddresses.front(), 0}));
|
||||
if (sent.is_array()) {
|
||||
for (const auto& sentTx : sent) {
|
||||
auto txid = readOptional<std::string>(sentTx, "txid");
|
||||
if (!txid || txid->empty()) continue;
|
||||
appendSentTransparentOutputs(result.transactions, *txid, sentTx);
|
||||
knownTxids.insert(*txid); // shielded (z->z) legs enriched via z_viewtransaction
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
transactionRpcError = true;
|
||||
DEBUG_LOGF("z_listsentbyaddress error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& txid : snapshot.sendTxids) {
|
||||
knownTxids.insert(txid);
|
||||
}
|
||||
@@ -1008,6 +1178,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
}
|
||||
|
||||
sortTransactionsNewestFirst(result.transactions);
|
||||
result.scanMs = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - scanStart).count();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1027,6 +1199,22 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
|
||||
? &hushChatReceivedOutputs
|
||||
: nullptr;
|
||||
|
||||
// Index result.transactions by (txid, type) once so the two replace-loops below
|
||||
// can find-and-replace in O(1) instead of a nested linear scan over the full
|
||||
// (potentially thousands-large) tx list on every 'recent' refresh cycle. The map
|
||||
// holds the index of the FIRST occurrence of each key (preserving the linear
|
||||
// scan's break-on-first-match), and is kept in sync on every append so a later
|
||||
// entry in the same cycle still finds an earlier appended one — exactly as the
|
||||
// re-scanned vector did before.
|
||||
auto txKey = [](const TransactionInfo& tx) {
|
||||
return tx.txid + '\x1f' + tx.type;
|
||||
};
|
||||
std::unordered_map<std::string, std::size_t> byKey;
|
||||
byKey.reserve(result.transactions.size());
|
||||
for (std::size_t i = 0; i < result.transactions.size(); ++i) {
|
||||
byKey.emplace(txKey(result.transactions[i]), i); // keep first-occurrence index
|
||||
}
|
||||
|
||||
try {
|
||||
std::set<std::string> recentTxids;
|
||||
std::vector<TransactionInfo> recentTransactions;
|
||||
@@ -1034,15 +1222,13 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
|
||||
appendTransparentTransactions(recentTransactions, recentTxids, transactions, snapshot.miningAddresses);
|
||||
|
||||
for (auto& recent : recentTransactions) {
|
||||
bool replaced = false;
|
||||
for (auto& existing : result.transactions) {
|
||||
if (existing.txid == recent.txid && existing.type == recent.type) {
|
||||
existing = recent;
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
auto it = byKey.find(txKey(recent));
|
||||
if (it != byKey.end()) {
|
||||
result.transactions[it->second] = recent;
|
||||
} else {
|
||||
byKey.emplace(txKey(recent), result.transactions.size());
|
||||
result.transactions.push_back(std::move(recent));
|
||||
}
|
||||
if (!replaced) result.transactions.push_back(std::move(recent));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("recent listtransactions error: %s\n", e.what());
|
||||
@@ -1068,15 +1254,13 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
|
||||
snapshot.miningAddresses,
|
||||
hushChatReceivedOutputsPtr);
|
||||
for (auto& scanned : scannedTransactions) {
|
||||
bool replaced = false;
|
||||
for (auto& existing : result.transactions) {
|
||||
if (existing.txid == scanned.txid && existing.type == scanned.type) {
|
||||
existing = scanned;
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
auto it = byKey.find(txKey(scanned));
|
||||
if (it != byKey.end()) {
|
||||
result.transactions[it->second] = scanned;
|
||||
} else {
|
||||
byKey.emplace(txKey(scanned), result.transactions.size());
|
||||
result.transactions.push_back(std::move(scanned));
|
||||
}
|
||||
if (!replaced) result.transactions.push_back(std::move(scanned));
|
||||
}
|
||||
if (currentBlockHeight >= 0) result.shieldedScanHeights[address] = currentBlockHeight;
|
||||
++result.shieldedAddressesScanned;
|
||||
@@ -1197,6 +1381,12 @@ void NetworkRefreshService::applyCoreRefreshResult(WalletState& state,
|
||||
if (result.shieldedBalance) state.shielded_balance = *result.shieldedBalance;
|
||||
if (result.transparentBalance) state.transparent_balance = *result.transparentBalance;
|
||||
if (result.totalBalance) state.total_balance = *result.totalBalance;
|
||||
// Confirmed/spendable totals; if the minconf=1 call was unavailable (old daemon) degrade to the
|
||||
// display value so nothing is *over*-reported as spendable (z_sendmany stays the final gate).
|
||||
state.spendablePrivateBalance = result.spendableShieldedBalance.value_or(state.privateBalance);
|
||||
state.spendableTransparentBalance = result.spendableTransparentBalance.value_or(state.transparentBalance);
|
||||
state.spendableTotalBalance = result.spendableTotalBalance.value_or(state.totalBalance);
|
||||
state.unconfirmedBalance = std::max(0.0, state.totalBalance - state.spendableTotalBalance);
|
||||
state.last_balance_update = updatedAt;
|
||||
}
|
||||
|
||||
@@ -1206,6 +1396,10 @@ void NetworkRefreshService::applyCoreRefreshResult(WalletState& state,
|
||||
if (result.headers) state.sync.headers = *result.headers;
|
||||
if (result.bestBlockHash) state.sync.best_blockhash = *result.bestBlockHash;
|
||||
if (result.verificationProgress) state.sync.verification_progress = *result.verificationProgress;
|
||||
// A peerless daemon reports longestchain == 0 (no peer heights). Track that as "tip unknown" from
|
||||
// THIS refresh's raw value (not the retained state, which keeps the last positive value) so a
|
||||
// peerless node isn't reported synced against its own stale local height. (audit: peerless-synced)
|
||||
state.sync.tip_known = (result.longestChain.has_value() && *result.longestChain > 0);
|
||||
if (result.longestChain && *result.longestChain > 0) state.longestchain = *result.longestChain;
|
||||
if (state.longestchain > 0 && state.sync.blocks > state.longestchain) state.longestchain = state.sync.blocks;
|
||||
if (state.longestchain > 0)
|
||||
|
||||
@@ -98,9 +98,12 @@ public:
|
||||
|
||||
struct CoreRefreshResult {
|
||||
bool balanceOk = false;
|
||||
std::optional<double> shieldedBalance;
|
||||
std::optional<double> shieldedBalance; // display (minconf=0, incl. pending change)
|
||||
std::optional<double> transparentBalance;
|
||||
std::optional<double> totalBalance;
|
||||
std::optional<double> spendableShieldedBalance; // confirmed (minconf=1)
|
||||
std::optional<double> spendableTransparentBalance;
|
||||
std::optional<double> spendableTotalBalance;
|
||||
bool blockchainOk = false;
|
||||
std::optional<int> blocks;
|
||||
std::optional<int> headers;
|
||||
@@ -108,6 +111,7 @@ public:
|
||||
std::optional<double> verificationProgress;
|
||||
std::optional<int> longestChain;
|
||||
std::optional<int> notarized;
|
||||
double balanceScanMs = 0.0; // wall-clock spent in z_gettotalbalance this refresh (0 if balance skipped)
|
||||
};
|
||||
|
||||
struct MiningRefreshResult {
|
||||
@@ -143,9 +147,27 @@ public:
|
||||
std::string errorMessage;
|
||||
};
|
||||
|
||||
// Minimal per-note view of a z_listunspent entry — just what a downstream consumer needs to derive
|
||||
// spendable-note budgets without re-scanning. Kept UI/feature-agnostic (no chat specifics here).
|
||||
struct UnspentNoteLite {
|
||||
double amount = 0.0; // note value, DRGX
|
||||
bool locked = false; // tied up by an in-flight send
|
||||
int confirmations = 0; // TRUE depth (rawconfirmations when present, else confirmations)
|
||||
};
|
||||
|
||||
struct AddressRefreshResult {
|
||||
std::vector<AddressInfo> shieldedAddresses;
|
||||
std::vector<AddressInfo> transparentAddresses;
|
||||
// False if either address-enumeration RPC (z_listaddresses / getaddressesbyaccount) threw, so the
|
||||
// lists may be falsely short. Consumers that treat an empty list as authoritative (e.g. the
|
||||
// empty-wallet warning) must not trust a 0 count unless this is true.
|
||||
bool addressListOk = true;
|
||||
// Wall-clock spent in the address scan (dominated by z_listunspent — O(mapWallet), holds the
|
||||
// daemon's cs_main). Lets the caller throttle how often it polls (addressRefreshDue()).
|
||||
double scanMs = 0.0;
|
||||
// The wallet's unspent notes from this same z_listunspent scan, so a consumer (e.g. the chat
|
||||
// note-budget) can be fed for free instead of running its own duplicate z_listunspent.
|
||||
std::vector<UnspentNoteLite> unspentNotes;
|
||||
};
|
||||
|
||||
struct AddressRefreshSnapshot {
|
||||
@@ -197,6 +219,9 @@ public:
|
||||
std::size_t shieldedAddressCount = 0;
|
||||
std::unordered_map<std::string, int> shieldedScanHeights;
|
||||
bool shieldedScanComplete = true;
|
||||
// Wall-clock spent in the history scan (z_listreceivedbyaddress — O(mapWallet), holds cs_main).
|
||||
// Lets the caller throttle the routine full rescan by its measured cost (txRefreshDue()).
|
||||
double scanMs = 0.0;
|
||||
};
|
||||
|
||||
struct OperationStatusPollResult {
|
||||
@@ -227,6 +252,7 @@ public:
|
||||
RefreshRpcGateway& rpc,
|
||||
const std::optional<ConnectionInfoResult>& prefetchedInfo = std::nullopt);
|
||||
static CoreRefreshResult parseCoreRefreshResult(const nlohmann::json& totalBalance,
|
||||
const nlohmann::json& spendableBalance,
|
||||
bool balanceOk,
|
||||
const nlohmann::json& blockInfo,
|
||||
bool blockOk);
|
||||
@@ -284,6 +310,12 @@ public:
|
||||
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
||||
const std::string& txid,
|
||||
const TransactionViewCacheEntry& entry);
|
||||
// Build "send" rows from a z_listsentbyaddress entry's sends.transparentSends[] — the transparent
|
||||
// (z->t deshield) outgoing outputs that z_viewtransaction does not surface. Deduped by
|
||||
// txid+address+amount against rows already present (e.g. listtransactions-derived t->t sends).
|
||||
static void appendSentTransparentOutputs(std::vector<TransactionInfo>& transactions,
|
||||
const std::string& txid,
|
||||
const nlohmann::json& sentEntry);
|
||||
static void sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions);
|
||||
static TransactionRefreshResult collectTransactionRefreshResult(RefreshRpcGateway& rpc,
|
||||
const TransactionRefreshSnapshot& snapshot,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "theme_effects.h"
|
||||
#include "low_spec.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../layout.h"
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -58,6 +59,7 @@ void ThemeEffects::beginFrame() {
|
||||
|
||||
void ThemeEffects::loadFromTheme() {
|
||||
auto& S = schema::UI();
|
||||
const float dp = Layout::dpiScale();
|
||||
auto eff = [&](const char* name) {
|
||||
return S.drawElement("effects", name);
|
||||
};
|
||||
@@ -98,7 +100,7 @@ void ThemeEffects::loadFromTheme() {
|
||||
// ---- Shimmer ----
|
||||
shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
|
||||
shimmer_.speed = eff("shimmer-speed").sizeOr(0.12f);
|
||||
shimmer_.width = eff("shimmer-width").sizeOr(80.0f);
|
||||
shimmer_.width = eff("shimmer-width").sizeOr(80.0f) * dp;
|
||||
shimmer_.alpha = eff("shimmer-alpha").sizeOr(0.06f);
|
||||
shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f);
|
||||
// Shimmer color: read from the schema's color resolver
|
||||
@@ -124,7 +126,7 @@ void ThemeEffects::loadFromTheme() {
|
||||
glow_pulse_.speed = eff("glow-pulse-speed").sizeOr(2.0f);
|
||||
glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f);
|
||||
glow_pulse_.maxAlpha = eff("glow-pulse-max-alpha").sizeOr(0.15f);
|
||||
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f);
|
||||
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f) * dp;
|
||||
auto glowColorElem = eff("glow-pulse-color");
|
||||
if (!glowColorElem.color.empty()) {
|
||||
glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255));
|
||||
@@ -136,7 +138,7 @@ void ThemeEffects::loadFromTheme() {
|
||||
edge_trace_.enabled = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f;
|
||||
edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f);
|
||||
edge_trace_.length = eff("edge-trace-length").sizeOr(0.20f);
|
||||
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f);
|
||||
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f) * dp;
|
||||
edge_trace_.alpha = eff("edge-trace-alpha").sizeOr(0.6f);
|
||||
auto edgeColorElem = eff("edge-trace-color");
|
||||
if (!edgeColorElem.color.empty()) {
|
||||
@@ -149,7 +151,7 @@ void ThemeEffects::loadFromTheme() {
|
||||
ember_rise_.enabled = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f;
|
||||
ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f);
|
||||
ember_rise_.speed = eff("ember-rise-speed").sizeOr(0.4f);
|
||||
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f);
|
||||
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f) * dp;
|
||||
ember_rise_.alpha = eff("ember-rise-alpha").sizeOr(0.5f);
|
||||
auto emberColorElem = eff("ember-rise-color");
|
||||
if (!emberColorElem.color.empty()) {
|
||||
@@ -165,7 +167,7 @@ void ThemeEffects::loadFromTheme() {
|
||||
// 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_.thickness = eff("gradient-border-thickness").sizeOr(1.5f) * dp;
|
||||
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
|
||||
auto gbColorA = eff("gradient-border-color-a");
|
||||
if (!gbColorA.color.empty()) {
|
||||
@@ -194,7 +196,7 @@ void ThemeEffects::loadFromTheme() {
|
||||
sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f);
|
||||
sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f);
|
||||
sandstorm_.windAngle = eff("sandstorm-wind-angle").sizeOr(15.0f);
|
||||
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f);
|
||||
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f) * dp;
|
||||
sandstorm_.alpha = eff("sandstorm-alpha").sizeOr(0.35f);
|
||||
sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f);
|
||||
sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f);
|
||||
@@ -694,6 +696,7 @@ void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const {
|
||||
if (!enabled_ || !ember_rise_.enabled) return;
|
||||
|
||||
const float dp = Layout::dpiScale();
|
||||
float w = pMax.x - pMin.x;
|
||||
float h = pMax.y - pMin.y;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
@@ -707,7 +710,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
|
||||
// Deterministic pseudo-random x position per particle
|
||||
// Simple hash: sin of large prime multiples
|
||||
float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f;
|
||||
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f; // gentle sway
|
||||
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f * dp; // gentle sway
|
||||
|
||||
float x = pMin.x + w * xHash + xDrift;
|
||||
float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top
|
||||
@@ -745,6 +748,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
|
||||
void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
|
||||
if (!enabled_ || !ember_rise_.enabled) return;
|
||||
|
||||
const float dp = Layout::dpiScale();
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
float vpW = vp->WorkSize.x;
|
||||
float vpH = vp->WorkSize.y;
|
||||
@@ -765,7 +769,7 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
|
||||
float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
|
||||
xHash = xHash - (int)xHash; // fractional part
|
||||
if (xHash < 0) xHash += 1.0f;
|
||||
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f;
|
||||
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f * dp;
|
||||
|
||||
float x = vpX + vpW * xHash + xDrift;
|
||||
float y = vpY + vpH * (1.0f - phase); // rise from bottom to top
|
||||
|
||||
@@ -173,6 +173,24 @@ inline float kSidePanelMinWidth() { return schema::UI().drawElement("panels",
|
||||
inline float kSidePanelMaxWidth() { return schema::UI().drawElement("panels", "side-panel").getFloat("max-width", 450.0f) * dpiScale(); }
|
||||
inline float kSidePanelWidthRatio() { return schema::UI().drawElement("panels", "side-panel").getFloat("width-ratio", 0.4f); }
|
||||
|
||||
// Overall content-column cap: the max width a tab's content occupies before it is centered in wider
|
||||
// windows. <= 0 disables the cap so tab content fills ALL available horizontal width (the default —
|
||||
// requested so large windows don't leave a big empty gutter on the right). Set a positive
|
||||
// ui.toml [layout] content-max-width to re-enable a centered readable column.
|
||||
inline float kContentMaxWidth() { return schema::UI().drawElement("layout", "content-max-width").sizeOr(0.0f) * dpiScale(); }
|
||||
|
||||
// Shared compose-card envelope for the Send + Receive tabs (and any tab wanting the same box): fill the
|
||||
// available column up to the content-max-width cap, then center the leftover as margin. Both tabs MUST
|
||||
// derive their card width/offset from this so the two envelopes stay byte-for-byte identical — they
|
||||
// previously drifted (Send capped at 760dp, Receive at 860dp), so the Send card rendered narrower than
|
||||
// Receive on any window wider than ~860dp. Returns {width, offsetX} in the same units as availW.
|
||||
struct CardBox { float width; float offsetX; };
|
||||
inline CardBox mainComposeCardBox(float availW) {
|
||||
float cap = kContentMaxWidth();
|
||||
float w = (cap > 0.0f) ? std::min(availW, cap) : availW; // cap <= 0 -> fill full width
|
||||
return CardBox{ w, std::max(0.0f, (availW - w) * 0.5f) };
|
||||
}
|
||||
|
||||
inline float kTableMinHeight() { return schema::UI().drawElement("panels", "table").getFloat("min-height", 150.0f) * dpiScale(); }
|
||||
inline float kTableHeightRatio() { return schema::UI().drawElement("panels", "table").getFloat("height-ratio", 0.45f); }
|
||||
|
||||
|
||||
@@ -116,6 +116,26 @@ inline ImVec4 WarningVec4() { return ImGui::ColorConvertU32ToFloat4(Warni
|
||||
// Convenience Functions for Common Patterns
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Theme-aware translucent overlay for tracks / hover fills / dividers.
|
||||
*
|
||||
* A raw white overlay (IM_COL32(255,255,255,a)) reads on dark skins but vanishes
|
||||
* on light/pastel skins (white-on-white). This picks a dark overlay on light
|
||||
* themes and a white overlay on dark themes so the alpha reads either way.
|
||||
* (Self-contained luminance check so colors.h stays free of draw_helpers.h.)
|
||||
*
|
||||
* @param alpha 0-255 opacity of the overlay
|
||||
*/
|
||||
inline ImU32 SurfaceOverlay(int alpha)
|
||||
{
|
||||
ImU32 bg = Background();
|
||||
float r = ((bg >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
|
||||
float g = ((bg >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
|
||||
float b = ((bg >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
|
||||
bool light = (0.299f * r + 0.587f * g + 0.114f * b) > 0.5f;
|
||||
return light ? IM_COL32(0, 0, 0, alpha) : IM_COL32(255, 255, 255, alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get color with applied state overlay
|
||||
*
|
||||
|
||||
+113
-24
@@ -79,6 +79,63 @@ inline const char* LoadingDots() {
|
||||
return kDots[n];
|
||||
}
|
||||
|
||||
// ── Centered empty state ─────────────────────────────────────────────────
|
||||
// A big muted icon + title + optional wrapped hint, centered on BOTH axes within
|
||||
// GetContentRegionAvail(). Mirrors chat_tab's centeredEmptyState so list-empty states
|
||||
// read the same across tabs. Call at the start of the region you want it centered in
|
||||
// (e.g. right after a BeginChild / a leading Dummy). Font metrics use the live font
|
||||
// scale (LegacySize * FontScaleMain) and PushFont draws at that same scale, so this is
|
||||
// crisp at HiDPI / font_scale 1.5 without any manual dpiScale multiply on the metrics.
|
||||
inline void DrawEmptyState(const char* iconGlyph, const char* title, const char* hint = nullptr)
|
||||
{
|
||||
auto scaled = [](ImFont* f) { return f->LegacySize * ImGui::GetStyle().FontScaleMain; };
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const ImVec2 origin = ImGui::GetCursorPos();
|
||||
ImFont* iconF = Type().iconXL();
|
||||
ImFont* titleF = Type().subtitle1();
|
||||
ImFont* hintF = Type().body2();
|
||||
const float dp = Layout::dpiScale();
|
||||
const float gap = 8.0f * dp;
|
||||
const float wrap = std::min(avail.x - 40.0f * dp, 360.0f * dp);
|
||||
const float iconSz = iconF ? scaled(iconF) : 40.0f;
|
||||
const float iconH = (iconF && iconGlyph) ? iconF->CalcTextSizeA(iconSz, FLT_MAX, 0.0f, iconGlyph).y : 0.0f;
|
||||
const float titleH = titleF->CalcTextSizeA(scaled(titleF), FLT_MAX, 0.0f, title).y;
|
||||
const float hintH = hint ? hintF->CalcTextSizeA(scaled(hintF), wrap, wrap, hint).y : 0.0f;
|
||||
const float totalH = iconH + (iconH > 0.0f ? gap : 0.0f) + titleH + (hint ? gap + hintH : 0.0f);
|
||||
float y = origin.y + std::max(0.0f, (avail.y - totalH) * 0.5f);
|
||||
|
||||
if (iconF && iconGlyph && iconGlyph[0]) {
|
||||
const float iw = iconF->CalcTextSizeA(iconSz, FLT_MAX, 0.0f, iconGlyph).x;
|
||||
ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - iw) * 0.5f, y));
|
||||
ImGui::PushFont(iconF);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, WithAlpha(OnSurface(), 70));
|
||||
ImGui::TextUnformatted(iconGlyph);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
y += iconH + gap;
|
||||
}
|
||||
{
|
||||
const float tw = titleF->CalcTextSizeA(scaled(titleF), FLT_MAX, 0.0f, title).x;
|
||||
ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - tw) * 0.5f, y));
|
||||
ImGui::PushFont(titleF);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium());
|
||||
ImGui::TextUnformatted(title);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
y += titleH + gap;
|
||||
}
|
||||
if (hint) {
|
||||
ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - wrap) * 0.5f, y));
|
||||
ImGui::PushFont(hintF);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, WithAlpha(OnSurface(), 120));
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap);
|
||||
ImGui::TextUnformatted(hint);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Text Drop Shadow
|
||||
// ============================================================================
|
||||
@@ -487,11 +544,16 @@ inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0),
|
||||
ImVec2 bMin = ImGui::GetItemRectMin();
|
||||
ImVec2 bMax = ImGui::GetItemRectMax();
|
||||
|
||||
// For icon fonts, manually draw centered icon after getting button rect
|
||||
// For icon fonts, manually draw centered icon after getting button rect. Measure/draw only the
|
||||
// VISIBLE label (up to the "##id" separator): CalcTextSizeA/AddText don't strip "##" the way
|
||||
// ImGui's own text render does, so an id suffix like "##pickContact" would inflate textSz and
|
||||
// shove the glyph left off-center (and try to draw the notdef id chars).
|
||||
if (isIconFont && size.x > 0 && size.y > 0) {
|
||||
ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label);
|
||||
const char* labelEnd = label;
|
||||
while (*labelEnd && !(labelEnd[0] == '#' && labelEnd[1] == '#')) ++labelEnd;
|
||||
ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label, labelEnd);
|
||||
ImVec2 textPos(bMin.x + (size.x - textSz.x) * 0.5f, bMin.y + (size.y - textSz.y) * 0.5f);
|
||||
dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label);
|
||||
dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label, labelEnd);
|
||||
}
|
||||
|
||||
float rounding = ImGui::GetStyle().FrameRounding;
|
||||
@@ -863,7 +925,7 @@ inline void DrawStatCard(ImDrawList* dl,
|
||||
// Draw a full-height rounded rect with card rounding (left corners)
|
||||
// and clip to stripe width so the shape follows the corner radius.
|
||||
if ((card.accentCol & IM_COL32_A_MASK) != 0) {
|
||||
float stripeW = 4.0f;
|
||||
float stripeW = 4.0f * Layout::dpiScale();
|
||||
dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true);
|
||||
dl->AddRectFilled(cMin, cMax, card.accentCol, rnd,
|
||||
ImDrawFlags_RoundCornersLeft);
|
||||
@@ -1220,7 +1282,8 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImVec2 winPos = ImGui::GetWindowPos();
|
||||
float winWidth = ImGui::GetWindowWidth();
|
||||
float barHeight = 36.0f;
|
||||
const float dp = Layout::dpiScale();
|
||||
float barHeight = 36.0f * dp;
|
||||
|
||||
// Get accent color from theme if not provided
|
||||
if (!accent_col) {
|
||||
@@ -1244,15 +1307,15 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
||||
ImFont* titleFont = Type().subtitle1();
|
||||
ImGui::PushFont(titleFont);
|
||||
ImVec2 titleSize = ImGui::CalcTextSize(title);
|
||||
float titleX = barMin.x + 16.0f;
|
||||
float titleX = barMin.x + 16.0f * dp;
|
||||
float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f;
|
||||
DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
|
||||
ImGui::PopFont();
|
||||
|
||||
// Close button (X) on right side
|
||||
if (p_open) {
|
||||
float btnSize = 24.0f;
|
||||
float btnX = barMax.x - btnSize - 12.0f;
|
||||
float btnSize = 24.0f * dp;
|
||||
float btnX = barMax.x - btnSize - 12.0f * dp;
|
||||
float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
|
||||
ImVec2 btnMin(btnX, btnY);
|
||||
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
|
||||
@@ -1265,7 +1328,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
||||
|
||||
// Button background on hover
|
||||
if (hovered) {
|
||||
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f);
|
||||
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f * dp);
|
||||
}
|
||||
|
||||
// Draw X icon
|
||||
@@ -1286,7 +1349,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
||||
}
|
||||
|
||||
// Reserve space for title bar so content starts below it
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f * dp);
|
||||
|
||||
return closeClicked;
|
||||
}
|
||||
@@ -1403,6 +1466,7 @@ struct OverlayCardState {
|
||||
int stableCount = 0; // consecutive frames the height held steady (within 1px)
|
||||
int appearFrames = 0; // frames since (re)appearing while still hidden — a safety cap
|
||||
bool shown = false; // revealed (centered) at least once this open; don't re-hide after
|
||||
bool overflow = false; // content once exceeded the viewport → clamp to viewport + scroll (sticky/open)
|
||||
};
|
||||
inline std::unordered_map<std::string, OverlayCardState> g_overlayCardHeights;
|
||||
inline std::string g_overlayCurrentKey;
|
||||
@@ -1528,6 +1592,7 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
|
||||
float cardY, cardBottomY;
|
||||
bool hideForMeasure = false; // true on an auto-height dialog's first (unmeasured) frame
|
||||
bool autoOverflow = false; // auto-height content taller than the viewport → clamp + scroll
|
||||
const bool fixedHeight = (spec.cardHeight > 0.0f);
|
||||
if (fixedHeight) {
|
||||
float cardH = std::min(spec.cardHeight * dp, vp_size.y - 32.0f);
|
||||
@@ -1537,9 +1602,16 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
} else {
|
||||
g_overlayCurrentKey = childId;
|
||||
OverlayCardState& cs = g_overlayCardHeights[childId];
|
||||
if (scrimAppearing) { cs.shown = false; cs.stableCount = 0; cs.appearFrames = 0; }
|
||||
if (scrimAppearing) { cs.shown = false; cs.stableCount = 0; cs.appearFrames = 0; cs.overflow = false; }
|
||||
if (!cs.shown) cs.appearFrames++;
|
||||
const float measuredH = cs.height;
|
||||
const float maxCardH = vp_size.y - 32.0f;
|
||||
// Once the measured content is taller than the viewport, lock the card to the viewport height and
|
||||
// let its content child scroll (autoOverflow) so the footer/actions stay reachable. Sticky for this
|
||||
// open: clamping makes next frame's measured height the clamped value, so re-deciding from it would
|
||||
// oscillate — decide once and hold until the dialog re-opens.
|
||||
if (measuredH > maxCardH) cs.overflow = true;
|
||||
autoOverflow = cs.overflow;
|
||||
// Reveal once the measured height has settled (auto-resize converges in ~2 frames) or it's
|
||||
// already been shown this open (don't re-hide on a mid-dialog content change); a frame cap
|
||||
// guarantees a pathological ever-changing height can't hide the dialog forever.
|
||||
@@ -1547,11 +1619,18 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
(cs.shown || cs.stableCount >= 1 || cs.appearFrames >= 8);
|
||||
if (ready) {
|
||||
cs.shown = true;
|
||||
// Center the measured content; if it's taller than the window, anchor at the top margin.
|
||||
cardY = (measuredH < vp_size.y - 32.0f)
|
||||
? vp_pos.y + (vp_size.y - measuredH) * 0.5f
|
||||
: vp_pos.y + 16.0f;
|
||||
cardBottomY = cardY + measuredH;
|
||||
if (autoOverflow) {
|
||||
// Taller than the screen: top-anchor at the 16px margin, clamp to the viewport; the
|
||||
// content child (below) becomes the scroll region so the footer/actions stay reachable.
|
||||
cardY = vp_pos.y + 16.0f;
|
||||
cardBottomY = cardY + maxCardH;
|
||||
} else {
|
||||
// Center the measured content; if it's taller than the window, anchor at the top margin.
|
||||
cardY = (measuredH < maxCardH)
|
||||
? vp_pos.y + (vp_size.y - measuredH) * 0.5f
|
||||
: vp_pos.y + 16.0f;
|
||||
cardBottomY = cardY + measuredH;
|
||||
}
|
||||
} else {
|
||||
// Still settling: lay the content out (so the auto-height child gets measured) but keep
|
||||
// the card hidden (hideForMeasure below) so it never flashes off-center — it appears,
|
||||
@@ -1567,7 +1646,10 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
// the measuring frame (its geometry is a placeholder; the whole card is hidden until centered).
|
||||
if (!floating && !hideForMeasure) {
|
||||
GlassPanelSpec cardGlass;
|
||||
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 35; cardGlass.borderAlpha = 50; cardGlass.borderWidth = 1.0f;
|
||||
// Fill/border alpha govern every overlay dialog's card boundary — kept well above the default
|
||||
// glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
|
||||
// tiles, chat) while staying translucent rather than opaque.
|
||||
cardGlass.rounding = 16.0f * dp; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
|
||||
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
||||
}
|
||||
|
||||
@@ -1581,17 +1663,24 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
|
||||
// Content child.
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f * dp : 16.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28 * dp, 20 * dp) : ImVec2(28 * dp, 24 * dp));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
|
||||
ImGuiChildFlags cflags = ImGuiChildFlags_AlwaysUseWindowPadding | (fixedHeight ? 0 : ImGuiChildFlags_AutoResizeY);
|
||||
// A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose
|
||||
// content overflowed the viewport); otherwise the child auto-resizes to its content.
|
||||
const bool clampedCard = fixedHeight || autoOverflow;
|
||||
ImGuiChildFlags cflags = ImGuiChildFlags_AlwaysUseWindowPadding | (clampedCard ? 0 : ImGuiChildFlags_AutoResizeY);
|
||||
// NoScrollWithMouse (not just NoScrollbar): a modal is a fixed frame — the wheel must never drift
|
||||
// the WHOLE card. If content marginally overflows a fixed card, the wheel would otherwise scroll
|
||||
// the entire dialog (title + footer and all). Inner scroll regions (lists, notes) still scroll on
|
||||
// their own; auto-height cards resize to content so they never overflow anyway.
|
||||
// their own; auto-height cards resize to content so they normally never overflow — EXCEPT when the
|
||||
// content is taller than the viewport (autoOverflow), where the card itself IS the scroll region.
|
||||
ImGuiWindowFlags childScroll = autoOverflow
|
||||
? ImGuiWindowFlags_None
|
||||
: (ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
bool childVisible = ImGui::BeginChild(childId.c_str(),
|
||||
ImVec2(cardWidth, fixedHeight ? (cardBottomY - cardY) : 0.0f),
|
||||
cflags, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImVec2(cardWidth, clampedCard ? (cardBottomY - cardY) : 0.0f),
|
||||
cflags, childScroll);
|
||||
// Floating (portfolio-style) cards: the padding applies to this content child only, so pop it
|
||||
// now (nested children mustn't inherit it), and center button labels. Net style-var count stays
|
||||
// at 2 (ChildRounding + ButtonTextAlign) so EndOverlayDialog's PopStyleVar(2) is unchanged.
|
||||
@@ -1714,7 +1803,7 @@ inline void DialogWarningHeader(const char* warningLabel, const ImVec4& col = Wa
|
||||
inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel,
|
||||
bool danger, bool& outCancel, bool& outConfirm)
|
||||
{
|
||||
float btnH = schema::UI().drawElement("components.overlay-dialog", "confirm-btn-height").sizeOr(40.0f);
|
||||
float btnH = schema::UI().drawElement("components.overlay-dialog", "confirm-btn-height").sizeOr(40.0f) * Layout::dpiScale();
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (ImGui::Button(cancelId, ImVec2(btnW, btnH))) {
|
||||
outCancel = true;
|
||||
|
||||
@@ -107,7 +107,7 @@ inline float ActionButtonWidth(const char* label, const char* icon, float minWid
|
||||
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 padX = 9.0f * dp, gap = 6.0f * dp; // mockup .btn padding: 9px horizontal
|
||||
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;
|
||||
@@ -133,19 +133,21 @@ inline bool ActionButton(const char* id, const char* label, const char* icon, Ac
|
||||
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 round = 7.0f * dp; // mockup .btn radius: 7px (softer than the global 4px frame)
|
||||
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);
|
||||
// Mockup .btn.acc: a dark accent-tinted chip with accent TEXT — not a bright filled button.
|
||||
bg = WithAlpha(Primary(), hov ? 52 : 38);
|
||||
border = WithAlpha(Primary(), hov ? 150 : 110);
|
||||
fg = Primary();
|
||||
break;
|
||||
case ActionTier::Secondary:
|
||||
bg = WithAlpha(OnSurface(), hov ? 26 : 16);
|
||||
border = WithAlpha(OnSurface(), 40);
|
||||
bg = WithAlpha(OnSurface(), hov ? 30 : 20);
|
||||
border = WithAlpha(OnSurface(), 48);
|
||||
glass = true;
|
||||
fg = OnSurface();
|
||||
break;
|
||||
|
||||
@@ -31,6 +31,7 @@ enum class NodeBannerReason {
|
||||
FullNodeOffline, // a reachable node was lost, or never came up; reconnect offered
|
||||
DaemonCrashed, // the embedded daemon crashed repeatedly and auto-restart stopped
|
||||
LiteOpenFailed, // lite build: the wallet failed to open
|
||||
NoPeers, // connected to the daemon but no peer-derived tip (0 peers) — stalled, stale
|
||||
};
|
||||
|
||||
struct NodeBannerState {
|
||||
@@ -50,6 +51,13 @@ struct NodeBannerInputs {
|
||||
bool daemon_initializing = false; // daemon launching / block index loading
|
||||
bool connection_in_progress = false; // a connect attempt is actively running
|
||||
|
||||
// Peer connectivity: a full node connected to the daemon but with NO peer-derived tip
|
||||
// (longestchain == 0 -> tip_known false) has no peers, so it silently stalls at a stale height
|
||||
// while balance/sync look "done". `blocks` gates the check so we don't flag the brief window
|
||||
// before the first chain-info refresh. (audit: no-peer-banner)
|
||||
bool tip_known = true; // sync.tip_known — a peer-derived network tip is known
|
||||
int blocks = 0; // sync.blocks — >0 once chain info has been fetched
|
||||
|
||||
// Full-node embedded-daemon crash signal.
|
||||
bool using_embedded_daemon = false;
|
||||
bool has_daemon_controller = false;
|
||||
@@ -81,13 +89,26 @@ inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Full node. Connected, or in an expected startup phase → the loading/warmup overlay owns
|
||||
// the screen, so no banner. An active connect attempt likewise shows progress, not an
|
||||
// error — don't flicker a banner over it.
|
||||
if (in.connected) return s;
|
||||
// Full node. In an expected startup phase → the loading/warmup overlay owns the screen, so no
|
||||
// banner. An active connect attempt likewise shows progress, not an error — don't flicker a
|
||||
// banner over it. (These return even if `connected` has flipped early, matching prior behavior.)
|
||||
if (in.warming_up || in.daemon_initializing) return s;
|
||||
if (in.connection_in_progress) return s;
|
||||
|
||||
// Connected to the daemon (RPC works) but with no peer-derived tip → the node has no peers and
|
||||
// cannot learn about new blocks; it silently stalls at a stale height while everything looks
|
||||
// synced. Surface it as a recoverable warning (reconnect won't help, so no action button).
|
||||
if (in.connected) {
|
||||
if (!in.tip_known && in.blocks > 0) {
|
||||
s.show = true;
|
||||
s.severity = NodeBannerSeverity::Warning;
|
||||
s.reason = NodeBannerReason::NoPeers;
|
||||
s.action = NodeBannerAction::None;
|
||||
s.detail = in.connection_status;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Genuinely offline. Distinguish "the embedded daemon crashed and we stopped retrying" (a
|
||||
// hard fault needing a manual restart) from an ordinary lost/failed connection (retryable).
|
||||
if (in.using_embedded_daemon && in.has_daemon_controller && !in.daemon_running &&
|
||||
|
||||
@@ -32,19 +32,22 @@ void Notifications::render()
|
||||
return v >= 0 ? v : fb;
|
||||
};
|
||||
|
||||
// Status bar geometry
|
||||
float sbHeight = S.window("components.status-bar").height;
|
||||
if (sbHeight <= 0.0f) sbHeight = 30.0f;
|
||||
// Status bar geometry. These are logical-px schema values; the icon/text drawn into the pill
|
||||
// are DPI-baked, so scale the box by dpiScale to match the (also DPI-scaled) rendered status bar
|
||||
// and keep the icon/text inside the pill at HiDPI.
|
||||
const float dp = Layout::dpiScale();
|
||||
float sbHeight = S.window("components.status-bar").height * dp;
|
||||
if (sbHeight <= 0.0f) sbHeight = 30.0f * dp;
|
||||
|
||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
float viewBottom = viewport->WorkPos.y + viewport->WorkSize.y;
|
||||
float viewCenterX = viewport->WorkPos.x + viewport->WorkSize.x * 0.5f;
|
||||
|
||||
// Toast pill sizing — fit inside status bar with margin
|
||||
float pillMarginY = nde("pill-margin-y", 3.0f);
|
||||
float pillMarginY = nde("pill-margin-y", 3.0f) * dp;
|
||||
float pillHeight = sbHeight - pillMarginY * 2.0f;
|
||||
float pillPadX = nde("padding-x", 12.0f);
|
||||
float pillRounding = nde("pill-rounding", 12.0f);
|
||||
float pillPadX = nde("padding-x", 12.0f) * dp;
|
||||
float pillRounding = nde("pill-rounding", 12.0f) * dp;
|
||||
|
||||
// Get accent color based on type — resolved from theme palette
|
||||
ImVec4 accent_color, text_color;
|
||||
@@ -89,7 +92,7 @@ void Notifications::render()
|
||||
ImFont* textFont = material::Type().caption();
|
||||
ImFont* iconFont = material::Type().iconSmall();
|
||||
float iconW = iconFont ? iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0.0f, icon).x : 0.0f;
|
||||
float iconGap = 4.0f;
|
||||
float iconGap = 4.0f * dp;
|
||||
float msgW = textFont ? textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, notif.message.c_str()).x : 100.0f;
|
||||
float pillWidth = pillPadX + iconW + iconGap + msgW + pillPadX;
|
||||
// Clamp to reasonable bounds
|
||||
@@ -122,7 +125,7 @@ void Notifications::render()
|
||||
// Progress bar at bottom of pill (accent-colored), clipped to pill rounded
|
||||
// corners. Draw a full-pill-size rounded rect and clip it to just the
|
||||
// bottom-left progress strip so both bottom corners are respected.
|
||||
float progH = nde("progress-bar-height", 2.0f);
|
||||
float progH = nde("progress-bar-height", 2.0f) * dp;
|
||||
float progW = pillWidth * (1.0f - progress);
|
||||
if (progW > 0.0f) {
|
||||
ImVec2 clipMin(pillX, pMax.y - progH);
|
||||
|
||||
+14
-2
@@ -31,6 +31,8 @@ struct AlertRecord {
|
||||
std::string message;
|
||||
NotificationType type;
|
||||
std::int64_t epoch; // std::time(nullptr) at push — wall-clock, for relative-age display
|
||||
std::function<void()> onClick; // optional: makes this bell-panel entry actionable
|
||||
std::string actionHint; // optional: accent link label rendered for the action
|
||||
};
|
||||
|
||||
struct Notification {
|
||||
@@ -92,15 +94,25 @@ public:
|
||||
if (duration < 0.0f) duration = schemaDuration("duration-error", 4.0f);
|
||||
push(message, NotificationType::Error, duration);
|
||||
}
|
||||
|
||||
// An actionable alert: a normal toast PLUS a clickable entry in the bell/alert-history panel.
|
||||
// onClick fires when the user clicks the accent `actionHint` link in that panel.
|
||||
void action(const std::string& message, NotificationType type, std::function<void()> onClick,
|
||||
const std::string& actionHint, float duration = -1.0f) {
|
||||
if (duration < 0.0f) duration = schemaDuration("duration-warning", 3.5f);
|
||||
push(message, type, duration, std::move(onClick), actionHint);
|
||||
}
|
||||
|
||||
void push(const std::string& message, NotificationType type, float duration = 5.0f) {
|
||||
void push(const std::string& message, NotificationType type, float duration = 5.0f,
|
||||
std::function<void()> onClick = nullptr, const std::string& actionHint = "") {
|
||||
notifications_.emplace_back(message, type, duration);
|
||||
|
||||
// Retain a copy in the persistent history (the toast above will fade in seconds; this
|
||||
// survives so the user can review what happened). Thread note: every push is on the UI
|
||||
// thread (RPC results run as main-thread MainCb callbacks), so this container needs no lock,
|
||||
// consistent with the rest of this class. Do NOT push from a raw worker thread.
|
||||
history_.push_back(AlertRecord{message, type, static_cast<std::int64_t>(std::time(nullptr))});
|
||||
history_.push_back(AlertRecord{message, type, static_cast<std::int64_t>(std::time(nullptr)),
|
||||
std::move(onClick), actionHint});
|
||||
++total_pushed_;
|
||||
while (history_.size() > kMaxHistory) {
|
||||
history_.pop_front();
|
||||
|
||||
+903
-805
File diff suppressed because it is too large
Load Diff
+40
-16
@@ -219,7 +219,7 @@ inline void DrawGlassCutout(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
float lineW = s_cc.lineW;
|
||||
|
||||
// --- Outer glow pass: wider, softer dark edge on top-left ---
|
||||
float glowExpand = s_cc.glowExpand;
|
||||
float glowExpand = s_cc.glowExpand * Layout::dpiScale();
|
||||
int glowA = (int)s_cc.glowAlpha;
|
||||
float glowLineW = s_cc.glowLineW;
|
||||
{
|
||||
@@ -289,6 +289,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
// inner pass gives crisp bevel edge. All use AddRect with rounding so
|
||||
// every layer follows the rounded corners perfectly — no clip rects needed.
|
||||
{
|
||||
const float dp = Layout::dpiScale();
|
||||
float cx = (mn.x + mx.x) * 0.5f;
|
||||
float cy = (mn.y + mx.y) * 0.5f;
|
||||
|
||||
@@ -304,7 +305,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
|
||||
struct BevelPass { float expand; float lineW; float fadeStart; float fadeEnd; };
|
||||
BevelPass passes[] = {
|
||||
{ 0.5f, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
|
||||
{ 0.5f * dp, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
|
||||
{ 0.0f, 0.75f, 0.38f, 0.58f }, // Inner crisp bevel
|
||||
};
|
||||
|
||||
@@ -387,7 +388,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
}
|
||||
}
|
||||
if (depth > s_ic.threshold) {
|
||||
float baseInset = s_ic.inset;
|
||||
float baseInset = s_ic.inset * Layout::dpiScale();
|
||||
int shadowMax = (int)(s_ic.maxAlpha * depth);
|
||||
float fadeRatio = s_ic.fadeRatio;
|
||||
float bW = mx.x - mn.x - baseInset * 2.0f;
|
||||
@@ -487,7 +488,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
float fixedH = stripH; // collapse strip
|
||||
for (int i = 0; i < (int)NavPage::Count_; ++i)
|
||||
if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels)
|
||||
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below
|
||||
fixedH += olFsz + 2.0f * dp + sectionLabelPadBot; // section label + pad below
|
||||
fixedH += bottomPadding + stripH; // exit area
|
||||
|
||||
float baseFlexH = baseNavGap;
|
||||
@@ -525,7 +526,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
if (showLabels) {
|
||||
curY += sectionGap;
|
||||
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
|
||||
curY += olFsz + 2.0f + sectionLabelPadBot;
|
||||
curY += olFsz + 2.0f * dp + sectionLabelPadBot;
|
||||
} else {
|
||||
curY += sectionGap * 0.4f;
|
||||
if (nSeparators < 4) separatorY[nSeparators++] = curY;
|
||||
@@ -538,11 +539,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
float exitRelY = curY + bottomPadding;
|
||||
float panelH = exitRelY + stripH;
|
||||
|
||||
// Vertical centering — offset so panel is centered in the child window
|
||||
float centerOffset = std::max(glassMarginY, (contentHeight - panelH) * 0.5f);
|
||||
if (centerOffset + panelH > contentHeight)
|
||||
centerOffset = std::max(0.0f, contentHeight - panelH);
|
||||
|
||||
// ===================================================================
|
||||
// PASS 2: Render using computed positions
|
||||
// ===================================================================
|
||||
@@ -552,6 +548,13 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImVec2 wp = ImGui::GetWindowPos();
|
||||
|
||||
// Vertical centering — center the panel within the child. app.cpp sizes the child (contentHeight)
|
||||
// to the visible area (child top -> status-bar top) using window-local geometry, so this yields
|
||||
// equal top/bottom gaps at any height on every platform, no viewport dependency.
|
||||
float centerOffset = std::max(glassMarginY, (contentHeight - panelH) * 0.5f);
|
||||
if (centerOffset + panelH > contentHeight)
|
||||
centerOffset = std::max(0.0f, contentHeight - panelH);
|
||||
|
||||
float panelLeft = wp.x + glassMarginL;
|
||||
float panelRight = wp.x + sidebarWidth - glassMarginR;
|
||||
float panelTopY = wp.y + centerOffset;
|
||||
@@ -651,7 +654,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
fx.drawShimmer(dl, indMin, indMax, btnRnd);
|
||||
fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd);
|
||||
}
|
||||
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f);
|
||||
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f * dp);
|
||||
DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18);
|
||||
buttonRects.push_back({indMin, indMax, btnRnd});
|
||||
}
|
||||
@@ -676,10 +679,31 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium());
|
||||
|
||||
if (showLabels) {
|
||||
// The badge is a fixed top-right corner overlay, so it must NOT move
|
||||
// the icon+label — otherwise the text jumps sideways the moment a live
|
||||
// count toggles the badge on/off. Reserve clearance from whether the
|
||||
// page CAN show a badge (constant per item), never from the current
|
||||
// count, and keep the icon+label centered in the FULL button width so
|
||||
// the text position and size stay identical with or without a badge.
|
||||
bool itemBadgeCapable =
|
||||
item.page == NavPage::History ||
|
||||
item.page == NavPage::Mining ||
|
||||
item.page == NavPage::Chat;
|
||||
float badgeReserve = 0.0f;
|
||||
if (itemBadgeCapable) {
|
||||
bool dotOnlyReserve = (item.page == NavPage::Mining);
|
||||
float badgeRReserve = dotOnlyReserve ? badgeRadiusDot : badgeRadiusNumber;
|
||||
float badgeInsetXReserve = sde("badge-inset-x", 6.0f);
|
||||
badgeReserve = badgeRReserve * 2.0f + badgeInsetXReserve;
|
||||
}
|
||||
|
||||
ImFont* font = selected ? Type().subtitle2() : Type().body2();
|
||||
float lblFsz = ScaledFontSize(font);
|
||||
float btnW = indMax.x - indMin.x;
|
||||
float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2;
|
||||
// Clearance is symmetric (2x) because the group stays centered in the
|
||||
// full width: reserving on both sides keeps the label's right edge clear
|
||||
// of the right-side corner badge without shifting the center off-axis.
|
||||
float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2 - badgeReserve * 2.0f;
|
||||
ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
|
||||
if (labelSz.x > maxLabelW && maxLabelW > 0) {
|
||||
lblFsz *= maxLabelW / labelSz.x;
|
||||
@@ -714,16 +738,16 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
badgeCol = Warning(); badgeTextCol = OnWarning();
|
||||
} else if (item.page == NavPage::Mining && status.miningActive) {
|
||||
dotOnly = true; badgeCol = Success();
|
||||
} else if (item.page == NavPage::Peers && status.peerCount > 0) {
|
||||
badgeCount = status.peerCount;
|
||||
} else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) {
|
||||
badgeCount = status.chatUnreadCount;
|
||||
}
|
||||
|
||||
if (badgeCount > 0 || dotOnly) {
|
||||
float badgeR = dotOnly ? badgeRadiusDot : badgeRadiusNumber;
|
||||
float bx = indMax.x - badgeR - 6.0f;
|
||||
float by = indMin.y + badgeR + 5.0f;
|
||||
float badgeInsetX = sde("badge-inset-x", 6.0f);
|
||||
float badgeInsetY = sde("badge-inset-y", 5.0f);
|
||||
float bx = indMax.x - badgeR - badgeInsetX;
|
||||
float by = indMin.y + badgeR + badgeInsetY;
|
||||
dl->AddCircleFilled(ImVec2(bx, by), badgeR, badgeCol);
|
||||
if (!dotOnly && showLabels) {
|
||||
char buf[16];
|
||||
|
||||
@@ -125,29 +125,26 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", TR("about_license_text"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Links
|
||||
if (material::StyledButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
// Links — 3-button action row, centered via the shared footer helper (draws its own
|
||||
// Spacing/Separator/Spacing above the row, replacing the hand-rolled divider block).
|
||||
const float linksTotalW = linkW * 3.0f + ImGui::GetStyle().ItemSpacing.x * 2.0f;
|
||||
material::BeginOverlayDialogFooter(linksTotalW);
|
||||
if (material::TactileButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
util::Platform::openUrl("https://dragonx.is");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("about_github"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::TactileButton(TR("about_github"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::TactileButton(TR("about_block_explorer"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
util::Platform::openUrl("https://explorer.dragonx.is");
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Close button
|
||||
float button_width = closeW;
|
||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f);
|
||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
||||
|
||||
// Close button — lone dismiss action, centered via the shared footer helper (no extra
|
||||
// divider above it, so it sits directly under the links row).
|
||||
material::BeginOverlayDialogFooter(closeW, false);
|
||||
if (material::TactileButton(TR("close"), ImVec2(closeW, 0), S.resolveFont(closeBtn.font))) {
|
||||
*p_open = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -138,8 +138,13 @@ public:
|
||||
const float controlsTopY = std::max(gridStartY + cellSz * 2.0f, buttonY - preButtonReserve);
|
||||
const float gridMaxH = std::max(cellSz * 2.0f, controlsTopY - gridStartY);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 11.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 5.5f * dp);
|
||||
// Scrollbar visible (not NoScrollbar) — the icon set exceeds the fixed-height
|
||||
// grid, so a real scrollbar is the discoverable way to reach the rest.
|
||||
ImGui::BeginChild("##IconGrid", ImVec2(avail, gridMaxH), ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar);
|
||||
ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ApplySmoothScroll();
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
@@ -185,6 +190,7 @@ public:
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2); // ScrollbarSize + ScrollbarRounding
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (ImGui::GetCursorPosY() < controlsTopY) {
|
||||
|
||||
@@ -93,7 +93,7 @@ public:
|
||||
// Arrow
|
||||
{
|
||||
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
|
||||
ImGui::SetCursorPosX(arrowCX - 8.0f);
|
||||
ImGui::SetCursorPosX(arrowCX - 8.0f * dp);
|
||||
ImFont* iconFont = Type().iconMed();
|
||||
float fsz = ScaledFontSize(iconFont);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
@@ -169,9 +169,9 @@ public:
|
||||
}
|
||||
if (amountValid && newFromBal < 1e-9) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||
ImGui::TextWrapped("%s", TR("sends_full_balance_warning"));
|
||||
ImGui::PopStyleColor();
|
||||
// Full-balance send: same warning-icon treatment as the de-shielding header above,
|
||||
// so this stakes-bearing line reads as distinct from the neutral preview text.
|
||||
DialogWarningHeader(TR("sends_full_balance_warning"));
|
||||
}
|
||||
|
||||
// Buttons
|
||||
@@ -180,16 +180,14 @@ public:
|
||||
const char* sendingLabel = TR("sending");
|
||||
ImFont* buttonFont = Type().button();
|
||||
float buttonFontSize = ScaledFontSize(buttonFont);
|
||||
float minBtnW = 120.0f * dp;
|
||||
float confirmMinW = 160.0f * dp;
|
||||
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
|
||||
float cancelW = std::max(minBtnW,
|
||||
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW);
|
||||
// Both footer buttons share one width (equal-width primary/Close pair), sized to fit the
|
||||
// widest label — the "Sending…" swap label included — so nothing clips.
|
||||
float confirmTextW = std::max(
|
||||
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, confirmLabel).x,
|
||||
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
|
||||
float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW);
|
||||
float totalW = cancelW + confirmW + Layout::spacingMd();
|
||||
float btnW = std::max(confirmMinW, confirmTextW + buttonPadW);
|
||||
float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f; // footer divider removed
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
float cardBottomY = vp->Pos.y + vp->Size.y * 0.85f;
|
||||
@@ -201,19 +199,18 @@ public:
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
// Standardized primary + Close footer (centered, no divider). The primary is the
|
||||
// Confirm/"Sending…" action; the Close button dismisses the dialog.
|
||||
bool outConfirm = false;
|
||||
bool outClose = false;
|
||||
DialogActionFooter(s_sending ? sendingLabel : confirmLabel,
|
||||
amountValid && !s_sending,
|
||||
cancelLabel, outConfirm, outClose, btnW);
|
||||
|
||||
float rowStartX = ImGui::GetCursorPosX();
|
||||
float contentW = ImGui::GetContentRegionAvail().x;
|
||||
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
|
||||
|
||||
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
|
||||
if (outClose) {
|
||||
s_open = false;
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
|
||||
ImGui::BeginDisabled(!amountValid || s_sending);
|
||||
if (TactileButton(s_sending ? sendingLabel : confirmLabel, ImVec2(confirmW, 0), buttonFont)) {
|
||||
if (outConfirm) {
|
||||
s_sending = true;
|
||||
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
|
||||
amount, s_fee, "",
|
||||
@@ -231,7 +228,6 @@ public:
|
||||
// state, and when the async callback sets s_resultMsg the in-dialog result screen shows
|
||||
// (with its own Close button). Previously closing here made that result screen dead code.
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
EndOverlayDialog();
|
||||
}
|
||||
@@ -316,7 +312,8 @@ private:
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
float btnW = 120.0f;
|
||||
const float dp = Layout::dpiScale();
|
||||
float btnW = 120.0f * dp;
|
||||
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
|
||||
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
|
||||
s_open = false;
|
||||
|
||||
@@ -116,7 +116,7 @@ void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float v
|
||||
|
||||
// Render the shared address list section (used by all layouts)
|
||||
void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
float glassRound, float hs, float vs) {
|
||||
float glassRound, float hs, float vs, float reserveBelow) {
|
||||
using namespace material;
|
||||
const auto& S = schema::UISchema::instance();
|
||||
const float dp = Layout::dpiScale();
|
||||
@@ -188,8 +188,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
}
|
||||
}
|
||||
|
||||
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
|
||||
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
|
||||
float buttonWidth = ((addrBtn.width > 0) ? addrBtn.width : 140.0f) * dp;
|
||||
float spacing = ((addrBtn.gap > 0) ? addrBtn.gap : 8.0f) * dp;
|
||||
float totalButtonsWidth = buttonWidth * 2 + spacing;
|
||||
float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size,
|
||||
S.drawElement("tabs.balance", "buttons-position").size * hs);
|
||||
@@ -225,6 +225,14 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
|
||||
// ---- Glass panel container ----
|
||||
float addrListH = listH;
|
||||
// Cap the card to the space that actually remains here (measured AFTER the title + toolbar are laid
|
||||
// out, so no chrome modelling is needed) minus what the caller reserves for the section below it
|
||||
// (recent-tx). Without this, a fixed dp-scaled listH grows ~1.5x at high font scale and evicts the
|
||||
// Recent Transactions list off the bottom of the fixed, non-scrolling tab host.
|
||||
if (reserveBelow > 0.0f) {
|
||||
float maxH = ImGui::GetContentRegionAvail().y - reserveBelow;
|
||||
if (maxH < addrListH) addrListH = maxH;
|
||||
}
|
||||
if (addrListH < 40.0f * dp) addrListH = 40.0f * dp;
|
||||
|
||||
ImDrawList* dlPanel = ImGui::GetWindowDrawList();
|
||||
@@ -256,7 +264,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
} else if (rows.empty()) {
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
float ch = ImGui::GetContentRegionAvail().y;
|
||||
if (ch < 60) ch = 60;
|
||||
if (ch < 60.0f * dp) ch = 60.0f * dp;
|
||||
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
|
||||
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
|
||||
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
||||
@@ -324,12 +332,12 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
s_dragIdx < (int)rows.size()) {
|
||||
const auto& srcRow = rows[s_dragIdx];
|
||||
const auto& dstRow = rows[s_dropTargetIdx];
|
||||
if (srcRow.info->balance > 1e-9) {
|
||||
if (srcRow.info->spendableBalance > 1e-9) { // only offer a transfer of CONFIRMED funds
|
||||
AddressTransferDialog::TransferInfo ti;
|
||||
ti.fromAddr = srcRow.info->address;
|
||||
ti.toAddr = dstRow.info->address;
|
||||
ti.fromBalance = srcRow.info->balance;
|
||||
ti.toBalance = dstRow.info->balance;
|
||||
ti.fromBalance = srcRow.info->spendableBalance; // spend cap — z_sendmany runs at minconf=1
|
||||
ti.toBalance = dstRow.info->balance; // destination display only
|
||||
ti.fromIsZ = srcRow.isZ;
|
||||
ti.toIsZ = dstRow.isZ;
|
||||
AddressTransferDialog::show(app, ti);
|
||||
@@ -466,7 +474,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
{
|
||||
const auto& starRect = rowLayout.favoriteButton;
|
||||
ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height);
|
||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||
// material::IsRectHovered so the button inherits overlay/popup input blocking
|
||||
bool bHov = material::IsRectHovered(bMin, bMax);
|
||||
dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound);
|
||||
dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp);
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
@@ -492,7 +501,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
if (showEye) {
|
||||
const auto& eyeRect = rowLayout.visibilityButton;
|
||||
ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height);
|
||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||
// material::IsRectHovered so the button inherits overlay/popup input blocking
|
||||
bool bHov = material::IsRectHovered(bMin, bMax);
|
||||
dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound);
|
||||
dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp);
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
@@ -539,12 +549,24 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
|
||||
// User label next to type
|
||||
// User label next to type — clip to the gap before the right-aligned
|
||||
// balance so a long custom label can't overrun the balance on this line
|
||||
// (mirrors the address-line width guard below).
|
||||
if (!row.label.empty()) {
|
||||
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
|
||||
OnSurfaceMedium(), row.label.c_str());
|
||||
float userLabelX = labelX + typeLabelW + Layout::spacingLg();
|
||||
// Balance is drawn right-aligned at contentRight; recompute its left edge here.
|
||||
char balBufPeek[32];
|
||||
snprintf(balBufPeek, sizeof(balBufPeek), "%.8f", addr.balance);
|
||||
float balW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBufPeek).x;
|
||||
float userLabelAvailW = (contentRight - balW - Layout::spacingMd()) - userLabelX;
|
||||
std::string userLabel = material::TruncateToWidth(
|
||||
row.label, capFont, capFont->LegacySize, userLabelAvailW);
|
||||
if (userLabelAvailW > 0.0f) {
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(userLabelX, cy),
|
||||
OnSurfaceMedium(), userLabel.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,6 +805,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
|
||||
const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f);
|
||||
const auto& state = app->state();
|
||||
|
||||
float headerStartY = ImGui::GetCursorPosY();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_transactions"));
|
||||
ImGui::SameLine();
|
||||
@@ -790,6 +813,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
|
||||
app->setCurrentPage(NavPage::History);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
float headerHeight = ImGui::GetCursorPosY() - headerStartY;
|
||||
|
||||
float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
|
||||
float availableListH = ImGui::GetContentRegionAvail().y;
|
||||
@@ -798,14 +822,17 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
|
||||
const auto& txs = state.transactions;
|
||||
int count = std::min(4, (int)txs.size()); // show only the 4 most recent (state.transactions is newest-first)
|
||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
||||
// Only draw as many rows as fully fit within the reserved section height (header + rows);
|
||||
// dropping the overflow row is fine since "View All" already links to full History.
|
||||
int maxRows = std::max(1, (int)((recentH - headerHeight) / rowH));
|
||||
int count = std::min({4, maxRows, (int)txs.size()}); // show only the most recent (state.transactions is newest-first)
|
||||
|
||||
if (count == 0) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions_yet"));
|
||||
} else {
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImFont* capFont = Type().caption();
|
||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
||||
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
|
||||
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||
|
||||
@@ -820,7 +847,11 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());
|
||||
|
||||
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
// Start the address column past the MEASURED type-label width (plus a fixed gap) so it can
|
||||
// never overlap the label — a fixed schema offset shrinks below the label width at narrow
|
||||
// widths (hs < 1) and collides. Mirrors how amtX/agoSz measure their own text below.
|
||||
ImVec2 typeSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.typeText.c_str());
|
||||
float addrX = tx_x + typeSz.x + Layout::spacingMd();
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
|
||||
|
||||
@@ -835,13 +866,13 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
|
||||
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.timeText.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs, rowPos.y + 2 * dp),
|
||||
OnSurfaceDisabled(), display.timeText.c_str());
|
||||
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
||||
if (material::IsRectHovered(rowPos, rowEnd)) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||
dl->AddRectFilled(rowPos, rowEnd, SurfaceOverlay(15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::History);
|
||||
@@ -868,7 +899,7 @@ void RenderSyncBar(App* app, ImDrawList* dl, float vs) {
|
||||
ImVec2 barPos = ImGui::GetCursorScreenPos();
|
||||
dl->AddRectFilled(barPos,
|
||||
ImVec2(barPos.x + barW, barPos.y + barH),
|
||||
IM_COL32(255, 255, 255, 15), 1.0f * dp);
|
||||
SurfaceOverlay(15), 1.0f * dp);
|
||||
dl->AddRectFilled(barPos,
|
||||
ImVec2(barPos.x + barW * prog, barPos.y + barH),
|
||||
WithAlpha(Warning(), 200), 1.0f * dp);
|
||||
|
||||
@@ -26,7 +26,8 @@ extern bool s_generating_z_address;
|
||||
void UpdateBalanceLerp(App* app);
|
||||
void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs,
|
||||
float heroHeightOverride = -1.0f);
|
||||
void RenderSharedAddressList(App* app, float listH, float availW, float glassRound, float hs, float vs);
|
||||
void RenderSharedAddressList(App* app, float listH, float availW, float glassRound, float hs, float vs,
|
||||
float reserveBelow = 0.0f);
|
||||
void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs);
|
||||
void RenderSyncBar(App* app, ImDrawList* dl, float vs);
|
||||
|
||||
|
||||
+137
-93
@@ -31,6 +31,7 @@
|
||||
#include "imgui.h"
|
||||
#include <toml++/toml.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <cmath>
|
||||
@@ -196,7 +197,9 @@ void RenderBalanceTab(App* app)
|
||||
for (const auto& l : allLayouts) {
|
||||
if (l.id == layoutId) { displayName = l.name; break; }
|
||||
}
|
||||
Notifications::instance().info("Layout: " + displayName);
|
||||
char layoutToast[128];
|
||||
snprintf(layoutToast, sizeof(layoutToast), TR("balance_layout_switched"), displayName.c_str());
|
||||
Notifications::instance().info(layoutToast);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,9 +302,9 @@ static void RenderBalanceClassic(App* app)
|
||||
float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();
|
||||
|
||||
// Card height: must fit the Market card's content (overline + price + 24h)
|
||||
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f);
|
||||
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f);
|
||||
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f);
|
||||
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f) * dp;
|
||||
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f) * dp;
|
||||
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f) * dp;
|
||||
float marketContentH = cardPadLg
|
||||
+ ovFont->LegacySize + ovGap
|
||||
+ sub1->LegacySize + 2.0f * dp
|
||||
@@ -320,7 +323,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Helper: draw accent stripe on left edge, clipped to card rounded corners.
|
||||
// We draw a full-size rounded rect (left corners only) and clip it to the
|
||||
// stripe width so the shape itself follows the card rounding.
|
||||
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
|
||||
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
|
||||
auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) {
|
||||
dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
|
||||
dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
|
||||
@@ -359,8 +362,11 @@ static void RenderBalanceClassic(App* app)
|
||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.classic", "logo-opacity").sizeOr(180.0f)));
|
||||
}
|
||||
|
||||
std::string totalLabelUpper = TR("total_balance_label");
|
||||
std::transform(totalLabelUpper.begin(), totalLabelUpper.end(), totalLabelUpper.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy),
|
||||
OnSurfaceMedium(), "TOTAL BALANCE");
|
||||
OnSurfaceMedium(), totalLabelUpper.c_str());
|
||||
cy += ovFont->LegacySize + ovGap;
|
||||
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||
@@ -387,7 +393,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Sync progress or mining indicator (whichever fits)
|
||||
if (state.sync.syncing && state.sync.headers > 0) {
|
||||
float pct = static_cast<float>(state.sync.verification_progress) * 100.0f;
|
||||
snprintf(buf, sizeof(buf), "Syncing %.1f%%", pct);
|
||||
snprintf(buf, sizeof(buf), TR("balance_syncing_pct"), pct);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
||||
Warning(), buf);
|
||||
|
||||
@@ -401,7 +407,7 @@ static void RenderBalanceClassic(App* app)
|
||||
dl->PushClipRect(ImVec2(cMin.x, barTop), cMax, true);
|
||||
// Background track
|
||||
dl->AddRectFilled(cMin, cMax,
|
||||
IM_COL32(255, 255, 255, 15), cardSpec.rounding);
|
||||
SurfaceOverlay(15), cardSpec.rounding);
|
||||
// Progress fill — additional horizontal clip
|
||||
float progRight = cMin.x + (cMax.x - cMin.x) * prog;
|
||||
dl->PushClipRect(ImVec2(cMin.x, barTop), ImVec2(progRight, cMax.y), true);
|
||||
@@ -418,7 +424,11 @@ static void RenderBalanceClassic(App* app)
|
||||
dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f),
|
||||
S.drawElement("tabs.balance.classic", "mining-dot-radius").sizeOr(3.0f), mineCol);
|
||||
double hr = state.mining.localHashrate;
|
||||
snprintf(buf, sizeof(buf), " Mining %s", FormatHashrate(hr).c_str());
|
||||
// Leading indent clears the mining dot drawn at cx+4dp; the text
|
||||
// itself starts at cx+12dp so keep the two spaces for spacing parity.
|
||||
char mineFmt[64];
|
||||
snprintf(mineFmt, sizeof(mineFmt), " %s", TR("balance_mining_rate"));
|
||||
snprintf(buf, sizeof(buf), mineFmt, FormatHashrate(hr).c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx + 12 * dp, cy),
|
||||
WithAlpha(Success(), 200), buf);
|
||||
@@ -436,7 +446,7 @@ static void RenderBalanceClassic(App* app)
|
||||
ImU32 bd = WithAlpha(fg, 90);
|
||||
snprintf(buf, sizeof(buf), "%s %s",
|
||||
TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str());
|
||||
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd);
|
||||
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd, ImVec2(4.0f * dp, 2.0f * dp));
|
||||
// Hover → explain what stale means and how old the data actually is.
|
||||
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
|
||||
Tooltip("%s", TR("data_stale_tooltip"));
|
||||
@@ -446,7 +456,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +487,7 @@ static void RenderBalanceClassic(App* app)
|
||||
{
|
||||
float privPct = (s_dispTotal > 1e-9)
|
||||
? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f;
|
||||
snprintf(buf, sizeof(buf), "%.0f%% of total · %d Z-addr",
|
||||
snprintf(buf, sizeof(buf), TR("baltab_pct_of_total_zaddr"),
|
||||
privPct, (int)state.z_addresses.size());
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
||||
WithAlpha(Success(), 160), buf);
|
||||
@@ -488,8 +498,8 @@ static void RenderBalanceClassic(App* app)
|
||||
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
|
||||
ImVec2 ts = capFont->CalcTextSizeA(
|
||||
capFont->LegacySize, 10000, 0, buf);
|
||||
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f);
|
||||
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f);
|
||||
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
|
||||
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f) * dp;
|
||||
ImVec2 bMin(cMax.x - ts.x - bp * 3,
|
||||
cMin.y + cardPadLg);
|
||||
ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
|
||||
@@ -503,7 +513,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow + click to Receive
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::Receive);
|
||||
@@ -533,7 +543,7 @@ static void RenderBalanceClassic(App* app)
|
||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||
cy += sub1->LegacySize + valGap;
|
||||
|
||||
snprintf(buf, sizeof(buf), "%d T-addresses",
|
||||
snprintf(buf, sizeof(buf), TR("baltab_t_addresses_count"),
|
||||
(int)state.t_addresses.size());
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
||||
OnSurfaceDisabled(), buf);
|
||||
@@ -541,7 +551,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow + click to Receive
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::Receive);
|
||||
@@ -574,15 +584,18 @@ static void RenderBalanceClassic(App* app)
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, "USD");
|
||||
|
||||
// Measure widest text line to determine sparkline left edge
|
||||
std::string marketLabel = TR("market");
|
||||
std::transform(marketLabel.begin(), marketLabel.end(), marketLabel.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
float textW = std::max(pSz.x + tickGap + usdSz.x,
|
||||
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, "MARKET").x);
|
||||
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f);
|
||||
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
|
||||
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f) * dp;
|
||||
float sparkLeft = cx + textW + sparkGap;
|
||||
float sparkRight = cMax.x - cardPadLg;
|
||||
|
||||
// Left side: label + price + 24h change
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy),
|
||||
OnSurfaceMedium(), "MARKET");
|
||||
OnSurfaceMedium(), marketLabel.c_str());
|
||||
cy += ovFont->LegacySize + ovGap;
|
||||
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy),
|
||||
@@ -598,7 +611,7 @@ static void RenderBalanceClassic(App* app)
|
||||
bool pos = market.change_24h >= 0;
|
||||
ImU32 chgCol = pos ? Success()
|
||||
: Error();
|
||||
snprintf(buf, sizeof(buf), "%s%.1f%% 24h",
|
||||
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"),
|
||||
pos ? "+" : "", market.change_24h);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx, cy), chgCol, buf);
|
||||
@@ -620,7 +633,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow + click to Market
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::Market);
|
||||
@@ -648,7 +661,7 @@ static void RenderBalanceClassic(App* app)
|
||||
float addrH = (classicAddrH >= 0.0f) ? classicAddrH * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, contentAvail.x, glassRound, hs, vs);
|
||||
RenderSharedAddressList(app, addrH, contentAvail.x, glassRound, hs, vs, recentReserve);
|
||||
RenderSharedRecentTx(app, recentReserve, contentAvail.x, hs, vs);
|
||||
}
|
||||
}
|
||||
@@ -682,7 +695,7 @@ static void RenderBalanceDonut(App* app) {
|
||||
else
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.donut", "hero-pad-ratio").sizeOr(8.0f) * vs));
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
|
||||
ImGui::Dummy(ImVec2(0, 2 * dp));
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||
ImFont* heroFont = Type().h2();
|
||||
@@ -777,20 +790,20 @@ static void RenderBalanceDonut(App* app) {
|
||||
ImFont* capFont = Type().caption();
|
||||
ImFont* body2 = Type().body2();
|
||||
|
||||
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f);
|
||||
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f);
|
||||
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f);
|
||||
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f);
|
||||
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f) * dp;
|
||||
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f) * dp;
|
||||
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f) * dp;
|
||||
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f) * dp;
|
||||
|
||||
// Shielded legend
|
||||
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success());
|
||||
snprintf(buf, sizeof(buf), "Shielded %.8f", s_dispShielded);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_shielded_amount"), s_dispShielded);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf);
|
||||
legendY += capFont->LegacySize + legendLineGap;
|
||||
|
||||
// Transparent legend
|
||||
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning());
|
||||
snprintf(buf, sizeof(buf), "Transparent %.8f", s_dispTransparent);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_transparent_amount"), s_dispTransparent);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf);
|
||||
legendY += capFont->LegacySize + legendSectionGap;
|
||||
|
||||
@@ -798,15 +811,15 @@ static void RenderBalanceDonut(App* app) {
|
||||
const auto& market = state.market;
|
||||
if (market.price_usd > 0) {
|
||||
if (market.price_usd >= 0.01)
|
||||
snprintf(buf, sizeof(buf), "Market: $%.4f", market.price_usd);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_market_price_4dp"), market.price_usd);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "Market: $%.8f", market.price_usd);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_market_price_8dp"), market.price_usd);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
|
||||
OnSurfaceMedium(), buf);
|
||||
legendY += capFont->LegacySize + 4 * dp;
|
||||
|
||||
bool pos = market.change_24h >= 0;
|
||||
snprintf(buf, sizeof(buf), "%s%.1f%% 24h", pos ? "+" : "", market.change_24h);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"), pos ? "+" : "", market.change_24h);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
|
||||
pos ? Success() : Error(), buf);
|
||||
}
|
||||
@@ -822,7 +835,7 @@ static void RenderBalanceDonut(App* app) {
|
||||
float addrH = (donutAddrOverride >= 0.0f) ? donutAddrOverride * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs);
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
|
||||
RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
|
||||
}
|
||||
|
||||
@@ -933,7 +946,7 @@ static void RenderBalanceConsolidated(App* app) {
|
||||
float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f);
|
||||
dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY),
|
||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)),
|
||||
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f));
|
||||
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f) * dp);
|
||||
|
||||
// Bottom half: proportion bars
|
||||
float barY = divY + Layout::spacingSm();
|
||||
@@ -947,10 +960,13 @@ static void RenderBalanceConsolidated(App* app) {
|
||||
|
||||
// Shielded bar
|
||||
float shieldX = cardMin.x + pad;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(shieldX, barY), Success(), "SHIELDED");
|
||||
std::string shieldLabelUpper = TR("shielded");
|
||||
std::transform(shieldLabelUpper.begin(), shieldLabelUpper.end(), shieldLabelUpper.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(shieldX, barY), Success(), shieldLabelUpper.c_str());
|
||||
barY += ovFont->LegacySize + 4 * dp;
|
||||
dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW, barY + barH),
|
||||
IM_COL32(255, 255, 255, 15), barH * 0.5f);
|
||||
SurfaceOverlay(15), barH * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW * shieldRatio, barY + barH),
|
||||
WithAlpha(Success(), 180), barH * 0.5f);
|
||||
barY += barH + 2 * dp;
|
||||
@@ -960,10 +976,13 @@ static void RenderBalanceConsolidated(App* app) {
|
||||
// Transparent bar
|
||||
float transX = cardMin.x + pad * 2 + halfW;
|
||||
barY = divY + Layout::spacingSm();
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(transX, barY), Warning(), "TRANSPARENT");
|
||||
std::string transLabelUpper = TR("transparent");
|
||||
std::transform(transLabelUpper.begin(), transLabelUpper.end(), transLabelUpper.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(transX, barY), Warning(), transLabelUpper.c_str());
|
||||
barY += ovFont->LegacySize + 4 * dp;
|
||||
dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW, barY + barH),
|
||||
IM_COL32(255, 255, 255, 15), barH * 0.5f);
|
||||
SurfaceOverlay(15), barH * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW * transRatio, barY + barH),
|
||||
WithAlpha(Warning(), 180), barH * 0.5f);
|
||||
barY += barH + 2 * dp;
|
||||
@@ -982,7 +1001,7 @@ static void RenderBalanceConsolidated(App* app) {
|
||||
dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), cardMax, true);
|
||||
// Background track
|
||||
dl->AddRectFilled(cardMin, cardMax,
|
||||
IM_COL32(255, 255, 255, 15), glassRound);
|
||||
SurfaceOverlay(15), glassRound);
|
||||
// Progress fill — additional horizontal clip
|
||||
float progRight = cardMin.x + (cardMax.x - cardMin.x) * prog;
|
||||
dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), ImVec2(progRight, cardMax.y), true);
|
||||
@@ -999,7 +1018,7 @@ static void RenderBalanceConsolidated(App* app) {
|
||||
float addrH = (consAddrOverride >= 0.0f) ? consAddrOverride * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs);
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
|
||||
RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
|
||||
}
|
||||
|
||||
@@ -1075,11 +1094,20 @@ static void RenderBalanceDashboard(App* app) {
|
||||
snprintf(shBuf, sizeof(shBuf), "%.8f", s_dispShielded);
|
||||
snprintf(trBuf, sizeof(trBuf), "%.8f", s_dispTransparent);
|
||||
|
||||
// Localized captions — raw dl->AddText (no auto-uppercase), so uppercase to preserve the caption look.
|
||||
auto upperTR = [](const char* key) {
|
||||
std::string s = TR(key);
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return (char)std::toupper(c); });
|
||||
return s;
|
||||
};
|
||||
std::string lblShielded = upperTR("shielded"), lblTransparent = upperTR("transparent");
|
||||
std::string lblQuickSend = upperTR("quick_send"), lblQuickReceive = upperTR("quick_receive");
|
||||
|
||||
TileInfo tiles[4] = {
|
||||
{"SHIELDED", shBuf, S.resolveColor("var(--accent-shielded)", Success()), ICON_MD_SHIELD, NavPage::Receive, false},
|
||||
{"TRANSPARENT", trBuf, S.resolveColor("var(--accent-transparent)", Warning()), ICON_MD_CIRCLE, NavPage::Receive, false},
|
||||
{"QUICK SEND", "Send", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_MADE, NavPage::Send, true},
|
||||
{"QUICK RECEIVE", "Receive", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_RECEIVED, NavPage::Receive, true},
|
||||
{lblShielded.c_str(), shBuf, S.resolveColor("var(--accent-shielded)", Success()), ICON_MD_SHIELD, NavPage::Receive, false},
|
||||
{lblTransparent.c_str(), trBuf, S.resolveColor("var(--accent-transparent)", Warning()), ICON_MD_CIRCLE, NavPage::Receive, false},
|
||||
{lblQuickSend.c_str(), "Send", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_MADE, NavPage::Send, true},
|
||||
{lblQuickReceive.c_str(), "Receive", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_RECEIVED, NavPage::Receive, true},
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -1094,7 +1122,7 @@ static void RenderBalanceDashboard(App* app) {
|
||||
|
||||
// Accent stripe — clipped to tile rounded corners
|
||||
{
|
||||
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
|
||||
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
|
||||
dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true);
|
||||
dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
|
||||
ImDrawFlags_RoundCornersLeft);
|
||||
@@ -1125,13 +1153,13 @@ static void RenderBalanceDashboard(App* app) {
|
||||
tiles[i].accent, tiles[i].value);
|
||||
} else {
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tMin.x + tilePad, py),
|
||||
OnSurfaceMedium(), "Click to open");
|
||||
OnSurfaceMedium(), TR("tile_click_to_open"));
|
||||
}
|
||||
|
||||
// Click
|
||||
if (material::IsRectHovered(tMin, tMax)) {
|
||||
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
||||
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(tiles[i].nav);
|
||||
@@ -1147,7 +1175,7 @@ static void RenderBalanceDashboard(App* app) {
|
||||
float addrH = (dashAddrOverride >= 0.0f) ? dashAddrOverride * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs);
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
|
||||
RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
|
||||
}
|
||||
|
||||
@@ -1180,7 +1208,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
// Font-content floor per row: icon + label + value must fit
|
||||
float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
|
||||
+ Layout::spacingSm() * 2;
|
||||
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f);
|
||||
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f) * dp;
|
||||
float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3;
|
||||
float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
|
||||
float stackH;
|
||||
@@ -1210,10 +1238,10 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
};
|
||||
|
||||
RowInfo rowInfos[4] = {
|
||||
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
|
||||
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
|
||||
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
|
||||
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
|
||||
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
|
||||
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
|
||||
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
|
||||
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -1268,7 +1296,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
// Proportion bar (for shielded/transparent rows — fills gap between label and amount)
|
||||
if (i == 1 || i == 2) {
|
||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
|
||||
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
|
||||
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
|
||||
float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
|
||||
float barH = std::max(
|
||||
S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f),
|
||||
@@ -1279,7 +1307,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
float barW = barRight - barLeft;
|
||||
float barY = rowPos.y + (rowH - barH) * 0.5f;
|
||||
dl->AddRectFilled(ImVec2(barLeft, barY), ImVec2(barRight, barY + barH),
|
||||
IM_COL32(255, 255, 255, 15), barH * 0.5f);
|
||||
SurfaceOverlay(15), barH * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(barLeft, barY),
|
||||
ImVec2(barLeft + barW * rowInfos[i].ratio, barY + barH),
|
||||
WithAlpha(rowInfos[i].accent, 180), barH * 0.5f);
|
||||
@@ -1298,8 +1326,8 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
// Sparkline in the gap between label and 24h change
|
||||
if (state.market.price_history.size() >= 2) {
|
||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
|
||||
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
|
||||
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
|
||||
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
|
||||
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f) * dp;
|
||||
float sparkLeft = px + labelSz.x + sparkGap;
|
||||
float sparkRight = chgX - sparkGap;
|
||||
if (sparkLeft < sparkRight) {
|
||||
@@ -1326,7 +1354,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
float addrH = (vstackAddrOverride >= 0.0f) ? vstackAddrOverride * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs);
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
|
||||
RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
|
||||
}
|
||||
|
||||
@@ -1359,8 +1387,8 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
// Font-content floor per row: caption text + vertical padding
|
||||
float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
|
||||
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f);
|
||||
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f);
|
||||
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f) * dp;
|
||||
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f) * dp;
|
||||
float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
|
||||
float cardHOverride = S.drawElement(cfgSec, "card-height").size;
|
||||
float stackH;
|
||||
@@ -1403,13 +1431,13 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
CellInfo cells[2][2] = {
|
||||
// Row 0: Total Balance (left), Shielded (right)
|
||||
{
|
||||
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
|
||||
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
|
||||
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
|
||||
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
|
||||
},
|
||||
// Row 1: Market (left), Transparent (right)
|
||||
{
|
||||
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
|
||||
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
|
||||
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
|
||||
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1473,7 +1501,7 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
float barX = cellMax.x - amtSz.x - rowPad - barW - Layout::spacingSm();
|
||||
float barY = cellMin.y + (rowH - barH) * 0.5f;
|
||||
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
|
||||
IM_COL32(255, 255, 255, 15), barH * 0.5f);
|
||||
SurfaceOverlay(15), barH * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(barX, barY),
|
||||
ImVec2(barX + barW * cell.ratio, barY + barH),
|
||||
WithAlpha(cell.accent, 180), barH * 0.5f);
|
||||
@@ -1491,8 +1519,8 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
// Sparkline between label and 24h change
|
||||
if (state.market.price_history.size() >= 2) {
|
||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
|
||||
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f);
|
||||
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f);
|
||||
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f) * dp;
|
||||
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f) * dp;
|
||||
float sparkLeft = px + labelSz.x + sparkGap;
|
||||
float sparkRight = chgX - sparkGap;
|
||||
if (sparkLeft < sparkRight) {
|
||||
@@ -1521,7 +1549,7 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
float addrH = (addrOverride >= 0.0f) ? addrOverride * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs);
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
|
||||
RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
|
||||
}
|
||||
|
||||
@@ -1551,7 +1579,7 @@ static void RenderBalanceShield(App* app) {
|
||||
else
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs));
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
|
||||
ImGui::Dummy(ImVec2(0, 2 * dp));
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||
ImFont* heroFont = Type().h2();
|
||||
@@ -1637,7 +1665,7 @@ static void RenderBalanceShield(App* app) {
|
||||
ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
|
||||
gaugeCy + sinf(needleAngle) * needleLen);
|
||||
dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol,
|
||||
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f));
|
||||
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f) * dp);
|
||||
|
||||
// Center text: percentage
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
@@ -1665,13 +1693,19 @@ static void RenderBalanceShield(App* app) {
|
||||
float infoY = panelMin.y + shieldPad;
|
||||
ImFont* ovFont = Type().overline();
|
||||
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Success(), "SHIELDED");
|
||||
std::string shieldLabelUpper = TR("shielded");
|
||||
std::transform(shieldLabelUpper.begin(), shieldLabelUpper.end(), shieldLabelUpper.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
std::string transLabelUpper = TR("transparent");
|
||||
std::transform(transLabelUpper.begin(), transLabelUpper.end(), transLabelUpper.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Success(), shieldLabelUpper.c_str());
|
||||
infoY += ovFont->LegacySize + 2 * dp;
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispShielded);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Success(), buf);
|
||||
infoY += capFont->LegacySize + 6 * dp;
|
||||
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Warning(), "TRANSPARENT");
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Warning(), transLabelUpper.c_str());
|
||||
infoY += ovFont->LegacySize + 2 * dp;
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTransparent);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Warning(), buf);
|
||||
@@ -1697,7 +1731,7 @@ static void RenderBalanceShield(App* app) {
|
||||
float addrH = (shieldAddrOverride >= 0.0f) ? shieldAddrOverride * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs);
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
|
||||
RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
|
||||
}
|
||||
|
||||
@@ -1727,7 +1761,7 @@ static void RenderBalanceTimeline(App* app) {
|
||||
else
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs));
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
|
||||
ImGui::Dummy(ImVec2(0, 2 * dp));
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||
ImFont* heroFont = Type().h2();
|
||||
@@ -1816,10 +1850,16 @@ static void RenderBalanceTimeline(App* app) {
|
||||
spec.rounding = glassRound;
|
||||
|
||||
struct SumCard { const char* label; ImU32 col; double val; bool isMoney; };
|
||||
auto upperTR = [](const char* key) {
|
||||
std::string s = TR(key);
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return (char)std::toupper(c); });
|
||||
return s;
|
||||
};
|
||||
std::string cShielded = upperTR("shielded"), cTransparent = upperTR("transparent"), cMarket = upperTR("market");
|
||||
SumCard cards[3] = {
|
||||
{"SHIELDED", Success(), s_dispShielded, false},
|
||||
{"TRANSPARENT", Warning(), s_dispTransparent, false},
|
||||
{"MARKET", Primary(), state.market.price_usd, true},
|
||||
{cShielded.c_str(), Success(), s_dispShielded, false},
|
||||
{cTransparent.c_str(), Warning(), s_dispTransparent, false},
|
||||
{cMarket.c_str(), Primary(), state.market.price_usd, true},
|
||||
};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
ImVec2 cMin(origin.x + i * (cardW + cGap), origin.y);
|
||||
@@ -1850,7 +1890,7 @@ static void RenderBalanceTimeline(App* app) {
|
||||
float addrH = (tlAddrOverride >= 0.0f) ? tlAddrOverride * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs);
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
|
||||
RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
|
||||
}
|
||||
|
||||
@@ -1887,32 +1927,36 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
ImFont* capFont = Type().caption();
|
||||
if (state.sync.syncing && state.sync.headers > 0) {
|
||||
float pct = static_cast<float>(state.sync.verification_progress) * 100.0f;
|
||||
snprintf(buf, sizeof(buf), "Syncing %.1f%%", pct);
|
||||
snprintf(buf, sizeof(buf), TR("balance_syncing_pct"), pct);
|
||||
Type().textColored(TypeStyle::Caption, Warning(), buf);
|
||||
ImGui::SameLine();
|
||||
}
|
||||
if (state.mining.generate) {
|
||||
double hr = state.mining.localHashrate;
|
||||
snprintf(buf, sizeof(buf), "Mining %s", FormatHashrate(hr).c_str());
|
||||
snprintf(buf, sizeof(buf), TR("balance_mining_rate"), FormatHashrate(hr).c_str());
|
||||
Type().textColored(TypeStyle::Caption, WithAlpha(Success(), 200), buf);
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
// Action buttons right-aligned
|
||||
float btnW = S.drawElement("tabs.balance.two-row", "action-btn-width").sizeOr(80.0f);
|
||||
float btnW = S.drawElement("tabs.balance.two-row", "action-btn-width").sizeOr(80.0f) * dp;
|
||||
float rightEdge = ImGui::GetWindowWidth() - Layout::spacingLg();
|
||||
ImGui::SameLine(rightEdge - btnW * 2 - Layout::spacingSm());
|
||||
if (TactileButton("Send", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
// Stable ## ids keep the button identity fixed across translations.
|
||||
char sendBtn[64], recvBtn[64];
|
||||
snprintf(sendBtn, sizeof(sendBtn), "%s##tworow-send", TR("send"));
|
||||
snprintf(recvBtn, sizeof(recvBtn), "%s##tworow-receive", TR("receive"));
|
||||
if (TactileButton(sendBtn, ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton("Receive", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
if (TactileButton(recvBtn, ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
app->setCurrentPage(NavPage::Receive);
|
||||
}
|
||||
}
|
||||
|
||||
RenderSyncBar(app, dl, vs);
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f)));
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f) * dp));
|
||||
|
||||
// Row 2: 3 mini-cards inline
|
||||
{
|
||||
@@ -1935,7 +1979,7 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f),
|
||||
glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f));
|
||||
ImFont* capFont = Type().caption();
|
||||
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f);
|
||||
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f) * dp;
|
||||
int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f);
|
||||
float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
|
||||
float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();
|
||||
@@ -2010,8 +2054,8 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
|
||||
// Sparkline between price and percentage
|
||||
if (market.price_history.size() >= 2) {
|
||||
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f);
|
||||
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f);
|
||||
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f) * dp;
|
||||
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f) * dp;
|
||||
float sparkLeft = cx + priceSz.x + sparkGap;
|
||||
float sparkRightEdge = sparkRight - sparkGap;
|
||||
if (sparkLeft < sparkRightEdge) {
|
||||
@@ -2037,7 +2081,7 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
float addrH = (twoRowAddrOverride >= 0.0f) ? twoRowAddrOverride * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs);
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
|
||||
RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
|
||||
}
|
||||
|
||||
@@ -2109,10 +2153,10 @@ static void RenderBalanceMinimal(App* app) {
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImVec2 sepPos = ImGui::GetCursorScreenPos();
|
||||
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f);
|
||||
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f);
|
||||
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f) * dp;
|
||||
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f) * dp;
|
||||
float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f);
|
||||
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f);
|
||||
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f) * dp;
|
||||
float x = sepPos.x;
|
||||
float endX = sepPos.x + availW;
|
||||
while (x < endX) {
|
||||
@@ -2130,7 +2174,7 @@ static void RenderBalanceMinimal(App* app) {
|
||||
float addrH = (minAddrOverride >= 0.0f) ? minAddrOverride * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs);
|
||||
RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs, recentReserve);
|
||||
RenderSharedRecentTx(app, recentReserve, availW, hs, vs);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
|
||||
s_loading = false;
|
||||
|
||||
if (!error.empty()) {
|
||||
s_error = "Error: " + error;
|
||||
s_error = std::string(TR("grpa_error_prefix")) + error;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
|
||||
|
||||
s_has_data = true;
|
||||
} else {
|
||||
s_error = "Invalid response from daemon";
|
||||
s_error = TR("grpa_invalid_response_from_daemon");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ void BlockInfoDialog::render(App* app)
|
||||
|
||||
// Height input
|
||||
ImGui::Text("%s", TR("block_height"));
|
||||
ImGui::SetNextItemWidth(heightInput.width);
|
||||
ImGui::SetNextItemWidth(heightInput.width * Layout::dpiScale());
|
||||
ImGui::InputInt("##Height", &s_height);
|
||||
if (s_height < 1) s_height = 1;
|
||||
// Clamp to the chain tip so navigation/typing can't request a height
|
||||
@@ -125,7 +125,7 @@ void BlockInfoDialog::render(App* app)
|
||||
|
||||
// Current block info
|
||||
if (state.sync.blocks > 0) {
|
||||
ImGui::TextDisabled("(Current: %d)", state.sync.blocks);
|
||||
ImGui::TextDisabled(TR("grpa_current_block_paren"), state.sync.blocks);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -135,7 +135,7 @@ void BlockInfoDialog::render(App* app)
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
if (material::StyledButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::TactileButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
if (rpc && rpc->isConnected() && app->worker()) {
|
||||
s_loading = true;
|
||||
s_error.clear();
|
||||
@@ -152,7 +152,7 @@ void BlockInfoDialog::render(App* app)
|
||||
rpc::RPCClient::TraceScope trace("Explorer / Block info");
|
||||
auto hashResult = rpc->call("getblockhash", {height});
|
||||
if (!hashResult.is_string()) {
|
||||
error = "unexpected getblockhash result";
|
||||
error = TR("grpa_unexpected_getblockhash_result");
|
||||
} else {
|
||||
block = rpc->call("getblock", {hashResult.get<std::string>()});
|
||||
}
|
||||
@@ -303,7 +303,7 @@ void BlockInfoDialog::render(App* app)
|
||||
// Navigation buttons
|
||||
if (s_has_data) {
|
||||
if (s_height > 1) {
|
||||
if (material::StyledButton(TR("block_nav_prev"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::TactileButton(TR("block_nav_prev"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
s_height--;
|
||||
s_has_data = false;
|
||||
s_error.clear();
|
||||
@@ -315,7 +315,7 @@ void BlockInfoDialog::render(App* app)
|
||||
// nextblockhash, so this stays hidden there).
|
||||
if (!s_next_hash.empty() &&
|
||||
(state.sync.blocks <= 0 || s_height < state.sync.blocks)) {
|
||||
if (material::StyledButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::TactileButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
s_height++;
|
||||
s_has_data = false;
|
||||
s_error.clear();
|
||||
@@ -323,9 +323,10 @@ void BlockInfoDialog::render(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// Close button at bottom
|
||||
ImGui::SetCursorPosY(ImGui::GetWindowHeight() - 40);
|
||||
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
// Close button at bottom — centered via the shared footer helper (no separator, matching
|
||||
// the prior hand-rolled placement).
|
||||
material::BeginOverlayDialogFooter(closeBtn.width, /*drawSeparator=*/false);
|
||||
if (material::TactileButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
|
||||
@@ -144,7 +144,7 @@ private:
|
||||
|
||||
if (!s_bootstrap) {
|
||||
s_state = State::Failed;
|
||||
s_errorMsg = "Bootstrap not initialized";
|
||||
s_errorMsg = TR("grpc_bootstrap_not_initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ private:
|
||||
s_state = State::Done;
|
||||
} else {
|
||||
s_errorMsg = finalProg.error;
|
||||
if (s_errorMsg.empty()) s_errorMsg = "Bootstrap failed";
|
||||
if (s_errorMsg.empty()) s_errorMsg = TR("grpc_bootstrap_failed");
|
||||
s_state = State::Failed;
|
||||
}
|
||||
s_bootstrap.reset();
|
||||
|
||||
+324
-52
@@ -10,6 +10,7 @@
|
||||
#include "../../data/address_book.h"
|
||||
#include "../../chat/chat_service.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/address_validation.h" // isShieldedAddress — chat requires a z-address recipient
|
||||
#include "../../util/platform.h" // getConfigDir + writeFileAtomically — conversation export (Q11)
|
||||
#include "../../config/settings.h" // per-conversation mute (Q10)
|
||||
#include "../material/colors.h"
|
||||
@@ -59,6 +60,10 @@ bool s_msgsel_dragging = false;
|
||||
// Composer + new-conversation UI state.
|
||||
char s_compose[512] = "";
|
||||
std::string s_compose_cid; // the conversation s_compose is a draft for; draft is wiped when it changes
|
||||
// Live byte offset of the composer's text caret, kept in sync by composeInputCallback while the composer
|
||||
// is active (the callback only fires then). The emoji picker uses it to splice a glyph at the cursor
|
||||
// instead of always appending. -1 = unknown/never-focused => append at the end.
|
||||
int s_composeCursor = -1;
|
||||
// On-chain chat body cap in bytes = (512 − len("utf8:"))/2 − secretstream ABYTES (see chat_outgoing.cpp).
|
||||
// The composer hard-caps input to this; the emoji picker respects it too.
|
||||
constexpr int kChatBodyMaxBytes = (512 - 5) / 2 - 17; // = 236
|
||||
@@ -70,11 +75,16 @@ float s_composerTargetH = 0.0f; // target height measured in the composer block
|
||||
// neither the plain-Enter (submit) nor the Ctrl+Enter shortcut — ImGui does nothing with it. We insert the
|
||||
// newline ourselves here (running under CallbackAlways), respecting the on-chain byte cap.
|
||||
int composeInputCallback(ImGuiInputTextCallbackData* data) {
|
||||
// Track the live caret so the emoji picker can insert at the cursor. This callback runs under
|
||||
// CallbackAlways, which ImGui only invokes while the field is active — so when the composer loses
|
||||
// focus (e.g. to the emoji picker) s_composeCursor keeps the last edit position.
|
||||
s_composeCursor = data->CursorPos;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (io.KeyShift
|
||||
&& (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter))
|
||||
&& data->BufTextLen < kChatBodyMaxBytes) {
|
||||
data->InsertChars(data->CursorPos, "\n");
|
||||
s_composeCursor = data->CursorPos; // InsertChars advanced the caret past the newline
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -83,6 +93,10 @@ char s_new_zaddr[128] = "";
|
||||
char s_new_msg[256] = "";
|
||||
char s_search[80] = ""; // conversation-list filter (Q8)
|
||||
bool s_show_hidden = false; // when on, the list also shows hidden conversations (with an Unhide action)
|
||||
bool s_show_delete_confirm = false; // "Delete conversation?" confirm overlay (revive vs block)
|
||||
std::string s_delete_cid; // conversation targeted by the delete confirm
|
||||
std::string s_delete_name; // its peer name (for the confirm copy / block-list label)
|
||||
bool s_show_blocked = false; // blocked-conversations manager overlay (unblock)
|
||||
bool s_show_emoji_picker = false; // emoji picker overlay — fills the conversation-list pane while open
|
||||
char s_emoji_search[48] = ""; // emoji picker keyword filter
|
||||
|
||||
@@ -415,9 +429,9 @@ void RenderChatSettingsPreview(App* app, float width) {
|
||||
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
|
||||
const std::string peer = "Ava";
|
||||
const PMsg msgs[] = {
|
||||
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 },
|
||||
{ u8"Yep — just confirmed ✅", true, true, false, std::string(TR("chat_you")) + " " + t2 },
|
||||
{ u8"Sending the rest now \U0001F44D", true, false, true, std::string() },
|
||||
{ TR("grpb_preview_msg_payment_through"), false, true, true, peer + " " + t1 },
|
||||
{ TR("grpb_preview_msg_yep_confirmed"), true, true, false, std::string(TR("chat_you")) + " " + t2 },
|
||||
{ TR("grpb_preview_msg_sending_rest"), true, false, true, std::string() },
|
||||
};
|
||||
const int N = 3;
|
||||
|
||||
@@ -441,7 +455,7 @@ void RenderChatSettingsPreview(App* app, float width) {
|
||||
totalH += padIn;
|
||||
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
material::GlassPanelSpec g; g.rounding = 10.0f * dp; g.fillAlpha = 16; g.borderAlpha = 36;
|
||||
material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 16; g.borderAlpha = 36;
|
||||
material::DrawGlassPanel(dl, origin, ImVec2(origin.x + width, origin.y + totalH), g);
|
||||
|
||||
const float leftX = origin.x + padIn;
|
||||
@@ -513,6 +527,16 @@ struct ConvSummary {
|
||||
bool hidden = false; // shown only while "Show hidden" is on
|
||||
};
|
||||
|
||||
// Per-frame memoization of the conversation-list build and the open-thread message list (both otherwise
|
||||
// rescan + copy + sort the whole chat history every frame). File-scope so ResetChatTab() can reset them
|
||||
// on a wallet switch; the hide/unhide/rename handlers reset s_convsKey directly to force a rebuild.
|
||||
std::vector<ConvSummary> s_convs;
|
||||
int s_convsHidden = 0;
|
||||
std::uint64_t s_convsKey = ~0ull;
|
||||
std::string s_threadCid;
|
||||
std::uint64_t s_threadRev = ~0ull;
|
||||
std::vector<chat::ChatMessage> s_threadMsgs;
|
||||
|
||||
// Centered, muted, wrapped hint for the empty states.
|
||||
void centeredHint(const char* text) {
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
@@ -537,7 +561,7 @@ void centeredEmptyState(const char* icon, const char* title, const char* hint) {
|
||||
ImFont* titleF = material::Type().subtitle1();
|
||||
ImFont* hintF = material::Type().body2();
|
||||
const float gap = 8.0f * Layout::dpiScale();
|
||||
const float wrap = std::min(avail.x - 40.0f, 360.0f);
|
||||
const float wrap = std::min(avail.x - 40.0f * Layout::dpiScale(), 360.0f * Layout::dpiScale());
|
||||
const float iconSz = iconF ? scaledSize(iconF) : 40.0f;
|
||||
const float iconH = iconF ? iconF->CalcTextSizeA(iconSz, FLT_MAX, 0.0f, icon).y : 0.0f;
|
||||
const float titleH = titleF->CalcTextSizeA(scaledSize(titleF), FLT_MAX, 0.0f, title).y;
|
||||
@@ -625,16 +649,22 @@ static const EmojiEntry kEmoji[] = {
|
||||
// Emoji picker overlay: fills the conversation-list pane (cancel + keyword search at the top, then a
|
||||
// grid). Clicking an emoji appends its UTF-8 bytes to `buf` (the composer), respecting the buffer.
|
||||
void renderEmojiPickerOverlay(char* buf, std::size_t bufSize, ImTextureID drgxTex) {
|
||||
// Insert a token (emoji glyph or the ":drgx:" shortcode) at the end of the draft, prepending a space
|
||||
// when the draft isn't empty and doesn't already end in whitespace. Respects the on-chain byte cap.
|
||||
// Insert a token (emoji glyph or the ":drgx:" shortcode) at the composer's caret (s_composeCursor,
|
||||
// kept live by composeInputCallback; -1 => end of draft), prepending a space when the char before the
|
||||
// caret is a non-space word char so the emoji doesn't fuse onto it. Respects the on-chain byte cap.
|
||||
// The composer is inactive whenever the picker is open, so it renders straight from buf — splicing
|
||||
// here shows immediately.
|
||||
auto insertToken = [&](const char* tok) {
|
||||
const std::size_t cur = std::strlen(buf), add = std::strlen(tok);
|
||||
const bool needsSpace = cur > 0 && static_cast<unsigned char>(buf[cur - 1]) > ' ';
|
||||
const std::size_t pos = (s_composeCursor < 0)
|
||||
? cur : std::min(static_cast<std::size_t>(s_composeCursor), cur);
|
||||
const bool needsSpace = pos > 0 && static_cast<unsigned char>(buf[pos - 1]) > ' ';
|
||||
const std::size_t pad = needsSpace ? 1 : 0;
|
||||
if (cur + pad + add <= static_cast<std::size_t>(kChatBodyMaxBytes) && cur + pad + add < bufSize) {
|
||||
if (needsSpace) buf[cur] = ' ';
|
||||
std::memcpy(buf + cur + pad, tok, add);
|
||||
buf[cur + pad + add] = '\0';
|
||||
std::memmove(buf + pos + pad + add, buf + pos, (cur - pos) + 1); // shift tail right (incl NUL)
|
||||
if (needsSpace) buf[pos] = ' ';
|
||||
std::memcpy(buf + pos + pad, tok, add);
|
||||
s_composeCursor = static_cast<int>(pos + pad + add); // keep the caret after the inserted token
|
||||
}
|
||||
};
|
||||
if (ImGui::SmallButton(TR("chat_cancel"))) { s_show_emoji_picker = false; s_emoji_search[0] = '\0'; return; }
|
||||
@@ -709,32 +739,46 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
|
||||
// Build conversation summaries (single scan per conversation), sorted by most-recent activity.
|
||||
std::vector<ConvSummary> convs;
|
||||
int hiddenCount = 0;
|
||||
for (const auto& cid : store.conversationIds()) {
|
||||
const bool hidden = app->settings() && app->settings()->isChatHidden(cid);
|
||||
if (hidden) ++hiddenCount;
|
||||
if (hidden && !s_show_hidden) continue; // filtered out unless "Show hidden" is on
|
||||
const auto messages = store.conversation(cid);
|
||||
if (messages.empty()) continue;
|
||||
ConvSummary c;
|
||||
c.cid = cid;
|
||||
c.hidden = hidden;
|
||||
c.count = static_cast<int>(messages.size());
|
||||
for (const auto& m : messages) { // pin to the EARLIEST (establishing) peer z-addr / key (B2 — the
|
||||
if (c.peerZaddr.empty() && !m.peer_zaddr.empty()) c.peerZaddr = m.peer_zaddr; // memo header rides
|
||||
if (c.peerPubKey.empty() && !m.peer_public_key_hex.empty()) c.peerPubKey = m.peer_public_key_hex; // outside the AEAD)
|
||||
// MEMOIZED: this previously rescanned + copied + sorted the ENTIRE chat history every frame. Rebuild
|
||||
// only when the store changed (revision), the show-hidden toggle flipped, or the contact list grew
|
||||
// (peerName resolution). Hide/unhide and rename don't move any of those, so those handlers force a
|
||||
// rebuild by resetting s_convsKey (delete/block already bump the store revision). s_convsKey is reset
|
||||
// in ResetChatTab on wallet switch.
|
||||
const std::uint64_t convsKey =
|
||||
store.revision() * 1000003ull
|
||||
+ static_cast<std::uint64_t>(s_show_hidden ? 1 : 0)
|
||||
+ (book.revision() << 20); // book.revision() catches in-place contact edits (rename) that keep size()
|
||||
if (convsKey != s_convsKey) {
|
||||
s_convs.clear();
|
||||
s_convsHidden = 0;
|
||||
for (const auto& cid : store.conversationIds()) {
|
||||
const bool hidden = app->settings() && app->settings()->isChatHidden(cid);
|
||||
if (hidden) ++s_convsHidden;
|
||||
if (hidden && !s_show_hidden) continue; // filtered out unless "Show hidden" is on
|
||||
const auto messages = store.conversation(cid);
|
||||
if (messages.empty()) continue;
|
||||
ConvSummary c;
|
||||
c.cid = cid;
|
||||
c.hidden = hidden;
|
||||
c.count = static_cast<int>(messages.size());
|
||||
for (const auto& m : messages) { // pin to the EARLIEST (establishing) peer z-addr / key (B2 — the
|
||||
if (c.peerZaddr.empty() && !m.peer_zaddr.empty()) c.peerZaddr = m.peer_zaddr; // memo header rides
|
||||
if (c.peerPubKey.empty() && !m.peer_public_key_hex.empty()) c.peerPubKey = m.peer_public_key_hex; // outside the AEAD)
|
||||
}
|
||||
const auto& last = messages.back();
|
||||
c.lastBody = last.body;
|
||||
c.lastTs = last.timestamp;
|
||||
const int idx = c.peerZaddr.empty() ? -1 : book.findByAddress(c.peerZaddr);
|
||||
c.peerName = (idx >= 0) ? book.entries()[idx].label
|
||||
: shorten(!c.peerZaddr.empty() ? c.peerZaddr : cid);
|
||||
s_convs.push_back(std::move(c));
|
||||
}
|
||||
const auto& last = messages.back();
|
||||
c.lastBody = last.body;
|
||||
c.lastTs = last.timestamp;
|
||||
const int idx = c.peerZaddr.empty() ? -1 : book.findByAddress(c.peerZaddr);
|
||||
c.peerName = (idx >= 0) ? book.entries()[idx].label
|
||||
: shorten(!c.peerZaddr.empty() ? c.peerZaddr : cid);
|
||||
convs.push_back(std::move(c));
|
||||
std::sort(s_convs.begin(), s_convs.end(),
|
||||
[](const ConvSummary& a, const ConvSummary& b) { return a.lastTs > b.lastTs; });
|
||||
s_convsKey = convsKey;
|
||||
}
|
||||
std::sort(convs.begin(), convs.end(),
|
||||
[](const ConvSummary& a, const ConvSummary& b) { return a.lastTs > b.lastTs; });
|
||||
std::vector<ConvSummary>& convs = s_convs;
|
||||
int hiddenCount = s_convsHidden;
|
||||
|
||||
// Keep the selection valid (only when there is something to select).
|
||||
if (!convs.empty() &&
|
||||
@@ -747,6 +791,7 @@ void RenderChatTab(App* app)
|
||||
// for one contact can't be sent to another (B5).
|
||||
if (s_selected_cid != s_compose_cid) {
|
||||
sodium_memzero(s_compose, sizeof(s_compose));
|
||||
s_composeCursor = -1; // fresh draft — next emoji appends until the caret is known again
|
||||
s_compose_cid = s_selected_cid;
|
||||
s_composerAnimH = 0.0f; // re-arm the first-frame snap so the box doesn't animate-collapse on switch
|
||||
}
|
||||
@@ -759,7 +804,7 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const float listW = std::clamp(avail.x * 0.32f, 220.0f, 360.0f);
|
||||
const float listW = std::clamp(avail.x * 0.32f, 220.0f * Layout::dpiScale(), 360.0f * Layout::dpiScale());
|
||||
// Row geometry is logical px — scale by dpiScale() so rows/padding grow with the (DPI-scaled)
|
||||
// fonts. Left raw, at higher DPI the row was too short for the enlarged text and the preview's
|
||||
// right margin (rowW - pad) shrank to ~zero, clipping the last glyph mid-word.
|
||||
@@ -778,7 +823,7 @@ void RenderChatTab(App* app)
|
||||
{
|
||||
ImDrawList* paneDL = ImGui::GetWindowDrawList();
|
||||
const ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||
material::GlassPanelSpec g; g.rounding = 12.0f * Layout::dpiScale(); g.fillAlpha = 20; g.borderAlpha = 34;
|
||||
material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 20; g.borderAlpha = 34;
|
||||
material::DrawGlassPanel(paneDL, pMin, ImVec2(pMin.x + listW, pMin.y + avail.y), g);
|
||||
}
|
||||
// Inner padding so the list content (buttons, search, conversation cards) doesn't hug the glass
|
||||
@@ -828,6 +873,15 @@ void RenderChatTab(App* app)
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
ImGui::InputTextWithHint("##chatsearch", TR("chat_search"), s_search, sizeof(s_search));
|
||||
}
|
||||
// Blocked-conversations manager opener — only when at least one is blocked. Blocked convs have no
|
||||
// stored messages (deleted), so they can't appear in the list; this opens a small manager to unblock.
|
||||
const int blockedCount = app->settings() ? (int)app->settings()->blockedChatConversations().size() : 0;
|
||||
if (blockedCount > 0) {
|
||||
const std::string bl = std::string(TR("chat_blocked_manage")) + " (" + std::to_string(blockedCount) + ")";
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
if (ImGui::SmallButton(bl.c_str())) s_show_blocked = true;
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
const std::string search = s_search;
|
||||
ImGui::Separator();
|
||||
if (convs.empty()) {
|
||||
@@ -878,16 +932,22 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
const float textX = avC.x + avR + pad;
|
||||
|
||||
// Name (top). Hidden conversations (shown via "Show hidden") render dimmed.
|
||||
dl->AddText(nameFont, nameSz, ImVec2(textX, p.y + pad),
|
||||
c.hidden ? material::OnSurfaceMedium() : material::OnSurface(), c.peerName.c_str());
|
||||
// Time (top-right, muted) — compact relative form (Q5).
|
||||
// Time (top-right, muted) — compact relative form (Q5). Measure/draw it FIRST so the name can be
|
||||
// clipped to the column left of it — otherwise a long peer name overruns the timestamp (worse at
|
||||
// HiDPI, where the fixed-length name grows ~1.5x).
|
||||
const std::string when = relativeTime(c.lastTs);
|
||||
float nameRight = mx.x - pad;
|
||||
if (!when.empty()) {
|
||||
const ImVec2 wsz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, when.c_str());
|
||||
dl->AddText(metaFont, metaSz, ImVec2(mx.x - pad - wsz.x, p.y + pad + 1.0f),
|
||||
material::OnSurfaceMedium(), when.c_str());
|
||||
nameRight = mx.x - pad - wsz.x - pad; // reserve the timestamp column + a gap
|
||||
}
|
||||
// Name (top), clipped to the space left of the timestamp. Hidden conversations render dimmed.
|
||||
dl->PushClipRect(ImVec2(textX, p.y), ImVec2(std::max(textX, nameRight), mx.y), true);
|
||||
dl->AddText(nameFont, nameSz, ImVec2(textX, p.y + pad),
|
||||
c.hidden ? material::OnSurfaceMedium() : material::OnSurface(), c.peerName.c_str());
|
||||
dl->PopClipRect();
|
||||
// Preview (bottom, clipped to the text column, muted).
|
||||
const std::string preview = previewOf(c.lastBody);
|
||||
dl->PushClipRect(ImVec2(textX, p.y), ImVec2(mx.x - pad, mx.y), true);
|
||||
@@ -950,7 +1010,7 @@ void RenderChatTab(App* app)
|
||||
ImDrawList* paneDL = ImGui::GetWindowDrawList();
|
||||
const ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||
const float pW = ImGui::GetContentRegionAvail().x;
|
||||
material::GlassPanelSpec g; g.rounding = 12.0f * tdp; g.fillAlpha = 12; g.borderAlpha = 34;
|
||||
material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 12; g.borderAlpha = 34;
|
||||
material::DrawGlassPanel(paneDL, pMin, ImVec2(pMin.x + pW, pMin.y + (avail.y - composerAreaH)), g);
|
||||
}
|
||||
// Inner padding so the header + messages don't hug the glass card's edges (the message child
|
||||
@@ -995,7 +1055,7 @@ void RenderChatTab(App* app)
|
||||
// The toolbar's left edge is known up front (from the button count). A rename (edit) icon is
|
||||
// shown whenever there's an address to save the contact under; the settings "notch" gear is
|
||||
// always the rightmost icon.
|
||||
const int nBtns = 4 + (hasAddr ? 1 : 0);
|
||||
const int nBtns = 5 + (hasAddr ? 1 : 0); // export, mute, hide, delete, settings (+rename)
|
||||
const float toolbarLeft = rightX - (nBtns * ib + (nBtns - 1) * gap);
|
||||
|
||||
// Compact address + lock (or waiting-chip) metrics, reserved to the right of the name.
|
||||
@@ -1046,6 +1106,7 @@ void RenderChatTab(App* app)
|
||||
} else {
|
||||
Notifications::instance().error(TR("address_book_exists"));
|
||||
}
|
||||
s_convsKey = ~0ull; // peerName changed — force the conversation-list memo to rebuild
|
||||
}
|
||||
s_rename_cid.clear();
|
||||
} else if (cancel) {
|
||||
@@ -1147,6 +1208,21 @@ void RenderChatTab(App* app)
|
||||
s_selected_cid.clear();
|
||||
Notifications::instance().info(TR("chat_hidden_toast"));
|
||||
}
|
||||
s_convsKey = ~0ull; // hidden-state changed — force the conversation-list memo to rebuild
|
||||
}
|
||||
bx += ib + gap;
|
||||
}
|
||||
// Delete — clears this conversation's LOCAL history. Destructive (and offers a
|
||||
// "delete & block" variant), so it opens a confirm dialog rather than acting inline.
|
||||
{
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx, by));
|
||||
material::IconButtonStyle a = base;
|
||||
a.tooltip = TR("chat_delete");
|
||||
a.hoverColor = material::Error();
|
||||
if (material::IconButton("##hdr_delete", ICON_MD_DELETE_OUTLINE, ifont, ImVec2(ib, ib), a)) {
|
||||
s_delete_cid = sel->cid;
|
||||
s_delete_name = sel->peerName;
|
||||
s_show_delete_confirm = true;
|
||||
}
|
||||
bx += ib + gap;
|
||||
}
|
||||
@@ -1255,7 +1331,15 @@ void RenderChatTab(App* app)
|
||||
const float groupGap = (compact ? 4.0f : 7.0f) * dp;
|
||||
const float msgGap = (compact ? 2.0f : 3.0f) * dp;
|
||||
const ImU32 accentBase = bubbleAccentColor(cs ? cs->getChatBubbleAccent() : 0);
|
||||
const auto messages = store.conversation(s_selected_cid);
|
||||
// MEMOIZED: store.conversation() linear-scans ALL messages across ALL conversations and
|
||||
// copies+sorts the match every call. Rebuild only when the open thread or the store changes,
|
||||
// not every frame while the thread is simply being read/scrolled.
|
||||
if (s_selected_cid != s_threadCid || store.revision() != s_threadRev) {
|
||||
s_threadMsgs = store.conversation(s_selected_cid);
|
||||
s_threadCid = s_selected_cid;
|
||||
s_threadRev = store.revision();
|
||||
}
|
||||
const auto& messages = s_threadMsgs;
|
||||
// Grouping + per-day separators (Tier 1). Same-sender messages within kGroupWindow share
|
||||
// one meta header and stack tightly; a date pill is drawn once per calendar day.
|
||||
const std::int64_t nowTs = static_cast<std::int64_t>(std::time(nullptr));
|
||||
@@ -1290,7 +1374,7 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
|
||||
const float availW = ImGui::GetContentRegionAvail().x;
|
||||
const float maxBubbleW = std::max(140.0f * dp, availW * 0.72f);
|
||||
const float maxBubbleW = std::clamp(availW * 0.72f, 140.0f * dp, 560.0f * dp);
|
||||
const float innerW = maxBubbleW - 2.0f * bpad;
|
||||
|
||||
// ── Date separator (once per calendar day): a centered pill.
|
||||
@@ -1576,7 +1660,8 @@ void RenderChatTab(App* app)
|
||||
const float ringR = std::max(7.0f, lineH * 0.42f);
|
||||
const float ringPad = 9.0f * tdp;
|
||||
const float ringSlot = 2.0f * ringR + ringPad * 1.6f;
|
||||
const float inputW = std::max(ringSlot + 48.0f * tdp, cw - emojiBtn - 2.0f * inGap - sendW);
|
||||
const float inputW = std::min(720.0f * tdp,
|
||||
std::max(ringSlot + 48.0f * tdp, cw - emojiBtn - 2.0f * inGap - sendW));
|
||||
const float sendX = inputX + inputW + inGap;
|
||||
const float textW = std::max(40.0f * tdp, inputW - ringSlot); // input area, left of the ring
|
||||
const ImVec2 ringC(inputX + inputW - ringPad - ringR, rowY + composerBoxH - ringPad - ringR);
|
||||
@@ -1598,7 +1683,7 @@ void RenderChatTab(App* app)
|
||||
// FrameBg); a flat FrameBg showed the sharp texture.
|
||||
{
|
||||
ImDrawList* cdl = ImGui::GetWindowDrawList();
|
||||
material::GlassPanelSpec g; g.rounding = 8.0f * tdp; g.fillAlpha = 16; g.borderAlpha = 34;
|
||||
material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 16; g.borderAlpha = 34;
|
||||
material::DrawGlassPanel(cdl, ImVec2(inputX, rowY),
|
||||
ImVec2(inputX + inputW, rowY + composerBoxH), g);
|
||||
}
|
||||
@@ -1705,6 +1790,7 @@ void RenderChatTab(App* app)
|
||||
if (submit && s_compose[0] != '\0' && !overCap) {
|
||||
app->sendChatMessage(sel->cid, s_compose);
|
||||
sodium_memzero(s_compose, sizeof(s_compose));
|
||||
s_composeCursor = -1;
|
||||
s_scroll_to_cid = sel->cid;
|
||||
s_composerAnimH = 0.0f; // snap back to collapsed instead of animating while unfocused
|
||||
// Sending closes the emoji picker (it takes over the conversation-list pane) so the list
|
||||
@@ -1726,6 +1812,17 @@ void RenderChatTab(App* app)
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
const float fieldW = ImGui::GetContentRegionAvail().x;
|
||||
material::LabeledInput(TR("chat_new_zaddr"), "##newz", s_new_zaddr, sizeof(s_new_zaddr), fieldW);
|
||||
// Chat rides on encrypted memos, which only shielded (z) addresses carry — a transparent (t)
|
||||
// address can't receive one. Contacts can hold t-addresses, so guard the manual field too:
|
||||
// warn when the entry isn't a valid z-address and keep Send disabled below.
|
||||
const bool newAddrIsZ = dragonx::util::isShieldedAddress(s_new_zaddr);
|
||||
if (s_new_zaddr[0] != '\0' && !newAddrIsZ) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::Warning());
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextUnformatted(TR("chat_new_needs_zaddr"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
// Or pick from contacts — chat needs a shielded z-address, so only z-addr contacts are listed.
|
||||
// Selecting one fills the field above (manual paste still works).
|
||||
ImGui::SetNextItemWidth(fieldW);
|
||||
@@ -1757,7 +1854,7 @@ void RenderChatTab(App* app)
|
||||
material::LabeledInput(TR("chat_new_message"), "##newm", s_new_msg, sizeof(s_new_msg), fieldW);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
const bool canSend = s_new_zaddr[0] != '\0' && s_new_msg[0] != '\0';
|
||||
const bool canSend = newAddrIsZ && s_new_msg[0] != '\0';
|
||||
const float actionW = std::max(130.0f * dp,
|
||||
ImGui::CalcTextSize(TR("chat_new_send")).x + ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp);
|
||||
const float actionGap = Layout::spacingSm();
|
||||
@@ -1784,6 +1881,122 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Delete-conversation confirm (revive-on-new-message vs delete & block) ----
|
||||
if (s_show_delete_confirm) {
|
||||
const float dp = Layout::dpiScale();
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("chat_delete_title");
|
||||
ov.p_open = &s_show_delete_confirm; // X / backdrop closes it (no-op)
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 500.0f; ov.idSuffix = "chatdelete";
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextUnformatted((std::string(TR("chat_delete_body_prefix")) + s_delete_name +
|
||||
TR("chat_delete_body_suffix")).c_str());
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
ImGui::TextUnformatted(TR("chat_delete_revive_note"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImGui::TextUnformatted(TR("chat_delete_local_note"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
auto doDelete = [&](bool block) {
|
||||
// Delete first — if the persisted rows can't be removed, change nothing else (no block,
|
||||
// no toast) so the store and DB can't diverge.
|
||||
if (!app->chatService().deleteConversation(s_delete_cid, block)) {
|
||||
Notifications::instance().error(TR("chat_delete_failed"));
|
||||
s_show_delete_confirm = false;
|
||||
s_delete_cid.clear(); s_delete_name.clear();
|
||||
return;
|
||||
}
|
||||
if (app->settings()) {
|
||||
if (block) app->settings()->setChatBlocked(s_delete_cid, s_delete_name, true);
|
||||
app->settings()->setChatHidden(s_delete_cid, false); // clear any prior hide flag
|
||||
app->settings()->save();
|
||||
}
|
||||
// Revive mode: forget the seen-watermark so a re-imported message badges as unread even if
|
||||
// its stamped time predates the deleted thread. Block mode keeps it, so an unblock-restored
|
||||
// history doesn't all re-badge.
|
||||
if (!block) app->forgetChatConversationSeen(s_delete_cid);
|
||||
if (s_selected_cid == s_delete_cid) s_selected_cid.clear();
|
||||
Notifications::instance().info(block ? TR("chat_blocked_toast") : TR("chat_deleted_toast"));
|
||||
s_show_delete_confirm = false;
|
||||
s_delete_cid.clear(); s_delete_name.clear();
|
||||
};
|
||||
|
||||
auto textW = [&](const char* t) {
|
||||
return ImGui::CalcTextSize(t).x + ImGui::GetStyle().FramePadding.x * 2.0f + 20.0f * dp;
|
||||
};
|
||||
const float gap2 = Layout::spacingSm();
|
||||
const float wDel = std::max(100.0f * dp, textW(TR("chat_delete_confirm")));
|
||||
const float wBlk = std::max(130.0f * dp, textW(TR("chat_delete_block")));
|
||||
const float wCan = std::max(90.0f * dp, textW(TR("chat_cancel")));
|
||||
material::BeginOverlayDialogFooter(wDel + wBlk + wCan + gap2 * 2.0f, /*drawSeparator=*/false);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Error(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Error()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Error(), 235)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnError()));
|
||||
const bool doDel = material::TactileButton(TR("chat_delete_confirm"), ImVec2(wDel, 0));
|
||||
ImGui::SameLine(0, gap2);
|
||||
const bool doBlk = material::TactileButton(TR("chat_delete_block"), ImVec2(wBlk, 0));
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::SameLine(0, gap2);
|
||||
const bool doCancel = material::TactileButton(TR("chat_cancel"), ImVec2(wCan, 0));
|
||||
|
||||
if (doDel) doDelete(false);
|
||||
if (doBlk) doDelete(true);
|
||||
if (doCancel) { s_show_delete_confirm = false; s_delete_cid.clear(); s_delete_name.clear(); }
|
||||
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Blocked-conversations manager (unblock) ----
|
||||
if (s_show_blocked) {
|
||||
const float dp = Layout::dpiScale();
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("chat_blocked_title");
|
||||
ov.p_open = &s_show_blocked;
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 500.0f; ov.idSuffix = "chatblocked";
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
ImGui::TextUnformatted(TR("chat_blocked_desc"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
if (app->settings()) {
|
||||
// Copy so unblocking (which mutates the settings vector) during iteration is safe.
|
||||
const auto blocked = app->settings()->blockedChatConversations();
|
||||
std::string unblockCid;
|
||||
for (const auto& b : blocked) {
|
||||
ImGui::PushID(b.cid.c_str());
|
||||
const std::string label = b.name.empty() ? shorten(b.cid, 10, 6) : b.name;
|
||||
const float bw = ImGui::CalcTextSize(TR("chat_unblock")).x +
|
||||
ImGui::GetStyle().FramePadding.x * 2.0f + 16.0f * dp;
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - bw);
|
||||
if (material::TactileButton(TR("chat_unblock"), ImVec2(bw, 0))) unblockCid = b.cid;
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (!unblockCid.empty()) {
|
||||
app->settings()->setChatBlocked(unblockCid, "", false);
|
||||
app->settings()->save();
|
||||
Notifications::instance().info(TR("chat_unblocked_toast")); // re-imports on the next chat scan
|
||||
if (app->settings()->blockedChatConversations().empty()) s_show_blocked = false;
|
||||
}
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Chat customization modal (opened by the header settings "notch") — house BlurFloat overlay ----
|
||||
if (s_show_chat_settings) {
|
||||
material::OverlayDialogSpec ov;
|
||||
@@ -1847,22 +2060,51 @@ void ResetChatTab()
|
||||
s_rename_focus = false;
|
||||
s_show_new_convo = false;
|
||||
s_show_chat_settings = false;
|
||||
s_show_delete_confirm = false;
|
||||
s_delete_cid.clear();
|
||||
s_delete_name.clear();
|
||||
s_show_blocked = false;
|
||||
// Drop the per-frame memoization caches so the next wallet doesn't briefly render the previous one's
|
||||
// conversations/thread (store.revision() is monotonic and would rebuild anyway, but be explicit).
|
||||
s_convs.clear();
|
||||
s_convsHidden = 0;
|
||||
s_convsKey = ~0ull;
|
||||
s_threadCid.clear();
|
||||
s_threadRev = ~0ull;
|
||||
s_threadMsgs.clear();
|
||||
}
|
||||
|
||||
void RenderChatSettingsControls(App* app, float contentWidth)
|
||||
void RenderChatSettingsControls(App* app, float contentWidth, bool drawCards)
|
||||
{
|
||||
auto* st = app ? app->settings() : nullptr;
|
||||
if (!st) return;
|
||||
const float dp = Layout::dpiScale();
|
||||
const float ctrlW = 250.0f * dp; // control column width (fits a 3-segment control comfortably)
|
||||
const float rowGap = 5.0f * dp;
|
||||
const float rowGap = 10.0f * dp;
|
||||
|
||||
// Optionally paint two glass cards (Appearance | Messaging) around our own two columns so the
|
||||
// Settings tab matches the mockup's card-per-group layout. The chat modal passes drawCards=false
|
||||
// and keeps its plain single-surface layout — the controls themselves are identical either way.
|
||||
ImDrawList* cardDL = ImGui::GetWindowDrawList();
|
||||
const float cardPad = drawCards ? Layout::cardInnerPadding() : 0.0f;
|
||||
material::GlassPanelSpec cardSpec; cardSpec.rounding = Layout::glassRounding();
|
||||
float cardTopScr = 0.0f, cardBaseXScr = 0.0f, cardLeftBotScr = 0.0f;
|
||||
if (drawCards) {
|
||||
cardTopScr = ImGui::GetCursorScreenPos().y;
|
||||
cardBaseXScr = ImGui::GetCursorScreenPos().x;
|
||||
cardDL->ChannelsSplit(2);
|
||||
cardDL->ChannelsSetCurrent(1);
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardBaseXScr, cardTopScr + cardPad));
|
||||
ImGui::Indent(cardPad);
|
||||
}
|
||||
// Right-align controls to the row's true right edge. The Settings tab renders us inside a GlassCard
|
||||
// whose content region isn't narrowed to the card padding, so it passes an explicit contentWidth;
|
||||
// the chat modal's dialog content region is correct, so it passes 0 (auto).
|
||||
// leftX/rowW define the current column the rows lay out in; retargeted below
|
||||
// to split Appearance | Messaging into two columns when the card is wide.
|
||||
float leftX = ImGui::GetCursorPosX();
|
||||
float rowW = (contentWidth > 0.0f) ? contentWidth : ImGui::GetContentRegionAvail().x;
|
||||
float rowW = drawCards ? (contentWidth - 2.0f * cardPad)
|
||||
: ((contentWidth > 0.0f) ? contentWidth : ImGui::GetContentRegionAvail().x);
|
||||
|
||||
// Label left, control right-aligned within [leftX, leftX+rowW]. Leaves the cursor at the control origin.
|
||||
auto beginRow = [&](const char* label) {
|
||||
@@ -1930,7 +2172,7 @@ void RenderChatSettingsControls(App* app, float contentWidth)
|
||||
// Two internal columns when the card is wide enough: Appearance on the left,
|
||||
// Messaging on the right — fills the width and roughly halves the height.
|
||||
// (Mirrors the Node & Security card.) Narrow (the chat modal) stays single-column.
|
||||
const float chatColGap = 24.0f * dp;
|
||||
const float chatColGap = drawCards ? (Layout::cardGap() + 2.0f * cardPad) : (24.0f * dp);
|
||||
const bool chatTwoCol = rowW > 760.0f * dp;
|
||||
const float chatColW = chatTwoCol ? (rowW - chatColGap) * 0.5f : rowW;
|
||||
const float chatBaseLeftX = leftX;
|
||||
@@ -1987,6 +2229,7 @@ void RenderChatSettingsControls(App* app, float contentWidth)
|
||||
// line-start holds the column; retarget leftX so controls right-align in it).
|
||||
if (chatTwoCol) {
|
||||
chatLeftBottomY = ImGui::GetCursorPosY();
|
||||
cardLeftBotScr = ImGui::GetCursorScreenPos().y; // left column bottom (screen), for its card panel
|
||||
ImGui::SetCursorPosY(chatTopY);
|
||||
ImGui::Indent(chatColW + chatColGap);
|
||||
leftX = chatBaseLeftX + chatColW + chatColGap;
|
||||
@@ -2020,12 +2263,41 @@ void RenderChatSettingsControls(App* app, float contentWidth)
|
||||
}
|
||||
|
||||
// Close the two-column band: un-indent and drop below the taller column.
|
||||
const float cardRightBotScr = ImGui::GetCursorScreenPos().y; // right (or only) column bottom, screen
|
||||
if (chatTwoCol) {
|
||||
ImGui::Unindent(chatColW + chatColGap);
|
||||
const float chatRightBottomY = ImGui::GetCursorPosY();
|
||||
ImGui::SetCursorPosX(chatBaseLeftX);
|
||||
ImGui::SetCursorPosY(std::max(chatLeftBottomY, chatRightBottomY));
|
||||
}
|
||||
|
||||
// Paint the glass card(s) behind the content, then merge the channels.
|
||||
if (drawCards) {
|
||||
ImGui::Unindent(cardPad);
|
||||
cardDL->ChannelsSetCurrent(0);
|
||||
const float cardW = (contentWidth - Layout::cardGap()) * 0.5f;
|
||||
if (chatTwoCol) {
|
||||
const float eqBot = std::max(cardLeftBotScr, cardRightBotScr); // equal-height cards (mockup grid stretch)
|
||||
material::DrawGlassPanel(cardDL, ImVec2(cardBaseXScr, cardTopScr),
|
||||
ImVec2(cardBaseXScr + cardW, eqBot + cardPad), cardSpec);
|
||||
material::DrawGlassPanel(cardDL, ImVec2(cardBaseXScr + cardW + Layout::cardGap(), cardTopScr),
|
||||
ImVec2(cardBaseXScr + contentWidth, eqBot + cardPad), cardSpec);
|
||||
} else {
|
||||
material::DrawGlassPanel(cardDL, ImVec2(cardBaseXScr, cardTopScr),
|
||||
ImVec2(cardBaseXScr + contentWidth, cardRightBotScr + cardPad), cardSpec);
|
||||
}
|
||||
cardDL->ChannelsMerge();
|
||||
const float botScr = chatTwoCol ? std::max(cardLeftBotScr, cardRightBotScr) : cardRightBotScr;
|
||||
// Reserve the card footprint with a Dummy so the parent scroll region grows to include it
|
||||
// (a bare SetCursorScreenPos past content warns in ImGui).
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardBaseXScr, cardTopScr));
|
||||
ImGui::Dummy(ImVec2(contentWidth, (botScr - cardTopScr) + cardPad));
|
||||
|
||||
// Live conversation preview below the two cards (Settings tab only — the chat modal renders
|
||||
// its own preview column beside these controls, so it passes drawCards=false and skips this).
|
||||
ImGui::Dummy(ImVec2(0.0f, Layout::spacingMd()));
|
||||
RenderChatSettingsPreview(app, contentWidth);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -34,7 +34,7 @@ void RenderChatTab(App* app);
|
||||
* 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);
|
||||
void RenderChatSettingsControls(App* app, float contentWidth = 0.0f, bool drawCards = false);
|
||||
|
||||
/**
|
||||
* @brief Securely wipe the Chat tab's UI-local state (composer / new-conversation
|
||||
|
||||
@@ -160,10 +160,10 @@ const ConsoleCommandEntry kWalletCommands[] = {
|
||||
"z_sendmany \"RfromAddr\" [{\"address\":\"zs1toAddr\",\"amount\":1.0}]", "send pay private shielded transfer money", true},
|
||||
{"z_shieldcoinbase", "Shield transparent coinbase funds to a z-address", "\"fromaddress\" \"tozaddress\" [fee] [limit]",
|
||||
"Moves newly mined (coinbase) transparent funds into a private shielded z-address, since mined rewards must be shielded before they can be spent normally. Runs in the background and returns an operation id.",
|
||||
"z_shieldcoinbase \"RyourMiningAddr\" \"zs1yourShieldedAddr\"", "shield mining rewards coinbase private hide mined funds move to shielded"},
|
||||
"z_shieldcoinbase \"RyourMiningAddr\" \"zs1yourShieldedAddr\"", "shield mining rewards coinbase private hide mined funds move to shielded", true},
|
||||
{"z_mergetoaddress", "Merge multiple UTXOs/notes to one address", "[\"fromaddress\",...] \"toaddress\" [fee] [limit]",
|
||||
"Combines many small balances (from transparent and/or shielded addresses) into a single destination address in one transaction, to consolidate funds. Runs in the background and returns an operation id.",
|
||||
"z_mergetoaddress [\"RyourAddr\",\"zs1yourShieldedAddr\"] \"zs1destShieldedAddr\"", "merge combine consolidate funds sweep small balances into one address"},
|
||||
"z_mergetoaddress [\"RyourAddr\",\"zs1yourShieldedAddr\"] \"zs1destShieldedAddr\"", "merge combine consolidate funds sweep small balances into one address", true},
|
||||
{"listtransactions", "List recent wallet transactions", "[\"account\"] [count] [from]",
|
||||
"Your most recent wallet transactions, newest first \xE2\x80\x94 amounts, addresses and confirmations.",
|
||||
"listtransactions", "transactions history recent payments received sent"},
|
||||
|
||||
@@ -37,18 +37,21 @@ ConsoleModel::DrainResult ConsoleModel::drain()
|
||||
lines_.pop_front();
|
||||
++result.popped;
|
||||
}
|
||||
++revision_; // deque changed — lets the view memoize its filter/layout passes
|
||||
return result;
|
||||
}
|
||||
|
||||
void ConsoleModel::clear()
|
||||
{
|
||||
lines_.clear();
|
||||
++revision_;
|
||||
}
|
||||
|
||||
bool ConsoleModel::toggleCollapsed(std::size_t i)
|
||||
{
|
||||
if (i >= lines_.size() || lines_[i].foldSpan <= 0) return false;
|
||||
lines_[i].collapsed = !lines_[i].collapsed;
|
||||
++revision_; // fold change alters which lines are visible
|
||||
return lines_[i].collapsed;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "console_channel.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -76,11 +77,16 @@ public:
|
||||
const ConsoleModelLine& operator[](std::size_t i) const { return lines_[i]; }
|
||||
const ConsoleModelLine& back() const { return lines_.back(); }
|
||||
|
||||
// Monotonic counter bumped whenever the visible deque changes (drain added/evicted lines, clear,
|
||||
// fold toggle). The view memoizes its per-frame filter + text-layout passes against this.
|
||||
std::uint64_t revision() const { return revision_; }
|
||||
|
||||
private:
|
||||
const std::size_t max_lines_;
|
||||
std::deque<ConsoleModelLine> lines_; // visible model — main thread only
|
||||
std::vector<ConsoleModelLine> pending_; // guarded by ingest_mutex_
|
||||
std::mutex ingest_mutex_;
|
||||
std::uint64_t revision_ = 0;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -337,7 +337,7 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
float outputH = ComputeConsoleOutputHeight(
|
||||
availHeight,
|
||||
input_height,
|
||||
schema::UI().drawElement("tabs.console", "output-min-height").size,
|
||||
schema::UI().drawElement("tabs.console", "output-min-height").size * Layout::dpiScale(),
|
||||
schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
|
||||
|
||||
ImDrawList* dlOut = ImGui::GetWindowDrawList();
|
||||
@@ -564,12 +564,27 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
// Line count
|
||||
ImGui::TextDisabled(TR("console_line_count"), model_.size());
|
||||
// Line count — the least-critical trailing element. When the row is too narrow to fit the
|
||||
// filter box (at its placeholder-sized minimum) AND its trailing controls, drop the line count
|
||||
// rather than starve/hide the filter (worst at 1024px). Mirror the reservation formula in
|
||||
// drawFilterInput(): trailing = 4 frame-height buttons + the group spacers, and the filter's
|
||||
// hard floor = its placeholder width + frame padding.
|
||||
{
|
||||
char lineCountBuf[64];
|
||||
snprintf(lineCountBuf, sizeof(lineCountBuf), TR("console_line_count"), model_.size());
|
||||
float lineCountW = ImGui::CalcTextSize(lineCountBuf).x + Layout::spacingSm() * 2.0f; // text + its trailing spacer
|
||||
float trailingW = ImGui::GetFrameHeight() * 4.0f + Layout::spacingSm() * 7.0f;
|
||||
float filterMinW = ImGui::CalcTextSize(TR("console_filter_hint")).x
|
||||
+ ImGui::GetStyle().FramePadding.x * 2.0f + 8.0f * Layout::dpiScale();
|
||||
bool showLineCount = ImGui::GetContentRegionAvail().x >= lineCountW + trailingW + filterMinW;
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Spacing();
|
||||
ImGui::SameLine();
|
||||
if (showLineCount) {
|
||||
ImGui::TextDisabled(TR("console_line_count"), model_.size());
|
||||
ImGui::SameLine();
|
||||
ImGui::Spacing();
|
||||
ImGui::SameLine();
|
||||
}
|
||||
}
|
||||
|
||||
// Output filter input
|
||||
drawFilterInput();
|
||||
@@ -617,7 +632,7 @@ void ConsoleTab::drawToolbarStatus(ConsoleCommandExecutor& exec)
|
||||
ConsoleStatusLine st = exec.toolbarStatus();
|
||||
if (!st.text.empty()) {
|
||||
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 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.0f * Layout::dpiScale();
|
||||
|
||||
@@ -687,9 +702,21 @@ void ConsoleTab::drawLogFilterToggles(const ConsoleLogFilterCaps& caps)
|
||||
void ConsoleTab::drawFilterInput()
|
||||
{
|
||||
using namespace material;
|
||||
float zoomBtnSpace = ImGui::GetFrameHeight() * 2.0f + Layout::spacingSm() * 3.0f;
|
||||
float filterAvail = ImGui::GetContentRegionAvail().x - zoomBtnSpace;
|
||||
float filterW = std::min(schema::UI().drawElement("tabs.console", "filter-max-width").size, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size);
|
||||
// Reserve room for EVERY trailing same-line control drawn AFTER this filter on the toolbar
|
||||
// row: the two icon toggles (accent-fill + text-color) and the two zoom buttons, plus the
|
||||
// group spacers between them. Otherwise the filter eats the row and the trailing controls
|
||||
// run off-window (worst at 1024px / font_scale 1.5). All four are GetFrameHeight() wide.
|
||||
float trailingBtnSpace = ImGui::GetFrameHeight() * 4.0f + Layout::spacingSm() * 7.0f;
|
||||
float filterAvail = ImGui::GetContentRegionAvail().x - trailingBtnSpace;
|
||||
float filterMaxW = schema::UI().drawElement("tabs.console", "filter-max-width").size * Layout::dpiScale();
|
||||
float filterW = std::min(filterMaxW, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size);
|
||||
// Never shrink below the placeholder — otherwise the hint clips to "Filter outp" (or the box
|
||||
// vanishes) at narrow widths. Floor = placeholder text + frame padding + a little breathing room.
|
||||
// (drawToolbar() drops the "NNN lines" count when even this floor won't fit alongside the row's
|
||||
// trailing controls, so this max() doesn't push the zoom/color buttons off-window.)
|
||||
float filterMinW = ImGui::CalcTextSize(TR("console_filter_hint")).x
|
||||
+ ImGui::GetStyle().FramePadding.x * 2.0f + 8.0f * Layout::dpiScale();
|
||||
filterW = std::max(filterMinW, filterW);
|
||||
ImGui::SetNextItemWidth(filterW);
|
||||
ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_));
|
||||
if (filter_text_[0] != '\0') {
|
||||
@@ -768,7 +795,10 @@ void ConsoleTab::renderOutput()
|
||||
// height. The inter-line gap is added explicitly to layout_.heights
|
||||
// so that layout_.cumulativeY stays perfectly in sync with actual
|
||||
// cursor positions (avoiding selection-offset drift).
|
||||
float interLineGap = S.drawElement("tabs.console", "output").getFloat("line-spacing", 0.0f);
|
||||
// Raw logical px from the schema; scale it so the inter-line gap grows at font_scale 1.5
|
||||
// (it is added to the already-DPI-scaled GetTextLineHeight in BuildConsoleLayout — scale the
|
||||
// gap only, never the line height).
|
||||
float interLineGap = S.drawElement("tabs.console", "output").getFloat("line-spacing", 0.0f) * Layout::dpiScale();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
|
||||
// Inner padding for glass panel
|
||||
@@ -790,11 +820,23 @@ void ConsoleTab::renderOutput()
|
||||
// segment records which bytes of the source text appear on that visual row, so
|
||||
// hit-testing and selection highlight can map screen positions to exact char offsets.
|
||||
float wrap_width = ClampConsoleWrapWidth(ImGui::GetContentRegionAvail().x, padX);
|
||||
ImFontConsoleMeasure measure(ImGui::GetFont(), ImGui::GetFontSize());
|
||||
layout_ = BuildConsoleLayout(
|
||||
static_cast<int>(visible_indices_.size()),
|
||||
[this](int vi) -> const std::string& { return model_[visible_indices_[vi]].text; },
|
||||
wrap_width, line_height, interLineGap, measure);
|
||||
// Memoize the text-shaping pass: rebuild only when the visible set, wrap width, line height, gap or
|
||||
// zoom changed. Otherwise this re-wrapped and re-measured every visible line (glyph-by-glyph) every
|
||||
// frame, even while idle/scrolled. layout_ is a member, so the cached geometry stays valid for
|
||||
// drawVisibleLines / screenToTextPos when we skip the rebuild.
|
||||
std::uint64_t layoutKey = vis_generation_ * 1000003ull;
|
||||
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(wrap_width * 16.0f);
|
||||
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(line_height * 16.0f);
|
||||
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(interLineGap * 16.0f);
|
||||
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(s_console_zoom * 1000.0f);
|
||||
if (layoutKey != layout_key_) {
|
||||
ImFontConsoleMeasure measure(ImGui::GetFont(), ImGui::GetFontSize());
|
||||
layout_ = BuildConsoleLayout(
|
||||
static_cast<int>(visible_indices_.size()),
|
||||
[this](int vi) -> const std::string& { return model_[visible_indices_[vi]].text; },
|
||||
wrap_width, line_height, interLineGap, measure);
|
||||
layout_key_ = layoutKey;
|
||||
}
|
||||
|
||||
// Mouse/keyboard interaction (wheel-up detach, selection drag, Ctrl+C/A). Raw IO bypasses
|
||||
// the child window's event consumption.
|
||||
@@ -861,6 +903,20 @@ void ConsoleTab::renderOutput()
|
||||
|
||||
void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLower)
|
||||
{
|
||||
// Memoize: rebuild the visible set only when the model changed or the filter state changed.
|
||||
// Otherwise this scanned the entire (up to 10k-line) model with a per-line filter predicate every
|
||||
// frame. The out-params + filter_match_count_/folding_active_ are members that stay valid until the
|
||||
// key moves, so an early return leaves last frame's (still-correct) results in place.
|
||||
std::uint64_t visKey = model_.revision() * 1000003ull;
|
||||
for (const char* p = filter_text_; *p; ++p) visKey = visKey * 131ull + static_cast<unsigned char>(*p);
|
||||
visKey = visKey * 2ull + (s_daemon_messages_enabled ? 1u : 0u);
|
||||
visKey = visKey * 2ull + (s_errors_only_enabled ? 1u : 0u);
|
||||
visKey = visKey * 2ull + (s_rpc_trace_enabled ? 1u : 0u);
|
||||
visKey = visKey * 2ull + (s_app_messages_enabled ? 1u : 0u);
|
||||
if (visKey == vis_key_) return; // nothing that affects the visible set changed
|
||||
vis_key_ = visKey;
|
||||
++vis_generation_; // the layout pass keys off this
|
||||
|
||||
ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled,
|
||||
s_errors_only_enabled, s_rpc_trace_enabled,
|
||||
s_app_messages_enabled};
|
||||
@@ -1050,8 +1106,11 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt
|
||||
ImVec2(cx, cy + sz * 0.7f), triCol); // ▼ expanded
|
||||
}
|
||||
// Click anywhere in the gutter cell for this line's first row toggles the fold.
|
||||
// Guard against a click that is actually dismissing the ConsoleContextMenu (or any
|
||||
// popup) — the same popup guard the text-selection path uses (see mouse_in_output).
|
||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopup) &&
|
||||
mp.x >= output_origin_.x - padX && mp.x < output_origin_.x &&
|
||||
mp.y >= lineOrigin.y && mp.y < lineOrigin.y + lineHeight) {
|
||||
pendingFoldToggle = i;
|
||||
@@ -1406,10 +1465,13 @@ void ConsoleTab::renderInput(ConsoleCommandExecutor& exec)
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::PopFont();
|
||||
|
||||
// Auto-focus on input
|
||||
if (reclaim_focus) {
|
||||
// Auto-focus on input — after submitting a command (reclaim), or once when the Console tab is opened
|
||||
// (focus_input_pending_, set by requestInputFocus() and gated on the console_auto_focus setting).
|
||||
// Skip while a command is running: SetKeyboardFocusHere can't focus the disabled field anyway.
|
||||
if ((reclaim_focus || focus_input_pending_) && !busy) {
|
||||
ImGui::SetKeyboardFocusHere(-1);
|
||||
}
|
||||
focus_input_pending_ = false;
|
||||
}
|
||||
|
||||
bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::string& cmd)
|
||||
@@ -1888,7 +1950,7 @@ void ConsoleTab::renderCommandsPopup(ConsoleCommandExecutor& exec)
|
||||
|
||||
// Both panes sit on soft Material glass surfaces (no hard 1px child border) with inner padding.
|
||||
GlassPanelSpec paneGlass;
|
||||
paneGlass.rounding = 14.0f;
|
||||
paneGlass.rounding = 14.0f * dp;
|
||||
paneGlass.fillAlpha = 30;
|
||||
paneGlass.borderAlpha = 30;
|
||||
|
||||
@@ -2043,6 +2105,12 @@ void ConsoleTab::clear()
|
||||
// line indices) here to avoid an out-of-bounds crash. computeVisibleLines() rebuilds them next frame.
|
||||
visible_indices_.clear();
|
||||
selection_.clear();
|
||||
// Force both memoized passes to rebuild: renderOutput() runs later THIS frame against the now-empty
|
||||
// visible_indices_, and computeVisibleLines() recomputes next frame — without these resets the layout
|
||||
// memo would skip the rebuild and keep stale geometry for the just-emptied set.
|
||||
vis_key_ = ~0ull;
|
||||
layout_key_ = ~0ull;
|
||||
++vis_generation_;
|
||||
stop_confirm_pending_ = false; // a pending 'stop' confirmation is cancelled by clearing
|
||||
addLine(TR("console_cleared"), ConsoleChannel::Info);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "../../rpc/rpc_client.h"
|
||||
#include "../../rpc/rpc_worker.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
@@ -65,6 +66,10 @@ public:
|
||||
*/
|
||||
void clear();
|
||||
|
||||
// Ask the console to place the keyboard focus in the command input on the next render (consumed once).
|
||||
// Called when the user switches to the Console tab, gated by the console_auto_focus setting.
|
||||
void requestInputFocus() { focus_input_pending_ = true; }
|
||||
|
||||
// Scanline effect toggle (set from settings)
|
||||
static bool s_scanline_enabled;
|
||||
|
||||
@@ -156,6 +161,7 @@ private:
|
||||
int history_index_ = -1;
|
||||
char input_buffer_[4096] = {0};
|
||||
bool stop_confirm_pending_ = false; // 'stop' typed once, awaiting a confirming second 'stop'
|
||||
bool focus_input_pending_ = false; // one-shot: focus the command input next render (tab-open auto-focus)
|
||||
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)
|
||||
|
||||
// Auto-scroll state machine (pin-to-bottom, wheel-up cooldown, new-line backlog count).
|
||||
@@ -183,6 +189,14 @@ private:
|
||||
bool has_text_filter_ = false; // computed once per frame (before the toolbar draws it)
|
||||
std::string filter_lower_; // lowercased filter needle for match highlighting
|
||||
|
||||
// Memoization keys so the two expensive per-frame passes rebuild only on change (not every frame):
|
||||
// computeVisibleLines (filter scan over the whole model) is keyed on the model revision + filter
|
||||
// state; BuildConsoleLayout (glyph-by-glyph text shaping of every visible line) is keyed on the
|
||||
// resulting visible-set generation + wrap width + line height/zoom.
|
||||
std::uint64_t vis_key_ = ~0ull; // key of the last computeVisibleLines
|
||||
std::uint64_t vis_generation_ = 0; // bumped whenever visible_indices_ is rebuilt
|
||||
std::uint64_t layout_key_ = ~0ull; // key of the last BuildConsoleLayout
|
||||
|
||||
// Wrap layout for the visible lines (segments + per-line heights + cumulative Y),
|
||||
// recomputed each frame by the pure BuildConsoleLayout (console_text_layout.h) and
|
||||
// consumed by the renderer + hit-testing.
|
||||
|
||||
@@ -40,6 +40,7 @@ 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 int s_confirm_avatar_del_idx = -1; // armed avatar-library index; a 2nd badge click confirms the (irreversible) file delete
|
||||
static char s_edit_label[128] = "";
|
||||
static char s_edit_address[512] = "";
|
||||
static char s_edit_notes[512] = "";
|
||||
@@ -206,6 +207,28 @@ static bool isShieldedAddr(const std::string& a) {
|
||||
return !a.empty() && a[0] == 'z';
|
||||
}
|
||||
|
||||
// Width-aware middle-ellipsis truncation (mirrors the add/edit dialog's local fitMiddle lambda, but
|
||||
// file-scope so the Cards/List rows can share it too): keeps head + tail, shrinking symmetrically
|
||||
// until the rendered width fits maxW. minFront/minBack are the schema-configured floor — below that
|
||||
// the fixed-length util::truncateMiddle result is used instead, so very cramped rows still read as
|
||||
// "front...back" rather than collapsing to a near-empty stub. Addresses are ASCII, so byte-wise
|
||||
// trimming is safe.
|
||||
static std::string truncateAddressToWidth(const std::string& s, ImFont* f, float size, float maxW,
|
||||
int minFront, int minBack) {
|
||||
const std::string floor = util::truncateMiddle(s, minFront, minBack);
|
||||
auto w = [&](const std::string& t){ return f->CalcTextSizeA(size, FLT_MAX, 0, t.c_str()).x; };
|
||||
if (w(s) <= maxW) return s; // fits in full — no truncation needed at all
|
||||
if (maxW <= 0.0f) return floor; // no room to measure against — fall back to the floor
|
||||
const std::string ell = "\xE2\x80\xA6";
|
||||
size_t head = s.size() / 2, tail = s.size() - head;
|
||||
while (head + tail > static_cast<size_t>(minFront + minBack)) {
|
||||
std::string cand = s.substr(0, head) + ell + s.substr(s.size() - tail);
|
||||
if (w(cand) <= maxW) return cand;
|
||||
if (head >= tail) --head; else --tail;
|
||||
}
|
||||
return floor; // couldn't fit even at the configured floor — use the fixed-length result
|
||||
}
|
||||
|
||||
// Accent colour for a contact's address type (Z = shielded/green, T = transparent/amber), tuned per
|
||||
// theme. File-scope so both the list rows and the edit-dialog preview share one source of truth.
|
||||
static ImU32 contactTypeColor(bool shielded, bool light) {
|
||||
@@ -555,7 +578,7 @@ void RenderContactsTab(App* app)
|
||||
sdl->AddRectFilled(ImVec2(cMin.x + 2.0f * dp, cMin.y + 2.0f * dp),
|
||||
ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp),
|
||||
material::WithAlpha(material::Primary(), 210), (segH - 4.0f * dp) * 0.5f);
|
||||
ImU32 fg = active ? IM_COL32(255, 255, 255, 255) : (hov ? material::OnSurface() : material::OnSurfaceMedium());
|
||||
ImU32 fg = active ? material::OnPrimary() : (hov ? material::OnSurface() : material::OnSurfaceMedium());
|
||||
float igW = segIcoF->CalcTextSizeA(segIcoF->LegacySize, FLT_MAX, 0, segIco[i]).x;
|
||||
float lbW = btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, segLbl[i]).x;
|
||||
float gapI = 5.0f * dp;
|
||||
@@ -747,12 +770,16 @@ void RenderContactsTab(App* app)
|
||||
float dr = std::max(7.0f * dp, cell * 0.15f);
|
||||
ImVec2 dcc(mx.x - dr - 3.0f * dp, mn.y + dr + 3.0f * dp);
|
||||
bool dhov = ImGui::IsMouseHoveringRect(ImVec2(dcc.x-dr, dcc.y-dr), ImVec2(dcc.x+dr, dcc.y+dr));
|
||||
const int libIdx = n - 1;
|
||||
const bool delArmed = (s_confirm_avatar_del_idx == libIdx);
|
||||
ImGui::PushID(n);
|
||||
bool thumbClicked = ImGui::InvisibleButton("##avthumb", ImVec2(cell, cell));
|
||||
bool thumbHov = ImGui::IsItemHovered();
|
||||
ImGui::PopID();
|
||||
if (thumbHov || sel || hov) { // draw the delete badge
|
||||
gdl->AddCircleFilled(dcc, dr, dhov ? material::ReadableError() : IM_COL32(0, 0, 0, 175));
|
||||
if (thumbHov || sel || hov || delArmed) { // draw the delete badge (persist while armed)
|
||||
gdl->AddCircleFilled(dcc, dr, (dhov || delArmed) ? material::ReadableError() : IM_COL32(0, 0, 0, 175));
|
||||
// While armed, ring the badge so the "click again to delete" state is unmistakable.
|
||||
if (delArmed) gdl->AddCircle(dcc, dr + 1.5f * dp, material::ReadableError(), 0, 1.5f * dp);
|
||||
ImFont* xf = material::Type().iconSmall();
|
||||
float xsz = dr * 1.35f;
|
||||
ImVec2 xs = xf->CalcTextSizeA(xsz, FLT_MAX, 0, ICON_MD_CLOSE);
|
||||
@@ -760,11 +787,18 @@ void RenderContactsTab(App* app)
|
||||
}
|
||||
if (dhov) { // badge takes priority over selecting the thumbnail
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
material::Tooltip("%s", TR("delete"));
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) pendingDelete = n - 1;
|
||||
// Two-stage confirm: the first click arms this badge; a second click on the SAME
|
||||
// badge deletes the image file (fs::remove is irreversible — no undo).
|
||||
material::Tooltip("%s", delArmed ? TR("address_book_confirm_delete") : TR("delete"));
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
if (delArmed) { pendingDelete = libIdx; s_confirm_avatar_del_idx = -1; }
|
||||
else s_confirm_avatar_del_idx = libIdx; // arm; requires a 2nd deliberate click
|
||||
}
|
||||
} else {
|
||||
if (thumbHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (thumbClicked) s_edit_avatar = "img:" + path;
|
||||
// Any click that lands off this badge (selecting the thumbnail or elsewhere)
|
||||
// disarms it, so a stale armed badge can't be confirmed by an unrelated click.
|
||||
if (thumbClicked) { s_edit_avatar = "img:" + path; s_confirm_avatar_del_idx = -1; }
|
||||
}
|
||||
}
|
||||
col = (col + 1) % cols;
|
||||
@@ -948,7 +982,7 @@ void RenderContactsTab(App* app)
|
||||
}
|
||||
|
||||
// Search / filter (tight against the toolbar row above — no extra spacer)
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
ImGui::SetNextItemWidth(std::min(ImGui::GetContentRegionAvail().x, 700.0f * dp));
|
||||
ImGui::InputTextWithHint("##ContactSearch", TR("contacts_search_placeholder"),
|
||||
s_search, sizeof(s_search));
|
||||
bool searchActive = ImGui::IsItemActive();
|
||||
@@ -1014,7 +1048,7 @@ void RenderContactsTab(App* app)
|
||||
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::GlassPanelSpec g; g.rounding = Layout::glassRounding(); 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));
|
||||
@@ -1117,7 +1151,7 @@ void RenderContactsTab(App* app)
|
||||
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::GlassPanelSpec g; g.rounding = Layout::glassRounding(); 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
|
||||
@@ -1207,10 +1241,13 @@ void RenderContactsTab(App* app)
|
||||
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.
|
||||
// runs under the trailing actions); otherwise middle-truncated to FIT the actual text
|
||||
// column width (tx..textMaxX) rather than a fixed char count — wide rows show more of
|
||||
// the address instead of leaving a dead gap before the action-icon cluster.
|
||||
std::string addr = rowHovered
|
||||
? entry.address
|
||||
: util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
: truncateAddressToWidth(entry.address, adrF, adrSz, textMaxX - tx,
|
||||
addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
|
||||
dl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp),
|
||||
material::OnSurfaceMedium(), addr.c_str());
|
||||
@@ -1378,7 +1415,7 @@ void RenderContactsTab(App* app)
|
||||
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::GlassPanelSpec g; g.rounding = Layout::glassRounding(); 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;
|
||||
|
||||
@@ -408,7 +408,9 @@ private:
|
||||
|
||||
// ---- Below the info card (outside the surface): verify note + install button ----
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x);
|
||||
Type().textColored(TypeStyle::Caption, downgrade ? Warning() : OnSurfaceMedium(), noteStr);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
// Install button sized to its text and centered in the pane.
|
||||
const float bw = ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg();
|
||||
|
||||
@@ -108,13 +108,13 @@ static const char* relativeTime(int64_t timestamp) {
|
||||
int64_t diff = now - timestamp;
|
||||
if (diff < 0) diff = 0;
|
||||
if (diff < 60)
|
||||
snprintf(buf, sizeof(buf), "%lld sec ago", (long long)diff);
|
||||
snprintf(buf, sizeof(buf), TR("grpa_sec_ago"), (long long)diff);
|
||||
else if (diff < 3600)
|
||||
snprintf(buf, sizeof(buf), "%lld min ago", (long long)(diff / 60));
|
||||
snprintf(buf, sizeof(buf), TR("grpa_min_ago"), (long long)(diff / 60));
|
||||
else if (diff < 86400)
|
||||
snprintf(buf, sizeof(buf), "%lld hr ago", (long long)(diff / 3600));
|
||||
snprintf(buf, sizeof(buf), TR("grpa_hr_ago"), (long long)(diff / 3600));
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%lld days ago", (long long)(diff / 86400));
|
||||
snprintf(buf, sizeof(buf), TR("grpa_days_ago"), (long long)(diff / 86400));
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -446,10 +446,10 @@ static void renderSearchBar(App* app, float availWidth) {
|
||||
float navW = navBtnSz * 2.0f + pageW + navGap * 2.0f;
|
||||
|
||||
float inputW = std::min(
|
||||
S.drawElement("tabs.explorer", "search-input-width").size,
|
||||
S.drawElement("tabs.explorer", "search-input-width").size * Layout::dpiScale(),
|
||||
availWidth * 0.65f);
|
||||
float btnW = S.drawElement("tabs.explorer", "search-button-width").size;
|
||||
float barH = S.drawElement("tabs.explorer", "search-bar-height").size;
|
||||
float btnW = S.drawElement("tabs.explorer", "search-button-width").size * Layout::dpiScale();
|
||||
float barH = S.drawElement("tabs.explorer", "search-bar-height").size * Layout::dpiScale();
|
||||
|
||||
// Clamp so search bar never overflows
|
||||
float maxInputW = availWidth - btnW - navW - pad * 4 - Type().iconMed()->LegacySize;
|
||||
@@ -620,7 +620,16 @@ static void renderChainStats(App* app, float availWidth) {
|
||||
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_chain_stats"));
|
||||
drawStatusPill(cardMin, cardW);
|
||||
|
||||
float labelY = cardMin.y + pad * 0.5f + headerH + Layout::spacingLg();
|
||||
// Distribute the two stat blocks (Height, Best Block) evenly through the
|
||||
// content region so the card matches the density of the sibling 2x2 metric
|
||||
// grid instead of pinning Height to the top and Best Block to the bottom
|
||||
// edge with a large empty gap between them.
|
||||
float contentTop = cardMin.y + pad * 0.5f + headerH;
|
||||
float contentBottom = cardMax.y - pad;
|
||||
float blockGap = std::max(Layout::spacingMd(),
|
||||
(contentBottom - contentTop - heroLineH - hashLineH) / 3.0f);
|
||||
|
||||
float labelY = contentTop + blockGap;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cardMin.x + pad, labelY), OnSurfaceMedium(), TR("explorer_block_height"));
|
||||
|
||||
@@ -640,7 +649,7 @@ static void renderChainStats(App* app, float availWidth) {
|
||||
ImVec2(cardMin.x + pad + barW * progress, barY + barH), WithAlpha(Warning(), 180), barH * 0.5f);
|
||||
}
|
||||
|
||||
float hashLabelY = cardMax.y - pad - hashLineH;
|
||||
float hashLabelY = labelY + heroLineH + blockGap;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cardMin.x + pad, hashLabelY), OnSurfaceMedium(), TR("peers_best_block"));
|
||||
|
||||
@@ -755,8 +764,8 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
ImFont* body2 = Type().body2();
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
|
||||
float baseRowH = S.drawElement("tabs.explorer", "row-height").size;
|
||||
float rowRound = S.drawElement("tabs.explorer", "row-rounding").size;
|
||||
float baseRowH = S.drawElement("tabs.explorer", "row-height").size * dp;
|
||||
float rowRound = S.drawElement("tabs.explorer", "row-rounding").size * dp;
|
||||
float headerH = ovFont->LegacySize + Layout::spacingSm() + pad * 0.5f;
|
||||
|
||||
// Stretch card to fill the remaining tab height; rows scroll inside.
|
||||
@@ -989,7 +998,7 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
float fadeZone = S.drawElement("tabs.explorer", "scroll-fade-zone").size;
|
||||
float fadeZone = S.drawElement("tabs.explorer", "scroll-fade-zone").size * dp;
|
||||
ApplyScrollEdgeMask(dl, parentVtx, childDL, childVtx,
|
||||
rowAreaTop, rowAreaTop + rowAreaH, fadeZone, scrollY, scrollMaxY);
|
||||
|
||||
@@ -1116,8 +1125,9 @@ static void renderBlockDetailModal(App* app) {
|
||||
|
||||
// ── Info grid ──
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
float dp = Layout::dpiScale();
|
||||
float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
|
||||
float labelW = S.drawElement("tabs.explorer", "label-column").size;
|
||||
float labelW = S.drawElement("tabs.explorer", "label-column").size * dp;
|
||||
{
|
||||
ImVec2 gridPos = ImGui::GetCursorScreenPos();
|
||||
float gx = gridPos.x;
|
||||
@@ -1178,7 +1188,7 @@ static void renderBlockDetailModal(App* app) {
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
float txRowH = S.drawElement("tabs.explorer", "tx-row-height").size;
|
||||
float txRowH = S.drawElement("tabs.explorer", "tx-row-height").size * dp;
|
||||
ImU32 linkCol = schema::UI().resolveColor("var(--secondary-light)");
|
||||
|
||||
for (int i = 0; i < (int)s_detail_txids.size(); i++) {
|
||||
@@ -1206,7 +1216,7 @@ static void renderBlockDetailModal(App* app) {
|
||||
txDL->AddRectFilled(rowStart,
|
||||
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
|
||||
WithAlpha(OnSurface(), 10),
|
||||
S.drawElement("tabs.explorer", "row-rounding").size);
|
||||
S.drawElement("tabs.explorer", "row-rounding").size * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
material::Tooltip("%s", txid.c_str());
|
||||
}
|
||||
@@ -1305,7 +1315,7 @@ static void renderBlockDetailModal(App* app) {
|
||||
}
|
||||
|
||||
if (s_detail_txids.size() > 100) {
|
||||
snprintf(buf, sizeof(buf), "... showing first 100 of %d", (int)s_detail_txids.size());
|
||||
snprintf(buf, sizeof(buf), TR("grpa_showing_first_100_of"), (int)s_detail_txids.size());
|
||||
ImGui::TextDisabled("%s", buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "faq_content.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace faq {
|
||||
|
||||
// ── Wallet group ───────────────────────────────────────────────────────────
|
||||
// Variant-aware: gs_1 (what the app IS) and sec_1 (where encryption lives) differ between the
|
||||
// full-node and Lite builds; migrate-to-seed (seed_2) is full-node only; and the Lite build gets a
|
||||
// trailing "Lite Wallet" subcategory that stands in for the (hidden) Daemon group. The remaining
|
||||
// answers are worded neutrally so one string serves both variants.
|
||||
static std::vector<FaqSubcategory> buildWalletFaq(bool fullNode)
|
||||
{
|
||||
std::vector<FaqSubcategory> w;
|
||||
w.push_back({ "faq_w_gs_title", {
|
||||
{ fullNode ? "faq_w_gs_1_q" : "faq_l_gs_1_q", fullNode ? "faq_w_gs_1_a" : "faq_l_gs_1_a" },
|
||||
{ "faq_w_gs_2_q", "faq_w_gs_2_a" },
|
||||
{ "faq_w_gs_3_q", "faq_w_gs_3_a" },
|
||||
{ "faq_w_gs_4_q", "faq_w_gs_4_a" },
|
||||
}});
|
||||
w.push_back({ "faq_w_addr_title", {
|
||||
{ "faq_w_addr_1_q", "faq_w_addr_1_a" },
|
||||
{ "faq_w_addr_2_q", "faq_w_addr_2_a" },
|
||||
{ "faq_w_addr_3_q", "faq_w_addr_3_a" },
|
||||
{ "faq_w_addr_4_q", "faq_w_addr_4_a" },
|
||||
}});
|
||||
w.push_back({ "faq_w_send_title", {
|
||||
{ "faq_w_send_1_q", "faq_w_send_1_a" },
|
||||
{ "faq_w_send_2_q", "faq_w_send_2_a" },
|
||||
{ "faq_w_send_3_q", "faq_w_send_3_a" },
|
||||
{ "faq_w_send_4_q", "faq_w_send_4_a" },
|
||||
}});
|
||||
w.push_back({ "faq_w_bal_title", {
|
||||
{ "faq_w_bal_1_q", "faq_w_bal_1_a" },
|
||||
{ "faq_w_bal_2_q", "faq_w_bal_2_a" },
|
||||
{ "faq_w_bal_3_q", "faq_w_bal_3_a" },
|
||||
}});
|
||||
w.push_back({ "faq_w_sec_title", {
|
||||
{ "faq_w_sec_1_q", fullNode ? "faq_w_sec_1_a" : "faq_l_sec_1_a" },
|
||||
{ "faq_w_sec_2_q", "faq_w_sec_2_a" },
|
||||
{ "faq_w_sec_3_q", "faq_w_sec_3_a" },
|
||||
}});
|
||||
{
|
||||
std::vector<FaqEntry> seed = { { "faq_w_seed_1_q", "faq_w_seed_1_a" } };
|
||||
if (fullNode) seed.push_back({ "faq_w_seed_2_q", "faq_w_seed_2_a" }); // migrate-to-seed is full-node only
|
||||
seed.push_back({ "faq_w_seed_3_q", "faq_w_seed_3_a" });
|
||||
seed.push_back({ "faq_w_seed_4_q", "faq_w_seed_4_a" });
|
||||
w.push_back({ "faq_w_seed_title", std::move(seed) });
|
||||
}
|
||||
w.push_back({ "faq_w_chat_title", {
|
||||
{ "faq_w_chat_1_q", "faq_w_chat_1_a" },
|
||||
{ "faq_w_chat_2_q", "faq_w_chat_2_a" },
|
||||
{ "faq_w_chat_3_q", "faq_w_chat_3_a" },
|
||||
{ "faq_w_chat_4_q", "faq_w_chat_4_a" },
|
||||
}});
|
||||
w.push_back({ "faq_w_set_title", {
|
||||
{ "faq_w_set_1_q", "faq_w_set_1_a" },
|
||||
{ "faq_w_set_2_q", "faq_w_set_2_a" },
|
||||
{ "faq_w_set_3_q", "faq_w_set_3_a" },
|
||||
{ "faq_w_set_4_q", "faq_w_set_4_a" },
|
||||
}});
|
||||
// Lite-only: explain the server model (stands in for the hidden Daemon group).
|
||||
if (!fullNode) {
|
||||
w.push_back({ "faq_l_lite_title", {
|
||||
{ "faq_l_lite_1_q", "faq_l_lite_1_a" },
|
||||
{ "faq_l_lite_2_q", "faq_l_lite_2_a" },
|
||||
{ "faq_l_lite_3_q", "faq_l_lite_3_a" },
|
||||
}});
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
const std::vector<FaqSubcategory>& walletFaq(bool fullNode)
|
||||
{
|
||||
static const std::vector<FaqSubcategory> kFull = buildWalletFaq(true);
|
||||
static const std::vector<FaqSubcategory> kLite = buildWalletFaq(false);
|
||||
return fullNode ? kFull : kLite;
|
||||
}
|
||||
|
||||
// ── Daemon group (full-node only) ──────────────────────────────────────────
|
||||
const std::vector<FaqSubcategory>& daemonFaq()
|
||||
{
|
||||
static const std::vector<FaqSubcategory> kDaemon = {
|
||||
{ "faq_d_node_title", {
|
||||
{ "faq_d_node_1_q", "faq_d_node_1_a" },
|
||||
{ "faq_d_node_2_q", "faq_d_node_2_a" },
|
||||
{ "faq_d_node_3_q", "faq_d_node_3_a" },
|
||||
}},
|
||||
{ "faq_d_sync_title", {
|
||||
{ "faq_d_sync_1_q", "faq_d_sync_1_a" },
|
||||
{ "faq_d_sync_2_q", "faq_d_sync_2_a" },
|
||||
{ "faq_d_sync_3_q", "faq_d_sync_3_a" },
|
||||
{ "faq_d_sync_4_q", "faq_d_sync_4_a" },
|
||||
}},
|
||||
{ "faq_d_mgmt_title", {
|
||||
{ "faq_d_mgmt_1_q", "faq_d_mgmt_1_a" },
|
||||
{ "faq_d_mgmt_2_q", "faq_d_mgmt_2_a" },
|
||||
{ "faq_d_mgmt_3_q", "faq_d_mgmt_3_a" },
|
||||
}},
|
||||
{ "faq_d_upd_title", {
|
||||
{ "faq_d_upd_1_q", "faq_d_upd_1_a" },
|
||||
{ "faq_d_upd_2_q", "faq_d_upd_2_a" },
|
||||
{ "faq_d_upd_3_q", "faq_d_upd_3_a" },
|
||||
}},
|
||||
{ "faq_d_mine_title", {
|
||||
{ "faq_d_mine_1_q", "faq_d_mine_1_a" },
|
||||
{ "faq_d_mine_2_q", "faq_d_mine_2_a" },
|
||||
{ "faq_d_mine_3_q", "faq_d_mine_3_a" },
|
||||
{ "faq_d_mine_4_q", "faq_d_mine_4_a" },
|
||||
}},
|
||||
{ "faq_d_net_title", {
|
||||
{ "faq_d_net_1_q", "faq_d_net_1_a" },
|
||||
{ "faq_d_net_2_q", "faq_d_net_2_a" },
|
||||
}},
|
||||
{ "faq_d_perf_title", {
|
||||
{ "faq_d_perf_1_q", "faq_d_perf_1_a" },
|
||||
{ "faq_d_perf_2_q", "faq_d_perf_2_a" },
|
||||
}},
|
||||
{ "faq_d_trbl_title", {
|
||||
{ "faq_d_trbl_1_q", "faq_d_trbl_1_a" },
|
||||
{ "faq_d_trbl_2_q", "faq_d_trbl_2_a" },
|
||||
{ "faq_d_trbl_3_q", "faq_d_trbl_3_a" },
|
||||
{ "faq_d_trbl_4_q", "faq_d_trbl_4_a" },
|
||||
}},
|
||||
};
|
||||
return kDaemon;
|
||||
}
|
||||
|
||||
} // namespace faq
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -0,0 +1,44 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// FAQ content model. The FAQ screen is data-driven: this header exposes the two
|
||||
// top-level groups (Wallet, Daemon) as ordered lists of subcategories, each a list
|
||||
// of question/answer entries. Every string is an i18n KEY (looked up with TR at
|
||||
// render time), so wording + translations live in src/util/i18n.cpp + res/lang/*.json
|
||||
// and never require touching UI code. Add a Q&A by appending a {qKey,aKey} pair here
|
||||
// and its two strings to loadBuiltinEnglish().
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace faq {
|
||||
|
||||
// One question and its answer, both i18n keys. The answer may contain "\n\n"
|
||||
// paragraph breaks; it is rendered wrapped. Keep answers free of printf specifiers
|
||||
// (%d/%s/…) — the i18n layer rejects translations whose format signature drifts.
|
||||
struct FaqEntry {
|
||||
const char* questionKey;
|
||||
const char* answerKey;
|
||||
};
|
||||
|
||||
// A named group of questions. titleKey is an i18n key for the subcategory header.
|
||||
struct FaqSubcategory {
|
||||
const char* titleKey;
|
||||
std::vector<FaqEntry> entries;
|
||||
};
|
||||
|
||||
// The two top-level groups. daemonFaq() is full-node material and is only shown when
|
||||
// the build supports full-node lifecycle actions (see App::supportsFullNodeLifecycleActions()).
|
||||
// walletFaq() is variant-aware: pass fullNode=false for the Lite variant, which swaps in
|
||||
// lite-appropriate answers (no local node / daemon), drops full-node-only entries
|
||||
// (e.g. migrate-to-seed), and appends a "Lite Wallet" subcategory explaining the server model.
|
||||
const std::vector<FaqSubcategory>& walletFaq(bool fullNode);
|
||||
const std::vector<FaqSubcategory>& daemonFaq();
|
||||
|
||||
} // namespace faq
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -0,0 +1,203 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "faq_dialog.h"
|
||||
#include "faq_content.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/text_format.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../layout.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
namespace {
|
||||
|
||||
// Persists across frames (the dialog is re-entered each frame while open). Group 0 = Wallet,
|
||||
// 1 = Daemon. `expanded` keys are FaqEntry::questionKey (stable string literals from faq_content).
|
||||
struct FaqDialogState {
|
||||
int group = 0;
|
||||
char search[128] = "";
|
||||
std::unordered_map<std::string, bool> expanded;
|
||||
};
|
||||
FaqDialogState s_faq;
|
||||
|
||||
bool entryMatches(const faq::FaqEntry& e, const char* query)
|
||||
{
|
||||
return util::containsIgnoreCase(TR(e.questionKey), query) ||
|
||||
util::containsIgnoreCase(TR(e.answerKey), query);
|
||||
}
|
||||
|
||||
// One answer body: wrapped, muted. Shared by the collapsible (non-search) and search paths.
|
||||
void renderAnswer(const char* answerKey)
|
||||
{
|
||||
ImGui::Indent(Layout::spacingMd());
|
||||
ImGui::PushFont(material::Type().body2());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||
ImGui::TextWrapped("%s", TR(answerKey));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
ImGui::Unindent(Layout::spacingMd());
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RenderFaqDialog(App* app, bool* p_open)
|
||||
{
|
||||
auto& S = schema::UI();
|
||||
auto win = S.window("dialogs.faq");
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
const bool daemonAvailable = app && app->supportsFullNodeLifecycleActions();
|
||||
if (!daemonAvailable) s_faq.group = 0; // no Daemon tab in lite builds
|
||||
|
||||
// Floating "BlurFloat" modal, matching the Wallets dialog: live-blur backdrop, no boxed card, a
|
||||
// plain heading (no ✕ — a Close button sits at the bottom). Roomy fixed width; height capped to the
|
||||
// viewport so the content flexes + scrolls on small / HiDPI screens.
|
||||
const float vpH = ImGui::GetMainViewport()->Size.y;
|
||||
const float wantH = (win.height > 0 ? win.height : 640.0f) * dp;
|
||||
material::OverlayDialogSpec spec;
|
||||
spec.title = TR("faq_title");
|
||||
spec.p_open = p_open;
|
||||
spec.style = material::OverlayStyle::BlurFloat;
|
||||
spec.cardWidth = (win.width > 0 ? win.width : 860.0f);
|
||||
spec.cardHeight = std::min(wantH, vpH * 0.86f) / dp;
|
||||
spec.idSuffix = "faq";
|
||||
if (!material::BeginOverlayDialog(spec)) {
|
||||
return;
|
||||
}
|
||||
// Esc closes (ImGui consumes Esc itself while the search box is being edited, so this only
|
||||
// fires when the field isn't capturing it).
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) *p_open = false;
|
||||
|
||||
// Subtitle under the plain heading, matching the Wallets dialog's intro caption.
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(),
|
||||
TR(daemonAvailable ? "faq_intro" : "faq_intro_lite"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
const float contentW = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
// ── Group tabs: Wallet | Daemon — only when there's a choice. Lite has just the Wallet group, so a
|
||||
// lone "Wallet" selector is redundant; skip it entirely (the group is already pinned to 0 above).
|
||||
if (daemonAvailable) {
|
||||
const float gap = ImGui::GetStyle().ItemSpacing.x;
|
||||
const float tabW = (contentW - gap) / 2.0f;
|
||||
auto tab = [&](const char* label, int idx) {
|
||||
const bool active = (s_faq.group == idx);
|
||||
if (active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnPrimary()));
|
||||
}
|
||||
if (material::TactileButton(label, ImVec2(tabW, 0))) s_faq.group = idx;
|
||||
if (active) ImGui::PopStyleColor(2);
|
||||
};
|
||||
tab(TR("faq_group_wallet"), 0);
|
||||
ImGui::SameLine();
|
||||
tab(TR("faq_group_daemon"), 1);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// ── Search ──
|
||||
ImGui::SetNextItemWidth(contentW);
|
||||
ImGui::InputTextWithHint("##FaqSearch", TR("faq_search_hint"), s_faq.search, sizeof(s_faq.search));
|
||||
const bool searching = s_faq.search[0] != '\0';
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// ── Scrollable Q&A body ── (reserve room for the Close button footer below)
|
||||
const float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingMd();
|
||||
float bodyH = ImGui::GetContentRegionAvail().y - footerH;
|
||||
if (bodyH < 80.0f * dp) bodyH = 80.0f * dp;
|
||||
// Inner padding gives the content breathing room and, on the right, a clear gap to the LEFT of the
|
||||
// scrollbar (WindowPadding.x is exactly that gap); the scrollbar itself is made chunkier than the
|
||||
// app default. NoScrollWithMouse + ApplySmoothScroll gives the wheel eased scrolling, matching the
|
||||
// Wallets dialog / Settings page (ApplySmoothScroll owns the wheel input and lerps ScrollY).
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingXs()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 16.0f * dp);
|
||||
ImGui::BeginChild("##FaqScroll", ImVec2(0, bodyH), false,
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
material::ApplySmoothScroll();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
const auto& groups = (s_faq.group == 1 && daemonAvailable) ? faq::daemonFaq() : faq::walletFaq(daemonAvailable);
|
||||
bool anyShown = false;
|
||||
|
||||
bool firstSection = true;
|
||||
for (const auto& subcat : groups) {
|
||||
// Collect the entries visible under the current search.
|
||||
std::vector<const faq::FaqEntry*> visible;
|
||||
for (const auto& e : subcat.entries) {
|
||||
if (!searching || entryMatches(e, s_faq.search)) visible.push_back(&e);
|
||||
}
|
||||
if (visible.empty()) continue;
|
||||
|
||||
// Section break: generous space above every section after the first, so topic groups read as
|
||||
// clearly separated bands rather than one uniform list.
|
||||
if (!firstSection) ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
firstSection = false;
|
||||
anyShown = true;
|
||||
|
||||
// Section header — accent overline + a thin full-width rule beneath it, anchoring the group
|
||||
// above its (brighter, normal-case) question rows.
|
||||
material::Type().textColored(material::TypeStyle::Overline,
|
||||
material::Primary(), TR(subcat.titleKey));
|
||||
{
|
||||
const ImVec2 rp = ImGui::GetCursorScreenPos();
|
||||
const float rw = ImGui::GetContentRegionAvail().x;
|
||||
dl->AddLine(ImVec2(rp.x, rp.y + dp), ImVec2(rp.x + rw, rp.y + dp),
|
||||
material::Divider(), 1.0f * dp);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
for (const auto* e : visible) {
|
||||
const float rowW = ImGui::GetContentRegionAvail().x;
|
||||
if (searching) {
|
||||
// Search results: show question + answer directly (no collapsing).
|
||||
ImGui::PushFont(material::Type().subtitle2());
|
||||
ImGui::TextWrapped("%s", TR(e->questionKey));
|
||||
ImGui::PopFont();
|
||||
renderAnswer(e->answerKey);
|
||||
} else {
|
||||
bool& exp = s_faq.expanded[e->questionKey];
|
||||
std::string id = std::string("##faq_") + e->questionKey;
|
||||
material::CollapsibleHeader(dl, id.c_str(), TR(e->questionKey), exp, rowW,
|
||||
material::Type().subtitle2(), material::OnSurface());
|
||||
if (exp) renderAnswer(e->answerKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyShown) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||
ImGui::TextWrapped("%s", TR("faq_no_results"));
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2); // ScrollbarSize, WindowPadding
|
||||
|
||||
// Close button footer (BlurFloat has no ✕ in the heading).
|
||||
const float closeW = 120.0f * dp;
|
||||
material::BeginOverlayDialogFooter(closeW, false);
|
||||
if (material::TactileButton(TR("close"), ImVec2(closeW, 0))) *p_open = false;
|
||||
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -0,0 +1,18 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace dragonx {
|
||||
class App;
|
||||
namespace ui {
|
||||
|
||||
// Renders the Help & FAQ modal. Call every frame while *p_open is true; the dialog
|
||||
// clears *p_open itself on close (✕ / click-outside / Esc). Content is data-driven
|
||||
// (see faq_content.h) and grouped into Wallet and Daemon tabs; the Daemon tab is
|
||||
// hidden on builds without full-node lifecycle support.
|
||||
void RenderFaqDialog(App* app, bool* p_open);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -173,7 +173,7 @@ void KeyExportDialog::render(App* app)
|
||||
s_key = found;
|
||||
s_show_key = wantViewing; // viewing keys are less sensitive
|
||||
} else {
|
||||
s_error = r.ok ? std::string("Key not available for this address") : r.error;
|
||||
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : r.error;
|
||||
}
|
||||
wallet::secureWipeLiteSecret(found);
|
||||
s_fetching = false;
|
||||
@@ -240,14 +240,15 @@ void KeyExportDialog::render(App* app)
|
||||
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
|
||||
} else {
|
||||
// Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ].
|
||||
const float gap = 12.0f;
|
||||
const float dp = Layout::dpiScale();
|
||||
const float gap = 12.0f * dp;
|
||||
const float avail = ImGui::GetContentRegionAvail().x;
|
||||
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
|
||||
float qrSize = avail * 0.30f;
|
||||
if (qrSize < 200.0f) qrSize = 200.0f;
|
||||
if (qrSize > 340.0f) qrSize = 340.0f;
|
||||
if (qrSize < 200.0f * dp) qrSize = 200.0f * dp;
|
||||
if (qrSize > 340.0f * dp) qrSize = 340.0f * dp;
|
||||
float keyW = avail - qrSize - gap;
|
||||
const bool sideBySide = keyW >= 200.0f; // too narrow -> stack the QR under the key
|
||||
const bool sideBySide = keyW >= 200.0f * dp; // too narrow -> stack the QR under the key
|
||||
if (!sideBySide) keyW = avail;
|
||||
|
||||
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact
|
||||
@@ -309,12 +310,10 @@ void KeyExportDialog::render(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Close button
|
||||
// Close button — centered via the shared footer helper (no divider, matching the
|
||||
// dialog's prior leading-Spacing placement) instead of hand-computing the offset.
|
||||
float button_width = closeBtn.width;
|
||||
float avail_width = ImGui::GetContentRegionAvail().x;
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail_width - button_width) / 2.0f);
|
||||
material::BeginOverlayDialogFooter(button_width, /*drawSeparator=*/false);
|
||||
|
||||
if (material::TactileButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
|
||||
@@ -137,6 +137,12 @@ struct PfEditState {
|
||||
};
|
||||
static PfEditState s_pfEdit;
|
||||
|
||||
// Two-stage delete arm/confirm for the master-list per-row delete icon (mirrors the
|
||||
// contacts tab's s_confirm_delete_idx): first click on a row's trash arms it (icon turns
|
||||
// red); a second click on the SAME row within the loop confirms the erase. Hovering/clicking
|
||||
// a different row, or clicking off the icons, disarms. -1 = nothing armed.
|
||||
static int s_pfConfirmDelIdx = -1;
|
||||
|
||||
// The pure price-series math lives in data/market_series.h; the selected chart range is
|
||||
// s_mkt.chartInterval (0=Live 1=1H 2=1D 3=1W 4=1M, session-scoped, default 1M).
|
||||
|
||||
@@ -837,6 +843,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
float rowH = 46.0f * dp;
|
||||
float delSlot = 36.0f * dp; // reserved trailing space for the delete icon + margin
|
||||
int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop
|
||||
bool armedThisFrame = false; // an arm-click also fires the row Selectable; don't let it disarm
|
||||
for (int vi = 0; vi < (int)vis.size(); vi++) {
|
||||
int i = vis[vi];
|
||||
ImGui::PushID(i);
|
||||
@@ -877,6 +884,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
// Per-row delete icon (larger, inset from the edge). Hit-tested manually so it is
|
||||
// NOT an overlapping ImGui item over the row Selectable (which asserted on hover).
|
||||
if (rowHov || selRow) {
|
||||
bool armed = (s_pfConfirmDelIdx == i); // this row is arm-confirmed
|
||||
ImFont* delFont = Type().iconMed();
|
||||
float ds = delFont->LegacySize;
|
||||
ImVec2 dc(rmx.x - Layout::spacingMd() - ds * 0.5f, rmn.y + rowH * 0.5f);
|
||||
@@ -884,11 +892,20 @@ static void RenderPortfolioEditor(App* app)
|
||||
bool dhov = ImGui::IsMouseHoveringRect(dmn, dmx);
|
||||
if (dhov) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) delRow = i;
|
||||
// Hovering a *different* row's trash disarms the previously armed one.
|
||||
if (!armed && s_pfConfirmDelIdx >= 0) s_pfConfirmDelIdx = -1;
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
if (armed) delRow = i; // 2nd click on the armed row erases
|
||||
else { s_pfConfirmDelIdx = i; // 1st click arms this row...
|
||||
armedThisFrame = true; } // ...and must survive the row-select this same click triggers
|
||||
}
|
||||
}
|
||||
ImVec2 isz = delFont->CalcTextSizeA(ds, FLT_MAX, 0, ICON_MD_DELETE_OUTLINE);
|
||||
// A filled trash glyph while armed (vs outline) reinforces the recolor.
|
||||
const char* delGlyph = armed ? ICON_MD_DELETE : ICON_MD_DELETE_OUTLINE;
|
||||
ImU32 delCol = armed ? ReadableError() : (dhov ? Error() : OnSurfaceMedium());
|
||||
ImVec2 isz = delFont->CalcTextSizeA(ds, FLT_MAX, 0, delGlyph);
|
||||
mdl->AddText(delFont, ds, ImVec2(dc.x - isz.x * 0.5f, dc.y - isz.y * 0.5f),
|
||||
dhov ? Error() : OnSurfaceMedium(), ICON_MD_DELETE_OUTLINE);
|
||||
delCol, delGlyph);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -905,10 +922,16 @@ static void RenderPortfolioEditor(App* app)
|
||||
else if (delRow == s_pfEdit.sel) s_pfEdit.sel = std::min(delRow, (int)es.size() - 1);
|
||||
PortfolioBeginEdit(app, s_pfEdit.sel);
|
||||
}
|
||||
} else if (clickedSel != -999) {
|
||||
s_pfConfirmDelIdx = -1; // erase done — storage indices shifted, drop the arm
|
||||
} else if (clickedSel != -999 && !armedThisFrame) {
|
||||
pfCommitIfNeeded(app); // auto-save the current group before switching
|
||||
PortfolioBeginEdit(app, clickedSel);
|
||||
s_pfConfirmDelIdx = -1; // switching rows disarms any pending delete
|
||||
}
|
||||
// A left-click that hit no row/action (empty list space) disarms a pending delete.
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
!ImGui::IsAnyItemHovered())
|
||||
s_pfConfirmDelIdx = -1;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
@@ -1324,9 +1347,10 @@ static void mktDrawPairSelector(App* app, const std::vector<data::ExchangeInfo>&
|
||||
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.market", "exchange-top-gap").size));
|
||||
{
|
||||
float chipH = S.drawElement("tabs.market", "pair-chip-height").height;
|
||||
float chipR = S.drawElement("tabs.market", "pair-chip-radius").radius;
|
||||
float chipSpacing = S.drawElement("tabs.market", "pair-chip-spacing").size;
|
||||
float dp = Layout::dpiScale();
|
||||
float chipH = S.drawElement("tabs.market", "pair-chip-height").height * dp;
|
||||
float chipR = S.drawElement("tabs.market", "pair-chip-radius").radius * dp;
|
||||
float chipSpacing = S.drawElement("tabs.market", "pair-chip-spacing").size * dp;
|
||||
float innerGap = Layout::spacingSm();
|
||||
float sidePad = Layout::spacingMd();
|
||||
|
||||
@@ -1556,7 +1580,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
||||
}
|
||||
} else {
|
||||
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10), OnSurfaceDisabled(), status);
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10 * dp), OnSurfaceDisabled(), status);
|
||||
if (!market.price_loading && !market.price_error.empty()) {
|
||||
std::string errorText = market.price_error;
|
||||
float maxErrorW = cardMax.x - cx0 - Layout::spacingLg();
|
||||
@@ -1566,7 +1590,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
||||
}
|
||||
if (errorText.size() < market.price_error.size()) errorText += "...";
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx0, cy + 10 + sub1->LegacySize + Layout::spacingXs()),
|
||||
ImVec2(cx0, cy + 10 * dp + sub1->LegacySize + Layout::spacingXs()),
|
||||
Warning(), errorText.c_str());
|
||||
}
|
||||
}
|
||||
@@ -1850,7 +1874,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
: (tk == ticks - 1) ? plotRight - lblSz.x
|
||||
: xpos - lblSz.x * 0.5f;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl);
|
||||
ImVec2(lx, plotBottom + 4 * mktDp), OnSurfaceDisabled(), tlbl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2112,11 +2136,29 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
float rowGap = pfRowGapFor(style, mktDp);
|
||||
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
|
||||
|
||||
// Anchor the bottom of the portfolio: the group list otherwise ends at its content height,
|
||||
// leaving un-anchored dead space between the last row and the content-area bottom. Grow the
|
||||
// rows region to fill the remaining vertical space (down to the scroll area's bottom, less a
|
||||
// small margin) and wrap it in a contained glass panel — so it reads as one framed table with
|
||||
// a real bottom edge (like Explorer's block-list card) instead of trailing off into nothing.
|
||||
// The empty state then centres in this full-height panel rather than in a one-row band.
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH));
|
||||
float floorH = ImGui::GetContentRegionAvail().y - gap; // to the scroll bottom, keep a margin
|
||||
float panelH = std::max(rowsH, floorH);
|
||||
{
|
||||
ImVec2 pMin(cardMin.x, cardMin.y + pfSummaryH);
|
||||
ImVec2 pMax(rightEdge, pMin.y + panelH);
|
||||
GlassPanelSpec pg;
|
||||
pg.rounding = Layout::glassRounding();
|
||||
pg.fillAlpha = 12; pg.borderAlpha = 24; // faint container: frames without competing with the per-row cards
|
||||
DrawGlassPanel(dl, pMin, pMax, pg);
|
||||
}
|
||||
// Flush-left child (no window padding) so rows align with the summary; a scrollbar appears
|
||||
// only when the visible groups overflow the bounded height.
|
||||
// only when the visible groups overflow the bounded height. NoBackground so the contained
|
||||
// glass panel drawn above is the sole surface — otherwise the opaque ChildBg (non-acrylic
|
||||
// mode) would paint over the panel fill + border.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::BeginChild("##pfRows", ImVec2(availWidth, rowsH), false);
|
||||
ImGui::BeginChild("##pfRows", ImVec2(availWidth, panelH), false, ImGuiWindowFlags_NoBackground);
|
||||
ImGui::PopStyleVar();
|
||||
// Zero item-spacing INSIDE the child: each row emits an InvisibleButton + a gap Dummy, and
|
||||
// ImGui's default ItemSpacing.y between those items would inflate the content past rowsH and
|
||||
@@ -2125,8 +2167,7 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
ImDrawList* rdl = ImGui::GetWindowDrawList();
|
||||
float rowW = ImGui::GetContentRegionAvail().x; // narrows automatically if a scrollbar shows
|
||||
if (vis.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||
material::DrawEmptyState(ICON_MD_PIE_CHART, 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.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "../../config/settings.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../util/address_validation.h" // isValidRecipientAddress — payout validation (M-01)
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -27,6 +28,7 @@
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdlib> // std::atoi (xmrig version compare)
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
@@ -55,6 +57,25 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
float miningBtnGap = gap;
|
||||
float miningBtnMaxW = availWidth * schema::UI().drawElement("tabs.mining", "btn-max-width-ratio").size;
|
||||
|
||||
// Thread-count tiles at an adaptive step (multiples of 1/2/4/8, chosen by core count) plus 1 and the
|
||||
// max, so the tile row stays bounded (~<=24 tiles) instead of one tile per thread — a 192-thread
|
||||
// EPYC would otherwise overflow the card. Exact in-between counts are settable via the input box.
|
||||
int tileStep = 1;
|
||||
if (max_threads > 96) tileStep = 8;
|
||||
else if (max_threads > 48) tileStep = 4;
|
||||
else if (max_threads > 24) tileStep = 2;
|
||||
std::vector<int> threadOptions;
|
||||
threadOptions.push_back(1);
|
||||
for (int v = tileStep; v < max_threads; v += tileStep)
|
||||
if (v > 1) threadOptions.push_back(v);
|
||||
if (max_threads > 1 && threadOptions.back() != max_threads) threadOptions.push_back(max_threads);
|
||||
const int nOpts = (int)threadOptions.size();
|
||||
|
||||
// Custom drawlist hit-tests (the thread tiles + the Mine button) bypass ImGui's popup input capture,
|
||||
// so a click on an open dropdown (saved pools / payout addresses) would ALSO fire them. Gate on this
|
||||
// so clicking the dropdown's X (or a row) doesn't bleed through to the tiles/Mine button.
|
||||
const bool anyPopupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
|
||||
|
||||
// --- Compute thread grid layout based on controls card width ---
|
||||
// Estimate controlsW first to compute cols correctly
|
||||
// The Mine button is square (= card height, which scales with DPI), so the width we
|
||||
@@ -62,10 +83,10 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
// 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 cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size * dp, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f) * dp);
|
||||
float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size);
|
||||
int cols = std::max(1, std::min(max_threads, (int)(innerW / (cellSz + cellGap))));
|
||||
int rows = (max_threads + cols - 1) / cols;
|
||||
int cols = std::max(1, std::min(nOpts, (int)(innerW / (cellSz + cellGap))));
|
||||
int rows = (nOpts + cols - 1) / cols;
|
||||
float gridW = cols * cellSz + (cols - 1) * cellGap;
|
||||
float gridH = rows * cellSz + (rows - 1) * cellGap;
|
||||
|
||||
@@ -115,6 +136,92 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// Centered, top-aligned thread-count stepper: [-] centered editable number [+]. Lets a
|
||||
// high-core CPU pick an EXACT count that isn't one of the tiles. The input commits on Enter
|
||||
// (not per keystroke) so it doesn't restart the miner while the user is still typing.
|
||||
{
|
||||
auto applyThreads = [&](int tc) {
|
||||
tc = std::clamp(tc, 1, max_threads);
|
||||
if (tc == s_selected_threads) return;
|
||||
s_selected_threads = tc;
|
||||
app->settings()->setPoolThreads(tc);
|
||||
app->settings()->save();
|
||||
if (mining.generate) app->startMining(tc);
|
||||
if (s_pool_mode && state.pool_mining.xmrig_running) {
|
||||
app->stopPoolMining();
|
||||
app->startPoolMining(tc);
|
||||
}
|
||||
};
|
||||
ImFont* stepFont = Type().iconSmall();
|
||||
// Match the -/+ button height to the InputInt's actual frame height (GetFrameHeight ==
|
||||
// fontSize + 2*FramePadding.y, evaluated with the same font/style the input renders with,
|
||||
// since nothing pushes a font between here and the InputInt below). This keeps the buttons
|
||||
// the SAME height as the number box and top-aligned with it at sy — previously fieldH was
|
||||
// capFont+6px, shorter than the box, so they sat misaligned.
|
||||
const float fieldH = ImGui::GetFrameHeight();
|
||||
const float sideW = fieldH; // square -/+ buttons
|
||||
const float fieldW = 46.0f * dp;
|
||||
const float g = 2.0f * dp;
|
||||
const float totalW = sideW + g + fieldW + g + sideW;
|
||||
float sx = cardMin.x + (controlsW - totalW) * 0.5f;
|
||||
const float sy = curY;
|
||||
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
||||
|
||||
// -/+ buttons drawn in the same style as the thread tiles (rounded rect + border + centered
|
||||
// glyph, same fill/border/rounding). Hit-tested with a real InvisibleButton so they stay
|
||||
// popup-safe.
|
||||
const float stepRound = schema::UI().drawElement("tabs.mining", "cell-rounding").size;
|
||||
// atBound: the stepper can't move further (already at 1 for "-" or max for "+"). When
|
||||
// at the bound the control is a no-op, so render it disabled: dim glyph/border, no hand
|
||||
// cursor, no tooltip — and swallow the click so it reads as inert.
|
||||
auto rectStepBtn = [&](const char* id, const char* glyph, float x, const char* tip, bool atBound) -> bool {
|
||||
ImGui::SetCursorScreenPos(ImVec2(x, sy));
|
||||
ImGui::InvisibleButton(id, ImVec2(sideW, fieldH));
|
||||
const bool hov = ImGui::IsItemHovered() && !atBound;
|
||||
const bool clk = ImGui::IsItemClicked() && !atBound;
|
||||
const ImVec2 mn(x, sy), mx(x + sideW, sy + fieldH);
|
||||
dl->AddRectFilled(mn, mx, hov ? WithAlpha(OnSurface(), 25) : WithAlpha(OnSurface(), 8), stepRound);
|
||||
dl->AddRect(mn, mx, WithAlpha(OnSurface(), atBound ? 15 : (hov ? 80 : 35)), stepRound);
|
||||
const ImVec2 gsz = stepFont->CalcTextSizeA(stepFont->LegacySize, FLT_MAX, 0, glyph);
|
||||
dl->AddText(stepFont, stepFont->LegacySize,
|
||||
ImVec2(x + (sideW - gsz.x) * 0.5f, sy + (fieldH - gsz.y) * 0.5f),
|
||||
WithAlpha(OnSurface(), atBound ? 30 : (hov ? 160 : 80)), glyph);
|
||||
if (hov) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (tip && tip[0]) material::Tooltip("%s", tip);
|
||||
}
|
||||
return clk;
|
||||
};
|
||||
|
||||
// "-" button (disabled at 1 thread)
|
||||
if (rectStepBtn("##ThreadMinus", ICON_MD_REMOVE, sx, TR("mining_threads_minus_tooltip"),
|
||||
s_selected_threads <= 1))
|
||||
applyThreads(s_selected_threads - 1);
|
||||
sx += sideW + g;
|
||||
|
||||
// Centered editable number: symmetric FramePadding pushes the digits to the field centre.
|
||||
char tb[8]; snprintf(tb, sizeof(tb), "%d", s_selected_threads);
|
||||
float basePad = ImGui::GetStyle().FramePadding.x;
|
||||
float txtW = ImGui::CalcTextSize(tb).x;
|
||||
float centerPad = std::max(basePad, (fieldW - txtW) * 0.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(centerPad, ImGui::GetStyle().FramePadding.y));
|
||||
ImGui::SetCursorScreenPos(ImVec2(sx, sy));
|
||||
ImGui::SetNextItemWidth(fieldW);
|
||||
int tc = s_selected_threads;
|
||||
if (ImGui::InputInt("##ThreadCountInput", &tc, 0, 0, ImGuiInputTextFlags_EnterReturnsTrue))
|
||||
applyThreads(tc);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("mining_threads_input_tooltip"));
|
||||
ImGui::PopStyleVar();
|
||||
sx += fieldW + g;
|
||||
|
||||
// "+" button (disabled at max_threads)
|
||||
if (rectStepBtn("##ThreadPlus", ICON_MD_ADD, sx, TR("mining_threads_plus_tooltip"),
|
||||
s_selected_threads >= max_threads))
|
||||
applyThreads(s_selected_threads + 1);
|
||||
|
||||
ImGui::SetCursorScreenPos(savedCur);
|
||||
}
|
||||
|
||||
// Idle mining toggle (top-right corner of card)
|
||||
float idleRightEdge = cardMax.x - pad;
|
||||
{
|
||||
@@ -191,7 +298,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
for (const auto& d : delays) {
|
||||
if (d.seconds == curDelay) { previewLabel = d.label; break; }
|
||||
}
|
||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f) * dp;
|
||||
float comboX = idleRightEdge - comboW;
|
||||
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||
@@ -230,7 +337,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
if (curVal <= 0) curVal = hwThreads;
|
||||
char previewBuf[16];
|
||||
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
|
||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f) * dp;
|
||||
float comboX = idleRightEdge - comboW;
|
||||
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||
@@ -269,7 +376,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
if (curVal <= 0) curVal = std::max(1, hwThreads / 2);
|
||||
char previewBuf[16];
|
||||
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
|
||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f) * dp;
|
||||
float comboX = idleRightEdge - comboW;
|
||||
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||
@@ -442,7 +549,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
s_benchConfirm = true;
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"Benchmark takes ~%ds and interrupts mining. Click again to start.",
|
||||
TR("grpc_benchmark_takes_secs"),
|
||||
(int)(s_benchmark.totalEstimatedSecs() + 0.5f));
|
||||
Notifications::instance().warning(msg);
|
||||
} else {
|
||||
@@ -474,8 +581,39 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
std::string curVer = s_live_miner_ver;
|
||||
if (curVer.empty()) curVer = app->poolMiningInstalledVersion();
|
||||
if (curVer.empty()) curVer = app->settings()->getXmrigVersion();
|
||||
// Version state: subtle GREEN + "xmrig releases" when up to date, subtle ORANGE +
|
||||
// "Update <latest>" when an update is available, neutral when either version is unknown.
|
||||
auto verNum = [](std::string s) -> std::vector<int> {
|
||||
if (!s.empty() && (s[0] == 'v' || s[0] == 'V')) s.erase(0, 1);
|
||||
s = s.substr(0, s.find('-')); // drop any -build suffix
|
||||
std::vector<int> v; size_t p = 0;
|
||||
while (p <= s.size()) {
|
||||
size_t q = s.find('.', p);
|
||||
v.push_back(std::atoi(s.substr(p, q == std::string::npos ? std::string::npos : q - p).c_str()));
|
||||
if (q == std::string::npos) break;
|
||||
p = q + 1;
|
||||
}
|
||||
return v;
|
||||
};
|
||||
const bool verKnown = !curVer.empty() && !s_xmrig_latest_tag.empty();
|
||||
bool upToDate = false;
|
||||
if (verKnown) {
|
||||
auto a = verNum(curVer), b = verNum(s_xmrig_latest_tag);
|
||||
int cmp = 0;
|
||||
for (size_t i = 0; i < std::max(a.size(), b.size()) && cmp == 0; ++i) {
|
||||
int x = i < a.size() ? a[i] : 0, y = i < b.size() ? b[i] : 0;
|
||||
if (x != y) cmp = x < y ? -1 : 1;
|
||||
}
|
||||
upToDate = (cmp >= 0); // installed >= latest
|
||||
}
|
||||
const bool outdated = verKnown && !upToDate;
|
||||
const ImU32 subtleGreen = IM_COL32(120, 190, 130, 255);
|
||||
const ImU32 subtleOrange = IM_COL32(214, 158, 74, 255);
|
||||
|
||||
char xbtn[64];
|
||||
if (!s_xmrig_latest_tag.empty())
|
||||
if (upToDate)
|
||||
snprintf(xbtn, sizeof(xbtn), "%s", TR("xmrig_releases"));
|
||||
else if (!s_xmrig_latest_tag.empty())
|
||||
snprintf(xbtn, sizeof(xbtn), "%s %s", TR("xmrig_update_short"), s_xmrig_latest_tag.c_str());
|
||||
else
|
||||
snprintf(xbtn, sizeof(xbtn), "%s", TR("xmrig_update_button"));
|
||||
@@ -498,7 +636,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
xpill, xbtnH * 0.3f);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(xbtnX + xpadX, xbtnY + (xbtnH - xlblSz.y) * 0.5f),
|
||||
minerBusy ? OnSurfaceDisabled() : OnSurfaceMedium(), xbtn);
|
||||
minerBusy ? OnSurfaceDisabled()
|
||||
: (upToDate ? subtleGreen : outdated ? subtleOrange : OnSurfaceMedium()),
|
||||
xbtn);
|
||||
if (xhov) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
material::Tooltip("%s", minerBusy ? TR("xmrig_stop_mining_first")
|
||||
@@ -514,7 +654,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
float xcurX = xbtnX - 6.0f * dp - xcurSz.x;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(xcurX, curY + (headerH - xcurSz.y) * 0.5f),
|
||||
OnSurfaceDisabled(), xcur);
|
||||
upToDate ? subtleGreen : outdated ? subtleOrange : OnSurfaceDisabled(), xcur);
|
||||
idleRightEdge = xcurX - 8.0f * dp;
|
||||
}
|
||||
|
||||
@@ -547,14 +687,14 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
// Track which thread the mouse is currently over (-1 = none)
|
||||
int hovered_thread = -1;
|
||||
|
||||
// First pass: hit-test all cells to find hovered thread
|
||||
for (int i = 0; i < max_threads; i++) {
|
||||
// First pass: hit-test all cells to find the hovered thread-count option (its value, not index)
|
||||
for (int i = 0; i < nOpts; i++) {
|
||||
int row = i / cols;
|
||||
int col = i % cols;
|
||||
float cx = gridX + col * (cellSz + cellGap);
|
||||
float cy = gridY + row * (cellSz + cellGap);
|
||||
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + cellSz, cy + cellSz))) {
|
||||
hovered_thread = i + 1;
|
||||
hovered_thread = threadOptions[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -565,8 +705,8 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
if (hovered_thread > 0 && !benchActive)
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
|
||||
// Drag-to-select logic (disabled during benchmark)
|
||||
if (!benchActive && ImGui::IsMouseClicked(0) && hovered_thread > 0) {
|
||||
// Drag-to-select logic (disabled during benchmark; ignored while a dropdown popup is open)
|
||||
if (!benchActive && !anyPopupOpen && ImGui::IsMouseClicked(0) && hovered_thread > 0) {
|
||||
// Begin drag
|
||||
s_drag_active = true;
|
||||
s_drag_anchor_thread = hovered_thread;
|
||||
@@ -586,8 +726,8 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
}
|
||||
}
|
||||
|
||||
// Render cells
|
||||
for (int i = 0; i < max_threads; i++) {
|
||||
// Render cells (one per discrete thread-count option)
|
||||
for (int i = 0; i < nOpts; i++) {
|
||||
int row = i / cols;
|
||||
int col = i % cols;
|
||||
float cx = gridX + col * (cellSz + cellGap);
|
||||
@@ -595,8 +735,8 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
ImVec2 cMin(cx, cy);
|
||||
ImVec2 cMax(cx + cellSz, cy + cellSz);
|
||||
|
||||
int threadNum = i + 1;
|
||||
bool active = threadNum <= s_selected_threads;
|
||||
int threadNum = threadOptions[i];
|
||||
bool active = threadNum <= s_selected_threads; // fill-up-to-selected heat metaphor
|
||||
bool hovered = (threadNum == hovered_thread);
|
||||
|
||||
// Determine visual state
|
||||
@@ -623,15 +763,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
dl->AddRect(cMin, cMax, WithAlpha(Primary(), (int)(160 + 60 * glow)), rounding, 0, schema::UI().drawElement("tabs.mining", "active-cell-border-thickness").size);
|
||||
} else if (active) {
|
||||
// Active but not mining: solid primary fill
|
||||
ImU32 pri = Primary();
|
||||
int priR = (pri >> 0) & 0xFF;
|
||||
int priG = (pri >> 8) & 0xFF;
|
||||
int priB = (pri >> 16) & 0xFF;
|
||||
ImU32 fillCol = hovered
|
||||
? IM_COL32(priR, priG, priB, 220)
|
||||
: IM_COL32(priR, priG, priB, 180);
|
||||
ImU32 fillCol = material::WithAlpha(Primary(), hovered ? 220 : 180);
|
||||
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
|
||||
dl->AddRect(cMin, cMax, IM_COL32(priR, priG, priB, 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size);
|
||||
dl->AddRect(cMin, cMax, material::WithAlpha(Primary(), 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size);
|
||||
} else {
|
||||
// Inactive: dim outline
|
||||
ImU32 fillCol = hovered
|
||||
@@ -673,7 +807,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
ImVec2 bMin(btnX, btnY);
|
||||
ImVec2 bMax(btnX + miningBtnSz, btnY + cardH);
|
||||
|
||||
bool btnHovered = material::IsRectHovered(bMin, bMax);
|
||||
bool btnHovered = !anyPopupOpen && material::IsRectHovered(bMin, bMax); // don't bleed through an open dropdown
|
||||
bool btnClicked = btnHovered && ImGui::IsMouseClicked(0);
|
||||
bool isSyncing = state.sync.syncing;
|
||||
bool poolBlockedBySolo = s_pool_mode && mining.generate && !state.pool_mining.xmrig_running;
|
||||
@@ -685,8 +819,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running;
|
||||
// Can't start pool mining without a payout address (blank for a new wallet with no z-address);
|
||||
// only blocks starting — stopping a running miner stays enabled.
|
||||
// Block start when the payout address is empty OR not a valid DragonX address — mining to a
|
||||
// malformed / wrong-chain address silently loses the rewards. (M-01)
|
||||
const std::string poolPayoutStr(s_pool_worker);
|
||||
bool poolNeedsPayout = s_pool_mode && !state.pool_mining.xmrig_running &&
|
||||
std::string(s_pool_worker).empty();
|
||||
(poolPayoutStr.empty() ||
|
||||
(poolPayoutStr != "x" && !util::isValidRecipientAddress(poolPayoutStr)));
|
||||
bool disabled = s_pool_mode
|
||||
? (isToggling || poolBlockedBySolo || poolNeedsPayout)
|
||||
: (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing));
|
||||
@@ -819,7 +957,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
else if (poolBlockedBySolo)
|
||||
material::Tooltip("%s", TR("mining_stop_solo_for_pool"));
|
||||
else if (poolNeedsPayout)
|
||||
material::Tooltip("%s", "Enter a payout address first (generate a Z address)");
|
||||
material::Tooltip("%s", TR("mining_pool_needs_payout_tooltip"));
|
||||
else
|
||||
material::Tooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining"));
|
||||
}
|
||||
|
||||
@@ -104,7 +104,10 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
}
|
||||
}
|
||||
|
||||
// Use pool hashrate for EST. DAILY when in pool mode
|
||||
// Est. Daily = expected reward for YOUR own hashrate share of the network. In pool mode this uses
|
||||
// your local miner rate (pool_mining.hashrate_10s), NOT the pool's aggregate rate: pool payouts are
|
||||
// share-proportional, so your expected daily is the same solo-equivalent value (a rough estimate,
|
||||
// before the pool fee). Using the pool's total rate here would show the POOL's earnings, not yours. (M-05)
|
||||
double estHashrate = s_pool_mode ? state.pool_mining.hashrate_10s : mining.localHashrate;
|
||||
double est_hours_2 = EstimateHoursToBlock(estHashrate, mining.networkHashrate, mining.difficulty);
|
||||
double estDailyBlocks = (est_hours_2 > 0) ? (24.0 / est_hours_2) : 0.0;
|
||||
@@ -217,13 +220,16 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
if (estActive)
|
||||
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
|
||||
else
|
||||
snprintf(estVal, sizeof(estVal), "N/A");
|
||||
snprintf(estVal, sizeof(estVal), "%s", TR("grpc_na"));
|
||||
// Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the
|
||||
// number isn't silently mismatched to its plain "Est. Daily" label. (M-05)
|
||||
const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
|
||||
|
||||
EarningsEntry entries[] = {
|
||||
{ TR("mining_today"), todayVal, todaySub, greenCol2 },
|
||||
{ TR("mining_yesterday"), yesterdayVal, yesterdaySub, OnSurface() },
|
||||
{ TR("mining_all_time"), allVal, allSub, OnSurface() },
|
||||
{ TR("mining_est_daily"), estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() },
|
||||
{ TR("mining_est_daily"), estVal, estSub, estActive ? greenCol2 : OnSurfaceDisabled() },
|
||||
};
|
||||
|
||||
for (int ei = 0; ei < numCols; ei++) {
|
||||
@@ -469,7 +475,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
else if (totalRAM > 0)
|
||||
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
|
||||
else
|
||||
snprintf(sysBuf, sizeof(sysBuf), "N/A");
|
||||
snprintf(sysBuf, sizeof(sysBuf), "%s", TR("grpc_na"));
|
||||
|
||||
float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
|
||||
float sysTextX = barX + barW - textPadX - sysTextW;
|
||||
@@ -527,7 +533,18 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
float recentAvailH = ImGui::GetContentRegionAvail().y - sHdr - gapOver;
|
||||
float minRows = recentMined.empty() ? 2.0f : (float)recentMined.size();
|
||||
float contentH_blocks = rowH_blocks * minRows + pad * 2.5f;
|
||||
float recentH = std::clamp(contentH_blocks, 30.0f * dp, std::max(30.0f * dp, recentAvailH));
|
||||
// Lower bound for the panel height. When the list is empty the centred empty-state
|
||||
// (top pad + icon + gap + caption + bottom pad) needs more vertical room than the bare
|
||||
// 30*dp floor, otherwise the caption is clipped once the thread-tile grid wraps to two
|
||||
// rows and recentAvailH shrinks (font metrics/Layout helpers are already DPI-scaled — do
|
||||
// not multiply them by dp; pad is a scaled param).
|
||||
float recentMinH = 30.0f * dp;
|
||||
if (recentMined.empty()) {
|
||||
recentMinH = std::max(recentMinH,
|
||||
pad * 0.5f + Type().iconMed()->LegacySize + Layout::spacingXs()
|
||||
+ capFont->LegacySize + pad * 0.5f);
|
||||
}
|
||||
float recentH = std::clamp(contentH_blocks, recentMinH, std::max(recentMinH, recentAvailH));
|
||||
|
||||
// Glass panel wrapping the list + scroll-edge mask state
|
||||
ImVec2 recentPanelMin = ImGui::GetCursorScreenPos();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "mining_mode_toggle.h"
|
||||
#include "mining_tab_helpers.h"
|
||||
#include "mining_tab.h" // CancelMiningBenchmark (L-11)
|
||||
#include "mining_pool_panel.h"
|
||||
|
||||
#include "../../app.h"
|
||||
@@ -16,6 +17,7 @@
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../../util/address_validation.h" // isValidRecipientAddress — payout validation (M-01)
|
||||
#include "../layout.h"
|
||||
#include "../notifications.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
@@ -40,10 +42,24 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
bool& s_pool_mode, char (&s_pool_url)[256], char (&s_pool_worker)[256],
|
||||
bool& s_pool_settings_dirty)
|
||||
{
|
||||
// Arm/confirm for the destructive X (delete) on saved pools & workers: a first click on a
|
||||
// row's X arms it (the X turns red + relabels via the trash glyph) and a second click within
|
||||
// a short window actually removes it; moving to another row or letting the window lapse disarms.
|
||||
// Same idiom as contacts_tab.cpp's s_confirm_delete_idx, keyed by row index within each list.
|
||||
static int s_armed_pool_idx = -1; // armed saved-pool row (-1 = none)
|
||||
static int s_armed_worker_idx = -1; // armed saved-worker row (-1 = none)
|
||||
static double s_armed_pool_time = 0.0;
|
||||
static double s_armed_worker_time = 0.0;
|
||||
const double kArmWindowSecs = 3.0; // second click must land within this window
|
||||
const double nowTime = ImGui::GetTime();
|
||||
// Lapse the arm if the confirm window elapsed.
|
||||
if (s_armed_pool_idx >= 0 && nowTime - s_armed_pool_time > kArmWindowSecs) s_armed_pool_idx = -1;
|
||||
if (s_armed_worker_idx >= 0 && nowTime - s_armed_worker_time > kArmWindowSecs) s_armed_worker_idx = -1;
|
||||
|
||||
const bool soloMiningAvailable = app->supportsSoloMining();
|
||||
float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs;
|
||||
float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size;
|
||||
float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size;
|
||||
float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size * hs;
|
||||
float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size * hs;
|
||||
float totalW = soloMiningAvailable ? (toggleW * 2) : toggleW;
|
||||
|
||||
ImVec2 tMin = ImGui::GetCursorScreenPos();
|
||||
@@ -101,6 +117,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
s_pool_mode = false;
|
||||
app->settings()->setPoolMode(false);
|
||||
app->settings()->save();
|
||||
CancelMiningBenchmark(app); // don't leave a pool benchmark running after switching to solo (L-11)
|
||||
app->stopPoolMining();
|
||||
}
|
||||
if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
@@ -172,8 +189,11 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
float perGroupExtra = iconBtnW * 2; // dropdown + bookmark
|
||||
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW
|
||||
- Layout::spacingSm() - perGroupExtra * 2;
|
||||
float urlW = std::max(60.0f, remainW * 0.30f);
|
||||
float wrkW = std::max(40.0f, remainW * 0.70f);
|
||||
// Floor keeps the inputs usable when cramped; the ceiling stops a
|
||||
// single input sprawling absurdly wide on a large window (leftover
|
||||
// space becomes right-side margin). Caps are logical px * dp.
|
||||
float urlW = std::min(std::max(60.0f, remainW * 0.30f), 420.0f * dp);
|
||||
float wrkW = std::min(std::max(40.0f, remainW * 0.70f), 560.0f * dp);
|
||||
|
||||
// Track positions for popup alignment
|
||||
float urlGroupStartX = ImGui::GetCursorScreenPos().x;
|
||||
@@ -293,9 +313,11 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
ImFont* rowFont = ImGui::GetFont();
|
||||
float rowFontSz = ImGui::GetFontSize();
|
||||
float rowH = ImGui::GetFrameHeight();
|
||||
int rowIdx = 0;
|
||||
for (const auto& url : savedUrls) {
|
||||
ImGui::PushID(url.c_str());
|
||||
bool isCurrent = (std::string(s_pool_url) == url);
|
||||
bool armed = (s_armed_pool_idx == rowIdx); // this row's X is arm-confirmed
|
||||
ImVec2 rowMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH);
|
||||
ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH));
|
||||
@@ -316,40 +338,46 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
pdl->AddText(rowFont, rowFontSz,
|
||||
ImVec2(rowMin.x + textPadX, textY),
|
||||
isCurrent ? Primary() : OnSurface(), urlDisp.c_str());
|
||||
// X button — flush with right edge, icon centered
|
||||
// X (delete) button — flush with right edge, icon centered. First click on the X
|
||||
// arms it (filled trash glyph + readable-error tint); a second click within the
|
||||
// arm window actually removes. Armed row stays lit even when not hovered so the
|
||||
// pending-delete state is visible.
|
||||
{
|
||||
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
|
||||
ImVec2 xMax(rowMax.x, rowMax.y);
|
||||
if (inXZone) {
|
||||
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
material::Tooltip("%s", TR("mining_remove"));
|
||||
} else if (rowHov) {
|
||||
// Show faint X when row is hovered
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
OnSurfaceDisabled(), xIcon);
|
||||
material::Tooltip("%s", armed ? TR("address_book_confirm_delete")
|
||||
: TR("mining_remove"));
|
||||
}
|
||||
// Always draw icon when hovering X zone
|
||||
if (inXZone) {
|
||||
if (armed || inXZone || rowHov) {
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
// Filled trash while armed reinforces the recolor; otherwise a plain X.
|
||||
const char* xIcon = armed ? ICON_MD_DELETE : ICON_MD_CLOSE;
|
||||
ImU32 xCol = armed ? ReadableError()
|
||||
: (inXZone ? Error() : OnSurfaceDisabled());
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
Error(), xIcon);
|
||||
xCol, xIcon);
|
||||
}
|
||||
}
|
||||
// Hovering a *different* row's X disarms the previously armed one.
|
||||
if (inXZone && !armed && s_armed_pool_idx >= 0)
|
||||
s_armed_pool_idx = -1;
|
||||
// Click handling
|
||||
if (rowClk) {
|
||||
if (inXZone) {
|
||||
urlToRemove = url;
|
||||
if (armed) {
|
||||
urlToRemove = url; // 2nd click on the armed row removes
|
||||
} else {
|
||||
s_armed_pool_idx = rowIdx; // 1st click arms this row
|
||||
s_armed_pool_time = nowTime;
|
||||
}
|
||||
} else {
|
||||
s_armed_pool_idx = -1; // selecting a row disarms
|
||||
strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1);
|
||||
s_pool_url[sizeof(s_pool_url) - 1] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
@@ -359,10 +387,12 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
if (rowHov && !inXZone)
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
rowIdx++;
|
||||
}
|
||||
if (!urlToRemove.empty()) {
|
||||
app->settings()->removeSavedPoolUrl(urlToRemove);
|
||||
app->settings()->save();
|
||||
s_armed_pool_idx = -1; // removal shifts indices — drop the arm
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
@@ -382,6 +412,8 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
std::string currentWorkerStr(s_pool_worker);
|
||||
if (currentWorkerStr.empty()) {
|
||||
material::Tooltip("%s", TR("mining_generate_z_address_hint"));
|
||||
} else if (currentWorkerStr != "x" && !util::isValidRecipientAddress(currentWorkerStr)) {
|
||||
material::Tooltip("%s", TR("mining_payout_invalid")); // block start below too (M-01)
|
||||
} else {
|
||||
material::Tooltip("%s", TR("mining_payout_tooltip"));
|
||||
}
|
||||
@@ -422,7 +454,9 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
// --- Worker: Popup positioned below the input group ---
|
||||
// Popup sized to fit full z-addresses without truncation;
|
||||
// zero horizontal padding so item highlights are flush with edges.
|
||||
float addrPopupW = std::max(wrkGroupW, availWidth * 0.55f);
|
||||
// Wide enough for full z-addresses, but ceiling it so it doesn't span
|
||||
// the whole window on a large display (leftover -> unused margin).
|
||||
float addrPopupW = std::min(std::max(wrkGroupW, availWidth * 0.55f), 640.0f * dp);
|
||||
ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2));
|
||||
ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
|
||||
@@ -455,9 +489,11 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
ImFont* wRowFont = ImGui::GetFont();
|
||||
float wRowFontSz = ImGui::GetFontSize();
|
||||
float wRowH = ImGui::GetFrameHeight();
|
||||
int wRowIdx = 0;
|
||||
for (const auto& addr : savedWorkers) {
|
||||
ImGui::PushID(addr.c_str());
|
||||
bool isCurrent = (std::string(s_pool_worker) == addr);
|
||||
bool armed = (s_armed_worker_idx == wRowIdx); // this row's X is arm-confirmed
|
||||
ImVec2 rowMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH);
|
||||
ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH));
|
||||
@@ -481,38 +517,46 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
// Tooltip for long addresses
|
||||
if (rowHov && !inXZone)
|
||||
material::Tooltip("%s", addr.c_str());
|
||||
// X button — flush with right edge, icon centered
|
||||
// X (delete) button — flush with right edge, icon centered. First click on the X
|
||||
// arms it (filled trash glyph + readable-error tint); a second click within the
|
||||
// arm window actually removes. Armed row stays lit even when not hovered so the
|
||||
// pending-delete state is visible.
|
||||
{
|
||||
ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y);
|
||||
ImVec2 xMax(rowMax.x, rowMax.y);
|
||||
if (inXZone) {
|
||||
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
material::Tooltip("%s", TR("mining_remove"));
|
||||
} else if (rowHov) {
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
OnSurfaceDisabled(), xIcon);
|
||||
material::Tooltip("%s", armed ? TR("address_book_confirm_delete")
|
||||
: TR("mining_remove"));
|
||||
}
|
||||
if (inXZone) {
|
||||
if (armed || inXZone || rowHov) {
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
// Filled trash while armed reinforces the recolor; otherwise a plain X.
|
||||
const char* xIcon = armed ? ICON_MD_DELETE : ICON_MD_CLOSE;
|
||||
ImU32 xCol = armed ? ReadableError()
|
||||
: (inXZone ? Error() : OnSurfaceDisabled());
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
Error(), xIcon);
|
||||
xCol, xIcon);
|
||||
}
|
||||
}
|
||||
// Hovering a *different* row's X disarms the previously armed one.
|
||||
if (inXZone && !armed && s_armed_worker_idx >= 0)
|
||||
s_armed_worker_idx = -1;
|
||||
// Click handling
|
||||
if (rowClk) {
|
||||
if (inXZone) {
|
||||
addrToRemove = addr;
|
||||
if (armed) {
|
||||
addrToRemove = addr; // 2nd click on the armed row removes
|
||||
} else {
|
||||
s_armed_worker_idx = wRowIdx; // 1st click arms this row
|
||||
s_armed_worker_time = nowTime;
|
||||
}
|
||||
} else {
|
||||
s_armed_worker_idx = -1; // selecting a row disarms
|
||||
strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1);
|
||||
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
@@ -522,10 +566,12 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
if (rowHov && !inXZone)
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
wRowIdx++;
|
||||
}
|
||||
if (!addrToRemove.empty()) {
|
||||
app->settings()->removeSavedPoolWorker(addrToRemove);
|
||||
app->settings()->save();
|
||||
s_armed_worker_idx = -1; // removal shifts indices — drop the arm
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
|
||||
@@ -312,11 +312,11 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
isCurrent ? Success() : OnSurfaceDisabled());
|
||||
|
||||
const float textY = rMin.y + (listRowH - capFont->LegacySize) * 0.5f;
|
||||
// Show the short host label (the narrow card can't fit host:port); the
|
||||
// full stratum URL is in the hover tooltip.
|
||||
cdl->AddText(capFont, capFont->LegacySize, ImVec2(rMin.x + 16 * dp, textY),
|
||||
isCurrent ? Primary() : OnSurface(), kp.label.c_str());
|
||||
|
||||
// Build the right-side "<hashrate> N% fee" run first so we know its width
|
||||
// and can bound (and ellipsis-truncate) the left host label to avoid a
|
||||
// collision — both text runs grow ~1.5x at font_scale 1.5 while the card
|
||||
// width barely does.
|
||||
char right[64];
|
||||
std::string hrStr = haveHr ? FormatHashrate(it->second.hashrateHs) : std::string("—");
|
||||
// Prefer the live fee the pool reports; fall back to the compile-time
|
||||
@@ -326,11 +326,21 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
? it->second.feePercent
|
||||
: kp.feePercent;
|
||||
if (feePct >= 0.0)
|
||||
snprintf(right, sizeof(right), "%s %s%% fee", hrStr.c_str(),
|
||||
snprintf(right, sizeof(right), TR("grpc_hashrate_fee"), hrStr.c_str(),
|
||||
FormatFeePercent(feePct).c_str());
|
||||
else
|
||||
snprintf(right, sizeof(right), "%s", hrStr.c_str());
|
||||
ImVec2 rSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, right);
|
||||
|
||||
// Show the short host label (the narrow card can't fit host:port); the
|
||||
// full stratum URL is in the hover tooltip. Truncate it to the gap left
|
||||
// of the right-side run so a long hostname can't overlap the hashrate.
|
||||
const float labelX = rMin.x + 16 * dp;
|
||||
const float labelMaxW = (rMax.x - rSz.x - 6 * dp - gap) - labelX;
|
||||
std::string label = TruncateToWidth(kp.label, capFont, capFont->LegacySize, labelMaxW);
|
||||
cdl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, textY),
|
||||
isCurrent ? Primary() : OnSurface(), label.c_str());
|
||||
|
||||
cdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rMax.x - rSz.x - 6 * dp, textY), OnSurfaceMedium(), right);
|
||||
|
||||
@@ -389,7 +399,9 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
// A bit wider than the old 25%/180dp so the pool rows fit the "<hashrate> N% fee" text.
|
||||
float leftW = std::clamp(availWidth * 0.30f, 210.0f * dp, availWidth * 0.45f);
|
||||
// Ratio ceiling (0.45) never binds on a wide window, so give leftW an
|
||||
// absolute cap too — the pool card doesn't need to be enormous.
|
||||
float leftW = std::clamp(availWidth * 0.30f, 210.0f * dp, 440.0f * dp);
|
||||
float colGap = gap;
|
||||
float rightW = availWidth - leftW - colGap;
|
||||
|
||||
@@ -404,6 +416,16 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
||||
RenderLeftPoolCard(app, state, dl, capFont, sub1, ovFont, dp, gap, pad,
|
||||
leftMin, leftMax, s_pool_url, s_pool_settings_dirty);
|
||||
|
||||
// rightW is uncapped, so the chart/hint band it draws would sprawl the
|
||||
// full panel on a wide window. Constrain just the drawn content to a
|
||||
// centered ~1000dp band inside the panel's padded content region; the
|
||||
// glass panel itself still fills rightW, leftover -> side margin.
|
||||
const float rightContentW = rightW - pad * 2.0f;
|
||||
// Fill the panel's content width so the chart extends across the whole panel on a wide window.
|
||||
// (Previously capped at ~1000dp and centered, which left large empty margins either side.)
|
||||
const float chartBandW = rightContentW;
|
||||
const float chartBandX = rightMin.x + pad;
|
||||
|
||||
// Right panel: live log (if toggled + available) else the sparkline.
|
||||
if (showLogView) {
|
||||
float logPad = pad * 0.5f;
|
||||
@@ -412,14 +434,14 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
||||
state.pool_mining.log_lines, "##PoolLogText");
|
||||
} else if (hasChartContent) {
|
||||
DrawHashrateSparkline(dl,
|
||||
ImVec2(rightMin.x + pad, rightMin.y + statRowH * 0.5f),
|
||||
ImVec2(rightMax.x - pad, rightMax.y),
|
||||
ImVec2(chartBandX, rightMin.y + statRowH * 0.5f),
|
||||
ImVec2(chartBandX + chartBandW, rightMax.y),
|
||||
chartHistory, capFont, dp);
|
||||
} else {
|
||||
const char* hint = TR("mining_chart_start");
|
||||
ImVec2 hs = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, hint);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2((rightMin.x + rightMax.x - hs.x) * 0.5f,
|
||||
ImVec2(chartBandX + (chartBandW - hs.x) * 0.5f,
|
||||
(rightMin.y + rightMax.y - hs.y) * 0.5f),
|
||||
OnSurfaceDisabled(), hint);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,19 @@ bool IsMiningBenchmarkActive() {
|
||||
return s_benchmark.active();
|
||||
}
|
||||
|
||||
void CancelMiningBenchmark(App* app) {
|
||||
if (!s_benchmark.active()) return;
|
||||
const int restoreThreads = s_benchmark.prev_threads;
|
||||
s_benchmark.reset();
|
||||
// Restore the miner to its pre-benchmark thread count. A benchmark runs in pool mode, so restore
|
||||
// regardless of the instantaneous running state — the sweep may be mid inter-candidate stop, where an
|
||||
// isPoolMinerRunning() check would be transiently false and silently drop the restart. (L-04, L-11)
|
||||
if (app && restoreThreads > 0) {
|
||||
app->stopPoolMining();
|
||||
app->startPoolMining(restoreThreads);
|
||||
}
|
||||
}
|
||||
|
||||
// Miner-update version check (one shot per session): fetches the latest DRG-XMRig release tag in
|
||||
// the background so the "Update" button can show it. Network call to the project Gitea, started
|
||||
// the first time the pool section is shown.
|
||||
@@ -250,8 +263,7 @@ static void RenderMiningTabContent(App* app)
|
||||
}
|
||||
if (benchmarkUpdate.inconclusive) {
|
||||
Notifications::instance().warning(
|
||||
"Benchmark inconclusive: no hashrate samples were recorded. "
|
||||
"Check the pool connection and try again.");
|
||||
TR("grpc_benchmark_inconclusive"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,5 +21,12 @@ void RenderMiningTab(App* app);
|
||||
*/
|
||||
bool IsMiningBenchmarkActive();
|
||||
|
||||
/**
|
||||
* @brief Cancel a running thread benchmark and restore the miner to its pre-benchmark thread count.
|
||||
* Safe to call when no benchmark is active (no-op). Used when leaving the Mining tab or switching to
|
||||
* solo mode so the miner isn't left stuck at a benchmark step. (L-04, L-11)
|
||||
*/
|
||||
void CancelMiningBenchmark(App* app);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -60,7 +60,7 @@ double EstimateHoursToBlock(double localHashrate, double networkHashrate, double
|
||||
{
|
||||
(void)difficulty;
|
||||
if (localHashrate <= 0.0 || networkHashrate <= 0.0) return 0.0;
|
||||
double blocksPerHour = 3600.0 / 75.0;
|
||||
double blocksPerHour = 3600.0 / 150.0; // DragonX mainnet target spacing is 150s (chainparams) (L-05)
|
||||
double share = localHashrate / networkHashrate;
|
||||
if (share <= 0.0) return 0.0;
|
||||
return 1.0 / (blocksPerHour * share);
|
||||
|
||||
@@ -93,9 +93,12 @@ void RenderLiteNetworkTab(App* app)
|
||||
const bool connected = ws.connected;
|
||||
ImDrawList* sdl = ImGui::GetWindowDrawList();
|
||||
const float panelH = 56.0f * dp;
|
||||
const float panelW = ImGui::GetContentRegionAvail().x;
|
||||
const float availPanelW = ImGui::GetContentRegionAvail().x;
|
||||
const float panelW = std::min(1000.0f * dp, availPanelW);
|
||||
const float panelOffsetX = std::max(0.0f, (availPanelW - panelW) * 0.5f);
|
||||
const float pad2 = 12.0f * dp;
|
||||
ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||
pMin.x += panelOffsetX;
|
||||
ImVec2 pMax(pMin.x + panelW, pMin.y + panelH);
|
||||
GlassPanelSpec sspec; sspec.rounding = 8.0f * dp;
|
||||
DrawGlassPanel(sdl, pMin, pMax, sspec);
|
||||
@@ -150,7 +153,7 @@ void RenderLiteNetworkTab(App* app)
|
||||
Primary(), barH * 0.5f);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(panelW, panelH)); // reserve the panel (proper boundary growth)
|
||||
ImGui::Dummy(ImVec2(availPanelW, panelH)); // reserve the full row (proper boundary growth)
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
@@ -174,8 +177,8 @@ void RenderLiteNetworkTab(App* app)
|
||||
{
|
||||
const float availW = ImGui::GetContentRegionAvail().x;
|
||||
const float addBtnW = 80.0f * dp;
|
||||
const float urlW = (availW - addBtnW - 16.0f * dp) * 0.6f;
|
||||
const float lblW = (availW - addBtnW - 16.0f * dp) * 0.4f;
|
||||
const float urlW = std::min(550.0f * dp, (availW - addBtnW - 16.0f * dp) * 0.6f);
|
||||
const float lblW = std::min(300.0f * dp, (availW - addBtnW - 16.0f * dp) * 0.4f);
|
||||
ImGui::SetNextItemWidth(urlW);
|
||||
ImGui::InputTextWithHint("##LiteAddUrl", TR("lite_net_add_url_hint"), s_addUrl, sizeof(s_addUrl));
|
||||
ImGui::SameLine();
|
||||
@@ -236,8 +239,11 @@ void RenderLiteNetworkTab(App* app)
|
||||
auto it = probe.find(sv.url);
|
||||
if (it != probe.end()) pr = it->second;
|
||||
|
||||
const float cardW = ImGui::GetContentRegionAvail().x;
|
||||
const float availCardW = ImGui::GetContentRegionAvail().x;
|
||||
const float cardW = std::min(1000.0f * dp, availCardW);
|
||||
const float cardOffsetX = std::max(0.0f, (availCardW - cardW) * 0.5f);
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
cardMin.x += cardOffsetX;
|
||||
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
||||
const float rnd = 8.0f * dp;
|
||||
const float cardBtnW = cardW - hideW;
|
||||
@@ -326,8 +332,10 @@ void RenderLiteNetworkTab(App* app)
|
||||
|
||||
// Advance to the next card via a real item (Dummy) below the card, so the scroll region's
|
||||
// content height actually grows — ImGui won't extend bounds from a bare SetCursorScreenPos.
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(cardW, gap));
|
||||
// Reset X to the (unshifted) region origin so next card's GetContentRegionAvail() isn't
|
||||
// cumulatively narrowed by this card's centering offset.
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x - cardOffsetX, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(availCardW, gap));
|
||||
};
|
||||
|
||||
// ── Visible servers ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -380,7 +380,7 @@ void RenderPeersTab(App* app)
|
||||
if (tlsCount == totalPeers) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4, valY), Success(), ICON_MD_CHECK);
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4 * dp, valY), Success(), ICON_MD_CHECK);
|
||||
}
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
@@ -621,12 +621,11 @@ void RenderPeersTab(App* app)
|
||||
if (!s_show_banned) {
|
||||
// ---- Connected Peers ----
|
||||
if (!app->isConnected()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
|
||||
material::DrawEmptyState(ICON_MD_CLOUD_OFF, TR("not_connected"));
|
||||
} else if (state.peers.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_connected"));
|
||||
material::DrawEmptyState(ICON_MD_WIFI_TETHERING, TR("peers_no_connected"));
|
||||
} else {
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg();
|
||||
float rowInset = Layout::spacingLg();
|
||||
float innerW = ImGui::GetContentRegionAvail().x - rowInset * 2;
|
||||
@@ -643,13 +642,13 @@ void RenderPeersTab(App* app)
|
||||
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
||||
|
||||
if (is_selected) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "row-selection-rounding").size);
|
||||
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size, rowEnd.y), Primary(), S.drawElement("tabs.peers", "row-accent-rounding").size);
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "row-selection-rounding").size * dp);
|
||||
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size * dp, rowEnd.y), Primary(), S.drawElement("tabs.peers", "row-accent-rounding").size * dp);
|
||||
}
|
||||
|
||||
bool hovered = material::IsRectHovered(rowPos, rowEnd);
|
||||
if (hovered && !is_selected) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "row-selection-rounding").size);
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "row-selection-rounding").size * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
}
|
||||
|
||||
@@ -662,17 +661,29 @@ void RenderPeersTab(App* app)
|
||||
else if (ping_ms < 500) dotCol = Warning();
|
||||
else dotCol = Error();
|
||||
float pingDotR = S.drawElement("tabs.peers", "ping-dot-radius-base").size + S.drawElement("tabs.peers", "ping-dot-radius-scale").size * hs;
|
||||
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);
|
||||
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size * dp, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);
|
||||
|
||||
float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size;
|
||||
float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size * dp;
|
||||
// Reserve the line-1 trailing zone (ping + direction pill live on the far right,
|
||||
// the nearest being the ping at innerW - pingW - spacingXl*3). A long IPv6+port
|
||||
// addr must clip before it so it can't run under those. Mirror the ping formula
|
||||
// conservatively and leave a gap.
|
||||
float line1RightLimit = rowPos.x + innerW - Layout::spacingXl() * 3 -
|
||||
capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "0000ms").x -
|
||||
Layout::spacingSm();
|
||||
dl->PushClipRect(ImVec2(addrX, rowPos.y), ImVec2(line1RightLimit, rowEnd.y), true);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(addrX, cy), OnSurface(), peer.addr.c_str());
|
||||
dl->PopClipRect();
|
||||
|
||||
// Seed node icon — rendered right after the IP address
|
||||
// Seed node icon — rendered right after the IP address, but never past the
|
||||
// reserved trailing zone (a long addr would otherwise push it into the ping text).
|
||||
if (IsSeedNode(peer.addr)) {
|
||||
ImVec2 addrSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, peer.addr.c_str());
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
float iconY = cy + (body2->LegacySize - iconFont->LegacySize) * 0.5f;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(addrX + addrSz.x + Layout::spacingSm(), iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
|
||||
float seedIconX = std::min(addrX + addrSz.x + Layout::spacingSm(),
|
||||
line1RightLimit - iconFont->LegacySize);
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(seedIconX, iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -681,10 +692,10 @@ void RenderPeersTab(App* app)
|
||||
ImU32 dirFg = peer.inbound ? WithAlpha(Success(), 200) : WithAlpha(Secondary(), 200);
|
||||
ImVec2 dirSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, dirLabel);
|
||||
float dirX = rowPos.x + innerW - dirSz.x - Layout::spacingXl();
|
||||
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size);
|
||||
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size);
|
||||
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel);
|
||||
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
|
||||
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
|
||||
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2 * dp), dirFg, dirLabel);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -696,30 +707,55 @@ void RenderPeersTab(App* app)
|
||||
}
|
||||
|
||||
float cy2 = cy + body2->LegacySize + Layout::spacingXs();
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy2),
|
||||
OnSurfaceDisabled(), peer.subver.c_str());
|
||||
float subverX = cx + S.drawElement("tabs.peers", "address-x-offset").size * dp;
|
||||
|
||||
// Reserve the line-2 trailing widths up front so an untrusted, arbitrarily long
|
||||
// remote subver cannot push the TLS badge / ban-score off the row. Order right→left:
|
||||
// ban-score pinned far right, then the TLS/no-TLS badge, then the clipped subver text.
|
||||
float line2RightLimit = rowPos.x + innerW - Layout::spacingLg();
|
||||
float banScoreLeftX = line2RightLimit;
|
||||
char banBuf[16];
|
||||
ImU32 banCol = 0;
|
||||
bool haveBanScore = peer.banscore > 0;
|
||||
if (haveBanScore) {
|
||||
snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore);
|
||||
banCol = peer.banscore > 50 ? Error() : Warning();
|
||||
float banW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf).x;
|
||||
banScoreLeftX = line2RightLimit - banW;
|
||||
// The TLS/no-TLS badge sits to the LEFT of the ban score with a gap.
|
||||
line2RightLimit = banScoreLeftX - Layout::spacingLg();
|
||||
}
|
||||
|
||||
float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs);
|
||||
float noTlsW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("peers_no_tls")).x;
|
||||
float badgeW = peer.tls_cipher.empty() ? noTlsW : tlsBadgeW;
|
||||
// Hard right boundary for the subver text column, leaving room for the badge.
|
||||
float subverMaxX = line2RightLimit - badgeW - Layout::spacingSm();
|
||||
if (subverMaxX < subverX) subverMaxX = subverX;
|
||||
|
||||
float verW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, peer.subver.c_str()).x;
|
||||
float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs);
|
||||
dl->PushClipRect(ImVec2(subverX, rowPos.y), ImVec2(subverMaxX, rowEnd.y), true);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(subverX, cy2),
|
||||
OnSurfaceDisabled(), peer.subver.c_str());
|
||||
dl->PopClipRect();
|
||||
|
||||
// Pin the badge just after the (clipped) subver, but never past the reserved zone.
|
||||
float badgeX = std::min(subverX + verW + Layout::spacingSm(), subverMaxX + Layout::spacingSm());
|
||||
if (!peer.tls_cipher.empty()) {
|
||||
ImU32 tlsBg = WithAlpha(Success(), 25);
|
||||
ImU32 tlsFg = WithAlpha(Success(), 200);
|
||||
ImVec2 tlsMin(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2);
|
||||
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2);
|
||||
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
|
||||
ImVec2 tlsMin(badgeX, cy2);
|
||||
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2 * dp);
|
||||
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4 * dp, cy2 + 1 * dp), tlsFg, "TLS");
|
||||
} else {
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2),
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
|
||||
WithAlpha(Error(), 140), TR("peers_no_tls"));
|
||||
}
|
||||
|
||||
if (peer.banscore > 0) {
|
||||
char banBuf[16];
|
||||
snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore);
|
||||
ImU32 banCol = peer.banscore > 50 ? Error() : Warning();
|
||||
ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf);
|
||||
if (haveBanScore) {
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rowPos.x + innerW - banSz.x - Layout::spacingLg(), cy2), banCol, banBuf);
|
||||
ImVec2(banScoreLeftX, cy2), banCol, banBuf);
|
||||
}
|
||||
|
||||
ImGui::InvisibleButton("##peerRow", ImVec2(innerW, rowH));
|
||||
@@ -778,7 +814,7 @@ void RenderPeersTab(App* app)
|
||||
|
||||
if (i < state.peers.size() - 1) {
|
||||
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
||||
dl->AddLine(ImVec2(divStart.x + pad + 18, divStart.y),
|
||||
dl->AddLine(ImVec2(divStart.x + pad + 18 * dp, divStart.y),
|
||||
ImVec2(divStart.x + innerW - pad, divStart.y),
|
||||
IM_COL32(255, 255, 255, 15));
|
||||
}
|
||||
@@ -789,13 +825,12 @@ void RenderPeersTab(App* app)
|
||||
} else {
|
||||
// ---- Banned Peers ----
|
||||
if (!app->isConnected()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
|
||||
material::DrawEmptyState(ICON_MD_CLOUD_OFF, TR("not_connected"));
|
||||
} else if (state.bannedPeers.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_banned"));
|
||||
material::DrawEmptyState(ICON_MD_BLOCK, TR("peers_no_banned"));
|
||||
} else {
|
||||
float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size;
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size * dp;
|
||||
float rowInsetB = pad;
|
||||
float innerW = ImGui::GetContentRegionAvail().x - rowInsetB * 2;
|
||||
listScrollY = ImGui::GetScrollY();
|
||||
@@ -811,21 +846,21 @@ void RenderPeersTab(App* app)
|
||||
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
||||
|
||||
if (is_selected) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "banned-row-rounding").size);
|
||||
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size, rowEnd.y), WithAlpha(Error(), 200), S.drawElement("tabs.peers", "banned-accent-rounding").size);
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "banned-row-rounding").size * dp);
|
||||
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size * dp, rowEnd.y), WithAlpha(Error(), 200), S.drawElement("tabs.peers", "banned-accent-rounding").size * dp);
|
||||
}
|
||||
|
||||
if (material::IsRectHovered(rowPos, rowEnd) && !is_selected) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "banned-row-rounding").size);
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "banned-row-rounding").size * dp);
|
||||
}
|
||||
|
||||
float cx = rowPos.x + pad;
|
||||
float cy = rowPos.y + Layout::spacingXs();
|
||||
|
||||
float banDotR = S.drawElement("tabs.peers", "ban-dot-radius-base").size + S.drawElement("tabs.peers", "ban-dot-radius-scale").size * hs;
|
||||
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ban-dot-x-offset").size, cy + capFont->LegacySize * 0.4f), banDotR, WithAlpha(Error(), 200));
|
||||
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ban-dot-x-offset").size * dp, cy + capFont->LegacySize * 0.4f), banDotR, WithAlpha(Error(), 200));
|
||||
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "banned-address-x-offset").size, cy),
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "banned-address-x-offset").size * dp, cy),
|
||||
OnSurfaceDisabled(), banned.address.c_str());
|
||||
|
||||
std::string banUntil = banned.getBannedUntilString();
|
||||
@@ -841,7 +876,7 @@ void RenderPeersTab(App* app)
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(rowPos);
|
||||
ImGui::InvisibleButton("##bannedRow", ImVec2(innerW - S.drawElement("tabs.peers", "banned-row-btn-reserve").size, rowH));
|
||||
ImGui::InvisibleButton("##bannedRow", ImVec2(innerW - S.drawElement("tabs.peers", "banned-row-btn-reserve").size * dp, rowH));
|
||||
if (ImGui::IsItemClicked(0)) {
|
||||
s_selected_banned_idx = static_cast<int>(i);
|
||||
}
|
||||
@@ -861,7 +896,7 @@ void RenderPeersTab(App* app)
|
||||
|
||||
if (i < state.bannedPeers.size() - 1) {
|
||||
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
||||
dl->AddLine(ImVec2(divStart.x + pad + 8, divStart.y),
|
||||
dl->AddLine(ImVec2(divStart.x + pad + 8 * dp, divStart.y),
|
||||
ImVec2(divStart.x + innerW - pad, divStart.y),
|
||||
IM_COL32(255, 255, 255, 15));
|
||||
}
|
||||
|
||||
@@ -120,8 +120,10 @@ void QRPopupDialog::render(App* app)
|
||||
widgets::AddressCopyField("##QRAddress", s_address);
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Buttons — size each to its label (so "Copy address" never clips), then center the pair.
|
||||
|
||||
// Footer — a primary "Copy address" + "Close" pair, placed via the shared design-system helper.
|
||||
// Size both buttons to the wider label (so "Copy address" never clips) and hand that width to
|
||||
// DialogActionFooter, which centers the pair.
|
||||
ImFont* btnFont = S.resolveFont(actionBtn.font);
|
||||
ImGui::PushFont(btnFont);
|
||||
const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f;
|
||||
@@ -130,14 +132,14 @@ void QRPopupDialog::render(App* app)
|
||||
ImGui::PopFont();
|
||||
if (w_copy < actionBtn.width) w_copy = actionBtn.width;
|
||||
if (w_close < actionBtn.width) w_close = actionBtn.width;
|
||||
const float total_width = w_copy + w_close + ImGui::GetStyle().ItemSpacing.x;
|
||||
ImGui::SetCursorPosX((window_width - total_width) / 2.0f);
|
||||
const float btnW = (w_copy > w_close ? w_copy : w_close);
|
||||
|
||||
if (material::TactileButton(TR("copy_address"), ImVec2(w_copy, 0), btnFont)) {
|
||||
bool doCopy = false, doClose = false;
|
||||
material::DialogActionFooter(TR("copy_address"), true, TR("close"), doCopy, doClose, btnW);
|
||||
if (doCopy) {
|
||||
ImGui::SetClipboardText(s_address.c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::TactileButton(TR("close"), ImVec2(w_close, 0), btnFont)) {
|
||||
if (doClose) {
|
||||
close();
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
|
||||
+132
-73
@@ -175,6 +175,11 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
}
|
||||
}
|
||||
|
||||
// Combo/button widths first — the preview truncates to the combo's real pixel width below.
|
||||
float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width));
|
||||
float newBtnW = std::max(schema::UI().drawElement("tabs.receive", "new-btn-min-width").size, schema::UI().drawElement("tabs.receive", "new-btn-width").size * Layout::hScale(width));
|
||||
float dropdownW = width - copyBtnW - newBtnW - Layout::spacingSm() * 2;
|
||||
|
||||
// Build preview string
|
||||
if (!app->isConnected()) {
|
||||
s_source_preview = TR(app->isLiteBuild() ? "lite_no_wallet_short" : "not_connected");
|
||||
@@ -183,18 +188,27 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
const auto& addr = state.addresses[s_selected_address_idx];
|
||||
bool isZ = addr.type == "shielded";
|
||||
const char* tag = isZ ? "[Z]" : "[T]";
|
||||
std::string trunc = util::truncateMiddle(addr.address,
|
||||
static_cast<int>(std::max(schema::UI().drawElement("tabs.receive", "addr-preview-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-preview-trunc-divisor").size)));
|
||||
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s",
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
||||
// Reserve pixel room for the tag prefix and the trailing balance, then middle-truncate the
|
||||
// address to whatever remains — measured with the combo's own Body2 font. Char-count
|
||||
// truncation kept MORE chars as the column widened, so at 150% the scaled font overflowed
|
||||
// and the combo hard-clipped "— 12.00000000 DRGX" to "— 1"; measuring in pixels keeps the
|
||||
// balance visible at any scale.
|
||||
ImFont* comboFont = Type().getFont(TypeStyle::Body2);
|
||||
float comboFontSz = comboFont->LegacySize;
|
||||
char prefix[16]; snprintf(prefix, sizeof(prefix), "%s ", tag);
|
||||
char suffix[64]; snprintf(suffix, sizeof(suffix), " \xe2\x80\x94 %.8f %s", addr.balance, DRAGONX_TICKER);
|
||||
float fixedW = comboFont->CalcTextSizeA(comboFontSz, FLT_MAX, 0.0f, prefix).x
|
||||
+ comboFont->CalcTextSizeA(comboFontSz, FLT_MAX, 0.0f, suffix).x;
|
||||
// Combo interior = dropdownW minus its dropdown-arrow button (~frame height) and both frame paddings.
|
||||
float addrBudget = dropdownW - ImGui::GetFrameHeight() - ImGui::GetStyle().FramePadding.x * 2.0f - fixedW;
|
||||
if (addrBudget < 24.0f) addrBudget = 24.0f; // floor: truncate to a stub rather than overflow
|
||||
std::string trunc = material::TruncateToWidth(addr.address, comboFont, comboFontSz, addrBudget);
|
||||
snprintf(buf, sizeof(buf), "%s%s%s", prefix, trunc.c_str(), suffix);
|
||||
s_source_preview = buf;
|
||||
} else {
|
||||
s_source_preview = TR("select_receiving_address");
|
||||
}
|
||||
|
||||
float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width));
|
||||
float newBtnW = std::max(schema::UI().drawElement("tabs.receive", "new-btn-min-width").size, schema::UI().drawElement("tabs.receive", "new-btn-width").size * Layout::hScale(width));
|
||||
float dropdownW = width - copyBtnW - newBtnW - Layout::spacingSm() * 2;
|
||||
ImGui::SetNextItemWidth(dropdownW);
|
||||
ImGui::PushFont(Type().getFont(TypeStyle::Body2));
|
||||
if (ImGui::BeginCombo("##RecvAddr", s_source_preview.c_str())) {
|
||||
@@ -245,11 +259,11 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
|
||||
tag, lblIt->second.c_str(), trunc.c_str(),
|
||||
addr.balance, DRAGONX_TICKER,
|
||||
isNew ? " [NEW]" : "");
|
||||
isNew ? TR("grpb_new_badge_suffix") : "");
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s",
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
|
||||
isNew ? " [NEW]" : "");
|
||||
isNew ? TR("grpb_new_badge_suffix") : "");
|
||||
}
|
||||
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
@@ -258,9 +272,9 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
s_cached_qr_data.clear(); // Force QR regeneration
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s\nBalance: %.8f %s%s",
|
||||
material::Tooltip(TR("grpb_tooltip_address_balance"),
|
||||
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
|
||||
isCurrent ? "\n(selected)" : "");
|
||||
isCurrent ? TR("grpb_selected_suffix") : "");
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -324,20 +338,9 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helpers: timeAgo / DrawRecvIcon (local copies — originals are static in send_tab)
|
||||
// Helpers: DrawRecvIcon (local copy — original is static in send_tab).
|
||||
// Relative time uses the shared util::formatTimeAgoShort ("14d ago"), matching Overview/Send.
|
||||
// ============================================================================
|
||||
static std::string recvTimeAgo(int64_t timestamp) {
|
||||
if (timestamp <= 0) return "";
|
||||
int64_t now = (int64_t)std::time(nullptr);
|
||||
int64_t diff = now - timestamp;
|
||||
if (diff < 0) diff = 0;
|
||||
char buf[32];
|
||||
if (diff < 60) { snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); return buf; }
|
||||
if (diff < 3600) { snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); return buf; }
|
||||
if (diff < 86400) { snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); return buf; }
|
||||
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); return buf;
|
||||
}
|
||||
|
||||
static void DrawRecvIcon(ImDrawList* dl, float cx, float cy, float s, ImU32 col) {
|
||||
dl->AddTriangleFilled(
|
||||
ImVec2(cx, cy + s),
|
||||
@@ -367,15 +370,25 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
|
||||
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||
ImU32 recvCol = Success();
|
||||
|
||||
// Collect matching transactions
|
||||
// Grow the list to fill the dead space beneath the (fixed-height) receive card:
|
||||
// fit as many newest-first rows as the remaining region can show, instead of a
|
||||
// fixed 4. The child scrolls if the real history exceeds what fits, so nothing is
|
||||
// lost. A floor of 4 keeps the section substantial when the region is short.
|
||||
// Compute maxRows BEFORE the collect loop so the scan can stop early (below).
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
size_t maxRows = std::max<size_t>(4, (size_t)std::floor(listH / rowH));
|
||||
|
||||
// Collect matching transactions. state.transactions is newest-first, so the first
|
||||
// maxRows matches ARE exactly the rows we render — stop scanning once we have them
|
||||
// instead of filtering the entire history every frame (mirrors RenderSharedRecentTx
|
||||
// in balance_components.cpp).
|
||||
std::vector<const TransactionInfo*> recvs;
|
||||
recvs.reserve(maxRows);
|
||||
for (const auto& tx : state.transactions) {
|
||||
if (tx.type != "receive" && tx.type != "mined") continue;
|
||||
recvs.push_back(&tx);
|
||||
if (recvs.size() >= maxRows) break; // newest-first: prefix is all we show
|
||||
}
|
||||
if (recvs.size() > 4) recvs.resize(4); // show only the 4 most recent (newest-first)
|
||||
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
|
||||
ImGui::BeginChild("##RecentReceivedRows", ImVec2(width, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
@@ -383,8 +396,9 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
|
||||
|
||||
char buf[64];
|
||||
if (recvs.empty()) {
|
||||
ImGui::SetCursorPosY(Layout::spacingMd());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_recent_receives"));
|
||||
// Fill the empty canvas with a centered material empty-state (icon + title)
|
||||
// rather than a lone left-aligned caption stranded at the top of dead space.
|
||||
material::DrawEmptyState(ICON_MD_CALL_RECEIVED, TR("no_recent_receives"));
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
@@ -404,30 +418,37 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), typeText);
|
||||
|
||||
// Address (second line)
|
||||
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
// Address — start it AFTER the measured type-label width (not a fixed offset that shrinks below
|
||||
// the label at narrow widths), mirroring Overview's recent-tx list. The schema offset is a floor.
|
||||
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, typeText).x;
|
||||
float addrX = txX + std::max(S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs,
|
||||
typeW + Layout::spacingSm());
|
||||
std::string addrDisplay = util::truncateMiddle(tx.address,
|
||||
(int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
|
||||
|
||||
// Amount (right-aligned, first line)
|
||||
// Time ago — short "14d ago" form (shared helper, matches Overview/Send). Measure it
|
||||
// first so the amount can chain its right edge off this width and never overlap.
|
||||
std::string ago = util::formatTimeAgoShort(tx.timestamp);
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
|
||||
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
||||
float agoMargin = S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs;
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - agoMargin, rowPos.y + 2.0f * dp),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
// Amount (right-aligned, first line) — anchored to the LEFT of the time-ago text
|
||||
// (measured width + a gap) so the two columns can never collide, whatever the strings.
|
||||
snprintf(buf, sizeof(buf), "+%.4f %s", std::abs(tx.amount), DRAGONX_TICKER);
|
||||
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf);
|
||||
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
||||
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size,
|
||||
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
||||
float amtGap = std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size,
|
||||
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
||||
float amtRightEdge = rightEdge - agoSz.x - agoMargin - amtGap;
|
||||
float amtX = amtRightEdge - amtSz.x;
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(amtX, rowPos.y + 2.0f * dp), recvCol, buf);
|
||||
|
||||
// Time ago
|
||||
std::string ago = recvTimeAgo(tx.timestamp);
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f),
|
||||
rowPos.y + 2.0f * dp),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
// Clickable row — hover highlight + navigate to History
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
||||
@@ -488,6 +509,16 @@ void RenderReceiveTab(App* app)
|
||||
float groupStartY = ImGui::GetCursorPosY();
|
||||
float contentStartY = ImGui::GetCursorPosY();
|
||||
|
||||
// Reserve a slice of the available height for RECENT RECEIVED (ratio — mirrors
|
||||
// balance_tab's recent-tx-reserve). The main card's target height is capped so it can
|
||||
// never grow past (available - reserve): a no-op at 1.0x (mainCardTargetH already fits
|
||||
// well within scrollAvailH there), but at HiDPI it prevents the card — whose QR (280*dp)
|
||||
// and pads scale with dp while scrollAvailH is physical px — from eating the whole child
|
||||
// and evicting the list.
|
||||
float recvReserveRatio = S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
|
||||
float recvRecentReserve = std::max(0.0f, scrollAvailH) * recvReserveRatio;
|
||||
float recvCardCapH = std::max(0.0f, scrollAvailH - recvRecentReserve);
|
||||
|
||||
float formAvailW = ImGui::GetContentRegionAvail().x;
|
||||
float formW = formAvailW;
|
||||
ImGui::BeginGroup();
|
||||
@@ -514,21 +545,25 @@ void RenderReceiveTab(App* app)
|
||||
|
||||
if (state.addresses.empty()) {
|
||||
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
|
||||
float emptyH = S.drawElement("tabs.receive", "skeleton-height").size;
|
||||
// Hand-drawn skeleton geometry is absolute px — scale heights/offsets/rounding by
|
||||
// dpiScale() so it doesn't render native-thin at >100% (width ratios multiply formW, leave as-is).
|
||||
float dp = Layout::dpiScale();
|
||||
float emptyH = S.drawElement("tabs.receive", "skeleton-height").size * dp;
|
||||
ImVec2 emptyMax(emptyMin.x + formW, emptyMin.y + emptyH);
|
||||
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
|
||||
float alpha = (float)(schema::UI().drawElement("animations", "skeleton-base").size + schema::UI().drawElement("animations", "skeleton-amp").size * std::sin(ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-slow").size));
|
||||
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
|
||||
float skelRound = schema::UI().drawElement("tabs.receive", "skeleton-rounding").size * dp;
|
||||
dl->AddRectFilled(
|
||||
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()),
|
||||
ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar1-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar1-height").size),
|
||||
skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size);
|
||||
ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar1-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar1-height").size * dp),
|
||||
skelCol, skelRound);
|
||||
dl->AddRectFilled(
|
||||
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-top").size),
|
||||
ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar2-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-bottom").size),
|
||||
skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size);
|
||||
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-top").size * dp),
|
||||
ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar2-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-bottom").size * dp),
|
||||
skelCol, skelRound);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size),
|
||||
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size * dp),
|
||||
OnSurfaceDisabled(), TR("loading_addresses"));
|
||||
ImGui::Dummy(ImVec2(formW, emptyH));
|
||||
ImGui::EndGroup();
|
||||
@@ -567,21 +602,36 @@ void RenderReceiveTab(App* app)
|
||||
// MAIN CARD — single glass panel (channel split like Send tab)
|
||||
// ================================================================
|
||||
{
|
||||
// Cap + center the card so the address/amount column stops stretching while the
|
||||
// QR plateaus. Shared with the Send tab via mainComposeCardBox() so the two card
|
||||
// envelopes are identical (they must never drift in width/position again).
|
||||
// The RECENT RECEIVED list below stays on the uncapped formW (handled separately).
|
||||
float cardDp = Layout::dpiScale();
|
||||
Layout::CardBox cardBox = Layout::mainComposeCardBox(formW);
|
||||
float cardW = cardBox.width;
|
||||
float cardLeftX = ImGui::GetCursorScreenPos().x;
|
||||
float cardOffsetX = cardBox.offsetX;
|
||||
if (cardOffsetX > 0.0f)
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + cardOffsetX);
|
||||
|
||||
ImVec2 containerMin = ImGui::GetCursorScreenPos();
|
||||
float pad = Layout::spacingLg();
|
||||
float innerW = formW - pad * 2;
|
||||
float innerW = cardW - pad * 2;
|
||||
float innerGap = Layout::spacingLg();
|
||||
|
||||
// Channel split: content on ch1, glass background on ch0
|
||||
dl->ChannelsSplit(2);
|
||||
dl->ChannelsSetCurrent(1);
|
||||
|
||||
ImGui::Indent(pad);
|
||||
// Indent carries the centering offset too, so ImGui auto-layout content (the address
|
||||
// dropdown/inputs/chips) lands at containerMin.x + pad — matching the hand-drawn geometry
|
||||
// (glass panel / QR column) that keys off the offset containerMin.x.
|
||||
ImGui::Indent(pad + cardOffsetX);
|
||||
ImGui::Dummy(ImVec2(0, pad)); // top padding
|
||||
|
||||
// ---- ADDRESS DROPDOWN + QR CODE — side by side ----
|
||||
{
|
||||
float qrColW = innerW * schema::UI().drawElement("tabs.receive", "qr-col-width-ratio").size;
|
||||
float qrColW = std::min(innerW * schema::UI().drawElement("tabs.receive", "qr-col-width-ratio").size, 340.0f * cardDp);
|
||||
float colGap = Layout::spacingLg();
|
||||
float addrColW = innerW - qrColW - colGap;
|
||||
float qrColX = containerMin.x + pad + addrColW + colGap;
|
||||
@@ -599,7 +649,7 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Amount input with currency toggle
|
||||
float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size;
|
||||
float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size * Layout::dpiScale();
|
||||
float amtInputW = addrColW - toggleW - Layout::spacingMd();
|
||||
if (amtInputW < S.drawElement("tabs.receive", "amount-input-min-width").size) amtInputW = S.drawElement("tabs.receive", "amount-input-min-width").size;
|
||||
double usd_price = state.market.price_usd;
|
||||
@@ -663,25 +713,25 @@ void RenderReceiveTab(App* app)
|
||||
float bH = bMax.y - bMin.y;
|
||||
ImFont* font = ImGui::GetFont();
|
||||
ImVec2 textSz = font->CalcTextSizeA(font->LegacySize, 10000, 0, currLabel);
|
||||
float iconW = schema::UI().drawElement("tabs.receive", "currency-icon-width").size;
|
||||
float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size;
|
||||
float iconW = schema::UI().drawElement("tabs.receive", "currency-icon-width").size * Layout::dpiScale();
|
||||
float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size * Layout::dpiScale();
|
||||
float totalW2 = iconW + iconGap2 + textSz.x;
|
||||
float startX = bMin.x + ((bMax.x - bMin.x) - totalW2) * 0.5f;
|
||||
float cy = bMin.y + bH * 0.5f;
|
||||
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
float ss = iconW * 0.5f;
|
||||
float cx = startX + ss;
|
||||
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size;
|
||||
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
|
||||
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
|
||||
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size;
|
||||
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
|
||||
float ay1 = cy - ag;
|
||||
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
|
||||
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
|
||||
dl->AddTriangleFilled(
|
||||
ImVec2(cx + al, ay1),
|
||||
ImVec2(cx + al - hss, ay1 - hss),
|
||||
ImVec2(cx + al - hss, ay1 + hss), iconCol);
|
||||
float ay2 = cy + ag;
|
||||
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
|
||||
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
|
||||
dl->AddTriangleFilled(
|
||||
ImVec2(cx - al, ay2),
|
||||
ImVec2(cx - al + hss, ay2 - hss),
|
||||
@@ -699,8 +749,8 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
|
||||
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size;
|
||||
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size;
|
||||
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
|
||||
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
|
||||
|
||||
struct Preset { const char* label; double amount; };
|
||||
Preset presets[] = {
|
||||
@@ -778,7 +828,7 @@ void RenderReceiveTab(App* app)
|
||||
size_t memo_len = strlen(s_request_memo);
|
||||
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
|
||||
bool memoAtCap = memo_len + 1 >= memoMax;
|
||||
snprintf(buf, sizeof(buf), "%zu / %zu bytes", memo_len, memoMax);
|
||||
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
|
||||
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
@@ -828,7 +878,7 @@ void RenderReceiveTab(App* app)
|
||||
RenderQRCode(s_qr_texture, qrSize);
|
||||
} else {
|
||||
ImGui::Dummy(ImVec2(qrSize, qrSize));
|
||||
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size,
|
||||
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size * cardDp,
|
||||
qrPanelMin.y + totalQrSize * 0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, textPos,
|
||||
OnSurfaceDisabled(), TR("qr_unavailable"));
|
||||
@@ -862,7 +912,7 @@ void RenderReceiveTab(App* app)
|
||||
S.drawElement("tabs.receive", "action-btn-height").size * vScale);
|
||||
float footerH = innerGap + actionBtnH + pad;
|
||||
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
|
||||
float targetCardH = Layout::mainCardTargetH(formW, vScale);
|
||||
float targetCardH = std::min(Layout::mainCardTargetH(cardW, vScale), recvCardCapH);
|
||||
float footerTopH = targetCardH - footerH;
|
||||
if (currentCardH < footerTopH) {
|
||||
ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
|
||||
@@ -873,7 +923,7 @@ void RenderReceiveTab(App* app)
|
||||
{
|
||||
ImVec2 divPos = ImGui::GetCursorScreenPos();
|
||||
dl->AddLine(ImVec2(containerMin.x + pad, divPos.y),
|
||||
ImVec2(containerMin.x + formW - pad, divPos.y),
|
||||
ImVec2(containerMin.x + cardW - pad, divPos.y),
|
||||
ImGui::GetColorU32(Divider()), S.drawElement("tabs.receive", "divider-thickness").size);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
|
||||
@@ -882,7 +932,13 @@ void RenderReceiveTab(App* app)
|
||||
{
|
||||
float btnGap = Layout::spacingMd();
|
||||
float btnH = std::max(schema::UI().drawElement("tabs.receive", "action-btn-min-height").size, schema::UI().drawElement("tabs.receive", "action-btn-height").size * vScale);
|
||||
float otherBtnW = std::max(S.drawElement("tabs.receive", "action-btn-min-width").size, innerW * S.drawElement("tabs.receive", "action-btn-width-ratio").size);
|
||||
// Justify the footer edge-to-edge like Send's [Review Send][Cancel] row instead of packing
|
||||
// fixed-width buttons from the left (which left a large dead gap on the right). Split innerW
|
||||
// into equal shares over the live button count: 2 by default (Clear Request + Explorer),
|
||||
// 4 when an amount is requested (+ Copy URI + Share).
|
||||
int nBtns = (s_request_amount > 0 ? 2 : 0) + 2;
|
||||
float otherBtnW = std::max(S.drawElement("tabs.receive", "action-btn-min-width").size,
|
||||
(innerW - (nBtns - 1) * btnGap) / (float)nBtns);
|
||||
|
||||
bool firstBtn = true;
|
||||
|
||||
@@ -953,23 +1009,26 @@ void RenderReceiveTab(App* app)
|
||||
|
||||
// Bottom padding
|
||||
ImGui::Dummy(ImVec2(0, pad));
|
||||
ImGui::Unindent(pad);
|
||||
ImGui::Unindent(pad + cardOffsetX);
|
||||
|
||||
// Enforce shared card height (matches QR-driven target)
|
||||
// Enforce shared card height (matches QR-driven target), capped so the reserved
|
||||
// RECENT RECEIVED slice below stays on-screen at HiDPI.
|
||||
{
|
||||
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
|
||||
float targetCardH = Layout::mainCardTargetH(formW, vScale);
|
||||
float targetCardH = std::min(Layout::mainCardTargetH(cardW, vScale), recvCardCapH);
|
||||
if (currentCardH < targetCardH)
|
||||
ImGui::Dummy(ImVec2(0, targetCardH - currentCardH));
|
||||
}
|
||||
|
||||
// Draw glass panel background on channel 0
|
||||
ImVec2 containerMax(containerMin.x + formW, ImGui::GetCursorScreenPos().y);
|
||||
ImVec2 containerMax(containerMin.x + cardW, ImGui::GetCursorScreenPos().y);
|
||||
dl->ChannelsSetCurrent(0);
|
||||
DrawGlassPanel(dl, containerMin, containerMax, glassSpec);
|
||||
dl->ChannelsMerge();
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(containerMin.x, containerMax.y));
|
||||
// Restore the original (uncapped) left edge so RECENT RECEIVED below renders
|
||||
// full-width as-is, unaffected by the card's centering offset.
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardLeftX, containerMax.y));
|
||||
ImGui::Dummy(ImVec2(formW, 0));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "../notifications.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../widgets/qr_code.h"
|
||||
#include "../widgets/copy_field.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "imgui.h"
|
||||
|
||||
@@ -146,7 +147,7 @@ void RequestPaymentDialog::render(App* app)
|
||||
|
||||
// Amount (optional)
|
||||
ImGui::Text("%s", TR("request_amount"));
|
||||
ImGui::SetNextItemWidth(amountInput.width);
|
||||
ImGui::SetNextItemWidth(amountInput.width * Layout::dpiScale());
|
||||
if (ImGui::InputDouble("##Amount", &s_amount, 0.1, 1.0, "%.8f")) {
|
||||
s_uri_dirty = true;
|
||||
}
|
||||
@@ -198,33 +199,65 @@ void RequestPaymentDialog::render(App* app)
|
||||
|
||||
// Payment URI display
|
||||
if (!s_payment_uri.empty()) {
|
||||
// Use a selectable text area for the URI
|
||||
char uri_buf[1024];
|
||||
strncpy(uri_buf, s_payment_uri.c_str(), sizeof(uri_buf) - 1);
|
||||
material::LabeledInput(TR("request_payment_uri"), "##URI", uri_buf, sizeof(uri_buf),
|
||||
-1.0f, nullptr, ImGuiInputTextFlags_ReadOnly);
|
||||
|
||||
ImGui::Text("%s", TR("request_payment_uri"));
|
||||
// Bordered read-only field: bounds the raw drgx: URI within a box (it horizontal-scrolls if
|
||||
// long) rather than 4-char-chunking it like an address or letting a plain field overflow.
|
||||
// The "Copy URI" button below copies the full value.
|
||||
{
|
||||
char uriBuf[2048];
|
||||
snprintf(uriBuf, sizeof(uriBuf), "%s", s_payment_uri.c_str());
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
ImGui::InputText("##URI", uriBuf, sizeof(uriBuf), ImGuiInputTextFlags_ReadOnly);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Copy button
|
||||
if (material::TactileButton(TR("request_copy_uri"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
|
||||
// Footer buttons: size each to ITS OWN label (action-button.width is a floor, not the size)
|
||||
// so "Copy Full Address" never clips its final letter — mirrors qr_popup_dialog's per-label
|
||||
// sizing. Font metrics are already DPI-scaled, so don't multiply by dpiScale() here.
|
||||
ImFont* btnFont = S.resolveFont(actionBtn.font);
|
||||
ImGui::PushFont(btnFont);
|
||||
const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 12.0f; // small margin
|
||||
float w_uri = ImGui::CalcTextSize(TR("request_copy_uri")).x + btnPad;
|
||||
float w_addr = ImGui::CalcTextSize(TR("copy_address")).x + btnPad;
|
||||
ImGui::PopFont();
|
||||
if (w_uri < actionBtn.width) w_uri = actionBtn.width;
|
||||
if (w_addr < actionBtn.width) w_addr = actionBtn.width;
|
||||
|
||||
// Center the two-button copy row via the shared helper (no extra divider; the Separator
|
||||
// above already frames this region). Total = both button widths + the ItemSpacing between.
|
||||
material::BeginOverlayDialogFooter(w_uri + w_addr + ImGui::GetStyle().ItemSpacing.x, false);
|
||||
|
||||
// Copy URI button
|
||||
if (material::TactileButton(TR("request_copy_uri"), ImVec2(w_uri, 0), btnFont)) {
|
||||
ImGui::SetClipboardText(s_payment_uri.c_str());
|
||||
Notifications::instance().success(TR("request_uri_copied"));
|
||||
}
|
||||
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::TactileButton(TR("copy_address"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
|
||||
if (material::TactileButton(TR("copy_address"), ImVec2(w_addr, 0), btnFont)) {
|
||||
ImGui::SetClipboardText(s_address);
|
||||
Notifications::instance().success(TR("address_copied"));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Close button
|
||||
if (material::TactileButton(TR("close"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
s_open = false;
|
||||
|
||||
// Close button — sized to its own label with the same floor.
|
||||
{
|
||||
ImFont* btnFont = S.resolveFont(actionBtn.font);
|
||||
ImGui::PushFont(btnFont);
|
||||
const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 12.0f;
|
||||
float w_close = ImGui::CalcTextSize(TR("close")).x + btnPad;
|
||||
ImGui::PopFont();
|
||||
if (w_close < actionBtn.width) w_close = actionBtn.width;
|
||||
// Center the lone Close button via the shared helper (no extra divider — matches the
|
||||
// existing tight spacing above it).
|
||||
material::BeginOverlayDialogFooter(w_close, false);
|
||||
if (material::TactileButton(TR("close"), ImVec2(w_close, 0), btnFont)) {
|
||||
s_open = false;
|
||||
}
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
+153
-62
@@ -137,13 +137,15 @@ static double GetAvailableBalance(App* app) {
|
||||
// not a stored list index. The index desyncs from s_from_address after an address-list
|
||||
// refresh, and is left at -1 when the source is chosen from another tab ("Send from this
|
||||
// address") — which previously made the sufficiency check see 0 and block a valid send.
|
||||
// CONFIRMED balance only — this is the spend ceiling (Max button, slider, validation, pre-broadcast
|
||||
// re-check). z_sendmany runs at minconf=1, so offering 0-conf change here would just make the send fail.
|
||||
if (s_from_address[0] != '\0') {
|
||||
for (const auto& a : state.addresses) {
|
||||
if (a.address == s_from_address) return a.balance;
|
||||
if (a.address == s_from_address) return a.spendableBalance;
|
||||
}
|
||||
}
|
||||
if (s_selected_from_idx >= 0 && s_selected_from_idx < static_cast<int>(state.addresses.size())) {
|
||||
return state.addresses[s_selected_from_idx].balance;
|
||||
return state.addresses[s_selected_from_idx].spendableBalance;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
@@ -158,6 +160,20 @@ static bool IsValidTransparentAddr(const char* a) {
|
||||
return a && dragonx::util::isTransparentAddress(a);
|
||||
}
|
||||
|
||||
// Source-address balance caption for the "sending from" dropdown: the TOTAL balance is the headline (so
|
||||
// it matches the Overview figure and a source whose change is still confirming never looks like it lost
|
||||
// funds), with the confirmed-spendable amount shown as a smaller "(N available)" note ONLY when a pending
|
||||
// send/receive makes it differ. The Max button + validation still cap spends at the spendable amount.
|
||||
static std::string FormatSourceBalance(double total, double spendable) {
|
||||
char b[128];
|
||||
if (total - spendable > 1e-9)
|
||||
snprintf(b, sizeof(b), "%.8f %s (%.8f %s)", total, DRAGONX_TICKER,
|
||||
spendable, TR("send_available_note"));
|
||||
else
|
||||
snprintf(b, sizeof(b), "%.8f %s", total, DRAGONX_TICKER);
|
||||
return b;
|
||||
}
|
||||
|
||||
static std::string timeAgo(int64_t timestamp) {
|
||||
return dragonx::util::formatTimeAgoShort(timestamp);
|
||||
}
|
||||
@@ -251,8 +267,9 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
const char* tag = isZ ? "[Z]" : "[T]";
|
||||
std::string trunc = util::truncateMiddle(addr.address,
|
||||
static_cast<int>(std::max(S.drawElement("tabs.send", "addr-preview-trunc-min").size, width / S.drawElement("tabs.send", "addr-preview-trunc-divisor").size)));
|
||||
snprintf(buf, sizeof(buf), "%s %s — %.8f %s",
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
||||
snprintf(buf, sizeof(buf), "%s %s — %s",
|
||||
tag, trunc.c_str(),
|
||||
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
||||
s_source_preview = buf;
|
||||
} else {
|
||||
s_source_preview = TR("send_select_source");
|
||||
@@ -265,8 +282,17 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
if (!app->isConnected() || state.addresses.empty()) {
|
||||
ImGui::TextDisabled("%s", TR("no_addresses_available"));
|
||||
} else {
|
||||
// Sort by balance descending, only show spendable addresses with balance
|
||||
std::vector<size_t> sortedIdx = sortedSpendableAddressIndices(state.addresses);
|
||||
// List every address that HOLDS a balance (confirmed or still-confirming), sorted by total
|
||||
// descending — so a source whose change is pending stays visible with its real total instead
|
||||
// of vanishing. The spend gates (Max / validation) below still cap at the spendable amount.
|
||||
std::vector<size_t> sortedIdx;
|
||||
sortedIdx.reserve(state.addresses.size());
|
||||
for (size_t i = 0; i < state.addresses.size(); ++i)
|
||||
if (state.addresses[i].isSpendable() && state.addresses[i].balance > 0.0)
|
||||
sortedIdx.push_back(i);
|
||||
std::sort(sortedIdx.begin(), sortedIdx.end(), [&](size_t a, size_t b) {
|
||||
return state.addresses[a].balance > state.addresses[b].balance;
|
||||
});
|
||||
|
||||
if (sortedIdx.empty()) {
|
||||
ImGui::TextDisabled("%s", TR("send_no_balance"));
|
||||
@@ -281,8 +307,9 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
const char* tag = isZ ? "[Z]" : "[T]";
|
||||
|
||||
std::string trunc = util::truncateMiddle(addr.address, (int)addrTruncLen);
|
||||
snprintf(buf, sizeof(buf), "%s %s — %.8f %s",
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
||||
snprintf(buf, sizeof(buf), "%s %s — %s",
|
||||
tag, trunc.c_str(),
|
||||
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
||||
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
if (ImGui::Selectable(buf, isCurrent)) {
|
||||
@@ -291,8 +318,9 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
addr.address.c_str());
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s\nBalance: %.8f %s",
|
||||
addr.address.c_str(), addr.balance, DRAGONX_TICKER);
|
||||
material::Tooltip("%s\n%s",
|
||||
addr.address.c_str(),
|
||||
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -336,7 +364,8 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
|
||||
if (suggestions.empty()) return;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color)));
|
||||
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size, schema::UI().drawElement("tabs.send", "suggestion-max-height").size);
|
||||
const float dp = Layout::dpiScale();
|
||||
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size * dp + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size * dp, schema::UI().drawElement("tabs.send", "suggestion-max-height").size * dp);
|
||||
ImGui::BeginChild(childId, ImVec2(width, sugH), true);
|
||||
for (size_t si = 0; si < suggestions.size(); si++) {
|
||||
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
|
||||
@@ -359,13 +388,14 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
|
||||
// ============================================================================
|
||||
static void RenderFeeTierSelector(const char* suffix = "") {
|
||||
auto& S = schema::UI();
|
||||
const float dp = Layout::dpiScale();
|
||||
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
|
||||
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
|
||||
for (int fi = 0; fi < 3; fi++) {
|
||||
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size);
|
||||
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size * dp);
|
||||
bool active = (s_fee_tier == fi);
|
||||
if (active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size)));
|
||||
@@ -396,11 +426,13 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
|
||||
? std::clamp((float)((s_amount + s_fee) / available), 0.0f, 1.0f)
|
||||
: 0.0f;
|
||||
|
||||
float maxBtnW = schema::UI().drawElement("tabs.send", "amount-bar-max-btn-width").size;
|
||||
const float dp = Layout::dpiScale();
|
||||
float maxBtnW = schema::UI().drawElement("tabs.send", "amount-bar-max-btn-width").size * dp;
|
||||
float gap = Layout::spacingMd();
|
||||
float barW = innerW - maxBtnW - gap;
|
||||
if (barW < schema::UI().drawElement("tabs.send", "progress-bar-min-width").size) barW = schema::UI().drawElement("tabs.send", "progress-bar-min-width").size;
|
||||
float barH = schema::UI().drawElement("tabs.send", "amount-bar-height").size;
|
||||
float minBarW = schema::UI().drawElement("tabs.send", "progress-bar-min-width").size * dp;
|
||||
if (barW < minBarW) barW = minBarW;
|
||||
float barH = schema::UI().drawElement("tabs.send", "amount-bar-height").size * dp;
|
||||
float barRound = barH * 0.5f;
|
||||
|
||||
ImVec2 barMin = ImGui::GetCursorScreenPos();
|
||||
@@ -485,7 +517,7 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
|
||||
// Max button — use caption font to fit bar height
|
||||
ImGui::SameLine(0, gap);
|
||||
char maxId[32];
|
||||
snprintf(maxId, sizeof(maxId), "Max%s", suffix);
|
||||
snprintf(maxId, sizeof(maxId), "%s%s", TR("grpb_max"), suffix);
|
||||
if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) {
|
||||
s_amount = maxAmount;
|
||||
s_send_max = true;
|
||||
@@ -509,6 +541,8 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
}
|
||||
if (s_tx_status.empty() && !s_sending) return;
|
||||
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
// Drive error styling from the authoritative result flag, not English substrings in a
|
||||
// (translatable) status string — otherwise a failed send renders as a green success
|
||||
// under a non-English locale.
|
||||
@@ -517,8 +551,8 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
// ---- ERROR: absolute-positioned overlay, does not displace layout ----
|
||||
if (is_error) {
|
||||
float pad = Layout::spacingLg();
|
||||
float btnH = std::max(schema::UI().drawElement("tabs.send", "error-btn-min-height").size, schema::UI().drawElement("tabs.send", "error-btn-height").size);
|
||||
float textWrapW = w - pad * 2 - schema::UI().drawElement("tabs.send", "error-icon-inset").size; // icon space
|
||||
float btnH = std::max(schema::UI().drawElement("tabs.send", "error-btn-min-height").size, schema::UI().drawElement("tabs.send", "error-btn-height").size) * dp;
|
||||
float textWrapW = w - pad * 2 - schema::UI().drawElement("tabs.send", "error-icon-inset").size * dp; // icon space
|
||||
ImVec2 textSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, textWrapW, s_tx_status.c_str());
|
||||
float contentH = textSz.y + Layout::spacingMd() + btnH + pad * 2;
|
||||
|
||||
@@ -542,7 +576,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
DrawGlassPanel(fgDl, pMin, pMax, errGlass);
|
||||
|
||||
// Red accent bar on left
|
||||
fgDl->AddRectFilled(pMin, ImVec2(pMin.x + schema::UI().drawElement("tabs.send", "error-accent-bar-width").size, pMax.y), Error(), errGlass.rounding);
|
||||
fgDl->AddRectFilled(pMin, ImVec2(pMin.x + schema::UI().drawElement("tabs.send", "error-accent-bar-width").size * dp, pMax.y), Error(), errGlass.rounding);
|
||||
|
||||
float ix = pMin.x + pad;
|
||||
float iy = pMin.y + pad;
|
||||
@@ -553,18 +587,18 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
const char* errIcon = ICON_MD_ERROR;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, errIcon);
|
||||
fgDl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(ix + schema::UI().drawElement("tabs.send", "error-icon-x-offset").size, iy + body2->LegacySize * 0.5f - iSz.y * 0.5f),
|
||||
ImVec2(ix + schema::UI().drawElement("tabs.send", "error-icon-x-offset").size * dp, iy + body2->LegacySize * 0.5f - iSz.y * 0.5f),
|
||||
Error(), errIcon);
|
||||
}
|
||||
|
||||
// Error text (wrapped)
|
||||
fgDl->AddText(body2, body2->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "error-text-x-offset").size, iy), Error(),
|
||||
fgDl->AddText(body2, body2->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "error-text-x-offset").size * dp, iy), Error(),
|
||||
s_tx_status.c_str(), s_tx_status.c_str() + s_tx_status.size(), textWrapW);
|
||||
|
||||
// Buttons row — use invisible window for interactive widgets on top of overlay
|
||||
float btnY = iy + textSz.y + Layout::spacingMd();
|
||||
ImGui::SetNextWindowPos(ImVec2(ix, btnY));
|
||||
ImGui::SetNextWindowSize(ImVec2(w - pad * 2, btnH + schema::UI().drawElement("tabs.send", "error-btn-area-padding").size));
|
||||
ImGui::SetNextWindowSize(ImVec2(w - pad * 2, btnH + schema::UI().drawElement("tabs.send", "error-btn-area-padding").size * dp));
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
@@ -576,7 +610,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-bg-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-hover-alpha").size)));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "error-btn-rounding").size);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "error-btn-rounding").size * dp);
|
||||
if (TactileSmallButton(TR("send_copy_error"), schema::UI().resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(s_tx_status.c_str());
|
||||
Notifications::instance().info(TR("send_error_copied"));
|
||||
@@ -599,8 +633,8 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
}
|
||||
|
||||
// ---- SENDING / SUCCESS: inline progress card ----
|
||||
float progCardH = schema::UI().drawElement("tabs.send", "progress-card-height").size;
|
||||
float progCardHTxid = schema::UI().drawElement("tabs.send", "progress-card-height-txid").size;
|
||||
float progCardH = schema::UI().drawElement("tabs.send", "progress-card-height").size * dp;
|
||||
float progCardHTxid = schema::UI().drawElement("tabs.send", "progress-card-height-txid").size * dp;
|
||||
float progH = s_result_txid.empty() ? progCardH : progCardHTxid;
|
||||
ImVec2 pMin(x, y);
|
||||
ImVec2 pMax(x + w, y + progH);
|
||||
@@ -609,8 +643,8 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
progGlass.rounding = Layout::glassRounding() * schema::UI().drawElement("tabs.send", "progress-glass-rounding-ratio").size;
|
||||
DrawGlassPanel(dl, pMin, pMax, progGlass);
|
||||
|
||||
float progPadX = schema::UI().drawElement("tabs.send", "progress-card-pad-x").size;
|
||||
float progPadY = schema::UI().drawElement("tabs.send", "progress-card-pad-y").size;
|
||||
float progPadX = schema::UI().drawElement("tabs.send", "progress-card-pad-x").size * dp;
|
||||
float progPadY = schema::UI().drawElement("tabs.send", "progress-card-pad-y").size * dp;
|
||||
float ix = pMin.x + progPadX;
|
||||
float iy = pMin.y + progPadY;
|
||||
char buf[128];
|
||||
@@ -624,7 +658,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
ImVec2(ix, iy), Primary(), spinIcon);
|
||||
double elapsed = ImGui::GetTime() - s_send_start_time;
|
||||
snprintf(buf, sizeof(buf), TR("send_submitting"), elapsed);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), OnSurface(), buf);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size * dp, iy), OnSurface(), buf);
|
||||
} else {
|
||||
// Success checkmark
|
||||
ImFont* iconFont = material::Type().iconMed();
|
||||
@@ -632,19 +666,19 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, checkIcon);
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(ix, iy), Success(), checkIcon);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), Success(), TR("send_tx_sent"));
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size * dp, iy), Success(), TR("send_tx_sent"));
|
||||
|
||||
if (!s_result_txid.empty()) {
|
||||
float txY = iy + body2->LegacySize + schema::UI().drawElement("tabs.send", "txid-y-offset").size;
|
||||
float txY = iy + body2->LegacySize + schema::UI().drawElement("tabs.send", "txid-y-offset").size * dp;
|
||||
int txidThreshold = (int)schema::UI().drawElement("tabs.send", "txid-display-threshold").size;
|
||||
int txidTruncLen = (int)schema::UI().drawElement("tabs.send", "txid-trunc-len").size;
|
||||
std::string dispTxid = (int)s_result_txid.length() > txidThreshold
|
||||
? s_result_txid.substr(0, txidTruncLen) + "..." + s_result_txid.substr(s_result_txid.length() - txidTruncLen)
|
||||
: s_result_txid;
|
||||
snprintf(buf, sizeof(buf), TR("send_txid_label"), dispTxid.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "txid-label-x-offset").size, txY),
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "txid-label-x-offset").size * dp, txY),
|
||||
OnSurfaceDisabled(), buf);
|
||||
ImGui::SetCursorScreenPos(ImVec2(pMax.x - schema::UI().drawElement("tabs.send", "txid-copy-btn-right-offset").size, txY - schema::UI().drawElement("tabs.send", "txid-copy-btn-y-offset").size));
|
||||
ImGui::SetCursorScreenPos(ImVec2(pMax.x - schema::UI().drawElement("tabs.send", "txid-copy-btn-right-offset").size * dp, txY - schema::UI().drawElement("tabs.send", "txid-copy-btn-y-offset").size * dp));
|
||||
if (TactileSmallButton(TR("copy"), schema::UI().resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(s_result_txid.c_str());
|
||||
Notifications::instance().info(TR("send_txid_copied"));
|
||||
@@ -696,6 +730,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
float popupAvailW = ImGui::GetMainViewport()->Size.x * S.drawElement("tabs.send", "confirm-popup-width-ratio").size;
|
||||
float popupW = std::min(schema::UI().drawElement("tabs.send", "confirm-popup-max-width").size, popupAvailW);
|
||||
float popVs = Layout::vScale();
|
||||
const float dp = Layout::dpiScale();
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("confirm_send"); ov.p_open = nullptr;
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
@@ -767,7 +802,12 @@ void RenderSendConfirmPopup(App* app) {
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_details"));
|
||||
ImVec2 cMin = ImGui::GetCursorScreenPos();
|
||||
// Floor the row step at the *actual* scaled caption-row height (+ a small dp-scaled
|
||||
// gap) so the Fee/Total divider — drawn at rowStep*0.5 below the Fee baseline —
|
||||
// clears the (already-DPI-scaled) fee text instead of striking through it at HiDPI.
|
||||
// capFont->LegacySize is already DPI-scaled; do not scale it again.
|
||||
float rowStep = std::max(schema::UI().drawElement("tabs.send", "confirm-row-step-min").size, schema::UI().drawElement("tabs.send", "confirm-row-step").size * popVs);
|
||||
rowStep = std::max(rowStep, capFont->LegacySize + 6.0f * dp);
|
||||
float configuredH = std::max(schema::UI().drawElement("tabs.send", "confirm-amount-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-amount-card-height").size * popVs);
|
||||
float contentH = Layout::spacingMd() * 2.0f + capFont->LegacySize * 2.0f + sub1->LegacySize + rowStep * 2.0f;
|
||||
float cH = std::max(configuredH, contentH);
|
||||
@@ -822,7 +862,17 @@ void RenderSendConfirmPopup(App* app) {
|
||||
if (s_sending) {
|
||||
Type().text(TypeStyle::Body2, TR("sending"));
|
||||
} else {
|
||||
if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width * Layout::dpiScale(), std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) {
|
||||
// Size to max(schema width, measured label + padding) so a longer translation (e.g. the
|
||||
// Russian "Подтвердить и отправить") isn't clipped on this pre-broadcast confirm button.
|
||||
ImFont* confirmFont = S.resolveFont(S.button("tabs.send", "confirm-button").font);
|
||||
if (!confirmFont) confirmFont = Type().button();
|
||||
const float confirmW = std::max(
|
||||
S.button("tabs.send", "confirm-button").width * Layout::dpiScale(),
|
||||
confirmFont->CalcTextSizeA(confirmFont->LegacySize, FLT_MAX, 0, TR("confirm_and_send")).x
|
||||
+ ImGui::GetStyle().FramePadding.x * 2.0f + 16.0f * Layout::dpiScale());
|
||||
const float confirmH = std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size,
|
||||
schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs);
|
||||
if (TactileButton(TR("confirm_and_send"), ImVec2(confirmW, confirmH), confirmFont)) {
|
||||
// Re-validate against LIVE state — the confirm dialog persists across frames, so the
|
||||
// balance could have dropped or sync (re)started (or the fee bumped total over available)
|
||||
// since Review. Don't broadcast a now-invalid transaction.
|
||||
@@ -896,9 +946,10 @@ static bool RenderZeroBalanceCTA(App* app, ImDrawList* dl, float width) {
|
||||
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
ImFont* capFont = Type().caption();
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
ImVec2 ctaMin = ImGui::GetCursorScreenPos();
|
||||
float ctaH = schema::UI().drawElement("tabs.send", "cta-height").size;
|
||||
float ctaH = schema::UI().drawElement("tabs.send", "cta-height").size * dp;
|
||||
ImVec2 ctaMax(ctaMin.x + width, ctaMin.y + ctaH);
|
||||
GlassPanelSpec ctaGlass;
|
||||
ctaGlass.rounding = Layout::glassRounding();
|
||||
@@ -912,7 +963,7 @@ static bool RenderZeroBalanceCTA(App* app, ImDrawList* dl, float width) {
|
||||
TR("send_switch_to_receive"));
|
||||
cy += capFont->LegacySize + Layout::spacingMd();
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
if (TactileButton(TR("send_go_to_receive"), ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size, schema::UI().drawElement("tabs.send", "cta-button-height").size), schema::UI().resolveFont("button"))) {
|
||||
if (TactileButton(TR("send_go_to_receive"), ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size * dp, schema::UI().drawElement("tabs.send", "cta-button-height").size * dp), schema::UI().resolveFont("button"))) {
|
||||
app->setCurrentPage(NavPage::Receive);
|
||||
}
|
||||
ImGui::SetCursorScreenPos(ImVec2(ctaMin.x, ctaMax.y + Layout::spacingLg()));
|
||||
@@ -927,6 +978,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
bool is_valid_address, double available,
|
||||
const char* suffix = "") {
|
||||
auto& S = schema::UI();
|
||||
const float dp = Layout::dpiScale();
|
||||
const auto& state = app->getWalletState();
|
||||
double total = s_amount + s_fee;
|
||||
// Block spending from a view-only source (imported viewing key, no spending key) — it would only
|
||||
@@ -962,7 +1014,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
|
||||
ImGui::BeginDisabled(!can_send);
|
||||
char sendId[64];
|
||||
snprintf(sendId, sizeof(sendId), "Review Send%s", suffix);
|
||||
snprintf(sendId, sizeof(sendId), "%s##ReviewSend%s", TR("review_send"), suffix);
|
||||
if (TactileButton(sendId, ImVec2(sendBtnW, btnH), S.resolveFont(S.button("tabs.send", "send-button").font))) {
|
||||
s_show_confirm = true;
|
||||
}
|
||||
@@ -982,7 +1034,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
else if (total > available)
|
||||
material::Tooltip("%s", TR("send_tooltip_exceeds_balance"));
|
||||
else if (!sourceSpendable)
|
||||
material::Tooltip("%s", "View-only address — no spending key, cannot send");
|
||||
material::Tooltip("%s", TR("send_tooltip_view_only"));
|
||||
else if (s_sending)
|
||||
material::Tooltip("%s", TR("send_tooltip_in_progress"));
|
||||
}
|
||||
@@ -995,7 +1047,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.send", "cancel-btn-border-size").size);
|
||||
char clearId[64];
|
||||
snprintf(clearId, sizeof(clearId), "Cancel%s", suffix);
|
||||
snprintf(clearId, sizeof(clearId), "%s##Cancel%s", TR("cancel"), suffix);
|
||||
if (TactileButton(clearId, ImVec2(cancelBtnW, btnH), S.resolveFont(S.button("tabs.send", "clear-button").font))) {
|
||||
if (FormHasData()) {
|
||||
s_clear_confirm_pending = true;
|
||||
@@ -1016,12 +1068,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
if (ImGui::BeginPopup(confirmClearId)) {
|
||||
ImGui::Text("%s", TR("send_clear_fields"));
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
|
||||
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size * dp, 0), S.resolveFont("button"))) {
|
||||
ClearFormWithUndo();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
|
||||
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size * dp, 0), S.resolveFont("button"))) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
@@ -1037,7 +1089,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-bg-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
|
||||
char undoId[32];
|
||||
snprintf(undoId, sizeof(undoId), "Undo Clear%s", suffix);
|
||||
snprintf(undoId, sizeof(undoId), "%s%s", TR("grpb_undo_clear"), suffix);
|
||||
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
|
||||
RestoreFormSnapshot();
|
||||
Notifications::instance().info(TR("send_form_restored"));
|
||||
@@ -1068,15 +1120,25 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
|
||||
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||
ImU32 sendCol = Error();
|
||||
|
||||
// Collect matching transactions
|
||||
// Grow the list to fill the dead space beneath the (fixed-height) compose card:
|
||||
// fit as many newest-first rows as the remaining region can show, instead of a
|
||||
// fixed 4. The child scrolls if the real history exceeds what fits, so nothing is
|
||||
// lost. A floor of 4 keeps the section substantial when the region is short.
|
||||
// Row budget is hoisted above the collect loop (it only needs the remaining region
|
||||
// height + row height, no per-row state) so the scan can early-exit.
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
size_t maxRows = std::max<size_t>(4, (size_t)std::floor(listH / rowH));
|
||||
|
||||
// Collect matching transactions. state.transactions is newest-first, so scan only a
|
||||
// bounded prefix and stop once maxRows matches are gathered (mirrors the early-exit in
|
||||
// balance_components.cpp:RenderSharedRecentTx) instead of filtering the whole history.
|
||||
std::vector<const TransactionInfo*> sends;
|
||||
sends.reserve(maxRows);
|
||||
for (const auto& tx : state.transactions) {
|
||||
if (tx.type != "send" && tx.type != "shield") continue;
|
||||
sends.push_back(&tx);
|
||||
if (sends.size() == maxRows) break; // newest-first: enough rows to fill the region
|
||||
}
|
||||
if (sends.size() > 4) sends.resize(4); // show only the 4 most recent (newest-first)
|
||||
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
|
||||
ImGui::BeginChild("##RecentSendRows", ImVec2(width, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
@@ -1084,8 +1146,9 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
|
||||
|
||||
char buf[64];
|
||||
if (sends.empty()) {
|
||||
ImGui::SetCursorPosY(Layout::spacingMd());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("send_no_recent"));
|
||||
// Fill the empty canvas with a centered material empty-state (icon + title)
|
||||
// rather than a lone left-aligned caption stranded at the top of dead space.
|
||||
material::DrawEmptyState(ICON_MD_CALL_MADE, TR("send_no_recent"));
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
@@ -1105,7 +1168,7 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
|
||||
ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), TR("sent_type"));
|
||||
|
||||
// Address (second line)
|
||||
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs;
|
||||
std::string addrDisplay = util::truncateMiddle(tx.address,
|
||||
(int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
@@ -1124,7 +1187,7 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
|
||||
std::string ago = timeAgo(tx.timestamp);
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f),
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs,
|
||||
rowPos.y + 2.0f * dp),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
@@ -1202,8 +1265,12 @@ void RenderSendTab(App* app)
|
||||
// SCROLLABLE CONTENT
|
||||
// ================================================================
|
||||
ImVec2 formAvail = ImGui::GetContentRegionAvail();
|
||||
// NOTE: no NoScrollbar/NoScrollWithMouse here (mirrors receive_tab's ##ReceiveScroll).
|
||||
// At font_scale 1.5 the form card grows to mainCardTargetH and pushes the appended
|
||||
// "Recent Sends" list below the fold; letting this child scroll keeps it reachable.
|
||||
// No-op at 1.0x where the content already fits (no scrollbar appears).
|
||||
ImGui::BeginChild("##SendFormScroll", formAvail, false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// Top-aligned content — consistent vertical position across all tabs
|
||||
@@ -1213,6 +1280,13 @@ void RenderSendTab(App* app)
|
||||
float contentStartY = ImGui::GetCursorPosY();
|
||||
|
||||
float formAvailW = ImGui::GetContentRegionAvail().x;
|
||||
// Fill the available column up to the content-max-width cap, then center. Shared with the
|
||||
// Receive tab via mainComposeCardBox() so the two card envelopes are identical in width and
|
||||
// position (Send previously capped at 760dp vs Receive's 860dp, so Send rendered narrower).
|
||||
// The recent-sends list below deliberately keeps the full column width (formAvailW).
|
||||
Layout::CardBox formBox = Layout::mainComposeCardBox(formAvailW);
|
||||
float formCardW = formBox.width;
|
||||
float formOffsetX = formBox.offsetX;
|
||||
float formW = formAvailW;
|
||||
ImGui::BeginGroup();
|
||||
|
||||
@@ -1227,7 +1301,15 @@ void RenderSendTab(App* app)
|
||||
// ================================================================
|
||||
// COMPOSE FORM — single container for all fields
|
||||
// ================================================================
|
||||
// Full-column left edge, restored after the centered card so the recent-sends
|
||||
// list below spans the full width again.
|
||||
float formLeftX = ImGui::GetCursorPosX();
|
||||
{
|
||||
// Center the capped compose card: offset the cursor by the leftover half-margin,
|
||||
// then derive every field/divider/button from the card width (not the full column).
|
||||
float formW = formCardW;
|
||||
if (formOffsetX > 0.0f)
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + formOffsetX);
|
||||
ImVec2 containerMin = ImGui::GetCursorScreenPos();
|
||||
float pad = Layout::spacingLg();
|
||||
float innerW = formW - pad * 2;
|
||||
@@ -1240,8 +1322,10 @@ void RenderSendTab(App* app)
|
||||
dl->ChannelsSplit(2);
|
||||
dl->ChannelsSetCurrent(1);
|
||||
|
||||
// Indent content by pad so every line is inset from the card edges
|
||||
ImGui::Indent(pad);
|
||||
// Indent content by pad so every line is inset from the card edges. Fold in formOffsetX so the
|
||||
// auto-layout fields land inside the centered card (Indent() positions from the window's left, so
|
||||
// without this the fields would sit at the un-offset column while the glass card is centered).
|
||||
ImGui::Indent(pad + formOffsetX);
|
||||
ImGui::Dummy(ImVec2(0, pad * vScale)); // top padding
|
||||
|
||||
// ---- SOURCE ADDRESS ----
|
||||
@@ -1292,7 +1376,10 @@ void RenderSendTab(App* app)
|
||||
|
||||
float pasteW = std::max(schema::UI().drawElement("tabs.send", "paste-btn-min-width").size, colW * schema::UI().drawElement("tabs.send", "paste-btn-width-ratio").size);
|
||||
float contactsW = ImGui::GetFrameHeight(); // compact square icon button for the contact picker
|
||||
ImGui::PushItemWidth(colW - pasteW - contactsW - Layout::spacingSm() * 2.0f);
|
||||
// Reserve the TWO real SameLine gaps (each = ItemSpacing.x) between input|Paste|icon.
|
||||
// Reserving spacingSm (a smaller token) under-counted the gap, so the row overshot colW by
|
||||
// ~2*(ItemSpacing.x - spacingSm) and the icon's right border clipped past the card edge.
|
||||
ImGui::PushItemWidth(colW - pasteW - contactsW - ImGui::GetStyle().ItemSpacing.x * 2.0f);
|
||||
|
||||
// Show clipboard preview as transparent overlay when paste button is hovered
|
||||
bool paste_hovered = false;
|
||||
@@ -1339,7 +1426,7 @@ void RenderSendTab(App* app)
|
||||
s_preview_text.c_str(), s_preview_text.c_str() + std::min(s_preview_text.size(), (size_t)S.drawElement("tabs.send", "paste-preview-max-chars").size));
|
||||
}
|
||||
|
||||
if (TactileButton("Paste##to", ImVec2(pasteW, 0), S.resolveFont(S.button("tabs.send", "paste-button").font))) {
|
||||
if (TactileButton((std::string(TR("paste")) + "##to").c_str(), ImVec2(pasteW, 0), S.resolveFont(S.button("tabs.send", "paste-button").font))) {
|
||||
if (s_paste_previewing) {
|
||||
// Commit the preview
|
||||
snprintf(s_to_address, sizeof(s_to_address), "%s", s_preview_text.c_str());
|
||||
@@ -1350,9 +1437,12 @@ void RenderSendTab(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// Contact picker — pick a saved contact's address as the recipient.
|
||||
// Contact picker — pick a saved contact's address as the recipient. Pin the height to the
|
||||
// frame height (== the input + Paste height) so the larger iconMed font doesn't auto-size
|
||||
// this square button taller than its row-mates. (Passing a non-zero size also routes
|
||||
// TactileButton through its precise InvisibleButton + centered-glyph path.)
|
||||
ImGui::SameLine();
|
||||
if (material::TactileButton(ICON_MD_CONTACTS "##pickContact", ImVec2(contactsW, 0),
|
||||
if (material::TactileButton(ICON_MD_CONTACTS "##pickContact", ImVec2(contactsW, ImGui::GetFrameHeight()),
|
||||
material::Type().iconMed()))
|
||||
ImGui::OpenPopup("##ContactPickerPopup");
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("send_contacts_button"));
|
||||
@@ -1379,7 +1469,7 @@ void RenderSendTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Toggle between DRGX and USD input
|
||||
float toggleW = schema::UI().drawElement("tabs.send", "toggle-currency-width").size;
|
||||
float toggleW = schema::UI().drawElement("tabs.send", "toggle-currency-width").size * Layout::dpiScale();
|
||||
float amtInputW = colW - toggleW - Layout::spacingMd();
|
||||
if (amtInputW < schema::UI().drawElement("tabs.send", "amount-input-min-width").size) amtInputW = schema::UI().drawElement("tabs.send", "amount-input-min-width").size;
|
||||
|
||||
@@ -1452,8 +1542,8 @@ void RenderSendTab(App* app)
|
||||
float bH = bMax.y - bMin.y;
|
||||
ImFont* font = ImGui::GetFont();
|
||||
ImVec2 textSz = font->CalcTextSizeA(font->LegacySize, 10000, 0, currLabel);
|
||||
float iconW = schema::UI().drawElement("tabs.send", "swap-icon-width").size;
|
||||
float iconGap = schema::UI().drawElement("tabs.send", "swap-icon-gap").size;
|
||||
float iconW = schema::UI().drawElement("tabs.send", "swap-icon-width").size * Layout::dpiScale();
|
||||
float iconGap = schema::UI().drawElement("tabs.send", "swap-icon-gap").size * Layout::dpiScale();
|
||||
float totalW = iconW + iconGap + textSz.x;
|
||||
float startX = bMin.x + ((bMax.x - bMin.x) - totalW) * 0.5f;
|
||||
float cy = bMin.y + bH * 0.5f;
|
||||
@@ -1509,7 +1599,7 @@ void RenderSendTab(App* app)
|
||||
size_t memo_len = strlen(s_memo);
|
||||
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
|
||||
bool memoAtCap = memo_len + 1 >= memoMax;
|
||||
snprintf(buf, sizeof(buf), "%zu / %zu bytes", memo_len, memoMax);
|
||||
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
|
||||
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
@@ -1540,7 +1630,7 @@ void RenderSendTab(App* app)
|
||||
|
||||
// Add bottom padding
|
||||
ImGui::Dummy(ImVec2(0, pad * vScale));
|
||||
ImGui::Unindent(pad);
|
||||
ImGui::Unindent(pad + formOffsetX);
|
||||
|
||||
// Enforce shared card height (matches receive tab)
|
||||
{
|
||||
@@ -1573,7 +1663,8 @@ void RenderSendTab(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- RECENT SENDS ----
|
||||
// ---- RECENT SENDS ---- (full column width; reset X off the centered card)
|
||||
ImGui::SetCursorPosX(formLeftX);
|
||||
RenderRecentSends(state, formW, capFont, app);
|
||||
|
||||
ImGui::EndGroup();
|
||||
|
||||
@@ -216,7 +216,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
if (!skin.valid) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::BeginDisabled(true);
|
||||
std::string label = skin.name + " (invalid)";
|
||||
std::string label = skin.name + TR("swin_invalid_suffix");
|
||||
ImGui::Selectable(label.c_str(), false);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor();
|
||||
@@ -261,7 +261,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
|
||||
skinMgr.refresh();
|
||||
Notifications::instance().info("Theme list refreshed");
|
||||
Notifications::instance().info(TR("swin_theme_list_refreshed"));
|
||||
}
|
||||
ImGui::PopFont();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
@@ -425,14 +425,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
|
||||
if (error.empty()) {
|
||||
std::string version = result.value("version", "unknown");
|
||||
std::string msg = "Connection successful!\ndragonxd version: " + version;
|
||||
std::string msg = std::string(TR("swin_connection_successful")) + version;
|
||||
Notifications::instance().success(msg);
|
||||
} else {
|
||||
Notifications::instance().error("Connection failed: " + error);
|
||||
Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Notifications::instance().error("RPC client not initialized");
|
||||
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,15 +455,15 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
if (error.empty()) {
|
||||
int start = result.value("start_height", 0);
|
||||
int end = result.value("stop_height", 0);
|
||||
std::string msg = "Rescan started from block " + std::to_string(start) +
|
||||
" to " + std::to_string(end);
|
||||
std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
|
||||
TR("swin_rescan_to") + std::to_string(end);
|
||||
Notifications::instance().success(msg);
|
||||
} else {
|
||||
Notifications::instance().error("Rescan failed: " + error);
|
||||
Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Notifications::instance().error("RPC client not initialized");
|
||||
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
|
||||
@@ -478,7 +478,12 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
|
||||
// Confirmation dialog
|
||||
if (s_confirm_clear_ztx) {
|
||||
if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f, 0.94f)) {
|
||||
// Distinct idSuffix: this confirm renders nested inside (and the same frame as) the parent
|
||||
// settings dialog, so it must not share the default ##OverlayDialogContent key — otherwise it
|
||||
// inherits the parent's OverlayCardState (incl. the sticky overflow flag) and collides on the
|
||||
// child window id.
|
||||
if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f,
|
||||
0.94f, 0.85f, "settings_clearztx")) {
|
||||
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f));
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -498,9 +503,9 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
if (doConfirm) {
|
||||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
||||
if (util::Platform::deleteFile(ztx_file)) {
|
||||
Notifications::instance().success("Z-transaction history cleared");
|
||||
Notifications::instance().success(TR("swin_ztx_history_cleared"));
|
||||
} else {
|
||||
Notifications::instance().info("No history file found");
|
||||
Notifications::instance().info(TR("swin_no_history_file_found"));
|
||||
}
|
||||
s_confirm_clear_ztx = false;
|
||||
}
|
||||
@@ -563,7 +568,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
// Save/Cancel buttons
|
||||
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
|
||||
saveSettingsFromUI(app->settings());
|
||||
Notifications::instance().success("Settings saved");
|
||||
Notifications::instance().success(TR("swin_settings_saved"));
|
||||
*p_open = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
+414
-252
@@ -5,6 +5,7 @@
|
||||
#include "shield_dialog.h"
|
||||
#include "../../app.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../../data/wallet_state.h"
|
||||
#include "../../rpc/rpc_client.h"
|
||||
#include "../../rpc/rpc_worker.h"
|
||||
#include "../../util/i18n.h"
|
||||
@@ -15,39 +16,98 @@
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
// Static state
|
||||
static bool s_open = false;
|
||||
// ── Static dialog state ─────────────────────────────────────────────────────────────────────────
|
||||
static bool s_open = false;
|
||||
static ShieldDialog::Mode s_mode = ShieldDialog::Mode::ShieldCoinbase;
|
||||
static char s_from_address[512] = "*";
|
||||
static char s_to_address[512] = "";
|
||||
static bool s_consolidate = false; // opened from the wallet-bloat nudge (shielded preset + framing)
|
||||
static int s_src = 2; // merge source: 0 = transparent, 1 = shielded, 2 = both
|
||||
static char s_from_address[512] = "*";
|
||||
static char s_to_address[512] = "";
|
||||
static int s_selected_zaddr_idx = -1;
|
||||
static double s_fee = DRAGONX_DEFAULT_FEE;
|
||||
static int s_utxo_limit = 50; // overridden by schema at runtime
|
||||
static bool s_operation_pending = false;
|
||||
static int s_utxo_limit = 50; // overridden by schema at runtime
|
||||
static bool s_advanced = false; // Advanced (fee + batch size) disclosure
|
||||
static bool s_confirm = false; // inline "confirm before moving funds" phase
|
||||
static bool s_operation_pending = false;
|
||||
static bool s_op_terminal = false; // async op reached success/failed — freeze inputs
|
||||
static std::string s_operation_id;
|
||||
static std::string s_status_message;
|
||||
static int s_selected_zaddr_idx = -1;
|
||||
static double s_last_poll = 0.0; // live-progress self-poll timer (ImGui::GetTime seconds)
|
||||
// Scope of what can be consolidated (fetched once on open, merge mode only).
|
||||
static bool s_scope_loading = false;
|
||||
static bool s_scope_loaded = false;
|
||||
static int s_t_count = 0, s_z_count = 0;
|
||||
static double s_t_amount = 0.0, s_z_amount = 0.0;
|
||||
static bool s_creating_addr = false; // z_getnewaddress in flight (empty state)
|
||||
|
||||
static void resetTransient()
|
||||
{
|
||||
s_operation_pending = false;
|
||||
s_op_terminal = false;
|
||||
s_confirm = false;
|
||||
s_status_message.clear();
|
||||
s_operation_id.clear();
|
||||
s_creating_addr = false;
|
||||
}
|
||||
|
||||
// Count + sum spendable transparent UTXOs and shielded notes so the user can see the scope of a
|
||||
// consolidation (and how many batches it may take). Read-only; runs off the UI thread.
|
||||
static void loadScope(App* app)
|
||||
{
|
||||
if (!app || !app->worker()) return;
|
||||
s_scope_loading = true; s_scope_loaded = false;
|
||||
app->worker()->post([app, rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
||||
int tC = 0, zC = 0; double tA = 0.0, zA = 0.0; std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Shield dialog / Scope count");
|
||||
nlohmann::json us = rpc->call("listunspent", nlohmann::json::array({0}));
|
||||
if (us.is_array()) for (const auto& u : us) {
|
||||
if (u.value("confirmations", 0) >= 1 && u.value("spendable", true)) { ++tC; tA += u.value("amount", 0.0); }
|
||||
}
|
||||
nlohmann::json zs = rpc->call("z_listunspent", nlohmann::json::array({0}));
|
||||
if (zs.is_array()) for (const auto& z : zs) {
|
||||
if (z.value("confirmations", 0) >= 1) { ++zC; zA += z.value("amount", 0.0); }
|
||||
}
|
||||
} catch (const std::exception& e) { error = e.what(); }
|
||||
return [tC, zC, tA, zA, error]() {
|
||||
s_scope_loading = false; s_scope_loaded = error.empty();
|
||||
s_t_count = tC; s_z_count = zC; s_t_amount = tA; s_z_amount = zA;
|
||||
// Clamp the source to what actually has inputs (unless the user is mid-op).
|
||||
const bool tOk = tC > 0, zOk = zC > 0;
|
||||
if (!s_operation_pending) {
|
||||
if (s_consolidate && zOk) s_src = 1; // bloat nudge → shielded
|
||||
else if (s_src == 0 && !tOk) s_src = zOk ? 1 : 2;
|
||||
else if (s_src == 1 && !zOk) s_src = tOk ? 0 : 2;
|
||||
else if (!tOk && zOk) s_src = 1;
|
||||
else if (tOk && !zOk) s_src = 0;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void ShieldDialog::show(Mode mode)
|
||||
{
|
||||
s_mode = mode;
|
||||
s_open = true;
|
||||
s_operation_pending = false;
|
||||
s_status_message.clear();
|
||||
s_operation_id.clear();
|
||||
|
||||
if (mode == Mode::ShieldCoinbase) {
|
||||
strncpy(s_from_address, "*", sizeof(s_from_address));
|
||||
} else {
|
||||
s_from_address[0] = '\0';
|
||||
}
|
||||
s_consolidate = false; // reset preset flags so stale statics don't leak across opens
|
||||
s_src = 2;
|
||||
resetTransient();
|
||||
s_from_address[0] = '\0';
|
||||
if (mode == Mode::ShieldCoinbase) strncpy(s_from_address, "*", sizeof(s_from_address));
|
||||
s_to_address[0] = '\0';
|
||||
s_selected_zaddr_idx = -1;
|
||||
s_fee = DRAGONX_DEFAULT_FEE;
|
||||
s_utxo_limit = (int)schema::UI().drawElement("business", "utxo-limit").size;
|
||||
s_selected_zaddr_idx = -1;
|
||||
if (s_utxo_limit < 1) s_utxo_limit = 50;
|
||||
s_advanced = false;
|
||||
s_scope_loaded = false; s_scope_loading = false;
|
||||
s_t_count = s_z_count = 0; s_t_amount = s_z_amount = 0.0;
|
||||
s_last_poll = 0.0;
|
||||
}
|
||||
|
||||
void ShieldDialog::showShieldCoinbase(const std::string& fromAddress)
|
||||
@@ -59,14 +119,146 @@ void ShieldDialog::showShieldCoinbase(const std::string& fromAddress)
|
||||
void ShieldDialog::showMerge()
|
||||
{
|
||||
show(Mode::MergeToAddress);
|
||||
s_consolidate = false;
|
||||
s_src = 2; // generic merge: both sources
|
||||
}
|
||||
|
||||
void ShieldDialog::showConsolidate()
|
||||
{
|
||||
show(Mode::MergeToAddress);
|
||||
s_consolidate = true;
|
||||
s_src = 1; // wallet-bloat consolidation targets shielded notes (witness bloat)
|
||||
}
|
||||
|
||||
void ShieldDialog::hide()
|
||||
{
|
||||
s_open = false;
|
||||
s_operation_pending = false;
|
||||
s_status_message.clear();
|
||||
s_operation_id.clear();
|
||||
resetTransient();
|
||||
}
|
||||
|
||||
// Relevant count/amount for the currently-selected merge source.
|
||||
static int srcCount() { return s_src == 0 ? s_t_count : s_src == 1 ? s_z_count : (s_t_count + s_z_count); }
|
||||
static double srcAmount() { return s_src == 0 ? s_t_amount : s_src == 1 ? s_z_amount : (s_t_amount + s_z_amount); }
|
||||
|
||||
static std::string fmtAmt(double v) { char b[48]; std::snprintf(b, sizeof(b), "%.4f", v); return b; }
|
||||
|
||||
static std::string shortAddr(const std::string& a)
|
||||
{
|
||||
if (a.size() <= 20) return a;
|
||||
return a.substr(0, 10) + "…" + a.substr(a.size() - 8);
|
||||
}
|
||||
|
||||
// Auto-pick the best spendable z-address as the default destination (fewest hops for the user).
|
||||
static void autoSelectDestination(const WalletState& state)
|
||||
{
|
||||
if (s_to_address[0] != '\0' || state.z_addresses.empty()) return;
|
||||
int idx = bestSpendableAddressIndex(state.z_addresses);
|
||||
if (idx < 0) idx = 0;
|
||||
s_selected_zaddr_idx = idx;
|
||||
strncpy(s_to_address, state.z_addresses[idx].address.c_str(), sizeof(s_to_address) - 1);
|
||||
}
|
||||
|
||||
// Fire the actual shield/merge op. Registers the opid with the shared poller (for balance refresh)
|
||||
// AND kicks the modal's own live-progress poll.
|
||||
static void submitOperation(App* app)
|
||||
{
|
||||
s_operation_pending = true;
|
||||
s_op_terminal = false;
|
||||
s_status_message = TR("shield_submitting");
|
||||
s_last_poll = ImGui::GetTime();
|
||||
|
||||
if (s_mode == ShieldDialog::Mode::ShieldCoinbase) {
|
||||
std::string from(s_from_address), to(s_to_address);
|
||||
double fee = s_fee; int limit = s_utxo_limit;
|
||||
if (!app->worker()) return;
|
||||
app->worker()->post([app, rpc = app->rpc(), from, to, fee, limit]() -> rpc::RPCWorker::MainCb {
|
||||
nlohmann::json result; std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Shield dialog / Shield coinbase");
|
||||
result = rpc->call("z_shieldcoinbase", {from, to, fee, limit});
|
||||
} catch (const std::exception& e) { error = e.what(); }
|
||||
return [app, result, error]() {
|
||||
if (error.empty()) {
|
||||
s_operation_id = result.value("opid", "");
|
||||
s_status_message = TR("merge_progress");
|
||||
Notifications::instance().success(TR("shield_started"));
|
||||
app->trackOperation(s_operation_id);
|
||||
} else {
|
||||
s_operation_pending = false; s_op_terminal = true;
|
||||
s_status_message = std::string(TR("shield_error_prefix")) + error;
|
||||
Notifications::instance().error(std::string(TR("shield_send_failed")) + error);
|
||||
}
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge / consolidate. Source → z_mergetoaddress fromaddress selector (this is the fix: shielded
|
||||
// notes, not just transparent UTXOs — the wallet-bloat the nudge warns about is shielded witnesses).
|
||||
std::vector<std::string> fromAddrs;
|
||||
if (s_src == 0) fromAddrs = { "ANY_TADDR" };
|
||||
else if (s_src == 1) fromAddrs = { "ANY_SAPLING" };
|
||||
else fromAddrs = { "*" };
|
||||
std::string to(s_to_address);
|
||||
double fee = s_fee; int limit = s_utxo_limit;
|
||||
if (!app->worker()) return;
|
||||
app->worker()->post([app, rpc = app->rpc(), fromAddrs, to, fee, limit]() -> rpc::RPCWorker::MainCb {
|
||||
nlohmann::json addrs = nlohmann::json::array();
|
||||
for (const auto& a : fromAddrs) addrs.push_back(a);
|
||||
nlohmann::json result; std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Shield dialog / Consolidate");
|
||||
// fromaddrs, toaddr, fee, transparent_limit, shielded_limit — cap both to the batch size.
|
||||
result = rpc->call("z_mergetoaddress", {addrs, to, fee, limit, limit});
|
||||
} catch (const std::exception& e) { error = e.what(); }
|
||||
return [app, result, error]() {
|
||||
if (error.empty()) {
|
||||
s_operation_id = result.value("opid", "");
|
||||
s_status_message = TR("merge_progress");
|
||||
Notifications::instance().success(TR("merge_started"));
|
||||
app->trackOperation(s_operation_id);
|
||||
} else {
|
||||
s_operation_pending = false; s_op_terminal = true;
|
||||
s_status_message = std::string(TR("shield_error_prefix")) + error;
|
||||
Notifications::instance().error(std::string(TR("merge_send_failed")) + error);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Live-progress self-poll: while an op is in flight, poll z_getoperationstatus every ~2s so the modal
|
||||
// shows "Consolidating… → Done/Failed" without a manual button. (The shared poller also tracks it for
|
||||
// balance refresh; this drives only the inline display.)
|
||||
static void pollOperation(App* app)
|
||||
{
|
||||
if (s_operation_id.empty() || s_op_terminal || !app->worker()) return;
|
||||
const double now = ImGui::GetTime();
|
||||
if (now - s_last_poll < 2.0) return;
|
||||
s_last_poll = now;
|
||||
std::string opid = s_operation_id;
|
||||
app->worker()->post([rpc = app->rpc(), opid]() -> rpc::RPCWorker::MainCb {
|
||||
nlohmann::json result; std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Shield dialog / Op status");
|
||||
result = rpc->call("z_getoperationstatus", {nlohmann::json::array({opid})});
|
||||
} catch (const std::exception& e) { error = e.what(); }
|
||||
return [result, error]() {
|
||||
if (!error.empty() || !result.is_array() || result.empty()) return; // transient — retry next tick
|
||||
const auto& op = result[0];
|
||||
const std::string status = op.value("status", "");
|
||||
if (status == "success") {
|
||||
s_operation_pending = false; s_op_terminal = true;
|
||||
s_status_message = TR("shield_completed");
|
||||
Notifications::instance().success(TR("shield_merge_done"));
|
||||
} else if (status == "failed") {
|
||||
std::string msg = op.value("error", nlohmann::json{}).value("message", std::string(TR("shield_unknown_error")));
|
||||
s_operation_pending = false; s_op_terminal = true;
|
||||
s_status_message = std::string(TR("shield_op_failed")) + msg;
|
||||
Notifications::instance().error(std::string(TR("shield_op_failed")) + msg);
|
||||
}
|
||||
// queued / executing → leave the "Consolidating…" message and keep polling.
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void ShieldDialog::render(App* app)
|
||||
@@ -74,251 +266,221 @@ void ShieldDialog::render(App* app)
|
||||
if (!s_open) return;
|
||||
|
||||
auto& S = schema::UI();
|
||||
auto win = S.window("dialogs.shield");
|
||||
auto addrLbl = S.label("dialogs.shield", "address-label");
|
||||
auto addrFrontLbl = S.label("dialogs.shield", "address-front-label");
|
||||
auto addrBackLbl = S.label("dialogs.shield", "address-back-label");
|
||||
auto feeInput = S.input("dialogs.shield", "fee-input");
|
||||
auto utxoInput = S.input("dialogs.shield", "utxo-limit-input");
|
||||
auto shieldBtn = S.button("dialogs.shield", "shield-button");
|
||||
auto cancelBtn = S.button("dialogs.shield", "cancel-button");
|
||||
auto win = S.window("dialogs.shield");
|
||||
auto addrLbl = S.label("dialogs.shield", "address-label");
|
||||
auto addrFront = S.label("dialogs.shield", "address-front-label");
|
||||
auto addrBack = S.label("dialogs.shield", "address-back-label");
|
||||
auto feeInput = S.input("dialogs.shield", "fee-input");
|
||||
auto utxoInput = S.input("dialogs.shield", "utxo-limit-input");
|
||||
auto shieldBtn = S.button("dialogs.shield", "shield-button");
|
||||
auto cancelBtn = S.button("dialogs.shield", "cancel-button");
|
||||
const float dp = Layout::dpiScale();
|
||||
const bool isMerge = (s_mode == Mode::MergeToAddress);
|
||||
|
||||
const char* title = (s_mode == Mode::ShieldCoinbase)
|
||||
? TR("shield_title")
|
||||
: TR("merge_title");
|
||||
const char* title = s_consolidate ? TR("consolidate_title")
|
||||
: isMerge ? TR("merge_title")
|
||||
: TR("shield_title");
|
||||
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = title; ov.p_open = &s_open;
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = win.width; ov.idSuffix = "shielddialog";
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
const auto& state = app->getWalletState();
|
||||
if (!material::BeginOverlayDialog(ov)) return;
|
||||
|
||||
// Description
|
||||
if (s_mode == Mode::ShieldCoinbase) {
|
||||
ImGui::TextWrapped("%s", TR("shield_description"));
|
||||
} else {
|
||||
ImGui::TextWrapped("%s", TR("merge_description"));
|
||||
const auto& state = app->getWalletState();
|
||||
autoSelectDestination(state);
|
||||
pollOperation(app);
|
||||
if (isMerge && !s_scope_loaded && !s_scope_loading && s_operation_id.empty()) loadScope(app);
|
||||
|
||||
// ── Description ──────────────────────────────────────────────────────────────────────────────
|
||||
ImGui::TextWrapped("%s", s_consolidate ? TR("consolidate_desc")
|
||||
: isMerge ? TR("merge_description")
|
||||
: TR("shield_description"));
|
||||
ImGui::Spacing();
|
||||
|
||||
const bool opInFlight = !s_operation_id.empty(); // submitted — inputs frozen, showing progress
|
||||
|
||||
// ── Merge: scope + source selector ──────────────────────────────────────────────────────────
|
||||
if (isMerge && !opInFlight) {
|
||||
if (s_scope_loading) {
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(),
|
||||
TR("merge_scope_loading"));
|
||||
} else if (s_scope_loaded) {
|
||||
char buf[160];
|
||||
std::snprintf(buf, sizeof(buf), TR("merge_scope_fmt"),
|
||||
s_t_count, s_z_count, fmtAmt(s_t_amount + s_z_amount).c_str());
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), buf);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// From address (for shield coinbase)
|
||||
if (s_mode == Mode::ShieldCoinbase) {
|
||||
material::LabeledInput(TR("shield_from_address"), "##FromAddr", s_from_address, sizeof(s_from_address));
|
||||
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
|
||||
// Source selector — only offer the types that actually have inputs.
|
||||
const bool tOk = s_t_count > 0, zOk = s_z_count > 0;
|
||||
if (tOk && zOk) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(TR("merge_source"));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::RadioButton(TR("merge_src_shielded"), &s_src, 1); ImGui::SameLine();
|
||||
ImGui::RadioButton(TR("merge_src_transparent"), &s_src, 0); ImGui::SameLine();
|
||||
ImGui::RadioButton(TR("merge_src_both"), &s_src, 2);
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// To address (z-address dropdown)
|
||||
ImGui::Text("%s", TR("shield_to_address"));
|
||||
|
||||
// Get z-addresses for dropdown
|
||||
std::string to_display = s_to_address[0] ? s_to_address : TR("shield_select_z");
|
||||
if (to_display.length() > static_cast<size_t>(addrLbl.truncate)) {
|
||||
to_display = to_display.substr(0, addrFrontLbl.truncate) + "..." + to_display.substr(to_display.length() - addrBackLbl.truncate);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::BeginCombo("##ToAddr", to_display.c_str())) {
|
||||
for (size_t i = 0; i < state.z_addresses.size(); i++) {
|
||||
const auto& addr = state.z_addresses[i];
|
||||
std::string label = addr.address;
|
||||
if (label.length() > static_cast<size_t>(addrLbl.truncate)) {
|
||||
label = label.substr(0, addrFrontLbl.truncate) + "..." + label.substr(label.length() - addrBackLbl.truncate);
|
||||
}
|
||||
|
||||
bool selected = (s_selected_zaddr_idx == static_cast<int>(i));
|
||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
s_selected_zaddr_idx = static_cast<int>(i);
|
||||
strncpy(s_to_address, addr.address.c_str(), sizeof(s_to_address) - 1);
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (state.z_addresses.empty()) {
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::Warning(),
|
||||
TR("shield_no_zaddr_hint"));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Fee
|
||||
ImGui::Text("%s", TR("fee_label"));
|
||||
ImGui::SetNextItemWidth(feeInput.width);
|
||||
ImGui::InputDouble("##Fee", &s_fee, 0.0001, 0.001, "%.8f");
|
||||
if (s_fee < 0.0) s_fee = 0.0; // no negative fee
|
||||
if (s_fee > 1.0) s_fee = 1.0; // guard a fat-fingered huge fee (mirrors utxo clamp)
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("DRGX");
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// UTXO limit
|
||||
ImGui::Text("%s", TR("shield_utxo_limit"));
|
||||
ImGui::SetNextItemWidth(utxoInput.width);
|
||||
ImGui::InputInt("##Limit", &s_utxo_limit);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", TR("shield_max_utxos"));
|
||||
if (s_utxo_limit < 1) s_utxo_limit = 1;
|
||||
if (s_utxo_limit > 100) s_utxo_limit = 100;
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Status message
|
||||
if (!s_status_message.empty()) {
|
||||
if (s_operation_pending) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", s_status_message.c_str());
|
||||
} else {
|
||||
ImGui::TextWrapped("%s", s_status_message.c_str());
|
||||
}
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Buttons — guard on connection/sync like the Send tab (a disconnected or mid-sync submit just
|
||||
// fails at the daemon with a raw error).
|
||||
bool sh_connected = app->isConnected();
|
||||
bool sh_syncing = state.sync.syncing;
|
||||
bool can_submit = !s_operation_pending && s_to_address[0] != '\0' && sh_connected && !sh_syncing;
|
||||
|
||||
if (!can_submit) ImGui::BeginDisabled();
|
||||
|
||||
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? TR("shield_funds") : TR("merge_funds");
|
||||
if (material::TactileButton(btn_label, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
|
||||
s_operation_pending = true;
|
||||
s_status_message = TR("shield_submitting");
|
||||
|
||||
if (s_mode == Mode::ShieldCoinbase) {
|
||||
std::string from(s_from_address), to(s_to_address);
|
||||
double fee = s_fee;
|
||||
int limit = s_utxo_limit;
|
||||
if (app->worker()) {
|
||||
app->worker()->post([app, rpc = app->rpc(), from, to, fee, limit]() -> rpc::RPCWorker::MainCb {
|
||||
nlohmann::json result;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Send tab / Shield coinbase");
|
||||
result = rpc->call("z_shieldcoinbase", {from, to, fee, limit});
|
||||
} catch (const std::exception& e) {
|
||||
error = e.what();
|
||||
}
|
||||
return [app, result, error]() {
|
||||
s_operation_pending = false;
|
||||
if (error.empty()) {
|
||||
s_operation_id = result.value("opid", "");
|
||||
s_status_message = std::string(TR("shield_op_submitted")) + s_operation_id;
|
||||
Notifications::instance().success(TR("shield_started"));
|
||||
// Register with the shared poller so an async failure is
|
||||
// surfaced (and balances refresh) even after this dialog closes.
|
||||
app->trackOperation(s_operation_id);
|
||||
} else {
|
||||
s_status_message = std::string(TR("shield_error_prefix")) + error;
|
||||
Notifications::instance().error(std::string(TR("shield_send_failed")) + error);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
std::vector<std::string> fromAddrs;
|
||||
fromAddrs.push_back("ANY_TADDR");
|
||||
std::string to(s_to_address);
|
||||
double fee = s_fee;
|
||||
int limit = s_utxo_limit;
|
||||
if (app->worker()) {
|
||||
app->worker()->post([app, rpc = app->rpc(), fromAddrs, to, fee, limit]() -> rpc::RPCWorker::MainCb {
|
||||
nlohmann::json addrs = nlohmann::json::array();
|
||||
for (const auto& addr : fromAddrs) addrs.push_back(addr);
|
||||
nlohmann::json result;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Send tab / Merge funds");
|
||||
result = rpc->call("z_mergetoaddress", {addrs, to, fee, 0, limit});
|
||||
} catch (const std::exception& e) {
|
||||
error = e.what();
|
||||
}
|
||||
return [app, result, error]() {
|
||||
s_operation_pending = false;
|
||||
if (error.empty()) {
|
||||
s_operation_id = result.value("opid", "");
|
||||
s_status_message = std::string(TR("shield_op_submitted")) + s_operation_id;
|
||||
Notifications::instance().success(TR("merge_started"));
|
||||
// Register with the shared poller so an async failure is
|
||||
// surfaced (and balances refresh) even after this dialog closes.
|
||||
app->trackOperation(s_operation_id);
|
||||
} else {
|
||||
s_status_message = std::string(TR("shield_error_prefix")) + error;
|
||||
Notifications::instance().error(std::string(TR("merge_send_failed")) + error);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!can_submit) ImGui::EndDisabled();
|
||||
if (!can_submit && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||
if (!sh_connected) material::Tooltip("%s", TR("send_tooltip_not_connected"));
|
||||
else if (sh_syncing) material::Tooltip("%s", TR("send_tooltip_syncing"));
|
||||
else if (s_to_address[0]=='\0') material::Tooltip("%s", TR("shield_select_z"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::TactileButton(TR("cancel"), ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
// Show operation status if we have an opid
|
||||
if (!s_operation_id.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text(TR("shield_operation_id"), s_operation_id.c_str());
|
||||
|
||||
if (material::TactileButton(TR("shield_check_status"), ImVec2(0,0), S.resolveFont(shieldBtn.font))) {
|
||||
std::string opid = s_operation_id;
|
||||
if (app->worker()) {
|
||||
app->worker()->post([rpc = app->rpc(), opid]() -> rpc::RPCWorker::MainCb {
|
||||
nlohmann::json result;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Send tab / Shield operation status");
|
||||
nlohmann::json ids = nlohmann::json::array();
|
||||
ids.push_back(opid);
|
||||
result = rpc->call("z_getoperationstatus", {ids});
|
||||
} catch (const std::exception& e) {
|
||||
error = e.what();
|
||||
}
|
||||
return [result, error]() {
|
||||
if (error.empty() && result.is_array() && !result.empty()) {
|
||||
auto& op = result[0];
|
||||
std::string status = op.value("status", "unknown");
|
||||
if (status == "success") {
|
||||
s_status_message = TR("shield_completed");
|
||||
Notifications::instance().success(TR("shield_merge_done"));
|
||||
} else if (status == "failed") {
|
||||
std::string errMsg = op.value("error", nlohmann::json{}).value("message", TR("shield_unknown_error"));
|
||||
s_status_message = std::string(TR("shield_op_failed")) + errMsg;
|
||||
Notifications::instance().error(std::string(TR("shield_op_failed")) + errMsg);
|
||||
} else if (status == "executing") {
|
||||
s_status_message = TR("shield_in_progress");
|
||||
} else {
|
||||
s_status_message = std::string(TR("shield_status_label")) + status;
|
||||
}
|
||||
} else if (!error.empty()) {
|
||||
s_status_message = std::string(TR("shield_status_check_error")) + error;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
// ── Shield coinbase: from address ───────────────────────────────────────────────────────────
|
||||
if (!isMerge && !opInFlight) {
|
||||
material::LabeledInput(TR("shield_from_address"), "##FromAddr", s_from_address, sizeof(s_from_address));
|
||||
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// ── Destination (z-address) ─────────────────────────────────────────────────────────────────
|
||||
if (!opInFlight) {
|
||||
ImGui::TextUnformatted(TR("shield_to_address"));
|
||||
if (state.z_addresses.empty()) {
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::Warning(), TR("shield_no_zaddr_hint"));
|
||||
ImGui::Spacing();
|
||||
if (s_creating_addr) {
|
||||
ImGui::TextDisabled("%s", TR("merge_creating"));
|
||||
} else if (material::TactileButton(TR("merge_create_zaddr"), ImVec2(0, 0), S.resolveFont(shieldBtn.font))) {
|
||||
s_creating_addr = true;
|
||||
if (app->worker()) app->worker()->post([app, rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
||||
std::string addr, error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Shield dialog / New z-address");
|
||||
addr = rpc->call("z_getnewaddress", nlohmann::json::array()).get<std::string>();
|
||||
} catch (const std::exception& e) { error = e.what(); }
|
||||
return [app, addr, error]() {
|
||||
s_creating_addr = false;
|
||||
if (error.empty() && !addr.empty()) {
|
||||
strncpy(s_to_address, addr.c_str(), sizeof(s_to_address) - 1);
|
||||
Notifications::instance().success(TR("merge_addr_created"));
|
||||
} else {
|
||||
Notifications::instance().error(std::string(TR("shield_error_prefix")) + error);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
std::string to_display = s_to_address[0] ? s_to_address : TR("shield_select_z");
|
||||
if (to_display.length() > static_cast<size_t>(addrLbl.truncate))
|
||||
to_display = to_display.substr(0, addrFront.truncate) + "..." + to_display.substr(to_display.length() - addrBack.truncate);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::BeginCombo("##ToAddr", to_display.c_str())) {
|
||||
for (size_t i = 0; i < state.z_addresses.size(); i++) {
|
||||
std::string label = state.z_addresses[i].address;
|
||||
if (label.length() > static_cast<size_t>(addrLbl.truncate))
|
||||
label = label.substr(0, addrFront.truncate) + "..." + label.substr(label.length() - addrBack.truncate);
|
||||
bool selected = (s_selected_zaddr_idx == static_cast<int>(i));
|
||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
s_selected_zaddr_idx = static_cast<int>(i);
|
||||
strncpy(s_to_address, state.z_addresses[i].address.c_str(), sizeof(s_to_address) - 1);
|
||||
}
|
||||
if (selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
// ── Advanced (fee + batch size) ─────────────────────────────────────────────────────────
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
material::CollapsibleHeader(dl, "##AdvToggle", TR("merge_advanced"), s_advanced,
|
||||
ImGui::GetContentRegionAvail().x, material::Type().caption(),
|
||||
material::OnSurfaceMedium());
|
||||
if (s_advanced) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted(TR("fee_label"));
|
||||
ImGui::SetNextItemWidth(feeInput.width * dp);
|
||||
ImGui::InputDouble("##Fee", &s_fee, 0.0001, 0.001, "%.8f");
|
||||
if (s_fee < 0.0) s_fee = 0.0;
|
||||
if (s_fee > 1.0) s_fee = 1.0;
|
||||
ImGui::SameLine(); ImGui::TextDisabled("DRGX");
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("merge_fee_hint"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted(TR("merge_max_inputs"));
|
||||
ImGui::SetNextItemWidth(utxoInput.width * dp);
|
||||
ImGui::InputInt("##Limit", &s_utxo_limit);
|
||||
if (s_utxo_limit < 1) s_utxo_limit = 1;
|
||||
if (s_utxo_limit > 100) s_utxo_limit = 100;
|
||||
}
|
||||
|
||||
// Batch hint: one run only merges up to the limit; large sets need repeats.
|
||||
if (isMerge && s_scope_loaded && srcCount() > s_utxo_limit) {
|
||||
char hb[160];
|
||||
std::snprintf(hb, sizeof(hb), TR("merge_batch_fmt"), s_utxo_limit);
|
||||
ImGui::Spacing();
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::Warning(), hb);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// ── Live progress / status ──────────────────────────────────────────────────────────────────
|
||||
if (!s_status_message.empty()) {
|
||||
if (s_operation_pending && !s_op_terminal)
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", s_status_message.c_str());
|
||||
else
|
||||
ImGui::TextWrapped("%s", s_status_message.c_str());
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// ── Footer ──────────────────────────────────────────────────────────────────────────────────
|
||||
const bool connected = app->isConnected();
|
||||
const bool syncing = state.sync.syncing;
|
||||
const bool haveDest = s_to_address[0] != '\0';
|
||||
|
||||
if (opInFlight) {
|
||||
// After submit: just a Close button (progress shows above; op continues in the background).
|
||||
material::BeginOverlayDialogFooter(cancelBtn.width, /*drawSeparator=*/false);
|
||||
if (material::TactileButton(s_op_terminal ? TR("done") : TR("close"),
|
||||
ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font)))
|
||||
s_open = false;
|
||||
material::EndOverlayDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
const char* primaryLabel = s_confirm ? TR("merge_confirm_btn")
|
||||
: s_consolidate ? TR("consolidate_funds_btn")
|
||||
: isMerge ? TR("merge_funds")
|
||||
: TR("shield_funds");
|
||||
const char* secondaryLabel = s_confirm ? TR("merge_back") : TR("cancel");
|
||||
|
||||
// Confirm summary (inline, before the fund-moving call). Merge/consolidate shows amount + input
|
||||
// count; shield-coinbase just gets the button relabel (its inputs aren't enumerated here).
|
||||
if (s_confirm && isMerge) {
|
||||
char cb[200];
|
||||
std::snprintf(cb, sizeof(cb), TR("merge_confirm_fmt"),
|
||||
fmtAmt(srcAmount()).c_str(), srcCount(), shortAddr(s_to_address).c_str());
|
||||
ImGui::TextWrapped("%s", cb);
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
bool can_submit = haveDest && connected && !syncing;
|
||||
if (isMerge && s_scope_loaded && srcCount() == 0) can_submit = false;
|
||||
|
||||
float footerBtnW = shieldBtn.width + cancelBtn.width + ImGui::GetStyle().ItemSpacing.x;
|
||||
material::BeginOverlayDialogFooter(footerBtnW, /*drawSeparator=*/false);
|
||||
|
||||
if (!can_submit) ImGui::BeginDisabled();
|
||||
if (material::TactileButton(primaryLabel, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
|
||||
if (s_confirm) { submitOperation(app); }
|
||||
else { s_confirm = true; } // first click → show the confirm summary
|
||||
}
|
||||
if (!can_submit) ImGui::EndDisabled();
|
||||
if (!can_submit && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||
if (!connected) material::Tooltip("%s", TR("send_tooltip_not_connected"));
|
||||
else if (syncing) material::Tooltip("%s", TR("send_tooltip_syncing"));
|
||||
else if (!haveDest) material::Tooltip("%s", TR("shield_select_z"));
|
||||
else if (isMerge && srcCount() == 0) material::Tooltip("%s", TR("merge_no_spendable"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::TactileButton(secondaryLabel, ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
|
||||
if (s_confirm) s_confirm = false; // Back → return to the form
|
||||
else s_open = false; // Cancel → close
|
||||
}
|
||||
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -33,10 +33,16 @@ public:
|
||||
static void showShieldCoinbase(const std::string& fromAddress = "*");
|
||||
|
||||
/**
|
||||
* @brief Show merge to address dialog
|
||||
* @brief Show merge to address dialog (generic — both transparent + shielded sources)
|
||||
*/
|
||||
static void showMerge();
|
||||
|
||||
/**
|
||||
* @brief Show the consolidate-funds flow preset for wallet-bloat reduction (shielded notes).
|
||||
* Used by the large-wallet nudges (Settings banner + alert action).
|
||||
*/
|
||||
static void showConsolidate();
|
||||
|
||||
/**
|
||||
* @brief Render the dialog (call each frame)
|
||||
*/
|
||||
|
||||
@@ -121,10 +121,10 @@ void TransactionDetailsDialog::render(App* app)
|
||||
char txid_buf[128];
|
||||
strncpy(txid_buf, tx.txid.c_str(), sizeof(txid_buf) - 1);
|
||||
txid_buf[sizeof(txid_buf) - 1] = '\0';
|
||||
ImGui::SetNextItemWidth(txidInput.width);
|
||||
ImGui::SetNextItemWidth(txidInput.width); // negative = fill, reserving |width| px for the Copy button (a content-region margin, not raw px — must NOT be dpi-scaled)
|
||||
ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Copy##TxID", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||
if (material::StyledButton((std::string(TR("grpb_copy")) + "##TxID").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||
ImGui::SetClipboardText(tx.txid.c_str());
|
||||
}
|
||||
|
||||
@@ -139,17 +139,18 @@ void TransactionDetailsDialog::render(App* app)
|
||||
char addr_buf[512];
|
||||
strncpy(addr_buf, tx.address.c_str(), sizeof(addr_buf) - 1);
|
||||
addr_buf[sizeof(addr_buf) - 1] = '\0';
|
||||
// width is a negative fill-with-right-margin sentinel — keep unscaled; only height is raw px.
|
||||
ImGui::InputTextMultiline("##Address", addr_buf, sizeof(addr_buf),
|
||||
ImVec2(addrInput.width, addrInput.height > 0 ? addrInput.height : 50), ImGuiInputTextFlags_ReadOnly);
|
||||
ImVec2(addrInput.width, (addrInput.height > 0 ? addrInput.height : 50) * Layout::dpiScale()), ImGuiInputTextFlags_ReadOnly);
|
||||
} else {
|
||||
char addr_buf[128];
|
||||
strncpy(addr_buf, tx.address.c_str(), sizeof(addr_buf) - 1);
|
||||
addr_buf[sizeof(addr_buf) - 1] = '\0';
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::SetNextItemWidth(addrInput.width); // negative fill sentinel — must NOT be dpi-scaled
|
||||
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Copy##Addr", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||
if (material::StyledButton((std::string(TR("grpb_copy")) + "##Addr").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||
ImGui::SetClipboardText(tx.address.c_str());
|
||||
}
|
||||
}
|
||||
@@ -173,25 +174,18 @@ void TransactionDetailsDialog::render(App* app)
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Buttons
|
||||
float button_width = bottomBtn.width;
|
||||
float total_width = button_width * 2 + ImGui::GetStyle().ItemSpacing.x;
|
||||
float start_x = (ImGui::GetWindowWidth() - total_width) / 2.0f;
|
||||
ImGui::SetCursorPosX(start_x);
|
||||
|
||||
// Buttons — centered primary + Close pair via the shared footer helper.
|
||||
// Guard against an empty/whitespace explorer URL so we never open a garbage link.
|
||||
std::string explorerBase = app->settings()->getTxExplorerUrl();
|
||||
bool explorerValid = explorerBase.find_first_not_of(" \t\r\n") != std::string::npos;
|
||||
if (!explorerValid) ImGui::BeginDisabled();
|
||||
if (material::StyledButton(TR("tx_view_explorer"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
|
||||
bool doExplorer = false, doClose = false;
|
||||
material::DialogActionFooter(TR("tx_view_explorer"), explorerValid, TR("close"),
|
||||
doExplorer, doClose, bottomBtn.width);
|
||||
if (doExplorer) {
|
||||
std::string url = explorerBase + tx.txid;
|
||||
util::Platform::openUrl(url);
|
||||
}
|
||||
if (!explorerValid) ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
|
||||
if (doClose) {
|
||||
s_open = false;
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user