24 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 d27f387d6d feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings
Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):

- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
  status-bar "?" (and the Windows title bar), styled like the Wallets
  modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
  Layout::dpiScale() across ~30 files so nothing renders native-size at
  HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
  match the input-box height; move the stratum-host toggle into
  Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
  the preceding i18n commit), so the security/PIN/lock flow, seed-backup
  wizard, and witness-rebuild dialog localize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:37:16 -05:00
DanSandClaude Opus 4.8 ed675f90d8 feat(i18n): add English source + 8-language translations for 193 UI strings
Adds the i18n source for 193 previously-hardcoded UI strings (the
security / PIN / lock flow, first-run seed-backup wizard, witness-rebuild
shutdown dialog, and balance / mining / explorer labels) so they can be
localized. The TR() call-site wrapping rides in the accompanying UI commit.

- 193 keys added to loadBuiltinEnglish() (the English source of truth),
  including one pre-existing missing key (copied_to_clipboard).
- Additive res/lang/{de,es,fr,ja,ko,pt,ru,zh}.json (+193 each); every
  translation's printf format-signature was validated against English
  (0 mismatches) so the runtime validator accepts them.
- Rebuilt res/fonts/NotoSansCJK-Subset.ttf to cover ~30 new Han/Hangul
  glyphs introduced by the zh/ja/ko translations (tofu-free).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:36:50 -05:00
DanSandClaude Opus 4.8 4492aa3425 fix(sync): prioritize getblockchaininfo and pause chat scans while behind
The node kept falling behind the network near the tip because sync
DETECTION was starved: getblockchaininfo was queued behind the
O(mapWallet) wallet RPCs (z_gettotalbalance / z_listunspent), so
longestchain went stale, the wallet decided it was "synced", and it
resumed hammering cs_main — a feedback loop.

- Issue getblockchaininfo FIRST each cycle and skip the balance/address/
  tx refresh entirely while behind, so sync state (and kSyncProfile)
  updates before any heavy wallet scan runs.
- Gate the two chat note scans (refreshChatNoteBudgetNode /
  fastScanChatMemos) on effectivelySyncing() and the active page, so chat
  memo scanning no longer competes with block connection during sync.
- Windows debug.log tailer: reset the read offset when dragonxd truncates
  the log on startup (it was stranding at Block:0 with no witness/rescan
  progress).
- Tests cover the getblockchaininfo-first ordering and behind-skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:36:30 -05:00
DanSandClaude Opus 4.8 56d93b6128 fix(shutdown): don't warn "node is rebuilding" for routine witness activity / on v1.3.0
The shutdown guard fired its "Node is rebuilding its witness cache" prompt almost always on an
active wallet: the daemon does a per-tx VerifyAndSetInitialWitness as each newly received wallet
tx lands during normal sync, and those markers (sparse — minutes apart — with no matching
"rebuilt N note witness cache(s) … in Xms") kept the heuristic latched "active".

- daemonWitnessRebuildActive() now requires the last progress marker to be part of the CURRENT
  log activity (within ~15s of the newest log line, via a same-log timestamp delta), so routine
  minutes-old per-tx witness sets no longer count as an ongoing rebuild.
- shouldConfirmDaemonStop() suppresses the prompt entirely on v1.3.0+ daemons (version >= 1030000):
  they checkpoint witness-rescan progress, so stopping mid-rebuild resumes on the next start rather
  than redoing it — the warning's "restarts it (several minutes)" premise no longer holds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 02:21:49 -05:00
DanSandClaude Opus 4.8 398fb274fa fix(ui): only toast "Blockchain rescan complete" for user-initiated rescans
The daemon-output parser treats autonomous background witness rebuilds as a rescan (they set
state_.sync.rescanning via the foundWitness branch), so one completing fired "Blockchain rescan
complete" even though the user never started a rescan — most visibly after a minimize, where a
whole rebuild's start+finish arrives in one batch.

Add user_initiated_rescan_ (atomic — some triggers run on worker threads), set it at the wallet's
real rescan triggers (the Rescan button, a -rescan/salvage/zap/reindex restart, key import, seed
migration), and gate the three "rescan complete" toasts on it, clearing it when shown. The
rescan/witness progress state machine is untouched — only the toast is gated — so background
rebuilds no longer announce a completed rescan while genuine user rescans still do (and can't
double-toast).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 01:18:57 -05:00
DanSandClaude Opus 4.8 0343d48c13 feat(ui): keep the wallet syncing while minimized
Previously the main loop just did SDL_Delay+continue when minimized, skipping app.update() —
so the wallet stopped draining RPC results, ticking the refresh scheduler, and reconnecting
until it was restored (and a large daemon-output backlog piled up, which is what produced the
spurious "rescan complete" toast on restore).

Now app.update() runs while minimized (it only reads GetIO/GetTime/IsAnyItemActive, all valid
outside a NewFrame) with a real-clock DeltaTime, skipping only the ImGui frame + GPU present,
throttled to ~5 Hz so CPU stays near-idle. Also clamp io.DeltaTime at the top of App::update()
so a long minimize (or machine sleep) can't report a huge delta and fire every refresh/animation
timer at once on the next update. No backlog now builds, so skipDaemonOutputBacklog becomes a
harmless no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 01:09:50 -05:00
DanSandClaude Opus 4.8 a3892c0fd3 fix(ui): stop spurious "Blockchain rescan complete" toast after restoring from minimize
While minimized the main loop skips the frame body (SDL_Delay+continue), so App::update() —
which drains the daemon output via outputSince(daemon_output_offset_) — never runs and the
offset isn't advanced. On restore the whole accumulated backlog is parsed in one batch: a
background witness rebuild's progress lines (parsed as a rescan → state_.sync.rescanning=true)
AND its "rebuilt … in Xms" completion (parsed as finished) arrive together and fire
"Blockchain rescan complete" for a scan the user never initiated.

On WINDOW_RESTORED, discard the daemon-output backlog (advance the offset to the current end,
via App::skipDaemonOutputBacklog) before the resumed update parses it — so only new output is
parsed. Genuine user-initiated rescans still surface completion via the getrescaninfo monitor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 00:49:06 -05:00
DanSandClaude Opus 4.8 f7df315695 fix(ui): remove the "Taking longer than expected" startup stall notice
It added clutter to the loading screen (the yellow title + two lines of explanatory text).
The live daemon-output panel below it is the real progress signal. connect_stall_since_ stays
maintained in app_network.cpp for connection bookkeeping; it just no longer drives any on-screen
text (loading_stall_* i18n strings + util::connectHasStalled are now unused).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 00:15:27 -05:00
DanSandClaude Opus 4.8 aec996a9ce feat(mining): host a RandomX stratum pool from the node (v1.3.0+)
Add an opt-in "Host a mining pool (stratum)" toggle so a v1.3.0+ node can run its native
RandomX stratum server for other miners to point at. Settings toggle + optional allow-IP/CIDR;
passes -stratum (+ -stratumallowip) to the daemon launch args, mirroring the -maxconnections
plumbing (EmbeddedDaemon::setStratumHosting <- DaemonController::syncSettings <- Settings).

Backwards compatible / safe by default:
- UI gated on daemon_version >= 1030000, so it's never offered where it would do nothing.
- The launch flag is harmless on older daemons (they ignore unknown args), and the toggle can
  only be enabled while connected to a v1.3.0+ node anyway.
- Blank allow-IP => the daemon serves loopback only (its safe default); entering a subnet opens
  it to that LAN, with an explicit exposure warning in the UI.
- Takes effect on the next daemon start/restart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-31 23:19:48 -05:00
DanSandClaude Opus 4.8 90e02b1ddd feat(ui): surface daemon DEGRADED mode + v1.3.0 auto-shield status
Rec3 — v1.3.0+ opens a wallet that lost its hdchain in DEGRADED mode (existing funds spendable,
but no new HD-key derivation) instead of aborting. Add a daemon-log classifier
(walletOpenedDegraded) + detectWalletDegraded(), warned once per session. Pre-1.3.0 daemons
never emit that line, so it's a no-op there.

O1 — probe z_autoshieldstatus once per connection (now decoupled from our own toggle/balance) and,
in Settings, show whether the node handles auto-shield itself (+ its destination, or the daemon's
disabled_reason). The checkbox now governs only the wallet's fallback shielder, which defers to the
node. Nothing renders on pre-1.3.0 daemons (no such RPC), so behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-31 22:48:46 -05:00
DanSandClaude Opus 4.8 ef8ceeaf9a feat(net): seed via the round-robin DNS record + node1/node5 (backwards-compatible)
Replace the wallet's stale hardcoded -addnode list (node. + node1-4.dragonx.is — a drifted
subset with a bogus bare 'node.') with the daemon's authoritative vSeeds: seed.dragonx.is (a
round-robin A record over the live seed set, so it self-updates without a wallet release) plus
node1/node5 as static fallbacks. Applied in both seeding sites: the launch args
(embedded_daemon.cpp) and the generated DRAGONX.conf (connection.cpp).

Kept (not deleted) — pre-1.3.0 daemons had broken peer discovery and rely on these -addnode
entries to find peers at all; plain hostname resolution works on every daemon version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-31 22:48:46 -05:00
DanSandClaude Opus 4.8 ba1d760bb3 fix(autoshield): defer to the daemon's own coinbase auto-shield on v1.3.0+
v1.3.0 daemons auto-shield coinbase by default (when the HD seed is recoverable — which
every ObsidianDragon-created wallet is, via -usemnemonic=1). The wallet also ran its own
client-side auto-shield every refresh tick, so both raced for the same coinbase UTXOs and
split funds across different z-addresses (the wallet picks the first z_listaddresses entry;
the daemon uses a seed-hardened derivation).

Probe z_autoshieldstatus once per connection (while synced, so the daemon is past warmup)
and skip the wallet's client-side shield when the daemon reports it active. Fail-closed: a
pre-1.3.0 daemon has no such RPC, so the probe returns active=false and the wallet keeps
shielding — no regression on the currently-bundled v1.0.3. Re-probes on reconnect (handles
a live daemon upgrade/swap).

Follow-up (not done): drive the Settings "Auto-shield" toggle + disabled_reason from
z_autoshieldstatus (O1) — needs runtime verification of the RPC fields on a live v1.3.0 node.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-31 22:33:12 -05:00
DanSandClaude Opus 4.8 0942691eb3 fix(win): route shell-outs through a windowless helper (no cmd.exe flash)
_popen/_popen-style shell-outs flash a cmd.exe console window on Windows. Add
Platform::runHiddenCapture() — CreateProcess + CREATE_NO_WINDOW capturing stdout on
Windows, popen on POSIX — and route the remaining shell-outs through it:
- GPU-aware idle detection (getGpuUtilization: "where nvidia-smi" / "nvidia-smi --query-gpu")
- xmrig discovery + version (findXmrigBinary "where xmrig.exe"; "<bin> --version", stderr merged)
- wallet-rebuild helper (app_network) — keeps its exit-code check via the new exitCode out-param

None of these are on the launch path (that was the daemon spawn, fixed in a2f84be); each
would flash a console only when it ran (idle-GPU mining, mining tab, wallet recovery).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-30 23:58:54 -05:00
DanSandClaude Opus 4.8 a2f84be2d4 fix(win): stop console-window flash on launch (spawn daemon with CREATE_NO_WINDOW)
The embedded daemon was launched with CREATE_NEW_CONSOLE + SW_HIDE. CREATE_NEW_CONSOLE
allocates a console window that flashes on screen before SW_HIDE hides it — visible as a
console-window flash every time the wallet starts dragonxd (i.e. on launch). Switch to
CREATE_NO_WINDOW (the console child gets no window at all, matching the xmrig launcher);
dragonxd logs to debug.log, not a console, so nothing is lost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-30 23:43:12 -05:00
DanSandClaude Opus 4.8 7e8b99a82b feat(shutdown): confirm before stopping the daemon mid witness-cache rebuild
Stopping dragonxd while it's rebuilding the Sapling witness cache discards the
in-progress work — BuildWitnessCache aborts on shutdown without persisting — so
the next launch redoes a multi-minute rebuild (the "Activating best chain…" hang).
This bites especially with stop_external_daemon enabled, where wallet exit sends
the node a stop.

beginShutdown() now defers when it would StopDaemon while a rebuild is active and
shows a confirm modal: "Keep node running & quit" (DisconnectOnly — leaves it up
to finish), "Stop anyway & quit", or "Cancel". Rebuild detection reads the
debug.log tail markers (Cleared witness data / Setting Initial Sapling Witness /
Reading blocks for witness rebuild, vs. the "rebuilt … in …ms" / abort lines).
The gate lives entirely in beginShutdown()/render() — no SDL event-loop changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-30 23:32:03 -05:00
DanSandClaude Opus 4.8 29274c2f48 fix(ui): trim verbose startup notice; show daemon output on shutdown for external daemons
- Loading "taking longer than expected" notice: shorten the body + hint so the
  startup screen reads less wordy (same info, ~half the text).
- Shutdown screen: when the wallet attached to an EXTERNAL daemon (no captured
  stdout — debug_log_path_ is only set when we spawn it), the "dragonxd output"
  panel was always empty, leaving just a spinner. Fall back to tailing the
  daemon's debug.log so the user can watch the node flush the block index and
  exit. Adds App::tailDaemonDebugLog() (best-effort, reads only the file tail).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-30 23:09:17 -05:00
DanSandClaude Opus 4.8 870793433b fix(sync): stop large-wallet balance polling from starving block connection
On a fully-shielded (ac_private=1) chain, z_gettotalbalance is O(mapWallet) and
holds the daemon's cs_main for its whole duration — ~20s on a ~5k-tx wallet. The
Overview refresh polled it every ~2s (twice: minconf 0 and 1), so cs_main was
held almost continuously, starving the single block-connection thread: the node
connected blocks only in the gaps between polls and could fall further behind
the tip than it caught up (observed live: gap growing 58→100 blocks while the
GUI was open, one core pegged on GetFilteredNotes, 22 idle, ~17 B/s download).

Two hardening changes on top of the existing "skip balance while syncing" guard:

- Hysteresis: keep the low-impact sync profile (and balance suppression) for a
  short settle window after catching up, so a large-wallet scan can't
  immediately re-starve connection and bounce the node back into syncing. Armed
  only on the syncing→caught-up edge, so a wallet synced from the start is never
  throttled at connect (effectivelySyncing()).

- Adaptive balance cadence: time each z_gettotalbalance scan and require the
  next poll to wait at least (cost / 10%), so balance scanning never occupies
  more than ~10% of wall-clock. Cheap wallets are unaffected (the tab's Core
  timer stays the cadence); a ~20s scan backs off to ~200s. Wallet mutations
  (send/shield) force the next poll through so the user's own action updates the
  balance immediately (balanceRefreshDue()).

getblockchaininfo keeps its normal cadence throughout, so sync progress stays
live. Build + test_phase4 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-30 22:45:51 -05:00
DanSandClaude Opus 4.8 08cfeb0e08 feat(ui): rework the consolidate/merge modal
Make Merge to Address actually serve wallet-bloat consolidation and be far less
opaque. New ShieldDialog::showConsolidate() preset (used by the large-wallet
Settings banner + alert action) frames it as "Consolidate funds" and targets
shielded notes — the bloat the nudge warns about.

- Source selector: consolidate shielded notes (ANY_SAPLING), transparent
  (ANY_TADDR), or both (*) — previously hardcoded to ANY_TADDR, which never
  reduced the shielded-witness bloat. Batch limit now applies to the right side.
- Scope: on open, count spendable UTXOs + notes (listunspent / z_listunspent)
  and show "N transparent + M shielded · ~X DRGX"; warn "repeat to finish" when
  the set exceeds one batch.
- Destination auto-selects the best spendable z-address (button enabled by
  default); empty wallets get an inline "Create shielded address" (z_getnewaddress).
- Advanced disclosure hides Fee + "Max inputs per batch" (renamed from the "UTXO
  Limit" jargon) with sane defaults.
- Inline confirm step before the fund-moving call (amount + input count + dest).
- Live progress: self-polls z_getoperationstatus to show Consolidating… →
  Done/Failed, replacing the raw opid + manual "Check status" button.

All three merge entry points now use the typed showMerge()/showConsolidate()
(no stale-static leaks from direct show(MergeToAddress)). Shield-coinbase mode
keeps working. New i18n keys fall back to English.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 22:33:59 -05:00
DanSandClaude Opus 4.8 5daf2d83b6 feat(ui): large-wallet nudge as a one-time toast + clickable alert
Extend the wallet-bloat warning beyond the Settings banner: when wallet.dat
first crosses 500 MB (full-node, synced), fire a one-time warning toast plus a
clickable "Consolidate notes…" entry in the bell/alerts panel that opens Merge
to Address. The persisted large_wallet_warned flag keeps it once-only and
re-arms if the file later shrinks back under the threshold.

- AlertRecord gains an optional onClick + actionHint; Notifications::action()
  pushes a toast and a clickable history entry. renderAlertHistoryPanel() now
  renders the accent action link (under the message) and measures true content
  height so wrapped messages + the link aren't clipped.
- App::maybeWarnLargeWallet() (mirrors maybeRemindSeedBackup) runs once per
  launch from update(); reuses the existing wallet_size_warn/consolidate strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 18:09:36 -05:00
DanSandClaude Opus 4.8 6d26ccd0ed feat(ui): large-wallet nudge in Node & Security
The BDB wallet.dat bloats with shielded-note witness data and never shrinks
in place, so a mining/shielded wallet can grow past 500 MB. Below the Wallet
Size row, show a one-line amber hint once wallet.dat crosses 500 MB with a
"Consolidate notes…" shortcut that opens the Merge to Address (z_mergetoaddress)
dialog. Full-node only (lite has no wallet.dat here); threshold is a single
named constant. i18n keys fall back to English for non-English locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 17:41:25 -05:00
DanSandClaude Opus 4.8 a7514becbc feat(ui): credit The DragonX Developers in the About tab
Add "The DragonX Developers" to the About-tab credits (after The Hush
Developers), acknowledging the DragonX chain/daemon this wallet drives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 17:17:03 -05:00
DanSandClaude Opus 4.8 558cfcbe56 fix(ui): restore ObsidianDragon logo in header and About tab
ensureLogoTexture() rasterized the embedded DragonX SVG into logo_tex_ and
returned early (added in 1752500 "themed DragonX logo"), so the app/product
branding — the top-left header (app.cpp AddImage) and the About tab
(getLogoTexture) — showed the DragonX coin mark instead of the ObsidianDragon
logo. Drop that step so logo_tex_ resolves via the intended path: active-skin
override → ui.toml header-icon → bundled ObsidianDragon dark/light PNG (disk,
then embedded RESOURCE_LOGO). The DragonX SVG stays for coin_logo_tex_ (balance
card) and drgx_emoji_tex_ (chat emoji), which are the currency mark and correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 17:06:04 -05:00
DanSandClaude Opus 4.8 d0bd55b9c1 feat(ui): settings polish — button retune, daemon card, RPC 2-row, chat preview
Settings tabs brought closer to the approved mockup:
- ActionButton/renderCardButton retune (settings-scoped): 7px radius, 9px
  padX, Primary → accent-outline chip, Secondary/card buttons more defined.
- Daemon-binary card: compact status right-aligned on the DAEMON BINARY
  heading (Up to date / Version differs / Not installed), filled/rounded
  status box, neutral danger divider (was alarming red), roomier spacing.
- RPC Connection: two-row column-aligned layout (Host | Port, then
  Username | Password) so the password no longer clips off the card edge.
- Chat settings tab: live conversation preview below the Appearance /
  Messaging cards; "Focus input on open" checkbox reflowed onto the console
  color-toggle row.
- Debug Options: "Current theme only" toggle restricts either screenshot
  sweep to the active theme instead of cycling every skin.
- Tabs fill the full content width (content-max-width cap disabled) and the
  sidebar nav panel centers within the true visible area.
- i18n: new keys for the above (untranslated keys fall back to English).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 16:59:03 -05:00
DanSandClaude Opus 4.8 b37d3d97b6 fix(send/receive): unify card width, justify receive footer, fix recipient-row button height/clip/glyph
Send and Receive are now consistent in layout, and the Send recipient row's buttons
render correctly.

Card envelope (Send ⇄ Receive consistency):
- Add Layout::mainComposeCardBox(availW) — a single shared source for the compose card's
  width + centering (fill the available column up to content-max-width, then center). Both
  tabs derive their card from it, so they can't drift again. Previously Send capped at
  760dp and Receive at 860dp, so the Send card rendered ~150px narrower on any window wider
  than ~860dp; now they fill available width identically.

Receive:
- Justify the footer buttons edge-to-edge (equal shares over the live count) instead of
  left-clustering with dead space, matching Send's full-width footer rhythm.
- Build the address-dropdown preview to the combo's real pixel width so the trailing
  balance ("— 12.00000000 DRGX") no longer hard-clips at 150% (was char-count truncation).

Send recipient row (input | Paste | contacts-icon):
- Pin the contacts icon button to the frame height so the larger iconMed font doesn't
  auto-size it taller than Paste/the input.
- Reserve the real ItemSpacing.x gaps (not the smaller spacingSm token) so the row no
  longer overshoots the card and clips the icon's right border.

draw_helpers (root cause, app-wide):
- TactileButton's icon path measured/drew the label INCLUDING the "##id" suffix (which
  CalcTextSizeA/AddText don't strip the way ImGui's text render does), shoving the glyph
  off-center-left. Strip at "##" before measuring/drawing. Corrects any icon button that
  passes an explicit size and a "##id" label; no-op for labels without "##".

Verified via headless sweeps at 1.0x and 1.5x, plus a real 3800px-wide render (both cards
byte-identical at L=1174/R=2773). ctest 1/1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-19 16:10:02 -05:00
66 changed files with 5321 additions and 1797 deletions
+4
View File
@@ -521,6 +521,8 @@ set(APP_SOURCES
src/ui/windows/settings_window.cpp src/ui/windows/settings_window.cpp
src/ui/pages/settings_page.cpp src/ui/pages/settings_page.cpp
src/ui/windows/about_dialog.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/key_export_dialog.cpp
src/ui/windows/transaction_details_dialog.cpp src/ui/windows/transaction_details_dialog.cpp
src/ui/windows/qr_popup_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/console_tab_helpers.h
src/ui/windows/settings_window.h src/ui/windows/settings_window.h
src/ui/windows/about_dialog.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/key_export_dialog.h
src/ui/windows/transaction_details_dialog.h src/ui/windows/transaction_details_dialog.h
src/ui/windows/qr_popup_dialog.h src/ui/windows/qr_popup_dialog.h
Binary file not shown.
+216 -16
View File
@@ -60,6 +60,80 @@
"amount_label": "Betrag:", "amount_label": "Betrag:",
"animate_avatars": "Avatare animieren", "animate_avatars": "Avatare animieren",
"appearance": "ERSCHEINUNGSBILD", "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", "auto_shield": "Mining automatisch abschirmen",
"av_intro": "Mining-Software wird oft als potenziell unerwünscht eingestuft. Führen Sie diese Schritte aus, um das Pool-Mining zu aktivieren:", "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_open_security": "Windows-Sicherheit öffnen",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender hat den Miner blockiert", "av_title": "Windows Defender hat den Miner blockiert",
"available": "Verfügbar", "available": "Verfügbar",
"backup_backing_up": "Sicherung läuft...", "backup_backing_up": "Sicherung läuft...",
"backup_col_backup": "SICHERUNG",
"backup_col_export": "EXPORTIEREN",
"backup_col_import": "IMPORTIEREN & WIEDERHERSTELLEN",
"backup_create": "Sicherung erstellen", "backup_create": "Sicherung erstellen",
"backup_created": "Wallet-Sicherung erstellt", "backup_created": "Wallet-Sicherung erstellt",
"backup_data": "SICHERUNG & DATEN", "backup_data": "SICHERUNG & DATEN",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "Abgeschirmt: %.8f", "balance_shielded_fmt": "Abgeschirmt: %.8f",
"balance_syncing_pct": "Synchronisiere %.1f%%", "balance_syncing_pct": "Synchronisiere %.1f%%",
"balance_transparent_fmt": "Transparent: %.8f", "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", "ban": "Sperren",
"banned_peers": "Gesperrte Peers", "banned_peers": "Gesperrte Peers",
"block": "Block", "block": "Block",
@@ -411,6 +499,7 @@
"contacts_shape_square": "Quadrat", "contacts_shape_square": "Quadrat",
"contacts_shape_tab": "Reiter", "contacts_shape_tab": "Reiter",
"copied": "Kopiert!", "copied": "Kopiert!",
"copied_to_clipboard": "In die Zwischenablage kopiert",
"copy": "Kopieren", "copy": "Kopieren",
"copy_address": "Vollständige Adresse kopieren", "copy_address": "Vollständige Adresse kopieren",
"copy_error": "Fehler kopieren", "copy_error": "Fehler kopieren",
@@ -423,6 +512,7 @@
"daemon_bundled": "Gebündelt", "daemon_bundled": "Gebündelt",
"daemon_install_bundled": "Gebündelten installieren", "daemon_install_bundled": "Gebündelten installieren",
"daemon_installed": "Installiert", "daemon_installed": "Installiert",
"daemon_maintenance_label": "WARTUNG",
"daemon_none_bundled": "keiner in diesem Build", "daemon_none_bundled": "keiner in diesem Build",
"daemon_not_installed": "nicht installiert", "daemon_not_installed": "nicht installiert",
"daemon_status_differ": "Installierte Binärdatei unterscheidet sich von der gebündelten Version.", "daemon_status_differ": "Installierte Binärdatei unterscheidet sich von der gebündelten Version.",
@@ -458,6 +548,7 @@
"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_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_verifying": "Wird verifiziert…",
"daemon_update_version": "Version:", "daemon_update_version": "Version:",
"daemon_updates_label": "AKTUALISIERUNGEN",
"daemon_version": "Daemon", "daemon_version": "Daemon",
"dark": "Dunkel", "dark": "Dunkel",
"data_stale_prefix": "Aktualisiert", "data_stale_prefix": "Aktualisiert",
@@ -588,6 +679,63 @@
"general": "Allgemein", "general": "Allgemein",
"generating": "Wird generiert", "generating": "Wird generiert",
"go_to_receive": "Zum Empfangen", "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", "height": "Höhe",
"help": "Hilfe", "help": "Hilfe",
"hidden_tag": " (versteckt)", "hidden_tag": " (versteckt)",
@@ -1228,12 +1376,52 @@
"sb_waiting_daemon_err": "Warten auf dragonxd — %s", "sb_waiting_daemon_err": "Warten auf dragonxd — %s",
"sb_warming_up": "Aufwärmen...", "sb_warming_up": "Aufwärmen...",
"sb_witness_cache": "Zeugen werden neu aufgebaut", "sb_witness_cache": "Zeugen werden neu aufgebaut",
"scale_effects": "SKALIERUNG & EFFEKTE",
"screenshot_open_dir": "Speicherort öffnen", "screenshot_open_dir": "Speicherort öffnen",
"screenshot_sweep": "Screenshot-Durchlauf ausführen", "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_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", "screenshot_sweep_full": "Vollständiger UI-Durchlauf",
"search_icons": "Symbole suchen...", "search_icons": "Symbole suchen...",
"search_placeholder": "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", "security": "SICHERHEIT",
"seed_backup_button": "Wiederherstellungsphrase", "seed_backup_button": "Wiederherstellungsphrase",
"seed_backup_close": "Schließen", "seed_backup_close": "Schließen",
@@ -1316,12 +1504,12 @@
"settings": "Einstellungen", "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_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_acrylic_level": "Acrylstufe:",
"settings_address_book": "Adressbuch...", "settings_address_book": "Adressbuch…",
"settings_auto_detected": "Automatisch erkannt aus DRAGONX.conf", "settings_auto_detected": "Automatisch erkannt aus DRAGONX.conf",
"settings_auto_lock": "AUTO-SPERRE", "settings_auto_lock": "AUTO-SPERRE",
"settings_auto_shield_desc": "Transparente Guthaben automatisch an geschirmte Adressen verschieben", "settings_auto_shield_desc": "Transparente Guthaben automatisch an geschirmte Adressen verschieben",
"settings_auto_shield_funds": "Transparente Guthaben automatisch abschirmen", "settings_auto_shield_funds": "Transparente Guthaben automatisch abschirmen",
"settings_backup": "Sicherung...", "settings_backup": "Sicherung…",
"settings_block_explorer_urls": "Block-Explorer-URLs", "settings_block_explorer_urls": "Block-Explorer-URLs",
"settings_builtin": "Integriert", "settings_builtin": "Integriert",
"settings_change_passphrase": "Passphrase ändern", "settings_change_passphrase": "Passphrase ändern",
@@ -1335,7 +1523,7 @@
"settings_copy_diagnostics": "Diagnose kopieren", "settings_copy_diagnostics": "Diagnose kopieren",
"settings_copyright": "Copyright 2024-2026 DragonX-Entwickler | GPLv3-Lizenz", "settings_copyright": "Copyright 2024-2026 DragonX-Entwickler | GPLv3-Lizenz",
"settings_custom": "Benutzerdefiniert", "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_changed": "Debug-Kategorien geändert — Daemon neu starten zum Anwenden",
"settings_debug_restart_note": "Änderungen werden nach einem Neustart des Daemons wirksam.", "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).", "settings_debug_select": "Kategorien auswählen, um Daemon-Fehlerprotokollierung zu aktivieren (-debug= Flags).",
@@ -1343,18 +1531,18 @@
"settings_encrypt_first_pin": "Verschlüsseln Sie zuerst die Wallet, um PIN zu aktivieren", "settings_encrypt_first_pin": "Verschlüsseln Sie zuerst die Wallet, um PIN zu aktivieren",
"settings_encrypt_wallet": "Wallet verschlüsseln", "settings_encrypt_wallet": "Wallet verschlüsseln",
"settings_explorer_hint": "URLs sollten einen abschließenden Schrägstrich enthalten. Die txid/Adresse wird angehängt.", "settings_explorer_hint": "URLs sollten einen abschließenden Schrägstrich enthalten. Die txid/Adresse wird angehängt.",
"settings_export_all": "Alle exportieren...", "settings_export_all": "Alle exportieren…",
"settings_export_csv": "CSV exportieren...", "settings_export_csv": "CSV exportieren…",
"settings_export_key": "Schlüssel exportieren...", "settings_export_key": "Schlüssel exportieren…",
"settings_gradient_bg": "Hintergrund-Verlauf", "settings_gradient_bg": "Hintergrund-Verlauf",
"settings_gradient_desc": "Strukturierte Hintergründe durch sanfte Verläufe ersetzen", "settings_gradient_desc": "Strukturierte Hintergründe durch sanfte Verläufe ersetzen",
"settings_idle_after": "nach", "settings_idle_after": "nach",
"settings_import_key": "Privaten Schlüssel importieren...", "settings_import_key": "Privaten Schlüssel importieren…",
"settings_import_viewkey": "Anzeigeschlüssel importieren...", "settings_import_viewkey": "Anzeigeschlüssel importieren…",
"settings_language_note": "Hinweis: Manche Texte erfordern einen Neustart zur Aktualisierung", "settings_language_note": "Hinweis: Manche Texte erfordern einen Neustart zur Aktualisierung",
"settings_lock_now": "Jetzt sperren", "settings_lock_now": "Jetzt sperren",
"settings_locked": "Gesperrt", "settings_locked": "Gesperrt",
"settings_merge_to_address": "An Adresse zusammenführen...", "settings_merge_to_address": "An Adresse zusammenführen…",
"settings_noise_opacity": "Rauschdichte:", "settings_noise_opacity": "Rauschdichte:",
"settings_not_connected": "Nicht mit dem Daemon verbunden", "settings_not_connected": "Nicht mit dem Daemon verbunden",
"settings_not_encrypted": "Nicht verschlüsselt", "settings_not_encrypted": "Nicht verschlüsselt",
@@ -1370,7 +1558,7 @@
"settings_reloaded": "Einstellungen von der Festplatte neu geladen", "settings_reloaded": "Einstellungen von der Festplatte neu geladen",
"settings_remove_encryption": "Verschlüsselung entfernen", "settings_remove_encryption": "Verschlüsselung entfernen",
"settings_remove_pin": "PIN 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_rescan_desc": "Blockchain nach fehlenden Transaktionen neu scannen",
"settings_restart_daemon": "Daemon neu starten", "settings_restart_daemon": "Daemon neu starten",
"settings_rpc_connection": "RPC-Verbindung", "settings_rpc_connection": "RPC-Verbindung",
@@ -1381,20 +1569,20 @@
"settings_save_shielded_local": "Geschirmten Transaktionsverlauf lokal speichern", "settings_save_shielded_local": "Geschirmten Transaktionsverlauf lokal speichern",
"settings_saved": "Einstellungen gespeichert", "settings_saved": "Einstellungen gespeichert",
"settings_set_pin": "PIN festlegen", "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_solid_colors_desc": "Feste Farben anstelle von Unschärfe-Effekten verwenden (Barrierefreiheit)",
"settings_theme_refreshed": "Themenliste aktualisiert", "settings_theme_refreshed": "Themenliste aktualisiert",
"settings_tor_desc": "Alle Verbindungen für erhöhte Privatsphäre über Tor leiten", "settings_tor_desc": "Alle Verbindungen für erhöhte Privatsphäre über Tor leiten",
"settings_unlocked": "Entsperrt", "settings_unlocked": "Entsperrt",
"settings_use_tor_network": "Tor für Netzwerkverbindungen verwenden", "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_visual_effects": "Visuelle Effekte",
"settings_wallet_file_size": "Wallet-Dateigröße: %s", "settings_wallet_file_size": "Wallet-Dateigröße: %s",
"settings_wallet_info": "Wallet-Informationen", "settings_wallet_info": "Wallet-Informationen",
"settings_wallet_location": "Wallet-Speicherort: %s", "settings_wallet_location": "Wallet-Speicherort: %s",
"settings_wallet_maintenance": "Wallet-Wartung", "settings_wallet_maintenance": "Wallet-Wartung",
"settings_wallet_not_found": "Wallet-Datei nicht gefunden", "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_cleared": "Z-Transaktionsverlauf gelöscht",
"settings_ztx_not_found": "Keine Verlaufsdatei gefunden", "settings_ztx_not_found": "Keine Verlaufsdatei gefunden",
"setup_wizard": "Einrichtungsassistent", "setup_wizard": "Einrichtungsassistent",
@@ -1455,6 +1643,17 @@
"sweep_to": "Gefegt an:", "sweep_to": "Gefegt an:",
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)", "sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
"sweep_tx": "Transaktion:", "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_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_corrupt_repair": "Reparatur versuchen (Salvage)",
"switch_progress_background": "Im Hintergrund fortsetzen", "switch_progress_background": "Im Hintergrund fortsetzen",
@@ -1494,6 +1693,7 @@
"to_upper": "AN", "to_upper": "AN",
"tools": "WERKZEUGE", "tools": "WERKZEUGE",
"tools_actions": "Werkzeuge & Aktionen...", "tools_actions": "Werkzeuge & Aktionen...",
"tools_actions_hdr": "WERKZEUGE & AKTIONEN",
"total": "Gesamt", "total": "Gesamt",
"total_balance_label": "Gesamtguthaben", "total_balance_label": "Gesamtguthaben",
"transaction_id": "TRANSAKTIONS-ID", "transaction_id": "TRANSAKTIONS-ID",
@@ -1517,7 +1717,7 @@
"tt_auto_shield": "Transparentes Guthaben automatisch an geschirmte Adressen für Datenschutz verschieben", "tt_auto_shield": "Transparentes Guthaben automatisch an geschirmte Adressen für Datenschutz verschieben",
"tt_backup": "Eine Sicherungskopie Ihrer wallet.dat erstellen", "tt_backup": "Eine Sicherungskopie Ihrer wallet.dat erstellen",
"tt_block_explorer": "Den DragonX Block-Explorer im Browser öffnen", "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_pass": "Die Wallet-Verschlüsselungspassphrase ändern",
"tt_change_pin": "Ihre Entsperr-PIN ändern", "tt_change_pin": "Ihre Entsperr-PIN ändern",
"tt_chat_bubble_accent": "Akzentfarbe für deine ausgehenden Nachrichtenblasen (oder dem aktuellen Theme folgen)", "tt_chat_bubble_accent": "Akzentfarbe für deine ausgehenden Nachrichtenblasen (oder dem aktuellen Theme folgen)",
@@ -1580,7 +1780,7 @@
"tt_low_spec": "Alle aufwendigen visuellen Effekte deaktivieren\\nHotkey: Ctrl+Shift+Down", "tt_low_spec": "Alle aufwendigen visuellen Effekte deaktivieren\\nHotkey: Ctrl+Shift+Down",
"tt_merge": "Mehrere UTXOs einer Adresse zusammenführen", "tt_merge": "Mehrere UTXOs einer Adresse zusammenführen",
"tt_mine_idle": "Mining automatisch starten, wenn das\\nSystem inaktiv ist (keine Tastatur-/Mauseingabe)", "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_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_data_dir": "Den Ordner mit Ihren Wallet- und Blockchain-Daten im Dateimanager öffnen",
"tt_open_dir": "Klicken, um im Dateimanager zu öffnen", "tt_open_dir": "Klicken, um im Dateimanager zu öffnen",
@@ -1619,7 +1819,7 @@
"tt_theme_hotkey": "Hotkey: Ctrl+Links/Rechts zum Wechseln der Themes", "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_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_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_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_verbose": "Detaillierte Verbindungsdiagnosen,\\nDaemon-Status und Port-Besitzer-Info\\nin der Konsolen-Registerkarte protokollieren",
"tt_wallets_button": "Ihre Wallet-Dateien auflisten und zwischen ihnen wechseln", "tt_wallets_button": "Ihre Wallet-Dateien auflisten und zwischen ihnen wechseln",
+216 -16
View File
@@ -60,6 +60,80 @@
"amount_label": "Cantidad:", "amount_label": "Cantidad:",
"animate_avatars": "Animar avatares", "animate_avatars": "Animar avatares",
"appearance": "APARIENCIA", "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", "auto_shield": "Auto-proteger minería",
"av_intro": "El software de minería suele marcarse como potencialmente no deseado. Sigue estos pasos para habilitar la minería en pool:", "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_open_security": "Abrir Seguridad de Windows",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender bloqueó el minero", "av_title": "Windows Defender bloqueó el minero",
"available": "Disponible", "available": "Disponible",
"backup_backing_up": "Respaldando...", "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_create": "Crear Respaldo",
"backup_created": "Respaldo de cartera creado", "backup_created": "Respaldo de cartera creado",
"backup_data": "RESPALDO Y DATOS", "backup_data": "RESPALDO Y DATOS",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "Protegido: %.8f", "balance_shielded_fmt": "Protegido: %.8f",
"balance_syncing_pct": "Sincronizando %.1f%%", "balance_syncing_pct": "Sincronizando %.1f%%",
"balance_transparent_fmt": "Transparente: %.8f", "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", "ban": "Bloquear",
"banned_peers": "Nodos Bloqueados", "banned_peers": "Nodos Bloqueados",
"block": "Bloque", "block": "Bloque",
@@ -411,6 +499,7 @@
"contacts_shape_square": "Cuadrado", "contacts_shape_square": "Cuadrado",
"contacts_shape_tab": "Pestaña", "contacts_shape_tab": "Pestaña",
"copied": "¡Copiado!", "copied": "¡Copiado!",
"copied_to_clipboard": "Copiado al portapapeles",
"copy": "Copiar", "copy": "Copiar",
"copy_address": "Copiar Dirección Completa", "copy_address": "Copiar Dirección Completa",
"copy_error": "Copiar Error", "copy_error": "Copiar Error",
@@ -423,6 +512,7 @@
"daemon_bundled": "Incluido", "daemon_bundled": "Incluido",
"daemon_install_bundled": "Instalar integrado", "daemon_install_bundled": "Instalar integrado",
"daemon_installed": "Instalado", "daemon_installed": "Instalado",
"daemon_maintenance_label": "MANTENIMIENTO",
"daemon_none_bundled": "ninguno en esta compilación", "daemon_none_bundled": "ninguno en esta compilación",
"daemon_not_installed": "no instalado", "daemon_not_installed": "no instalado",
"daemon_status_differ": "El binario instalado difiere de la versión incluida.", "daemon_status_differ": "El binario instalado difiere de la versión incluida.",
@@ -458,6 +548,7 @@
"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_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_verifying": "Verificando…",
"daemon_update_version": "Versión:", "daemon_update_version": "Versión:",
"daemon_updates_label": "ACTUALIZACIONES",
"daemon_version": "Daemon", "daemon_version": "Daemon",
"dark": "Oscuro", "dark": "Oscuro",
"data_stale_prefix": "Actualizado", "data_stale_prefix": "Actualizado",
@@ -588,6 +679,63 @@
"general": "General", "general": "General",
"generating": "Generando", "generating": "Generando",
"go_to_receive": "Ir a Recibir", "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", "height": "Altura",
"help": "Ayuda", "help": "Ayuda",
"hidden_tag": " (oculto)", "hidden_tag": " (oculto)",
@@ -1228,12 +1376,52 @@
"sb_waiting_daemon_err": "Esperando a dragonxd — %s", "sb_waiting_daemon_err": "Esperando a dragonxd — %s",
"sb_warming_up": "Calentando...", "sb_warming_up": "Calentando...",
"sb_witness_cache": "Reconstruyendo testigos", "sb_witness_cache": "Reconstruyendo testigos",
"scale_effects": "ESCALA Y EFECTOS",
"screenshot_open_dir": "Abrir ubicación", "screenshot_open_dir": "Abrir ubicación",
"screenshot_sweep": "Ejecutar barrido de capturas", "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_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", "screenshot_sweep_full": "Barrido completo de la interfaz",
"search_icons": "Buscar iconos...", "search_icons": "Buscar iconos...",
"search_placeholder": "Buscar...", "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", "security": "SEGURIDAD",
"seed_backup_button": "Frase de recuperación", "seed_backup_button": "Frase de recuperación",
"seed_backup_close": "Cerrar", "seed_backup_close": "Cerrar",
@@ -1316,12 +1504,12 @@
"settings": "Ajustes", "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_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_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_detected": "Autodetectado de DRAGONX.conf",
"settings_auto_lock": "BLOQUEO AUTOMÁTICO", "settings_auto_lock": "BLOQUEO AUTOMÁTICO",
"settings_auto_shield_desc": "Mover automáticamente fondos transparentes a direcciones blindadas", "settings_auto_shield_desc": "Mover automáticamente fondos transparentes a direcciones blindadas",
"settings_auto_shield_funds": "Blindar fondos transparentes automáticamente", "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_block_explorer_urls": "URLs del explorador de bloques",
"settings_builtin": "Integrado", "settings_builtin": "Integrado",
"settings_change_passphrase": "Cambiar contraseña", "settings_change_passphrase": "Cambiar contraseña",
@@ -1335,7 +1523,7 @@
"settings_copy_diagnostics": "Copiar diagnósticos", "settings_copy_diagnostics": "Copiar diagnósticos",
"settings_copyright": "Copyright 2024-2026 Desarrolladores de DragonX | Licencia GPLv3", "settings_copyright": "Copyright 2024-2026 Desarrolladores de DragonX | Licencia GPLv3",
"settings_custom": "Personalizado", "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_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_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=).", "settings_debug_select": "Seleccione categorías para habilitar el registro de depuración del daemon (flags -debug=).",
@@ -1343,18 +1531,18 @@
"settings_encrypt_first_pin": "Primero cifre la billetera para habilitar el PIN", "settings_encrypt_first_pin": "Primero cifre la billetera para habilitar el PIN",
"settings_encrypt_wallet": "Cifrar billetera", "settings_encrypt_wallet": "Cifrar billetera",
"settings_explorer_hint": "Las URLs deben incluir una barra final. Se añadirá el txid/dirección.", "settings_explorer_hint": "Las URLs deben incluir una barra final. Se añadirá el txid/dirección.",
"settings_export_all": "Exportar todo...", "settings_export_all": "Exportar todo…",
"settings_export_csv": "Exportar CSV...", "settings_export_csv": "Exportar CSV…",
"settings_export_key": "Exportar clave...", "settings_export_key": "Exportar clave…",
"settings_gradient_bg": "Fondo degradado", "settings_gradient_bg": "Fondo degradado",
"settings_gradient_desc": "Reemplazar fondos con texturas por degradados suaves", "settings_gradient_desc": "Reemplazar fondos con texturas por degradados suaves",
"settings_idle_after": "después de", "settings_idle_after": "después de",
"settings_import_key": "Importar Clave Privada...", "settings_import_key": "Importar Clave Privada…",
"settings_import_viewkey": "Importar clave de visualización...", "settings_import_viewkey": "Importar clave de visualización…",
"settings_language_note": "Nota: Parte del texto requiere reinicio para actualizarse", "settings_language_note": "Nota: Parte del texto requiere reinicio para actualizarse",
"settings_lock_now": "Bloquear ahora", "settings_lock_now": "Bloquear ahora",
"settings_locked": "Bloqueado", "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_noise_opacity": "Opacidad de ruido:",
"settings_not_connected": "No conectado al daemon", "settings_not_connected": "No conectado al daemon",
"settings_not_encrypted": "Sin cifrar", "settings_not_encrypted": "Sin cifrar",
@@ -1370,7 +1558,7 @@
"settings_reloaded": "Configuración recargada desde el disco", "settings_reloaded": "Configuración recargada desde el disco",
"settings_remove_encryption": "Quitar cifrado", "settings_remove_encryption": "Quitar cifrado",
"settings_remove_pin": "Quitar PIN", "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_rescan_desc": "Reescanear la cadena de bloques en busca de transacciones faltantes",
"settings_restart_daemon": "Reiniciar daemon", "settings_restart_daemon": "Reiniciar daemon",
"settings_rpc_connection": "Conexión RPC", "settings_rpc_connection": "Conexión RPC",
@@ -1381,20 +1569,20 @@
"settings_save_shielded_local": "Guardar historial de transacciones blindadas localmente", "settings_save_shielded_local": "Guardar historial de transacciones blindadas localmente",
"settings_saved": "Configuración guardada", "settings_saved": "Configuración guardada",
"settings_set_pin": "Establecer PIN", "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_solid_colors_desc": "Usar colores sólidos en lugar de efectos de desenfoque (accesibilidad)",
"settings_theme_refreshed": "Lista de temas actualizada", "settings_theme_refreshed": "Lista de temas actualizada",
"settings_tor_desc": "Enrutar todas las conexiones a través de Tor para mayor privacidad", "settings_tor_desc": "Enrutar todas las conexiones a través de Tor para mayor privacidad",
"settings_unlocked": "Desbloqueado", "settings_unlocked": "Desbloqueado",
"settings_use_tor_network": "Usar Tor para conexiones de red", "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_visual_effects": "Efectos visuales",
"settings_wallet_file_size": "Tamaño del archivo de billetera: %s", "settings_wallet_file_size": "Tamaño del archivo de billetera: %s",
"settings_wallet_info": "Información de billetera", "settings_wallet_info": "Información de billetera",
"settings_wallet_location": "Ubicación de billetera: %s", "settings_wallet_location": "Ubicación de billetera: %s",
"settings_wallet_maintenance": "Mantenimiento de billetera", "settings_wallet_maintenance": "Mantenimiento de billetera",
"settings_wallet_not_found": "Archivo de billetera no encontrado", "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_cleared": "Historial de transacciones Z borrado",
"settings_ztx_not_found": "No se encontró archivo de historial", "settings_ztx_not_found": "No se encontró archivo de historial",
"setup_wizard": "Asistente de Configuración", "setup_wizard": "Asistente de Configuración",
@@ -1455,6 +1643,17 @@
"sweep_to": "Barrido a:", "sweep_to": "Barrido a:",
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)", "sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
"sweep_tx": "Transacción:", "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_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_corrupt_repair": "Intentar reparar (salvage)",
"switch_progress_background": "Continuar en segundo plano", "switch_progress_background": "Continuar en segundo plano",
@@ -1494,6 +1693,7 @@
"to_upper": "PARA", "to_upper": "PARA",
"tools": "HERRAMIENTAS", "tools": "HERRAMIENTAS",
"tools_actions": "Herramientas y Acciones...", "tools_actions": "Herramientas y Acciones...",
"tools_actions_hdr": "HERRAMIENTAS Y ACCIONES",
"total": "Total", "total": "Total",
"total_balance_label": "Saldo Total", "total_balance_label": "Saldo Total",
"transaction_id": "ID DE TRANSACCIÓN", "transaction_id": "ID DE TRANSACCIÓN",
@@ -1517,7 +1717,7 @@
"tt_auto_shield": "Mover automáticamente el saldo transparente a direcciones blindadas para privacidad", "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_backup": "Crear una copia de seguridad de su wallet.dat",
"tt_block_explorer": "Abrir el explorador de bloques DragonX en su navegador", "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_pass": "Cambiar la contraseña de cifrado de la billetera",
"tt_change_pin": "Cambiar su PIN de desbloqueo", "tt_change_pin": "Cambiar su PIN de desbloqueo",
"tt_chat_bubble_accent": "Color de acento para tus burbujas de mensaje salientes (o sigue el tema actual)", "tt_chat_bubble_accent": "Color de acento para tus burbujas de mensaje salientes (o sigue el tema actual)",
@@ -1580,7 +1780,7 @@
"tt_low_spec": "Desactivar todos los efectos visuales pesados\\nAtajo: Ctrl+Shift+Down", "tt_low_spec": "Desactivar todos los efectos visuales pesados\\nAtajo: Ctrl+Shift+Down",
"tt_merge": "Consolidar múltiples UTXOs en una dirección", "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_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_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_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", "tt_open_dir": "Clic para abrir en explorador de archivos",
@@ -1619,7 +1819,7 @@
"tt_theme_hotkey": "Atajo: Ctrl+Izquierda/Derecha para cambiar temas", "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_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_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_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_verbose": "Registrar diagnósticos detallados de conexión,\\nestado del daemon e info de propietario de puerto\\nen la pestaña de Consola",
"tt_wallets_button": "Enumera tus archivos de cartera y cambia entre ellos", "tt_wallets_button": "Enumera tus archivos de cartera y cambia entre ellos",
+216 -16
View File
@@ -60,6 +60,80 @@
"amount_label": "Montant :", "amount_label": "Montant :",
"animate_avatars": "Animer les avatars", "animate_avatars": "Animer les avatars",
"appearance": "APPARENCE", "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", "auto_shield": "Auto-blindage du minage",
"av_intro": "Les logiciels de minage sont souvent signalés comme potentiellement indésirables. Suivez ces étapes pour activer le minage en pool :", "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_open_security": "Ouvrir Sécurité Windows",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender a bloqué le mineur", "av_title": "Windows Defender a bloqué le mineur",
"available": "Disponible", "available": "Disponible",
"backup_backing_up": "Sauvegarde en cours...", "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_create": "Créer une sauvegarde",
"backup_created": "Sauvegarde du portefeuille créée", "backup_created": "Sauvegarde du portefeuille créée",
"backup_data": "SAUVEGARDE & DONNÉES", "backup_data": "SAUVEGARDE & DONNÉES",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "Blindé : %.8f", "balance_shielded_fmt": "Blindé : %.8f",
"balance_syncing_pct": "Synchronisation %.1f%%", "balance_syncing_pct": "Synchronisation %.1f%%",
"balance_transparent_fmt": "Transparent : %.8f", "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", "ban": "Bannir",
"banned_peers": "Pairs bannis", "banned_peers": "Pairs bannis",
"block": "Bloc", "block": "Bloc",
@@ -411,6 +499,7 @@
"contacts_shape_square": "Carré", "contacts_shape_square": "Carré",
"contacts_shape_tab": "Onglet", "contacts_shape_tab": "Onglet",
"copied": "Copié !", "copied": "Copié !",
"copied_to_clipboard": "Copié dans le presse-papiers",
"copy": "Copier", "copy": "Copier",
"copy_address": "Copier l'adresse complète", "copy_address": "Copier l'adresse complète",
"copy_error": "Copier l'erreur", "copy_error": "Copier l'erreur",
@@ -423,6 +512,7 @@
"daemon_bundled": "Intégré", "daemon_bundled": "Intégré",
"daemon_install_bundled": "Installer la version intégrée", "daemon_install_bundled": "Installer la version intégrée",
"daemon_installed": "Installé", "daemon_installed": "Installé",
"daemon_maintenance_label": "MAINTENANCE",
"daemon_none_bundled": "aucun dans cette version", "daemon_none_bundled": "aucun dans cette version",
"daemon_not_installed": "non installé", "daemon_not_installed": "non installé",
"daemon_status_differ": "Le binaire installé diffère de la version intégrée.", "daemon_status_differ": "Le binaire installé diffère de la version intégrée.",
@@ -458,6 +548,7 @@
"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_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_verifying": "Vérification…",
"daemon_update_version": "Version :", "daemon_update_version": "Version :",
"daemon_updates_label": "MISES À JOUR",
"daemon_version": "Daemon", "daemon_version": "Daemon",
"dark": "Sombre", "dark": "Sombre",
"data_stale_prefix": "Mis à jour", "data_stale_prefix": "Mis à jour",
@@ -588,6 +679,63 @@
"general": "Général", "general": "Général",
"generating": "Génération", "generating": "Génération",
"go_to_receive": "Aller à Recevoir", "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", "height": "Hauteur",
"help": "Aide", "help": "Aide",
"hidden_tag": " (masqué)", "hidden_tag": " (masqué)",
@@ -1228,12 +1376,52 @@
"sb_waiting_daemon_err": "En attente de dragonxd — %s", "sb_waiting_daemon_err": "En attente de dragonxd — %s",
"sb_warming_up": "Démarrage...", "sb_warming_up": "Démarrage...",
"sb_witness_cache": "Reconstruction des témoins", "sb_witness_cache": "Reconstruction des témoins",
"scale_effects": "ÉCHELLE ET EFFETS",
"screenshot_open_dir": "Ouvrir l'emplacement", "screenshot_open_dir": "Ouvrir l'emplacement",
"screenshot_sweep": "Lancer la capture d'écran", "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_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", "screenshot_sweep_full": "Balayage complet de l'interface",
"search_icons": "Rechercher des icônes...", "search_icons": "Rechercher des icônes...",
"search_placeholder": "Rechercher...", "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É", "security": "SÉCURITÉ",
"seed_backup_button": "Phrase de récupération", "seed_backup_button": "Phrase de récupération",
"seed_backup_close": "Fermer", "seed_backup_close": "Fermer",
@@ -1316,12 +1504,12 @@
"settings": "Paramètres", "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_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_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_detected": "Détecté automatiquement depuis DRAGONX.conf",
"settings_auto_lock": "VERROUILLAGE AUTO", "settings_auto_lock": "VERROUILLAGE AUTO",
"settings_auto_shield_desc": "Déplacer automatiquement les fonds transparents vers des adresses blindées", "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_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_block_explorer_urls": "URLs de l'explorateur de blocs",
"settings_builtin": "Intégré", "settings_builtin": "Intégré",
"settings_change_passphrase": "Changer la phrase secrète", "settings_change_passphrase": "Changer la phrase secrète",
@@ -1335,7 +1523,7 @@
"settings_copy_diagnostics": "Copier les diagnostics", "settings_copy_diagnostics": "Copier les diagnostics",
"settings_copyright": "Copyright 2024-2026 Développeurs DragonX | Licence GPLv3", "settings_copyright": "Copyright 2024-2026 Développeurs DragonX | Licence GPLv3",
"settings_custom": "Personnalisé", "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_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_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=).", "settings_debug_select": "Sélectionnez les catégories pour activer la journalisation de débogage du daemon (flags -debug=).",
@@ -1343,18 +1531,18 @@
"settings_encrypt_first_pin": "Chiffrez d'abord le portefeuille pour activer le PIN", "settings_encrypt_first_pin": "Chiffrez d'abord le portefeuille pour activer le PIN",
"settings_encrypt_wallet": "Chiffrer le portefeuille", "settings_encrypt_wallet": "Chiffrer le portefeuille",
"settings_explorer_hint": "Les URLs doivent inclure une barre oblique finale. Le txid/adresse sera ajouté.", "settings_explorer_hint": "Les URLs doivent inclure une barre oblique finale. Le txid/adresse sera ajouté.",
"settings_export_all": "Tout exporter...", "settings_export_all": "Tout exporter…",
"settings_export_csv": "Exporter CSV...", "settings_export_csv": "Exporter CSV…",
"settings_export_key": "Exporter la clé...", "settings_export_key": "Exporter la clé…",
"settings_gradient_bg": "Fond dégradé", "settings_gradient_bg": "Fond dégradé",
"settings_gradient_desc": "Remplacer les arrière-plans texturés par des dégradés lisses", "settings_gradient_desc": "Remplacer les arrière-plans texturés par des dégradés lisses",
"settings_idle_after": "après", "settings_idle_after": "après",
"settings_import_key": "Importer une clé privée...", "settings_import_key": "Importer une clé privée…",
"settings_import_viewkey": "Importer la clé de visualisation...", "settings_import_viewkey": "Importer la clé de visualisation…",
"settings_language_note": "Remarque : Certains textes nécessitent un redémarrage pour se mettre à jour", "settings_language_note": "Remarque : Certains textes nécessitent un redémarrage pour se mettre à jour",
"settings_lock_now": "Verrouiller maintenant", "settings_lock_now": "Verrouiller maintenant",
"settings_locked": "Verrouillé", "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_noise_opacity": "Opacité du bruit :",
"settings_not_connected": "Non connecté au démon", "settings_not_connected": "Non connecté au démon",
"settings_not_encrypted": "Non chiffré", "settings_not_encrypted": "Non chiffré",
@@ -1370,7 +1558,7 @@
"settings_reloaded": "Paramètres rechargés depuis le disque", "settings_reloaded": "Paramètres rechargés depuis le disque",
"settings_remove_encryption": "Supprimer le chiffrement", "settings_remove_encryption": "Supprimer le chiffrement",
"settings_remove_pin": "Supprimer le PIN", "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_rescan_desc": "Rescanner la blockchain pour les transactions manquantes",
"settings_restart_daemon": "Redémarrer le daemon", "settings_restart_daemon": "Redémarrer le daemon",
"settings_rpc_connection": "Connexion RPC", "settings_rpc_connection": "Connexion RPC",
@@ -1381,20 +1569,20 @@
"settings_save_shielded_local": "Enregistrer l'historique des transactions blindées localement", "settings_save_shielded_local": "Enregistrer l'historique des transactions blindées localement",
"settings_saved": "Paramètres enregistrés", "settings_saved": "Paramètres enregistrés",
"settings_set_pin": "Définir le PIN", "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_solid_colors_desc": "Utiliser des couleurs unies au lieu des effets de flou (accessibilité)",
"settings_theme_refreshed": "Liste des thèmes actualisée", "settings_theme_refreshed": "Liste des thèmes actualisée",
"settings_tor_desc": "Acheminer toutes les connexions via Tor pour une confidentialité renforcée", "settings_tor_desc": "Acheminer toutes les connexions via Tor pour une confidentialité renforcée",
"settings_unlocked": "Déverrouillé", "settings_unlocked": "Déverrouillé",
"settings_use_tor_network": "Utiliser Tor pour les connexions réseau", "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_visual_effects": "Effets visuels",
"settings_wallet_file_size": "Taille du fichier portefeuille : %s", "settings_wallet_file_size": "Taille du fichier portefeuille : %s",
"settings_wallet_info": "Informations du portefeuille", "settings_wallet_info": "Informations du portefeuille",
"settings_wallet_location": "Emplacement du portefeuille : %s", "settings_wallet_location": "Emplacement du portefeuille : %s",
"settings_wallet_maintenance": "Maintenance du portefeuille", "settings_wallet_maintenance": "Maintenance du portefeuille",
"settings_wallet_not_found": "Fichier portefeuille introuvable", "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_cleared": "Historique des transactions Z effacé",
"settings_ztx_not_found": "Aucun fichier d'historique trouvé", "settings_ztx_not_found": "Aucun fichier d'historique trouvé",
"setup_wizard": "Assistant de configuration", "setup_wizard": "Assistant de configuration",
@@ -1455,6 +1643,17 @@
"sweep_to": "Balayé vers :", "sweep_to": "Balayé vers :",
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)", "sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
"sweep_tx": "Transaction :", "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_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_corrupt_repair": "Tenter une réparation (salvage)",
"switch_progress_background": "Continuer en arrière-plan", "switch_progress_background": "Continuer en arrière-plan",
@@ -1494,6 +1693,7 @@
"to_upper": "À", "to_upper": "À",
"tools": "OUTILS", "tools": "OUTILS",
"tools_actions": "Outils & Actions...", "tools_actions": "Outils & Actions...",
"tools_actions_hdr": "OUTILS ET ACTIONS",
"total": "Total", "total": "Total",
"total_balance_label": "Solde total", "total_balance_label": "Solde total",
"transaction_id": "ID DE TRANSACTION", "transaction_id": "ID DE TRANSACTION",
@@ -1517,7 +1717,7 @@
"tt_auto_shield": "Déplacer automatiquement le solde transparent vers des adresses blindées pour la confidentialité", "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_backup": "Créer une sauvegarde de votre wallet.dat",
"tt_block_explorer": "Ouvrir l'explorateur de blocs DragonX dans votre navigateur", "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_pass": "Changer la phrase secrète de chiffrement du portefeuille",
"tt_change_pin": "Changer votre PIN de déverrouillage", "tt_change_pin": "Changer votre PIN de déverrouillage",
"tt_chat_bubble_accent": "Couleur d'accent de vos bulles de message sortantes (ou suivre le thème actuel)", "tt_chat_bubble_accent": "Couleur d'accent de vos bulles de message sortantes (ou suivre le thème actuel)",
@@ -1580,7 +1780,7 @@
"tt_low_spec": "Désactiver tous les effets visuels lourds\\nRaccourci : Ctrl+Shift+Down", "tt_low_spec": "Désactiver tous les effets visuels lourds\\nRaccourci : Ctrl+Shift+Down",
"tt_merge": "Consolider plusieurs UTXOs vers une adresse", "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_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_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_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", "tt_open_dir": "Cliquer pour ouvrir dans l'explorateur de fichiers",
@@ -1619,7 +1819,7 @@
"tt_theme_hotkey": "Raccourci : Ctrl+Gauche/Droite pour changer de 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_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_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_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_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_wallets_button": "Répertoriez vos fichiers de portefeuille et passez de l'un à l'autre", "tt_wallets_button": "Répertoriez vos fichiers de portefeuille et passez de l'un à l'autre",
+214 -14
View File
@@ -60,6 +60,80 @@
"amount_label": "金額:", "amount_label": "金額:",
"animate_avatars": "アバターをアニメーション", "animate_avatars": "アバターをアニメーション",
"appearance": "外観", "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": "マイニング自動シールド", "auto_shield": "マイニング自動シールド",
"av_intro": "マイニングソフトウェアは、望ましくない可能性があるものとしてフラグが立てられることがよくあります。プールマイニングを有効にするには、次の手順に従ってください。", "av_intro": "マイニングソフトウェアは、望ましくない可能性があるものとしてフラグが立てられることがよくあります。プールマイニングを有効にするには、次の手順に従ってください。",
"av_open_security": "Windows セキュリティを開く", "av_open_security": "Windows セキュリティを開く",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender がマイナーをブロックしました", "av_title": "Windows Defender がマイナーをブロックしました",
"available": "利用可能", "available": "利用可能",
"backup_backing_up": "バックアップ中...", "backup_backing_up": "バックアップ中...",
"backup_col_backup": "バックアップ",
"backup_col_export": "エクスポート",
"backup_col_import": "インポートと復元",
"backup_create": "バックアップを作成", "backup_create": "バックアップを作成",
"backup_created": "ウォレットのバックアップを作成しました", "backup_created": "ウォレットのバックアップを作成しました",
"backup_data": "バックアップとデータ", "backup_data": "バックアップとデータ",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "シールド: %.8f", "balance_shielded_fmt": "シールド: %.8f",
"balance_syncing_pct": "同期中 %.1f%%", "balance_syncing_pct": "同期中 %.1f%%",
"balance_transparent_fmt": "透明: %.8f", "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": "ブロック", "ban": "ブロック",
"banned_peers": "ブロック済みピア", "banned_peers": "ブロック済みピア",
"block": "ブロック", "block": "ブロック",
@@ -411,6 +499,7 @@
"contacts_shape_square": "四角", "contacts_shape_square": "四角",
"contacts_shape_tab": "左タブ", "contacts_shape_tab": "左タブ",
"copied": "コピーしました!", "copied": "コピーしました!",
"copied_to_clipboard": "クリップボードにコピーしました",
"copy": "コピー", "copy": "コピー",
"copy_address": "完全なアドレスをコピー", "copy_address": "完全なアドレスをコピー",
"copy_error": "エラーをコピー", "copy_error": "エラーをコピー",
@@ -423,6 +512,7 @@
"daemon_bundled": "バンドル版", "daemon_bundled": "バンドル版",
"daemon_install_bundled": "バンドル版をインストール", "daemon_install_bundled": "バンドル版をインストール",
"daemon_installed": "インストール済み", "daemon_installed": "インストール済み",
"daemon_maintenance_label": "メンテナンス",
"daemon_none_bundled": "このビルドにはなし", "daemon_none_bundled": "このビルドにはなし",
"daemon_not_installed": "未インストール", "daemon_not_installed": "未インストール",
"daemon_status_differ": "インストール済みのバイナリはバンドル版と異なります。", "daemon_status_differ": "インストール済みのバイナリはバンドル版と異なります。",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "ダウンロードは、インストール前にリリースで公開された SHA-256 と固定された ed25519 署名で検証されます。", "daemon_update_verify_note": "ダウンロードは、インストール前にリリースで公開された SHA-256 と固定された ed25519 署名で検証されます。",
"daemon_update_verifying": "検証中…", "daemon_update_verifying": "検証中…",
"daemon_update_version": "バージョン:", "daemon_update_version": "バージョン:",
"daemon_updates_label": "アップデート",
"daemon_version": "デーモン", "daemon_version": "デーモン",
"dark": "ダーク", "dark": "ダーク",
"data_stale_prefix": "更新", "data_stale_prefix": "更新",
@@ -588,6 +679,63 @@
"general": "一般", "general": "一般",
"generating": "生成中", "generating": "生成中",
"go_to_receive": "受信へ移動", "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": "高さ", "height": "高さ",
"help": "ヘルプ", "help": "ヘルプ",
"hidden_tag": " (非表示)", "hidden_tag": " (非表示)",
@@ -1225,12 +1373,52 @@
"sb_waiting_daemon_err": "dragonxd を待機中 — %s", "sb_waiting_daemon_err": "dragonxd を待機中 — %s",
"sb_warming_up": "ウォームアップ中...", "sb_warming_up": "ウォームアップ中...",
"sb_witness_cache": "ウィットネスを再構築中", "sb_witness_cache": "ウィットネスを再構築中",
"scale_effects": "スケールとエフェクト",
"screenshot_open_dir": "場所を開く", "screenshot_open_dir": "場所を開く",
"screenshot_sweep": "スクリーンショットスイープを実行", "screenshot_sweep": "スクリーンショットスイープを実行",
"screenshot_sweep_desc": "すべてのテーマをすべてのタブで巡回し、それぞれのスクリーンショットを設定ディレクトリの screenshots フォルダ内のタブごとのサブフォルダに保存します(前回のスイープを上書きします)。数秒間実行されます。", "screenshot_sweep_desc": "すべてのテーマをすべてのタブで巡回し、それぞれのスクリーンショットを設定ディレクトリの screenshots フォルダ内のタブごとのサブフォルダに保存します(前回のスイープを上書きします)。数秒間実行されます。",
"screenshot_sweep_full": "UI全体スイープ", "screenshot_sweep_full": "UI全体スイープ",
"search_icons": "アイコンを検索...", "search_icons": "アイコンを検索...",
"search_placeholder": "検索...", "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": "セキュリティ", "security": "セキュリティ",
"seed_backup_button": "シードフレーズ", "seed_backup_button": "シードフレーズ",
"seed_backup_close": "閉じる", "seed_backup_close": "閉じる",
@@ -1313,12 +1501,12 @@
"settings": "設定", "settings": "設定",
"settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。", "settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。",
"settings_acrylic_level": "アクリルレベル:", "settings_acrylic_level": "アクリルレベル:",
"settings_address_book": "アドレス帳...", "settings_address_book": "アドレス帳…",
"settings_auto_detected": "DRAGONX.conf から自動検出", "settings_auto_detected": "DRAGONX.conf から自動検出",
"settings_auto_lock": "オートロック", "settings_auto_lock": "オートロック",
"settings_auto_shield_desc": "透明資金を自動的にシールドアドレスに移動", "settings_auto_shield_desc": "透明資金を自動的にシールドアドレスに移動",
"settings_auto_shield_funds": "透明資金を自動シールド", "settings_auto_shield_funds": "透明資金を自動シールド",
"settings_backup": "バックアップ...", "settings_backup": "バックアップ…",
"settings_block_explorer_urls": "ブロックエクスプローラーURL", "settings_block_explorer_urls": "ブロックエクスプローラーURL",
"settings_builtin": "内蔵", "settings_builtin": "内蔵",
"settings_change_passphrase": "パスフレーズを変更", "settings_change_passphrase": "パスフレーズを変更",
@@ -1340,18 +1528,18 @@
"settings_encrypt_first_pin": "PIN を有効にするには、まずウォレットを暗号化してください", "settings_encrypt_first_pin": "PIN を有効にするには、まずウォレットを暗号化してください",
"settings_encrypt_wallet": "ウォレットを暗号化", "settings_encrypt_wallet": "ウォレットを暗号化",
"settings_explorer_hint": "URLには末尾のスラッシュを含めてください。txid/アドレスが追加されます。", "settings_explorer_hint": "URLには末尾のスラッシュを含めてください。txid/アドレスが追加されます。",
"settings_export_all": "すべてエクスポート...", "settings_export_all": "すべてエクスポート…",
"settings_export_csv": "CSV エクスポート...", "settings_export_csv": "CSV エクスポート…",
"settings_export_key": "鍵をエクスポート...", "settings_export_key": "鍵をエクスポート…",
"settings_gradient_bg": "グラデーション背景", "settings_gradient_bg": "グラデーション背景",
"settings_gradient_desc": "テクスチャ背景を滑らかなグラデーションに置換", "settings_gradient_desc": "テクスチャ背景を滑らかなグラデーションに置換",
"settings_idle_after": "経過後", "settings_idle_after": "経過後",
"settings_import_key": "秘密鍵をインポート...", "settings_import_key": "秘密鍵をインポート…",
"settings_import_viewkey": "閲覧鍵をインポート...", "settings_import_viewkey": "閲覧鍵をインポート…",
"settings_language_note": "注意:一部のテキストは更新に再起動が必要です", "settings_language_note": "注意:一部のテキストは更新に再起動が必要です",
"settings_lock_now": "今すぐロック", "settings_lock_now": "今すぐロック",
"settings_locked": "ロック済み", "settings_locked": "ロック済み",
"settings_merge_to_address": "アドレスにマージ...", "settings_merge_to_address": "アドレスにマージ…",
"settings_noise_opacity": "ノイズ不透明度:", "settings_noise_opacity": "ノイズ不透明度:",
"settings_not_connected": "デーモンに接続されていません", "settings_not_connected": "デーモンに接続されていません",
"settings_not_encrypted": "暗号化されていません", "settings_not_encrypted": "暗号化されていません",
@@ -1367,7 +1555,7 @@
"settings_reloaded": "ディスクから設定を再読み込みしました", "settings_reloaded": "ディスクから設定を再読み込みしました",
"settings_remove_encryption": "暗号化を解除", "settings_remove_encryption": "暗号化を解除",
"settings_remove_pin": "PIN を削除", "settings_remove_pin": "PIN を削除",
"settings_request_payment": "支払い請求...", "settings_request_payment": "支払い請求…",
"settings_rescan_desc": "欠落したトランザクションのためにブロックチェーンを再スキャン", "settings_rescan_desc": "欠落したトランザクションのためにブロックチェーンを再スキャン",
"settings_restart_daemon": "デーモンを再起動", "settings_restart_daemon": "デーモンを再起動",
"settings_rpc_connection": "RPC 接続", "settings_rpc_connection": "RPC 接続",
@@ -1378,13 +1566,13 @@
"settings_save_shielded_local": "シールドトランザクション履歴をローカルに保存", "settings_save_shielded_local": "シールドトランザクション履歴をローカルに保存",
"settings_saved": "設定を保存しました", "settings_saved": "設定を保存しました",
"settings_set_pin": "PIN を設定", "settings_set_pin": "PIN を設定",
"settings_shield_mining": "マイニングシールド...", "settings_shield_mining": "マイニングシールド…",
"settings_solid_colors_desc": "ぼかし効果の代わりに単色を使用(アクセシビリティ)", "settings_solid_colors_desc": "ぼかし効果の代わりに単色を使用(アクセシビリティ)",
"settings_theme_refreshed": "テーマ一覧を更新しました", "settings_theme_refreshed": "テーマ一覧を更新しました",
"settings_tor_desc": "プライバシー向上のため全接続を Tor 経由にする", "settings_tor_desc": "プライバシー向上のため全接続を Tor 経由にする",
"settings_unlocked": "ロック解除", "settings_unlocked": "ロック解除",
"settings_use_tor_network": "ネットワーク接続に Tor を使用", "settings_use_tor_network": "ネットワーク接続に Tor を使用",
"settings_validate_address": "アドレス検証...", "settings_validate_address": "アドレス検証…",
"settings_visual_effects": "視覚効果", "settings_visual_effects": "視覚効果",
"settings_wallet_file_size": "ウォレットファイルサイズ:%s", "settings_wallet_file_size": "ウォレットファイルサイズ:%s",
"settings_wallet_info": "ウォレット情報", "settings_wallet_info": "ウォレット情報",
@@ -1452,6 +1640,17 @@
"sweep_to": "集約先:", "sweep_to": "集約先:",
"sweep_toggle": "ウォレットに集約(鍵は保持しない)", "sweep_toggle": "ウォレットに集約(鍵は保持しない)",
"sweep_tx": "取引:", "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_body": "このウォレットは破損しているようです。ノードが開けませんでした。バックアップから復元するか、作り直すか、修復を試してください。",
"switch_corrupt_repair": "修復を試す(salvage)", "switch_corrupt_repair": "修復を試す(salvage)",
"switch_progress_background": "バックグラウンドで続行", "switch_progress_background": "バックグラウンドで続行",
@@ -1491,6 +1690,7 @@
"to_upper": "宛先", "to_upper": "宛先",
"tools": "ツール", "tools": "ツール",
"tools_actions": "ツールとアクション...", "tools_actions": "ツールとアクション...",
"tools_actions_hdr": "ツールと操作",
"total": "合計", "total": "合計",
"total_balance_label": "総残高", "total_balance_label": "総残高",
"transaction_id": "取引ID", "transaction_id": "取引ID",
@@ -1514,7 +1714,7 @@
"tt_auto_shield": "プライバシーのため透明残高を自動的にシールドアドレスに移動", "tt_auto_shield": "プライバシーのため透明残高を自動的にシールドアドレスに移動",
"tt_backup": "wallet.dat のバックアップを作成", "tt_backup": "wallet.dat のバックアップを作成",
"tt_block_explorer": "ブラウザで DragonX ブロックエクスプローラーを開く", "tt_block_explorer": "ブラウザで DragonX ブロックエクスプローラーを開く",
"tt_blur": "ぼかし量(0%% = オフ、100%% = 最大)", "tt_blur": "ぼかし量(0% = オフ、100% = 最大)",
"tt_change_pass": "ウォレットの暗号化パスフレーズを変更", "tt_change_pass": "ウォレットの暗号化パスフレーズを変更",
"tt_change_pin": "アンロック PIN を変更", "tt_change_pin": "アンロック PIN を変更",
"tt_chat_bubble_accent": "送信メッセージの吹き出しのアクセントカラー(または現在のテーマに従う)", "tt_chat_bubble_accent": "送信メッセージの吹き出しのアクセントカラー(または現在のテーマに従う)",
@@ -1577,7 +1777,7 @@
"tt_low_spec": "すべての重い視覚効果を無効化\\nホットキー:Ctrl+Shift+Down", "tt_low_spec": "すべての重い視覚効果を無効化\\nホットキー:Ctrl+Shift+Down",
"tt_merge": "複数の UTXO を一つのアドレスに統合", "tt_merge": "複数の UTXO を一つのアドレスに統合",
"tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始", "tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始",
"tt_noise": "グレインテクスチャ強度(0%% = オフ、100%% = 最大)", "tt_noise": "グレインテクスチャ強度(0% = オフ、100% = 最大)",
"tt_open_app_dir": "ObsidianDragon フォルダ(設定、テーマ、ログ)をファイルマネージャーで開く", "tt_open_app_dir": "ObsidianDragon フォルダ(設定、テーマ、ログ)をファイルマネージャーで開く",
"tt_open_data_dir": "ファイルマネージャーでウォレットとブロックチェーンデータのフォルダを開きます", "tt_open_data_dir": "ファイルマネージャーでウォレットとブロックチェーンデータのフォルダを開きます",
"tt_open_dir": "クリックしてファイルエクスプローラーで開く", "tt_open_dir": "クリックしてファイルエクスプローラーで開く",
@@ -1616,7 +1816,7 @@
"tt_theme_hotkey": "ホットキー:Ctrl+左/右でテーマを切り替え", "tt_theme_hotkey": "ホットキー:Ctrl+左/右でテーマを切り替え",
"tt_tor": "匿名性のためにデーモン接続を Tor ネットワーク経由でルーティング", "tt_tor": "匿名性のためにデーモン接続を Tor ネットワーク経由でルーティング",
"tt_tx_url": "ブロックエクスプローラーでトランザクションを表示するためのベース URL", "tt_tx_url": "ブロックエクスプローラーでトランザクションを表示するためのベース URL",
"tt_ui_opacity": "カードとサイドバーの不透明度(100%% = 完全不透明、低い = より透過)", "tt_ui_opacity": "カードとサイドバーの不透明度(100% = 完全不透明、低い = より透過)",
"tt_validate": "DragonX アドレスが有効かどうかを確認", "tt_validate": "DragonX アドレスが有効かどうかを確認",
"tt_verbose": "詳細な接続診断、デーモン状態、\\nポート所有者情報をコンソールタブに記録", "tt_verbose": "詳細な接続診断、デーモン状態、\\nポート所有者情報をコンソールタブに記録",
"tt_wallets_button": "ウォレットファイルを一覧表示して切り替えます", "tt_wallets_button": "ウォレットファイルを一覧表示して切り替えます",
+216 -16
View File
@@ -60,6 +60,80 @@
"amount_label": "금액:", "amount_label": "금액:",
"animate_avatars": "아바타 애니메이션", "animate_avatars": "아바타 애니메이션",
"appearance": "외관", "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": "채굴 자동 차폐", "auto_shield": "채굴 자동 차폐",
"av_intro": "채굴 소프트웨어는 종종 잠재적으로 원치 않는 항목으로 표시됩니다. 풀 채굴을 활성화하려면 다음 단계를 따르세요:", "av_intro": "채굴 소프트웨어는 종종 잠재적으로 원치 않는 항목으로 표시됩니다. 풀 채굴을 활성화하려면 다음 단계를 따르세요:",
"av_open_security": "Windows 보안 열기", "av_open_security": "Windows 보안 열기",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender가 채굴기를 차단했습니다", "av_title": "Windows Defender가 채굴기를 차단했습니다",
"available": "사용 가능", "available": "사용 가능",
"backup_backing_up": "백업 중...", "backup_backing_up": "백업 중...",
"backup_col_backup": "백업",
"backup_col_export": "내보내기",
"backup_col_import": "가져오기 및 복원",
"backup_create": "백업 생성", "backup_create": "백업 생성",
"backup_created": "지갑 백업이 생성되었습니다", "backup_created": "지갑 백업이 생성되었습니다",
"backup_data": "백업 및 데이터", "backup_data": "백업 및 데이터",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "차폐: %.8f", "balance_shielded_fmt": "차폐: %.8f",
"balance_syncing_pct": "동기화 중 %.1f%%", "balance_syncing_pct": "동기화 중 %.1f%%",
"balance_transparent_fmt": "투명: %.8f", "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": "차단", "ban": "차단",
"banned_peers": "차단된 피어", "banned_peers": "차단된 피어",
"block": "블록", "block": "블록",
@@ -411,6 +499,7 @@
"contacts_shape_square": "사각형", "contacts_shape_square": "사각형",
"contacts_shape_tab": "왼쪽 탭", "contacts_shape_tab": "왼쪽 탭",
"copied": "복사됨!", "copied": "복사됨!",
"copied_to_clipboard": "클립보드에 복사됨",
"copy": "복사", "copy": "복사",
"copy_address": "전체 주소 복사", "copy_address": "전체 주소 복사",
"copy_error": "오류 복사", "copy_error": "오류 복사",
@@ -423,6 +512,7 @@
"daemon_bundled": "번들", "daemon_bundled": "번들",
"daemon_install_bundled": "번들 버전 설치", "daemon_install_bundled": "번들 버전 설치",
"daemon_installed": "설치됨", "daemon_installed": "설치됨",
"daemon_maintenance_label": "유지 관리",
"daemon_none_bundled": "이 빌드에 없음", "daemon_none_bundled": "이 빌드에 없음",
"daemon_not_installed": "설치되지 않음", "daemon_not_installed": "설치되지 않음",
"daemon_status_differ": "설치된 바이너리가 번들 버전과 다릅니다.", "daemon_status_differ": "설치된 바이너리가 번들 버전과 다릅니다.",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "다운로드는 설치 전에 릴리스에 게시된 SHA-256과 고정된 ed25519 서명으로 검증됩니다.", "daemon_update_verify_note": "다운로드는 설치 전에 릴리스에 게시된 SHA-256과 고정된 ed25519 서명으로 검증됩니다.",
"daemon_update_verifying": "확인 중…", "daemon_update_verifying": "확인 중…",
"daemon_update_version": "버전:", "daemon_update_version": "버전:",
"daemon_updates_label": "업데이트",
"daemon_version": "데몬", "daemon_version": "데몬",
"dark": "다크", "dark": "다크",
"data_stale_prefix": "업데이트", "data_stale_prefix": "업데이트",
@@ -588,6 +679,63 @@
"general": "일반", "general": "일반",
"generating": "생성 중", "generating": "생성 중",
"go_to_receive": "수신으로 이동", "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": "높이", "height": "높이",
"help": "도움말", "help": "도움말",
"hidden_tag": " (숨김)", "hidden_tag": " (숨김)",
@@ -1227,12 +1375,52 @@
"sb_waiting_daemon_err": "dragonxd 대기 중 — %s", "sb_waiting_daemon_err": "dragonxd 대기 중 — %s",
"sb_warming_up": "워밍업 중...", "sb_warming_up": "워밍업 중...",
"sb_witness_cache": "증인 재구축 중", "sb_witness_cache": "증인 재구축 중",
"scale_effects": "배율 및 효과",
"screenshot_open_dir": "위치 열기", "screenshot_open_dir": "위치 열기",
"screenshot_sweep": "스크린샷 스윕 실행", "screenshot_sweep": "스크린샷 스윕 실행",
"screenshot_sweep_desc": "모든 탭에 대해 모든 테마를 순회하며 각각의 스크린샷을 설정 디렉터리의 screenshots 폴더 아래 탭별 하위 폴더에 저장합니다(이전 스윕을 덮어씀). 몇 초 동안 실행됩니다.", "screenshot_sweep_desc": "모든 탭에 대해 모든 테마를 순회하며 각각의 스크린샷을 설정 디렉터리의 screenshots 폴더 아래 탭별 하위 폴더에 저장합니다(이전 스윕을 덮어씀). 몇 초 동안 실행됩니다.",
"screenshot_sweep_full": "전체 UI 스윕", "screenshot_sweep_full": "전체 UI 스윕",
"search_icons": "아이콘 검색...", "search_icons": "아이콘 검색...",
"search_placeholder": "검색...", "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": "보안", "security": "보안",
"seed_backup_button": "시드 문구", "seed_backup_button": "시드 문구",
"seed_backup_close": "닫기", "seed_backup_close": "닫기",
@@ -1315,12 +1503,12 @@
"settings": "설정", "settings": "설정",
"settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.", "settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.",
"settings_acrylic_level": "아크릴 레벨:", "settings_acrylic_level": "아크릴 레벨:",
"settings_address_book": "주소록...", "settings_address_book": "주소록…",
"settings_auto_detected": "DRAGONX.conf에서 자동 감지", "settings_auto_detected": "DRAGONX.conf에서 자동 감지",
"settings_auto_lock": "자동 잠금", "settings_auto_lock": "자동 잠금",
"settings_auto_shield_desc": "투명 자금을 자동으로 차폐 주소로 이동", "settings_auto_shield_desc": "투명 자금을 자동으로 차폐 주소로 이동",
"settings_auto_shield_funds": "투명 자금 자동 차폐", "settings_auto_shield_funds": "투명 자금 자동 차폐",
"settings_backup": "백업...", "settings_backup": "백업…",
"settings_block_explorer_urls": "블록 탐색기 URL", "settings_block_explorer_urls": "블록 탐색기 URL",
"settings_builtin": "내장", "settings_builtin": "내장",
"settings_change_passphrase": "비밀번호 변경", "settings_change_passphrase": "비밀번호 변경",
@@ -1334,7 +1522,7 @@
"settings_copy_diagnostics": "진단 정보 복사", "settings_copy_diagnostics": "진단 정보 복사",
"settings_copyright": "Copyright 2024-2026 DragonX 개발자 | GPLv3 라이선스", "settings_copyright": "Copyright 2024-2026 DragonX 개발자 | GPLv3 라이선스",
"settings_custom": "사용자 지정", "settings_custom": "사용자 지정",
"settings_data_dir": "데이터 디렉터리:", "settings_data_dir": "데이터 디렉터리",
"settings_debug_changed": "디버그 카테고리가 변경되었습니다 — 데몬을 재시작하여 적용", "settings_debug_changed": "디버그 카테고리가 변경되었습니다 — 데몬을 재시작하여 적용",
"settings_debug_restart_note": "변경 사항은 데몬을 다시 시작한 후에 적용됩니다.", "settings_debug_restart_note": "변경 사항은 데몬을 다시 시작한 후에 적용됩니다.",
"settings_debug_select": "데몬 디버그 로깅을 활성화할 카테고리를 선택하세요 (-debug= 플래그).", "settings_debug_select": "데몬 디버그 로깅을 활성화할 카테고리를 선택하세요 (-debug= 플래그).",
@@ -1342,18 +1530,18 @@
"settings_encrypt_first_pin": "PIN을 활성화하려면 먼저 지갑을 암호화하세요", "settings_encrypt_first_pin": "PIN을 활성화하려면 먼저 지갑을 암호화하세요",
"settings_encrypt_wallet": "지갑 암호화", "settings_encrypt_wallet": "지갑 암호화",
"settings_explorer_hint": "URL에 후행 슬래시를 포함해야 합니다. txid/주소가 추가됩니다.", "settings_explorer_hint": "URL에 후행 슬래시를 포함해야 합니다. txid/주소가 추가됩니다.",
"settings_export_all": "모두 내보내기...", "settings_export_all": "모두 내보내기…",
"settings_export_csv": "CSV 내보내기...", "settings_export_csv": "CSV 내보내기…",
"settings_export_key": "키 내보내기...", "settings_export_key": "키 내보내기…",
"settings_gradient_bg": "그라데이션 배경", "settings_gradient_bg": "그라데이션 배경",
"settings_gradient_desc": "텍스처 배경을 부드러운 그라데이션으로 교체", "settings_gradient_desc": "텍스처 배경을 부드러운 그라데이션으로 교체",
"settings_idle_after": "후", "settings_idle_after": "후",
"settings_import_key": "개인 키 가져오기...", "settings_import_key": "개인 키 가져오기…",
"settings_import_viewkey": "조회 키 가져오기...", "settings_import_viewkey": "조회 키 가져오기…",
"settings_language_note": "참고: 일부 텍스트는 업데이트하려면 다시 시작해야 합니다", "settings_language_note": "참고: 일부 텍스트는 업데이트하려면 다시 시작해야 합니다",
"settings_lock_now": "지금 잠금", "settings_lock_now": "지금 잠금",
"settings_locked": "잠김", "settings_locked": "잠김",
"settings_merge_to_address": "주소로 병합...", "settings_merge_to_address": "주소로 병합…",
"settings_noise_opacity": "노이즈 불투명도:", "settings_noise_opacity": "노이즈 불투명도:",
"settings_not_connected": "데몬에 연결되지 않음", "settings_not_connected": "데몬에 연결되지 않음",
"settings_not_encrypted": "암호화되지 않음", "settings_not_encrypted": "암호화되지 않음",
@@ -1369,7 +1557,7 @@
"settings_reloaded": "디스크에서 설정을 다시 불러왔습니다", "settings_reloaded": "디스크에서 설정을 다시 불러왔습니다",
"settings_remove_encryption": "암호화 제거", "settings_remove_encryption": "암호화 제거",
"settings_remove_pin": "PIN 제거", "settings_remove_pin": "PIN 제거",
"settings_request_payment": "결제 요청...", "settings_request_payment": "결제 요청…",
"settings_rescan_desc": "누락된 거래를 찾기 위해 블록체인 재스캔", "settings_rescan_desc": "누락된 거래를 찾기 위해 블록체인 재스캔",
"settings_restart_daemon": "데몬 재시작", "settings_restart_daemon": "데몬 재시작",
"settings_rpc_connection": "RPC 연결", "settings_rpc_connection": "RPC 연결",
@@ -1380,20 +1568,20 @@
"settings_save_shielded_local": "차폐 거래 기록을 로컬에 저장", "settings_save_shielded_local": "차폐 거래 기록을 로컬에 저장",
"settings_saved": "설정이 저장되었습니다", "settings_saved": "설정이 저장되었습니다",
"settings_set_pin": "PIN 설정", "settings_set_pin": "PIN 설정",
"settings_shield_mining": "채굴 차폐...", "settings_shield_mining": "채굴 차폐…",
"settings_solid_colors_desc": "블러 효과 대신 단색 사용 (접근성)", "settings_solid_colors_desc": "블러 효과 대신 단색 사용 (접근성)",
"settings_theme_refreshed": "테마 목록을 새로고침했습니다", "settings_theme_refreshed": "테마 목록을 새로고침했습니다",
"settings_tor_desc": "향상된 개인 정보 보호를 위해 모든 연결을 Tor를 통해 라우팅", "settings_tor_desc": "향상된 개인 정보 보호를 위해 모든 연결을 Tor를 통해 라우팅",
"settings_unlocked": "잠금 해제", "settings_unlocked": "잠금 해제",
"settings_use_tor_network": "네트워크 연결에 Tor 사용", "settings_use_tor_network": "네트워크 연결에 Tor 사용",
"settings_validate_address": "주소 확인...", "settings_validate_address": "주소 확인…",
"settings_visual_effects": "시각 효과", "settings_visual_effects": "시각 효과",
"settings_wallet_file_size": "지갑 파일 크기: %s", "settings_wallet_file_size": "지갑 파일 크기: %s",
"settings_wallet_info": "지갑 정보", "settings_wallet_info": "지갑 정보",
"settings_wallet_location": "지갑 위치: %s", "settings_wallet_location": "지갑 위치: %s",
"settings_wallet_maintenance": "지갑 유지보수", "settings_wallet_maintenance": "지갑 유지보수",
"settings_wallet_not_found": "지갑 파일을 찾을 수 없음", "settings_wallet_not_found": "지갑 파일을 찾을 수 없음",
"settings_wallet_size_label": "지갑 크기:", "settings_wallet_size_label": "지갑 크기",
"settings_ztx_cleared": "Z-거래 내역이 삭제되었습니다", "settings_ztx_cleared": "Z-거래 내역이 삭제되었습니다",
"settings_ztx_not_found": "내역 파일을 찾을 수 없습니다", "settings_ztx_not_found": "내역 파일을 찾을 수 없습니다",
"setup_wizard": "설정 마법사", "setup_wizard": "설정 마법사",
@@ -1454,6 +1642,17 @@
"sweep_to": "쓸어담은 주소:", "sweep_to": "쓸어담은 주소:",
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)", "sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
"sweep_tx": "거래:", "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_body": "이 지갑이 손상된 것 같습니다. 노드가 열 수 없습니다. 백업에서 복원하거나 다시 만들거나 복구를 시도하세요.",
"switch_corrupt_repair": "복구 시도(salvage)", "switch_corrupt_repair": "복구 시도(salvage)",
"switch_progress_background": "백그라운드에서 계속", "switch_progress_background": "백그라운드에서 계속",
@@ -1493,6 +1692,7 @@
"to_upper": "받는 곳", "to_upper": "받는 곳",
"tools": "도구", "tools": "도구",
"tools_actions": "도구 및 작업...", "tools_actions": "도구 및 작업...",
"tools_actions_hdr": "도구 및 작업",
"total": "합계", "total": "합계",
"total_balance_label": "총 잔액", "total_balance_label": "총 잔액",
"transaction_id": "거래 ID", "transaction_id": "거래 ID",
@@ -1516,7 +1716,7 @@
"tt_auto_shield": "개인 정보 보호를 위해 투명 잔액을 자동으로 차폐 주소로 이동", "tt_auto_shield": "개인 정보 보호를 위해 투명 잔액을 자동으로 차폐 주소로 이동",
"tt_backup": "wallet.dat 백업 만들기", "tt_backup": "wallet.dat 백업 만들기",
"tt_block_explorer": "브라우저에서 DragonX 블록 탐색기 열기", "tt_block_explorer": "브라우저에서 DragonX 블록 탐색기 열기",
"tt_blur": "블러 양 (0%% = 끔, 100%% = 최대)", "tt_blur": "블러 양 (0% = 끔, 100% = 최대)",
"tt_change_pass": "지갑 암호화 비밀번호 변경", "tt_change_pass": "지갑 암호화 비밀번호 변경",
"tt_change_pin": "잠금 해제 PIN 변경", "tt_change_pin": "잠금 해제 PIN 변경",
"tt_chat_bubble_accent": "보내는 메시지 말풍선의 강조 색상(또는 현재 테마를 따름)", "tt_chat_bubble_accent": "보내는 메시지 말풍선의 강조 색상(또는 현재 테마를 따름)",
@@ -1579,7 +1779,7 @@
"tt_low_spec": "모든 고부하 시각 효과 비활성화\\n단축키: Ctrl+Shift+Down", "tt_low_spec": "모든 고부하 시각 효과 비활성화\\n단축키: Ctrl+Shift+Down",
"tt_merge": "여러 UTXO를 하나의 주소로 통합", "tt_merge": "여러 UTXO를 하나의 주소로 통합",
"tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작", "tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작",
"tt_noise": "그레인 텍스처 강도 (0%% = 끔, 100%% = 최대)", "tt_noise": "그레인 텍스처 강도 (0% = 끔, 100% = 최대)",
"tt_open_app_dir": "파일 관리자에서 ObsidianDragon 폴더(설정, 테마, 로그)를 엽니다", "tt_open_app_dir": "파일 관리자에서 ObsidianDragon 폴더(설정, 테마, 로그)를 엽니다",
"tt_open_data_dir": "지갑 및 블록체인 데이터가 있는 폴더를 파일 탐색기에서 엽니다", "tt_open_data_dir": "지갑 및 블록체인 데이터가 있는 폴더를 파일 탐색기에서 엽니다",
"tt_open_dir": "파일 탐색기에서 열려면 클릭", "tt_open_dir": "파일 탐색기에서 열려면 클릭",
@@ -1618,7 +1818,7 @@
"tt_theme_hotkey": "단축키: Ctrl+왼쪽/오른쪽으로 테마 전환", "tt_theme_hotkey": "단축키: Ctrl+왼쪽/오른쪽으로 테마 전환",
"tt_tor": "익명성을 위해 데몬 연결을 Tor 네트워크를 통해 라우팅", "tt_tor": "익명성을 위해 데몬 연결을 Tor 네트워크를 통해 라우팅",
"tt_tx_url": "블록 탐색기에서 거래를 보기 위한 기본 URL", "tt_tx_url": "블록 탐색기에서 거래를 보기 위한 기본 URL",
"tt_ui_opacity": "카드 및 사이드바 불투명도 (100%% = 완전 불투명, 낮을수록 더 투명)", "tt_ui_opacity": "카드 및 사이드바 불투명도 (100% = 완전 불투명, 낮을수록 더 투명)",
"tt_validate": "DragonX 주소가 유효한지 확인", "tt_validate": "DragonX 주소가 유효한지 확인",
"tt_verbose": "콘솔 탭에 상세 연결 진단,\\n데몬 상태 및 포트 소유자 정보 기록", "tt_verbose": "콘솔 탭에 상세 연결 진단,\\n데몬 상태 및 포트 소유자 정보 기록",
"tt_wallets_button": "지갑 파일 목록을 보고 전환합니다", "tt_wallets_button": "지갑 파일 목록을 보고 전환합니다",
+216 -16
View File
@@ -60,6 +60,80 @@
"amount_label": "Valor:", "amount_label": "Valor:",
"animate_avatars": "Animar avatares", "animate_avatars": "Animar avatares",
"appearance": "APARÊNCIA", "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", "auto_shield": "Auto-blindar mineração",
"av_intro": "Softwares de mineração costumam ser sinalizados como potencialmente indesejados. Siga estes passos para habilitar a mineração em pool:", "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_open_security": "Abrir Segurança do Windows",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender bloqueou o minerador", "av_title": "Windows Defender bloqueou o minerador",
"available": "Disponível", "available": "Disponível",
"backup_backing_up": "Fazendo backup...", "backup_backing_up": "Fazendo backup...",
"backup_col_backup": "BACKUP",
"backup_col_export": "EXPORTAR",
"backup_col_import": "IMPORTAR E RESTAURAR",
"backup_create": "Criar Backup", "backup_create": "Criar Backup",
"backup_created": "Backup da carteira criado", "backup_created": "Backup da carteira criado",
"backup_data": "BACKUP & DADOS", "backup_data": "BACKUP & DADOS",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "Blindado: %.8f", "balance_shielded_fmt": "Blindado: %.8f",
"balance_syncing_pct": "Sincronizando %.1f%%", "balance_syncing_pct": "Sincronizando %.1f%%",
"balance_transparent_fmt": "Transparente: %.8f", "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", "ban": "Banir",
"banned_peers": "Pares Banidos", "banned_peers": "Pares Banidos",
"block": "Bloco", "block": "Bloco",
@@ -411,6 +499,7 @@
"contacts_shape_square": "Quadrado", "contacts_shape_square": "Quadrado",
"contacts_shape_tab": "Aba", "contacts_shape_tab": "Aba",
"copied": "Copiado!", "copied": "Copiado!",
"copied_to_clipboard": "Copiado para a área de transferência",
"copy": "Copiar", "copy": "Copiar",
"copy_address": "Copiar Endereço Completo", "copy_address": "Copiar Endereço Completo",
"copy_error": "Copiar Erro", "copy_error": "Copiar Erro",
@@ -423,6 +512,7 @@
"daemon_bundled": "Empacotado", "daemon_bundled": "Empacotado",
"daemon_install_bundled": "Instalar incluído", "daemon_install_bundled": "Instalar incluído",
"daemon_installed": "Instalado", "daemon_installed": "Instalado",
"daemon_maintenance_label": "MANUTENÇÃO",
"daemon_none_bundled": "nenhum nesta build", "daemon_none_bundled": "nenhum nesta build",
"daemon_not_installed": "não instalado", "daemon_not_installed": "não instalado",
"daemon_status_differ": "O binário instalado difere da versão empacotada.", "daemon_status_differ": "O binário instalado difere da versão empacotada.",
@@ -458,6 +548,7 @@
"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_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_verifying": "Verificando…",
"daemon_update_version": "Versão:", "daemon_update_version": "Versão:",
"daemon_updates_label": "ATUALIZAÇÕES",
"daemon_version": "Daemon", "daemon_version": "Daemon",
"dark": "Escuro", "dark": "Escuro",
"data_stale_prefix": "Atualizado", "data_stale_prefix": "Atualizado",
@@ -588,6 +679,63 @@
"general": "Geral", "general": "Geral",
"generating": "Gerando", "generating": "Gerando",
"go_to_receive": "Ir para Receber", "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", "height": "Altura",
"help": "Ajuda", "help": "Ajuda",
"hidden_tag": " (oculto)", "hidden_tag": " (oculto)",
@@ -1228,12 +1376,52 @@
"sb_waiting_daemon_err": "Aguardando dragonxd — %s", "sb_waiting_daemon_err": "Aguardando dragonxd — %s",
"sb_warming_up": "Aquecendo...", "sb_warming_up": "Aquecendo...",
"sb_witness_cache": "Reconstruindo testemunhas", "sb_witness_cache": "Reconstruindo testemunhas",
"scale_effects": "ESCALA E EFEITOS",
"screenshot_open_dir": "Abrir local", "screenshot_open_dir": "Abrir local",
"screenshot_sweep": "Executar varredura de capturas de tela", "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_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", "screenshot_sweep_full": "Varredura completa da interface",
"search_icons": "Pesquisar ícones...", "search_icons": "Pesquisar ícones...",
"search_placeholder": "Pesquisar...", "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", "security": "SEGURANÇA",
"seed_backup_button": "Frase de recuperação", "seed_backup_button": "Frase de recuperação",
"seed_backup_close": "Fechar", "seed_backup_close": "Fechar",
@@ -1316,12 +1504,12 @@
"settings": "Ajustes", "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_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_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_detected": "Detectado automaticamente de DRAGONX.conf",
"settings_auto_lock": "BLOQUEIO AUTOMÁTICO", "settings_auto_lock": "BLOQUEIO AUTOMÁTICO",
"settings_auto_shield_desc": "Mover automaticamente fundos transparentes para endereços blindados", "settings_auto_shield_desc": "Mover automaticamente fundos transparentes para endereços blindados",
"settings_auto_shield_funds": "Blindar fundos transparentes automaticamente", "settings_auto_shield_funds": "Blindar fundos transparentes automaticamente",
"settings_backup": "Backup...", "settings_backup": "Backup…",
"settings_block_explorer_urls": "URLs do explorador de blocos", "settings_block_explorer_urls": "URLs do explorador de blocos",
"settings_builtin": "Integrado", "settings_builtin": "Integrado",
"settings_change_passphrase": "Alterar frase secreta", "settings_change_passphrase": "Alterar frase secreta",
@@ -1335,7 +1523,7 @@
"settings_copy_diagnostics": "Copiar diagnósticos", "settings_copy_diagnostics": "Copiar diagnósticos",
"settings_copyright": "Copyright 2024-2026 Desenvolvedores DragonX | Licença GPLv3", "settings_copyright": "Copyright 2024-2026 Desenvolvedores DragonX | Licença GPLv3",
"settings_custom": "Personalizado", "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_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_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=).", "settings_debug_select": "Selecione categorias para ativar o registro de depuração do daemon (flags -debug=).",
@@ -1343,18 +1531,18 @@
"settings_encrypt_first_pin": "Encripte a carteira primeiro para ativar o PIN", "settings_encrypt_first_pin": "Encripte a carteira primeiro para ativar o PIN",
"settings_encrypt_wallet": "Encriptar carteira", "settings_encrypt_wallet": "Encriptar carteira",
"settings_explorer_hint": "As URLs devem incluir uma barra final. O txid/endereço será adicionado.", "settings_explorer_hint": "As URLs devem incluir uma barra final. O txid/endereço será adicionado.",
"settings_export_all": "Exportar tudo...", "settings_export_all": "Exportar tudo…",
"settings_export_csv": "Exportar CSV...", "settings_export_csv": "Exportar CSV…",
"settings_export_key": "Exportar chave...", "settings_export_key": "Exportar chave…",
"settings_gradient_bg": "Fundo gradiente", "settings_gradient_bg": "Fundo gradiente",
"settings_gradient_desc": "Substituir fundos texturizados por gradientes suaves", "settings_gradient_desc": "Substituir fundos texturizados por gradientes suaves",
"settings_idle_after": "após", "settings_idle_after": "após",
"settings_import_key": "Importar Chave Privada...", "settings_import_key": "Importar Chave Privada…",
"settings_import_viewkey": "Importar chave de visualização...", "settings_import_viewkey": "Importar chave de visualização…",
"settings_language_note": "Nota: Alguns textos requerem reinício para atualizar", "settings_language_note": "Nota: Alguns textos requerem reinício para atualizar",
"settings_lock_now": "Bloquear agora", "settings_lock_now": "Bloquear agora",
"settings_locked": "Bloqueado", "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_noise_opacity": "Opacidade do ruído:",
"settings_not_connected": "Não conectado ao daemon", "settings_not_connected": "Não conectado ao daemon",
"settings_not_encrypted": "Não encriptado", "settings_not_encrypted": "Não encriptado",
@@ -1370,7 +1558,7 @@
"settings_reloaded": "Configurações recarregadas do disco", "settings_reloaded": "Configurações recarregadas do disco",
"settings_remove_encryption": "Remover encriptação", "settings_remove_encryption": "Remover encriptação",
"settings_remove_pin": "Remover PIN", "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_rescan_desc": "Reescanear a blockchain em busca de transações ausentes",
"settings_restart_daemon": "Reiniciar daemon", "settings_restart_daemon": "Reiniciar daemon",
"settings_rpc_connection": "Conexão RPC", "settings_rpc_connection": "Conexão RPC",
@@ -1381,20 +1569,20 @@
"settings_save_shielded_local": "Salvar histórico de transações blindadas localmente", "settings_save_shielded_local": "Salvar histórico de transações blindadas localmente",
"settings_saved": "Configurações salvas", "settings_saved": "Configurações salvas",
"settings_set_pin": "Definir PIN", "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_solid_colors_desc": "Usar cores sólidas em vez de efeitos de desfoque (acessibilidade)",
"settings_theme_refreshed": "Lista de temas atualizada", "settings_theme_refreshed": "Lista de temas atualizada",
"settings_tor_desc": "Rotear todas as conexões através do Tor para maior privacidade", "settings_tor_desc": "Rotear todas as conexões através do Tor para maior privacidade",
"settings_unlocked": "Desbloqueado", "settings_unlocked": "Desbloqueado",
"settings_use_tor_network": "Usar Tor para conexões de rede", "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_visual_effects": "Efeitos visuais",
"settings_wallet_file_size": "Tamanho do arquivo da carteira: %s", "settings_wallet_file_size": "Tamanho do arquivo da carteira: %s",
"settings_wallet_info": "Informações da carteira", "settings_wallet_info": "Informações da carteira",
"settings_wallet_location": "Localização da carteira: %s", "settings_wallet_location": "Localização da carteira: %s",
"settings_wallet_maintenance": "Manutenção da carteira", "settings_wallet_maintenance": "Manutenção da carteira",
"settings_wallet_not_found": "Arquivo da carteira não encontrado", "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_cleared": "Histórico de transações Z limpo",
"settings_ztx_not_found": "Nenhum arquivo de histórico encontrado", "settings_ztx_not_found": "Nenhum arquivo de histórico encontrado",
"setup_wizard": "Assistente de Configuração", "setup_wizard": "Assistente de Configuração",
@@ -1455,6 +1643,17 @@
"sweep_to": "Varrido para:", "sweep_to": "Varrido para:",
"sweep_toggle": "Varrer para minha carteira (não manter a chave)", "sweep_toggle": "Varrer para minha carteira (não manter a chave)",
"sweep_tx": "Transação:", "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_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_corrupt_repair": "Tentar reparar (salvage)",
"switch_progress_background": "Continuar em segundo plano", "switch_progress_background": "Continuar em segundo plano",
@@ -1494,6 +1693,7 @@
"to_upper": "PARA", "to_upper": "PARA",
"tools": "FERRAMENTAS", "tools": "FERRAMENTAS",
"tools_actions": "Ferramentas e Ações...", "tools_actions": "Ferramentas e Ações...",
"tools_actions_hdr": "FERRAMENTAS E AÇÕES",
"total": "Total", "total": "Total",
"total_balance_label": "Saldo Total", "total_balance_label": "Saldo Total",
"transaction_id": "ID DA TRANSAÇÃO", "transaction_id": "ID DA TRANSAÇÃO",
@@ -1517,7 +1717,7 @@
"tt_auto_shield": "Mover automaticamente o saldo transparente para endereços blindados para privacidade", "tt_auto_shield": "Mover automaticamente o saldo transparente para endereços blindados para privacidade",
"tt_backup": "Criar um backup do seu wallet.dat", "tt_backup": "Criar um backup do seu wallet.dat",
"tt_block_explorer": "Abrir o explorador de blocos DragonX no seu navegador", "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_pass": "Alterar a frase secreta de encriptação da carteira",
"tt_change_pin": "Alterar seu PIN de desbloqueio", "tt_change_pin": "Alterar seu PIN de desbloqueio",
"tt_chat_bubble_accent": "Cor de destaque para seus balões de mensagem enviados (ou seguir o tema atual)", "tt_chat_bubble_accent": "Cor de destaque para seus balões de mensagem enviados (ou seguir o tema atual)",
@@ -1580,7 +1780,7 @@
"tt_low_spec": "Desativar todos os efeitos visuais pesados\\nAtalho: Ctrl+Shift+Down", "tt_low_spec": "Desativar todos os efeitos visuais pesados\\nAtalho: Ctrl+Shift+Down",
"tt_merge": "Consolidar múltiplos UTXOs em um endereço", "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_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_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_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", "tt_open_dir": "Clique para abrir no explorador de arquivos",
@@ -1619,7 +1819,7 @@
"tt_theme_hotkey": "Atalho: Ctrl+Esquerda/Direita para alternar temas", "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_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_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_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_verbose": "Registrar diagnósticos detalhados de conexão,\\nestado do daemon e info de proprietário de porta\\nna aba Console",
"tt_wallets_button": "Liste os arquivos de carteira e alterne entre eles", "tt_wallets_button": "Liste os arquivos de carteira e alterne entre eles",
+216 -16
View File
@@ -60,6 +60,80 @@
"amount_label": "Сумма:", "amount_label": "Сумма:",
"animate_avatars": "Анимировать аватары", "animate_avatars": "Анимировать аватары",
"appearance": "ВНЕШНИЙ ВИД", "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": "Авто-экранирование майнинга", "auto_shield": "Авто-экранирование майнинга",
"av_intro": "Программы для майнинга часто помечаются как потенциально нежелательные. Выполните эти шаги, чтобы включить пул-майнинг:", "av_intro": "Программы для майнинга часто помечаются как потенциально нежелательные. Выполните эти шаги, чтобы включить пул-майнинг:",
"av_open_security": "Открыть Безопасность Windows", "av_open_security": "Открыть Безопасность Windows",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender заблокировал майнер", "av_title": "Windows Defender заблокировал майнер",
"available": "Доступно", "available": "Доступно",
"backup_backing_up": "Создание резервной копии...", "backup_backing_up": "Создание резервной копии...",
"backup_col_backup": "РЕЗЕРВНАЯ КОПИЯ",
"backup_col_export": "ЭКСПОРТ",
"backup_col_import": "ИМПОРТ И ВОССТАНОВЛЕНИЕ",
"backup_create": "Создать резервную копию", "backup_create": "Создать резервную копию",
"backup_created": "Резервная копия кошелька создана", "backup_created": "Резервная копия кошелька создана",
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ", "backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "Экранировано: %.8f", "balance_shielded_fmt": "Экранировано: %.8f",
"balance_syncing_pct": "Синхронизация %.1f%%", "balance_syncing_pct": "Синхронизация %.1f%%",
"balance_transparent_fmt": "Прозрачный: %.8f", "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": "Заблокировать", "ban": "Заблокировать",
"banned_peers": "Заблокированные узлы", "banned_peers": "Заблокированные узлы",
"block": "Блок", "block": "Блок",
@@ -411,6 +499,7 @@
"contacts_shape_square": "Квадрат", "contacts_shape_square": "Квадрат",
"contacts_shape_tab": "Вкладка", "contacts_shape_tab": "Вкладка",
"copied": "Скопировано!", "copied": "Скопировано!",
"copied_to_clipboard": "Скопировано в буфер обмена",
"copy": "Копировать", "copy": "Копировать",
"copy_address": "Копировать полный адрес", "copy_address": "Копировать полный адрес",
"copy_error": "Копировать ошибку", "copy_error": "Копировать ошибку",
@@ -423,6 +512,7 @@
"daemon_bundled": "Встроенный", "daemon_bundled": "Встроенный",
"daemon_install_bundled": "Установить встроенную", "daemon_install_bundled": "Установить встроенную",
"daemon_installed": "Установлено", "daemon_installed": "Установлено",
"daemon_maintenance_label": "ОБСЛУЖИВАНИЕ",
"daemon_none_bundled": "нет в этой сборке", "daemon_none_bundled": "нет в этой сборке",
"daemon_not_installed": "не установлен", "daemon_not_installed": "не установлен",
"daemon_status_differ": "Установленный бинарный файл отличается от встроенной версии.", "daemon_status_differ": "Установленный бинарный файл отличается от встроенной версии.",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "Перед установкой загрузка проверяется по опубликованному для релиза SHA-256 и закреплённой подписи ed25519.", "daemon_update_verify_note": "Перед установкой загрузка проверяется по опубликованному для релиза SHA-256 и закреплённой подписи ed25519.",
"daemon_update_verifying": "Проверка…", "daemon_update_verifying": "Проверка…",
"daemon_update_version": "Версия:", "daemon_update_version": "Версия:",
"daemon_updates_label": "ОБНОВЛЕНИЯ",
"daemon_version": "Демон", "daemon_version": "Демон",
"dark": "Тёмная", "dark": "Тёмная",
"data_stale_prefix": "Обновлено", "data_stale_prefix": "Обновлено",
@@ -588,6 +679,63 @@
"general": "Общие", "general": "Общие",
"generating": "Генерация", "generating": "Генерация",
"go_to_receive": "Перейти к получению", "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": "Высота", "height": "Высота",
"help": "Справка", "help": "Справка",
"hidden_tag": " (скрыт)", "hidden_tag": " (скрыт)",
@@ -1228,12 +1376,52 @@
"sb_waiting_daemon_err": "Ожидание dragonxd — %s", "sb_waiting_daemon_err": "Ожидание dragonxd — %s",
"sb_warming_up": "Прогрев...", "sb_warming_up": "Прогрев...",
"sb_witness_cache": "Перестроение свидетелей", "sb_witness_cache": "Перестроение свидетелей",
"scale_effects": "МАСШТАБ И ЭФФЕКТЫ",
"screenshot_open_dir": "Открыть расположение", "screenshot_open_dir": "Открыть расположение",
"screenshot_sweep": "Запустить прогон скриншотов", "screenshot_sweep": "Запустить прогон скриншотов",
"screenshot_sweep_desc": "Перебирает каждую тему по всем вкладкам и сохраняет скриншот каждой в подпапки по вкладкам внутри папки screenshots в каталоге конфигурации (перезаписывая предыдущий проход). Выполняется несколько секунд.", "screenshot_sweep_desc": "Перебирает каждую тему по всем вкладкам и сохраняет скриншот каждой в подпапки по вкладкам внутри папки screenshots в каталоге конфигурации (перезаписывая предыдущий проход). Выполняется несколько секунд.",
"screenshot_sweep_full": "Полный обход интерфейса", "screenshot_sweep_full": "Полный обход интерфейса",
"search_icons": "Поиск значков...", "search_icons": "Поиск значков...",
"search_placeholder": "Поиск...", "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": "БЕЗОПАСНОСТЬ", "security": "БЕЗОПАСНОСТЬ",
"seed_backup_button": "Сид-фраза", "seed_backup_button": "Сид-фраза",
"seed_backup_close": "Закрыть", "seed_backup_close": "Закрыть",
@@ -1316,12 +1504,12 @@
"settings": "Настройки", "settings": "Настройки",
"settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.", "settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.",
"settings_acrylic_level": "Уровень акрила:", "settings_acrylic_level": "Уровень акрила:",
"settings_address_book": "Адресная книга...", "settings_address_book": "Адресная книга…",
"settings_auto_detected": "Автоопределено из DRAGONX.conf", "settings_auto_detected": "Автоопределено из DRAGONX.conf",
"settings_auto_lock": "АВТОБЛОКИРОВКА", "settings_auto_lock": "АВТОБЛОКИРОВКА",
"settings_auto_shield_desc": "Автоматически перемещать прозрачные средства на экранированные адреса", "settings_auto_shield_desc": "Автоматически перемещать прозрачные средства на экранированные адреса",
"settings_auto_shield_funds": "Автоматически экранировать прозрачные средства", "settings_auto_shield_funds": "Автоматически экранировать прозрачные средства",
"settings_backup": "Резервная копия...", "settings_backup": "Резервная копия…",
"settings_block_explorer_urls": "URL-адреса обозревателя блоков", "settings_block_explorer_urls": "URL-адреса обозревателя блоков",
"settings_builtin": "Встроенные", "settings_builtin": "Встроенные",
"settings_change_passphrase": "Сменить пароль", "settings_change_passphrase": "Сменить пароль",
@@ -1335,7 +1523,7 @@
"settings_copy_diagnostics": "Копировать диагностику", "settings_copy_diagnostics": "Копировать диагностику",
"settings_copyright": "Copyright 2024-2026 Разработчики DragonX | Лицензия GPLv3", "settings_copyright": "Copyright 2024-2026 Разработчики DragonX | Лицензия GPLv3",
"settings_custom": "Пользовательские", "settings_custom": "Пользовательские",
"settings_data_dir": "Каталог данных:", "settings_data_dir": "Каталог данных",
"settings_debug_changed": "Категории отладки изменены — перезапустите демон для применения", "settings_debug_changed": "Категории отладки изменены — перезапустите демон для применения",
"settings_debug_restart_note": "Изменения вступают в силу после перезапуска демона.", "settings_debug_restart_note": "Изменения вступают в силу после перезапуска демона.",
"settings_debug_select": "Выберите категории для включения журнала отладки демона (флаги -debug=).", "settings_debug_select": "Выберите категории для включения журнала отладки демона (флаги -debug=).",
@@ -1343,18 +1531,18 @@
"settings_encrypt_first_pin": "Сначала зашифруйте кошелёк, чтобы включить PIN", "settings_encrypt_first_pin": "Сначала зашифруйте кошелёк, чтобы включить PIN",
"settings_encrypt_wallet": "Зашифровать кошелёк", "settings_encrypt_wallet": "Зашифровать кошелёк",
"settings_explorer_hint": "URL-адреса должны заканчиваться косой чертой. Txid/адрес будет добавлен.", "settings_explorer_hint": "URL-адреса должны заканчиваться косой чертой. Txid/адрес будет добавлен.",
"settings_export_all": "Экспортировать все...", "settings_export_all": "Экспортировать все…",
"settings_export_csv": "Экспорт CSV...", "settings_export_csv": "Экспорт CSV…",
"settings_export_key": "Экспортировать ключ...", "settings_export_key": "Экспортировать ключ…",
"settings_gradient_bg": "Градиент фона", "settings_gradient_bg": "Градиент фона",
"settings_gradient_desc": "Заменить текстурные фоны плавными градиентами", "settings_gradient_desc": "Заменить текстурные фоны плавными градиентами",
"settings_idle_after": "через", "settings_idle_after": "через",
"settings_import_key": "Импорт приватного ключа...", "settings_import_key": "Импорт приватного ключа…",
"settings_import_viewkey": "Импортировать ключ просмотра...", "settings_import_viewkey": "Импортировать ключ просмотра…",
"settings_language_note": "Примечание: Некоторый текст требует перезапуска для обновления", "settings_language_note": "Примечание: Некоторый текст требует перезапуска для обновления",
"settings_lock_now": "Заблокировать сейчас", "settings_lock_now": "Заблокировать сейчас",
"settings_locked": "Заблокирован", "settings_locked": "Заблокирован",
"settings_merge_to_address": "Объединить на адрес...", "settings_merge_to_address": "Объединить на адрес…",
"settings_noise_opacity": "Непрозрачность шума:", "settings_noise_opacity": "Непрозрачность шума:",
"settings_not_connected": "Нет соединения с демоном", "settings_not_connected": "Нет соединения с демоном",
"settings_not_encrypted": "Не зашифрован", "settings_not_encrypted": "Не зашифрован",
@@ -1370,7 +1558,7 @@
"settings_reloaded": "Настройки перезагружены с диска", "settings_reloaded": "Настройки перезагружены с диска",
"settings_remove_encryption": "Удалить шифрование", "settings_remove_encryption": "Удалить шифрование",
"settings_remove_pin": "Удалить PIN", "settings_remove_pin": "Удалить PIN",
"settings_request_payment": "Запросить платёж...", "settings_request_payment": "Запросить платёж…",
"settings_rescan_desc": "Пересканировать блокчейн для поиска пропущенных транзакций", "settings_rescan_desc": "Пересканировать блокчейн для поиска пропущенных транзакций",
"settings_restart_daemon": "Перезапустить демон", "settings_restart_daemon": "Перезапустить демон",
"settings_rpc_connection": "RPC-соединение", "settings_rpc_connection": "RPC-соединение",
@@ -1381,20 +1569,20 @@
"settings_save_shielded_local": "Сохранять историю защищённых транзакций локально", "settings_save_shielded_local": "Сохранять историю защищённых транзакций локально",
"settings_saved": "Настройки сохранены", "settings_saved": "Настройки сохранены",
"settings_set_pin": "Установить PIN", "settings_set_pin": "Установить PIN",
"settings_shield_mining": "Экранировать майнинг...", "settings_shield_mining": "Экранировать майнинг…",
"settings_solid_colors_desc": "Использовать сплошные цвета вместо эффектов размытия (доступность)", "settings_solid_colors_desc": "Использовать сплошные цвета вместо эффектов размытия (доступность)",
"settings_theme_refreshed": "Список тем обновлён", "settings_theme_refreshed": "Список тем обновлён",
"settings_tor_desc": "Маршрутизировать все соединения через Tor для повышения конфиденциальности", "settings_tor_desc": "Маршрутизировать все соединения через Tor для повышения конфиденциальности",
"settings_unlocked": "Разблокирован", "settings_unlocked": "Разблокирован",
"settings_use_tor_network": "Использовать Tor для сетевых подключений", "settings_use_tor_network": "Использовать Tor для сетевых подключений",
"settings_validate_address": "Проверить адрес...", "settings_validate_address": "Проверить адрес…",
"settings_visual_effects": "Визуальные эффекты", "settings_visual_effects": "Визуальные эффекты",
"settings_wallet_file_size": "Размер файла кошелька: %s", "settings_wallet_file_size": "Размер файла кошелька: %s",
"settings_wallet_info": "Информация о кошельке", "settings_wallet_info": "Информация о кошельке",
"settings_wallet_location": "Расположение кошелька: %s", "settings_wallet_location": "Расположение кошелька: %s",
"settings_wallet_maintenance": "Обслуживание кошелька", "settings_wallet_maintenance": "Обслуживание кошелька",
"settings_wallet_not_found": "Файл кошелька не найден", "settings_wallet_not_found": "Файл кошелька не найден",
"settings_wallet_size_label": "Размер кошелька:", "settings_wallet_size_label": "Размер кошелька",
"settings_ztx_cleared": "История Z-транзакций очищена", "settings_ztx_cleared": "История Z-транзакций очищена",
"settings_ztx_not_found": "Файл истории не найден", "settings_ztx_not_found": "Файл истории не найден",
"setup_wizard": "Мастер настройки", "setup_wizard": "Мастер настройки",
@@ -1455,6 +1643,17 @@
"sweep_to": "Переведено на:", "sweep_to": "Переведено на:",
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)", "sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
"sweep_tx": "Транзакция:", "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_body": "Похоже, этот кошелёк повреждён — узел не смог его открыть. Восстановите из резервной копии, создайте заново или попробуйте восстановить.",
"switch_corrupt_repair": "Попробовать восстановить (salvage)", "switch_corrupt_repair": "Попробовать восстановить (salvage)",
"switch_progress_background": "Продолжить в фоне", "switch_progress_background": "Продолжить в фоне",
@@ -1494,6 +1693,7 @@
"to_upper": "КОМУ", "to_upper": "КОМУ",
"tools": "УТИЛИТЫ", "tools": "УТИЛИТЫ",
"tools_actions": "Инструменты и действия...", "tools_actions": "Инструменты и действия...",
"tools_actions_hdr": "ИНСТРУМЕНТЫ И ДЕЙСТВИЯ",
"total": "Итого", "total": "Итого",
"total_balance_label": "Общий баланс", "total_balance_label": "Общий баланс",
"transaction_id": "ID ТРАНЗАКЦИИ", "transaction_id": "ID ТРАНЗАКЦИИ",
@@ -1517,7 +1717,7 @@
"tt_auto_shield": "Автоматически перемещать прозрачный баланс на экранированные адреса для конфиденциальности", "tt_auto_shield": "Автоматически перемещать прозрачный баланс на экранированные адреса для конфиденциальности",
"tt_backup": "Создать резервную копию вашего wallet.dat", "tt_backup": "Создать резервную копию вашего wallet.dat",
"tt_block_explorer": "Открыть обозреватель блоков DragonX в браузере", "tt_block_explorer": "Открыть обозреватель блоков DragonX в браузере",
"tt_blur": "Степень размытия (0%% = выкл., 100%% = максимум)", "tt_blur": "Степень размытия (0% = выкл., 100% = максимум)",
"tt_change_pass": "Сменить пароль шифрования кошелька", "tt_change_pass": "Сменить пароль шифрования кошелька",
"tt_change_pin": "Изменить PIN-код разблокировки", "tt_change_pin": "Изменить PIN-код разблокировки",
"tt_chat_bubble_accent": "Акцентный цвет для ваших исходящих пузырьков сообщений (или следовать текущей теме)", "tt_chat_bubble_accent": "Акцентный цвет для ваших исходящих пузырьков сообщений (или следовать текущей теме)",
@@ -1580,7 +1780,7 @@
"tt_low_spec": "Отключить все тяжёлые визуальные эффекты\\nГорячая клавиша: Ctrl+Shift+Down", "tt_low_spec": "Отключить все тяжёлые визуальные эффекты\\nГорячая клавиша: Ctrl+Shift+Down",
"tt_merge": "Объединить несколько UTXO в один адрес", "tt_merge": "Объединить несколько UTXO в один адрес",
"tt_mine_idle": "Автоматически начать майнинг при\\nпростое системы (нет ввода с клавиатуры/мыши)", "tt_mine_idle": "Автоматически начать майнинг при\\nпростое системы (нет ввода с клавиатуры/мыши)",
"tt_noise": "Интенсивность зернистой текстуры (0%% = выкл., 100%% = максимум)", "tt_noise": "Интенсивность зернистой текстуры (0% = выкл., 100% = максимум)",
"tt_open_app_dir": "Открыть папку ObsidianDragon (настройки, темы, логи) в файловом менеджере", "tt_open_app_dir": "Открыть папку ObsidianDragon (настройки, темы, логи) в файловом менеджере",
"tt_open_data_dir": "Открыть в файловом менеджере папку с данными кошелька и блокчейна", "tt_open_data_dir": "Открыть в файловом менеджере папку с данными кошелька и блокчейна",
"tt_open_dir": "Нажмите, чтобы открыть в проводнике", "tt_open_dir": "Нажмите, чтобы открыть в проводнике",
@@ -1619,7 +1819,7 @@
"tt_theme_hotkey": "Горячая клавиша: Ctrl+Влево/Вправо для переключения тем", "tt_theme_hotkey": "Горячая клавиша: Ctrl+Влево/Вправо для переключения тем",
"tt_tor": "Маршрутизировать подключения демона через сеть Tor для анонимности", "tt_tor": "Маршрутизировать подключения демона через сеть Tor для анонимности",
"tt_tx_url": "Базовый URL для просмотра транзакций в обозревателе блоков", "tt_tx_url": "Базовый URL для просмотра транзакций в обозревателе блоков",
"tt_ui_opacity": "Непрозрачность карточек и боковой панели (100%% = полностью непрозрачно, ниже = прозрачнее)", "tt_ui_opacity": "Непрозрачность карточек и боковой панели (100% = полностью непрозрачно, ниже = прозрачнее)",
"tt_validate": "Проверить, действителен ли адрес DragonX", "tt_validate": "Проверить, действителен ли адрес DragonX",
"tt_verbose": "Записывать подробную диагностику подключений,\\nсостояние демона и информацию о владельце порта\\nна вкладке Консоль", "tt_verbose": "Записывать подробную диагностику подключений,\\nсостояние демона и информацию о владельце порта\\nна вкладке Консоль",
"tt_wallets_button": "Показать файлы кошельков и переключаться между ними", "tt_wallets_button": "Показать файлы кошельков и переключаться между ними",
+214 -14
View File
@@ -60,6 +60,80 @@
"amount_label": "金额:", "amount_label": "金额:",
"animate_avatars": "动画头像", "animate_avatars": "动画头像",
"appearance": "外观", "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": "自动屏蔽挖矿", "auto_shield": "自动屏蔽挖矿",
"av_intro": "挖矿软件经常被标记为潜在有害程序。请按照以下步骤启用矿池挖矿:", "av_intro": "挖矿软件经常被标记为潜在有害程序。请按照以下步骤启用矿池挖矿:",
"av_open_security": "打开 Windows 安全中心", "av_open_security": "打开 Windows 安全中心",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender 已阻止矿工程序", "av_title": "Windows Defender 已阻止矿工程序",
"available": "可用", "available": "可用",
"backup_backing_up": "正在备份...", "backup_backing_up": "正在备份...",
"backup_col_backup": "备份",
"backup_col_export": "导出",
"backup_col_import": "导入与恢复",
"backup_create": "创建备份", "backup_create": "创建备份",
"backup_created": "钱包备份已创建", "backup_created": "钱包备份已创建",
"backup_data": "备份与数据", "backup_data": "备份与数据",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "屏蔽:%.8f", "balance_shielded_fmt": "屏蔽:%.8f",
"balance_syncing_pct": "同步中 %.1f%%", "balance_syncing_pct": "同步中 %.1f%%",
"balance_transparent_fmt": "透明:%.8f", "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": "封禁", "ban": "封禁",
"banned_peers": "已封禁节点", "banned_peers": "已封禁节点",
"block": "区块", "block": "区块",
@@ -411,6 +499,7 @@
"contacts_shape_square": "方形", "contacts_shape_square": "方形",
"contacts_shape_tab": "左标签", "contacts_shape_tab": "左标签",
"copied": "已复制!", "copied": "已复制!",
"copied_to_clipboard": "已复制到剪贴板",
"copy": "复制", "copy": "复制",
"copy_address": "复制完整地址", "copy_address": "复制完整地址",
"copy_error": "复制错误", "copy_error": "复制错误",
@@ -423,6 +512,7 @@
"daemon_bundled": "内置", "daemon_bundled": "内置",
"daemon_install_bundled": "安装内置版本", "daemon_install_bundled": "安装内置版本",
"daemon_installed": "已安装", "daemon_installed": "已安装",
"daemon_maintenance_label": "维护",
"daemon_none_bundled": "此版本未内置", "daemon_none_bundled": "此版本未内置",
"daemon_not_installed": "未安装", "daemon_not_installed": "未安装",
"daemon_status_differ": "已安装的程序文件与内置版本不同。", "daemon_status_differ": "已安装的程序文件与内置版本不同。",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "在安装前,会根据该版本发布的 SHA-256 和固定的 ed25519 签名对下载内容进行校验。", "daemon_update_verify_note": "在安装前,会根据该版本发布的 SHA-256 和固定的 ed25519 签名对下载内容进行校验。",
"daemon_update_verifying": "正在验证…", "daemon_update_verifying": "正在验证…",
"daemon_update_version": "版本:", "daemon_update_version": "版本:",
"daemon_updates_label": "更新",
"daemon_version": "守护进程", "daemon_version": "守护进程",
"dark": "深色", "dark": "深色",
"data_stale_prefix": "更新于", "data_stale_prefix": "更新于",
@@ -588,6 +679,63 @@
"general": "常规", "general": "常规",
"generating": "正在生成", "generating": "正在生成",
"go_to_receive": "前往接收", "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": "高度", "height": "高度",
"help": "帮助", "help": "帮助",
"hidden_tag": " (已隐藏)", "hidden_tag": " (已隐藏)",
@@ -1226,12 +1374,52 @@
"sb_waiting_daemon_err": "等待 dragonxd — %s", "sb_waiting_daemon_err": "等待 dragonxd — %s",
"sb_warming_up": "正在预热...", "sb_warming_up": "正在预热...",
"sb_witness_cache": "正在重建见证", "sb_witness_cache": "正在重建见证",
"scale_effects": "缩放与效果",
"screenshot_open_dir": "打开位置", "screenshot_open_dir": "打开位置",
"screenshot_sweep": "运行截图批处理", "screenshot_sweep": "运行截图批处理",
"screenshot_sweep_desc": "遍历每个标签页的每一种主题,并将每一个的截图保存到配置目录 screenshots 文件夹下的各标签页子文件夹中(覆盖上一次的遍历)。运行几秒钟。", "screenshot_sweep_desc": "遍历每个标签页的每一种主题,并将每一个的截图保存到配置目录 screenshots 文件夹下的各标签页子文件夹中(覆盖上一次的遍历)。运行几秒钟。",
"screenshot_sweep_full": "完整 UI 遍历", "screenshot_sweep_full": "完整 UI 遍历",
"search_icons": "搜索图标...", "search_icons": "搜索图标...",
"search_placeholder": "搜索...", "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": "安全", "security": "安全",
"seed_backup_button": "助记词", "seed_backup_button": "助记词",
"seed_backup_close": "关闭", "seed_backup_close": "关闭",
@@ -1314,12 +1502,12 @@
"settings": "设置", "settings": "设置",
"settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。", "settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。",
"settings_acrylic_level": "亚克力级别:", "settings_acrylic_level": "亚克力级别:",
"settings_address_book": "地址簿...", "settings_address_book": "地址簿…",
"settings_auto_detected": "从 DRAGONX.conf 自动检测", "settings_auto_detected": "从 DRAGONX.conf 自动检测",
"settings_auto_lock": "自动锁定", "settings_auto_lock": "自动锁定",
"settings_auto_shield_desc": "自动将透明资金转移到屏蔽地址", "settings_auto_shield_desc": "自动将透明资金转移到屏蔽地址",
"settings_auto_shield_funds": "自动屏蔽透明资金", "settings_auto_shield_funds": "自动屏蔽透明资金",
"settings_backup": "备份...", "settings_backup": "备份…",
"settings_block_explorer_urls": "区块浏览器网址", "settings_block_explorer_urls": "区块浏览器网址",
"settings_builtin": "内置", "settings_builtin": "内置",
"settings_change_passphrase": "更改密码", "settings_change_passphrase": "更改密码",
@@ -1341,18 +1529,18 @@
"settings_encrypt_first_pin": "请先加密钱包以启用 PIN", "settings_encrypt_first_pin": "请先加密钱包以启用 PIN",
"settings_encrypt_wallet": "加密钱包", "settings_encrypt_wallet": "加密钱包",
"settings_explorer_hint": "URL 应包含尾部斜杠。将自动附加 txid/地址。", "settings_explorer_hint": "URL 应包含尾部斜杠。将自动附加 txid/地址。",
"settings_export_all": "全部导出...", "settings_export_all": "全部导出…",
"settings_export_csv": "导出 CSV...", "settings_export_csv": "导出 CSV…",
"settings_export_key": "导出密钥...", "settings_export_key": "导出密钥…",
"settings_gradient_bg": "渐变背景", "settings_gradient_bg": "渐变背景",
"settings_gradient_desc": "用平滑渐变替换纹理背景", "settings_gradient_desc": "用平滑渐变替换纹理背景",
"settings_idle_after": "之后", "settings_idle_after": "之后",
"settings_import_key": "导入私钥...", "settings_import_key": "导入私钥…",
"settings_import_viewkey": "导入查看密钥...", "settings_import_viewkey": "导入查看密钥…",
"settings_language_note": "注意:部分文本需要重启才能更新", "settings_language_note": "注意:部分文本需要重启才能更新",
"settings_lock_now": "立即锁定", "settings_lock_now": "立即锁定",
"settings_locked": "已锁定", "settings_locked": "已锁定",
"settings_merge_to_address": "合并到地址...", "settings_merge_to_address": "合并到地址…",
"settings_noise_opacity": "噪点不透明度:", "settings_noise_opacity": "噪点不透明度:",
"settings_not_connected": "未连接到守护进程", "settings_not_connected": "未连接到守护进程",
"settings_not_encrypted": "未加密", "settings_not_encrypted": "未加密",
@@ -1368,7 +1556,7 @@
"settings_reloaded": "已从磁盘重新加载设置", "settings_reloaded": "已从磁盘重新加载设置",
"settings_remove_encryption": "移除加密", "settings_remove_encryption": "移除加密",
"settings_remove_pin": "移除 PIN", "settings_remove_pin": "移除 PIN",
"settings_request_payment": "请求付款...", "settings_request_payment": "请求付款…",
"settings_rescan_desc": "重新扫描区块链以查找丢失的交易", "settings_rescan_desc": "重新扫描区块链以查找丢失的交易",
"settings_restart_daemon": "重启守护进程", "settings_restart_daemon": "重启守护进程",
"settings_rpc_connection": "RPC 连接", "settings_rpc_connection": "RPC 连接",
@@ -1379,13 +1567,13 @@
"settings_save_shielded_local": "将屏蔽交易历史保存到本地", "settings_save_shielded_local": "将屏蔽交易历史保存到本地",
"settings_saved": "设置已保存", "settings_saved": "设置已保存",
"settings_set_pin": "设置 PIN", "settings_set_pin": "设置 PIN",
"settings_shield_mining": "屏蔽挖矿...", "settings_shield_mining": "屏蔽挖矿…",
"settings_solid_colors_desc": "使用纯色代替模糊效果(无障碍功能)", "settings_solid_colors_desc": "使用纯色代替模糊效果(无障碍功能)",
"settings_theme_refreshed": "主题列表已刷新", "settings_theme_refreshed": "主题列表已刷新",
"settings_tor_desc": "通过 Tor 路由所有连接以增强隐私", "settings_tor_desc": "通过 Tor 路由所有连接以增强隐私",
"settings_unlocked": "已解锁", "settings_unlocked": "已解锁",
"settings_use_tor_network": "使用 Tor 进行网络连接", "settings_use_tor_network": "使用 Tor 进行网络连接",
"settings_validate_address": "验证地址...", "settings_validate_address": "验证地址…",
"settings_visual_effects": "视觉效果", "settings_visual_effects": "视觉效果",
"settings_wallet_file_size": "钱包文件大小:%s", "settings_wallet_file_size": "钱包文件大小:%s",
"settings_wallet_info": "钱包信息", "settings_wallet_info": "钱包信息",
@@ -1453,6 +1641,17 @@
"sweep_to": "归集到:", "sweep_to": "归集到:",
"sweep_toggle": "归集到我的钱包(不保留密钥)", "sweep_toggle": "归集到我的钱包(不保留密钥)",
"sweep_tx": "交易:", "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_body": "此钱包似乎已损坏——节点无法打开它。请从备份恢复、重新创建,或尝试修复。",
"switch_corrupt_repair": "尝试修复(salvage)", "switch_corrupt_repair": "尝试修复(salvage)",
"switch_progress_background": "在后台继续", "switch_progress_background": "在后台继续",
@@ -1492,6 +1691,7 @@
"to_upper": "至", "to_upper": "至",
"tools": "工具", "tools": "工具",
"tools_actions": "工具与操作...", "tools_actions": "工具与操作...",
"tools_actions_hdr": "工具与操作",
"total": "合计", "total": "合计",
"total_balance_label": "总余额", "total_balance_label": "总余额",
"transaction_id": "交易 ID", "transaction_id": "交易 ID",
@@ -1515,7 +1715,7 @@
"tt_auto_shield": "自动将透明余额转移到屏蔽地址以增强隐私", "tt_auto_shield": "自动将透明余额转移到屏蔽地址以增强隐私",
"tt_backup": "创建 wallet.dat 的备份", "tt_backup": "创建 wallet.dat 的备份",
"tt_block_explorer": "在浏览器中打开 DragonX 区块浏览器", "tt_block_explorer": "在浏览器中打开 DragonX 区块浏览器",
"tt_blur": "模糊程度(0%% = 关闭,100%% = 最大)", "tt_blur": "模糊程度(0% = 关闭,100% = 最大)",
"tt_change_pass": "更改钱包加密密码", "tt_change_pass": "更改钱包加密密码",
"tt_change_pin": "更改您的解锁 PIN", "tt_change_pin": "更改您的解锁 PIN",
"tt_chat_bubble_accent": "你发出的消息气泡的强调色(或跟随当前主题)", "tt_chat_bubble_accent": "你发出的消息气泡的强调色(或跟随当前主题)",
@@ -1578,7 +1778,7 @@
"tt_low_spec": "禁用所有重度视觉效果\\n快捷键:Ctrl+Shift+Down", "tt_low_spec": "禁用所有重度视觉效果\\n快捷键:Ctrl+Shift+Down",
"tt_merge": "将多个 UTXO 合并到一个地址", "tt_merge": "将多个 UTXO 合并到一个地址",
"tt_mine_idle": "系统空闲时自动开始挖矿\\n(无键盘/鼠标输入)", "tt_mine_idle": "系统空闲时自动开始挖矿\\n(无键盘/鼠标输入)",
"tt_noise": "颗粒纹理强度(0%% = 关闭,100%% = 最大)", "tt_noise": "颗粒纹理强度(0% = 关闭,100% = 最大)",
"tt_open_app_dir": "在文件管理器中打开 ObsidianDragon 文件夹(设置、主题、日志)", "tt_open_app_dir": "在文件管理器中打开 ObsidianDragon 文件夹(设置、主题、日志)",
"tt_open_data_dir": "在文件管理器中打开包含您钱包和区块链数据的文件夹", "tt_open_data_dir": "在文件管理器中打开包含您钱包和区块链数据的文件夹",
"tt_open_dir": "点击在文件管理器中打开", "tt_open_dir": "点击在文件管理器中打开",
@@ -1617,7 +1817,7 @@
"tt_theme_hotkey": "快捷键:Ctrl+左/右箭头切换主题", "tt_theme_hotkey": "快捷键:Ctrl+左/右箭头切换主题",
"tt_tor": "通过 Tor 网络路由守护进程连接以实现匿名", "tt_tor": "通过 Tor 网络路由守护进程连接以实现匿名",
"tt_tx_url": "在区块浏览器中查看交易的基础 URL", "tt_tx_url": "在区块浏览器中查看交易的基础 URL",
"tt_ui_opacity": "卡片和侧边栏不透明度(100%% = 完全不透明,越低越透明)", "tt_ui_opacity": "卡片和侧边栏不透明度(100% = 完全不透明,越低越透明)",
"tt_validate": "检查 DragonX 地址是否有效", "tt_validate": "检查 DragonX 地址是否有效",
"tt_verbose": "将详细连接诊断、守护进程状态\\n和端口所有者信息记录到控制台选项卡", "tt_verbose": "将详细连接诊断、守护进程状态\\n和端口所有者信息记录到控制台选项卡",
"tt_wallets_button": "列出您的钱包文件并在它们之间切换", "tt_wallets_button": "列出您的钱包文件并在它们之间切换",
+5
View File
@@ -965,6 +965,11 @@ edition-label = { position = 120 }
link-button = { width = 100, font = "button-sm" } link-button = { width = 100, font = "button-sm" }
close-button = { width = 120, font = "button", align = "center" } close-button = { width = 120, font = "button", align = "center" }
[dialogs.faq]
width = 860.0
height = 660.0
window = { width = 860, height = 660 }
[dialogs.settings] [dialogs.settings]
width = 600.0 width = 600.0
height = 550.0 height = 550.0
+394 -191
View File
File diff suppressed because it is too large Load Diff
+86
View File
@@ -163,6 +163,13 @@ public:
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); } bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(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_; }
// W7 QoL: a plaintext support-diagnostics snapshot (version, variant, daemon/RPC/wallet/log state) // W7 QoL: a plaintext support-diagnostics snapshot (version, variant, daemon/RPC/wallet/log state)
// for the "Copy diagnostics" action. Contains no secrets. // for the "Copy diagnostics" action. Contains no secrets.
std::string buildDiagnosticsReport(); std::string buildDiagnosticsReport();
@@ -175,6 +182,21 @@ public:
*/ */
void renderShutdownScreen(); 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 * @brief Render loading overlay in content area while daemon is starting/syncing
* @param contentH Height of the content area child window * @param contentH Height of the content area child window
@@ -371,6 +393,10 @@ public:
// Force refresh // Force refresh
void refreshNow(); 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 refreshMiningInfo();
void refreshPeerInfo(); void refreshPeerInfo();
void refreshMarketData(); void refreshMarketData();
@@ -408,6 +434,9 @@ public:
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index. // each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
void startFullUiSweep(); void startFullUiSweep();
std::string screenshotFullDir() const; 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 isScreenshotSweeping() const { return screenshot_sweep_active_; }
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; } bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
const std::string& screenshotSweepPath() const { return sweep_current_path_; } const std::string& screenshotSweepPath() const { return sweep_current_path_; }
@@ -433,6 +462,7 @@ public:
return 0; return 0;
} }
void showAboutDialog() { show_about_ = true; } void showAboutDialog() { show_about_ = true; }
void showFaqDialog() { show_faq_ = true; }
// Legacy tab compat — maps int to NavPage // Legacy tab compat — maps int to NavPage
void setCurrentTab(int tab); void setCurrentTab(int tab);
@@ -771,6 +801,7 @@ private:
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_. // balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
bool chat_note_scan_in_flight_ = false; 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_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 // 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 // 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. // this cache rather than requiring the current model to have both — else the budget flickers to 0.
@@ -780,6 +811,9 @@ private:
// Coordinator helpers (both variants unless noted; see app_network.cpp). // Coordinator helpers (both variants unless noted; see app_network.cpp).
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan 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 void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
@@ -814,6 +848,7 @@ private:
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act. // install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
void maybeRemindSeedBackup(); void maybeRemindSeedBackup();
void maybeWarnEmptyWalletWithFundedSiblings(); // full-node: empty active wallet + a funded sibling → warn once 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 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) 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) void sweepStaleDecryptExports(); // startup net: purge stale obsidiandecryptexport* files (H-02)
@@ -847,6 +882,8 @@ private:
void fastScanChatMemos(); void fastScanChatMemos();
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs 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) 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() 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. // Lite first-run welcome prompt: dismissed for the session once the user picks an action.
bool lite_firstrun_dismissed_ = false; bool lite_firstrun_dismissed_ = false;
@@ -900,6 +937,11 @@ private:
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05) GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05)
std::thread shutdown_thread_; 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; float shutdown_timer_ = 0.0f;
bool force_quit_confirm_ = false; bool force_quit_confirm_ = false;
std::chrono::steady_clock::time_point shutdown_start_time_; std::chrono::steady_clock::time_point shutdown_start_time_;
@@ -934,6 +976,8 @@ private:
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session. // 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_ = false; // a salvage happened this session
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per 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 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 // 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 // run, or under an external daemon whose startup output we never captured): if the active wallet loads
@@ -991,6 +1035,7 @@ private:
bool show_demo_window_ = false; bool show_demo_window_ = false;
bool show_settings_ = false; bool show_settings_ = false;
bool show_about_ = false; bool show_about_ = false;
bool show_faq_ = false;
bool show_import_key_ = false; bool show_import_key_ = false;
bool show_export_key_ = false; bool show_export_key_ = false;
bool show_backup_ = false; bool show_backup_ = false;
@@ -1004,6 +1049,7 @@ private:
bool seed_backup_loading_ = false; bool seed_backup_loading_ = false;
bool seed_backup_no_mnemonic_ = false; bool seed_backup_no_mnemonic_ = false;
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe 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 // 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 // once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
@@ -1097,6 +1143,22 @@ private:
bool daemon_start_error_shown_ = false; bool daemon_start_error_shown_ = false;
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once 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 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. // Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0; std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0; double clipboard_clear_deadline_ = 0.0;
@@ -1114,6 +1176,7 @@ private:
// Debug screenshot sweep state. // Debug screenshot sweep state.
bool screenshot_sweep_active_ = false; bool screenshot_sweep_active_ = false;
bool sweep_current_theme_only_ = false; // Debug Options: sweep only the active theme
bool sweep_capture_this_frame_ = false; bool sweep_capture_this_frame_ = false;
int sweep_skin_idx_ = 0; int sweep_skin_idx_ = 0;
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
@@ -1207,6 +1270,16 @@ private:
// Auto-shield guard (prevents concurrent auto-shield operations) // Auto-shield guard (prevents concurrent auto-shield operations)
std::atomic<bool> auto_shield_pending_{false}; 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 // P4: Incremental transaction cache
int last_tx_block_height_ = -1; // block height at last full tx fetch int last_tx_block_height_ = -1; // block height at last full tx fetch
@@ -1245,6 +1318,11 @@ private:
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for // 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. // the whole scan and would block them); completion is signalled by the rescan RPC callback.
bool runtime_rescan_active_ = false; 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 // 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. // that reconciles the preserved wallet.dat against the freshly-imported chain.
bool post_bootstrap_rescan_pending_ = false; bool post_bootstrap_rescan_pending_ = false;
@@ -1399,6 +1477,7 @@ private:
void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat 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 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 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 restoreOriginalWallet(); // swap the wallet.<ts>.bak back over the salvaged copy + restart
void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result
void rebuildWalletDatabase(); // rebuild a BDB-inconsistent wallet into a loadable one (helper) void rebuildWalletDatabase(); // rebuild a BDB-inconsistent wallet into a loadable one (helper)
@@ -1433,6 +1512,13 @@ private:
void refreshPrice(); void refreshPrice();
void refreshWalletEncryptionState(); void refreshWalletEncryptionState();
void applyRefreshPolicy(ui::NavPage page); 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 currentPageNeedsWalletDataRefresh() const;
bool shouldRunWalletTransactionRefresh() const; bool shouldRunWalletTransactionRefresh() const;
bool shouldRefreshTransactions() const; bool shouldRefreshTransactions() const;
+264 -29
View File
@@ -35,6 +35,7 @@
#include "rpc/connection.h" #include "rpc/connection.h"
#include "chat/chat_identity.h" // deriveChatIdentityFromSecret for HushChat identity provisioning #include "chat/chat_identity.h" // deriveChatIdentityFromSecret for HushChat identity provisioning
#include "ui/windows/chat_tab.h" // ui::ResetChatTab — wipe chat UI plaintext on a wallet switch #include "ui/windows/chat_tab.h" // ui::ResetChatTab — wipe chat UI plaintext on a wallet switch
#include "ui/windows/shield_dialog.h" // ui::ShieldDialog — Merge to Address shortcut from the bloat nudge
#include "ui/windows/mining_pool_panel.h" // ui::resolveMiningUserAddress #include "ui/windows/mining_pool_panel.h" // ui::resolveMiningUserAddress
#include <sodium.h> // sodium_memzero for wiping the fetched mnemonic #include <sodium.h> // sodium_memzero for wiping the fetched mnemonic
#include <cctype> #include <cctype>
@@ -248,6 +249,21 @@ void App::detectWalletAutoRecovery()
VERBOSE_LOGF("[recovery] Daemon auto-recovered/salvaged wallet.dat — surfacing the recovery dialog\n"); VERBOSE_LOGF("[recovery] Daemon auto-recovered/salvaged wallet.dat — surfacing the recovery dialog\n");
} }
// v1.3.0+ opens a wallet that lost its hdchain in DEGRADED mode (funds spendable) instead of aborting,
// but can't derive NEW HD keys — z_getnewaddress / z_shieldcoinbase / t->z z_sendmany fail with "HD seed
// not found". Only a startup log line signals it, so scan the captured output and warn once. Pre-1.3.0
// daemons never emit it, so this is a no-op there (backwards compatible).
void App::detectWalletDegraded()
{
if (wallet_degraded_warned_) return;
if (!isUsingEmbeddedDaemon() || !daemon_controller_ || !daemon_controller_->daemon()) return;
if (!daemon::walletOpenedDegraded(daemon_controller_->daemon()->getOutput())) return;
wallet_degraded_ = true;
wallet_degraded_warned_ = true;
ui::Notifications::instance().warning(TR("wallet_degraded_notify"), 30.0f);
VERBOSE_LOGF("[recovery] Daemon opened wallet in DEGRADED mode — new-key derivation disabled\n");
}
void App::tryConnect() void App::tryConnect()
{ {
// Lite builds have no full node / RPC daemon, so never run the RPC connection state machine // Lite builds have no full node / RPC daemon, so never run the RPC connection state machine
@@ -258,6 +274,7 @@ void App::tryConnect()
// Catch a startup wallet salvage as soon as it appears in the node's output — independent of whether // Catch a startup wallet salvage as soon as it appears in the node's output — independent of whether
// the node ever finishes starting or connects (skip only while an orchestrated swap is mid-flight). // the node ever finishes starting or connects (skip only while an orchestrated swap is mid-flight).
if (!daemon_restarting_) detectWalletAutoRecovery(); if (!daemon_restarting_) detectWalletAutoRecovery();
if (!daemon_restarting_) detectWalletDegraded();
if (connection_in_progress_) return; if (connection_in_progress_) return;
@@ -713,6 +730,11 @@ void App::onDisconnected(const std::string& reason)
wallet_seed_status_ = WalletSeedStatus::Unknown; wallet_seed_status_ = WalletSeedStatus::Unknown;
wallet_seed_status_attempts_ = 0; wallet_seed_status_attempts_ = 0;
// Re-probe whether the daemon auto-shields coinbase on the next connect — it may have been
// upgraded/swapped (e.g. v1.0.3 which has no z_autoshieldstatus -> v1.3.0 which auto-shields).
daemon_autoshield_probed_ = false;
daemon_autoshield_active_ = false;
// Clear RPC result caches // Clear RPC result caches
viewtx_cache_.clear(); viewtx_cache_.clear();
confirmed_tx_cache_.clear(); confirmed_tx_cache_.clear();
@@ -836,13 +858,58 @@ void App::applyRefreshPolicy(ui::NavPage page)
// While the daemon is syncing, override the per-tab cadence with the low-impact sync profile so // While the daemon is syncing, override the per-tab cadence with the low-impact sync profile so
// the wallet stops contending for the daemon's cs_main lock (frequent getpeerinfo / per-block // the wallet stops contending for the daemon's cs_main lock (frequent getpeerinfo / per-block
// transaction scans / balance polls slow block connection). This makes every tab sync as fast // transaction scans / balance polls slow block connection). This makes every tab sync as fast
// as the Console tab does today. Reverts to the per-tab profile once sync finishes. // as the Console tab does today. effectivelySyncing() keeps this profile on briefly after catching
refresh_policy_syncing_ = state_.sync.syncing; // up (hysteresis) so a large-wallet scan can't immediately re-starve connection and bounce the
// node back into "syncing". Reverts to the per-tab profile once the settle window passes.
refresh_policy_syncing_ = effectivelySyncing();
network_refresh_.setIntervals(refresh_policy_syncing_ network_refresh_.setIntervals(refresh_policy_syncing_
? services::RefreshScheduler::kSyncProfile ? services::RefreshScheduler::kSyncProfile
: getIntervalsForPage(page)); : getIntervalsForPage(page));
} }
// True while the node is behind, and for a short settle window after it first catches up. The settle
// window is armed only on the syncing→caught-up edge (see the Core refresh callback), so a wallet that
// was synced from the start is never throttled at connect — only a node that just finished catching up.
bool App::effectivelySyncing() const
{
if (state_.sync.syncing) return true;
if (sync_settle_until_ == 0) return false; // no pending settle → genuinely caught up
return std::time(nullptr) < sync_settle_until_;
}
// Adaptive throttle: the next balance poll must wait at least (lastScanCost / kBalanceDutyCycle) since
// the last one, so balance scanning can never occupy more than ~kBalanceDutyCycle of wall-clock. A
// cheap wallet (sub-cadence cost) is unaffected — the tab's Core timer stays the real cadence; a ~20s
// scan on a large wallet backs off to roughly every ~200s instead of every 2s, freeing cs_main for
// block connection. A wallet mutation bypasses this via force_balance_refresh_.
bool App::scanRefreshDue(std::int64_t lastUpdate, double lastScanMs) const
{
constexpr double kScanDutyCycle = 0.10;
if (lastUpdate == 0) return true; // never fetched
if (lastScanMs <= 0.0) return true; // no cost measured yet
const double minInterval = (lastScanMs / 1000.0) / kScanDutyCycle;
return std::difftime(std::time(nullptr), static_cast<std::time_t>(lastUpdate)) >= minInterval;
}
bool App::balanceRefreshDue() const
{
return scanRefreshDue(state_.last_balance_update, last_balance_scan_ms_);
}
// The address scan (z_listunspent) and history scan (z_listreceivedbyaddress) are just as O(mapWallet)
// and cs_main-bound as the balance scan, so they get the same adaptive back-off. This is what keeps a
// large synced wallet from re-scanning them every tab cadence — the near-tip starvation the balance-only
// throttle missed (only the periodic poll is gated; explicit refreshes call the refresh directly).
bool App::addressRefreshDue() const
{
return scanRefreshDue(state_.last_address_update, last_address_scan_ms_);
}
bool App::txRefreshDue() const
{
return scanRefreshDue(state_.last_tx_update, last_tx_scan_ms_);
}
bool App::currentPageNeedsWalletDataRefresh() const bool App::currentPageNeedsWalletDataRefresh() const
{ {
using NP = ui::NavPage; using NP = ui::NavPage;
@@ -1317,6 +1384,7 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected(); if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected();
if (salvage && daemon_controller_) daemon_controller_->setSalvageOnNextStart(true); // repair a corrupt wallet if (salvage && daemon_controller_) daemon_controller_->setSalvageOnNextStart(true); // repair a corrupt wallet
else if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); // (salvage implies rescan) else if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); // (salvage implies rescan)
if (salvage || needRescan) user_initiated_rescan_ = true; // wallet-triggered repair/rescan → completion should toast
// Start ONCE. Do NOT retry-spawn: a second start while the first is still shutting down leaves // Start ONCE. Do NOT retry-spawn: a second start while the first is still shutting down leaves
// two dragonxd holding wallet.dat against each other (BDB "Failed to rename … Error"). The // two dragonxd holding wallet.dat against each other (BDB "Failed to rename … Error"). The
// stopDaemonForWalletSwitch() wait already ensured the old node's process is gone, so a valid // stopDaemonForWalletSwitch() wait already ensured the old node's process is gone, so a valid
@@ -1632,7 +1700,10 @@ void App::refreshCoreData()
transactions_dirty_ = true; transactions_dirty_ = true;
last_tx_block_height_ = -1; last_tx_block_height_ = -1;
invalidateShieldedHistoryScanProgress(true); invalidateShieldedHistoryScanProgress(true);
ui::Notifications::instance().success("Blockchain rescan complete"); if (user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
} }
NetworkRefreshService::applyConnectionInfoResult(state_, result.info); NetworkRefreshService::applyConnectionInfoResult(state_, result.info);
@@ -1665,9 +1736,15 @@ void App::refreshCoreData()
? fast_rpc_.get() : rpc_.get(); ? fast_rpc_.get() : rpc_.get();
if (!w || !rpc) return; if (!w || !rpc) return;
ui::NavPage tracePage = current_page_; ui::NavPage tracePage = current_page_;
// Skip the balance call while syncing (it's incomplete anyway and takes the wallet lock + // Decide whether to include the balance call (z_gettotalbalance — O(mapWallet), holds cs_main).
// cs_main). Captured on the main thread to avoid reading state_ off the worker thread. // Suppress it (a) while syncing or within the post-sync settle window, so it can't starve block
const bool includeBalance = !state_.sync.syncing; // connection, and (b) unless enough time has elapsed given the LAST scan's measured cost, so a
// large shielded wallet backs off automatically instead of re-scanning every couple of seconds.
// A wallet mutation (send/shield) forces the next poll through so the user's own action updates the
// balance immediately. Captured on the main thread to avoid reading state_ off the worker thread.
const bool includeBalance = !effectivelySyncing() &&
(force_balance_refresh_ || balanceRefreshDue());
if (includeBalance) force_balance_refresh_ = false;
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *w, [this, rpc, tracePage, includeBalance]() -> rpc::RPCWorker::MainCb { auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *w, [this, rpc, tracePage, includeBalance]() -> rpc::RPCWorker::MainCb {
AppRefreshRpcGateway refreshRpc(*rpc, traceSource(tracePage, "Core refresh")); AppRefreshRpcGateway refreshRpc(*rpc, traceSource(tracePage, "Core refresh"));
@@ -1677,6 +1754,27 @@ void App::refreshCoreData()
NetworkRefreshService::applyCoreRefreshResult(state_, result, std::time(nullptr)); NetworkRefreshService::applyCoreRefreshResult(state_, result, std::time(nullptr));
applyPendingSendBalanceDeltas(true); applyPendingSendBalanceDeltas(true);
// Feed the adaptive balance throttle + sync-settle hysteresis. Record the last scan's
// cost (0 when balance was skipped), and arm the settle window only on the
// syncing→caught-up edge so a wallet synced from the start is never throttled at connect.
if (result.balanceScanMs > 0.0) last_balance_scan_ms_ = result.balanceScanMs;
// Sticky-behind hysteresis: hold the low-impact profile for kSyncSettleSeconds after the
// LAST time we read "behind" — not only on the caught-up edge. While actually syncing we
// re-arm every Core tick, so the profile stays engaged continuously; once we truly reach
// the tip (within the 2-block tolerance in applyCoreRefreshResult) we stop re-arming and it
// lapses after the window. This is what breaks the near-tip feedback loop: a single stale or
// optimistic "caught up" reading can no longer immediately unleash the heavy O(mapWallet)
// scans and re-starve the tail (which dropped us behind again → oscillation). With the
// scans suppressed, kSyncProfile keeps sync detection itself cheap, so we keep getting fresh
// "behind" readings and stay latched until genuinely caught up. Window must exceed the
// worst-case Core-refresh interval so it can't lapse between two behind readings mid-catchup.
const bool nowSyncing = state_.sync.syncing;
constexpr double kSyncSettleSeconds = 30.0;
if (nowSyncing || was_core_syncing_) {
sync_settle_until_ = std::time(nullptr) + static_cast<std::time_t>(kSyncSettleSeconds);
}
was_core_syncing_ = nowSyncing;
// Mid-session connection-loss detection. During normal operation, both core // Mid-session connection-loss detection. During normal operation, both core
// RPCs failing together means the daemon connection is dead (a busy daemon // RPCs failing together means the daemon connection is dead (a busy daemon
// fails them individually, not both at once). Warmup is excluded — both fail // fails them individually, not both at once). Warmup is excluded — both fail
@@ -1695,9 +1793,40 @@ void App::refreshCoreData()
} }
} }
// Auto-shield transparent funds if enabled // Probe the daemon's auto-shield status once per connection — independent of our own
if (result.balanceOk && settings_ && settings_->getAutoShield() && // toggle/balance — so both the defer-gate below and the Settings UI (O1) can read it.
state_.spendableTransparentBalance > 0.0001 && !state_.sync.syncing && // Only when synced (daemon past warmup). Fail-closed: a pre-1.3.0 daemon lacks the RPC,
// so the probe leaves active=false and the wallet keeps shielding client-side.
if (result.balanceOk && !state_.sync.syncing && !daemon_autoshield_probed_ && worker_ &&
!daemon_autoshield_probe_inflight_.exchange(true)) {
worker_->post([this]() -> rpc::RPCWorker::MainCb {
bool active = false, seedRecoverable = false;
std::string addr, reason;
try {
auto st = rpc_->call("z_autoshieldstatus", json::array());
if (st.is_object()) {
if (st.contains("autoshield")) active = st["autoshield"].get<bool>();
if (st.contains("autoshieldaddress")) addr = st["autoshieldaddress"].get<std::string>();
if (st.contains("disabled_reason")) reason = st["disabled_reason"].get<std::string>();
if (st.contains("seed_recoverable")) seedRecoverable = st["seed_recoverable"].get<bool>();
}
} catch (...) {} // pre-1.3.0 daemon: no such method — leave defaults (inactive)
return [this, active, addr, reason, seedRecoverable]() {
daemon_autoshield_active_ = active;
daemon_autoshield_address_ = addr;
daemon_autoshield_disabled_reason_ = reason;
daemon_autoshield_seed_recoverable_ = seedRecoverable;
daemon_autoshield_probed_ = true;
daemon_autoshield_probe_inflight_ = false;
};
});
}
// Auto-shield transparent funds — but defer to the daemon's own coinbase auto-shielder
// (v1.3.0+) when it's active, so we don't double-shield and split funds across z-addrs.
const bool autoShieldEligible = result.balanceOk && settings_ && settings_->getAutoShield() &&
state_.spendableTransparentBalance > 0.0001 && !state_.sync.syncing;
if (autoShieldEligible && daemon_autoshield_probed_ && !daemon_autoshield_active_ &&
!auto_shield_pending_.exchange(true)) { !auto_shield_pending_.exchange(true)) {
std::string targetZAddr; std::string targetZAddr;
for (const auto& addr : state_.addresses) { for (const auto& addr : state_.addresses) {
@@ -1770,7 +1899,13 @@ void App::refreshAddressData()
return [this, previousAddressCount, previousWalletIdentity, result = std::move(result)]() mutable { return [this, previousAddressCount, previousWalletIdentity, result = std::move(result)]() mutable {
const bool addrListOk = result.addressListOk; // capture before the move const bool addrListOk = result.addressListOk; // capture before the move
const double scanMs = result.scanMs; // feed the adaptive throttle (addressRefreshDue)
if (scanMs > 0.0) last_address_scan_ms_ = scanMs;
// #3: this scan already listed the wallet's unspent notes — feed the chat send-budget from it
// (dedups the dedicated chat z_listunspent). Grab it before `result` is moved below.
auto unspentNotes = std::move(result.unspentNotes);
NetworkRefreshService::applyAddressRefreshResult(state_, std::move(result)); NetworkRefreshService::applyAddressRefreshResult(state_, std::move(result));
updateChatNoteBudgetFromUnspent(unspentNotes);
// Mark the address list as loaded ONLY if enumeration actually succeeded — a swallowed // Mark the address list as loaded ONLY if enumeration actually succeeded — a swallowed
// z_listaddresses/getaddressesbyaccount failure returns a falsely-short list, and stamping it // z_listaddresses/getaddressesbyaccount failure returns a falsely-short list, and stamping it
// would let the empty-wallet warning trust a spurious 0 count (see maybeWarnEmptyWallet…). // would let the empty-wallet warning trust a spurious 0 count (see maybeWarnEmptyWallet…).
@@ -1829,6 +1964,13 @@ void App::refreshTransactionData()
transactionSnapshot.maxShieldedReceiveScans = shieldedReceiveScanBudget(current_page_); transactionSnapshot.maxShieldedReceiveScans = shieldedReceiveScanBudget(current_page_);
transactionSnapshot.shieldedScanTipTolerance = transactionSnapshot.shieldedScanTipTolerance =
shieldedScanTipTolerance(transactionSnapshot.shieldedAddresses.size()); shieldedScanTipTolerance(transactionSnapshot.shieldedAddresses.size());
// When fully synced, widen the re-scan tolerance so the O(mapWallet) per-address history scan runs
// every several blocks instead of ~every 2 — the heaviest residual synced-state cs_main cost. The
// balance poll still surfaces incoming funds on its own cadence; only the detailed history list lags a
// few minutes. (During catch-up the tx refresh is suppressed by kSyncProfile, so this is synced-only.)
if (!effectivelySyncing())
transactionSnapshot.shieldedScanTipTolerance =
std::max(transactionSnapshot.shieldedScanTipTolerance, 8);
ui::NavPage tracePage = current_page_; ui::NavPage tracePage = current_page_;
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks, auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks,
@@ -1839,6 +1981,7 @@ void App::refreshTransactionData()
return [this, result = std::move(result)]() mutable { return [this, result = std::move(result)]() mutable {
bool shieldedScanComplete = result.shieldedScanComplete; bool shieldedScanComplete = result.shieldedScanComplete;
if (result.scanMs > 0.0) last_tx_scan_ms_ = result.scanMs; // feed the adaptive throttle (txRefreshDue)
std::size_t nextShieldedScanStartIndex = result.nextShieldedScanStartIndex; std::size_t nextShieldedScanStartIndex = result.nextShieldedScanStartIndex;
auto shieldedScanHeights = std::move(result.shieldedScanHeights); auto shieldedScanHeights = std::move(result.shieldedScanHeights);
NetworkRefreshService::TransactionCacheUpdate cacheUpdate{ NetworkRefreshService::TransactionCacheUpdate cacheUpdate{
@@ -1904,6 +2047,9 @@ void App::refreshRecentTransactionData()
transactionSnapshot.maxShieldedReceiveScans = 1; transactionSnapshot.maxShieldedReceiveScans = 1;
transactionSnapshot.shieldedScanTipTolerance = transactionSnapshot.shieldedScanTipTolerance =
shieldedScanTipTolerance(transactionSnapshot.shieldedAddresses.size()); shieldedScanTipTolerance(transactionSnapshot.shieldedAddresses.size());
if (!effectivelySyncing()) // synced: widen the re-scan tolerance (see refreshTransactionData)
transactionSnapshot.shieldedScanTipTolerance =
std::max(transactionSnapshot.shieldedScanTipTolerance, 8);
ui::NavPage tracePage = current_page_; ui::NavPage tracePage = current_page_;
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks, auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks,
@@ -3433,9 +3579,25 @@ void App::refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model)
void App::refreshChatNoteBudgetNode() { void App::refreshChatNoteBudgetNode() {
if (lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return; if (lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
if (!state_.connected || !rpc_ || !worker_ || state_.isLocked()) return; if (!state_.connected || !rpc_ || !worker_ || state_.isLocked()) return;
// Never contend the daemon's cs_main while the node is catching up. z_listunspent is O(mapWallet)
// and holds cs_main for its whole duration — seconds on a large wallet — so an ungated per-few-second
// chat scan here starves block connection and keeps the node from ever pinning to the tip. Chat
// send-readiness can wait until we're synced; this budget is only advisory (the send path re-checks).
if (effectivelySyncing()) return;
// Only pre-warm the send-budget while the user is actually in chat. Off the Chat tab this heavy
// z_listunspent is unnecessary — and on a wallet-data tab, refreshAddressData's own z_listunspent
// already feeds the budget for free (updateChatNoteBudgetFromUnspent), so this dedicated scan is a
// fallback for the Chat tab (where the address refresh doesn't run).
if (current_page_ != ui::NavPage::Chat) return;
if (chat_note_scan_in_flight_) return; // one scan at a time if (chat_note_scan_in_flight_) return; // one scan at a time
const double now = ImGui::GetTime(); const double now = ImGui::GetTime();
if (now - chat_note_scan_last_ < 4.0) return; // rate limit; self-throttles via the in-flight flag too // Adaptive back-off keyed on the last scan's own cost: on a large wallet where the scan takes many
// seconds, this stretches the interval so the scan can't occupy more than ~10% of wall-clock (same
// duty-cycle discipline as the balance/address/tx polls). A cheap wallet stays at the 4s floor.
double minInterval = 4.0;
if (chat_note_scan_ms_ > 0.0)
minInterval = std::max(4.0, (chat_note_scan_ms_ / 1000.0) / 0.10);
if (now - chat_note_scan_last_ < minInterval) return;
chat_note_scan_last_ = now; chat_note_scan_last_ = now;
chat_note_scan_in_flight_ = true; chat_note_scan_in_flight_ = true;
@@ -3446,6 +3608,7 @@ void App::refreshChatNoteBudgetNode() {
int verified = 0, pipeline = 0; int verified = 0, pipeline = 0;
std::uint64_t verifiedZat = 0; std::uint64_t verifiedZat = 0;
bool ok = false; bool ok = false;
const auto scanStart = std::chrono::steady_clock::now();
try { try {
rpc::RPCClient::TraceScope trace("HushChat / note-buffer scan"); rpc::RPCClient::TraceScope trace("HushChat / note-buffer scan");
nlohmann::json notes = rpc_->call("z_listunspent", nlohmann::json::array({0})); // 0 = include maturing nlohmann::json notes = rpc_->call("z_listunspent", nlohmann::json::array({0})); // 0 = include maturing
@@ -3465,9 +3628,12 @@ void App::refreshChatNoteBudgetNode() {
} }
} }
} catch (const std::exception&) {} } catch (const std::exception&) {}
return [this, scanGen, ok, verified, pipeline, verifiedZat]() { const double scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return [this, scanGen, ok, verified, pipeline, verifiedZat, scanMs]() {
if (scanGen != chat_session_generation_) return; // wallet switched — drop (reset cleared the flag) if (scanGen != chat_session_generation_) return; // wallet switched — drop (reset cleared the flag)
chat_note_scan_in_flight_ = false; chat_note_scan_in_flight_ = false;
if (scanMs > 0.0) chat_note_scan_ms_ = scanMs; // feed the adaptive back-off
if (!ok) return; // z_listunspent unavailable — leave caches as-is if (!ok) return; // z_listunspent unavailable — leave caches as-is
chat_note_model_seen_ = true; chat_note_model_seen_ = true;
chat_verified_note_budget_ = verified; chat_verified_note_budget_ = verified;
@@ -3482,6 +3648,36 @@ void App::refreshChatNoteBudgetNode() {
}); });
} }
// #3: derive the chat send-budget from an already-collected z_listunspent (refreshAddressData's scan),
// so we don't issue a duplicate z_listunspent. Runs on the main thread; the notes are pre-parsed to the
// fields we need (amount / locked / TRUE confirmations), so there's no RPC and no worker hop. Mirrors the
// apply in refreshChatNoteBudgetNode and stamps chat_note_scan_last_ so the dedicated scan backs off.
void App::updateChatNoteBudgetFromUnspent(
const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes) {
if (lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
const int confsNeeded = chatConfsRequired();
const std::uint64_t minVal = chatDrgxToZat(kChatMinFeeDrgx);
int verified = 0, pipeline = 0;
std::uint64_t verifiedZat = 0;
for (const auto& nz : unspentNotes) {
if (nz.locked) continue; // tied up by an in-flight send
const std::uint64_t amtZat = chatDrgxToZat(nz.amount);
if (amtZat < minVal) continue; // skip dust below a fee
++pipeline; // unspent self-note (verified or maturing)
if (nz.confirmations >= confsNeeded) { ++verified; verifiedZat += amtZat; }
}
chat_note_model_seen_ = true;
chat_verified_note_budget_ = verified;
chat_pipeline_note_count_ = pipeline;
chat_verified_shielded_zat_ = verifiedZat;
chat_note_scan_last_ = ImGui::GetTime(); // counts as a fresh scan → dedicated scan backs off
if (chat_split_outstanding_ &&
(chat_pipeline_note_count_ >= kChatRefillTrigger ||
ImGui::GetTime() - chat_split_submitted_at_ > kChatSplitWatchdogSecs)) {
chat_split_outstanding_ = false;
}
}
void App::enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId) { void App::enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId) {
QueuedChatOp op; QueuedChatOp op;
op.kind = kind; op.kind = kind;
@@ -3723,15 +3919,28 @@ void App::fastScanChatMemos()
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return; if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
if (!state_.connected || !rpc_ || !worker_) return; if (!state_.connected || !rpc_ || !worker_) return;
if (chat_fast_scan_in_flight_) return; // don't stack RPCs if a previous scan is still running if (chat_fast_scan_in_flight_) return; // don't stack RPCs if a previous scan is still running
// z_listreceivedbyaddress is O(mapWallet) (the daemon iterates every wtx even when filtered to one
// address) — seconds on a large wallet — so this 0-conf poll must NOT run while the node is catching
// up, and even when synced it backs off to ~10% wall-clock keyed on its own cost. The block-tip
// harvest still ingests every message on confirmation; this only trades away mempool-speed delivery.
if (effectivelySyncing()) return;
// Mempool-speed 0-conf delivery is only worth its heavy cost while the user is watching chat. Off the
// Chat tab, incoming messages still arrive via the confirmed block-tip harvest (+ toast) within ~1 block.
if (current_page_ != ui::NavPage::Chat) return;
const double nowT = ImGui::GetTime();
if (chat_fast_scan_ms_ > 0.0 &&
nowT - chat_fast_scan_last_ < (chat_fast_scan_ms_ / 1000.0) / 0.10) return;
const std::string addr = chatReplyZaddr(); const std::string addr = chatReplyZaddr();
if (addr.empty()) return; if (addr.empty()) return;
chat_fast_scan_last_ = nowT;
chat_fast_scan_in_flight_ = true; chat_fast_scan_in_flight_ = true;
const int scanGen = chat_session_generation_; // guard: drop the result if the wallet switches/locks const int scanGen = chat_session_generation_; // guard: drop the result if the wallet switches/locks
worker_->post([this, addr, scanGen]() -> rpc::RPCWorker::MainCb { worker_->post([this, addr, scanGen]() -> rpc::RPCWorker::MainCb {
std::vector<chat::HushChatTransactionMetadata> metadata; std::vector<chat::HushChatTransactionMetadata> metadata;
int rawMemoCount = 0; // received notes carrying a memo at the reply addr (0-conf visibility signal) int rawMemoCount = 0; // received notes carrying a memo at the reply addr (0-conf visibility signal)
std::string scanError; std::string scanError;
const auto scanStart = std::chrono::steady_clock::now();
try { try {
rpc::RPCClient::TraceScope trace("HushChat / 0-conf fast scan"); rpc::RPCClient::TraceScope trace("HushChat / 0-conf fast scan");
nlohmann::json received = rpc_->call("z_listreceivedbyaddress", {addr, 0}); // 0 = include mempool nlohmann::json received = rpc_->call("z_listreceivedbyaddress", {addr, 0}); // 0 = include mempool
@@ -3762,12 +3971,15 @@ void App::fastScanChatMemos()
} }
} catch (const std::exception& e) { scanError = e.what(); } } catch (const std::exception& e) { scanError = e.what(); }
const int parsedCount = static_cast<int>(metadata.size()); const int parsedCount = static_cast<int>(metadata.size());
return [this, scanGen, metadata = std::move(metadata), rawMemoCount, parsedCount, scanError]() mutable { const double scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return [this, scanGen, metadata = std::move(metadata), rawMemoCount, parsedCount, scanError, scanMs]() mutable {
// The wallet was switched/locked between post and now — this metadata belongs to the previous // The wallet was switched/locked between post and now — this metadata belongs to the previous
// session. resetChatSession already reset the in-flight flag, so just drop; clearing it here // session. resetChatSession already reset the in-flight flag, so just drop; clearing it here
// would clobber a new session's own in-flight scan (mirrors the broadcast/identity guards). // would clobber a new session's own in-flight scan (mirrors the broadcast/identity guards).
if (scanGen != chat_session_generation_) return; if (scanGen != chat_session_generation_) return;
chat_fast_scan_in_flight_ = false; chat_fast_scan_in_flight_ = false;
if (scanMs > 0.0) chat_fast_scan_ms_ = scanMs; // feed the adaptive back-off
// Diagnostic (console App/chat channel): log when the memo-note count at the reply address // Diagnostic (console App/chat channel): log when the memo-note count at the reply address
// CHANGES, so a stable mempool doesn't spam every 2.5s. Reveals whether inbound 0-conf messages // CHANGES, so a stable mempool doesn't spam every 2.5s. Reveals whether inbound 0-conf messages
// are reaching us at all, and how many parse as chat vs are unrelated memos. // are reaching us at all, and how many parse as chat vs are unrelated memos.
@@ -4232,6 +4444,36 @@ void App::maybeRemindSeedBackup()
}); });
} }
// One-time nudge (full-node) when the BDB wallet.dat has bloated past the threshold. Berkeley DB never
// shrinks in place and shielded-note witness data accumulates, so a mining/shielded wallet can grow
// unbounded. Fires ONCE (persisted flag) a warning toast + a clickable "Consolidate notes…" entry in the
// bell/alert panel that opens Merge to Address; re-arms if the file later drops back under the threshold.
void App::maybeWarnLargeWallet()
{
if (capture_mode_ || lite_wallet_) return; // no live nags during a UI sweep; lite has no wallet.dat
if (!supportsFullNodeLifecycleActions() || !settings_) return;
if (!state_.connected || !state_.encryption_state_known) return;
if (state_.warming_up || state_.daemon_initializing || !state_.sync.isSynced()) return;
if (large_wallet_checked_) return; // stat wallet.dat at most once per launch
large_wallet_checked_ = true;
static constexpr uint64_t kWalletBloatWarnBytes = 500ull * 1024 * 1024; // 500 MB (matches the Settings banner)
const std::string walletPath = util::Platform::getDragonXDataDir() + "/wallet.dat";
const uint64_t sz = util::Platform::getFileSize(walletPath);
if (sz <= kWalletBloatWarnBytes) {
// Re-arm the one-time warning if the file shrank back under the threshold (e.g. after a fresh seed wallet).
if (settings_->getLargeWalletWarned()) { settings_->setLargeWalletWarned(false); settings_->save(); }
return;
}
if (settings_->getLargeWalletWarned()) return; // already warned once for this bloat episode
settings_->setLargeWalletWarned(true);
settings_->save();
ui::Notifications::instance().action(
TR("wallet_size_warn"), ui::NotificationType::Warning,
[]() { ui::ShieldDialog::showConsolidate(); },
TR("wallet_size_consolidate"), 12.0f);
}
// Complementary on-disk safety net (full-node) for a wallet salvage we did NOT witness this launch — it // Complementary on-disk safety net (full-node) for a wallet salvage we did NOT witness this launch — it
// happened on a prior run, or under an external daemon whose startup output we never captured, so // happened on a prior run, or under an external daemon whose startup output we never captured, so
// detectWalletAutoRecovery() never fired. If the active wallet loads EMPTY while a sibling wallet file in // detectWalletAutoRecovery() never fired. If the active wallet loads EMPTY while a sibling wallet file in
@@ -4703,7 +4945,7 @@ void App::beginAdoptSeedWallet()
// 3. Rescan on next start (only if the swap happened) and bring the daemon back up — // 3. Rescan on next start (only if the swap happened) and bring the daemon back up —
// unless we're quitting, in which case don't resurrect it. // unless we're quitting, in which case don't resurrect it.
if (swapDone && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); if (swapDone && daemon_controller_) { daemon_controller_->setRescanOnNextStart(true); user_initiated_rescan_ = true; }
if (!shutting_down_) { if (!shutting_down_) {
// We stopped the daemon ourselves (port_free) — clear the adopted-external latch so the // We stopped the daemon ourselves (port_free) — clear the adopted-external latch so the
// relaunched process is treated as owned (stop/isRunning/exit behave normally afterward). // relaunched process is treated as owned (stop/isRunning/exit behave normally afterward).
@@ -4967,21 +5209,12 @@ void App::rebuildWalletDatabase()
const std::string tmpOut = datadir + "/wallet.rebuilt-" + std::string(ts) + ".tmp"; const std::string tmpOut = datadir + "/wallet.rebuilt-" + std::string(ts) + ".tmp";
{ std::error_code ec; fs::remove(tmpOut, ec); } // helper uses DB_EXCL — path must be fresh { std::error_code ec; fs::remove(tmpOut, ec); } // helper uses DB_EXCL — path must be fresh
// 3. Run the helper (src -> tmpOut). Quote both paths; capture its JSON line. // 3. Run the helper (src -> tmpOut). Quote both paths; capture its JSON line. Windowless
std::string cmd = "\"" + helper + "\" \"" + src + "\" \"" + tmpOut + "\""; // (runHiddenCapture) so a wallet rebuild never flashes a cmd.exe console; it runs the
#ifdef _WIN32 // helper via CreateProcess directly on Windows, so no cmd.exe outer-quote wrap is needed.
cmd = "\"" + cmd + "\""; // cmd.exe strips the outermost quotes const std::string cmd = "\"" + helper + "\" \"" + src + "\" \"" + tmpOut + "\"";
FILE* fp = _popen(cmd.c_str(), "r"); int rc = -1;
#else const std::string jout = util::Platform::runHiddenCapture(cmd, /*mergeStderr=*/false, &rc);
FILE* fp = popen(cmd.c_str(), "r");
#endif
std::string jout;
if (fp) { char b[512]; while (std::fgets(b, sizeof b, fp)) jout += b; }
#ifdef _WIN32
const int rc = fp ? _pclose(fp) : -1;
#else
const int rc = fp ? pclose(fp) : -1;
#endif
DEBUG_LOGF("[App] wallet-rebuild helper rc=%d out=%s\n", rc, jout.c_str()); DEBUG_LOGF("[App] wallet-rebuild helper rc=%d out=%s\n", rc, jout.c_str());
// 4. Verify-before-swap: the output must be a readable BDB with the fund-critical keys. // 4. Verify-before-swap: the output must be a readable BDB with the fund-critical keys.
@@ -5011,7 +5244,7 @@ void App::rebuildWalletDatabase()
fs::rename(datadir + "/database", datadir + "/database.prerebuild-" + std::string(ts) + ".bak", e2); fs::rename(datadir + "/database", datadir + "/database.prerebuild-" + std::string(ts) + ".bak", e2);
for (const auto& e : fs::directory_iterator(datadir, e2)) for (const auto& e : fs::directory_iterator(datadir, e2))
if (e.path().filename().string().rfind("__db.", 0) == 0) { std::error_code e3; fs::remove(e.path(), e3); } if (e.path().filename().string().rfind("__db.", 0) == 0) { std::error_code e3; fs::remove(e.path(), e3); }
if (daemon_controller_) daemon_controller_->setRescanOnNextStart(true); if (daemon_controller_) { daemon_controller_->setRescanOnNextStart(true); user_initiated_rescan_ = true; }
} }
} }
} }
@@ -5269,6 +5502,7 @@ void App::submitZSendMany(const std::string& from, const std::string& to, double
// Force transaction list refresh so the sent tx appears immediately // Force transaction list refresh so the sent tx appears immediately
transactions_dirty_ = true; transactions_dirty_ = true;
last_tx_block_height_ = -1; last_tx_block_height_ = -1;
force_balance_refresh_ = true; // the user's own send must update the balance now, past the throttle
network_refresh_.markWalletMutationRefresh(); network_refresh_.markWalletMutationRefresh();
// z_sendmany only returned an opid: the transaction is built/signed/ // z_sendmany only returned an opid: the transaction is built/signed/
// broadcast asynchronously by the daemon. Defer the user-facing // broadcast asynchronously by the daemon. Defer the user-facing
@@ -5438,6 +5672,7 @@ void App::runtimeRescan(int startHeight)
runtime_rescan_active_ = true; runtime_rescan_active_ = true;
state_.sync.rescanning = true; state_.sync.rescanning = true;
rescan_confirmed_active_ = true; rescan_confirmed_active_ = true;
user_initiated_rescan_ = true; // user clicked Rescan → completion should toast
state_.sync.rescan_progress = 0.0f; state_.sync.rescan_progress = 0.0f;
state_.sync.rescan_status = "Rescanning from block " + std::to_string(startHeight) + "..."; state_.sync.rescan_status = "Rescanning from block " + std::to_string(startHeight) + "...";
transactions_dirty_ = true; transactions_dirty_ = true;
+44 -45
View File
@@ -300,14 +300,14 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
}); });
} else { } else {
ui::Notifications::instance().warning( 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) { void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (!rpc_ || !rpc_->isConnected()) return; if (!rpc_ || !rpc_->isConnected()) return;
encrypt_in_progress_ = true; encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet..."; encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) { if (worker_) {
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb { worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
@@ -317,7 +317,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (result.encrypted) { if (result.encrypted) {
return [this]() { return [this]() {
encrypt_in_progress_ = false; 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"); DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");
// Immediately update local encryption state so the // Immediately update local encryption state so the
@@ -336,7 +336,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
} }
ui::Notifications::instance().info( ui::Notifications::instance().info(
"Wallet encrypted successfully", 5.0f); TR("sec_wallet_encrypted_successfully"), 5.0f);
// The daemon shuts itself down after encryptwallet. // The daemon shuts itself down after encryptwallet.
// Update connection_status_ so the loading overlay // Update connection_status_ so the loading overlay
@@ -348,11 +348,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
std::string err = result.error; std::string err = result.error;
return [this, err]() { return [this, err]() {
encrypt_in_progress_ = false; 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()); DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error( ui::Notifications::instance().error(
"Encryption failed: " + err); std::string(TR("sec_encryption_failed_prefix")) + err);
// Return to passphrase entry on failure // Return to passphrase entry on failure
if (show_encrypt_dialog_ && if (show_encrypt_dialog_ &&
@@ -393,7 +393,7 @@ void App::processDeferredEncryption() {
std::string pin = std::move(deferredEncryption.pin); std::string pin = std::move(deferredEncryption.pin);
encrypt_in_progress_ = true; encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet..."; encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) { if (worker_) {
worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb { worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
@@ -413,13 +413,13 @@ void App::processDeferredEncryption() {
if (result.pinStored) { if (result.pinStored) {
settings_->setPinEnabled(true); settings_->setPinEnabled(true);
settings_->save(); 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 { } else {
ui::Notifications::instance().warning( ui::Notifications::instance().warning(
"Wallet encrypted but PIN vault failed"); TR("sec_wallet_encrypted_but_pin_vault_failed"));
} }
} else { } else {
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f); ui::Notifications::instance().info(TR("sec_wallet_encrypted_successfully"), 5.0f);
} }
wallet_security_.clearDeferredEncryption(); wallet_security_.clearDeferredEncryption();
@@ -432,9 +432,9 @@ void App::processDeferredEncryption() {
std::string err = result.error; std::string err = result.error;
return [this, err]() { return [this, err]() {
encrypt_in_progress_ = false; 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()); 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(); wallet_security_.clearDeferredEncryption();
}; };
} }
@@ -531,7 +531,7 @@ void App::lockWallet() {
if (!lock_failure_warned_) { if (!lock_failure_warned_) {
lock_failure_warned_ = true; lock_failure_warned_ = true;
ui::Notifications::instance().warning( 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);
} }
} }
}; };
@@ -541,7 +541,7 @@ void App::lockWallet() {
void App::changePassphrase(const std::string& oldPass, const std::string& newPass) { void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
if (!rpc_ || !rpc_->isConnected() || !worker_) return; if (!rpc_ || !rpc_->isConnected() || !worker_) return;
encrypt_in_progress_ = true; 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* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
@@ -574,9 +574,9 @@ void App::changePassphrase(const std::string& oldPass, const std::string& newPas
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_)); memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
unlockTransactionHistoryCacheWithPassphrase(newPass); unlockTransactionHistoryCacheWithPassphrase(newPass);
storeTransactionHistoryCacheIfAvailable(); storeTransactionHistoryCacheIfAvailable();
ui::Notifications::instance().info("Passphrase changed successfully"); ui::Notifications::instance().info(TR("sec_passphrase_changed_successfully"));
} else { } else {
encrypt_status_ = "Failed: " + err_msg; encrypt_status_ = std::string(TR("sec_failed_prefix")) + err_msg;
} }
util::SecureVault::secureZero(newPass.data(), newPass.size()); util::SecureVault::secureZero(newPass.data(), newPass.size());
}; };
@@ -637,8 +637,7 @@ void App::refreshWalletEncryptionState() {
!encryption_incomplete_warned_) { !encryption_incomplete_warned_) {
encryption_incomplete_warned_ = true; encryption_incomplete_warned_ = true;
ui::Notifications::instance().warning( ui::Notifications::instance().warning(
"Wallet encryption did not complete — your wallet is NOT encrypted. " TR("sec_encryption_did_not_complete"), 30.0f);
"Open Settings to finish encrypting it.", 30.0f);
} }
if (state_.transactions.empty()) { if (state_.transactions.empty()) {
loadTransactionHistoryCacheIfAvailable(); loadTransactionHistoryCacheIfAvailable();
@@ -951,7 +950,7 @@ void App::renderLockScreen() {
ImU32 textCol = ui::material::OnSurface(); 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); ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
dl->AddText(titleFont, titleFont->LegacySize, dl->AddText(titleFont, titleFont->LegacySize,
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title); ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
@@ -964,7 +963,7 @@ void App::renderLockScreen() {
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0; if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
char msg[128]; 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); ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize, dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg); ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
@@ -977,10 +976,10 @@ void App::renderLockScreen() {
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists // Mode toggle (PIN / Passphrase) — only show if PIN vault exists
if (hasPinVault) { if (hasPinVault) {
const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD; 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_ const char* switchLabel = lock_use_pin_
? "Use passphrase instead" ? TR("sec_use_passphrase_instead")
: "Use PIN instead"; : TR("sec_use_pin_instead");
// Current mode indicator — icon with icon font, text with caption font // Current mode indicator — icon with icon font, text with caption font
ImFont* iconFont = ui::material::Type().iconSmall(); ImFont* iconFont = ui::material::Type().iconSmall();
@@ -1082,7 +1081,7 @@ void App::renderLockScreen() {
if (lock_unlock_in_progress_) { if (lock_unlock_in_progress_) {
// Animated spinner dots // Animated spinner dots
char msg[64]; 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); ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize, dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
@@ -1106,7 +1105,7 @@ void App::renderLockScreen() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canSubmit); 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::EndDisabled();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
@@ -1160,7 +1159,7 @@ void App::renderLockScreen() {
r->call("walletpassphrase", {passphrase, timeout}); r->call("walletpassphrase", {passphrase, timeout});
rpcOk = true; rpcOk = true;
} else { } else {
rpcErr = "Not connected to daemon"; rpcErr = TR("sec_not_connected_to_daemon");
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
rpcErr = e.what(); rpcErr = e.what();
@@ -1176,7 +1175,7 @@ void App::renderLockScreen() {
// so route through applyUnlockFailure so the lockout curve applies // so route through applyUnlockFailure so the lockout curve applies
// (this path previously bumped the counter but skipped the lockout math). // (this path previously bumped the counter but skipped the lockout math).
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable { 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()); util::SecureVault::secureZero(passphrase.data(), passphrase.size());
}; };
} }
@@ -1579,7 +1578,7 @@ void App::renderDecryptWalletDialog() {
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size()); if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
if (!unlock.ok) { if (!unlock.ok) {
return [this]() { return [this]() {
wallet_security_workflow_.failEntry("Incorrect passphrase"); wallet_security_workflow_.failEntry(TR("sec_incorrect_passphrase_decrypt"));
}; };
} }
@@ -1695,7 +1694,7 @@ void App::renderDecryptWalletDialog() {
}); });
ui::Notifications::instance().info( ui::Notifications::instance().info(
"Importing keys & rescanning blockchain — wallet is usable while this runs", TR("sec_importing_keys_rescanning"),
8.0f); 8.0f);
}; };
}); });
@@ -1714,7 +1713,7 @@ void App::renderDecryptWalletDialog() {
wallet_security_workflow_.finishImport(); wallet_security_workflow_.finishImport();
ui::Notifications::instance().error( ui::Notifications::instance().error(
err + err +
"\nEncrypted backup: wallet.dat.encrypted.bak", TR("sec_encrypted_backup_suffix"),
12.0f); 12.0f);
}; };
}); });
@@ -1740,7 +1739,7 @@ void App::renderDecryptWalletDialog() {
refreshPeerInfo(); refreshPeerInfo();
ui::Notifications::instance().success( ui::Notifications::instance().success(
"Wallet decrypted successfully! All keys imported.", TR("sec_wallet_decrypted_all_keys_imported"),
8.0f); 8.0f);
DEBUG_LOGF("[App] Wallet decrypted successfully\n"); DEBUG_LOGF("[App] Wallet decrypted successfully\n");
}; };
@@ -1862,7 +1861,7 @@ void App::renderDecryptWalletDialog() {
int tMins = (int)(totalElapsed / 60); int tMins = (int)(totalElapsed / 60);
int tSecs = (int)(totalElapsed % 60); int tSecs = (int)(totalElapsed % 60);
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs); ImGui::TextDisabled(TR("sec_total_elapsed_fmt"), tMins, tSecs);
} }
// ---- Phase 2: Success ---- // ---- Phase 2: Success ----
@@ -1965,7 +1964,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_); ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) { if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
pin_in_progress_ = true; pin_in_progress_ = true;
pin_status_ = "Verifying passphrase..."; pin_status_ = TR("sec_verifying_passphrase");
// Verify passphrase + store vault on worker thread to avoid // Verify passphrase + store vault on worker thread to avoid
// blocking the UI with Argon2id key derivation. // blocking the UI with Argon2id key derivation.
@@ -1985,7 +1984,7 @@ void App::renderPinDialogs() {
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size()); if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size()); if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
return [this]() { return [this]() {
pin_status_ = "Incorrect passphrase"; pin_status_ = TR("sec_incorrect_passphrase_pin_setup");
pin_in_progress_ = false; pin_in_progress_ = false;
}; };
} }
@@ -2009,15 +2008,15 @@ void App::renderPinDialogs() {
pin_status_.clear(); pin_status_.clear();
pin_in_progress_ = false; pin_in_progress_ = false;
show_pin_setup_ = false; show_pin_setup_ = false;
ui::Notifications::instance().info("PIN set successfully"); ui::Notifications::instance().info(TR("sec_pin_set_successfully"));
} else { } else {
pin_status_ = "Failed to create vault"; pin_status_ = TR("sec_failed_to_create_vault");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
}; };
}); });
} else { } else {
pin_status_ = "Not connected to daemon"; pin_status_ = TR("sec_not_connected_to_daemon_pin");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
} }
@@ -2082,7 +2081,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_); ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) { if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true; pin_in_progress_ = true;
pin_status_ = "Changing PIN..."; pin_status_ = TR("sec_changing_pin");
std::string oldPin(pin_old_buf_); std::string oldPin(pin_old_buf_);
std::string newPinCopy = newPin; std::string newPinCopy = newPin;
memset(pin_old_buf_, 0, sizeof(pin_old_buf_)); memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2098,15 +2097,15 @@ void App::renderPinDialogs() {
pin_status_.clear(); pin_status_.clear();
pin_in_progress_ = false; pin_in_progress_ = false;
show_pin_change_ = false; show_pin_change_ = false;
ui::Notifications::instance().info("PIN changed successfully"); ui::Notifications::instance().info(TR("sec_pin_changed_successfully"));
} else { } else {
pin_status_ = "Incorrect current PIN"; pin_status_ = TR("sec_incorrect_current_pin");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
}; };
}); });
} else { } else {
pin_status_ = "Internal error"; pin_status_ = TR("sec_internal_error_change_pin");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
} }
@@ -2147,7 +2146,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_); ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) { if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true; pin_in_progress_ = true;
pin_status_ = "Verifying PIN..."; pin_status_ = TR("sec_verifying_pin");
std::string oldPin(pin_old_buf_); std::string oldPin(pin_old_buf_);
memset(pin_old_buf_, 0, sizeof(pin_old_buf_)); memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2167,15 +2166,15 @@ void App::renderPinDialogs() {
pin_status_.clear(); pin_status_.clear();
pin_in_progress_ = false; pin_in_progress_ = false;
show_pin_remove_ = false; show_pin_remove_ = false;
ui::Notifications::instance().info("PIN removed"); ui::Notifications::instance().info(TR("sec_pin_removed"));
} else { } else {
pin_status_ = "Incorrect PIN"; pin_status_ = TR("sec_incorrect_pin_remove");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
}; };
}); });
} else { } else {
pin_status_ = "Internal error"; pin_status_ = TR("sec_internal_error_remove_pin");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
} }
+19
View File
@@ -310,6 +310,8 @@ void App::buildSweepCatalog()
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); }); [](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
add("modal-backup", ui::NavPage::Overview, add("modal-backup", ui::NavPage::Overview,
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); }); [](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). // Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
add("modal-encrypt", ui::NavPage::Settings, add("modal-encrypt", ui::NavPage::Settings,
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; }, [](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 (sk.valid) sweep_skins_.push_back(sk.id);
if (sweep_skins_.empty()) return; 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; sweep_full_ = full;
if (full) { capture_mode_ = true; installDemoWalletData(); } if (full) { capture_mode_ = true; installDemoWalletData(); }
buildSweepCatalog(); 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; } if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
sweep_dir_ = full ? screenshotFullDir() : screenshotDir(); sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
+6
View File
@@ -232,6 +232,7 @@ bool Settings::load(const std::string& path)
} }
loadScalar(j, "wizard_completed", wizard_completed_); loadScalar(j, "wizard_completed", wizard_completed_);
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_); 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()) { if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
empty_wallet_warning_acked_.clear(); empty_wallet_warning_acked_.clear();
for (const auto& w : j["empty_wallet_warning_acked"]) for (const auto& w : j["empty_wallet_warning_acked"])
@@ -251,6 +252,8 @@ bool Settings::load(const std::string& path)
loadScalar(j, "keep_daemon_running", keep_daemon_running_); loadScalar(j, "keep_daemon_running", keep_daemon_running_);
loadScalar(j, "stop_external_daemon", stop_external_daemon_); loadScalar(j, "stop_external_daemon", stop_external_daemon_);
loadScalar(j, "max_connections", max_connections_); 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()) { if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
const auto& lite = j["lite_wallet"]; const auto& lite = j["lite_wallet"];
if (lite.contains("server_selection_mode")) { if (lite.contains("server_selection_mode")) {
@@ -506,6 +509,7 @@ bool Settings::save(const std::string& path)
} }
j["wizard_completed"] = wizard_completed_; j["wizard_completed"] = wizard_completed_;
j["seed_backup_reminded"] = seed_backup_reminded_; j["seed_backup_reminded"] = seed_backup_reminded_;
j["large_wallet_warned"] = large_wallet_warned_;
j["empty_wallet_warning_acked"] = json::array(); j["empty_wallet_warning_acked"] = json::array();
for (const auto& w : empty_wallet_warning_acked_) for (const auto& w : empty_wallet_warning_acked_)
j["empty_wallet_warning_acked"].push_back(w); j["empty_wallet_warning_acked"].push_back(w);
@@ -523,6 +527,8 @@ bool Settings::save(const std::string& path)
j["keep_daemon_running"] = keep_daemon_running_; j["keep_daemon_running"] = keep_daemon_running_;
j["stop_external_daemon"] = stop_external_daemon_; j["stop_external_daemon"] = stop_external_daemon_;
j["max_connections"] = max_connections_; j["max_connections"] = max_connections_;
j["stratum_host_enabled"] = stratum_host_enabled_;
j["stratum_allowip"] = stratum_allowip_;
{ {
json lite = json::object(); json lite = json::object();
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_); lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
+12
View File
@@ -330,6 +330,10 @@ public:
bool getSeedBackupReminded() const { return seed_backup_reminded_; } bool getSeedBackupReminded() const { return seed_backup_reminded_; }
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; } 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" // 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 // warning has been dismissed. Keyed per active wallet file so switching to a different empty
// wallet can warn again (see App::maybeWarnEmptyWalletWithFundedSiblings). // wallet can warn again (see App::maybeWarnEmptyWalletWithFundedSiblings).
@@ -396,6 +400,11 @@ public:
// Daemon — maximum peer connections (0 = daemon default) // Daemon — maximum peer connections (0 = daemon default)
int getMaxConnections() const { return max_connections_; } int getMaxConnections() const { return max_connections_; }
void setMaxConnections(int v) { max_connections_ = std::max(0, v); } 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 // Lite wallet server selection
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; } LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
@@ -597,6 +606,7 @@ private:
std::map<std::string, AddressMeta> address_meta_; std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false; bool wizard_completed_ = false;
bool seed_backup_reminded_ = 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 std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
bool encryption_pending_ = false; bool encryption_pending_ = false;
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
@@ -612,6 +622,8 @@ private:
bool keep_daemon_running_ = false; bool keep_daemon_running_ = false;
bool stop_external_daemon_ = false; bool stop_external_daemon_ = false;
int max_connections_ = 0; // 0 = daemon default 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; // Lite wallet server preferences. These are user/server settings only;
// wallet secrets, wallet files, and lifecycle state are never stored here. // wallet secrets, wallet files, and lifecycle state are never stored here.
+1
View File
@@ -26,6 +26,7 @@ void DaemonController::syncSettings(const config::Settings* settings)
if (!settings) return; if (!settings) return;
daemon_->setDebugCategories(settings->getDebugCategories()); daemon_->setDebugCategories(settings->getDebugCategories());
daemon_->setMaxConnections(settings->getMaxConnections()); daemon_->setMaxConnections(settings->getMaxConnections());
daemon_->setStratumHosting(settings->getStratumHost(), settings->getStratumAllowIp());
std::string walletFile = settings->getActiveWalletFile(); std::string walletFile = settings->getActiveWalletFile();
// The Wallets dialog opens an out-of-datadir wallet by linking it into the datadir under a // The Wallets dialog opens an out-of-datadir wallet by linking it into the datadir under a
+10
View File
@@ -49,6 +49,16 @@ inline bool walletAutoRecovered(const std::string& out)
&& out.find(".bak") != std::string::npos); // Recover moved wallet.dat aside && 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. // If `name` is a daemon salvage backup "wallet.<unixtime>.bak", return its timestamp; else -1.
inline long long parseWalletSalvageBakTs(const std::string& name) inline long long parseWalletSalvageBakTs(const std::string& name)
{ {
+30 -6
View File
@@ -205,11 +205,13 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
"-ac_reward=300000000", "-ac_reward=300000000",
"-ac_blocktime=36", "-ac_blocktime=36",
"-ac_private=1", "-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=node1.dragonx.is",
"-addnode=node2.dragonx.is", "-addnode=node5.dragonx.is",
"-addnode=node3.dragonx.is",
"-addnode=node4.dragonx.is",
"-experimentalfeatures", "-experimentalfeatures",
"-developerencryptwallet", "-developerencryptwallet",
// Create fresh wallets from a BIP39 mnemonic so their 24-word phrase can be // Create fresh wallets from a BIP39 mnemonic so their 24-word phrase can be
@@ -543,6 +545,15 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
args.push_back("-maxconnections=" + std::to_string(max_connections_)); 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 // 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 // non-default name so the common case's command line is unchanged; skip during an isolated
// start (seed migration manages its own throwaway wallet). // start (seed migration manages its own throwaway wallet).
@@ -689,7 +700,10 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
debug_log_path_.c_str(), debug_log_offset_); 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) // 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. // — it must be in <exe_dir>/dragonx/ to avoid conflicts with lock files and data.
STARTUPINFOA si; STARTUPINFOA si;
@@ -707,7 +721,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
NULL, NULL,
NULL, NULL,
FALSE, FALSE,
CREATE_NEW_CONSOLE, CREATE_NO_WINDOW,
NULL, NULL,
work_dir.c_str(), work_dir.c_str(),
&si, &si,
@@ -802,6 +816,16 @@ void EmbeddedDaemon::drainOutput()
} }
size_t currentSize = static_cast<size_t>(fileSize.QuadPart); 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_) { if (currentSize <= debug_log_offset_) {
CloseHandle(hFile); CloseHandle(hFile);
return; // No new data return; // No new data
+3
View File
@@ -182,6 +182,7 @@ public:
* @brief Set maximum peer connections (0 = use daemon default) * @brief Set maximum peer connections (0 = use daemon default)
*/ */
void setMaxConnections(int v) { max_connections_ = v; } 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 * @brief Request a blockchain rescan on the next daemon start
@@ -311,6 +312,8 @@ private:
std::atomic<bool> should_stop_{false}; std::atomic<bool> should_stop_{false};
std::set<std::string> debug_categories_; std::set<std::string> debug_categories_;
int max_connections_ = 0; // 0 = daemon default 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::string wallet_file_; // -wallet=<name> for the active wallet; empty/"wallet.dat" = default
std::atomic<int> crash_count_{0}; // consecutive crash counter std::atomic<int> crash_count_{0}; // consecutive crash counter
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
+14 -41
View File
@@ -23,6 +23,7 @@
#include <curl/curl.h> #include <curl/curl.h>
#include "../util/logger.h" #include "../util/logger.h"
#include "../util/platform.h"
#include "../util/pool_registry.h" #include "../util/pool_registry.h"
#ifdef _WIN32 #ifdef _WIN32
@@ -145,32 +146,18 @@ std::string XmrigManager::findXmrigBinary() {
return path; return path;
} }
// Fallback: system PATH // Fallback: system PATH — windowless so it never flashes a console.
#ifdef _WIN32 #ifdef _WIN32
FILE* f = _popen("where xmrig.exe 2>nul", "r"); std::string out = util::Platform::runHiddenCapture("where xmrig.exe");
#else #else
FILE* f = popen("which xmrig 2>/dev/null", "r"); std::string out = util::Platform::runHiddenCapture("which xmrig");
#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);
#endif #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 {}; return {};
@@ -927,24 +914,10 @@ void XmrigManager::startVersionDetection()
const bool binShellSafe = const bool binShellSafe =
!bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos; !bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos;
if (binShellSafe) { if (binShellSafe) {
const std::string cmd = "\"" + bin + "\" --version 2>&1"; // Windowless capture (mergeStderr: xmrig may print --version to stderr) — never flashes.
#ifdef _WIN32 const std::string cmd = "\"" + bin + "\" --version";
FILE* fp = _popen(cmd.c_str(), "r"); const std::string out = util::Platform::runHiddenCapture(cmd, /*mergeStderr=*/true);
#else if (!out.empty()) ver = parseMinerVersion(out);
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);
}
} }
std::lock_guard<std::mutex> lk(g_installed_ver_mutex); std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
g_installed_ver = ver; g_installed_ver = ver;
+54 -5
View File
@@ -1263,6 +1263,36 @@ int main(int argc, char* argv[])
SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale)); 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 // 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 // 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). // 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", DEBUG_LOGF("Startup: window fitted %dx%d -> %dx%d (scale %.2f)\n",
curW, curH, newW, newH, currentDpiScale); curW, curH, newW, newH, currentDpiScale);
} }
}
} }
#endif #endif
winlog("STARTUP savedSize=%dx%d currentDpiScale=%.3f", savedWinW, savedWinH, currentDpiScale); 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. // WINDOW_RESIZED events during the transition can't corrupt savedSizeForScale / lastKnownW/H.
int dpiSettleFrames = 0; int dpiSettleFrames = 0;
SDL_DisplayID lastLoggedDisplay = 0; // [WINLOG] throttle: log MOVED only when the display changes 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(); float s = dragonx::ui::material::Typography::instance().getDpiScale();
int w = 0, h = 0; int w = 0, h = 0;
@@ -1552,6 +1584,7 @@ int main(int argc, char* argv[])
// Window restored from minimized — trigger immediate data refresh // Window restored from minimized — trigger immediate data refresh
if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED && if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED &&
waitEvent.window.windowID == SDL_GetWindowID(window)) { waitEvent.window.windowID == SDL_GetWindowID(window)) {
app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
app.refreshNow(); app.refreshNow();
} }
// Handle DPI change that arrived while idle (same logic as poll loop) // 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 // Window restored from minimized — trigger immediate data refresh
if (event.type == SDL_EVENT_WINDOW_RESTORED && if (event.type == SDL_EVENT_WINDOW_RESTORED &&
event.window.windowID == SDL_GetWindowID(window)) { event.window.windowID == SDL_GetWindowID(window)) {
app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
app.refreshNow(); app.refreshNow();
} }
// Handle DPI/display scale changes (e.g. window dragged to a // 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 // Check if window is minimized
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) { if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
// Still check shouldQuit while minimized to avoid hang // Keep the wallet syncing while minimized: run the logic update (drains RPC results, ticks the
if (app.shouldQuit()) { // refresh scheduler, keeps the daemon connection/reconnect + sync status live) but skip the
running = false; // 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
SDL_Delay(10); // 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; continue;
} }
minimizedLastTickMs = 0; // visible again — reset the minimized clock
// --- PerfLog: begin frame --- // --- PerfLog: begin frame ---
dragonx::util::PerfLog::instance().beginFrame(); dragonx::util::PerfLog::instance().beginFrame();
+4 -4
View File
@@ -456,11 +456,11 @@ bool Connection::createDefaultConfig(const std::string& path)
file << "exportdir=" << dataDir << "\n"; file << "exportdir=" << dataDir << "\n";
file << "experimentalfeatures=1\n"; file << "experimentalfeatures=1\n";
file << "developerencryptwallet=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=node1.dragonx.is\n";
file << "addnode=node2.dragonx.is\n"; file << "addnode=node5.dragonx.is\n";
file << "addnode=node3.dragonx.is\n";
file << "addnode=node4.dragonx.is\n";
file.close(); file.close();
+64 -9
View File
@@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <chrono>
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include <map> #include <map>
@@ -294,8 +295,37 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
json blockInfo; json blockInfo;
bool balanceOk = false; bool balanceOk = false;
bool blockOk = false; bool blockOk = false;
double balanceScanMs = 0.0;
if (includeBalance) { // 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;
} catch (const std::exception& e) {
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
}
// 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 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})); totalBalance = rpc.call("z_gettotalbalance", json::array({0}));
balanceOk = true; balanceOk = true;
@@ -305,16 +335,13 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
try { // SPENDABLE total: minconf=1 (confirmed). If absent, spendable degrades to display in apply. try { // SPENDABLE total: minconf=1 (confirmed). If absent, spendable degrades to display in apply.
spendableBalance = rpc.call("z_gettotalbalance", json::array({1})); spendableBalance = rpc.call("z_gettotalbalance", json::array({1}));
} catch (...) {} } catch (...) {}
balanceScanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - balanceStart).count();
} }
try { auto result = parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk);
blockInfo = rpc.call("getblockchaininfo", json::array()); result.balanceScanMs = balanceScanMs;
blockOk = true; return result;
} catch (const std::exception& e) {
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
}
return parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk);
} }
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult( NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult(
@@ -593,6 +620,10 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
const AddressRefreshSnapshot& snapshot) const AddressRefreshSnapshot& snapshot)
{ {
AddressRefreshResult result; 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 { try {
json zList = rpc.call("z_listaddresses", json::array()); json zList = rpc.call("z_listaddresses", json::array());
@@ -634,6 +665,22 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
try { try {
json unspent = rpc.call("z_listunspent", json::array({0, 9999999, false})); // minconf=0 → include 0-conf change json unspent = rpc.call("z_listunspent", json::array({0, 9999999, false})); // minconf=0 → include 0-conf change
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent); 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) { } catch (const std::exception& e) {
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what()); DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
for (auto& info : result.shieldedAddresses) { for (auto& info : result.shieldedAddresses) {
@@ -663,6 +710,8 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
DEBUG_LOGF("listunspent error: %s\n", e.what()); 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; return result;
} }
@@ -854,6 +903,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
result.blockHeight = currentBlockHeight; result.blockHeight = currentBlockHeight;
result.shieldedAddressCount = snapshot.shieldedAddresses.size(); result.shieldedAddressCount = snapshot.shieldedAddresses.size();
result.shieldedScanHeights = snapshot.shieldedScanHeights; 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; std::set<std::string> knownTxids;
HushChatMemoOutputMap hushChatReceivedOutputs; HushChatMemoOutputMap hushChatReceivedOutputs;
@@ -1034,6 +1087,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
} }
sortTransactionsNewestFirst(result.transactions); sortTransactionsNewestFirst(result.transactions);
result.scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return result; return result;
} }
+18
View File
@@ -111,6 +111,7 @@ public:
std::optional<double> verificationProgress; std::optional<double> verificationProgress;
std::optional<int> longestChain; std::optional<int> longestChain;
std::optional<int> notarized; std::optional<int> notarized;
double balanceScanMs = 0.0; // wall-clock spent in z_gettotalbalance this refresh (0 if balance skipped)
}; };
struct MiningRefreshResult { struct MiningRefreshResult {
@@ -146,6 +147,14 @@ public:
std::string errorMessage; 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 { struct AddressRefreshResult {
std::vector<AddressInfo> shieldedAddresses; std::vector<AddressInfo> shieldedAddresses;
std::vector<AddressInfo> transparentAddresses; std::vector<AddressInfo> transparentAddresses;
@@ -153,6 +162,12 @@ public:
// lists may be falsely short. Consumers that treat an empty list as authoritative (e.g. 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. // empty-wallet warning) must not trust a 0 count unless this is true.
bool addressListOk = 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 { struct AddressRefreshSnapshot {
@@ -204,6 +219,9 @@ public:
std::size_t shieldedAddressCount = 0; std::size_t shieldedAddressCount = 0;
std::unordered_map<std::string, int> shieldedScanHeights; std::unordered_map<std::string, int> shieldedScanHeights;
bool shieldedScanComplete = true; 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 { struct OperationStatusPollResult {
+12 -8
View File
@@ -5,6 +5,7 @@
#include "theme_effects.h" #include "theme_effects.h"
#include "low_spec.h" #include "low_spec.h"
#include "../schema/ui_schema.h" #include "../schema/ui_schema.h"
#include "../layout.h"
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@@ -58,6 +59,7 @@ void ThemeEffects::beginFrame() {
void ThemeEffects::loadFromTheme() { void ThemeEffects::loadFromTheme() {
auto& S = schema::UI(); auto& S = schema::UI();
const float dp = Layout::dpiScale();
auto eff = [&](const char* name) { auto eff = [&](const char* name) {
return S.drawElement("effects", name); return S.drawElement("effects", name);
}; };
@@ -98,7 +100,7 @@ void ThemeEffects::loadFromTheme() {
// ---- Shimmer ---- // ---- Shimmer ----
shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f; shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
shimmer_.speed = eff("shimmer-speed").sizeOr(0.12f); 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_.alpha = eff("shimmer-alpha").sizeOr(0.06f);
shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f); shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f);
// Shimmer color: read from the schema's color resolver // 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_.speed = eff("glow-pulse-speed").sizeOr(2.0f);
glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.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_.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"); auto glowColorElem = eff("glow-pulse-color");
if (!glowColorElem.color.empty()) { if (!glowColorElem.color.empty()) {
glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255)); 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_.enabled = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f;
edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f); edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f);
edge_trace_.length = eff("edge-trace-length").sizeOr(0.20f); 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); edge_trace_.alpha = eff("edge-trace-alpha").sizeOr(0.6f);
auto edgeColorElem = eff("edge-trace-color"); auto edgeColorElem = eff("edge-trace-color");
if (!edgeColorElem.color.empty()) { 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_.enabled = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f;
ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f); ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f);
ember_rise_.speed = eff("ember-rise-speed").sizeOr(0.4f); 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); ember_rise_.alpha = eff("ember-rise-alpha").sizeOr(0.5f);
auto emberColorElem = eff("ember-rise-color"); auto emberColorElem = eff("ember-rise-color");
if (!emberColorElem.color.empty()) { if (!emberColorElem.color.empty()) {
@@ -165,7 +167,7 @@ void ThemeEffects::loadFromTheme() {
// accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero. // 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_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f); 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); gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
auto gbColorA = eff("gradient-border-color-a"); auto gbColorA = eff("gradient-border-color-a");
if (!gbColorA.color.empty()) { if (!gbColorA.color.empty()) {
@@ -194,7 +196,7 @@ void ThemeEffects::loadFromTheme() {
sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f); sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f);
sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f); sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f);
sandstorm_.windAngle = eff("sandstorm-wind-angle").sizeOr(15.0f); 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_.alpha = eff("sandstorm-alpha").sizeOr(0.35f);
sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f); sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f);
sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f); 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 { void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const {
if (!enabled_ || !ember_rise_.enabled) return; if (!enabled_ || !ember_rise_.enabled) return;
const float dp = Layout::dpiScale();
float w = pMax.x - pMin.x; float w = pMax.x - pMin.x;
float h = pMax.y - pMin.y; float h = pMax.y - pMin.y;
if (w <= 0 || h <= 0) return; 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 // Deterministic pseudo-random x position per particle
// Simple hash: sin of large prime multiples // Simple hash: sin of large prime multiples
float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f; 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 x = pMin.x + w * xHash + xDrift;
float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top 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 { void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
if (!enabled_ || !ember_rise_.enabled) return; if (!enabled_ || !ember_rise_.enabled) return;
const float dp = Layout::dpiScale();
ImGuiViewport* vp = ImGui::GetMainViewport(); ImGuiViewport* vp = ImGui::GetMainViewport();
float vpW = vp->WorkSize.x; float vpW = vp->WorkSize.x;
float vpH = vp->WorkSize.y; 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; float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
xHash = xHash - (int)xHash; // fractional part xHash = xHash - (int)xHash; // fractional part
if (xHash < 0) xHash += 1.0f; 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 x = vpX + vpW * xHash + xDrift;
float y = vpY + vpH * (1.0f - phase); // rise from bottom to top float y = vpY + vpH * (1.0f - phase); // rise from bottom to top
+17 -5
View File
@@ -173,11 +173,23 @@ 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 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); } 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 should occupy before it is centered in wider // Overall content-column cap: the max width a tab's content occupies before it is centered in wider
// windows. Prevents cards/forms/tables (which all derive their size from the content child's width) from // windows. <= 0 disables the cap so tab content fills ALL available horizontal width (the default —
// stretching edge-to-edge at wide/ultrawide widths. <= 0 disables (fill full width). Tunable via ui.toml // requested so large windows don't leave a big empty gutter on the right). Set a positive
// [layout] content-max-width; default is generous so data-dense screens stay comfortable. // 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(1600.0f) * dpiScale(); } 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 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); } inline float kTableHeightRatio() { return schema::UI().drawElement("panels", "table").getFloat("height-ratio", 0.45f); }
+19 -13
View File
@@ -544,11 +544,16 @@ inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0),
ImVec2 bMin = ImGui::GetItemRectMin(); ImVec2 bMin = ImGui::GetItemRectMin();
ImVec2 bMax = ImGui::GetItemRectMax(); 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) { 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); 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; float rounding = ImGui::GetStyle().FrameRounding;
@@ -920,7 +925,7 @@ inline void DrawStatCard(ImDrawList* dl,
// Draw a full-height rounded rect with card rounding (left corners) // Draw a full-height rounded rect with card rounding (left corners)
// and clip to stripe width so the shape follows the corner radius. // and clip to stripe width so the shape follows the corner radius.
if ((card.accentCol & IM_COL32_A_MASK) != 0) { 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->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, card.accentCol, rnd, dl->AddRectFilled(cMin, cMax, card.accentCol, rnd,
ImDrawFlags_RoundCornersLeft); ImDrawFlags_RoundCornersLeft);
@@ -1277,7 +1282,8 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 winPos = ImGui::GetWindowPos(); ImVec2 winPos = ImGui::GetWindowPos();
float winWidth = ImGui::GetWindowWidth(); 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 // Get accent color from theme if not provided
if (!accent_col) { if (!accent_col) {
@@ -1301,15 +1307,15 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
ImFont* titleFont = Type().subtitle1(); ImFont* titleFont = Type().subtitle1();
ImGui::PushFont(titleFont); ImGui::PushFont(titleFont);
ImVec2 titleSize = ImGui::CalcTextSize(title); 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; float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f;
DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title); DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
ImGui::PopFont(); ImGui::PopFont();
// Close button (X) on right side // Close button (X) on right side
if (p_open) { if (p_open) {
float btnSize = 24.0f; float btnSize = 24.0f * dp;
float btnX = barMax.x - btnSize - 12.0f; float btnX = barMax.x - btnSize - 12.0f * dp;
float btnY = barMin.y + (barHeight - btnSize) * 0.5f; float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
ImVec2 btnMin(btnX, btnY); ImVec2 btnMin(btnX, btnY);
ImVec2 btnMax(btnX + btnSize, btnY + btnSize); ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
@@ -1322,7 +1328,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
// Button background on hover // Button background on hover
if (hovered) { 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 // Draw X icon
@@ -1343,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 // 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; return closeClicked;
} }
@@ -1643,7 +1649,7 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// Fill/border alpha govern every overlay dialog's card boundary — kept well above the default // 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 // glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
// tiles, chat) while staying translucent rather than opaque. // tiles, chat) while staying translucent rather than opaque.
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f; cardGlass.rounding = 16.0f * dp; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass); DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
} }
@@ -1657,8 +1663,8 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// Content child. // Content child.
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY)); ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f * dp : 16.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24)); 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) ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
// A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose // 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. // content overflowed the viewport); otherwise the child auto-resizes to its content.
+8 -6
View File
@@ -107,7 +107,7 @@ inline float ActionButtonWidth(const char* label, const char* icon, float minWid
ImFont* lf = Type().button(); ImFont* lf = Type().button();
ImFont* icf = Type().iconSmall(); ImFont* icf = Type().iconSmall();
const float dp = Layout::dpiScale(); 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 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 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; 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); if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
const ImVec2 pMax(pos.x + w, pos.y + h); 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 const float a = ImGui::GetStyle().Alpha; // BeginDisabled() lowers this
ImU32 bg = 0, border = 0, fg = OnSurface(); ImU32 bg = 0, border = 0, fg = OnSurface();
bool glass = false; bool glass = false;
switch (tier) { switch (tier) {
case ActionTier::Primary: case ActionTier::Primary:
bg = WithAlpha(Primary(), act ? 255 : (hov ? 245 : 220)); // Mockup .btn.acc: a dark accent-tinted chip with accent TEXT — not a bright filled button.
fg = IM_COL32(255, 255, 255, 240); bg = WithAlpha(Primary(), hov ? 52 : 38);
border = WithAlpha(Primary(), hov ? 150 : 110);
fg = Primary();
break; break;
case ActionTier::Secondary: case ActionTier::Secondary:
bg = WithAlpha(OnSurface(), hov ? 26 : 16); bg = WithAlpha(OnSurface(), hov ? 30 : 20);
border = WithAlpha(OnSurface(), 40); border = WithAlpha(OnSurface(), 48);
glass = true; glass = true;
fg = OnSurface(); fg = OnSurface();
break; break;
+14 -2
View File
@@ -31,6 +31,8 @@ struct AlertRecord {
std::string message; std::string message;
NotificationType type; NotificationType type;
std::int64_t epoch; // std::time(nullptr) at push — wall-clock, for relative-age display 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 { struct Notification {
@@ -93,14 +95,24 @@ public:
push(message, NotificationType::Error, duration); push(message, NotificationType::Error, duration);
} }
void push(const std::string& message, NotificationType type, float duration = 5.0f) { // 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,
std::function<void()> onClick = nullptr, const std::string& actionHint = "") {
notifications_.emplace_back(message, type, duration); notifications_.emplace_back(message, type, duration);
// Retain a copy in the persistent history (the toast above will fade in seconds; this // 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 // 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, // 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. // 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_; ++total_pushed_;
while (history_.size() > kMaxHistory) { while (history_.size() > kMaxHistory) {
history_.pop_front(); history_.pop_front();
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -219,7 +219,7 @@ inline void DrawGlassCutout(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
float lineW = s_cc.lineW; float lineW = s_cc.lineW;
// --- Outer glow pass: wider, softer dark edge on top-left --- // --- 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; int glowA = (int)s_cc.glowAlpha;
float glowLineW = s_cc.glowLineW; 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 // inner pass gives crisp bevel edge. All use AddRect with rounding so
// every layer follows the rounded corners perfectly — no clip rects needed. // 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 cx = (mn.x + mx.x) * 0.5f;
float cy = (mn.y + mx.y) * 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; }; struct BevelPass { float expand; float lineW; float fadeStart; float fadeEnd; };
BevelPass passes[] = { 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 { 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) { if (depth > s_ic.threshold) {
float baseInset = s_ic.inset; float baseInset = s_ic.inset * Layout::dpiScale();
int shadowMax = (int)(s_ic.maxAlpha * depth); int shadowMax = (int)(s_ic.maxAlpha * depth);
float fadeRatio = s_ic.fadeRatio; float fadeRatio = s_ic.fadeRatio;
float bW = mx.x - mn.x - baseInset * 2.0f; 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 float fixedH = stripH; // collapse strip
for (int i = 0; i < (int)NavPage::Count_; ++i) for (int i = 0; i < (int)NavPage::Count_; ++i)
if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels) if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels)
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below fixedH += olFsz + 2.0f * dp + sectionLabelPadBot; // section label + pad below
fixedH += bottomPadding + stripH; // exit area fixedH += bottomPadding + stripH; // exit area
float baseFlexH = baseNavGap; float baseFlexH = baseNavGap;
@@ -525,7 +526,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
if (showLabels) { if (showLabels) {
curY += sectionGap; curY += sectionGap;
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY; if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
curY += olFsz + 2.0f + sectionLabelPadBot; curY += olFsz + 2.0f * dp + sectionLabelPadBot;
} else { } else {
curY += sectionGap * 0.4f; curY += sectionGap * 0.4f;
if (nSeparators < 4) separatorY[nSeparators++] = curY; if (nSeparators < 4) separatorY[nSeparators++] = curY;
@@ -538,11 +539,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float exitRelY = curY + bottomPadding; float exitRelY = curY + bottomPadding;
float panelH = exitRelY + stripH; 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 // PASS 2: Render using computed positions
// =================================================================== // ===================================================================
@@ -552,6 +548,13 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 wp = ImGui::GetWindowPos(); 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 panelLeft = wp.x + glassMarginL;
float panelRight = wp.x + sidebarWidth - glassMarginR; float panelRight = wp.x + sidebarWidth - glassMarginR;
float panelTopY = wp.y + centerOffset; 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.drawShimmer(dl, indMin, indMax, btnRnd);
fx.drawGradientBorderShift(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); DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18);
buttonRects.push_back({indMin, indMax, btnRnd}); buttonRects.push_back({indMin, indMax, btnRnd});
} }
@@ -682,7 +685,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
bool itemHasBadge = bool itemHasBadge =
(item.page == NavPage::History && status.unconfirmedTxCount > 0) || (item.page == NavPage::History && status.unconfirmedTxCount > 0) ||
(item.page == NavPage::Mining && status.miningActive) || (item.page == NavPage::Mining && status.miningActive) ||
(item.page == NavPage::Peers && status.peerCount > 0) ||
(item.page == NavPage::Chat && status.chatUnreadCount > 0); (item.page == NavPage::Chat && status.chatUnreadCount > 0);
float badgeReserve = 0.0f; float badgeReserve = 0.0f;
if (itemHasBadge) { if (itemHasBadge) {
@@ -730,8 +732,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
badgeCol = Warning(); badgeTextCol = OnWarning(); badgeCol = Warning(); badgeTextCol = OnWarning();
} else if (item.page == NavPage::Mining && status.miningActive) { } else if (item.page == NavPage::Mining && status.miningActive) {
dotOnly = true; badgeCol = Success(); 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) { } else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) {
badgeCount = status.chatUnreadCount; badgeCount = status.chatUnreadCount;
} }
+3 -2
View File
@@ -93,7 +93,7 @@ public:
// Arrow // Arrow
{ {
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f; float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
ImGui::SetCursorPosX(arrowCX - 8.0f); ImGui::SetCursorPosX(arrowCX - 8.0f * dp);
ImFont* iconFont = Type().iconMed(); ImFont* iconFont = Type().iconMed();
float fsz = ScaledFontSize(iconFont); float fsz = ScaledFontSize(iconFont);
ImVec2 pos = ImGui::GetCursorScreenPos(); ImVec2 pos = ImGui::GetCursorScreenPos();
@@ -312,7 +312,8 @@ private:
ImGui::Spacing(); ImGui::Spacing();
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); ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
if (TactileButton(TR("close"), ImVec2(btnW, 0))) { if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
s_open = false; s_open = false;
+1 -1
View File
@@ -264,7 +264,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
} else if (rows.empty()) { } else if (rows.empty()) {
float cw = ImGui::GetContentRegionAvail().x; float cw = ImGui::GetContentRegionAvail().x;
float ch = ImGui::GetContentRegionAvail().y; 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"); const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg); ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f); ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
+51 -51
View File
@@ -302,9 +302,9 @@ static void RenderBalanceClassic(App* app)
float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg(); float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();
// Card height: must fit the Market card's content (overline + price + 24h) // 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 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); 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); const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f) * dp;
float marketContentH = cardPadLg float marketContentH = cardPadLg
+ ovFont->LegacySize + ovGap + ovFont->LegacySize + ovGap
+ sub1->LegacySize + 2.0f * dp + sub1->LegacySize + 2.0f * dp
@@ -323,7 +323,7 @@ static void RenderBalanceClassic(App* app)
// Helper: draw accent stripe on left edge, clipped to card rounded corners. // 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 // 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. // 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) { auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) {
dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true); dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding, dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
@@ -446,7 +446,7 @@ static void RenderBalanceClassic(App* app)
ImU32 bd = WithAlpha(fg, 90); ImU32 bd = WithAlpha(fg, 90);
snprintf(buf, sizeof(buf), "%s %s", snprintf(buf, sizeof(buf), "%s %s",
TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str()); 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. // 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))) if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
Tooltip("%s", TR("data_stale_tooltip")); Tooltip("%s", TR("data_stale_tooltip"));
@@ -456,7 +456,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow // Hover glow
if (material::IsRectHovered(cMin, cMax)) { 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)), 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);
} }
} }
@@ -487,7 +487,7 @@ static void RenderBalanceClassic(App* app)
{ {
float privPct = (s_dispTotal > 1e-9) float privPct = (s_dispTotal > 1e-9)
? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f; ? (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()); privPct, (int)state.z_addresses.size());
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
WithAlpha(Success(), 160), buf); WithAlpha(Success(), 160), buf);
@@ -498,8 +498,8 @@ static void RenderBalanceClassic(App* app)
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance); snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
ImVec2 ts = capFont->CalcTextSizeA( ImVec2 ts = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, buf); capFont->LegacySize, 10000, 0, buf);
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").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); float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f) * dp;
ImVec2 bMin(cMax.x - ts.x - bp * 3, ImVec2 bMin(cMax.x - ts.x - bp * 3,
cMin.y + cardPadLg); cMin.y + cardPadLg);
ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp); ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
@@ -513,7 +513,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Receive // Hover glow + click to Receive
if (material::IsRectHovered(cMin, cMax)) { 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)), 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); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Receive); app->setCurrentPage(NavPage::Receive);
@@ -543,7 +543,7 @@ static void RenderBalanceClassic(App* app)
OnSurfaceMedium(), DRAGONX_TICKER); OnSurfaceMedium(), DRAGONX_TICKER);
cy += sub1->LegacySize + valGap; 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()); (int)state.t_addresses.size());
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
OnSurfaceDisabled(), buf); OnSurfaceDisabled(), buf);
@@ -551,7 +551,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Receive // Hover glow + click to Receive
if (material::IsRectHovered(cMin, cMax)) { 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)), 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); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Receive); app->setCurrentPage(NavPage::Receive);
@@ -589,7 +589,7 @@ static void RenderBalanceClassic(App* app)
[](unsigned char c){ return (char)std::toupper(c); }); [](unsigned char c){ return (char)std::toupper(c); });
float textW = std::max(pSz.x + tickGap + usdSz.x, float textW = std::max(pSz.x + tickGap + usdSz.x,
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x); ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f); float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f) * dp;
float sparkLeft = cx + textW + sparkGap; float sparkLeft = cx + textW + sparkGap;
float sparkRight = cMax.x - cardPadLg; float sparkRight = cMax.x - cardPadLg;
@@ -611,7 +611,7 @@ static void RenderBalanceClassic(App* app)
bool pos = market.change_24h >= 0; bool pos = market.change_24h >= 0;
ImU32 chgCol = pos ? Success() ImU32 chgCol = pos ? Success()
: Error(); : Error();
snprintf(buf, sizeof(buf), "%s%.1f%% 24h", snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"),
pos ? "+" : "", market.change_24h); pos ? "+" : "", market.change_24h);
dl->AddText(capFont, capFont->LegacySize, dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx, cy), chgCol, buf); ImVec2(cx, cy), chgCol, buf);
@@ -633,7 +633,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Market // Hover glow + click to Market
if (material::IsRectHovered(cMin, cMax)) { 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)), 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); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Market); app->setCurrentPage(NavPage::Market);
@@ -790,20 +790,20 @@ static void RenderBalanceDonut(App* app) {
ImFont* capFont = Type().caption(); ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2(); ImFont* body2 = Type().body2();
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.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); 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); 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); float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f) * dp;
// Shielded legend // Shielded legend
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success()); 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); dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf);
legendY += capFont->LegacySize + legendLineGap; legendY += capFont->LegacySize + legendLineGap;
// Transparent legend // Transparent legend
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning()); 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); dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf);
legendY += capFont->LegacySize + legendSectionGap; legendY += capFont->LegacySize + legendSectionGap;
@@ -811,15 +811,15 @@ static void RenderBalanceDonut(App* app) {
const auto& market = state.market; const auto& market = state.market;
if (market.price_usd > 0) { if (market.price_usd > 0) {
if (market.price_usd >= 0.01) 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 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), dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
OnSurfaceMedium(), buf); OnSurfaceMedium(), buf);
legendY += capFont->LegacySize + 4 * dp; legendY += capFont->LegacySize + 4 * dp;
bool pos = market.change_24h >= 0; 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), dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
pos ? Success() : Error(), buf); pos ? Success() : Error(), buf);
} }
@@ -946,7 +946,7 @@ static void RenderBalanceConsolidated(App* app) {
float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f); 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), 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)), 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 // Bottom half: proportion bars
float barY = divY + Layout::spacingSm(); float barY = divY + Layout::spacingSm();
@@ -1122,7 +1122,7 @@ static void RenderBalanceDashboard(App* app) {
// Accent stripe — clipped to tile rounded corners // 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->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true);
dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding, dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
ImDrawFlags_RoundCornersLeft); ImDrawFlags_RoundCornersLeft);
@@ -1159,7 +1159,7 @@ static void RenderBalanceDashboard(App* app) {
// Click // Click
if (material::IsRectHovered(tMin, tMax)) { 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)), 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); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) if (ImGui::IsMouseClicked(0))
app->setCurrentPage(tiles[i].nav); app->setCurrentPage(tiles[i].nav);
@@ -1208,7 +1208,7 @@ static void RenderBalanceVerticalStack(App* app) {
// Font-content floor per row: icon + label + value must fit // Font-content floor per row: icon + label + value must fit
float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize) float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
+ Layout::spacingSm() * 2; + 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 vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3;
float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size; float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
float stackH; float stackH;
@@ -1238,10 +1238,10 @@ static void RenderBalanceVerticalStack(App* app) {
}; };
RowInfo rowInfos[4] = { RowInfo rowInfos[4] = {
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f}, {TR("baltab_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}, {TR("baltab_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}, {TR("baltab_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_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
}; };
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
@@ -1296,7 +1296,7 @@ static void RenderBalanceVerticalStack(App* app) {
// Proportion bar (for shielded/transparent rows — fills gap between label and amount) // Proportion bar (for shielded/transparent rows — fills gap between label and amount)
if (i == 1 || i == 2) { if (i == 1 || i == 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label); 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 barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
float barH = std::max( float barH = std::max(
S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f), S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f),
@@ -1326,8 +1326,8 @@ static void RenderBalanceVerticalStack(App* app) {
// Sparkline in the gap between label and 24h change // Sparkline in the gap between label and 24h change
if (state.market.price_history.size() >= 2) { if (state.market.price_history.size() >= 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label); 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 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); float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = px + labelSz.x + sparkGap; float sparkLeft = px + labelSz.x + sparkGap;
float sparkRight = chgX - sparkGap; float sparkRight = chgX - sparkGap;
if (sparkLeft < sparkRight) { if (sparkLeft < sparkRight) {
@@ -1387,8 +1387,8 @@ static void RenderBalanceVertical2x2(App* app) {
ImFont* iconFont = Type().iconSmall(); ImFont* iconFont = Type().iconSmall();
// Font-content floor per row: caption text + vertical padding // Font-content floor per row: caption text + vertical padding
float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2; float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f); float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f) * dp;
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f); float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f) * dp;
float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap; float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
float cardHOverride = S.drawElement(cfgSec, "card-height").size; float cardHOverride = S.drawElement(cfgSec, "card-height").size;
float stackH; float stackH;
@@ -1431,13 +1431,13 @@ static void RenderBalanceVertical2x2(App* app) {
CellInfo cells[2][2] = { CellInfo cells[2][2] = {
// Row 0: Total Balance (left), Shielded (right) // 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}, {TR("baltab_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_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
}, },
// Row 1: Market (left), Transparent (right) // 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}, {TR("baltab_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_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
}, },
}; };
@@ -1519,8 +1519,8 @@ static void RenderBalanceVertical2x2(App* app) {
// Sparkline between label and 24h change // Sparkline between label and 24h change
if (state.market.price_history.size() >= 2) { if (state.market.price_history.size() >= 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label); ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f); float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f) * dp;
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f); float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = px + labelSz.x + sparkGap; float sparkLeft = px + labelSz.x + sparkGap;
float sparkRight = chgX - sparkGap; float sparkRight = chgX - sparkGap;
if (sparkLeft < sparkRight) { if (sparkLeft < sparkRight) {
@@ -1665,7 +1665,7 @@ static void RenderBalanceShield(App* app) {
ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen, ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
gaugeCy + sinf(needleAngle) * needleLen); gaugeCy + sinf(needleAngle) * needleLen);
dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol, 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 // Center text: percentage
ImFont* sub1 = Type().subtitle1(); ImFont* sub1 = Type().subtitle1();
@@ -1956,7 +1956,7 @@ static void RenderBalanceTwoRow(App* app) {
} }
RenderSyncBar(app, dl, vs); 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 // Row 2: 3 mini-cards inline
{ {
@@ -1979,7 +1979,7 @@ static void RenderBalanceTwoRow(App* app) {
S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f), 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)); glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f));
ImFont* capFont = Type().caption(); 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); 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 twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm(); float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();
@@ -2054,8 +2054,8 @@ static void RenderBalanceTwoRow(App* app) {
// Sparkline between price and percentage // Sparkline between price and percentage
if (market.price_history.size() >= 2) { if (market.price_history.size() >= 2) {
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.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); float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = cx + priceSz.x + sparkGap; float sparkLeft = cx + priceSz.x + sparkGap;
float sparkRightEdge = sparkRight - sparkGap; float sparkRightEdge = sparkRight - sparkGap;
if (sparkLeft < sparkRightEdge) { if (sparkLeft < sparkRightEdge) {
@@ -2153,10 +2153,10 @@ static void RenderBalanceMinimal(App* app) {
{ {
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImVec2 sepPos = ImGui::GetCursorScreenPos(); ImVec2 sepPos = ImGui::GetCursorScreenPos();
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.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); 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 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 x = sepPos.x;
float endX = sepPos.x + availW; float endX = sepPos.x + availW;
while (x < endX) { while (x < endX) {
+4 -4
View File
@@ -59,7 +59,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
s_loading = false; s_loading = false;
if (!error.empty()) { if (!error.empty()) {
s_error = "Error: " + error; s_error = std::string(TR("grpa_error_prefix")) + error;
return; return;
} }
@@ -84,7 +84,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
s_has_data = true; s_has_data = true;
} else { } else {
s_error = "Invalid response from daemon"; s_error = TR("grpa_invalid_response_from_daemon");
} }
} }
@@ -125,7 +125,7 @@ void BlockInfoDialog::render(App* app)
// Current block info // Current block info
if (state.sync.blocks > 0) { if (state.sync.blocks > 0) {
ImGui::TextDisabled("(Current: %d)", state.sync.blocks); ImGui::TextDisabled(TR("grpa_current_block_paren"), state.sync.blocks);
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -152,7 +152,7 @@ void BlockInfoDialog::render(App* app)
rpc::RPCClient::TraceScope trace("Explorer / Block info"); rpc::RPCClient::TraceScope trace("Explorer / Block info");
auto hashResult = rpc->call("getblockhash", {height}); auto hashResult = rpc->call("getblockhash", {height});
if (!hashResult.is_string()) { if (!hashResult.is_string()) {
error = "unexpected getblockhash result"; error = TR("grpa_unexpected_getblockhash_result");
} else { } else {
block = rpc->call("getblock", {hashResult.get<std::string>()}); block = rpc->call("getblock", {hashResult.get<std::string>()});
} }
+2 -2
View File
@@ -144,7 +144,7 @@ private:
if (!s_bootstrap) { if (!s_bootstrap) {
s_state = State::Failed; s_state = State::Failed;
s_errorMsg = "Bootstrap not initialized"; s_errorMsg = TR("grpc_bootstrap_not_initialized");
return; return;
} }
@@ -227,7 +227,7 @@ private:
s_state = State::Done; s_state = State::Done;
} else { } else {
s_errorMsg = finalProg.error; 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_state = State::Failed;
} }
s_bootstrap.reset(); s_bootstrap.reset();
+54 -7
View File
@@ -425,9 +425,9 @@ void RenderChatSettingsPreview(App* app, float width) {
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; }; struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
const std::string peer = "Ava"; const std::string peer = "Ava";
const PMsg msgs[] = { const PMsg msgs[] = {
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 }, { TR("grpb_preview_msg_payment_through"), false, true, true, peer + " " + t1 },
{ u8"Yep — just confirmed ✅", true, true, false, std::string(TR("chat_you")) + " " + t2 }, { TR("grpb_preview_msg_yep_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_sending_rest"), true, false, true, std::string() },
}; };
const int N = 3; const int N = 3;
@@ -1885,20 +1885,37 @@ void ResetChatTab()
s_show_chat_settings = false; s_show_chat_settings = false;
} }
void RenderChatSettingsControls(App* app, float contentWidth) void RenderChatSettingsControls(App* app, float contentWidth, bool drawCards)
{ {
auto* st = app ? app->settings() : nullptr; auto* st = app ? app->settings() : nullptr;
if (!st) return; if (!st) return;
const float dp = Layout::dpiScale(); const float dp = Layout::dpiScale();
const float ctrlW = 250.0f * dp; // control column width (fits a 3-segment control comfortably) 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 // 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; // 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). // 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 // 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. // to split Appearance | Messaging into two columns when the card is wide.
float leftX = ImGui::GetCursorPosX(); 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. // Label left, control right-aligned within [leftX, leftX+rowW]. Leaves the cursor at the control origin.
auto beginRow = [&](const char* label) { auto beginRow = [&](const char* label) {
@@ -1966,7 +1983,7 @@ void RenderChatSettingsControls(App* app, float contentWidth)
// Two internal columns when the card is wide enough: Appearance on the left, // 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. // Messaging on the right — fills the width and roughly halves the height.
// (Mirrors the Node & Security card.) Narrow (the chat modal) stays single-column. // (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 bool chatTwoCol = rowW > 760.0f * dp;
const float chatColW = chatTwoCol ? (rowW - chatColGap) * 0.5f : rowW; const float chatColW = chatTwoCol ? (rowW - chatColGap) * 0.5f : rowW;
const float chatBaseLeftX = leftX; const float chatBaseLeftX = leftX;
@@ -2023,6 +2040,7 @@ void RenderChatSettingsControls(App* app, float contentWidth)
// line-start holds the column; retarget leftX so controls right-align in it). // line-start holds the column; retarget leftX so controls right-align in it).
if (chatTwoCol) { if (chatTwoCol) {
chatLeftBottomY = ImGui::GetCursorPosY(); chatLeftBottomY = ImGui::GetCursorPosY();
cardLeftBotScr = ImGui::GetCursorScreenPos().y; // left column bottom (screen), for its card panel
ImGui::SetCursorPosY(chatTopY); ImGui::SetCursorPosY(chatTopY);
ImGui::Indent(chatColW + chatColGap); ImGui::Indent(chatColW + chatColGap);
leftX = chatBaseLeftX + chatColW + chatColGap; leftX = chatBaseLeftX + chatColW + chatColGap;
@@ -2056,12 +2074,41 @@ void RenderChatSettingsControls(App* app, float contentWidth)
} }
// Close the two-column band: un-indent and drop below the taller column. // 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) { if (chatTwoCol) {
ImGui::Unindent(chatColW + chatColGap); ImGui::Unindent(chatColW + chatColGap);
const float chatRightBottomY = ImGui::GetCursorPosY(); const float chatRightBottomY = ImGui::GetCursorPosY();
ImGui::SetCursorPosX(chatBaseLeftX); ImGui::SetCursorPosX(chatBaseLeftX);
ImGui::SetCursorPosY(std::max(chatLeftBottomY, chatRightBottomY)); 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 } // namespace ui
+1 -1
View File
@@ -34,7 +34,7 @@ void RenderChatTab(App* app);
* width from the Settings tab (whose GlassCard doesn't narrow the content region). 0 = auto * 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). * (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 * @brief Securely wipe the Chat tab's UI-local state (composer / new-conversation
+1 -1
View File
@@ -337,7 +337,7 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
float outputH = ComputeConsoleOutputHeight( float outputH = ComputeConsoleOutputHeight(
availHeight, availHeight,
input_height, 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); schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
ImDrawList* dlOut = ImGui::GetWindowDrawList(); ImDrawList* dlOut = ImGui::GetWindowDrawList();
+6 -6
View File
@@ -108,13 +108,13 @@ static const char* relativeTime(int64_t timestamp) {
int64_t diff = now - timestamp; int64_t diff = now - timestamp;
if (diff < 0) diff = 0; if (diff < 0) diff = 0;
if (diff < 60) 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) 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) 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 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; return buf;
} }
@@ -1216,7 +1216,7 @@ static void renderBlockDetailModal(App* app) {
txDL->AddRectFilled(rowStart, txDL->AddRectFilled(rowStart,
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH), ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
WithAlpha(OnSurface(), 10), WithAlpha(OnSurface(), 10),
S.drawElement("tabs.explorer", "row-rounding").size); S.drawElement("tabs.explorer", "row-rounding").size * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", txid.c_str()); material::Tooltip("%s", txid.c_str());
} }
@@ -1315,7 +1315,7 @@ static void renderBlockDetailModal(App* app) {
} }
if (s_detail_txids.size() > 100) { 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); ImGui::TextDisabled("%s", buf);
} }
} }
+115
View File
@@ -0,0 +1,115 @@
// 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 ───────────────────────────────────────────────────────────
const std::vector<FaqSubcategory>& walletFaq()
{
static const std::vector<FaqSubcategory> kWallet = {
{ "faq_w_gs_title", {
{ "faq_w_gs_1_q", "faq_w_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" },
}},
{ "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" },
}},
{ "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" },
}},
{ "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" },
}},
{ "faq_w_sec_title", {
{ "faq_w_sec_1_q", "faq_w_sec_1_a" },
{ "faq_w_sec_2_q", "faq_w_sec_2_a" },
{ "faq_w_sec_3_q", "faq_w_sec_3_a" },
}},
{ "faq_w_seed_title", {
{ "faq_w_seed_1_q", "faq_w_seed_1_a" },
{ "faq_w_seed_2_q", "faq_w_seed_2_a" },
{ "faq_w_seed_3_q", "faq_w_seed_3_a" },
{ "faq_w_seed_4_q", "faq_w_seed_4_a" },
}},
{ "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_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" },
}},
};
return kWallet;
}
// ── 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
+41
View File
@@ -0,0 +1,41 @@
// 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()).
const std::vector<FaqSubcategory>& walletFaq();
const std::vector<FaqSubcategory>& daemonFaq();
} // namespace faq
} // namespace ui
} // namespace dragonx
+201
View File
@@ -0,0 +1,201 @@
// 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("faq_intro"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
const float contentW = ImGui::GetContentRegionAvail().x;
// ── Group tabs: Wallet | Daemon ──
{
const int nTabs = daemonAvailable ? 2 : 1;
const float gap = ImGui::GetStyle().ItemSpacing.x;
const float tabW = (contentW - gap * (nTabs - 1)) / static_cast<float>(nTabs);
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);
if (daemonAvailable) { 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();
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
+18
View File
@@ -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
+6 -5
View File
@@ -173,7 +173,7 @@ void KeyExportDialog::render(App* app)
s_key = found; s_key = found;
s_show_key = wantViewing; // viewing keys are less sensitive s_show_key = wantViewing; // viewing keys are less sensitive
} else { } 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); wallet::secureWipeLiteSecret(found);
s_fetching = false; s_fetching = false;
@@ -240,14 +240,15 @@ void KeyExportDialog::render(App* app)
ImGui::TextDisabled("%s", TR("key_export_click_retrieve")); ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
} else { } else {
// Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ]. // 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; const float avail = ImGui::GetContentRegionAvail().x;
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range. // Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
float qrSize = avail * 0.30f; float qrSize = avail * 0.30f;
if (qrSize < 200.0f) qrSize = 200.0f; if (qrSize < 200.0f * dp) qrSize = 200.0f * dp;
if (qrSize > 340.0f) qrSize = 340.0f; if (qrSize > 340.0f * dp) qrSize = 340.0f * dp;
float keyW = avail - qrSize - gap; 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; if (!sideBySide) keyW = avail;
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact // Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact
+3 -3
View File
@@ -1580,7 +1580,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
} }
} else { } else {
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable"); 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()) { if (!market.price_loading && !market.price_error.empty()) {
std::string errorText = market.price_error; std::string errorText = market.price_error;
float maxErrorW = cardMax.x - cx0 - Layout::spacingLg(); float maxErrorW = cardMax.x - cx0 - Layout::spacingLg();
@@ -1590,7 +1590,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
} }
if (errorText.size() < market.price_error.size()) errorText += "..."; if (errorText.size() < market.price_error.size()) errorText += "...";
dl->AddText(capFont, capFont->LegacySize, 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()); Warning(), errorText.c_str());
} }
} }
@@ -1874,7 +1874,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
: (tk == ticks - 1) ? plotRight - lblSz.x : (tk == ticks - 1) ? plotRight - lblSz.x
: xpos - lblSz.x * 0.5f; : xpos - lblSz.x * 0.5f;
dl->AddText(capFont, capFont->LegacySize, dl->AddText(capFont, capFont->LegacySize,
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl); ImVec2(lx, plotBottom + 4 * mktDp), OnSurfaceDisabled(), tlbl);
} }
} }
+7 -2
View File
@@ -153,7 +153,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
} }
}; };
ImFont* stepFont = Type().iconSmall(); ImFont* stepFont = Type().iconSmall();
const float fieldH = capFont->LegacySize + 6.0f * dp; // 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 sideW = fieldH; // square -/+ buttons
const float fieldW = 46.0f * dp; const float fieldW = 46.0f * dp;
const float g = 2.0f * dp; const float g = 2.0f * dp;
@@ -544,7 +549,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
s_benchConfirm = true; s_benchConfirm = true;
char msg[128]; char msg[128];
snprintf(msg, sizeof(msg), 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)); (int)(s_benchmark.totalEstimatedSecs() + 0.5f));
Notifications::instance().warning(msg); Notifications::instance().warning(msg);
} else { } else {
+2 -2
View File
@@ -220,7 +220,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
if (estActive) if (estActive)
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily); snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
else 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 // 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) // 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; const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
@@ -475,7 +475,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
else if (totalRAM > 0) else if (totalRAM > 0)
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0); snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
else 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 sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
float sysTextX = barX + barW - textPadX - sysTextW; float sysTextX = barX + barW - textPadX - sysTextW;
+5 -4
View File
@@ -326,7 +326,7 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
? it->second.feePercent ? it->second.feePercent
: kp.feePercent; : kp.feePercent;
if (feePct >= 0.0) 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()); FormatFeePercent(feePct).c_str());
else else
snprintf(right, sizeof(right), "%s", hrStr.c_str()); snprintf(right, sizeof(right), "%s", hrStr.c_str());
@@ -421,9 +421,10 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
// centered ~1000dp band inside the panel's padded content region; the // centered ~1000dp band inside the panel's padded content region; the
// glass panel itself still fills rightW, leftover -> side margin. // glass panel itself still fills rightW, leftover -> side margin.
const float rightContentW = rightW - pad * 2.0f; const float rightContentW = rightW - pad * 2.0f;
const float chartBandW = std::min(rightContentW, 1000.0f * dp); // Fill the panel's content width so the chart extends across the whole panel on a wide window.
const float chartBandX = rightMin.x + pad // (Previously capped at ~1000dp and centered, which left large empty margins either side.)
+ std::max(0.0f, (rightContentW - chartBandW) * 0.5f); const float chartBandW = rightContentW;
const float chartBandX = rightMin.x + pad;
// Right panel: live log (if toggled + available) else the sparkline. // Right panel: live log (if toggled + available) else the sparkline.
if (showLogView) { if (showLogView) {
+1 -2
View File
@@ -263,8 +263,7 @@ static void RenderMiningTabContent(App* app)
} }
if (benchmarkUpdate.inconclusive) { if (benchmarkUpdate.inconclusive) {
Notifications::instance().warning( Notifications::instance().warning(
"Benchmark inconclusive: no hashrate samples were recorded. " TR("grpc_benchmark_inconclusive"));
"Check the pool connection and try again.");
} }
} }
+4 -4
View File
@@ -380,7 +380,7 @@ void RenderPeersTab(App* app)
if (tlsCount == totalPeers) { if (tlsCount == totalPeers) {
ImFont* iconFont = Type().iconSmall(); ImFont* iconFont = Type().iconSmall();
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf); 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 { } else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94"); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
@@ -695,7 +695,7 @@ void RenderPeersTab(App* app)
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp); 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); 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->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel); dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2 * dp), dirFg, dirLabel);
} }
{ {
@@ -745,9 +745,9 @@ void RenderPeersTab(App* app)
ImU32 tlsBg = WithAlpha(Success(), 25); ImU32 tlsBg = WithAlpha(Success(), 25);
ImU32 tlsFg = WithAlpha(Success(), 200); ImU32 tlsFg = WithAlpha(Success(), 200);
ImVec2 tlsMin(badgeX, cy2); ImVec2 tlsMin(badgeX, cy2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2); 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->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS"); dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4 * dp, cy2 + 1 * dp), tlsFg, "TLS");
} else { } else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2), dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
WithAlpha(Error(), 140), TR("peers_no_tls")); WithAlpha(Error(), 140), TR("peers_no_tls"));
+44 -22
View File
@@ -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 // Build preview string
if (!app->isConnected()) { if (!app->isConnected()) {
s_source_preview = TR(app->isLiteBuild() ? "lite_no_wallet_short" : "not_connected"); 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]; const auto& addr = state.addresses[s_selected_address_idx];
bool isZ = addr.type == "shielded"; bool isZ = addr.type == "shielded";
const char* tag = isZ ? "[Z]" : "[T]"; const char* tag = isZ ? "[Z]" : "[T]";
std::string trunc = util::truncateMiddle(addr.address, // Reserve pixel room for the tag prefix and the trailing balance, then middle-truncate the
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))); // address to whatever remains — measured with the combo's own Body2 font. Char-count
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s", // truncation kept MORE chars as the column widened, so at 150% the scaled font overflowed
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER); // 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; s_source_preview = buf;
} else { } else {
s_source_preview = TR("select_receiving_address"); 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::SetNextItemWidth(dropdownW);
ImGui::PushFont(Type().getFont(TypeStyle::Body2)); ImGui::PushFont(Type().getFont(TypeStyle::Body2));
if (ImGui::BeginCombo("##RecvAddr", s_source_preview.c_str())) { 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", snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
tag, lblIt->second.c_str(), trunc.c_str(), tag, lblIt->second.c_str(), trunc.c_str(),
addr.balance, DRAGONX_TICKER, addr.balance, DRAGONX_TICKER,
isNew ? " [NEW]" : ""); isNew ? TR("grpb_new_badge_suffix") : "");
} else { } else {
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s", snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s",
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER, tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
isNew ? " [NEW]" : ""); isNew ? TR("grpb_new_badge_suffix") : "");
} }
ImGui::PushID(static_cast<int>(i)); 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 s_cached_qr_data.clear(); // Force QR regeneration
} }
if (ImGui::IsItemHovered()) { 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, addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? "\n(selected)" : ""); isCurrent ? TR("grpb_selected_suffix") : "");
} }
ImGui::PopID(); ImGui::PopID();
} }
@@ -584,12 +598,14 @@ void RenderReceiveTab(App* app)
// ================================================================ // ================================================================
{ {
// Cap + center the card so the address/amount column stops stretching while the // Cap + center the card so the address/amount column stops stretching while the
// QR plateaus. cardW never exceeds formW (only shrinks); leftover becomes margin. // 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). // The RECENT RECEIVED list below stays on the uncapped formW (handled separately).
float cardDp = Layout::dpiScale(); float cardDp = Layout::dpiScale();
float cardW = std::min(formW, 860.0f * cardDp); Layout::CardBox cardBox = Layout::mainComposeCardBox(formW);
float cardW = cardBox.width;
float cardLeftX = ImGui::GetCursorScreenPos().x; float cardLeftX = ImGui::GetCursorScreenPos().x;
float cardOffsetX = std::max(0.0f, (formW - cardW) * 0.5f); float cardOffsetX = cardBox.offsetX;
if (cardOffsetX > 0.0f) if (cardOffsetX > 0.0f)
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + cardOffsetX); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + cardOffsetX);
@@ -700,17 +716,17 @@ void RenderReceiveTab(App* app)
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text); ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
float ss = iconW * 0.5f; float ss = iconW * 0.5f;
float cx = startX + ss; 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 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; 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( dl->AddTriangleFilled(
ImVec2(cx + al, ay1), ImVec2(cx + al, ay1),
ImVec2(cx + al - hss, ay1 - hss), ImVec2(cx + al - hss, ay1 - hss),
ImVec2(cx + al - hss, ay1 + hss), iconCol); ImVec2(cx + al - hss, ay1 + hss), iconCol);
float ay2 = cy + ag; 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( dl->AddTriangleFilled(
ImVec2(cx - al, ay2), ImVec2(cx - al, ay2),
ImVec2(cx - al + hss, ay2 - hss), ImVec2(cx - al + hss, ay2 - hss),
@@ -728,8 +744,8 @@ void RenderReceiveTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size; float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size; float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size; float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
struct Preset { const char* label; double amount; }; struct Preset { const char* label; double amount; };
Preset presets[] = { Preset presets[] = {
@@ -857,7 +873,7 @@ void RenderReceiveTab(App* app)
RenderQRCode(s_qr_texture, qrSize); RenderQRCode(s_qr_texture, qrSize);
} else { } else {
ImGui::Dummy(ImVec2(qrSize, qrSize)); 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); qrPanelMin.y + totalQrSize * 0.5f);
dl->AddText(capFont, capFont->LegacySize, textPos, dl->AddText(capFont, capFont->LegacySize, textPos,
OnSurfaceDisabled(), TR("qr_unavailable")); OnSurfaceDisabled(), TR("qr_unavailable"));
@@ -911,7 +927,13 @@ void RenderReceiveTab(App* app)
{ {
float btnGap = Layout::spacingMd(); 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 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; bool firstBtn = true;
+25 -15
View File
@@ -364,7 +364,8 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
if (suggestions.empty()) return; if (suggestions.empty()) return;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color))); 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); ImGui::BeginChild(childId, ImVec2(width, sugH), true);
for (size_t si = 0; si < suggestions.size(); si++) { for (size_t si = 0; si < suggestions.size(); si++) {
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size; int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
@@ -387,13 +388,14 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
// ============================================================================ // ============================================================================
static void RenderFeeTierSelector(const char* suffix = "") { static void RenderFeeTierSelector(const char* suffix = "") {
auto& S = schema::UI(); 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 char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH }; const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
for (int fi = 0; fi < 3; fi++) { 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); bool active = (s_fee_tier == fi);
if (active) { if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size))); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size)));
@@ -515,7 +517,7 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
// Max button — use caption font to fit bar height // Max button — use caption font to fit bar height
ImGui::SameLine(0, gap); ImGui::SameLine(0, gap);
char maxId[32]; 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)) { if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) {
s_amount = maxAmount; s_amount = maxAmount;
s_send_max = true; s_send_max = true;
@@ -976,6 +978,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
bool is_valid_address, double available, bool is_valid_address, double available,
const char* suffix = "") { const char* suffix = "") {
auto& S = schema::UI(); auto& S = schema::UI();
const float dp = Layout::dpiScale();
const auto& state = app->getWalletState(); const auto& state = app->getWalletState();
double total = s_amount + s_fee; double total = s_amount + s_fee;
// Block spending from a view-only source (imported viewing key, no spending key) — it would only // Block spending from a view-only source (imported viewing key, no spending key) — it would only
@@ -1065,12 +1068,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
if (ImGui::BeginPopup(confirmClearId)) { if (ImGui::BeginPopup(confirmClearId)) {
ImGui::Text("%s", TR("send_clear_fields")); ImGui::Text("%s", TR("send_clear_fields"));
ImGui::Spacing(); 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(); ClearFormWithUndo();
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
} }
ImGui::SameLine(); 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::CloseCurrentPopup();
} }
ImGui::EndPopup(); ImGui::EndPopup();
@@ -1086,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_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))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
char undoId[32]; 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"))) { if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
RestoreFormSnapshot(); RestoreFormSnapshot();
Notifications::instance().info(TR("send_form_restored")); Notifications::instance().info(TR("send_form_restored"));
@@ -1272,12 +1275,13 @@ void RenderSendTab(App* app)
float contentStartY = ImGui::GetCursorPosY(); float contentStartY = ImGui::GetCursorPosY();
float formAvailW = ImGui::GetContentRegionAvail().x; float formAvailW = ImGui::GetContentRegionAvail().x;
// The compose form reads best as a centered fixed-width column, not edge-to-edge. // Fill the available column up to the content-max-width cap, then center. Shared with the
// Cap the card to a readable form width and center it; the recent-sends list below // Receive tab via mainComposeCardBox() so the two card envelopes are identical in width and
// deliberately keeps the full column width (formAvailW). // position (Send previously capped at 760dp vs Receive's 860dp, so Send rendered narrower).
const float sendDp = Layout::dpiScale(); // The recent-sends list below deliberately keeps the full column width (formAvailW).
float formCardW = std::min(formAvailW, 760.0f * sendDp); Layout::CardBox formBox = Layout::mainComposeCardBox(formAvailW);
float formOffsetX = std::max(0.0f, (formAvailW - formCardW) * 0.5f); float formCardW = formBox.width;
float formOffsetX = formBox.offsetX;
float formW = formAvailW; float formW = formAvailW;
ImGui::BeginGroup(); ImGui::BeginGroup();
@@ -1367,7 +1371,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 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 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 // Show clipboard preview as transparent overlay when paste button is hovered
bool paste_hovered = false; bool paste_hovered = false;
@@ -1425,9 +1432,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(); 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())) material::Type().iconMed()))
ImGui::OpenPopup("##ContactPickerPopup"); ImGui::OpenPopup("##ContactPickerPopup");
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("send_contacts_button")); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("send_contacts_button"));
+12 -12
View File
@@ -216,7 +216,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (!skin.valid) { if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true); 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::Selectable(label.c_str(), false);
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -261,7 +261,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::PushFont(material::Type().iconSmall()); ImGui::PushFont(material::Type().iconSmall());
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) { if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
skinMgr.refresh(); skinMgr.refresh();
Notifications::instance().info("Theme list refreshed"); Notifications::instance().info(TR("swin_theme_list_refreshed"));
} }
ImGui::PopFont(); ImGui::PopFont();
if (ImGui::IsItemHovered()) { 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) { app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) { if (error.empty()) {
std::string version = result.value("version", "unknown"); 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); Notifications::instance().success(msg);
} else { } else {
Notifications::instance().error("Connection failed: " + error); Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
} }
}); });
} else { } 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()) { if (error.empty()) {
int start = result.value("start_height", 0); int start = result.value("start_height", 0);
int end = result.value("stop_height", 0); int end = result.value("stop_height", 0);
std::string msg = "Rescan started from block " + std::to_string(start) + std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
" to " + std::to_string(end); TR("swin_rescan_to") + std::to_string(end);
Notifications::instance().success(msg); Notifications::instance().success(msg);
} else { } else {
Notifications::instance().error("Rescan failed: " + error); Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
} }
}); });
} else { } else {
Notifications::instance().error("RPC client not initialized"); Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
} }
} }
ImGui::TextDisabled(" %s", TR("settings_rescan_desc")); ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
@@ -503,9 +503,9 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (doConfirm) { if (doConfirm) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json"; std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) { if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared"); Notifications::instance().success(TR("swin_ztx_history_cleared"));
} else { } else {
Notifications::instance().info("No history file found"); Notifications::instance().info(TR("swin_no_history_file_found"));
} }
s_confirm_clear_ztx = false; s_confirm_clear_ztx = false;
} }
@@ -568,7 +568,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
// Save/Cancel buttons // Save/Cancel buttons
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) { if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
saveSettingsFromUI(app->settings()); saveSettingsFromUI(app->settings());
Notifications::instance().success("Settings saved"); Notifications::instance().success(TR("swin_settings_saved"));
*p_open = false; *p_open = false;
} }
ImGui::SameLine(); ImGui::SameLine();
+414 -264
View File
@@ -5,6 +5,7 @@
#include "shield_dialog.h" #include "shield_dialog.h"
#include "../../app.h" #include "../../app.h"
#include "../../config/version.h" #include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../rpc/rpc_client.h" #include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h" #include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h" #include "../../util/i18n.h"
@@ -15,39 +16,98 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <cstdio>
namespace dragonx { namespace dragonx {
namespace ui { namespace ui {
// Static state // ── Static dialog state ─────────────────────────────────────────────────────────────────────────
static bool s_open = false; static bool s_open = false;
static ShieldDialog::Mode s_mode = ShieldDialog::Mode::ShieldCoinbase; static ShieldDialog::Mode s_mode = ShieldDialog::Mode::ShieldCoinbase;
static char s_from_address[512] = "*"; static bool s_consolidate = false; // opened from the wallet-bloat nudge (shielded preset + framing)
static char s_to_address[512] = ""; 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 double s_fee = DRAGONX_DEFAULT_FEE;
static int s_utxo_limit = 50; // overridden by schema at runtime static int s_utxo_limit = 50; // overridden by schema at runtime
static bool s_operation_pending = false; 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_operation_id;
static std::string s_status_message; 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) void ShieldDialog::show(Mode mode)
{ {
s_mode = mode; s_mode = mode;
s_open = true; s_open = true;
s_operation_pending = false; s_consolidate = false; // reset preset flags so stale statics don't leak across opens
s_status_message.clear(); s_src = 2;
s_operation_id.clear(); resetTransient();
s_from_address[0] = '\0';
if (mode == Mode::ShieldCoinbase) { if (mode == Mode::ShieldCoinbase) strncpy(s_from_address, "*", sizeof(s_from_address));
strncpy(s_from_address, "*", sizeof(s_from_address));
} else {
s_from_address[0] = '\0';
}
s_to_address[0] = '\0'; s_to_address[0] = '\0';
s_selected_zaddr_idx = -1;
s_fee = DRAGONX_DEFAULT_FEE; s_fee = DRAGONX_DEFAULT_FEE;
s_utxo_limit = (int)schema::UI().drawElement("business", "utxo-limit").size; 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) void ShieldDialog::showShieldCoinbase(const std::string& fromAddress)
@@ -59,14 +119,146 @@ void ShieldDialog::showShieldCoinbase(const std::string& fromAddress)
void ShieldDialog::showMerge() void ShieldDialog::showMerge()
{ {
show(Mode::MergeToAddress); 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() void ShieldDialog::hide()
{ {
s_open = false; s_open = false;
s_operation_pending = false; resetTransient();
s_status_message.clear(); }
s_operation_id.clear();
// 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) void ShieldDialog::render(App* app)
@@ -74,263 +266,221 @@ void ShieldDialog::render(App* app)
if (!s_open) return; if (!s_open) return;
auto& S = schema::UI(); auto& S = schema::UI();
auto win = S.window("dialogs.shield"); auto win = S.window("dialogs.shield");
auto addrLbl = S.label("dialogs.shield", "address-label"); auto addrLbl = S.label("dialogs.shield", "address-label");
auto addrFrontLbl = S.label("dialogs.shield", "address-front-label"); auto addrFront = S.label("dialogs.shield", "address-front-label");
auto addrBackLbl = S.label("dialogs.shield", "address-back-label"); auto addrBack = S.label("dialogs.shield", "address-back-label");
auto feeInput = S.input("dialogs.shield", "fee-input"); auto feeInput = S.input("dialogs.shield", "fee-input");
auto utxoInput = S.input("dialogs.shield", "utxo-limit-input"); auto utxoInput = S.input("dialogs.shield", "utxo-limit-input");
auto shieldBtn = S.button("dialogs.shield", "shield-button"); auto shieldBtn = S.button("dialogs.shield", "shield-button");
auto cancelBtn = S.button("dialogs.shield", "cancel-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) const char* title = s_consolidate ? TR("consolidate_title")
? TR("shield_title") : isMerge ? TR("merge_title")
: TR("merge_title"); : TR("shield_title");
material::OverlayDialogSpec ov; material::OverlayDialogSpec ov;
ov.title = title; ov.p_open = &s_open; ov.title = title; ov.p_open = &s_open;
ov.style = material::OverlayStyle::BlurFloat; ov.style = material::OverlayStyle::BlurFloat;
ov.cardWidth = win.width; ov.idSuffix = "shielddialog"; ov.cardWidth = win.width; ov.idSuffix = "shielddialog";
if (material::BeginOverlayDialog(ov)) { if (!material::BeginOverlayDialog(ov)) return;
const auto& state = app->getWalletState();
// Description const auto& state = app->getWalletState();
if (s_mode == Mode::ShieldCoinbase) { autoSelectDestination(state);
ImGui::TextWrapped("%s", TR("shield_description")); pollOperation(app);
} else { if (isMerge && !s_scope_loaded && !s_scope_loading && s_operation_id.empty()) loadScope(app);
ImGui::TextWrapped("%s", TR("merge_description"));
// ── 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(); ImGui::Spacing();
// From address (for shield coinbase) // Source selector — only offer the types that actually have inputs.
if (s_mode == Mode::ShieldCoinbase) { const bool tOk = s_t_count > 0, zOk = s_z_count > 0;
material::LabeledInput(TR("shield_from_address"), "##FromAddr", s_from_address, sizeof(s_from_address)); if (tOk && zOk) {
ImGui::TextDisabled("%s", TR("shield_wildcard_hint")); 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(); 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 + UTXO limit share one row (two columns) to tighten vertical rhythm.
float pairColX = ImGui::GetContentRegionAvail().x * 0.5f;
// Fee (left column)
ImGui::Text("%s", TR("fee_label"));
ImGui::SetNextItemWidth(feeInput.width * Layout::dpiScale());
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");
// UTXO limit (right column) — hint drops under the input (rather than beside it) since
// "Max UTXOs per operation" is too long to share the narrower half-width column with "DRGX".
ImGui::SameLine(pairColX);
ImGui::BeginGroup();
ImGui::Text("%s", TR("shield_utxo_limit"));
ImGui::SetNextItemWidth(utxoInput.width * Layout::dpiScale());
ImGui::InputInt("##Limit", &s_utxo_limit);
if (s_utxo_limit < 1) s_utxo_limit = 1;
if (s_utxo_limit > 100) s_utxo_limit = 100;
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(),
TR("shield_max_utxos"));
ImGui::EndGroup();
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;
// Center the primary + Cancel action row via the shared footer helper. We can't use
// DialogActionFooter here because the primary button carries a disabled-hover tooltip that must
// fire on ITS item (the helper draws primary+Close internally, leaving no hook between them), so
// we keep the two TactileButtons + the interleaved tooltip and only standardize the placement.
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? TR("shield_funds") : TR("merge_funds");
float footerBtnW = shieldBtn.width + cancelBtn.width + ImGui::GetStyle().ItemSpacing.x;
material::BeginOverlayDialogFooter(footerBtnW, /*drawSeparator=*/false);
if (!can_submit) ImGui::BeginDisabled();
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 } // namespace ui
+7 -1
View File
@@ -33,10 +33,16 @@ public:
static void showShieldCoinbase(const std::string& fromAddress = "*"); 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(); 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) * @brief Render the dialog (call each frame)
*/ */
@@ -124,7 +124,7 @@ void TransactionDetailsDialog::render(App* app)
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::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::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine(); 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()); ImGui::SetClipboardText(tx.txid.c_str());
} }
@@ -150,7 +150,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly); ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
} }
ImGui::SameLine(); 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()); ImGui::SetClipboardText(tx.address.c_str());
} }
} }
+2 -2
View File
@@ -263,10 +263,10 @@ void RenderTransactionsTab(App* app)
int idx_map[] = {-1, 1, 0, 2}; int idx_map[] = {-1, 1, 0, 2};
int idx = idx_map[type_filter]; int idx = idx_map[type_filter];
float xOff = idx * (cardW + cardGap); float xOff = idx * (cardW + cardGap);
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3); ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3 * vs);
ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH); ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH);
ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol; ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol;
dl->AddRectFilled(acMin, acMax, acCol, 2.0f); dl->AddRectFilled(acMin, acMax, acCol, 2.0f * hs);
} }
ImGui::Dummy(ImVec2(availWidth, cardH)); ImGui::Dummy(ImVec2(availWidth, cardH));
+1 -1
View File
@@ -100,7 +100,7 @@ public:
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
+ Type().caption()->LegacySize + Layout::spacingSm() // intro + gap + Type().caption()->LegacySize + Layout::spacingSm() // intro + gap
+ ctrlRow + Layout::spacingSm(); // sort control row + gap + ctrlRow + Layout::spacingSm(); // sort control row + gap
const float padV = 48.0f; // content-child padding (top+bottom) + margin const float padV = 48.0f * dp; // content-child padding (top+bottom) + margin
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI // Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
// screen the content-sized card could exceed the window (the framework would clip a too-tall // screen the content-sized card could exceed the window (the framework would clip a too-tall
// card, footer and all). When capped, the wallet list flexes + scrolls and the controls // card, footer and all). When capped, the wallet list flexes + scrolls and the controls
+453 -22
View File
@@ -460,15 +460,23 @@ void I18n::loadBuiltinEnglish()
// Settings sections // Settings sections
strings_["appearance"] = "APPEARANCE"; strings_["appearance"] = "APPEARANCE";
strings_["theme_language"] = "THEME & LANGUAGE"; strings_["theme_language"] = "THEME & LANGUAGE";
strings_["scale_effects"] = "SCALE & EFFECTS";
strings_["advanced_effects"] = "Advanced Effects..."; strings_["advanced_effects"] = "Advanced Effects...";
strings_["tools_actions"] = "Tools & Actions..."; strings_["tools_actions"] = "Tools & Actions...";
strings_["tools_actions_hdr"] = "TOOLS & ACTIONS";
strings_["wallet_options_hdr"] = "OPTIONS";
strings_["wallet_diagnostics_hdr"] = "DIAGNOSTICS";
strings_["wallet"] = "WALLET"; strings_["wallet"] = "WALLET";
strings_["node_security"] = "NODE & SECURITY"; strings_["node_security"] = "NODE & SECURITY";
strings_["node"] = "NODE"; strings_["node"] = "NODE";
strings_["security"] = "SECURITY"; strings_["security"] = "SECURITY";
strings_["explorer_section"] = "EXPLORER"; strings_["explorer_section"] = "EXPLORER";
strings_["explorer_urls_hdr"] = "URLS";
strings_["about"] = "About"; strings_["about"] = "About";
strings_["backup_data"] = "BACKUP & DATA"; strings_["backup_data"] = "BACKUP & DATA";
strings_["backup_col_import"] = "IMPORT & RESTORE";
strings_["backup_col_backup"] = "BACKUP";
strings_["backup_col_export"] = "EXPORT";
strings_["balance_layout"] = "Balance Layout"; strings_["balance_layout"] = "Balance Layout";
strings_["low_spec_mode"] = "Low-spec mode"; strings_["low_spec_mode"] = "Low-spec mode";
strings_["simple_background"] = "Simple background"; strings_["simple_background"] = "Simple background";
@@ -496,9 +504,11 @@ void I18n::loadBuiltinEnglish()
strings_["screenshot_sweep"] = "Run screenshot sweep"; strings_["screenshot_sweep"] = "Run screenshot sweep";
strings_["screenshot_sweep_full"] = "Full UI sweep"; strings_["screenshot_sweep_full"] = "Full UI sweep";
strings_["screenshot_open_dir"] = "Open location"; strings_["screenshot_open_dir"] = "Open location";
strings_["sweep_current_theme_only"] = "Current theme only";
strings_["tt_sweep_current_theme_only"] = "Sweep only the active theme instead of cycling every theme";
strings_["screenshot_sweep_desc"] = "Cycles every theme across every tab and saves a screenshot of each into per-tab subfolders under the config directory's screenshots folder (overwriting the previous sweep). Runs for a few seconds."; strings_["screenshot_sweep_desc"] = "Cycles every theme across every tab and saves a screenshot of each into per-tab subfolders under the config directory's screenshots folder (overwriting the previous sweep). Runs for a few seconds.";
strings_["mine_when_idle"] = "Mine when idle"; strings_["mine_when_idle"] = "Mine when idle";
strings_["setup_wizard"] = "Run Setup Wizard..."; strings_["setup_wizard"] = "Run Setup Wizard…";
// RPC / Explorer settings // RPC / Explorer settings
strings_["rpc_connection"] = "RPC Connection..."; strings_["rpc_connection"] = "RPC Connection...";
@@ -512,21 +522,21 @@ void I18n::loadBuiltinEnglish()
strings_["fetch_prices"] = "Fetch price data from CoinGecko"; strings_["fetch_prices"] = "Fetch price data from CoinGecko";
strings_["block_explorer"] = "Block Explorer"; strings_["block_explorer"] = "Block Explorer";
strings_["test_connection"] = "Test Connection"; strings_["test_connection"] = "Test Connection";
strings_["rescan"] = "Rescan Blockchain"; strings_["rescan"] = "Rescan";
// Settings: buttons // Settings: buttons
strings_["settings_address_book"] = "Address Book..."; strings_["settings_address_book"] = "Address Book…";
strings_["settings_validate_address"] = "Validate Address..."; strings_["settings_validate_address"] = "Validate Address…";
strings_["settings_request_payment"] = "Request Payment..."; strings_["settings_request_payment"] = "Request Payment…";
strings_["settings_shield_mining"] = "Shield Mining..."; strings_["settings_shield_mining"] = "Shield Mining…";
strings_["settings_merge_to_address"] = "Merge to Address..."; strings_["settings_merge_to_address"] = "Merge to Address…";
strings_["settings_clear_ztx"] = "Clear Z-Tx History"; strings_["settings_clear_ztx"] = "Clear Z-Tx History";
strings_["settings_import_key"] = "Import Private Key..."; strings_["settings_import_key"] = "Import Private Key…";
strings_["settings_import_viewkey"] = "Import Viewing Key..."; strings_["settings_import_viewkey"] = "Import Viewing Key…";
strings_["settings_export_key"] = "Export Key..."; strings_["settings_export_key"] = "Export Key…";
strings_["settings_export_all"] = "Export All..."; strings_["settings_export_all"] = "Export All…";
strings_["settings_backup"] = "Backup..."; strings_["settings_backup"] = "Backup…";
strings_["settings_export_csv"] = "Export CSV..."; strings_["settings_export_csv"] = "Export CSV…";
strings_["settings_encrypt_wallet"] = "Encrypt Wallet"; strings_["settings_encrypt_wallet"] = "Encrypt Wallet";
strings_["settings_change_passphrase"] = "Change Passphrase"; strings_["settings_change_passphrase"] = "Change Passphrase";
strings_["settings_lock_now"] = "Lock Now"; strings_["settings_lock_now"] = "Lock Now";
@@ -655,8 +665,11 @@ void I18n::loadBuiltinEnglish()
strings_["wiz_pin_confirm"] = "Confirm PIN:"; strings_["wiz_pin_confirm"] = "Confirm PIN:";
strings_["wiz_pin_invalid"] = "PIN must be 4-8 digits"; strings_["wiz_pin_invalid"] = "PIN must be 4-8 digits";
strings_["wiz_pin_mismatch"] = "PINs do not match"; strings_["wiz_pin_mismatch"] = "PINs do not match";
strings_["settings_data_dir"] = "Data Dir:"; strings_["settings_data_dir"] = "Data Dir";
strings_["settings_wallet_size_label"] = "Wallet Size:"; strings_["settings_wallet_size_label"] = "Wallet Size";
strings_["wallet_size_warn"] = "This wallet file is large. Consolidating your notes can curb further growth.";
strings_["tt_wallet_size_warn"] = "Shielded wallets grow with each note's witness data — merging many notes into one address reduces it. Back up first.";
strings_["wallet_size_consolidate"] = "Consolidate notes\xE2\x80\xA6";
strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply"; strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply";
strings_["settings_auto_detected"] = "Auto-detected from DRAGONX.conf"; strings_["settings_auto_detected"] = "Auto-detected from DRAGONX.conf";
strings_["settings_visual_effects"] = "Visual Effects"; strings_["settings_visual_effects"] = "Visual Effects";
@@ -670,7 +683,7 @@ void I18n::loadBuiltinEnglish()
strings_["settings_wallet_info"] = "Wallet Info"; strings_["settings_wallet_info"] = "Wallet Info";
strings_["settings_block_explorer_urls"] = "Block Explorer URLs"; strings_["settings_block_explorer_urls"] = "Block Explorer URLs";
strings_["settings_configure_explorer"] = "Configure external block explorer links"; strings_["settings_configure_explorer"] = "Configure external block explorer links";
strings_["settings_auto_lock"] = "AUTO-LOCK"; strings_["settings_auto_lock"] = "Auto-lock";
strings_["timeout_off"] = "Off"; strings_["timeout_off"] = "Off";
strings_["timeout_1min"] = "1 min"; strings_["timeout_1min"] = "1 min";
strings_["timeout_5min"] = "5 min"; strings_["timeout_5min"] = "5 min";
@@ -700,9 +713,9 @@ void I18n::loadBuiltinEnglish()
strings_["tt_scanline"] = "CRT scanline effect in console"; strings_["tt_scanline"] = "CRT scanline effect in console";
strings_["tt_theme_effects"] = "Shimmer, glow, hue-cycling per theme"; strings_["tt_theme_effects"] = "Shimmer, glow, hue-cycling per theme";
strings_["tt_animate_avatars"] = "Play animated (GIF / WebP) contact avatars; off shows the first frame only"; strings_["tt_animate_avatars"] = "Play animated (GIF / WebP) contact avatars; off shows the first frame only";
strings_["tt_blur"] = "Blur amount (0%% = off, 100%% = maximum)"; strings_["tt_blur"] = "Blur amount (0% = off, 100% = maximum)";
strings_["tt_noise"] = "Grain texture intensity (0%% = off, 100%% = maximum)"; strings_["tt_noise"] = "Grain texture intensity (0% = off, 100% = maximum)";
strings_["tt_ui_opacity"] = "Card and sidebar opacity (100%% = fully opaque, lower = more see-through)"; strings_["tt_ui_opacity"] = "Card and sidebar opacity (100% = fully opaque, lower = more see-through)";
strings_["tt_window_opacity"] = "Background opacity (lower = desktop visible through window)"; strings_["tt_window_opacity"] = "Background opacity (lower = desktop visible through window)";
strings_["tt_font_scale"] = "Scale all text and UI (1.0x = default, up to 1.5x). Hotkey: Alt + Scroll Wheel"; strings_["tt_font_scale"] = "Scale all text and UI (1.0x = default, up to 1.5x). Hotkey: Alt + Scroll Wheel";
strings_["tt_custom_theme"] = "Custom theme active"; strings_["tt_custom_theme"] = "Custom theme active";
@@ -717,6 +730,12 @@ void I18n::loadBuiltinEnglish()
strings_["tt_tor"] = "Route daemon connections through the Tor network for anonymity"; strings_["tt_tor"] = "Route daemon connections through the Tor network for anonymity";
strings_["tt_keep_daemon"] = "Daemon will still stop when running the setup wizard"; strings_["tt_keep_daemon"] = "Daemon will still stop when running the setup wizard";
strings_["tt_stop_external"] = "Applies when connecting to a daemon\nyou started outside this wallet"; strings_["tt_stop_external"] = "Applies when connecting to a daemon\nyou started outside this wallet";
strings_["stratum_host_section"] = "MINING POOL HOSTING";
strings_["stratum_host"] = "Host a mining pool (stratum)";
strings_["tt_stratum_host"] = "Run a RandomX stratum pool server on this node so other RandomX miners can point at this computer. Requires a v1.3.0+ node and a daemon restart to apply.";
strings_["stratum_host_hint"] = "Miners connect to this computer on port 22769 (RPC port + 1000) with a RandomX stratum miner. Restart the daemon to apply.";
strings_["stratum_allowip_hint"] = "Allow miners from IP or CIDR (blank = this computer only)";
strings_["stratum_expose_warn"] = "Opens a mining port to the network you allow — only use on a trusted LAN.";
strings_["tt_verbose"] = "Log detailed connection diagnostics,\ndaemon state, and port owner info\nto the Console tab"; strings_["tt_verbose"] = "Log detailed connection diagnostics,\ndaemon state, and port owner info\nto the Console tab";
strings_["tt_mine_idle"] = "Automatically start mining when the\nsystem is idle (no keyboard/mouse input)"; strings_["tt_mine_idle"] = "Automatically start mining when the\nsystem is idle (no keyboard/mouse input)";
strings_["tt_idle_delay"] = "How long to wait before starting mining"; strings_["tt_idle_delay"] = "How long to wait before starting mining";
@@ -805,12 +824,14 @@ void I18n::loadBuiltinEnglish()
strings_["rescan_detecting"] = "Checking which blocks your node has on disk…"; strings_["rescan_detecting"] = "Checking which blocks your node has on disk…";
strings_["rescan_bootstrapped_msg"] = "Your node was bootstrapped, so blocks below the snapshot aren't on disk and a rescan from genesis would fail. Rescan from a height your snapshot includes to reconcile your wallet's spent balance. Your wallet.dat and chain data are not deleted."; strings_["rescan_bootstrapped_msg"] = "Your node was bootstrapped, so blocks below the snapshot aren't on disk and a rescan from genesis would fail. Rescan from a height your snapshot includes to reconcile your wallet's spent balance. Your wallet.dat and chain data are not deleted.";
strings_["rescan_from_height"] = "Rescan from block height:"; strings_["rescan_from_height"] = "Rescan from block height:";
strings_["repair_wallet"] = "Repair Wallet"; strings_["repair_wallet"] = "Repair";
strings_["tt_repair_wallet"] = "Wipe and rebuild the wallet's transaction records from the blockchain (fixes notes that fail to send after a rescan)"; strings_["tt_repair_wallet"] = "Wipe and rebuild the wallet's transaction records from the blockchain (fixes notes that fail to send after a rescan)";
strings_["confirm_repair_wallet_title"] = "Repair Wallet"; strings_["confirm_repair_wallet_title"] = "Repair Wallet";
strings_["confirm_repair_wallet_msg"] = "This restarts the daemon with -zapwallettxes=2: it deletes all of the wallet's transaction and note records, then rebuilds them from the blockchain. Use this when transactions fail to build (\"Invalid sapling spend proof\" / \"shielded requirements not met\") even after a full rescan. It takes a long time and the wallet stays offline until it finishes."; strings_["confirm_repair_wallet_msg"] = "This restarts the daemon with -zapwallettxes=2: it deletes all of the wallet's transaction and note records, then rebuilds them from the blockchain. Use this when transactions fail to build (\"Invalid sapling spend proof\" / \"shielded requirements not met\") even after a full rescan. It takes a long time and the wallet stays offline until it finishes.";
strings_["confirm_repair_wallet_safe"] = "Your keys, addresses and balance are preserved — only the cached transaction records are rebuilt."; strings_["confirm_repair_wallet_safe"] = "Your keys, addresses and balance are preserved — only the cached transaction records are rebuilt.";
strings_["daemon_binary"] = "Daemon binary"; strings_["daemon_binary"] = "Daemon binary";
strings_["daemon_updates_label"] = "UPDATES";
strings_["daemon_maintenance_label"] = "MAINTENANCE";
strings_["daemon_installed"] = "Installed"; strings_["daemon_installed"] = "Installed";
strings_["daemon_bundled"] = "Bundled"; strings_["daemon_bundled"] = "Bundled";
strings_["daemon_not_installed"] = "not installed"; strings_["daemon_not_installed"] = "not installed";
@@ -818,6 +839,10 @@ void I18n::loadBuiltinEnglish()
strings_["daemon_status_match"] = "Installed binary matches the bundled version."; strings_["daemon_status_match"] = "Installed binary matches the bundled version.";
strings_["daemon_status_differ"] = "Installed binary differs from the bundled version."; strings_["daemon_status_differ"] = "Installed binary differs from the bundled version.";
strings_["daemon_status_missing"] = "No daemon installed — install the bundled version."; strings_["daemon_status_missing"] = "No daemon installed — install the bundled version.";
// Compact status shown right-aligned on the DAEMON BINARY heading row.
strings_["daemon_status_ok"] = "Up to date";
strings_["daemon_status_diff"] = "Version differs";
strings_["daemon_status_none"] = "Not installed";
strings_["daemon_install_bundled"] = "Install bundled"; strings_["daemon_install_bundled"] = "Install bundled";
strings_["tt_daemon_install_bundled"] = "Stop the node, overwrite the installed dragonxd with the version bundled in this wallet build, then restart"; strings_["tt_daemon_install_bundled"] = "Stop the node, overwrite the installed dragonxd with the version bundled in this wallet build, then restart";
strings_["confirm_reinstall_daemon_title"] = "Install Bundled Daemon"; strings_["confirm_reinstall_daemon_title"] = "Install Bundled Daemon";
@@ -1231,6 +1256,8 @@ void I18n::loadBuiltinEnglish()
strings_["wallet_recovered_restore"] = "Restore the original file instead"; strings_["wallet_recovered_restore"] = "Restore the original file instead";
strings_["wallet_recovered_restore_sub"] = "Puts your largest untouched backup back in place, verbatim, then re-scans — slightly faster, but only as complete as that one file was. Your current file is kept as a dated backup either way."; strings_["wallet_recovered_restore_sub"] = "Puts your largest untouched backup back in place, verbatim, then re-scans — slightly faster, but only as complete as that one file was. Your current file is kept as a dated backup either way.";
strings_["wallet_recovered_notify"] = "Your wallet file needed a repair — your original was safely backed up. Open the app to review your options."; strings_["wallet_recovered_notify"] = "Your wallet file needed a repair — your original was safely backed up. Open the app to review your options.";
strings_["wallet_degraded_notify"] = "Your wallet opened in reduced-function mode: existing funds are safe and spendable, but creating new addresses and shielding are disabled. Back up your seed phrase and restore it to fully repair the wallet.";
strings_["autoshield_by_node"] = "Auto-shield is handled by your node";
// In-dialog recovery lifecycle (Offer → Working → Done/Failed) + disclosures. // In-dialog recovery lifecycle (Offer → Working → Done/Failed) + disclosures.
strings_["wallet_recovery_working_label"] = "Working"; strings_["wallet_recovery_working_label"] = "Working";
strings_["wallet_recovery_done"] = "Done"; strings_["wallet_recovery_done"] = "Done";
@@ -1437,8 +1464,8 @@ void I18n::loadBuiltinEnglish()
strings_["sb_daemon_extract_failed"] = "Failed to write daemon files — check free disk space and permissions."; strings_["sb_daemon_extract_failed"] = "Failed to write daemon files — check free disk space and permissions.";
strings_["sb_daemon_files_failed"] = "Failed to write daemon files to %s — check free disk space and permissions."; strings_["sb_daemon_files_failed"] = "Failed to write daemon files to %s — check free disk space and permissions.";
strings_["loading_stall_title"] = "Taking longer than expected"; strings_["loading_stall_title"] = "Taking longer than expected";
strings_["loading_stall_body"] = "The daemon has been initializing for %.0fs. This can be normal after an update or on first launch (loading the block index or rescanning) — it will connect automatically once ready."; strings_["loading_stall_body"] = "Initializing for %.0fs — normal after an update or first launch. Connects automatically when ready.";
strings_["loading_stall_hint"] = "Still stuck? Open Settings and use Restart Daemon, or check the Console for details."; strings_["loading_stall_hint"] = "Stuck? Settings → Restart Daemon, or check the Console.";
strings_["sb_plaintext_remote_blocked"] = "Refusing to send RPC credentials over plaintext to a remote host. Add rpcallowplaintext=1 to DRAGONX.conf to allow it, or enable TLS with rpctls=1."; strings_["sb_plaintext_remote_blocked"] = "Refusing to send RPC credentials over plaintext to a remote host. Add rpcallowplaintext=1 to DRAGONX.conf to allow it, or enable TLS with rpctls=1.";
strings_["rpc_plaintext_remote_warning"] = "Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS."; strings_["rpc_plaintext_remote_warning"] = "Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS.";
strings_["settings_open_log_folder"] = "Open log folder"; strings_["settings_open_log_folder"] = "Open log folder";
@@ -1500,6 +1527,7 @@ void I18n::loadBuiltinEnglish()
strings_["about_chain"] = "Chain:"; strings_["about_chain"] = "Chain:";
strings_["about_connections"] = "Connections:"; strings_["about_connections"] = "Connections:";
strings_["about_credits"] = "Credits"; strings_["about_credits"] = "Credits";
strings_["about_source"] = "Source";
strings_["about_daemon"] = "Daemon:"; strings_["about_daemon"] = "Daemon:";
strings_["about_debug"] = "Debug"; strings_["about_debug"] = "Debug";
strings_["about_edition"] = "ImGui Edition"; strings_["about_edition"] = "ImGui Edition";
@@ -1513,6 +1541,175 @@ void I18n::loadBuiltinEnglish()
strings_["about_version"] = "Version:"; strings_["about_version"] = "Version:";
strings_["about_website"] = "Website"; strings_["about_website"] = "Website";
// --- Help & FAQ ---
strings_["faq"] = "FAQ";
strings_["faq_title"] = "Help & FAQ";
strings_["faq_intro"] = "Answers about your wallet and the DragonX node. Search, or browse by topic below.";
strings_["faq_search_hint"] = "Search the FAQ\xE2\x80\xA6";
strings_["faq_open_tooltip"] = "Help & FAQ";
strings_["faq_no_results"] = "No results. Try a different search term.";
strings_["faq_group_wallet"] = "Wallet";
strings_["faq_group_daemon"] = "Daemon";
// Wallet subcategory titles
strings_["faq_w_gs_title"] = "Getting Started";
strings_["faq_w_addr_title"] = "Addresses & Privacy";
strings_["faq_w_send_title"] = "Sending & Receiving";
strings_["faq_w_bal_title"] = "Balance & Sync";
strings_["faq_w_sec_title"] = "Security & Encryption";
strings_["faq_w_seed_title"] = "Seed Phrase & Backup";
strings_["faq_w_chat_title"] = "Chat & Contacts";
strings_["faq_w_set_title"] = "Settings & Appearance";
// Daemon subcategory titles
strings_["faq_d_node_title"] = "The Full Node";
strings_["faq_d_sync_title"] = "Sync & Blockchain";
strings_["faq_d_mgmt_title"] = "Node Management";
strings_["faq_d_upd_title"] = "Updating the Node";
strings_["faq_d_mine_title"] = "Mining & Pools";
strings_["faq_d_net_title"] = "Peers & Network";
strings_["faq_d_perf_title"] = "Storage & Performance";
strings_["faq_d_trbl_title"] = "Troubleshooting";
// Wallet > Getting Started
strings_["faq_w_gs_1_q"] = "What is ObsidianDragon?";
strings_["faq_w_gs_1_a"] = "ObsidianDragon is a full-node wallet for DragonX (DRGX). It manages your coins and, on the desktop build, runs a DragonX full node (the daemon) in the background so your wallet verifies the blockchain itself instead of trusting a third-party server.\n\nThink of it as two parts working together: the wallet (keys, balances, sending) and the node (the blockchain and network). The questions here are split the same way \xE2\x80\x94 see the Wallet and Daemon tabs above.";
strings_["faq_w_gs_2_q"] = "How do I create a new wallet?";
strings_["faq_w_gs_2_a"] = "On first launch the setup wizard creates a wallet for you automatically. New wallets are backed by a secret recovery phrase (a list of words). Write that phrase down and keep it offline \xE2\x80\x94 it is the only way to restore your funds if this computer is lost.\n\nYou can review or back up the phrase any time from Settings > Backup & Data > Seed phrase.";
strings_["faq_w_gs_3_q"] = "How do I restore a wallet I already have?";
strings_["faq_w_gs_3_a"] = "Use the seed-phrase restore flow if you have a recovery phrase, or copy an existing wallet.dat file into the wallet's data directory before launch.\n\nAfter restoring, the node re-scans the blockchain to find your past transactions, so your balance and history may take a while to appear the first time.";
strings_["faq_w_gs_4_q"] = "What does the first-run setup do?";
strings_["faq_w_gs_4_a"] = "The wizard lets you pick an appearance/theme, optionally download a bootstrap to speed up the initial blockchain sync, and set up encryption and a PIN. You can skip any step and change all of these later in Settings.";
// Wallet > Addresses & Privacy
strings_["faq_w_addr_1_q"] = "What's the difference between transparent and shielded addresses?";
strings_["faq_w_addr_1_a"] = "Transparent addresses (they start with R or t) work like most coins: the amounts and addresses are public on the blockchain.\n\nShielded addresses (z-addresses, starting with zs) use zero-knowledge cryptography \xE2\x80\x94 the amount, sender, and receiver are encrypted on-chain. Balances held in shielded addresses are private.";
strings_["faq_w_addr_2_q"] = "Which address type should I use?";
strings_["faq_w_addr_2_a"] = "Prefer shielded (z) addresses whenever possible \xE2\x80\x94 they keep your balance and payment history private. DragonX is a privacy coin and shielded is the default for received funds.\n\nUse a transparent address only when a service you interact with cannot handle shielded addresses.";
strings_["faq_w_addr_3_q"] = "Should I reuse an address or make a new one?";
strings_["faq_w_addr_3_a"] = "For shielded addresses, reuse is fine and does not leak history. For transparent addresses, using a fresh address per payment improves privacy. Create new addresses from the Receive tab.";
strings_["faq_w_addr_4_q"] = "What is 'shielding'?";
strings_["faq_w_addr_4_a"] = "Shielding moves coins from a transparent address into a shielded one, making them private. Newly mined coins arrive transparent and are shielded for you automatically.\n\nYou can also shield manually from the wallet; a shield is just a special transaction, so it takes a normal confirmation time to complete.";
// Wallet > Sending & Receiving
strings_["faq_w_send_1_q"] = "How do I send funds?";
strings_["faq_w_send_1_a"] = "Open the Send tab, paste the recipient's address, enter an amount, and confirm. If the wallet is encrypted you'll be asked to unlock it for the transaction.\n\nSends to shielded addresses are private; sends to transparent addresses are public.";
strings_["faq_w_send_2_q"] = "What fee do I pay?";
strings_["faq_w_send_2_a"] = "DragonX fees are very low and are set automatically. The fee is shown before you confirm. You do not normally need to change it.";
strings_["faq_w_send_3_q"] = "Can I attach a message to a payment?";
strings_["faq_w_send_3_a"] = "Yes \xE2\x80\x94 when sending to a shielded (z) address you can include an encrypted memo. Only the recipient can read it. Transparent addresses do not support memos.";
strings_["faq_w_send_4_q"] = "I sent a payment but it says pending \xE2\x80\x94 why?";
strings_["faq_w_send_4_a"] = "A transaction is 'pending' until it is mined into a block and gains confirmations. This usually takes a minute or two. Shielded transactions also need the node to be synced to build the proof.\n\nIf a send stays pending unusually long, check that the node is connected and synced (see the status bar at the bottom of the window).";
// Wallet > Balance & Sync
strings_["faq_w_bal_1_q"] = "Why is my balance 0 or not updating?";
strings_["faq_w_bal_1_a"] = "While the node is still syncing the blockchain, your balance is incomplete and may read 0 \xE2\x80\x94 the wallet hasn't scanned all of your transactions yet. It fills in once syncing finishes.\n\nWatch the status bar at the bottom: it shows the connection state and block height. Once the node is synced, your balance is accurate.";
strings_["faq_w_bal_2_q"] = "What are confirmations?";
strings_["faq_w_bal_2_a"] = "Each new block mined on top of the block containing your transaction adds one confirmation. More confirmations mean the payment is more firmly settled. Received funds become spendable after the first confirmation.";
strings_["faq_w_bal_3_q"] = "What's the difference between total and spendable balance?";
strings_["faq_w_bal_3_a"] = "Total includes funds that are still confirming or are temporarily locked (for example, coins in the middle of being shielded). Spendable is what you can send right now. They converge as transactions confirm.";
// Wallet > Security & Encryption
strings_["faq_w_sec_1_q"] = "How do I encrypt my wallet?";
strings_["faq_w_sec_1_a"] = "Open Settings > Node & Security and set a passphrase (the SECURITY section). Encryption protects your private keys on disk, so someone with access to the files still cannot spend your coins.\n\nChoose a strong passphrase and don't lose it \xE2\x80\x94 there is no way to recover an encrypted wallet without it.";
strings_["faq_w_sec_2_q"] = "What is the PIN / lock screen?";
strings_["faq_w_sec_2_a"] = "The PIN locks the app's screen so balances and actions are hidden when you step away. It's a convenience lock on top of encryption; you set it up during the wizard or in Settings > Node & Security (encrypt the wallet first).";
strings_["faq_w_sec_3_q"] = "I forgot my passphrase \xE2\x80\x94 can it be recovered?";
strings_["faq_w_sec_3_a"] = "No. Wallet encryption cannot be bypassed. If you still have your seed recovery phrase, you can restore the wallet from it into a fresh wallet and set a new passphrase. Without either the passphrase or the seed phrase, the funds cannot be recovered.";
// Wallet > Seed Phrase & Backup
strings_["faq_w_seed_1_q"] = "How do I back up my recovery phrase?";
strings_["faq_w_seed_1_a"] = "Go to Settings > Backup & Data > Seed phrase. Write the words down on paper in order and store them somewhere safe and offline. Anyone with the phrase can spend your coins, so never store it in a photo, email, or cloud note.";
strings_["faq_w_seed_2_q"] = "My wallet has no recovery phrase \xE2\x80\x94 can I add one?";
strings_["faq_w_seed_2_a"] = "Older (legacy) wallets weren't seed-based. The wallet can migrate a legacy wallet into a modern seed-backed one: it creates a new seed wallet and sweeps your funds into it. Look for 'Migrate to seed\xE2\x80\xA6' in Settings > Backup & Data. This feature needs the current daemon version.";
strings_["faq_w_seed_3_q"] = "How do I restore from my recovery phrase?";
strings_["faq_w_seed_3_a"] = "Use the seed-restore flow when setting up a wallet and enter your words in order. The node then re-scans the chain to rebuild your balance and history, which can take a while the first time.";
strings_["faq_w_seed_4_q"] = "Should I also back up wallet.dat?";
strings_["faq_w_seed_4_a"] = "Your seed phrase is the primary backup and is enough to restore everything. A copy of the wallet.dat file is a convenient secondary backup that also preserves labels and settings. Keep any backup offline and private.";
// Wallet > Chat & Contacts
strings_["faq_w_chat_1_q"] = "What is the Chat feature?";
strings_["faq_w_chat_1_a"] = "Chat is an encrypted, on-chain messenger built into the wallet. Messages are sent as private shielded memos, so only you and your contact can read them.";
strings_["faq_w_chat_2_q"] = "How does my chat identity work?";
strings_["faq_w_chat_2_a"] = "Your chat identity is derived from your wallet's recovery phrase, so it travels with your wallet \xE2\x80\x94 restore the wallet and your identity comes back. You don't create a separate account or password.";
strings_["faq_w_chat_3_q"] = "How do contacts work?";
strings_["faq_w_chat_3_a"] = "Add a contact by their address in the Contacts tab, optionally with a name and avatar. Contacts can be kept private to the current wallet or shared across all wallets you open on this computer.";
// Wallet > Settings & Appearance
strings_["faq_w_set_1_q"] = "How do I change the theme?";
strings_["faq_w_set_1_a"] = "Open Settings > Appearance to pick a theme and accent. Several light and dark skins are included, and effects like blur can be toggled off for lower-powered machines.";
strings_["faq_w_set_2_q"] = "Can I change the language?";
strings_["faq_w_set_2_a"] = "Yes \xE2\x80\x94 Settings > Appearance has a language selector. The wallet ships with several translations; anything not yet translated falls back to English.";
strings_["faq_w_set_3_q"] = "The text is too small (or too large) \xE2\x80\x94 can I scale it?";
strings_["faq_w_set_3_a"] = "Use the font-scale option in Settings > Appearance. It scales the whole interface, which is handy on high-resolution (HiDPI) displays where the app might otherwise render small.";
strings_["faq_w_set_4_q"] = "How do I show fiat prices?";
strings_["faq_w_set_4_a"] = "Enable price fetching in Settings. The wallet then shows an approximate fiat value alongside balances and can display a small market chart. Prices are informational only.";
// Daemon > The Full Node
strings_["faq_d_node_1_q"] = "What is the daemon (dragonxd)?";
strings_["faq_d_node_1_a"] = "The daemon, dragonxd, is the DragonX full node. It downloads and verifies the entire blockchain, relays transactions to the network, and answers the wallet's queries. Running your own node means you don't have to trust anyone else's server.";
strings_["faq_d_node_2_q"] = "Embedded vs external node \xE2\x80\x94 what's the difference?";
strings_["faq_d_node_2_a"] = "By default the wallet launches and manages its own bundled node (embedded) \xE2\x80\x94 you don't have to do anything. If you already run dragonxd yourself, the wallet can connect to that external node instead. The wallet detects which situation it's in and behaves accordingly.";
strings_["faq_d_node_3_q"] = "Why run a full node at all?";
strings_["faq_d_node_3_a"] = "A full node validates every rule of the blockchain independently, so your wallet trusts math instead of a third party. It also strengthens the network. The trade-off is disk space and an initial sync.";
// Daemon > Sync & Blockchain
strings_["faq_d_sync_1_q"] = "What does 'Processing blocks' at startup mean?";
strings_["faq_d_sync_1_a"] = "On launch the node loads and validates blocks before it can serve the wallet. The 'Processing blocks / Applying blocks to build the current chain state' screen is that warm-up. It's normal; the wallet becomes usable once the node finishes and reports its height.";
strings_["faq_d_sync_2_q"] = "What is 'Rebuilding witness cache'?";
strings_["faq_d_sync_2_a"] = "For your shielded funds the node keeps cryptographic 'witnesses' that let you spend privately. After certain restarts or updates it rebuilds this cache by re-scanning recent blocks. On a wallet with many transactions this can take several minutes and use extra memory.\n\nIt is not stuck \xE2\x80\x94 let it finish. Force-quitting in the middle just makes it restart the rebuild next time.";
strings_["faq_d_sync_3_q"] = "Why is syncing slow when I'm almost caught up?";
strings_["faq_d_sync_3_a"] = "Near the chain tip, work that scans your wallet (checking balances and notes) competes with the node for the same internal lock, which can slow down connecting the final blocks on a large wallet. Recent versions throttle that scanning so the node can catch up. Staying on a lighter tab (or just waiting) lets it finish faster.";
strings_["faq_d_sync_4_q"] = "How long does the initial sync take?";
strings_["faq_d_sync_4_a"] = "The first sync downloads and verifies the whole chain and can take a while depending on your connection and disk. You can speed it up dramatically by enabling the bootstrap download in the setup wizard, which fetches a recent verified copy of the chain data.";
// Daemon > Node Management
strings_["faq_d_mgmt_1_q"] = "How do I start or stop the node?";
strings_["faq_d_mgmt_1_a"] = "The embedded node starts automatically with the wallet and stops when appropriate, so you normally don't manage it by hand. Advanced controls (including restarting the daemon) live in Settings > Node & Security.";
strings_["faq_d_mgmt_2_q"] = "What does 'close external daemon on exit' do?";
strings_["faq_d_mgmt_2_a"] = "If you connect the wallet to a node you started yourself, this option decides whether closing the wallet also shuts that node down. Leave it off if you want your node to keep running after you close the wallet.";
strings_["faq_d_mgmt_3_q"] = "Where is the blockchain and wallet data stored?";
strings_["faq_d_mgmt_3_a"] = "Node and wallet data live in the DragonX data directory under your user profile (for example, in AppData on Windows or your home folder on Linux/macOS). The blockchain is the large part; keep enough free disk space for it to grow.";
// Daemon > Updating the Node
strings_["faq_d_upd_1_q"] = "How do I update the node?";
strings_["faq_d_upd_1_a"] = "Open Settings > Node & Security > Daemon binary and use 'Check for updates\xE2\x80\xA6'. The wallet downloads the latest verified node build and installs it; the new version takes effect the next time the daemon starts.";
strings_["faq_d_upd_2_q"] = "Can I install a specific node version?";
strings_["faq_d_upd_2_a"] = "Yes \xE2\x80\x94 the update dialog lists every release so you can pick a specific or older build. Be cautious downgrading: an older node may not accept blockchain data written by a newer one and could need a re-index.";
strings_["faq_d_upd_3_q"] = "Is the update safe?";
strings_["faq_d_upd_3_a"] = "Every downloaded node archive is verified against a checksum and a cryptographic signature before it's installed. If verification fails, the wallet refuses the update.";
// Daemon > Mining & Pools
strings_["faq_d_mine_1_q"] = "How do I mine DragonX?";
strings_["faq_d_mine_1_a"] = "The Mining tab lets you mine with your CPU. DragonX uses the RandomX algorithm, which is designed for regular processors. Set the number of threads and start \xE2\x80\x94 rewards accrue to your wallet.";
strings_["faq_d_mine_2_q"] = "Solo vs pool mining \xE2\x80\x94 which should I choose?";
strings_["faq_d_mine_2_a"] = "Solo mining sends rewards straight to you but pays only when you find a block, which is infrequent unless you have a lot of hashrate. Pool mining shares work with others for smaller, steadier payouts. The Mining tab supports both.";
strings_["faq_d_mine_3_q"] = "How do I update the miner?";
strings_["faq_d_mine_3_a"] = "The Mining tab's pool section has an 'Update miner\xE2\x80\xA6' button that downloads, verifies, and installs the latest optimized DragonX miner build. Like the node updater, each download is checksum- and signature-verified before install.";
strings_["faq_d_mine_4_q"] = "Can I host a mining pool?";
strings_["faq_d_mine_4_a"] = "The current node can run a built-in stratum server so other miners can point at your machine. There's a toggle for it under Settings > Node & Security. By default it only listens locally; exposing it to other computers requires opening it up deliberately.";
// Daemon > Peers & Network
strings_["faq_d_net_1_q"] = "The wallet shows no peers or connections \xE2\x80\x94 what's wrong?";
strings_["faq_d_net_1_a"] = "Right after launch the node needs a moment to find peers, so a brief '0 peers' is normal. If it persists, check your internet connection and that a firewall isn't blocking the node. The node finds peers automatically through built-in seeds.";
strings_["faq_d_net_2_q"] = "Is my connection to the network encrypted?";
strings_["faq_d_net_2_a"] = "Yes \xE2\x80\x94 DragonX nodes talk to each other over TLS, so peer connections are encrypted. The wallet also talks to its own node over a secure local channel.";
// Daemon > Storage & Performance
strings_["faq_d_perf_1_q"] = "How much disk does the node use, and can I tune memory?";
strings_["faq_d_perf_1_a"] = "The blockchain is the large item and grows over time, so keep several gigabytes free. The node uses a database cache for speed; the bundled defaults are tuned for typical machines. Advanced users can adjust the node's database cache by editing dbcache in its config file (DRAGONX.conf).";
strings_["faq_d_perf_2_q"] = "How do I make the initial sync faster?";
strings_["faq_d_perf_2_a"] = "Enable the bootstrap download (offered in the setup wizard) to fetch a recent, verified copy of the chain instead of validating every block from scratch. A fast disk (SSD) also helps a lot.";
// Daemon > Troubleshooting
strings_["faq_d_trbl_1_q"] = "The node seems stuck on 'Activating best chain' \xE2\x80\x94 is it frozen?";
strings_["faq_d_trbl_1_a"] = "Usually not. On a large wallet the node can spend several minutes rebuilding shielded witnesses or applying blocks, during which it looks paused and may be slow to answer. Give it time \xE2\x80\x94 the debug log (Settings) shows steady progress if it's working.\n\nAvoid force-quitting during this phase; it restarts the work from scratch next launch.";
strings_["faq_d_trbl_2_q"] = "What is 'degraded mode'?";
strings_["faq_d_trbl_2_a"] = "If a wallet file is recovered but part of its key data is missing, the node may open it in a limited 'degraded' mode where it can't derive new addresses or shield funds. The safest fix is to restore from your seed recovery phrase into a fresh wallet.";
strings_["faq_d_trbl_3_q"] = "When should I re-index or re-scan?";
strings_["faq_d_trbl_3_a"] = "A re-scan makes the wallet re-read the chain to rediscover your transactions (useful after importing keys). A re-index rebuilds the node's blockchain database and is only needed if that database is corrupted or incompatible after a version change. Both can take a while; start them from Settings > Node & Security.";
strings_["faq_d_trbl_4_q"] = "The node is using a lot of memory \xE2\x80\x94 is that normal?";
strings_["faq_d_trbl_4_a"] = "Memory use spikes during heavy work like a witness rebuild or an initial sync, then drops back down once it finishes. Sustained high memory at idle is unusual \xE2\x80\x94 restarting the wallet clears it. On a low-RAM machine, make sure other memory-heavy apps aren't competing.";
// --- Address Book Dialog --- // --- Address Book Dialog ---
strings_["address_book_add"] = "Add Address"; strings_["address_book_add"] = "Add Address";
strings_["address_book_add_new"] = "Add New"; strings_["address_book_add_new"] = "Add New";
@@ -2221,6 +2418,29 @@ void I18n::loadBuiltinEnglish()
strings_["merge_funds"] = "Merge Funds"; strings_["merge_funds"] = "Merge Funds";
strings_["merge_started"] = "Merge operation started"; strings_["merge_started"] = "Merge operation started";
strings_["merge_title"] = "Merge to Address"; strings_["merge_title"] = "Merge to Address";
// Consolidate-funds flow (rich merge modal + wallet-bloat preset).
strings_["consolidate_title"] = "Consolidate funds";
strings_["consolidate_desc"] = "Combine many small inputs into a single shielded note. Fewer notes means a smaller wallet file and better privacy.";
strings_["consolidate_funds_btn"] = "Consolidate";
strings_["merge_scope_loading"] = "Checking your inputs\xE2\x80\xA6";
strings_["merge_scope_fmt"] = "%d transparent + %d shielded inputs \xC2\xB7 ~%s DRGX spendable";
strings_["merge_source"] = "Consolidate";
strings_["merge_src_transparent"] = "Transparent";
strings_["merge_src_shielded"] = "Shielded";
strings_["merge_src_both"] = "Both";
strings_["merge_batch_fmt"] = "Merges up to %d inputs per run \xE2\x80\x94 repeat to finish the rest.";
strings_["merge_advanced"] = "Advanced";
strings_["merge_max_inputs"] = "Max inputs per batch";
strings_["merge_fee_hint"] = "Network fee for this transaction.";
strings_["merge_create_zaddr"] = "Create shielded address";
strings_["merge_creating"] = "Creating address\xE2\x80\xA6";
strings_["merge_addr_created"] = "Shielded address created.";
strings_["merge_confirm_fmt"] = "Consolidate ~%s DRGX from %d input(s) into %s?";
strings_["merge_confirm_btn"] = "Confirm";
strings_["merge_back"] = "Back";
strings_["merge_progress"] = "Consolidating\xE2\x80\xA6 this can take a few minutes. You can close this window.";
strings_["merge_no_spendable"] = "No spendable inputs to consolidate yet.";
strings_["done"] = "Done";
// --- Transaction Details Dialog --- // --- Transaction Details Dialog ---
strings_["tx_confirmations"] = "%d confirmations"; strings_["tx_confirmations"] = "%d confirmations";
@@ -2304,6 +2524,217 @@ void I18n::loadBuiltinEnglish()
strings_["explorer_hash_not_found"] = "No block or transaction found for this hash"; strings_["explorer_hash_not_found"] = "No block or transaction found for this hash";
strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash"; strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash";
strings_["explorer_no_results"] = "No matching cached blocks"; strings_["explorer_no_results"] = "No matching cached blocks";
// ---- i18n audit 2026-09: wrap previously-hardcoded UI strings ----
// app_security.cpp — encryption / PIN / lock flow
strings_["sec_restart_daemon_for_encryption"] = "Please restart your daemon for encryption to take effect.";
strings_["sec_encrypting_wallet"] = "Encrypting wallet...";
strings_["sec_wallet_encrypted_restarting_daemon"] = "Wallet encrypted. Restarting daemon...";
strings_["sec_wallet_encrypted_successfully"] = "Wallet encrypted successfully";
strings_["sec_encryption_failed_prefix"] = "Encryption failed: ";
strings_["sec_wallet_encrypted_and_pin_set"] = "Wallet encrypted & PIN set";
strings_["sec_wallet_encrypted_but_pin_vault_failed"] = "Wallet encrypted but PIN vault failed";
strings_["sec_couldnt_lock_wallet"] = "Couldn't lock the wallet — it is still unlocked. Check the daemon connection.";
strings_["sec_changing_passphrase"] = "Changing passphrase...";
strings_["sec_passphrase_changed_successfully"] = "Passphrase changed successfully";
strings_["sec_failed_prefix"] = "Failed: ";
strings_["sec_encryption_did_not_complete"] = "Wallet encryption did not complete — your wallet is NOT encrypted. Open Settings to finish encrypting it.";
strings_["sec_wallet_locked_title"] = "Wallet Locked";
strings_["sec_too_many_attempts_wait"] = "Too many attempts. Wait %.0f seconds...";
strings_["sec_mode_passphrase"] = " Passphrase";
strings_["sec_use_passphrase_instead"] = "Use passphrase instead";
strings_["sec_use_pin_instead"] = "Use PIN instead";
strings_["sec_unlocking_fmt"] = "Unlocking%s";
strings_["sec_unlock_button"] = "Unlock";
strings_["sec_not_connected_to_daemon"] = "Not connected to daemon";
strings_["sec_unlock_failed_prefix"] = "Unlock failed: ";
strings_["sec_incorrect_passphrase_decrypt"] = "Incorrect passphrase";
strings_["sec_importing_keys_rescanning"] = "Importing keys & rescanning blockchain — wallet is usable while this runs";
strings_["sec_encrypted_backup_suffix"] = "\nEncrypted backup: wallet.dat.encrypted.bak";
strings_["sec_wallet_decrypted_all_keys_imported"] = "Wallet decrypted successfully! All keys imported.";
strings_["sec_total_elapsed_fmt"] = "Total elapsed: %dm %02ds";
strings_["sec_verifying_passphrase"] = "Verifying passphrase...";
strings_["sec_incorrect_passphrase_pin_setup"] = "Incorrect passphrase";
strings_["sec_pin_set_successfully"] = "PIN set successfully";
strings_["sec_failed_to_create_vault"] = "Failed to create vault";
strings_["sec_not_connected_to_daemon_pin"] = "Not connected to daemon";
strings_["sec_changing_pin"] = "Changing PIN...";
strings_["sec_pin_changed_successfully"] = "PIN changed successfully";
strings_["sec_incorrect_current_pin"] = "Incorrect current PIN";
strings_["sec_internal_error_change_pin"] = "Internal error";
strings_["sec_verifying_pin"] = "Verifying PIN...";
strings_["sec_pin_removed"] = "PIN removed";
strings_["sec_incorrect_pin_remove"] = "Incorrect PIN";
strings_["sec_internal_error_remove_pin"] = "Internal error";
// app.cpp — wizard, seed backup, shutdown, daemon/miner lifecycle
strings_["appx_creating_your_wallet"] = "Creating your wallet…";
strings_["appx_create_failed_prefix"] = "Create failed: ";
strings_["appx_back_up_seed_phrase_title"] = "Back up your seed phrase";
strings_["appx_seed_backup_warning"] = "These 24 words are the ONLY way to restore your wallet. Write them down in order, store them offline, and never share them. If you lose them, your funds are gone forever.";
strings_["appx_birthday_block_height"] = "Birthday (block height): %llu — back this up too.";
strings_["appx_ive_written_it_down"] = "I've written it down";
strings_["appx_copy"] = "Copy";
strings_["appx_skip_anyway"] = "Skip anyway";
strings_["appx_skip"] = "Skip";
strings_["appx_seed_not_backed_up_warning"] = "You have not backed up your seed — funds could be lost. Skip anyway?";
strings_["appx_confirm_your_backup"] = "Confirm your backup";
strings_["appx_tap_words_in_order"] = "Tap the words in the correct order to confirm you saved them.";
strings_["appx_progress_n_of_n"] = "Progress: %d / %d";
strings_["appx_not_next_word"] = " — that's not the next word";
strings_["appx_done"] = "Done";
strings_["appx_wallet_created_and_backed_up"] = "Wallet created and backed up.";
strings_["appx_back"] = "Back";
strings_["appx_restoring_your_wallet"] = "Restoring your wallet…";
strings_["appx_recovery_phrase_word_count"] = "Recovery phrase should be 24 words — you have %d.";
strings_["appx_could_not_start_restore"] = "Could not start restore";
strings_["appx_node_rebuilding_witness_cache"] = "Node is rebuilding its witness cache";
strings_["appx_stopping_node_discards_rebuild"] = "Stopping the node now discards the in-progress rebuild and restarts it (several minutes) the next time you open the wallet. You can keep the node running instead.";
strings_["appx_keep_node_running_and_quit"] = "Keep node running & quit";
strings_["appx_stop_anyway_and_quit"] = "Stop anyway & quit";
strings_["appx_cancel"] = "Cancel";
strings_["appx_last_used_wallet_not_found_prefix"] = "Your last-used wallet file (";
strings_["appx_last_used_wallet_not_found_suffix"] = ") was not found — opened the default wallet instead. If you moved it, restore it and switch back from the wallet list.";
strings_["appx_wallet_open_failed_prefix"] = "Wallet open failed: ";
strings_["appx_blockchain_rescan_complete"] = "Blockchain rescan complete";
strings_["appx_miner_stopped_unexpectedly"] = "Miner stopped unexpectedly.";
strings_["appx_miner_stopped_prefix"] = "Miner stopped: ";
strings_["appx_pool_miner_connected_and_hashing"] = "Pool miner connected and hashing.";
strings_["appx_bootstrap_complete_reconciling"] = "Bootstrap complete — reconciling your wallet with the new chain data.";
strings_["appx_blockchain_data_deleted"] = "Blockchain data deleted (%d items). The daemon is restarting to re-sync from the network.";
strings_["appx_invalid_payment_uri_prefix"] = "Invalid payment URI: ";
strings_["appx_payment_request_loaded"] = "Payment request loaded";
strings_["appx_fullnode_lifecycle_unavailable_lite"] = "Full-node lifecycle actions are unavailable in lite build";
strings_["appx_blockchain_maintenance_in_progress"] = "A blockchain maintenance operation is already in progress.";
strings_["appx_node_busy_restarting"] = "The node is busy restarting — try again in a moment.";
strings_["appx_restarting_daemon_rescan_flag"] = "Restarting daemon with -rescan flag...";
strings_["appx_restarting_daemon_zapwallettxes"] = "Restarting daemon with -zapwallettxes=2 (wallet repair)...";
strings_["appx_no_bundled_daemon_to_install"] = "This build has no bundled daemon to install";
strings_["appx_no_embedded_daemon_to_install"] = "This build has no embedded daemon to install";
strings_["appx_daemon_reinstall_in_progress"] = "The daemon reinstall is already in progress.";
strings_["appx_installing_bundled_daemon"] = "Installing bundled daemon — the node will stop, update, and restart...";
strings_["appx_stopping_daemon_deleting_blockchain"] = "Stopping daemon and deleting blockchain data...";
strings_["appx_stopping_pool_miner"] = "Stopping pool miner...";
strings_["appx_disconnecting"] = "Disconnecting...";
strings_["appx_sending_stop_command_to_daemon"] = "Sending stop command to daemon...";
strings_["appx_cleaning_up"] = "Cleaning up...";
strings_["appx_shutdown_complete"] = "Shutdown complete";
strings_["appx_n_seconds"] = "%d seconds";
strings_["appx_n_min_n_sec"] = "%d min %d sec";
strings_["appx_still_status_prefix"] = "Still \"";
strings_["appx_still_status_suffix"] = "\" — force quitting now may corrupt chain data.";
strings_["appx_rebuilding_witness_cache_blocks_left"] = "Rebuilding witness cache %.0f%% — %d blocks left";
strings_["appx_rebuilding_witness_cache_pct"] = "Rebuilding witness cache %.0f%%";
strings_["appx_setting_initial_sapling_witnesses"] = "Setting initial Sapling witnesses %.0f%%";
strings_["appx_rebuilding_sapling_note_witnesses"] = "Rebuilding Sapling note witnesses…";
strings_["appx_syncing_pct_block_n_of_n"] = "Syncing %.1f%% — Block %d / %d";
strings_["appx_last_block_n"] = "Last block: %d";
strings_["appx_encrypting_wallet"] = "Encrypting wallet...";
strings_["appx_waiting_for_daemon_to_encrypt_wallet"] = "Waiting for daemon to encrypt wallet...";
strings_["appx_daemon_error"] = "Daemon Error";
strings_["appx_use_settings_restart_daemon_hint"] = "Use Settings > Restart Daemon to try again";
strings_["appx_copied_clipboard_autoclears"] = "Copied — clipboard auto-clears in 45s";
strings_["appx_dragonxd_output"] = "dragonxd output";
strings_["appx_theme_prefix"] = "Theme: ";
strings_["appx_low_spec_mode_enabled"] = "Low-spec mode enabled";
strings_["appx_low_spec_mode_disabled"] = "Low-spec mode disabled";
strings_["appx_theme_effects_enabled"] = "Theme effects enabled";
strings_["appx_theme_effects_disabled"] = "Theme effects disabled";
strings_["appx_simple_background_enabled"] = "Simple background enabled";
strings_["appx_simple_background_disabled"] = "Simple background disabled";
// balance_tab.cpp
strings_["baltab_shielded_amount"] = "Shielded %.8f";
strings_["baltab_transparent_amount"] = "Transparent %.8f";
strings_["baltab_market_price_4dp"] = "Market: $%.4f";
strings_["baltab_market_price_8dp"] = "Market: $%.8f";
strings_["baltab_pct_of_total_zaddr"] = "%.0f%% of total · %d Z-addr";
strings_["baltab_t_addresses_count"] = "%d T-addresses";
strings_["baltab_pct_change_24h"] = "%s%.1f%% 24h";
strings_["baltab_total_balance"] = "Total Balance";
strings_["baltab_shielded"] = "Shielded";
strings_["baltab_transparent"] = "Transparent";
strings_["baltab_market"] = "Market";
// settings_window.cpp
strings_["swin_invalid_suffix"] = " (invalid)";
strings_["swin_theme_list_refreshed"] = "Theme list refreshed";
strings_["swin_connection_successful"] = "Connection successful!\ndragonxd version: ";
strings_["swin_connection_failed"] = "Connection failed: ";
strings_["swin_rpc_client_not_initialized"] = "RPC client not initialized";
strings_["swin_rescan_started_from_block"] = "Rescan started from block ";
strings_["swin_rescan_to"] = " to ";
strings_["swin_rescan_failed"] = "Rescan failed: ";
strings_["swin_ztx_history_cleared"] = "Z-transaction history cleared";
strings_["swin_no_history_file_found"] = "No history file found";
strings_["swin_settings_saved"] = "Settings saved";
// settings_page.cpp / explorer_tab.cpp / block_info_dialog.cpp
strings_["grpa_tab_appearance"] = "Appearance";
strings_["grpa_tab_wallet"] = "Wallet";
strings_["grpa_tab_backup_data"] = "Backup & Data";
strings_["grpa_tab_node_security"] = "Node & Security";
strings_["grpa_tab_explorer"] = "Explorer";
strings_["grpa_tab_chat"] = "Chat";
strings_["grpa_tab_about"] = "About";
strings_["grpa_enter_private_key_to_import"] = "Enter a private key to import.";
strings_["grpa_invalid_suffix"] = " (invalid)";
strings_["grpa_seed_demo_chat"] = "Seed demo chat";
strings_["grpa_dbg_addrman"] = "Peer address tracking and management";
strings_["grpa_dbg_alert"] = "Alert system messages";
strings_["grpa_dbg_bench"] = "Benchmark timings for operations";
strings_["grpa_dbg_coindb"] = "Coin database read/write operations";
strings_["grpa_dbg_db"] = "Berkeley DB operations";
strings_["grpa_dbg_estimatefee"] = "Fee estimation algorithm";
strings_["grpa_dbg_http"] = "HTTP RPC server activity";
strings_["grpa_dbg_libevent"] = "Libevent networking library";
strings_["grpa_dbg_lock"] = "Lock contention debugging";
strings_["grpa_dbg_mempool"] = "Transaction memory pool activity";
strings_["grpa_dbg_net"] = "Network connections and messages";
strings_["grpa_dbg_paymentdisclosure"] = "Payment disclosure protocol";
strings_["grpa_dbg_pow"] = "Proof-of-work mining activity";
strings_["grpa_dbg_proxy"] = "SOCKS5 proxy connections";
strings_["grpa_dbg_prune"] = "Block pruning operations";
strings_["grpa_dbg_rand"] = "Random number generation";
strings_["grpa_dbg_reindex"] = "Blockchain reindexing progress";
strings_["grpa_dbg_rpc"] = "RPC command processing";
strings_["grpa_dbg_selectcoins"] = "Coin selection for transactions";
strings_["grpa_dbg_tor"] = "Tor integration and circuit info";
strings_["grpa_dbg_zmq"] = "ZeroMQ notification system";
strings_["grpa_dbg_zrpc"] = "Shielded (z-addr) RPC operations";
strings_["grpa_sec_ago"] = "%lld sec ago";
strings_["grpa_min_ago"] = "%lld min ago";
strings_["grpa_hr_ago"] = "%lld hr ago";
strings_["grpa_days_ago"] = "%lld days ago";
strings_["grpa_showing_first_100_of"] = "... showing first 100 of %d";
strings_["grpa_error_prefix"] = "Error: ";
strings_["grpa_invalid_response_from_daemon"] = "Invalid response from daemon";
strings_["grpa_current_block_paren"] = "(Current: %d)";
strings_["grpa_unexpected_getblockhash_result"] = "unexpected getblockhash result";
// receive / chat / send / transaction_details
strings_["grpb_new_badge_suffix"] = " [NEW]";
strings_["grpb_tooltip_address_balance"] = "%s\nBalance: %.8f %s%s";
strings_["grpb_selected_suffix"] = "\n(selected)";
strings_["grpb_preview_msg_payment_through"] = "Did the payment go through? 🙂";
strings_["grpb_preview_msg_yep_confirmed"] = "Yep — just confirmed ✅";
strings_["grpb_preview_msg_sending_rest"] = "Sending the rest now 👍";
strings_["grpb_max"] = "Max";
strings_["grpb_undo_clear"] = "Undo Clear";
strings_["grpb_copy"] = "Copy";
// bootstrap / mining_* / key_export
strings_["grpc_bootstrap_not_initialized"] = "Bootstrap not initialized";
strings_["grpc_bootstrap_failed"] = "Bootstrap failed";
strings_["grpc_na"] = "N/A";
strings_["grpc_key_not_available"] = "Key not available for this address";
strings_["grpc_benchmark_takes_secs"] = "Benchmark takes ~%ds and interrupts mining. Click again to start.";
strings_["grpc_hashrate_fee"] = "%s %s%% fee";
strings_["grpc_benchmark_inconclusive"] = "Benchmark inconclusive: no hashrate samples were recorded. Check the pool connection and try again.";
// misc pre-existing missing keys
strings_["copied_to_clipboard"] = "Copied to clipboard";
} }
const char* I18n::translate(const char* key) const const char* I18n::translate(const char* key) const
+72 -15
View File
@@ -867,6 +867,70 @@ int Platform::getSystemIdleSeconds()
// GPU utilization detection // GPU utilization detection
// ============================================================================ // ============================================================================
std::string Platform::runHiddenCapture(const std::string& cmdLine, bool mergeStderr, int* exitCode)
{
if (exitCode) *exitCode = -1;
#ifdef _WIN32
SECURITY_ATTRIBUTES sa;
ZeroMemory(&sa, sizeof(sa));
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
HANDLE hRead = NULL, hWrite = NULL;
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return {};
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0); // parent's read end stays private
HANDLE hNul = INVALID_HANDLE_VALUE;
if (!mergeStderr) {
hNul = CreateFileA("NUL", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
}
STARTUPINFOA si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
si.hStdOutput = hWrite;
si.hStdError = mergeStderr ? hWrite : hNul;
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));
std::string cl = cmdLine; // CreateProcessA may modify lpCommandLine → needs a mutable buffer
std::string out;
if (CreateProcessA(NULL, cl.empty() ? NULL : &cl[0], NULL, NULL, TRUE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
CloseHandle(hWrite); hWrite = NULL; // close our copy so ReadFile hits EOF when the child exits
if (hNul != INVALID_HANDLE_VALUE) { CloseHandle(hNul); hNul = INVALID_HANDLE_VALUE; }
char buf[4096];
DWORD n = 0;
while (ReadFile(hRead, buf, sizeof(buf), &n, NULL) && n > 0) out.append(buf, n);
WaitForSingleObject(pi.hProcess, INFINITE);
if (exitCode) {
DWORD code = 0;
if (GetExitCodeProcess(pi.hProcess, &code)) *exitCode = static_cast<int>(code);
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
if (hWrite != NULL) CloseHandle(hWrite);
if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul);
CloseHandle(hRead);
return out;
#else
const std::string full = cmdLine + (mergeStderr ? " 2>&1" : " 2>/dev/null");
std::string out;
FILE* f = popen(full.c_str(), "r");
if (!f) return out;
char buf[512];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) out.append(buf, n);
const int st = pclose(f);
if (exitCode) *exitCode = st; // raw status (matches prior pclose-based rc checks)
return out;
#endif
}
int Platform::getGpuUtilization() int Platform::getGpuUtilization()
{ {
#ifdef _WIN32 #ifdef _WIN32
@@ -877,23 +941,16 @@ int Platform::getGpuUtilization()
static bool s_has_nvidia = false; static bool s_has_nvidia = false;
if (!s_tried_nvidia) { if (!s_tried_nvidia) {
s_tried_nvidia = true; s_tried_nvidia = true;
FILE* f = _popen("where nvidia-smi 2>nul", "r"); // Windowless (runHiddenCapture) so GPU-aware idle detection never flashes a cmd.exe console.
if (f) { const std::string w = runHiddenCapture("where nvidia-smi");
char buf[256]; s_has_nvidia = (w.find_first_not_of(" \t\r\n") != std::string::npos);
s_has_nvidia = (fgets(buf, sizeof(buf), f) != nullptr);
_pclose(f);
}
} }
if (s_has_nvidia) { if (s_has_nvidia) {
FILE* f = _popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>nul", "r"); const std::string o = runHiddenCapture(
if (f) { "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits");
char buf[64]; if (!o.empty()) {
int util = -1; int util = atoi(o.c_str());
if (fgets(buf, sizeof(buf), f)) { if (util < 0 || util > 100) util = -1;
util = atoi(buf);
if (util < 0 || util > 100) util = -1;
}
_pclose(f);
return util; return util;
} }
} }
+9
View File
@@ -179,6 +179,15 @@ public:
* @return GPU busy percent, or -1 if unavailable. * @return GPU busy percent, or -1 if unavailable.
*/ */
static int getGpuUtilization(); static int getGpuUtilization();
// Run a command line and capture its stdout WITHOUT ever popping a console window: Windows uses
// CreateProcess + CREATE_NO_WINDOW (a plain popen()/_popen() flashes a cmd.exe console), POSIX uses
// popen(). Use this instead of _popen for anything run while the GUI is up. `mergeStderr` folds the
// child's stderr into the result (like "2>&1"); otherwise stderr is discarded. `exitCode`, if given,
// receives the child's exit status (raw pclose() status on POSIX, GetExitCodeProcess on Windows; -1
// if the process could not be launched).
static std::string runHiddenCapture(const std::string& cmdLine, bool mergeStderr = false,
int* exitCode = nullptr);
}; };
/** /**
+28 -8
View File
@@ -1417,31 +1417,51 @@ void testNetworkRefreshRpcCollectors()
}); });
coreRpc.addResponse("getblockchaininfo", json{ coreRpc.addResponse("getblockchaininfo", json{
{"blocks", 150}, {"blocks", 150},
{"headers", 155}, {"headers", 150},
{"bestblockhash", "core-best-150"}, {"bestblockhash", "core-best-150"},
{"verificationprogress", 0.80}, {"verificationprogress", 1.0},
{"longestchain", 160}, {"longestchain", 150}, // caught up → balance scan runs
{"notarized", 145} {"notarized", 145}
}); });
auto core = Refresh::collectCoreRefreshResult(coreRpc); auto core = Refresh::collectCoreRefreshResult(coreRpc);
// getblockchaininfo is issued FIRST — sync detection gates (and must not be delayed behind) the
// O(mapWallet) balance scan. When caught up, the balance scan follows: display (minconf=0) +
// spendable (minconf=1).
EXPECT_TRUE(coreRpc.methodNames() == std::vector<std::string>({ EXPECT_TRUE(coreRpc.methodNames() == std::vector<std::string>({
"z_gettotalbalance", "z_gettotalbalance", "getblockchaininfo" // display (minconf=0) + spendable (minconf=1) "getblockchaininfo", "z_gettotalbalance", "z_gettotalbalance"
})); }));
EXPECT_EQ(coreRpc.calls[0].params, json::array({0})); EXPECT_EQ(coreRpc.calls[1].params, json::array({0}));
EXPECT_EQ(coreRpc.calls[1].params, json::array({1})); EXPECT_EQ(coreRpc.calls[2].params, json::array({1}));
EXPECT_TRUE(core.balanceOk); EXPECT_TRUE(core.balanceOk);
EXPECT_TRUE(core.blockchainOk); EXPECT_TRUE(core.blockchainOk);
EXPECT_NEAR(*core.totalBalance, 4.25, 0.00000001); EXPECT_NEAR(*core.totalBalance, 4.25, 0.00000001);
EXPECT_EQ(*core.blocks, 150); EXPECT_EQ(*core.blocks, 150);
EXPECT_EQ(*core.bestBlockHash, std::string("core-best-150")); EXPECT_EQ(*core.bestBlockHash, std::string("core-best-150"));
EXPECT_EQ(*core.longestChain, 160); EXPECT_EQ(*core.longestChain, 150);
// When getblockchaininfo shows the node is behind (blocks < longestchain - 2), the balance scan is
// skipped this cycle regardless of includeBalance — so sync-state (and hence kSyncProfile) updates
// promptly instead of after the multi-second scan. Only getblockchaininfo is issued.
MockRefreshRpc coreBehindRpc;
coreBehindRpc.addResponse("z_gettotalbalance", json{{"total", "4.25000000"}});
coreBehindRpc.addResponse("getblockchaininfo", json{
{"blocks", 150}, {"headers", 160}, {"longestchain", 160}
});
auto coreBehind = Refresh::collectCoreRefreshResult(coreBehindRpc);
EXPECT_TRUE(coreBehindRpc.methodNames() == std::vector<std::string>({"getblockchaininfo"}));
EXPECT_FALSE(coreBehind.balanceOk);
EXPECT_TRUE(coreBehind.blockchainOk);
EXPECT_EQ(*coreBehind.blocks, 150);
EXPECT_EQ(*coreBehind.longestChain, 160);
MockRefreshRpc coreFallbackRpc; MockRefreshRpc coreFallbackRpc;
coreFallbackRpc.addFailure("z_gettotalbalance", "wallet warming up"); coreFallbackRpc.addFailure("z_gettotalbalance", "wallet warming up");
coreFallbackRpc.addResponse("getblockchaininfo", json{{"blocks", 8}, {"headers", 9}}); coreFallbackRpc.addResponse("getblockchaininfo", json{{"blocks", 8}, {"headers", 9}});
auto partialCore = Refresh::collectCoreRefreshResult(coreFallbackRpc); auto partialCore = Refresh::collectCoreRefreshResult(coreFallbackRpc);
// No longestchain in the response → not classified as "behind" → balance is still attempted (here it
// fails). getblockchaininfo is still issued first.
EXPECT_TRUE(coreFallbackRpc.methodNames() == std::vector<std::string>({ EXPECT_TRUE(coreFallbackRpc.methodNames() == std::vector<std::string>({
"z_gettotalbalance", "z_gettotalbalance", "getblockchaininfo" "getblockchaininfo", "z_gettotalbalance", "z_gettotalbalance"
})); }));
EXPECT_FALSE(partialCore.balanceOk); EXPECT_FALSE(partialCore.balanceOk);
EXPECT_TRUE(partialCore.blockchainOk); EXPECT_TRUE(partialCore.blockchainOk);