Compare commits
54 Commits
bfe8b4d77d
...
f6024557d5
| Author | SHA1 | Date | |
|---|---|---|---|
| f6024557d5 | |||
| d82821ba06 | |||
| 8290e96ec4 | |||
| 46f7da4fac | |||
| 5bd07b0505 | |||
| 90df19b8b6 | |||
| 5985f05fb2 | |||
| 60ee73bf1d | |||
| 625df8abe6 | |||
| 5dd68dc9bd | |||
| 8a30578872 | |||
| 63d5c817aa | |||
| 3662df550c | |||
| 067c96c425 | |||
| a2c4a8df73 | |||
| 822891d4ef | |||
| 22638b094c | |||
| bf81ec1702 | |||
| 56cd9b6273 | |||
| 8b9cd5bf80 | |||
| 2b191cea34 | |||
| 6ecbf835ec | |||
| a13190a6df | |||
| ffc24ee99b | |||
| 3274c72a58 | |||
| b7b32bfde8 | |||
| 0b47134112 | |||
| 828018de2b | |||
| 2a96c41e07 | |||
| c25da0d26d | |||
| 0c55ed0fe4 | |||
| 1fc3e03e6f | |||
| f428292ea4 | |||
| 46f3001360 | |||
| 8293f02e36 | |||
| b34069f57c | |||
| 4416e01f9c | |||
| 44a17f3ad0 | |||
| c9e9c9f979 | |||
| ab4dd370c0 | |||
| 675d434958 | |||
| e8055888a5 | |||
| 48eceb6593 | |||
| 4f71c18884 | |||
| 232b81f8ee | |||
| c40c252c9a | |||
| d156dddcd0 | |||
| 0e1957c5f9 | |||
| 3f95765dcc | |||
| 62c92cc862 | |||
| 3aeb847657 | |||
| 2a9d32e78b | |||
| 97a54f0e0c | |||
| 82da4af857 |
@@ -79,13 +79,13 @@ The detailed milestone plan and design history (the v2 plan, backend artifact/AB
|
||||
|
||||
## Miner updater (xmrig)
|
||||
|
||||
The mining tab's pool section has an **"Update miner…"** button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (`util/XmrigUpdater` + `ui/windows/xmrig_download_dialog.h`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest` → pick the asset for this platform (`linux-x64` / `win-x64` / `macos-x86_64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (from the release body) **and** a detached **ed25519 signature** → miniz-extract the binary (flattening the versioned subdir) into `resources::getDaemonDirectory()`. The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into `xmrig_updater_core.cpp` for unit tests; an env-gated (`DRAGONX_TEST_NETWORK=1`) test exercises the worker live. A **"Browse all releases…"** button (the `/releases` list, newest first, pre-releases included) lets users pin an older or pre-release build — same verify/install path via `startInstallRelease()`; the picker UI is shared with the daemon updater (`ui/windows/release_list_view.h`).
|
||||
The mining tab's pool section has an **"Update miner…"** button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (`util/XmrigUpdater` + `ui/windows/xmrig_download_dialog.h`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest` → pick the asset for this platform (`linux-x64` / `win-x64` / `macos-x86_64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (from the release body) **and** a detached **ed25519 signature** → miniz-extract the binary (flattening the versioned subdir) into `resources::getDaemonDirectory()`. The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into `xmrig_updater_core.cpp` for unit tests; an env-gated (`DRAGONX_TEST_NETWORK=1`) test exercises the worker live. The dialog is a two-pane version picker (every `/releases` entry on the left, newest first, pre-releases included) so users can pin an older or pre-release build — same verify/install path via `startInstallRelease()`. It shares only the `ReleaseRow` row model with the daemon updater (`ui/windows/release_list_view.h`); each dialog renders its own tactile Material list.
|
||||
|
||||
**Signature verification is enforced** (`kXmrigRequireSignature = true` in `src/util/xmrig_updater.h`), checked against the public key pinned in `kXmrigSignaturePublicKeyBase64`. **Consequence for releases:** every `drg-xmrig` release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then `scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>...` (OpenSSL-based, no extra deps) and upload each `<archive>.sig` as a release asset alongside its `.zip`. The signing **secret key must stay offline** (it is gitignored: `*.ed25519.key`); only its base64 public key is pinned in the source. To rotate the key, regenerate (`scripts/sign-xmrig-release.sh keygen`) and update `kXmrigSignaturePublicKeyBase64`. An emergency env override is not provided — disabling verification means setting `kXmrigSignaturePublicKeyBase64` empty (and rebuilding).
|
||||
|
||||
## Daemon updater (dragonxd)
|
||||
|
||||
Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…"** button that downloads/verifies/installs the latest **dragonxd full node** from the project Gitea — the full-node sibling of the xmrig updater (`util/DaemonUpdater` + `ui/windows/daemon_download_dialog.h`, pure no-I/O core in `daemon_updater_core.cpp`; gated full-node-only via `supportsFullNodeLifecycleActions()`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest` → pick the archive for this platform (`linux-amd64` / `macos` / `win64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (parsed from the release body's markdown **checksum table**, not xmrig's `<hash> <name>` lines) **and** a detached **ed25519 signature** → miniz-extract the three executables (`dragonxd`/`dragonx-cli`/`dragonx-tx`, flattening the versioned subdir) into `resources::getDaemonDirectory()`. The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is **atomic and safe while the node runs** (POSIX `rename()` replaces the in-use binary; Windows moves the locked `.exe` aside to `.old`); the new binary takes effect on the **next daemon start**, so the Done screen offers **"Restart daemon now"** (`App::restartDaemon()`). A **"Browse all releases…"** button (shared `release_list_view.h` picker) lets users pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data.
|
||||
Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…"** button that downloads/verifies/installs the latest **dragonxd full node** from the project Gitea — the full-node sibling of the xmrig updater (`util/DaemonUpdater` + `ui/windows/daemon_download_dialog.h`, pure no-I/O core in `daemon_updater_core.cpp`; gated full-node-only via `supportsFullNodeLifecycleActions()`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest` → pick the archive for this platform (`linux-amd64` / `macos` / `win64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (parsed from the release body's markdown **checksum table**, not xmrig's `<hash> <name>` lines) **and** a detached **ed25519 signature** → miniz-extract the three executables (`dragonxd`/`dragonx-cli`/`dragonx-tx`, flattening the versioned subdir) into `resources::getDaemonDirectory()`. The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is **atomic and safe while the node runs** (POSIX `rename()` replaces the in-use binary; Windows moves the locked `.exe` aside to `.old`); the new binary takes effect on the **next daemon start**, so the Done screen offers **"Restart daemon now"** (`App::restartDaemon()`). The dialog is a two-pane version picker (every `/releases` entry on the left) so users can pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data. It shares only the `ReleaseRow` row model (`ui/windows/release_list_view.h`) with the miner updater; each renders its own tactile Material list.
|
||||
|
||||
**Signature verification is enforced** (`kDaemonRequireSignature = true` in `src/util/daemon_updater.h`), checked against `kDaemonSignaturePublicKeyBase64`. **Consequence for releases:** every `dragonx` release MUST ship a detached `<archive>.sig` per platform archive or the in-app updater refuses it (as of v1.0.2 the releases publish SHA-256 but **no** signatures yet — sign + upload them to enable in-app updates). To cut a release: `scripts/sign-daemon-release.sh sign <secret.key> dragonx-<ver>-{linux-amd64,macos,win64}.zip` (OpenSSL-based) and upload each `.sig` next to its `.zip`. The signing **secret key stays offline** (gitignored `*.ed25519.key`; this repo's is `dragonx-daemon.ed25519.key`); only the base64 public key is pinned. To rotate: `scripts/sign-daemon-release.sh keygen` and update `kDaemonSignaturePublicKeyBase64`. The generic SHA-256 / ed25519 primitives are shared with the miner updater (`util::sha256Hex` / `util::verifyXmrigSignature`).
|
||||
|
||||
|
||||
Binary file not shown.
107
res/lang/de.json
107
res/lang/de.json
@@ -62,6 +62,7 @@
|
||||
"backup_data": "SICHERUNG & DATEN",
|
||||
"backup_description": "Erstellen Sie eine Sicherung Ihrer wallet.dat-Datei. Diese Datei enthält alle Ihre privaten Schlüssel und den Transaktionsverlauf. Bewahren Sie die Sicherung an einem sicheren Ort auf.",
|
||||
"backup_destination": "Sicherungsziel:",
|
||||
"backup_overwrite_confirm": "Dort existiert bereits eine Datei — erneut speichern, um sie zu überschreiben.",
|
||||
"backup_source": "Quelle: %s",
|
||||
"backup_tip_external": "Speichern Sie Sicherungen auf externen Laufwerken oder Cloud-Speicher",
|
||||
"backup_tip_multiple": "Erstellen Sie mehrere Sicherungen an verschiedenen Orten",
|
||||
@@ -70,6 +71,7 @@
|
||||
"backup_title": "Wallet sichern",
|
||||
"backup_wallet": "Wallet sichern...",
|
||||
"backup_wallet_not_found": "Warnung: wallet.dat nicht am erwarteten Speicherort gefunden",
|
||||
"backup_warn": "Diese Datei enthält alle Ihre privaten Schlüssel — bewahren Sie sie sicher auf.",
|
||||
"balance": "Guthaben",
|
||||
"balance_history_collecting": "Guthabenverlauf — Daten werden gesammelt...",
|
||||
"balance_layout": "Guthaben-Layout",
|
||||
@@ -114,6 +116,10 @@
|
||||
"bootstrap_wallet_protected": "(wallet.dat ist geschützt)",
|
||||
"bootstrap_warning": "Vorhandene Blockdaten (blocks, chainstate, notarizations) werden gelöscht und ersetzt. Ihre wallet.dat wird NICHT verändert oder gelöscht.",
|
||||
"cancel": "Abbrechen",
|
||||
"change_pass_confirm": "Neue bestätigen:",
|
||||
"change_pass_current": "Aktuelle Passphrase:",
|
||||
"change_pass_new": "Neue Passphrase:",
|
||||
"change_pass_title": "Passphrase ändern",
|
||||
"characters": "Zeichen",
|
||||
"chat": "Chat",
|
||||
"chat_cancel": "Abbrechen",
|
||||
@@ -180,10 +186,18 @@
|
||||
"connected_peers": "Verbundene Peers",
|
||||
"connecting": "Verbinde...",
|
||||
"console": "Konsole",
|
||||
"console_accents": "Farbakzente",
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Automatisch scrollen",
|
||||
"console_available_commands": "Verfügbare Befehle:",
|
||||
"console_capturing_output": "Erfasse Daemon-Ausgabe...",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Steuerung",
|
||||
"console_cat_mining": "Mining",
|
||||
"console_cat_network": "Netzwerk",
|
||||
"console_cat_raw_transactions": "Rohtransaktionen",
|
||||
"console_cat_utility": "Dienstprogramme",
|
||||
"console_cat_wallet": "Wallet",
|
||||
"console_clear": "Leeren",
|
||||
"console_clear_console": "Konsole leeren",
|
||||
"console_cleared": "Konsole geleert",
|
||||
@@ -221,6 +235,20 @@
|
||||
"console_no_daemon": "Kein Daemon",
|
||||
"console_not_connected": "Fehler: Nicht mit Daemon verbunden",
|
||||
"console_quit_note": "'quit'/'exit' werden hier nicht benötigt — schließen Sie einfach das Fenster.",
|
||||
"console_ref_builds": "Ergibt",
|
||||
"console_ref_cancel": "Abbrechen",
|
||||
"console_ref_destructive": "Folgenreich",
|
||||
"console_ref_example": "Beispiel",
|
||||
"console_ref_insert": "In Konsole einfügen",
|
||||
"console_ref_insert_run": "Einfügen & ausführen",
|
||||
"console_ref_no_match": "Keine Befehle gefunden.",
|
||||
"console_ref_no_params": "Erwartet keine Parameter.",
|
||||
"console_ref_optional": "optional",
|
||||
"console_ref_parameters": "Parameter",
|
||||
"console_ref_run": "Ausführen",
|
||||
"console_ref_run_confirm": "%s jetzt ausführen? Dies ist ein folgenreicher Befehl.",
|
||||
"console_ref_search_hint": "Nach Name oder Aufgabe suchen…",
|
||||
"console_ref_select_hint": "Wählen Sie einen Befehl, um zu sehen, was er tut.",
|
||||
"console_rpc_reference": "RPC-Befehlsreferenz",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Konsolen-Scanline",
|
||||
@@ -240,6 +268,9 @@
|
||||
"console_status_stopping": "Stoppt",
|
||||
"console_status_unknown": "Unbekannt",
|
||||
"console_tab_completion": "Tab zur Vervollständigung",
|
||||
"console_text_colors": "Textfarben",
|
||||
"console_toggle_accents": "Farbakzente der Zeilen umschalten",
|
||||
"console_toggle_text_color": "Textfarben der Zeilen umschalten",
|
||||
"console_type_help": "Geben Sie 'help' ein für verfügbare Befehle",
|
||||
"console_welcome": "Willkommen bei ObsidianDragon Konsole",
|
||||
"console_zoom_in": "Vergrößern",
|
||||
@@ -302,6 +333,19 @@
|
||||
"date": "Datum",
|
||||
"date_label": "Datum:",
|
||||
"debug_logging": "FEHLERPROTOKOLLIERUNG",
|
||||
"decrypt_desc": "Die Wallet wird exportiert, der Daemon mit einer frischen, unverschlüsselten Wallet neu gestartet und alle Schlüssel werden erneut importiert. Je nach Wallet-Größe kann dies einige Minuten dauern.",
|
||||
"decrypt_error_title": "Entschlüsselung fehlgeschlagen",
|
||||
"decrypt_step_backup": "Verschlüsselte Wallet wird gesichert",
|
||||
"decrypt_step_export": "Wallet-Schlüssel werden exportiert",
|
||||
"decrypt_step_restart": "Daemon wird neu gestartet",
|
||||
"decrypt_step_stop": "Daemon wird gestoppt",
|
||||
"decrypt_step_unlock": "Wallet wird entsperrt",
|
||||
"decrypt_success_desc": "Ihre Wallet ist jetzt unverschlüsselt. Eine Sicherung der verschlüsselten Wallet wurde als wallet.dat.encrypted.bak in Ihrem Datenverzeichnis gespeichert.",
|
||||
"decrypt_success_title": "Wallet erfolgreich entschlüsselt!",
|
||||
"decrypt_title": "Wallet-Verschlüsselung entfernen",
|
||||
"decrypt_wait_general": "Bitte warten. Der Daemon exportiert Schlüssel, startet neu und importiert erneut. Dies kann einige Minuten dauern.",
|
||||
"decrypt_wait_restart": "Warten, bis der Daemon vollständig gestartet ist...",
|
||||
"decrypt_warning": "Dadurch wird die Verschlüsselung Ihrer Wallet entfernt. Ihre privaten Schlüssel werden ungeschützt auf der Festplatte gespeichert.",
|
||||
"delete": "Löschen",
|
||||
"delete_blockchain": "Blockchain löschen",
|
||||
"delete_blockchain_confirm": "Löschen & Neu synchronisieren",
|
||||
@@ -314,6 +358,15 @@
|
||||
"download_bootstrap": "Bootstrap herunterladen",
|
||||
"dragonx_green": "DragonX (Grün)",
|
||||
"edit": "Bearbeiten",
|
||||
"enc_confirm": "Bestätigen:",
|
||||
"enc_desc": "Die Verschlüsselung Ihrer Wallet schützt Ihre privaten Schlüssel mit einer Passphrase. Nach der Verschlüsselung wird der Daemon neu gestartet.",
|
||||
"enc_encrypting": "Wallet wird verschlüsselt...",
|
||||
"enc_pin_desc": "Mit einer 4-8-stelligen PIN entsperren Sie Ihre Wallet, ohne jedes Mal die vollständige Passphrase einzugeben.",
|
||||
"enc_pin_set_ok": "PIN erfolgreich festgelegt",
|
||||
"enc_pin_skipped": "PIN übersprungen. Sie können später in den Einstellungen eine festlegen.",
|
||||
"enc_pin_vault_fail": "PIN-Tresor konnte nicht erstellt werden",
|
||||
"enc_success": "Wallet erfolgreich verschlüsselt!",
|
||||
"enc_wait": "Bitte warten, schließen Sie die Anwendung nicht.",
|
||||
"error": "Fehler",
|
||||
"error_format": "Fehler: %s",
|
||||
"est_time_to_block": "Gesch. Zeit bis Block",
|
||||
@@ -346,10 +399,18 @@
|
||||
"export_keys_danger": "ACHTUNG: Dies exportiert ALLE privaten Schlüssel aus Ihrer Wallet! Jeder mit Zugriff auf diese Datei kann Ihre Gelder stehlen. Sicher aufbewahren und nach Gebrauch löschen.",
|
||||
"export_keys_include_t": "T-Adressen einschließen (transparent)",
|
||||
"export_keys_include_z": "Z-Adressen einschließen (abgeschirmt)",
|
||||
"export_keys_none_addrs": "Keine Adressen zum Exportieren",
|
||||
"export_keys_none_result": "Keine Schlüssel exportiert (0 von %d) — entsperren Sie die Wallet und versuchen Sie es erneut.",
|
||||
"export_keys_none_toast": "Es konnten keine Schlüssel exportiert werden — ist die Wallet entsperrt?",
|
||||
"export_keys_not_connected": "Nicht mit dem Daemon verbunden",
|
||||
"export_keys_options": "Export-Optionen:",
|
||||
"export_keys_partial": "%d von %d Schlüsseln exportiert — unvollständig (einige hatten keinen Ausgabeschlüssel oder die Wallet ist gesperrt).",
|
||||
"export_keys_partial_toast": "Teilexport: %d von %d Schlüsseln",
|
||||
"export_keys_progress": "Exportiere %d/%d...",
|
||||
"export_keys_select_type": "Wählen Sie mindestens einen Adresstyp",
|
||||
"export_keys_success": "Schlüssel erfolgreich exportiert",
|
||||
"export_keys_title": "Alle privaten Schlüssel exportieren",
|
||||
"export_keys_write_fail": "Schlüsseldatei konnte nicht geschrieben werden.",
|
||||
"export_private_key": "Privaten Schlüssel exportieren",
|
||||
"export_tx_count": "%zu Transaktionen als CSV exportieren.",
|
||||
"export_tx_file_fail": "CSV-Datei konnte nicht erstellt werden",
|
||||
@@ -391,21 +452,42 @@
|
||||
"history": "Verlauf",
|
||||
"immature_type": "Unreif",
|
||||
"import": "Importieren",
|
||||
"import_key_address": "Adresse:",
|
||||
"import_key_btn": "Schlüssel importieren",
|
||||
"import_key_done": "Importiert. Wallet wird neu gescannt.",
|
||||
"import_key_field": "Schlüssel",
|
||||
"import_key_formats": "Unterstützte Schlüsselformate:",
|
||||
"import_key_full_rescan": "(0 = vollständiger Rescan)",
|
||||
"import_key_import": "Importieren",
|
||||
"import_key_label": "Privater Schlüssel:",
|
||||
"import_key_need_node": "Verbinden Sie einen laufenden Node, um einen Schlüssel zu importieren.",
|
||||
"import_key_no_valid": "Keine gültigen Schlüssel in der Eingabe gefunden",
|
||||
"import_key_progress": "Importiere %d/%d...",
|
||||
"import_key_rescan": "Blockchain nach Import neu scannen",
|
||||
"import_key_rescanning": "Import & erneuter Scan — das kann einige Minuten dauern",
|
||||
"import_key_reveal_tip": "Schlüssel ein-/ausblenden",
|
||||
"import_key_start_height": "Starthöhe:",
|
||||
"import_key_success": "Schlüssel erfolgreich importiert",
|
||||
"import_key_t_format": "T-Adresse WIF private Schlüssel",
|
||||
"import_key_title": "Privaten Schlüssel importieren",
|
||||
"import_key_tooltip": "Geben Sie einen oder mehrere private Schlüssel ein, einen pro Zeile.\nUnterstützt sowohl z-Adresse als auch t-Adresse Schlüssel.\nZeilen die mit # beginnen werden als Kommentare behandelt.",
|
||||
"import_key_type_tkey": "Transparenter privater Schlüssel",
|
||||
"import_key_type_unknown": "Unbekanntes Schlüsselformat",
|
||||
"import_key_type_zspend": "Abgeschirmter Ausgabeschlüssel",
|
||||
"import_key_type_zview": "Abgeschirmter Anzeigeschlüssel (nur Lesen)",
|
||||
"import_key_warn": "Importieren Sie nur einen Schlüssel, der Ihnen gehört — er gewährt Zugriff auf dessen Guthaben.",
|
||||
"import_key_warning": "Warnung: Teilen Sie niemals Ihre privaten Schlüssel! Das Importieren von Schlüsseln aus nicht vertrauenswürdigen Quellen kann Ihr Wallet gefährden.",
|
||||
"import_key_wrong_type": "Das sieht nach einem Anzeigeschlüssel aus. Verwenden Sie stattdessen \"Anzeigeschlüssel importieren\".",
|
||||
"import_key_z_format": "Z-Adresse Ausgabeschlüssel (secret-extended-key-...)",
|
||||
"import_private_key": "Privaten Schlüssel importieren...",
|
||||
"import_scan_hint": "0 = von Anfang an neu scannen",
|
||||
"import_scan_label": "Ab Blockhöhe scannen (optional)",
|
||||
"import_scan_tip": "aktuelle Höhe",
|
||||
"import_scan_transparent": "Transparente Schlüssel scannen immer vollständig neu",
|
||||
"import_viewkey_field": "Anzeigeschlüssel",
|
||||
"import_viewkey_note": "Nur Lesen: Ein Anzeigeschlüssel zeigt Guthaben und Transaktionen einer Adresse, kann deren Mittel aber nicht ausgeben.",
|
||||
"import_viewkey_title": "Anzeigeschlüssel importieren",
|
||||
"import_viewkey_wrong_type": "Das sieht nach einem Ausgabeschlüssel aus. Verwenden Sie stattdessen \"Privaten Schlüssel importieren\".",
|
||||
"incorrect_passphrase": "Falsches Passwort",
|
||||
"incorrect_pin": "Falsche PIN",
|
||||
"insufficient_funds": "Unzureichendes Guthaben für diesen Betrag plus Gebühr.",
|
||||
@@ -414,6 +496,7 @@
|
||||
"keep": "Behalten",
|
||||
"keep_daemon": "Daemon weiterlaufen lassen",
|
||||
"key_export_click_retrieve": "Klicken Sie, um den Schlüssel aus Ihrer Wallet abzurufen",
|
||||
"key_export_failed": "Schlüssel konnte nicht exportiert werden — entsperren Sie die Wallet (falls verschlüsselt) und versuchen Sie es erneut.",
|
||||
"key_export_fetching": "Schlüssel wird aus Wallet abgerufen...",
|
||||
"key_export_private_key": "Privater Schlüssel:",
|
||||
"key_export_private_warning": "Halten Sie diesen Schlüssel GEHEIM! Jeder mit diesem Schlüssel kann Ihre Gelder ausgeben. Teilen Sie ihn niemals online oder mit nicht vertrauenswürdigen Parteien.",
|
||||
@@ -739,6 +822,7 @@
|
||||
"output_filename": "Ausgabedateiname:",
|
||||
"overview": "Übersicht",
|
||||
"paste": "Einfügen",
|
||||
"paste_clip_empty": "Zwischenablage ist leer",
|
||||
"paste_from_clipboard": "Aus Zwischenablage einfügen",
|
||||
"pay_from": "Zahlen von",
|
||||
"payment_request": "ZAHLUNGSANFRAGE",
|
||||
@@ -790,7 +874,14 @@
|
||||
"peers_upper": "PEERS",
|
||||
"peers_version": "Version",
|
||||
"pending": "Ausstehend",
|
||||
"pin_change_desc": "Ändern Sie Ihre Entsperr-PIN. Sie benötigen Ihre aktuelle PIN und eine neue PIN.",
|
||||
"pin_confirm_new_label": "Neue PIN bestätigen:",
|
||||
"pin_current_label": "Aktuelle PIN:",
|
||||
"pin_new_label": "Neue PIN (4-8 Ziffern):",
|
||||
"pin_not_set": "PIN nicht gesetzt. Verwenden Sie das Passwort zum Entsperren.",
|
||||
"pin_remove_desc": "Geben Sie Ihre aktuelle PIN ein, um das Entfernen zu bestätigen. Sie benötigen dann Ihre vollständige Passphrase zum Entsperren.",
|
||||
"pin_setup_desc": "Legen Sie eine 4-8-stellige PIN für die schnelle Wallet-Entsperrung fest. Ihre Wallet-Passphrase wird mit dieser PIN verschlüsselt und lokal gespeichert.",
|
||||
"pin_wallet_passphrase": "Wallet-Passphrase:",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "Eintrag hinzufügen",
|
||||
"portfolio_add_to": "Zum Portfolio hinzufügen",
|
||||
@@ -829,6 +920,9 @@
|
||||
"portfolio_remove_from": "Aus Portfolio entfernen",
|
||||
"portfolio_revert": "Zurücksetzen",
|
||||
"portfolio_save": "Speichern",
|
||||
"portfolio_save_need_address": "Fügen Sie mindestens eine Adresse hinzu, um zu speichern.",
|
||||
"portfolio_save_need_name": "Geben Sie einen Namen ein, um diese Gruppe zu speichern.",
|
||||
"portfolio_save_need_price": "Geben Sie einen manuellen Preis über 0 ein, um zu speichern.",
|
||||
"portfolio_search": "Adressen durchsuchen…",
|
||||
"portfolio_search_icons": "Symbole suchen…",
|
||||
"portfolio_select_all": "Alle",
|
||||
@@ -1064,7 +1158,8 @@
|
||||
"settings_gradient_bg": "Hintergrund-Verlauf",
|
||||
"settings_gradient_desc": "Strukturierte Hintergründe durch sanfte Verläufe ersetzen",
|
||||
"settings_idle_after": "nach",
|
||||
"settings_import_key": "Schlüssel importieren...",
|
||||
"settings_import_key": "Privaten Schlüssel importieren...",
|
||||
"settings_import_viewkey": "Anzeigeschlüssel importieren...",
|
||||
"settings_language_note": "Hinweis: Manche Texte erfordern einen Neustart zur Aktualisierung",
|
||||
"settings_lock_now": "Jetzt sperren",
|
||||
"settings_locked": "Gesperrt",
|
||||
@@ -1151,6 +1246,14 @@
|
||||
"submitting_transaction": "Transaktion wird übermittelt...",
|
||||
"success": "Erfolg",
|
||||
"summary": "Zusammenfassung",
|
||||
"sweep_button": "Fegen",
|
||||
"sweep_caveat": "Importiert den Schlüssel, um eine Transaktion zu signieren, die alle Mittel an Ihre Adresse verschiebt. Der Schlüssel bleibt mit leerem Guthaben in Ihrer Wallet.",
|
||||
"sweep_dest_label": "Gefegte Mittel senden an",
|
||||
"sweep_dest_new": "Neue abgeschirmte Adresse (empfohlen)",
|
||||
"sweep_done": "Fertig — Mittel an Ihre Adresse gefegt.",
|
||||
"sweep_to": "Gefegt an:",
|
||||
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
|
||||
"sweep_tx": "Transaktion:",
|
||||
"syncing": "Synchronisiere...",
|
||||
"t_address": "T-Adresse",
|
||||
"t_addresses": "T-Adressen",
|
||||
@@ -1186,6 +1289,7 @@
|
||||
"transfer_to": "Überweisen an:",
|
||||
"transparent": "Transparent",
|
||||
"transparent_address": "Transparente Adresse",
|
||||
"try_again": "Erneut versuchen",
|
||||
"tt_addr_url": "Basis-URL zum Anzeigen von Adressen in einem Block-Explorer",
|
||||
"tt_address_book": "Gespeicherte Adressen für schnelles Senden verwalten",
|
||||
"tt_auto_lock": "Wallet nach dieser Inaktivitätszeit sperren",
|
||||
@@ -1212,6 +1316,7 @@
|
||||
"tt_font_scale": "Alle Texte und UI skalieren (1.0x = Standard, bis 1.5x).",
|
||||
"tt_idle_delay": "Wie lange vor dem Start des Minings gewartet werden soll",
|
||||
"tt_import_key": "Einen privaten Schlüssel (zkey oder tkey) in diese Wallet importieren",
|
||||
"tt_import_viewkey": "Einen abgeschirmten Anzeigeschlüssel importieren, um eine Adresse anzusehen (nur Lesen)",
|
||||
"tt_keep_daemon": "Der Daemon wird beim Ausführen des Einrichtungsassistenten gestoppt",
|
||||
"tt_language": "Schnittstellensprache der Wallet-UI",
|
||||
"tt_layout_hotkey": "Hotkey: Links-/Rechts-Pfeiltasten zum Wechseln der Balance-Layouts",
|
||||
|
||||
107
res/lang/es.json
107
res/lang/es.json
@@ -62,6 +62,7 @@
|
||||
"backup_data": "RESPALDO Y DATOS",
|
||||
"backup_description": "Crea un respaldo de tu archivo wallet.dat. Este archivo contiene todas tus claves privadas e historial de transacciones. Guarda el respaldo en un lugar seguro.",
|
||||
"backup_destination": "Destino del respaldo:",
|
||||
"backup_overwrite_confirm": "Ya existe un archivo ahí: guarda de nuevo para sobrescribirlo.",
|
||||
"backup_source": "Origen: %s",
|
||||
"backup_tip_external": "Guarda respaldos en unidades externas o almacenamiento en la nube",
|
||||
"backup_tip_multiple": "Crea múltiples respaldos en diferentes ubicaciones",
|
||||
@@ -70,6 +71,7 @@
|
||||
"backup_title": "Respaldar Cartera",
|
||||
"backup_wallet": "Respaldar Cartera...",
|
||||
"backup_wallet_not_found": "Advertencia: wallet.dat no encontrado en la ubicación esperada",
|
||||
"backup_warn": "Este archivo contiene todas tus claves privadas: guárdalo en un lugar seguro.",
|
||||
"balance": "Saldo",
|
||||
"balance_history_collecting": "Historial de saldo — recopilando datos...",
|
||||
"balance_layout": "Diseño de Saldo",
|
||||
@@ -114,6 +116,10 @@
|
||||
"bootstrap_wallet_protected": "(wallet.dat está protegido)",
|
||||
"bootstrap_warning": "Los datos de bloques existentes (blocks, chainstate, notarizations) se eliminarán y reemplazarán. Su wallet.dat NO será modificado ni eliminado.",
|
||||
"cancel": "Cancelar",
|
||||
"change_pass_confirm": "Confirmar nueva:",
|
||||
"change_pass_current": "Frase de contraseña actual:",
|
||||
"change_pass_new": "Nueva frase de contraseña:",
|
||||
"change_pass_title": "Cambiar frase de contraseña",
|
||||
"characters": "caracteres",
|
||||
"chat": "Chat",
|
||||
"chat_cancel": "Cancelar",
|
||||
@@ -180,10 +186,18 @@
|
||||
"connected_peers": "Nodos Conectados",
|
||||
"connecting": "Conectando...",
|
||||
"console": "Consola",
|
||||
"console_accents": "Acentos de color",
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Auto-desplazamiento",
|
||||
"console_available_commands": "Comandos disponibles:",
|
||||
"console_capturing_output": "Capturando salida del daemon...",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Control",
|
||||
"console_cat_mining": "Minería",
|
||||
"console_cat_network": "Red",
|
||||
"console_cat_raw_transactions": "Transacciones sin procesar",
|
||||
"console_cat_utility": "Utilidades",
|
||||
"console_cat_wallet": "Cartera",
|
||||
"console_clear": "Limpiar",
|
||||
"console_clear_console": "Limpiar Consola",
|
||||
"console_cleared": "Consola limpiada",
|
||||
@@ -221,6 +235,20 @@
|
||||
"console_no_daemon": "Sin daemon",
|
||||
"console_not_connected": "Error: No conectado al daemon",
|
||||
"console_quit_note": "'quit'/'exit' no son necesarios aquí — simplemente cierra la ventana.",
|
||||
"console_ref_builds": "Genera",
|
||||
"console_ref_cancel": "Cancelar",
|
||||
"console_ref_destructive": "Delicado",
|
||||
"console_ref_example": "Ejemplo",
|
||||
"console_ref_insert": "Insertar en la consola",
|
||||
"console_ref_insert_run": "Insertar y ejecutar",
|
||||
"console_ref_no_match": "Ningún comando coincide.",
|
||||
"console_ref_no_params": "No requiere parámetros.",
|
||||
"console_ref_optional": "opcional",
|
||||
"console_ref_parameters": "Parámetros",
|
||||
"console_ref_run": "Ejecutar",
|
||||
"console_ref_run_confirm": "¿Ejecutar %s ahora? Es un comando con consecuencias.",
|
||||
"console_ref_search_hint": "Buscar por nombre o tarea…",
|
||||
"console_ref_select_hint": "Selecciona un comando para ver qué hace.",
|
||||
"console_rpc_reference": "Referencia de Comandos RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Líneas de consola",
|
||||
@@ -240,6 +268,9 @@
|
||||
"console_status_stopping": "Deteniendo",
|
||||
"console_status_unknown": "Desconocido",
|
||||
"console_tab_completion": "Tab para completar",
|
||||
"console_text_colors": "Colores de texto",
|
||||
"console_toggle_accents": "Alternar acentos de color de línea",
|
||||
"console_toggle_text_color": "Alternar colores de texto de línea",
|
||||
"console_type_help": "Escribe 'help' para ver los comandos disponibles",
|
||||
"console_welcome": "Bienvenido a la Consola de ObsidianDragon",
|
||||
"console_zoom_in": "Acercar",
|
||||
@@ -302,6 +333,19 @@
|
||||
"date": "Fecha",
|
||||
"date_label": "Fecha:",
|
||||
"debug_logging": "REGISTRO DE DEPURACIÓN",
|
||||
"decrypt_desc": "Se exportará el monedero, se reiniciará el daemon con un monedero nuevo sin cifrar y se reimportarán todas las claves. Esto puede tardar varios minutos según el tamaño del monedero.",
|
||||
"decrypt_error_title": "Error al descifrar",
|
||||
"decrypt_step_backup": "Respaldando el monedero cifrado",
|
||||
"decrypt_step_export": "Exportando las claves del monedero",
|
||||
"decrypt_step_restart": "Reiniciando el daemon",
|
||||
"decrypt_step_stop": "Deteniendo el daemon",
|
||||
"decrypt_step_unlock": "Desbloqueando el monedero",
|
||||
"decrypt_success_desc": "Tu monedero ahora está sin cifrar. Se guardó una copia de seguridad del monedero cifrado como wallet.dat.encrypted.bak en tu directorio de datos.",
|
||||
"decrypt_success_title": "¡Monedero descifrado correctamente!",
|
||||
"decrypt_title": "Quitar el cifrado del monedero",
|
||||
"decrypt_wait_general": "Espera. El daemon está exportando claves, reiniciando y reimportando. Esto puede tardar varios minutos.",
|
||||
"decrypt_wait_restart": "Esperando a que el daemon termine de iniciarse...",
|
||||
"decrypt_warning": "Esto quitará el cifrado de tu monedero. Tus claves privadas se almacenarán sin protección en el disco.",
|
||||
"delete": "Eliminar",
|
||||
"delete_blockchain": "Eliminar Blockchain",
|
||||
"delete_blockchain_confirm": "Eliminar y Resincronizar",
|
||||
@@ -314,6 +358,15 @@
|
||||
"download_bootstrap": "Descargar Bootstrap",
|
||||
"dragonx_green": "DragonX (Verde)",
|
||||
"edit": "Editar",
|
||||
"enc_confirm": "Confirmar:",
|
||||
"enc_desc": "Cifrar tu monedero protege tus claves privadas con una frase de contraseña. Tras el cifrado, el daemon se reiniciará.",
|
||||
"enc_encrypting": "Cifrando el monedero...",
|
||||
"enc_pin_desc": "Un PIN de 4-8 dígitos te permite desbloquear tu monedero sin escribir la frase de contraseña completa cada vez.",
|
||||
"enc_pin_set_ok": "PIN configurado correctamente",
|
||||
"enc_pin_skipped": "PIN omitido. Puedes configurar uno más tarde en Ajustes.",
|
||||
"enc_pin_vault_fail": "No se pudo crear la bóveda del PIN",
|
||||
"enc_success": "¡Monedero cifrado correctamente!",
|
||||
"enc_wait": "Espera, no cierres la aplicación.",
|
||||
"error": "Error",
|
||||
"error_format": "Error: %s",
|
||||
"est_time_to_block": "Tiempo Est. al Bloque",
|
||||
@@ -346,10 +399,18 @@
|
||||
"export_keys_danger": "PELIGRO: ¡Esto exportará TODAS las claves privadas de tu cartera! Cualquiera con acceso a este archivo puede robar tus fondos. Guárdalo de forma segura y elimínalo después de usar.",
|
||||
"export_keys_include_t": "Incluir direcciones T (transparentes)",
|
||||
"export_keys_include_z": "Incluir direcciones Z (protegidas)",
|
||||
"export_keys_none_addrs": "No hay direcciones para exportar",
|
||||
"export_keys_none_result": "No se exportaron claves (0 de %d): desbloquea el monedero e inténtalo de nuevo.",
|
||||
"export_keys_none_toast": "No se pudieron exportar claves: ¿está desbloqueado el monedero?",
|
||||
"export_keys_not_connected": "Sin conexión con el daemon",
|
||||
"export_keys_options": "Opciones de exportación:",
|
||||
"export_keys_partial": "Se exportaron %d de %d claves: incompleto (algunas no tenían clave de gasto o el monedero está bloqueado).",
|
||||
"export_keys_partial_toast": "Exportación parcial: %d de %d claves",
|
||||
"export_keys_progress": "Exportando %d/%d...",
|
||||
"export_keys_select_type": "Selecciona al menos un tipo de dirección",
|
||||
"export_keys_success": "Claves exportadas exitosamente",
|
||||
"export_keys_title": "Exportar Todas las Claves Privadas",
|
||||
"export_keys_write_fail": "No se pudo escribir el archivo de claves.",
|
||||
"export_private_key": "Exportar Clave Privada",
|
||||
"export_tx_count": "Exportar %zu transacciones a archivo CSV.",
|
||||
"export_tx_file_fail": "Error al crear archivo CSV",
|
||||
@@ -391,21 +452,42 @@
|
||||
"history": "Historial",
|
||||
"immature_type": "Inmaduro",
|
||||
"import": "Importar",
|
||||
"import_key_address": "Dirección:",
|
||||
"import_key_btn": "Importar Clave(s)",
|
||||
"import_key_done": "Importada. El monedero está reescaneando.",
|
||||
"import_key_field": "Clave",
|
||||
"import_key_formats": "Formatos de clave soportados:",
|
||||
"import_key_full_rescan": "(0 = re-escaneo completo)",
|
||||
"import_key_import": "Importar",
|
||||
"import_key_label": "Clave(s) Privada(s):",
|
||||
"import_key_need_node": "Conecta un nodo en ejecución para importar una clave.",
|
||||
"import_key_no_valid": "No se encontraron claves válidas en la entrada",
|
||||
"import_key_progress": "Importando %d/%d...",
|
||||
"import_key_rescan": "Re-escanear blockchain después de importar",
|
||||
"import_key_rescanning": "Importando y reescaneando: puede tardar varios minutos",
|
||||
"import_key_reveal_tip": "Mostrar/ocultar la clave",
|
||||
"import_key_start_height": "Altura inicial:",
|
||||
"import_key_success": "Claves importadas exitosamente",
|
||||
"import_key_t_format": "Claves privadas WIF de direcciones T",
|
||||
"import_key_title": "Importar Clave Privada",
|
||||
"import_key_tooltip": "Ingresa una o más claves privadas, una por línea.\nSoporta claves de direcciones z y t.\nLas líneas que empiezan con # se tratan como comentarios.",
|
||||
"import_key_type_tkey": "Clave privada transparente",
|
||||
"import_key_type_unknown": "Formato de clave no reconocido",
|
||||
"import_key_type_zspend": "Clave de gasto blindada",
|
||||
"import_key_type_zview": "Clave de visualización blindada (solo lectura)",
|
||||
"import_key_warn": "Importa solo una clave que te pertenezca: da acceso a sus fondos.",
|
||||
"import_key_warning": "Advertencia: ¡Nunca compartas tus claves privadas! Importar claves de fuentes no confiables puede comprometer tu cartera.",
|
||||
"import_key_wrong_type": "Esto parece una clave de visualización. Usa \"Importar clave de visualización\" en su lugar.",
|
||||
"import_key_z_format": "Claves de gasto de direcciones Z (secret-extended-key-...)",
|
||||
"import_private_key": "Importar Clave Privada...",
|
||||
"import_scan_hint": "0 = volver a escanear desde el inicio",
|
||||
"import_scan_label": "Escanear desde la altura de bloque (opcional)",
|
||||
"import_scan_tip": "altura actual",
|
||||
"import_scan_transparent": "Las claves transparentes siempre se reescanean por completo",
|
||||
"import_viewkey_field": "Clave de visualización",
|
||||
"import_viewkey_note": "Solo lectura: una clave de visualización revela el saldo y las transacciones de una dirección, pero no puede gastar sus fondos.",
|
||||
"import_viewkey_title": "Importar clave de visualización",
|
||||
"import_viewkey_wrong_type": "Esto parece una clave de gasto. Usa \"Importar Clave Privada\" en su lugar.",
|
||||
"incorrect_passphrase": "Contraseña incorrecta",
|
||||
"incorrect_pin": "PIN incorrecto",
|
||||
"insufficient_funds": "Fondos insuficientes para este monto más la comisión.",
|
||||
@@ -414,6 +496,7 @@
|
||||
"keep": "Mantener",
|
||||
"keep_daemon": "Mantener daemon activo",
|
||||
"key_export_click_retrieve": "Haga clic para recuperar la clave de su billetera",
|
||||
"key_export_failed": "No se pudo exportar la clave: desbloquea el monedero (si está cifrado) e inténtalo de nuevo.",
|
||||
"key_export_fetching": "Obteniendo clave de la cartera...",
|
||||
"key_export_private_key": "Clave Privada:",
|
||||
"key_export_private_warning": "¡Mantén esta clave en SECRETO! Cualquiera con esta clave puede gastar tus fondos. Nunca la compartas en línea ni con personas no confiables.",
|
||||
@@ -739,6 +822,7 @@
|
||||
"output_filename": "Nombre del archivo:",
|
||||
"overview": "Resumen",
|
||||
"paste": "Pegar",
|
||||
"paste_clip_empty": "El portapapeles está vacío",
|
||||
"paste_from_clipboard": "Pegar del Portapapeles",
|
||||
"pay_from": "Pagar desde",
|
||||
"payment_request": "SOLICITUD DE PAGO",
|
||||
@@ -790,7 +874,14 @@
|
||||
"peers_upper": "NODOS",
|
||||
"peers_version": "Versión",
|
||||
"pending": "Pendiente",
|
||||
"pin_change_desc": "Cambia tu PIN de desbloqueo. Necesitas tu PIN actual y un nuevo PIN.",
|
||||
"pin_confirm_new_label": "Confirmar nuevo PIN:",
|
||||
"pin_current_label": "PIN actual:",
|
||||
"pin_new_label": "Nuevo PIN (4-8 dígitos):",
|
||||
"pin_not_set": "PIN no configurado. Use la contraseña para desbloquear.",
|
||||
"pin_remove_desc": "Introduce tu PIN actual para confirmar la eliminación. Necesitarás tu frase de contraseña completa para desbloquear.",
|
||||
"pin_setup_desc": "Establece un PIN de 4-8 dígitos para desbloquear el monedero rápidamente. Tu frase de contraseña se cifrará con este PIN y se almacenará localmente.",
|
||||
"pin_wallet_passphrase": "Frase de contraseña del monedero:",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "Añadir entrada",
|
||||
"portfolio_add_to": "Agregar a la cartera de inversiones",
|
||||
@@ -829,6 +920,9 @@
|
||||
"portfolio_remove_from": "Quitar del portafolio",
|
||||
"portfolio_revert": "Revertir",
|
||||
"portfolio_save": "Guardar",
|
||||
"portfolio_save_need_address": "Añade al menos una dirección para guardar.",
|
||||
"portfolio_save_need_name": "Introduce un nombre para guardar este grupo.",
|
||||
"portfolio_save_need_price": "Introduce un precio manual mayor que 0 para guardar.",
|
||||
"portfolio_search": "Buscar direcciones…",
|
||||
"portfolio_search_icons": "Buscar iconos…",
|
||||
"portfolio_select_all": "Todos",
|
||||
@@ -1064,7 +1158,8 @@
|
||||
"settings_gradient_bg": "Fondo degradado",
|
||||
"settings_gradient_desc": "Reemplazar fondos con texturas por degradados suaves",
|
||||
"settings_idle_after": "después de",
|
||||
"settings_import_key": "Importar clave...",
|
||||
"settings_import_key": "Importar Clave Privada...",
|
||||
"settings_import_viewkey": "Importar clave de visualización...",
|
||||
"settings_language_note": "Nota: Parte del texto requiere reinicio para actualizarse",
|
||||
"settings_lock_now": "Bloquear ahora",
|
||||
"settings_locked": "Bloqueado",
|
||||
@@ -1151,6 +1246,14 @@
|
||||
"submitting_transaction": "Enviando transacción...",
|
||||
"success": "Éxito",
|
||||
"summary": "Resumen",
|
||||
"sweep_button": "Barrer",
|
||||
"sweep_caveat": "Importa la clave para firmar una transacción que mueve todos sus fondos a tu dirección. La clave permanece en tu monedero con saldo cero.",
|
||||
"sweep_dest_label": "Enviar los fondos barridos a",
|
||||
"sweep_dest_new": "Nueva dirección blindada (recomendado)",
|
||||
"sweep_done": "Listo — fondos barridos a tu dirección.",
|
||||
"sweep_to": "Barrido a:",
|
||||
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
|
||||
"sweep_tx": "Transacción:",
|
||||
"syncing": "Sincronizando...",
|
||||
"t_address": "Dirección T",
|
||||
"t_addresses": "Direcciones T",
|
||||
@@ -1186,6 +1289,7 @@
|
||||
"transfer_to": "Transferir a:",
|
||||
"transparent": "Transparente",
|
||||
"transparent_address": "Dirección Transparente",
|
||||
"try_again": "Reintentar",
|
||||
"tt_addr_url": "URL base para ver direcciones en un explorador de bloques",
|
||||
"tt_address_book": "Administrar direcciones guardadas para envío rápido",
|
||||
"tt_auto_lock": "Bloquear billetera después de este tiempo de inactividad",
|
||||
@@ -1212,6 +1316,7 @@
|
||||
"tt_font_scale": "Escalar todo el texto y la interfaz (1.0x = predeterminado, hasta 1.5x).",
|
||||
"tt_idle_delay": "Cuánto tiempo esperar antes de empezar a minar",
|
||||
"tt_import_key": "Importar una clave privada (zkey o tkey) en esta billetera",
|
||||
"tt_import_viewkey": "Importar una clave de visualización blindada para observar una dirección (solo lectura)",
|
||||
"tt_keep_daemon": "El daemon se detendrá cuando ejecute el asistente de configuración",
|
||||
"tt_language": "Idioma de la interfaz de la billetera",
|
||||
"tt_layout_hotkey": "Atajo: teclas de flecha izquierda/derecha para cambiar diseños de Balance",
|
||||
|
||||
107
res/lang/fr.json
107
res/lang/fr.json
@@ -62,6 +62,7 @@
|
||||
"backup_data": "SAUVEGARDE & DONNÉES",
|
||||
"backup_description": "Créez une sauvegarde de votre fichier wallet.dat. Ce fichier contient toutes vos clés privées et l'historique des transactions. Conservez la sauvegarde dans un endroit sûr.",
|
||||
"backup_destination": "Destination de sauvegarde :",
|
||||
"backup_overwrite_confirm": "Un fichier existe déjà là — enregistrez à nouveau pour l'écraser.",
|
||||
"backup_source": "Source : %s",
|
||||
"backup_tip_external": "Stockez les sauvegardes sur des disques externes ou un stockage cloud",
|
||||
"backup_tip_multiple": "Créez plusieurs sauvegardes à différents endroits",
|
||||
@@ -70,6 +71,7 @@
|
||||
"backup_title": "Sauvegarder le portefeuille",
|
||||
"backup_wallet": "Sauvegarder le portefeuille...",
|
||||
"backup_wallet_not_found": "Attention : wallet.dat introuvable à l'emplacement prévu",
|
||||
"backup_warn": "Ce fichier contient toutes vos clés privées — conservez-le en lieu sûr.",
|
||||
"balance": "Solde",
|
||||
"balance_history_collecting": "Historique du solde — collecte des données...",
|
||||
"balance_layout": "Disposition du solde",
|
||||
@@ -114,6 +116,10 @@
|
||||
"bootstrap_wallet_protected": "(wallet.dat est protégé)",
|
||||
"bootstrap_warning": "Les données de blocs existantes (blocks, chainstate, notarizations) seront supprimées et remplacées. Votre wallet.dat ne sera PAS modifié ni supprimé.",
|
||||
"cancel": "Annuler",
|
||||
"change_pass_confirm": "Confirmer la nouvelle :",
|
||||
"change_pass_current": "Phrase secrète actuelle :",
|
||||
"change_pass_new": "Nouvelle phrase secrète :",
|
||||
"change_pass_title": "Changer la phrase secrète",
|
||||
"characters": "caractères",
|
||||
"chat": "Discussion",
|
||||
"chat_cancel": "Annuler",
|
||||
@@ -180,10 +186,18 @@
|
||||
"connected_peers": "Pairs connectés",
|
||||
"connecting": "Connexion...",
|
||||
"console": "Console",
|
||||
"console_accents": "Accents de couleur",
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Défilement auto",
|
||||
"console_available_commands": "Commandes disponibles :",
|
||||
"console_capturing_output": "Capture de la sortie du daemon...",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Contrôle",
|
||||
"console_cat_mining": "Minage",
|
||||
"console_cat_network": "Réseau",
|
||||
"console_cat_raw_transactions": "Transactions brutes",
|
||||
"console_cat_utility": "Utilitaires",
|
||||
"console_cat_wallet": "Portefeuille",
|
||||
"console_clear": "Effacer",
|
||||
"console_clear_console": "Effacer la console",
|
||||
"console_cleared": "Console effacée",
|
||||
@@ -221,6 +235,20 @@
|
||||
"console_no_daemon": "Pas de daemon",
|
||||
"console_not_connected": "Erreur : Non connecté au daemon",
|
||||
"console_quit_note": "'quit'/'exit' ne sont pas nécessaires ici — fermez simplement la fenêtre.",
|
||||
"console_ref_builds": "Génère",
|
||||
"console_ref_cancel": "Annuler",
|
||||
"console_ref_destructive": "Sensible",
|
||||
"console_ref_example": "Exemple",
|
||||
"console_ref_insert": "Insérer dans la console",
|
||||
"console_ref_insert_run": "Insérer et exécuter",
|
||||
"console_ref_no_match": "Aucune commande ne correspond.",
|
||||
"console_ref_no_params": "Ne prend aucun paramètre.",
|
||||
"console_ref_optional": "optionnel",
|
||||
"console_ref_parameters": "Paramètres",
|
||||
"console_ref_run": "Exécuter",
|
||||
"console_ref_run_confirm": "Exécuter %s maintenant ? C'est une commande à conséquences.",
|
||||
"console_ref_search_hint": "Rechercher par nom ou tâche…",
|
||||
"console_ref_select_hint": "Sélectionnez une commande pour voir ce qu'elle fait.",
|
||||
"console_rpc_reference": "Référence des commandes RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Scanline de la console",
|
||||
@@ -240,6 +268,9 @@
|
||||
"console_status_stopping": "Arrêt",
|
||||
"console_status_unknown": "Inconnu",
|
||||
"console_tab_completion": "Tab pour compléter",
|
||||
"console_text_colors": "Couleurs du texte",
|
||||
"console_toggle_accents": "Basculer les accents de couleur des lignes",
|
||||
"console_toggle_text_color": "Basculer les couleurs du texte des lignes",
|
||||
"console_type_help": "Tapez 'help' pour les commandes disponibles",
|
||||
"console_welcome": "Bienvenue dans la console ObsidianDragon",
|
||||
"console_zoom_in": "Agrandir",
|
||||
@@ -302,6 +333,19 @@
|
||||
"date": "Date",
|
||||
"date_label": "Date :",
|
||||
"debug_logging": "JOURNALISATION DE DÉBOGAGE",
|
||||
"decrypt_desc": "Le portefeuille sera exporté, le daemon redémarré avec un nouveau portefeuille non chiffré, et toutes les clés réimportées. Cela peut prendre plusieurs minutes selon la taille du portefeuille.",
|
||||
"decrypt_error_title": "Échec du déchiffrement",
|
||||
"decrypt_step_backup": "Sauvegarde du portefeuille chiffré",
|
||||
"decrypt_step_export": "Exportation des clés du portefeuille",
|
||||
"decrypt_step_restart": "Redémarrage du daemon",
|
||||
"decrypt_step_stop": "Arrêt du daemon",
|
||||
"decrypt_step_unlock": "Déverrouillage du portefeuille",
|
||||
"decrypt_success_desc": "Votre portefeuille est maintenant non chiffré. Une sauvegarde du portefeuille chiffré a été enregistrée sous wallet.dat.encrypted.bak dans votre répertoire de données.",
|
||||
"decrypt_success_title": "Portefeuille déchiffré avec succès !",
|
||||
"decrypt_title": "Supprimer le chiffrement du portefeuille",
|
||||
"decrypt_wait_general": "Veuillez patienter. Le daemon exporte les clés, redémarre et réimporte. Cela peut prendre plusieurs minutes.",
|
||||
"decrypt_wait_restart": "En attente du démarrage complet du daemon...",
|
||||
"decrypt_warning": "Cela supprimera le chiffrement de votre portefeuille. Vos clés privées seront stockées sans protection sur le disque.",
|
||||
"delete": "Supprimer",
|
||||
"delete_blockchain": "Supprimer Blockchain",
|
||||
"delete_blockchain_confirm": "Supprimer & Resynchroniser",
|
||||
@@ -314,6 +358,15 @@
|
||||
"download_bootstrap": "Télécharger Bootstrap",
|
||||
"dragonx_green": "DragonX (Vert)",
|
||||
"edit": "Modifier",
|
||||
"enc_confirm": "Confirmer :",
|
||||
"enc_desc": "Chiffrer votre portefeuille protège vos clés privées avec une phrase secrète. Après le chiffrement, le daemon redémarrera.",
|
||||
"enc_encrypting": "Chiffrement du portefeuille...",
|
||||
"enc_pin_desc": "Un code PIN de 4 à 8 chiffres vous permet de déverrouiller votre portefeuille sans saisir la phrase secrète complète à chaque fois.",
|
||||
"enc_pin_set_ok": "Code PIN défini avec succès",
|
||||
"enc_pin_skipped": "Code PIN ignoré. Vous pourrez en définir un plus tard dans les Paramètres.",
|
||||
"enc_pin_vault_fail": "Échec de la création du coffre du code PIN",
|
||||
"enc_success": "Portefeuille chiffré avec succès !",
|
||||
"enc_wait": "Veuillez patienter, ne fermez pas l'application.",
|
||||
"error": "Erreur",
|
||||
"error_format": "Erreur : %s",
|
||||
"est_time_to_block": "Temps est. par bloc",
|
||||
@@ -346,10 +399,18 @@
|
||||
"export_keys_danger": "DANGER : Ceci exportera TOUTES les clés privées de votre portefeuille ! Toute personne ayant accès à ce fichier peut voler vos fonds. Conservez-le en sécurité et supprimez-le après utilisation.",
|
||||
"export_keys_include_t": "Inclure les adresses T (transparentes)",
|
||||
"export_keys_include_z": "Inclure les adresses Z (blindées)",
|
||||
"export_keys_none_addrs": "Aucune adresse à exporter",
|
||||
"export_keys_none_result": "Aucune clé exportée (0 sur %d) — déverrouillez le portefeuille et réessayez.",
|
||||
"export_keys_none_toast": "Aucune clé n'a pu être exportée — le portefeuille est-il déverrouillé ?",
|
||||
"export_keys_not_connected": "Non connecté au daemon",
|
||||
"export_keys_options": "Options d'exportation :",
|
||||
"export_keys_partial": "%d clés sur %d exportées — incomplet (certaines sans clé de dépense, ou le portefeuille est verrouillé).",
|
||||
"export_keys_partial_toast": "Export partiel : %d clés sur %d",
|
||||
"export_keys_progress": "Exportation %d/%d...",
|
||||
"export_keys_select_type": "Sélectionnez au moins un type d'adresse",
|
||||
"export_keys_success": "Clés exportées avec succès",
|
||||
"export_keys_title": "Exporter toutes les clés privées",
|
||||
"export_keys_write_fail": "Échec de l'écriture du fichier de clés.",
|
||||
"export_private_key": "Exporter la clé privée",
|
||||
"export_tx_count": "Exporter %zu transactions en fichier CSV.",
|
||||
"export_tx_file_fail": "Impossible de créer le fichier CSV",
|
||||
@@ -391,21 +452,42 @@
|
||||
"history": "Historique",
|
||||
"immature_type": "Immature",
|
||||
"import": "Importer",
|
||||
"import_key_address": "Adresse :",
|
||||
"import_key_btn": "Importer clé(s)",
|
||||
"import_key_done": "Importée. Le portefeuille réanalyse.",
|
||||
"import_key_field": "Clé",
|
||||
"import_key_formats": "Formats de clés pris en charge :",
|
||||
"import_key_full_rescan": "(0 = rescan complet)",
|
||||
"import_key_import": "Importer",
|
||||
"import_key_label": "Clé(s) privée(s) :",
|
||||
"import_key_need_node": "Connectez un nœud en cours d'exécution pour importer une clé.",
|
||||
"import_key_no_valid": "Aucune clé valide trouvée dans l'entrée",
|
||||
"import_key_progress": "Importation %d/%d...",
|
||||
"import_key_rescan": "Re-scanner la blockchain après l'importation",
|
||||
"import_key_rescanning": "Import et réanalyse — cela peut prendre plusieurs minutes",
|
||||
"import_key_reveal_tip": "Afficher/masquer la clé",
|
||||
"import_key_start_height": "Hauteur de départ :",
|
||||
"import_key_success": "Clés importées avec succès",
|
||||
"import_key_t_format": "Clés privées WIF d'adresses T",
|
||||
"import_key_title": "Importer une clé privée",
|
||||
"import_key_tooltip": "Entrez une ou plusieurs clés privées, une par ligne.\nPrend en charge les clés z-adresse et t-adresse.\nLes lignes commençant par # sont traitées comme des commentaires.",
|
||||
"import_key_type_tkey": "Clé privée transparente",
|
||||
"import_key_type_unknown": "Format de clé non reconnu",
|
||||
"import_key_type_zspend": "Clé de dépense blindée",
|
||||
"import_key_type_zview": "Clé de visualisation blindée (lecture seule)",
|
||||
"import_key_warn": "N'importez qu'une clé qui vous appartient — elle donne accès à ses fonds.",
|
||||
"import_key_warning": "Attention : Ne partagez jamais vos clés privées ! L'importation de clés provenant de sources non fiables peut compromettre votre portefeuille.",
|
||||
"import_key_wrong_type": "Ceci ressemble à une clé de visualisation. Utilisez plutôt « Importer la clé de visualisation ».",
|
||||
"import_key_z_format": "Clés de dépenses z-adresse (secret-extended-key-...)",
|
||||
"import_private_key": "Importer une clé privée...",
|
||||
"import_scan_hint": "0 = réanalyser depuis le début",
|
||||
"import_scan_label": "Analyser à partir de la hauteur de bloc (facultatif)",
|
||||
"import_scan_tip": "hauteur actuelle",
|
||||
"import_scan_transparent": "Les clés transparentes réanalysent toujours entièrement",
|
||||
"import_viewkey_field": "Clé de visualisation",
|
||||
"import_viewkey_note": "Lecture seule : une clé de visualisation révèle le solde et les transactions d'une adresse, mais ne peut pas dépenser ses fonds.",
|
||||
"import_viewkey_title": "Importer la clé de visualisation",
|
||||
"import_viewkey_wrong_type": "Ceci ressemble à une clé de dépense. Utilisez plutôt « Importer une clé privée ».",
|
||||
"incorrect_passphrase": "Mot de passe incorrect",
|
||||
"incorrect_pin": "PIN incorrect",
|
||||
"insufficient_funds": "Fonds insuffisants pour ce montant plus les frais.",
|
||||
@@ -414,6 +496,7 @@
|
||||
"keep": "Conserver",
|
||||
"keep_daemon": "Garder le daemon en marche",
|
||||
"key_export_click_retrieve": "Cliquez pour récupérer la clé de votre portefeuille",
|
||||
"key_export_failed": "Impossible d'exporter la clé — déverrouillez le portefeuille (s'il est chiffré) et réessayez.",
|
||||
"key_export_fetching": "Récupération de la clé depuis le portefeuille...",
|
||||
"key_export_private_key": "Clé privée :",
|
||||
"key_export_private_warning": "Gardez cette clé SECRÈTE ! Toute personne possédant cette clé peut dépenser vos fonds. Ne la partagez jamais en ligne ou avec des tiers non fiables.",
|
||||
@@ -739,6 +822,7 @@
|
||||
"output_filename": "Nom du fichier de sortie :",
|
||||
"overview": "Aperçu",
|
||||
"paste": "Coller",
|
||||
"paste_clip_empty": "Le presse-papiers est vide",
|
||||
"paste_from_clipboard": "Coller depuis le presse-papiers",
|
||||
"pay_from": "Payer depuis",
|
||||
"payment_request": "DEMANDE DE PAIEMENT",
|
||||
@@ -790,7 +874,14 @@
|
||||
"peers_upper": "PAIRS",
|
||||
"peers_version": "Version",
|
||||
"pending": "En attente",
|
||||
"pin_change_desc": "Modifiez votre code PIN de déverrouillage. Vous avez besoin de votre code PIN actuel et d'un nouveau code PIN.",
|
||||
"pin_confirm_new_label": "Confirmer le nouveau code PIN :",
|
||||
"pin_current_label": "Code PIN actuel :",
|
||||
"pin_new_label": "Nouveau code PIN (4 à 8 chiffres) :",
|
||||
"pin_not_set": "PIN non défini. Utilisez le mot de passe pour déverrouiller.",
|
||||
"pin_remove_desc": "Saisissez votre code PIN actuel pour confirmer la suppression. Vous devrez utiliser votre phrase secrète complète pour déverrouiller.",
|
||||
"pin_setup_desc": "Définissez un code PIN de 4 à 8 chiffres pour déverrouiller rapidement votre portefeuille. Votre phrase secrète sera chiffrée avec ce code PIN et stockée localement.",
|
||||
"pin_wallet_passphrase": "Phrase secrète du portefeuille :",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "Ajouter une entrée",
|
||||
"portfolio_add_to": "Ajouter au portefeuille",
|
||||
@@ -829,6 +920,9 @@
|
||||
"portfolio_remove_from": "Retirer du portefeuille d'actifs",
|
||||
"portfolio_revert": "Annuler",
|
||||
"portfolio_save": "Enregistrer",
|
||||
"portfolio_save_need_address": "Ajoutez au moins une adresse pour enregistrer.",
|
||||
"portfolio_save_need_name": "Saisissez un nom pour enregistrer ce groupe.",
|
||||
"portfolio_save_need_price": "Saisissez un prix manuel supérieur à 0 pour enregistrer.",
|
||||
"portfolio_search": "Rechercher des adresses\\xE2\\x80\\xA6",
|
||||
"portfolio_search_icons": "Rechercher des icônes\\xE2\\x80\\xA6",
|
||||
"portfolio_select_all": "Tout",
|
||||
@@ -1064,7 +1158,8 @@
|
||||
"settings_gradient_bg": "Fond dégradé",
|
||||
"settings_gradient_desc": "Remplacer les arrière-plans texturés par des dégradés lisses",
|
||||
"settings_idle_after": "après",
|
||||
"settings_import_key": "Importer la clé...",
|
||||
"settings_import_key": "Importer une clé privée...",
|
||||
"settings_import_viewkey": "Importer la clé de visualisation...",
|
||||
"settings_language_note": "Remarque : Certains textes nécessitent un redémarrage pour se mettre à jour",
|
||||
"settings_lock_now": "Verrouiller maintenant",
|
||||
"settings_locked": "Verrouillé",
|
||||
@@ -1151,6 +1246,14 @@
|
||||
"submitting_transaction": "Soumission de la transaction...",
|
||||
"success": "Succès",
|
||||
"summary": "Résumé",
|
||||
"sweep_button": "Balayer",
|
||||
"sweep_caveat": "Importe la clé pour signer une transaction qui déplace tous ses fonds vers votre adresse. La clé reste dans votre portefeuille avec un solde vide.",
|
||||
"sweep_dest_label": "Envoyer les fonds balayés vers",
|
||||
"sweep_dest_new": "Nouvelle adresse blindée (recommandé)",
|
||||
"sweep_done": "Terminé — fonds balayés vers votre adresse.",
|
||||
"sweep_to": "Balayé vers :",
|
||||
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
|
||||
"sweep_tx": "Transaction :",
|
||||
"syncing": "Synchronisation...",
|
||||
"t_address": "Adresse T",
|
||||
"t_addresses": "Adresses T",
|
||||
@@ -1186,6 +1289,7 @@
|
||||
"transfer_to": "Transférer à :",
|
||||
"transparent": "Transparent",
|
||||
"transparent_address": "Adresse transparente",
|
||||
"try_again": "Réessayer",
|
||||
"tt_addr_url": "URL de base pour consulter les adresses dans un explorateur de blocs",
|
||||
"tt_address_book": "Gérer les adresses enregistrées pour un envoi rapide",
|
||||
"tt_auto_lock": "Verrouiller le portefeuille après cette durée d'inactivité",
|
||||
@@ -1212,6 +1316,7 @@
|
||||
"tt_font_scale": "Mettre à l'échelle tout le texte et l'interface (1.0x = par défaut, jusqu'à 1.5x).",
|
||||
"tt_idle_delay": "Combien de temps attendre avant de commencer le minage",
|
||||
"tt_import_key": "Importer une clé privée (zkey ou tkey) dans ce portefeuille",
|
||||
"tt_import_viewkey": "Importer une clé de visualisation blindée pour observer une adresse (lecture seule)",
|
||||
"tt_keep_daemon": "Le daemon s'arrêtera lors de l'exécution de l'assistant de configuration",
|
||||
"tt_language": "Langue de l'interface du portefeuille",
|
||||
"tt_layout_hotkey": "Raccourci : touches fléchées gauche/droite pour changer les dispositions de Balance",
|
||||
|
||||
107
res/lang/ja.json
107
res/lang/ja.json
@@ -62,6 +62,7 @@
|
||||
"backup_data": "バックアップとデータ",
|
||||
"backup_description": "wallet.datファイルのバックアップを作成します。このファイルにはすべての秘密鍵と取引履歴が含まれています。バックアップは安全な場所に保管してください。",
|
||||
"backup_destination": "バックアップ先:",
|
||||
"backup_overwrite_confirm": "そこには既にファイルがあります。もう一度保存すると上書きされます。",
|
||||
"backup_source": "ソース:%s",
|
||||
"backup_tip_external": "外部ドライブまたはクラウドストレージにバックアップを保存",
|
||||
"backup_tip_multiple": "異なる場所に複数のバックアップを作成",
|
||||
@@ -70,6 +71,7 @@
|
||||
"backup_title": "ウォレットのバックアップ",
|
||||
"backup_wallet": "ウォレットをバックアップ...",
|
||||
"backup_wallet_not_found": "警告:予想される場所にwallet.datが見つかりません",
|
||||
"backup_warn": "このファイルにはすべての秘密鍵が含まれています。安全な場所に保管してください。",
|
||||
"balance": "残高",
|
||||
"balance_history_collecting": "残高履歴 — データを収集中...",
|
||||
"balance_layout": "残高レイアウト",
|
||||
@@ -114,6 +116,10 @@
|
||||
"bootstrap_wallet_protected": "(wallet.dat は保護されています)",
|
||||
"bootstrap_warning": "既存のブロックデータ(blocks、chainstate、notarizations)は削除され置き換えられます。wallet.dat は変更・削除されません。",
|
||||
"cancel": "キャンセル",
|
||||
"change_pass_confirm": "新しいパスフレーズ(確認):",
|
||||
"change_pass_current": "現在のパスフレーズ:",
|
||||
"change_pass_new": "新しいパスフレーズ:",
|
||||
"change_pass_title": "パスフレーズを変更",
|
||||
"characters": "文字",
|
||||
"chat": "チャット",
|
||||
"chat_cancel": "キャンセル",
|
||||
@@ -180,10 +186,18 @@
|
||||
"connected_peers": "接続中のピア",
|
||||
"connecting": "接続中...",
|
||||
"console": "コンソール",
|
||||
"console_accents": "カラーアクセント",
|
||||
"console_app": "アプリ",
|
||||
"console_auto_scroll": "自動スクロール",
|
||||
"console_available_commands": "利用可能なコマンド:",
|
||||
"console_capturing_output": "デーモン出力をキャプチャ中...",
|
||||
"console_cat_blockchain": "ブロックチェーン",
|
||||
"console_cat_control": "制御",
|
||||
"console_cat_mining": "マイニング",
|
||||
"console_cat_network": "ネットワーク",
|
||||
"console_cat_raw_transactions": "生トランザクション",
|
||||
"console_cat_utility": "ユーティリティ",
|
||||
"console_cat_wallet": "ウォレット",
|
||||
"console_clear": "クリア",
|
||||
"console_clear_console": "コンソールをクリア",
|
||||
"console_cleared": "コンソールをクリアしました",
|
||||
@@ -221,6 +235,20 @@
|
||||
"console_no_daemon": "デーモンなし",
|
||||
"console_not_connected": "エラー:デーモンに接続されていません",
|
||||
"console_quit_note": "ここでは 'quit'/'exit' は不要です — ウィンドウを閉じるだけで構いません。",
|
||||
"console_ref_builds": "生成",
|
||||
"console_ref_cancel": "キャンセル",
|
||||
"console_ref_destructive": "要注意",
|
||||
"console_ref_example": "例",
|
||||
"console_ref_insert": "コンソールに挿入",
|
||||
"console_ref_insert_run": "挿入して実行",
|
||||
"console_ref_no_match": "一致するコマンドはありません。",
|
||||
"console_ref_no_params": "パラメーターは不要です。",
|
||||
"console_ref_optional": "任意",
|
||||
"console_ref_parameters": "パラメーター",
|
||||
"console_ref_run": "実行",
|
||||
"console_ref_run_confirm": "%s を今すぐ実行しますか? 影響の大きいコマンドです。",
|
||||
"console_ref_search_hint": "名前または用途で検索…",
|
||||
"console_ref_select_hint": "コマンドを選ぶと内容が表示されます。",
|
||||
"console_rpc_reference": "RPCコマンドリファレンス",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "コンソールスキャンライン",
|
||||
@@ -240,6 +268,9 @@
|
||||
"console_status_stopping": "停止中",
|
||||
"console_status_unknown": "不明",
|
||||
"console_tab_completion": "Tabで補完",
|
||||
"console_text_colors": "テキスト色",
|
||||
"console_toggle_accents": "行のカラーアクセントを切り替え",
|
||||
"console_toggle_text_color": "行のテキスト色を切り替え",
|
||||
"console_type_help": "'help'と入力して利用可能なコマンドを表示",
|
||||
"console_welcome": "ObsidianDragonコンソールへようこそ",
|
||||
"console_zoom_in": "拡大",
|
||||
@@ -302,6 +333,19 @@
|
||||
"date": "日付",
|
||||
"date_label": "日付:",
|
||||
"debug_logging": "デバッグログ",
|
||||
"decrypt_desc": "ウォレットがエクスポートされ、デーモンが新しい暗号化されていないウォレットで再起動し、すべての鍵が再インポートされます。ウォレットのサイズによっては数分かかる場合があります。",
|
||||
"decrypt_error_title": "復号に失敗しました",
|
||||
"decrypt_step_backup": "暗号化されたウォレットをバックアップしています",
|
||||
"decrypt_step_export": "ウォレットの鍵をエクスポートしています",
|
||||
"decrypt_step_restart": "デーモンを再起動しています",
|
||||
"decrypt_step_stop": "デーモンを停止しています",
|
||||
"decrypt_step_unlock": "ウォレットのロックを解除しています",
|
||||
"decrypt_success_desc": "ウォレットは暗号化されていない状態になりました。暗号化されたウォレットのバックアップが、データディレクトリに wallet.dat.encrypted.bak として保存されました。",
|
||||
"decrypt_success_title": "ウォレットの復号に成功しました!",
|
||||
"decrypt_title": "ウォレットの暗号化を解除",
|
||||
"decrypt_wait_general": "お待ちください。デーモンが鍵をエクスポートし、再起動して再インポートしています。数分かかる場合があります。",
|
||||
"decrypt_wait_restart": "デーモンの起動が完了するのを待っています...",
|
||||
"decrypt_warning": "ウォレットの暗号化が解除されます。秘密鍵は保護されずにディスクに保存されます。",
|
||||
"delete": "削除",
|
||||
"delete_blockchain": "ブロックチェーンを削除",
|
||||
"delete_blockchain_confirm": "削除して再同期",
|
||||
@@ -314,6 +358,15 @@
|
||||
"download_bootstrap": "ブートストラップをダウンロード",
|
||||
"dragonx_green": "DragonX(グリーン)",
|
||||
"edit": "編集",
|
||||
"enc_confirm": "確認:",
|
||||
"enc_desc": "ウォレットを暗号化すると、パスフレーズで秘密鍵が保護されます。暗号化後、デーモンが再起動します。",
|
||||
"enc_encrypting": "ウォレットを暗号化しています...",
|
||||
"enc_pin_desc": "4〜8桁のPINを使うと、毎回パスフレーズ全体を入力せずにウォレットのロックを解除できます。",
|
||||
"enc_pin_set_ok": "PINを設定しました",
|
||||
"enc_pin_skipped": "PINをスキップしました。後で設定で設定できます。",
|
||||
"enc_pin_vault_fail": "PINボルトの作成に失敗しました",
|
||||
"enc_success": "ウォレットの暗号化に成功しました!",
|
||||
"enc_wait": "お待ちください。アプリケーションを閉じないでください。",
|
||||
"error": "エラー",
|
||||
"error_format": "エラー:%s",
|
||||
"est_time_to_block": "予測ブロック時間",
|
||||
@@ -346,10 +399,18 @@
|
||||
"export_keys_danger": "危険:ウォレットからすべての秘密鍵がエクスポートされます!このファイルにアクセスできる人は誰でもあなたの資金を盗めます。安全に保管し、使用後は削除してください。",
|
||||
"export_keys_include_t": "Tアドレスを含める(透明)",
|
||||
"export_keys_include_z": "Zアドレスを含める(シールド)",
|
||||
"export_keys_none_addrs": "エクスポートするアドレスがありません",
|
||||
"export_keys_none_result": "鍵をエクスポートできませんでした(%d 件中 0 件)。ウォレットのロックを解除して再試行してください。",
|
||||
"export_keys_none_toast": "鍵をエクスポートできませんでした。ウォレットはロック解除されていますか?",
|
||||
"export_keys_not_connected": "デーモンに接続されていません",
|
||||
"export_keys_options": "エクスポートオプション:",
|
||||
"export_keys_partial": "%d 件中 %d 件の鍵をエクスポート — 不完全(支払い鍵がない、またはウォレットがロックされています)。",
|
||||
"export_keys_partial_toast": "部分エクスポート: %d 件中 %d 件",
|
||||
"export_keys_progress": "エクスポート中 %d/%d...",
|
||||
"export_keys_select_type": "アドレスの種類を少なくとも1つ選択してください",
|
||||
"export_keys_success": "鍵のエクスポートに成功しました",
|
||||
"export_keys_title": "すべての秘密鍵をエクスポート",
|
||||
"export_keys_write_fail": "鍵ファイルの書き込みに失敗しました。",
|
||||
"export_private_key": "秘密鍵をエクスポート",
|
||||
"export_tx_count": "%zu件の取引をCSVファイルにエクスポート。",
|
||||
"export_tx_file_fail": "CSVファイルの作成に失敗しました",
|
||||
@@ -391,21 +452,42 @@
|
||||
"history": "履歴",
|
||||
"immature_type": "未成熟",
|
||||
"import": "インポート",
|
||||
"import_key_address": "アドレス:",
|
||||
"import_key_btn": "鍵をインポート",
|
||||
"import_key_done": "インポートしました。ウォレットを再スキャンしています。",
|
||||
"import_key_field": "鍵",
|
||||
"import_key_formats": "サポートされる鍵形式:",
|
||||
"import_key_full_rescan": "(0 = 完全再スキャン)",
|
||||
"import_key_import": "インポート",
|
||||
"import_key_label": "秘密鍵:",
|
||||
"import_key_need_node": "鍵をインポートするには稼働中のノードに接続してください。",
|
||||
"import_key_no_valid": "入力に有効な鍵が見つかりません",
|
||||
"import_key_progress": "インポート中 %d/%d...",
|
||||
"import_key_rescan": "インポート後にブロックチェーンを再スキャン",
|
||||
"import_key_rescanning": "インポートと再スキャン中 — 数分かかることがあります",
|
||||
"import_key_reveal_tip": "鍵の表示/非表示",
|
||||
"import_key_start_height": "開始高:",
|
||||
"import_key_success": "鍵のインポートに成功しました",
|
||||
"import_key_t_format": "TアドレスWIF秘密鍵",
|
||||
"import_key_title": "秘密鍵をインポート",
|
||||
"import_key_tooltip": "1行に1つずつ秘密鍵を入力してください。\nzアドレスとtアドレスの鍵の両方に対応しています。\n#で始まる行はコメントとして扱われます。",
|
||||
"import_key_type_tkey": "透明な秘密鍵",
|
||||
"import_key_type_unknown": "認識できない鍵の形式",
|
||||
"import_key_type_zspend": "シールド支払鍵",
|
||||
"import_key_type_zview": "シールド閲覧鍵(監視のみ)",
|
||||
"import_key_warn": "自分が所有する鍵のみをインポートしてください — その資金へのアクセスを許可します。",
|
||||
"import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。",
|
||||
"import_key_wrong_type": "これは閲覧鍵のようです。代わりに「閲覧鍵をインポート」を使用してください。",
|
||||
"import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)",
|
||||
"import_private_key": "秘密鍵をインポート...",
|
||||
"import_scan_hint": "0 = 最初から再スキャン",
|
||||
"import_scan_label": "スキャン開始ブロック高(任意)",
|
||||
"import_scan_tip": "現在の高さ",
|
||||
"import_scan_transparent": "透明鍵は常に全体を再スキャンします",
|
||||
"import_viewkey_field": "閲覧鍵",
|
||||
"import_viewkey_note": "監視のみ:閲覧鍵はアドレスの残高と取引を表示できますが、資金を送金することはできません。",
|
||||
"import_viewkey_title": "閲覧鍵をインポート",
|
||||
"import_viewkey_wrong_type": "これは支払い鍵のようです。代わりに「秘密鍵をインポート」を使用してください。",
|
||||
"incorrect_passphrase": "パスフレーズが正しくありません",
|
||||
"incorrect_pin": "PINが正しくありません",
|
||||
"insufficient_funds": "この金額と手数料に対して残高が不足しています。",
|
||||
@@ -414,6 +496,7 @@
|
||||
"keep": "保持",
|
||||
"keep_daemon": "デーモンを実行し続ける",
|
||||
"key_export_click_retrieve": "クリックしてウォレットからキーを取得",
|
||||
"key_export_failed": "鍵をエクスポートできませんでした。ウォレットのロックを解除して(暗号化されている場合)再試行してください。",
|
||||
"key_export_fetching": "ウォレットから鍵を取得中...",
|
||||
"key_export_private_key": "秘密鍵:",
|
||||
"key_export_private_warning": "この鍵は秘密にしてください!この鍵を持つ人は誰でもあなたの資金を使えます。オンラインや信頼できない相手と共有しないでください。",
|
||||
@@ -739,6 +822,7 @@
|
||||
"output_filename": "出力ファイル名:",
|
||||
"overview": "概要",
|
||||
"paste": "貼り付け",
|
||||
"paste_clip_empty": "クリップボードは空です",
|
||||
"paste_from_clipboard": "クリップボードから貼り付け",
|
||||
"pay_from": "支払い元",
|
||||
"payment_request": "支払い請求",
|
||||
@@ -790,7 +874,14 @@
|
||||
"peers_upper": "ピア",
|
||||
"peers_version": "バージョン",
|
||||
"pending": "保留中",
|
||||
"pin_change_desc": "ロック解除PINを変更します。現在のPINと新しいPINが必要です。",
|
||||
"pin_confirm_new_label": "新しいPINの確認:",
|
||||
"pin_current_label": "現在のPIN:",
|
||||
"pin_new_label": "新しいPIN(4〜8桁):",
|
||||
"pin_not_set": "PINが設定されていません。パスフレーズで解除してください。",
|
||||
"pin_remove_desc": "削除を確認するには現在のPINを入力してください。ロック解除には完全なパスフレーズが必要になります。",
|
||||
"pin_setup_desc": "ウォレットをすばやくロック解除するための4〜8桁のPINを設定します。ウォレットのパスフレーズはこのPINで暗号化され、ローカルに保存されます。",
|
||||
"pin_wallet_passphrase": "ウォレットのパスフレーズ:",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "エントリを追加",
|
||||
"portfolio_add_to": "ポートフォリオに追加",
|
||||
@@ -829,6 +920,9 @@
|
||||
"portfolio_remove_from": "ポートフォリオから削除",
|
||||
"portfolio_revert": "元に戻す",
|
||||
"portfolio_save": "保存",
|
||||
"portfolio_save_need_address": "保存するにはアドレスを1つ以上追加してください。",
|
||||
"portfolio_save_need_name": "このグループを保存するには名前を入力してください。",
|
||||
"portfolio_save_need_price": "保存するには0より大きい手動価格を入力してください。",
|
||||
"portfolio_search": "アドレスを検索…",
|
||||
"portfolio_search_icons": "アイコンを検索…",
|
||||
"portfolio_select_all": "すべて",
|
||||
@@ -1064,7 +1158,8 @@
|
||||
"settings_gradient_bg": "グラデーション背景",
|
||||
"settings_gradient_desc": "テクスチャ背景を滑らかなグラデーションに置換",
|
||||
"settings_idle_after": "経過後",
|
||||
"settings_import_key": "鍵をインポート...",
|
||||
"settings_import_key": "秘密鍵をインポート...",
|
||||
"settings_import_viewkey": "閲覧鍵をインポート...",
|
||||
"settings_language_note": "注意:一部のテキストは更新に再起動が必要です",
|
||||
"settings_lock_now": "今すぐロック",
|
||||
"settings_locked": "ロック済み",
|
||||
@@ -1151,6 +1246,14 @@
|
||||
"submitting_transaction": "取引を送信中...",
|
||||
"success": "成功",
|
||||
"summary": "概要",
|
||||
"sweep_button": "集約",
|
||||
"sweep_caveat": "鍵をインポートして、すべての資金をあなたのアドレスへ移す取引に署名します。鍵は残高ゼロでウォレットに残ります。",
|
||||
"sweep_dest_label": "集約先",
|
||||
"sweep_dest_new": "新しいシールドアドレス(推奨)",
|
||||
"sweep_done": "完了 — 資金をあなたのアドレスに集約しました。",
|
||||
"sweep_to": "集約先:",
|
||||
"sweep_toggle": "ウォレットに集約(鍵は保持しない)",
|
||||
"sweep_tx": "取引:",
|
||||
"syncing": "同期中...",
|
||||
"t_address": "Tアドレス",
|
||||
"t_addresses": "Tアドレス",
|
||||
@@ -1186,6 +1289,7 @@
|
||||
"transfer_to": "送金先:",
|
||||
"transparent": "透明",
|
||||
"transparent_address": "トランスペアレントアドレス",
|
||||
"try_again": "再試行",
|
||||
"tt_addr_url": "ブロックエクスプローラーでアドレスを表示するためのベース URL",
|
||||
"tt_address_book": "クイック送信用の保存済みアドレスを管理",
|
||||
"tt_auto_lock": "この無操作時間後にウォレットをロック",
|
||||
@@ -1212,6 +1316,7 @@
|
||||
"tt_font_scale": "すべてのテキストと UI をスケーリング(1.0x = デフォルト、最大 1.5x)。",
|
||||
"tt_idle_delay": "マイニング開始前の待機時間",
|
||||
"tt_import_key": "このウォレットに秘密鍵(zkey または tkey)をインポート",
|
||||
"tt_import_viewkey": "シールド閲覧鍵をインポートしてアドレスを閲覧(読み取り専用)",
|
||||
"tt_keep_daemon": "セットアップウィザード実行時にデーモンは停止します",
|
||||
"tt_language": "ウォレット UI のインターフェース言語",
|
||||
"tt_layout_hotkey": "ホットキー:左右矢印キーでバランスレイアウトを切り替え",
|
||||
|
||||
107
res/lang/ko.json
107
res/lang/ko.json
@@ -62,6 +62,7 @@
|
||||
"backup_data": "백업 및 데이터",
|
||||
"backup_description": "wallet.dat 파일의 백업을 생성합니다. 이 파일에는 모든 개인 키와 거래 내역이 포함되어 있습니다. 백업을 안전한 곳에 보관하세요.",
|
||||
"backup_destination": "백업 위치:",
|
||||
"backup_overwrite_confirm": "그곳에 이미 파일이 있습니다. 다시 저장하면 덮어씁니다.",
|
||||
"backup_source": "소스: %s",
|
||||
"backup_tip_external": "외장 드라이브 또는 클라우드 스토리지에 백업 저장",
|
||||
"backup_tip_multiple": "서로 다른 위치에 여러 백업 생성",
|
||||
@@ -70,6 +71,7 @@
|
||||
"backup_title": "지갑 백업",
|
||||
"backup_wallet": "지갑 백업...",
|
||||
"backup_wallet_not_found": "경고: 예상 위치에서 wallet.dat를 찾을 수 없습니다",
|
||||
"backup_warn": "이 파일에는 모든 개인 키가 들어 있습니다. 안전한 곳에 보관하세요.",
|
||||
"balance": "잔액",
|
||||
"balance_history_collecting": "잔액 내역 — 데이터 수집 중...",
|
||||
"balance_layout": "잔액 레이아웃",
|
||||
@@ -114,6 +116,10 @@
|
||||
"bootstrap_wallet_protected": "(wallet.dat 보호됨)",
|
||||
"bootstrap_warning": "기존 블록 데이터(blocks, chainstate, notarizations)가 삭제되고 교체됩니다. wallet.dat는 수정되거나 삭제되지 않습니다.",
|
||||
"cancel": "취소",
|
||||
"change_pass_confirm": "새 암호 확인:",
|
||||
"change_pass_current": "현재 암호:",
|
||||
"change_pass_new": "새 암호:",
|
||||
"change_pass_title": "암호 변경",
|
||||
"characters": "문자",
|
||||
"chat": "채팅",
|
||||
"chat_cancel": "취소",
|
||||
@@ -180,10 +186,18 @@
|
||||
"connected_peers": "연결된 피어",
|
||||
"connecting": "연결 중...",
|
||||
"console": "콘솔",
|
||||
"console_accents": "색상 강조",
|
||||
"console_app": "앱",
|
||||
"console_auto_scroll": "자동 스크롤",
|
||||
"console_available_commands": "사용 가능한 명령어:",
|
||||
"console_capturing_output": "데몬 출력 캡처 중...",
|
||||
"console_cat_blockchain": "블록체인",
|
||||
"console_cat_control": "제어",
|
||||
"console_cat_mining": "채굴",
|
||||
"console_cat_network": "네트워크",
|
||||
"console_cat_raw_transactions": "원시 트랜잭션",
|
||||
"console_cat_utility": "유틸리티",
|
||||
"console_cat_wallet": "지갑",
|
||||
"console_clear": "지우기",
|
||||
"console_clear_console": "콘솔 지우기",
|
||||
"console_cleared": "콘솔이 지워졌습니다",
|
||||
@@ -221,6 +235,20 @@
|
||||
"console_no_daemon": "데몬 없음",
|
||||
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
|
||||
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.",
|
||||
"console_ref_builds": "생성",
|
||||
"console_ref_cancel": "취소",
|
||||
"console_ref_destructive": "주의",
|
||||
"console_ref_example": "예시",
|
||||
"console_ref_insert": "콘솔에 삽입",
|
||||
"console_ref_insert_run": "삽입 후 실행",
|
||||
"console_ref_no_match": "일치하는 명령이 없습니다.",
|
||||
"console_ref_no_params": "매개변수가 없습니다.",
|
||||
"console_ref_optional": "선택",
|
||||
"console_ref_parameters": "매개변수",
|
||||
"console_ref_run": "실행",
|
||||
"console_ref_run_confirm": "%s 을(를) 지금 실행할까요? 영향이 큰 명령입니다.",
|
||||
"console_ref_search_hint": "이름 또는 용도로 검색…",
|
||||
"console_ref_select_hint": "명령을 선택하면 설명이 표시됩니다.",
|
||||
"console_rpc_reference": "RPC 명령어 참조",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "콘솔 스캔라인",
|
||||
@@ -240,6 +268,9 @@
|
||||
"console_status_stopping": "중지 중",
|
||||
"console_status_unknown": "알 수 없음",
|
||||
"console_tab_completion": "Tab으로 자동 완성",
|
||||
"console_text_colors": "텍스트 색상",
|
||||
"console_toggle_accents": "줄 색상 강조 전환",
|
||||
"console_toggle_text_color": "줄 텍스트 색상 전환",
|
||||
"console_type_help": "'help'를 입력하여 사용 가능한 명령어 보기",
|
||||
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
|
||||
"console_zoom_in": "확대",
|
||||
@@ -302,6 +333,19 @@
|
||||
"date": "날짜",
|
||||
"date_label": "날짜:",
|
||||
"debug_logging": "디버그 로깅",
|
||||
"decrypt_desc": "지갑을 내보내고 데몬을 새 암호화되지 않은 지갑으로 다시 시작한 후 모든 키를 다시 가져옵니다. 지갑 크기에 따라 몇 분 정도 걸릴 수 있습니다.",
|
||||
"decrypt_error_title": "복호화 실패",
|
||||
"decrypt_step_backup": "암호화된 지갑 백업 중",
|
||||
"decrypt_step_export": "지갑 키 내보내는 중",
|
||||
"decrypt_step_restart": "데몬 다시 시작 중",
|
||||
"decrypt_step_stop": "데몬 중지 중",
|
||||
"decrypt_step_unlock": "지갑 잠금 해제 중",
|
||||
"decrypt_success_desc": "이제 지갑이 암호화되지 않았습니다. 암호화된 지갑의 백업이 데이터 디렉터리에 wallet.dat.encrypted.bak로 저장되었습니다.",
|
||||
"decrypt_success_title": "지갑 복호화에 성공했습니다!",
|
||||
"decrypt_title": "지갑 암호화 제거",
|
||||
"decrypt_wait_general": "잠시 기다려 주세요. 데몬이 키를 내보내고 다시 시작한 후 다시 가져오고 있습니다. 몇 분 정도 걸릴 수 있습니다.",
|
||||
"decrypt_wait_restart": "데몬이 완전히 시작될 때까지 기다리는 중...",
|
||||
"decrypt_warning": "지갑의 암호화가 제거됩니다. 개인 키가 보호되지 않은 상태로 디스크에 저장됩니다.",
|
||||
"delete": "삭제",
|
||||
"delete_blockchain": "블록체인 삭제",
|
||||
"delete_blockchain_confirm": "삭제 후 재동기화",
|
||||
@@ -314,6 +358,15 @@
|
||||
"download_bootstrap": "부트스트랩 다운로드",
|
||||
"dragonx_green": "DragonX(그린)",
|
||||
"edit": "편집",
|
||||
"enc_confirm": "확인:",
|
||||
"enc_desc": "지갑을 암호화하면 암호로 개인 키를 보호합니다. 암호화 후 데몬이 다시 시작됩니다.",
|
||||
"enc_encrypting": "지갑을 암호화하는 중...",
|
||||
"enc_pin_desc": "4~8자리 PIN으로 매번 전체 암호를 입력하지 않고도 지갑 잠금을 해제할 수 있습니다.",
|
||||
"enc_pin_set_ok": "PIN이 설정되었습니다",
|
||||
"enc_pin_skipped": "PIN을 건너뛰었습니다. 나중에 설정에서 만들 수 있습니다.",
|
||||
"enc_pin_vault_fail": "PIN 보관소를 만들지 못했습니다",
|
||||
"enc_success": "지갑이 성공적으로 암호화되었습니다!",
|
||||
"enc_wait": "잠시 기다려 주세요. 애플리케이션을 닫지 마세요.",
|
||||
"error": "오류",
|
||||
"error_format": "오류: %s",
|
||||
"est_time_to_block": "예상 블록 시간",
|
||||
@@ -346,10 +399,18 @@
|
||||
"export_keys_danger": "위험: 지갑의 모든 개인 키가 내보내집니다! 이 파일에 접근할 수 있는 사람은 누구나 자금을 훔칠 수 있습니다. 안전하게 보관하고 사용 후 삭제하세요.",
|
||||
"export_keys_include_t": "T 주소 포함 (투명)",
|
||||
"export_keys_include_z": "Z 주소 포함 (차폐)",
|
||||
"export_keys_none_addrs": "내보낼 주소가 없습니다",
|
||||
"export_keys_none_result": "키를 내보내지 못했습니다(%d개 중 0개). 지갑 잠금을 해제하고 다시 시도하세요.",
|
||||
"export_keys_none_toast": "키를 내보낼 수 없습니다. 지갑이 잠금 해제되어 있나요?",
|
||||
"export_keys_not_connected": "데몬에 연결되어 있지 않습니다",
|
||||
"export_keys_options": "내보내기 옵션:",
|
||||
"export_keys_partial": "%d개 중 %d개 키 내보냄 — 불완전(일부는 지출 키가 없거나 지갑이 잠겨 있음).",
|
||||
"export_keys_partial_toast": "부분 내보내기: %d개 중 %d개 키",
|
||||
"export_keys_progress": "내보내는 중 %d/%d...",
|
||||
"export_keys_select_type": "주소 유형을 하나 이상 선택하세요",
|
||||
"export_keys_success": "키 내보내기 성공",
|
||||
"export_keys_title": "모든 개인 키 내보내기",
|
||||
"export_keys_write_fail": "키 파일을 쓰지 못했습니다.",
|
||||
"export_private_key": "개인 키 내보내기",
|
||||
"export_tx_count": "%zu건의 거래를 CSV 파일로 내보냈습니다.",
|
||||
"export_tx_file_fail": "CSV 파일 생성 실패",
|
||||
@@ -391,21 +452,42 @@
|
||||
"history": "내역",
|
||||
"immature_type": "미성숙",
|
||||
"import": "가져오기",
|
||||
"import_key_address": "주소:",
|
||||
"import_key_btn": "키 가져오기",
|
||||
"import_key_done": "가져왔습니다. 지갑을 재검색 중입니다.",
|
||||
"import_key_field": "키",
|
||||
"import_key_formats": "지원되는 키 형식:",
|
||||
"import_key_full_rescan": "(0 = 전체 재스캔)",
|
||||
"import_key_import": "가져오기",
|
||||
"import_key_label": "개인 키:",
|
||||
"import_key_need_node": "키를 가져오려면 실행 중인 노드에 연결하세요.",
|
||||
"import_key_no_valid": "입력에서 유효한 키를 찾을 수 없습니다",
|
||||
"import_key_progress": "가져오는 중 %d/%d...",
|
||||
"import_key_rescan": "가져오기 후 블록체인 재스캔",
|
||||
"import_key_rescanning": "가져오기 및 재검색 중 — 몇 분 걸릴 수 있습니다",
|
||||
"import_key_reveal_tip": "키 표시/숨기기",
|
||||
"import_key_start_height": "시작 높이:",
|
||||
"import_key_success": "키 가져오기 성공",
|
||||
"import_key_t_format": "T 주소 WIF 개인 키",
|
||||
"import_key_title": "개인 키 가져오기",
|
||||
"import_key_tooltip": "한 줄에 하나의 개인 키를 입력하세요.\nz 주소와 t 주소 키 모두 지원됩니다.\n#으로 시작하는 줄은 주석으로 처리됩니다.",
|
||||
"import_key_type_tkey": "투명 개인 키",
|
||||
"import_key_type_unknown": "알 수 없는 키 형식",
|
||||
"import_key_type_zspend": "차폐 지출 키",
|
||||
"import_key_type_zview": "차폐 조회 키 (읽기 전용)",
|
||||
"import_key_warn": "본인이 소유한 키만 가져오세요 — 해당 자금에 접근할 수 있습니다.",
|
||||
"import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.",
|
||||
"import_key_wrong_type": "조회 키인 것 같습니다. 대신 \"조회 키 가져오기\"를 사용하세요.",
|
||||
"import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)",
|
||||
"import_private_key": "개인 키 가져오기...",
|
||||
"import_scan_hint": "0 = 처음부터 다시 스캔",
|
||||
"import_scan_label": "스캔 시작 블록 높이(선택 사항)",
|
||||
"import_scan_tip": "현재 높이",
|
||||
"import_scan_transparent": "투명 키는 항상 전체를 다시 스캔합니다",
|
||||
"import_viewkey_field": "조회 키",
|
||||
"import_viewkey_note": "읽기 전용: 조회 키는 주소의 잔액과 거래를 보여주지만 자금을 사용할 수는 없습니다.",
|
||||
"import_viewkey_title": "조회 키 가져오기",
|
||||
"import_viewkey_wrong_type": "지출 키인 것 같습니다. 대신 \"개인 키 가져오기\"를 사용하세요.",
|
||||
"incorrect_passphrase": "잘못된 암호",
|
||||
"incorrect_pin": "잘못된 PIN",
|
||||
"insufficient_funds": "이 금액과 수수료를 위한 잔액이 부족합니다.",
|
||||
@@ -414,6 +496,7 @@
|
||||
"keep": "유지",
|
||||
"keep_daemon": "데몬 계속 실행",
|
||||
"key_export_click_retrieve": "지갑에서 키를 가져오려면 클릭",
|
||||
"key_export_failed": "키를 내보내지 못했습니다. 지갑 잠금을 해제하고(암호화된 경우) 다시 시도하세요.",
|
||||
"key_export_fetching": "지갑에서 키를 가져오는 중...",
|
||||
"key_export_private_key": "개인 키:",
|
||||
"key_export_private_warning": "이 키를 비밀로 유지하세요! 이 키를 가진 사람은 누구나 자금을 사용할 수 있습니다. 온라인이나 신뢰할 수 없는 사람과 공유하지 마세요.",
|
||||
@@ -739,6 +822,7 @@
|
||||
"output_filename": "출력 파일명:",
|
||||
"overview": "개요",
|
||||
"paste": "붙여넣기",
|
||||
"paste_clip_empty": "클립보드가 비어 있습니다",
|
||||
"paste_from_clipboard": "클립보드에서 붙여넣기",
|
||||
"pay_from": "보낼 곳",
|
||||
"payment_request": "결제 요청",
|
||||
@@ -790,7 +874,14 @@
|
||||
"peers_upper": "피어",
|
||||
"peers_version": "버전",
|
||||
"pending": "대기 중",
|
||||
"pin_change_desc": "잠금 해제 PIN을 변경합니다. 현재 PIN과 새 PIN이 필요합니다.",
|
||||
"pin_confirm_new_label": "새 PIN 확인:",
|
||||
"pin_current_label": "현재 PIN:",
|
||||
"pin_new_label": "새 PIN(4~8자리):",
|
||||
"pin_not_set": "PIN이 설정되지 않았습니다. 암호를 사용하여 잠금 해제하세요.",
|
||||
"pin_remove_desc": "삭제를 확인하려면 현재 PIN을 입력하세요. 잠금 해제에는 전체 암호가 필요합니다.",
|
||||
"pin_setup_desc": "지갑을 빠르게 잠금 해제할 4~8자리 PIN을 설정하세요. 지갑 암호가 이 PIN으로 암호화되어 로컬에 저장됩니다.",
|
||||
"pin_wallet_passphrase": "지갑 암호:",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "항목 추가",
|
||||
"portfolio_add_to": "포트폴리오에 추가",
|
||||
@@ -829,6 +920,9 @@
|
||||
"portfolio_remove_from": "포트폴리오에서 제거",
|
||||
"portfolio_revert": "되돌리기",
|
||||
"portfolio_save": "저장",
|
||||
"portfolio_save_need_address": "저장하려면 주소를 하나 이상 추가하세요.",
|
||||
"portfolio_save_need_name": "이 그룹을 저장하려면 이름을 입력하세요.",
|
||||
"portfolio_save_need_price": "저장하려면 0보다 큰 수동 가격을 입력하세요.",
|
||||
"portfolio_search": "주소 검색…",
|
||||
"portfolio_search_icons": "아이콘 검색\\xE2\\x80\\xA6",
|
||||
"portfolio_select_all": "전체",
|
||||
@@ -1064,7 +1158,8 @@
|
||||
"settings_gradient_bg": "그라데이션 배경",
|
||||
"settings_gradient_desc": "텍스처 배경을 부드러운 그라데이션으로 교체",
|
||||
"settings_idle_after": "후",
|
||||
"settings_import_key": "키 가져오기...",
|
||||
"settings_import_key": "개인 키 가져오기...",
|
||||
"settings_import_viewkey": "조회 키 가져오기...",
|
||||
"settings_language_note": "참고: 일부 텍스트는 업데이트하려면 다시 시작해야 합니다",
|
||||
"settings_lock_now": "지금 잠금",
|
||||
"settings_locked": "잠김",
|
||||
@@ -1151,6 +1246,14 @@
|
||||
"submitting_transaction": "거래 제출 중...",
|
||||
"success": "성공",
|
||||
"summary": "요약",
|
||||
"sweep_button": "쓸어담기",
|
||||
"sweep_caveat": "키를 가져와 모든 자금을 내 주소로 옮기는 거래에 서명합니다. 키는 잔액이 0인 상태로 지갑에 남습니다.",
|
||||
"sweep_dest_label": "쓸어담은 자금을 보낼 주소",
|
||||
"sweep_dest_new": "새 차폐 주소 (권장)",
|
||||
"sweep_done": "완료 — 자금을 내 주소로 쓸어담았습니다.",
|
||||
"sweep_to": "쓸어담은 주소:",
|
||||
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
|
||||
"sweep_tx": "거래:",
|
||||
"syncing": "동기화 중...",
|
||||
"t_address": "T 주소",
|
||||
"t_addresses": "T 주소",
|
||||
@@ -1186,6 +1289,7 @@
|
||||
"transfer_to": "이체 대상:",
|
||||
"transparent": "투명",
|
||||
"transparent_address": "투명 주소",
|
||||
"try_again": "다시 시도",
|
||||
"tt_addr_url": "블록 탐색기에서 주소를 보기 위한 기본 URL",
|
||||
"tt_address_book": "빠른 전송을 위해 저장된 주소 관리",
|
||||
"tt_auto_lock": "이 비활성 시간 후 지갑 잠금",
|
||||
@@ -1212,6 +1316,7 @@
|
||||
"tt_font_scale": "모든 텍스트 및 UI 크기 조정 (1.0x = 기본, 최대 1.5x).",
|
||||
"tt_idle_delay": "채굴 시작 전 대기 시간",
|
||||
"tt_import_key": "이 지갑에 개인키 (zkey 또는 tkey) 가져오기",
|
||||
"tt_import_viewkey": "차폐 조회 키를 가져와 주소를 조회(읽기 전용)",
|
||||
"tt_keep_daemon": "설정 마법사를 실행하면 데몬이 여전히 중지됩니다",
|
||||
"tt_language": "지갑 UI 인터페이스 언어",
|
||||
"tt_layout_hotkey": "단축키: 좌/우 화살표 키로 잔액 레이아웃 전환",
|
||||
|
||||
107
res/lang/pt.json
107
res/lang/pt.json
@@ -62,6 +62,7 @@
|
||||
"backup_data": "BACKUP & DADOS",
|
||||
"backup_description": "Crie um backup do seu arquivo wallet.dat. Este arquivo contém todas as suas chaves privadas e histórico de transações. Guarde o backup em um local seguro.",
|
||||
"backup_destination": "Destino do backup:",
|
||||
"backup_overwrite_confirm": "Já existe um arquivo ali — salve novamente para sobrescrevê-lo.",
|
||||
"backup_source": "Origem: %s",
|
||||
"backup_tip_external": "Armazene backups em unidades externas ou armazenamento em nuvem",
|
||||
"backup_tip_multiple": "Crie múltiplos backups em diferentes locais",
|
||||
@@ -70,6 +71,7 @@
|
||||
"backup_title": "Backup da Carteira",
|
||||
"backup_wallet": "Fazer Backup da Carteira...",
|
||||
"backup_wallet_not_found": "Aviso: wallet.dat não encontrado no local esperado",
|
||||
"backup_warn": "Este arquivo contém todas as suas chaves privadas — guarde-o em local seguro.",
|
||||
"balance": "Saldo",
|
||||
"balance_history_collecting": "Histórico de saldo — coletando dados...",
|
||||
"balance_layout": "Layout do Saldo",
|
||||
@@ -114,6 +116,10 @@
|
||||
"bootstrap_wallet_protected": "(wallet.dat está protegido)",
|
||||
"bootstrap_warning": "Os dados de blocos existentes (blocks, chainstate, notarizations) serão excluídos e substituídos. Seu wallet.dat NÃO será modificado ou excluído.",
|
||||
"cancel": "Cancelar",
|
||||
"change_pass_confirm": "Confirmar nova:",
|
||||
"change_pass_current": "Senha atual:",
|
||||
"change_pass_new": "Nova senha:",
|
||||
"change_pass_title": "Alterar senha",
|
||||
"characters": "caracteres",
|
||||
"chat": "Chat",
|
||||
"chat_cancel": "Cancelar",
|
||||
@@ -180,10 +186,18 @@
|
||||
"connected_peers": "Pares Conectados",
|
||||
"connecting": "Conectando...",
|
||||
"console": "Console",
|
||||
"console_accents": "Destaques de cor",
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Rolagem automática",
|
||||
"console_available_commands": "Comandos disponíveis:",
|
||||
"console_capturing_output": "Capturando saída do daemon...",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Controle",
|
||||
"console_cat_mining": "Mineração",
|
||||
"console_cat_network": "Rede",
|
||||
"console_cat_raw_transactions": "Transações brutas",
|
||||
"console_cat_utility": "Utilitários",
|
||||
"console_cat_wallet": "Carteira",
|
||||
"console_clear": "Limpar",
|
||||
"console_clear_console": "Limpar Console",
|
||||
"console_cleared": "Console limpo",
|
||||
@@ -221,6 +235,20 @@
|
||||
"console_no_daemon": "Sem daemon",
|
||||
"console_not_connected": "Erro: Não conectado ao daemon",
|
||||
"console_quit_note": "'quit'/'exit' não são necessários aqui — basta fechar a janela.",
|
||||
"console_ref_builds": "Gera",
|
||||
"console_ref_cancel": "Cancelar",
|
||||
"console_ref_destructive": "Sensível",
|
||||
"console_ref_example": "Exemplo",
|
||||
"console_ref_insert": "Inserir no console",
|
||||
"console_ref_insert_run": "Inserir e executar",
|
||||
"console_ref_no_match": "Nenhum comando corresponde.",
|
||||
"console_ref_no_params": "Não requer parâmetros.",
|
||||
"console_ref_optional": "opcional",
|
||||
"console_ref_parameters": "Parâmetros",
|
||||
"console_ref_run": "Executar",
|
||||
"console_ref_run_confirm": "Executar %s agora? Este é um comando com consequências.",
|
||||
"console_ref_search_hint": "Pesquisar por nome ou tarefa…",
|
||||
"console_ref_select_hint": "Selecione um comando para ver o que ele faz.",
|
||||
"console_rpc_reference": "Referência de Comandos RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Scanline do console",
|
||||
@@ -240,6 +268,9 @@
|
||||
"console_status_stopping": "Parando",
|
||||
"console_status_unknown": "Desconhecido",
|
||||
"console_tab_completion": "Tab para completar",
|
||||
"console_text_colors": "Cores do texto",
|
||||
"console_toggle_accents": "Alternar destaques de cor das linhas",
|
||||
"console_toggle_text_color": "Alternar cores do texto das linhas",
|
||||
"console_type_help": "Digite 'help' para comandos disponíveis",
|
||||
"console_welcome": "Bem-vindo ao Console ObsidianDragon",
|
||||
"console_zoom_in": "Aumentar zoom",
|
||||
@@ -302,6 +333,19 @@
|
||||
"date": "Data",
|
||||
"date_label": "Data:",
|
||||
"debug_logging": "REGISTRO DE DEPURAÇÃO",
|
||||
"decrypt_desc": "A carteira será exportada, o daemon reiniciado com uma nova carteira sem criptografia e todas as chaves reimportadas. Isso pode levar alguns minutos, dependendo do tamanho da carteira.",
|
||||
"decrypt_error_title": "Falha na descriptografia",
|
||||
"decrypt_step_backup": "Fazendo backup da carteira criptografada",
|
||||
"decrypt_step_export": "Exportando as chaves da carteira",
|
||||
"decrypt_step_restart": "Reiniciando o daemon",
|
||||
"decrypt_step_stop": "Parando o daemon",
|
||||
"decrypt_step_unlock": "Desbloqueando a carteira",
|
||||
"decrypt_success_desc": "Sua carteira agora está sem criptografia. Um backup da carteira criptografada foi salvo como wallet.dat.encrypted.bak no seu diretório de dados.",
|
||||
"decrypt_success_title": "Carteira descriptografada com sucesso!",
|
||||
"decrypt_title": "Remover criptografia da carteira",
|
||||
"decrypt_wait_general": "Aguarde. O daemon está exportando chaves, reiniciando e reimportando. Isso pode levar alguns minutos.",
|
||||
"decrypt_wait_restart": "Aguardando o daemon terminar de iniciar...",
|
||||
"decrypt_warning": "Isso removerá a criptografia da sua carteira. Suas chaves privadas serão armazenadas sem proteção no disco.",
|
||||
"delete": "Excluir",
|
||||
"delete_blockchain": "Excluir Blockchain",
|
||||
"delete_blockchain_confirm": "Excluir e Ressincronizar",
|
||||
@@ -314,6 +358,15 @@
|
||||
"download_bootstrap": "Baixar Bootstrap",
|
||||
"dragonx_green": "DragonX (Verde)",
|
||||
"edit": "Editar",
|
||||
"enc_confirm": "Confirmar:",
|
||||
"enc_desc": "Criptografar sua carteira protege suas chaves privadas com uma senha. Após a criptografia, o daemon será reiniciado.",
|
||||
"enc_encrypting": "Criptografando a carteira...",
|
||||
"enc_pin_desc": "Um PIN de 4 a 8 dígitos permite desbloquear sua carteira sem digitar a senha completa toda vez.",
|
||||
"enc_pin_set_ok": "PIN definido com sucesso",
|
||||
"enc_pin_skipped": "PIN ignorado. Você pode definir um depois em Configurações.",
|
||||
"enc_pin_vault_fail": "Falha ao criar o cofre do PIN",
|
||||
"enc_success": "Carteira criptografada com sucesso!",
|
||||
"enc_wait": "Aguarde, não feche o aplicativo.",
|
||||
"error": "Erro",
|
||||
"error_format": "Erro: %s",
|
||||
"est_time_to_block": "Tempo Est. por Bloco",
|
||||
@@ -346,10 +399,18 @@
|
||||
"export_keys_danger": "PERIGO: Isto exportará TODAS as chaves privadas da sua carteira! Qualquer pessoa com acesso a este arquivo pode roubar seus fundos. Guarde com segurança e exclua após o uso.",
|
||||
"export_keys_include_t": "Incluir endereços T (transparentes)",
|
||||
"export_keys_include_z": "Incluir endereços Z (blindados)",
|
||||
"export_keys_none_addrs": "Nenhum endereço para exportar",
|
||||
"export_keys_none_result": "Nenhuma chave exportada (0 de %d) — desbloqueie a carteira e tente novamente.",
|
||||
"export_keys_none_toast": "Não foi possível exportar chaves — a carteira está desbloqueada?",
|
||||
"export_keys_not_connected": "Não conectado ao daemon",
|
||||
"export_keys_options": "Opções de exportação:",
|
||||
"export_keys_partial": "Exportadas %d de %d chaves — incompleto (algumas sem chave de gasto, ou a carteira está bloqueada).",
|
||||
"export_keys_partial_toast": "Exportação parcial: %d de %d chaves",
|
||||
"export_keys_progress": "Exportando %d/%d...",
|
||||
"export_keys_select_type": "Selecione ao menos um tipo de endereço",
|
||||
"export_keys_success": "Chaves exportadas com sucesso",
|
||||
"export_keys_title": "Exportar Todas as Chaves Privadas",
|
||||
"export_keys_write_fail": "Falha ao gravar o arquivo de chaves.",
|
||||
"export_private_key": "Exportar Chave Privada",
|
||||
"export_tx_count": "Exportar %zu transações para arquivo CSV.",
|
||||
"export_tx_file_fail": "Falha ao criar arquivo CSV",
|
||||
@@ -391,21 +452,42 @@
|
||||
"history": "Histórico",
|
||||
"immature_type": "Imaturo",
|
||||
"import": "Importar",
|
||||
"import_key_address": "Endereço:",
|
||||
"import_key_btn": "Importar Chave(s)",
|
||||
"import_key_done": "Importada. A carteira está reescaneando.",
|
||||
"import_key_field": "Chave",
|
||||
"import_key_formats": "Formatos de chave suportados:",
|
||||
"import_key_full_rescan": "(0 = rescan completo)",
|
||||
"import_key_import": "Importar",
|
||||
"import_key_label": "Chave(s) Privada(s):",
|
||||
"import_key_need_node": "Conecte um nó em execução para importar uma chave.",
|
||||
"import_key_no_valid": "Nenhuma chave válida encontrada na entrada",
|
||||
"import_key_progress": "Importando %d/%d...",
|
||||
"import_key_rescan": "Reescanear blockchain após importação",
|
||||
"import_key_rescanning": "Importando e reescaneando — pode levar vários minutos",
|
||||
"import_key_reveal_tip": "Mostrar/ocultar a chave",
|
||||
"import_key_start_height": "Altura inicial:",
|
||||
"import_key_success": "Chaves importadas com sucesso",
|
||||
"import_key_t_format": "Chaves privadas WIF de endereços T",
|
||||
"import_key_title": "Importar Chave Privada",
|
||||
"import_key_tooltip": "Digite uma ou mais chaves privadas, uma por linha.\nSuporta chaves de z-endereço e t-endereço.\nLinhas começando com # são tratadas como comentários.",
|
||||
"import_key_type_tkey": "Chave privada transparente",
|
||||
"import_key_type_unknown": "Formato de chave não reconhecido",
|
||||
"import_key_type_zspend": "Chave de gasto blindada",
|
||||
"import_key_type_zview": "Chave de visualização blindada (somente leitura)",
|
||||
"import_key_warn": "Importe apenas uma chave que você possui — ela concede acesso aos seus fundos.",
|
||||
"import_key_warning": "Aviso: Nunca compartilhe suas chaves privadas! Importar chaves de fontes não confiáveis pode comprometer sua carteira.",
|
||||
"import_key_wrong_type": "Isto parece uma chave de visualização. Use \"Importar chave de visualização\".",
|
||||
"import_key_z_format": "Chaves de gasto de z-endereço (secret-extended-key-...)",
|
||||
"import_private_key": "Importar Chave Privada...",
|
||||
"import_scan_hint": "0 = reescanear desde o início",
|
||||
"import_scan_label": "Escanear a partir da altura do bloco (opcional)",
|
||||
"import_scan_tip": "altura atual",
|
||||
"import_scan_transparent": "Chaves transparentes sempre reescaneiam totalmente",
|
||||
"import_viewkey_field": "Chave de visualização",
|
||||
"import_viewkey_note": "Somente leitura: uma chave de visualização revela o saldo e as transações de um endereço, mas não pode gastar seus fundos.",
|
||||
"import_viewkey_title": "Importar chave de visualização",
|
||||
"import_viewkey_wrong_type": "Isto parece uma chave de gasto. Use \"Importar Chave Privada\".",
|
||||
"incorrect_passphrase": "Senha incorreta",
|
||||
"incorrect_pin": "PIN incorreto",
|
||||
"insufficient_funds": "Fundos insuficientes para este valor mais taxa.",
|
||||
@@ -414,6 +496,7 @@
|
||||
"keep": "Manter",
|
||||
"keep_daemon": "Manter daemon em execução",
|
||||
"key_export_click_retrieve": "Clique para recuperar a chave da sua carteira",
|
||||
"key_export_failed": "Não foi possível exportar a chave — desbloqueie a carteira (se estiver criptografada) e tente novamente.",
|
||||
"key_export_fetching": "Buscando chave da carteira...",
|
||||
"key_export_private_key": "Chave Privada:",
|
||||
"key_export_private_warning": "Mantenha esta chave em SEGREDO! Qualquer pessoa com esta chave pode gastar seus fundos. Nunca a compartilhe online ou com terceiros não confiáveis.",
|
||||
@@ -739,6 +822,7 @@
|
||||
"output_filename": "Nome do arquivo de saída:",
|
||||
"overview": "Resumo",
|
||||
"paste": "Colar",
|
||||
"paste_clip_empty": "A área de transferência está vazia",
|
||||
"paste_from_clipboard": "Colar da Área de Transferência",
|
||||
"pay_from": "Pagar de",
|
||||
"payment_request": "SOLICITAÇÃO DE PAGAMENTO",
|
||||
@@ -790,7 +874,14 @@
|
||||
"peers_upper": "PARES",
|
||||
"peers_version": "Versão",
|
||||
"pending": "Pendente",
|
||||
"pin_change_desc": "Altere seu PIN de desbloqueio. Você precisa do seu PIN atual e de um novo PIN.",
|
||||
"pin_confirm_new_label": "Confirmar novo PIN:",
|
||||
"pin_current_label": "PIN atual:",
|
||||
"pin_new_label": "Novo PIN (4 a 8 dígitos):",
|
||||
"pin_not_set": "PIN não definido. Use a senha para desbloquear.",
|
||||
"pin_remove_desc": "Digite seu PIN atual para confirmar a remoção. Você precisará usar sua senha completa para desbloquear.",
|
||||
"pin_setup_desc": "Defina um PIN de 4 a 8 dígitos para desbloquear a carteira rapidamente. Sua senha da carteira será criptografada com este PIN e armazenada localmente.",
|
||||
"pin_wallet_passphrase": "Senha da carteira:",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "Adicionar entrada",
|
||||
"portfolio_add_to": "Adicionar ao portfólio",
|
||||
@@ -829,6 +920,9 @@
|
||||
"portfolio_remove_from": "Remover do portfólio",
|
||||
"portfolio_revert": "Reverter",
|
||||
"portfolio_save": "Salvar",
|
||||
"portfolio_save_need_address": "Adicione pelo menos um endereço para salvar.",
|
||||
"portfolio_save_need_name": "Insira um nome para salvar este grupo.",
|
||||
"portfolio_save_need_price": "Insira um preço manual acima de 0 para salvar.",
|
||||
"portfolio_search": "Pesquisar endereços…",
|
||||
"portfolio_search_icons": "Pesquisar ícones\\xE2\\x80\\xA6",
|
||||
"portfolio_select_all": "Todos",
|
||||
@@ -1064,7 +1158,8 @@
|
||||
"settings_gradient_bg": "Fundo gradiente",
|
||||
"settings_gradient_desc": "Substituir fundos texturizados por gradientes suaves",
|
||||
"settings_idle_after": "após",
|
||||
"settings_import_key": "Importar chave...",
|
||||
"settings_import_key": "Importar Chave Privada...",
|
||||
"settings_import_viewkey": "Importar chave de visualização...",
|
||||
"settings_language_note": "Nota: Alguns textos requerem reinício para atualizar",
|
||||
"settings_lock_now": "Bloquear agora",
|
||||
"settings_locked": "Bloqueado",
|
||||
@@ -1151,6 +1246,14 @@
|
||||
"submitting_transaction": "Enviando transação...",
|
||||
"success": "Sucesso",
|
||||
"summary": "Resumo",
|
||||
"sweep_button": "Varrer",
|
||||
"sweep_caveat": "Importa a chave para assinar uma transação que move todos os seus fundos para o seu endereço. A chave permanece na carteira com saldo zero.",
|
||||
"sweep_dest_label": "Enviar fundos varridos para",
|
||||
"sweep_dest_new": "Novo endereço blindado (recomendado)",
|
||||
"sweep_done": "Concluído — fundos varridos para o seu endereço.",
|
||||
"sweep_to": "Varrido para:",
|
||||
"sweep_toggle": "Varrer para minha carteira (não manter a chave)",
|
||||
"sweep_tx": "Transação:",
|
||||
"syncing": "Sincronizando...",
|
||||
"t_address": "Endereço T",
|
||||
"t_addresses": "Endereços T",
|
||||
@@ -1186,6 +1289,7 @@
|
||||
"transfer_to": "Transferir para:",
|
||||
"transparent": "Transparente",
|
||||
"transparent_address": "Endereço Transparente",
|
||||
"try_again": "Tentar novamente",
|
||||
"tt_addr_url": "URL base para visualizar endereços em um explorador de blocos",
|
||||
"tt_address_book": "Gerenciar endereços salvos para envio rápido",
|
||||
"tt_auto_lock": "Bloquear carteira após este tempo de inatividade",
|
||||
@@ -1212,6 +1316,7 @@
|
||||
"tt_font_scale": "Escalar todo o texto e interface (1.0x = padrão, até 1.5x).",
|
||||
"tt_idle_delay": "Quanto tempo esperar antes de iniciar a mineração",
|
||||
"tt_import_key": "Importar uma chave privada (zkey ou tkey) nesta carteira",
|
||||
"tt_import_viewkey": "Importar uma chave de visualização blindada para observar um endereço (somente leitura)",
|
||||
"tt_keep_daemon": "O daemon será parado ao executar o assistente de configuração",
|
||||
"tt_language": "Idioma da interface da carteira",
|
||||
"tt_layout_hotkey": "Atalho: teclas de seta esquerda/direita para alternar layouts de Saldo",
|
||||
|
||||
107
res/lang/ru.json
107
res/lang/ru.json
@@ -62,6 +62,7 @@
|
||||
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
|
||||
"backup_description": "Создайте резервную копию файла wallet.dat. Этот файл содержит все ваши приватные ключи и историю транзакций. Храните копию в безопасном месте.",
|
||||
"backup_destination": "Место сохранения:",
|
||||
"backup_overwrite_confirm": "Там уже есть файл — сохраните ещё раз, чтобы перезаписать его.",
|
||||
"backup_source": "Источник: %s",
|
||||
"backup_tip_external": "Храните резервные копии на внешних дисках или в облаке",
|
||||
"backup_tip_multiple": "Создавайте несколько копий в разных местах",
|
||||
@@ -70,6 +71,7 @@
|
||||
"backup_title": "Резервное копирование кошелька",
|
||||
"backup_wallet": "Создать резервную копию...",
|
||||
"backup_wallet_not_found": "Предупреждение: wallet.dat не найден в ожидаемом расположении",
|
||||
"backup_warn": "Этот файл содержит все ваши приватные ключи — храните его в надёжном месте.",
|
||||
"balance": "Баланс",
|
||||
"balance_history_collecting": "История баланса — сбор данных...",
|
||||
"balance_layout": "Макет баланса",
|
||||
@@ -114,6 +116,10 @@
|
||||
"bootstrap_wallet_protected": "(wallet.dat защищён)",
|
||||
"bootstrap_warning": "Существующие данные блоков (blocks, chainstate, notarizations) будут удалены и заменены. Ваш wallet.dat НЕ будет изменён или удалён.",
|
||||
"cancel": "Отмена",
|
||||
"change_pass_confirm": "Подтвердите новый:",
|
||||
"change_pass_current": "Текущий пароль:",
|
||||
"change_pass_new": "Новый пароль:",
|
||||
"change_pass_title": "Сменить пароль",
|
||||
"characters": "символов",
|
||||
"chat": "Чат",
|
||||
"chat_cancel": "Отмена",
|
||||
@@ -180,10 +186,18 @@
|
||||
"connected_peers": "Подключённые узлы",
|
||||
"connecting": "Подключение...",
|
||||
"console": "Консоль",
|
||||
"console_accents": "Цветовые акценты",
|
||||
"console_app": "Прил.",
|
||||
"console_auto_scroll": "Авто-прокрутка",
|
||||
"console_available_commands": "Доступные команды:",
|
||||
"console_capturing_output": "Захват вывода daemon...",
|
||||
"console_cat_blockchain": "Блокчейн",
|
||||
"console_cat_control": "Управление",
|
||||
"console_cat_mining": "Майнинг",
|
||||
"console_cat_network": "Сеть",
|
||||
"console_cat_raw_transactions": "Сырые транзакции",
|
||||
"console_cat_utility": "Утилиты",
|
||||
"console_cat_wallet": "Кошелёк",
|
||||
"console_clear": "Очистить",
|
||||
"console_clear_console": "Очистить консоль",
|
||||
"console_cleared": "Консоль очищена",
|
||||
@@ -221,6 +235,20 @@
|
||||
"console_no_daemon": "Нет daemon",
|
||||
"console_not_connected": "Ошибка: Не подключено к daemon",
|
||||
"console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.",
|
||||
"console_ref_builds": "Формирует",
|
||||
"console_ref_cancel": "Отмена",
|
||||
"console_ref_destructive": "Осторожно",
|
||||
"console_ref_example": "Пример",
|
||||
"console_ref_insert": "Вставить в консоль",
|
||||
"console_ref_insert_run": "Вставить и выполнить",
|
||||
"console_ref_no_match": "Нет подходящих команд.",
|
||||
"console_ref_no_params": "Не требует параметров.",
|
||||
"console_ref_optional": "необязательно",
|
||||
"console_ref_parameters": "Параметры",
|
||||
"console_ref_run": "Выполнить",
|
||||
"console_ref_run_confirm": "Выполнить %s сейчас? Это ответственная команда.",
|
||||
"console_ref_search_hint": "Поиск по названию или задаче…",
|
||||
"console_ref_select_hint": "Выберите команду, чтобы увидеть, что она делает.",
|
||||
"console_rpc_reference": "Справочник RPC-команд",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Скан-линия консоли",
|
||||
@@ -240,6 +268,9 @@
|
||||
"console_status_stopping": "Остановка",
|
||||
"console_status_unknown": "Неизвестно",
|
||||
"console_tab_completion": "Tab для дополнения",
|
||||
"console_text_colors": "Цвета текста",
|
||||
"console_toggle_accents": "Переключить цветовые акценты строк",
|
||||
"console_toggle_text_color": "Переключить цвета текста строк",
|
||||
"console_type_help": "Введите 'help' для списка команд",
|
||||
"console_welcome": "Добро пожаловать в консоль ObsidianDragon",
|
||||
"console_zoom_in": "Увеличить",
|
||||
@@ -302,6 +333,19 @@
|
||||
"date": "Дата",
|
||||
"date_label": "Дата:",
|
||||
"debug_logging": "ЖУРНАЛ ОТЛАДКИ",
|
||||
"decrypt_desc": "Кошелёк будет экспортирован, демон перезапущен с новым незашифрованным кошельком, и все ключи будут повторно импортированы. Это может занять несколько минут в зависимости от размера кошелька.",
|
||||
"decrypt_error_title": "Не удалось расшифровать",
|
||||
"decrypt_step_backup": "Резервное копирование зашифрованного кошелька",
|
||||
"decrypt_step_export": "Экспорт ключей кошелька",
|
||||
"decrypt_step_restart": "Перезапуск демона",
|
||||
"decrypt_step_stop": "Остановка демона",
|
||||
"decrypt_step_unlock": "Разблокировка кошелька",
|
||||
"decrypt_success_desc": "Ваш кошелёк теперь не зашифрован. Резервная копия зашифрованного кошелька сохранена как wallet.dat.encrypted.bak в вашем каталоге данных.",
|
||||
"decrypt_success_title": "Кошелёк успешно расшифрован!",
|
||||
"decrypt_title": "Удалить шифрование кошелька",
|
||||
"decrypt_wait_general": "Пожалуйста, подождите. Демон экспортирует ключи, перезапускается и повторно импортирует. Это может занять несколько минут.",
|
||||
"decrypt_wait_restart": "Ожидание полного запуска демона...",
|
||||
"decrypt_warning": "Это удалит шифрование вашего кошелька. Ваши приватные ключи будут храниться на диске без защиты.",
|
||||
"delete": "Удалить",
|
||||
"delete_blockchain": "Удалить блокчейн",
|
||||
"delete_blockchain_confirm": "Удалить и пересинхронизировать",
|
||||
@@ -314,6 +358,15 @@
|
||||
"download_bootstrap": "Скачать бутстрап",
|
||||
"dragonx_green": "DragonX (Зелёная)",
|
||||
"edit": "Редактировать",
|
||||
"enc_confirm": "Подтвердите:",
|
||||
"enc_desc": "Шифрование кошелька защищает ваши приватные ключи паролем. После шифрования демон перезапустится.",
|
||||
"enc_encrypting": "Шифрование кошелька...",
|
||||
"enc_pin_desc": "PIN-код из 4–8 цифр позволяет разблокировать кошелёк, не вводя полный пароль каждый раз.",
|
||||
"enc_pin_set_ok": "PIN-код успешно установлен",
|
||||
"enc_pin_skipped": "PIN-код пропущен. Вы можете задать его позже в настройках.",
|
||||
"enc_pin_vault_fail": "Не удалось создать хранилище PIN-кода",
|
||||
"enc_success": "Кошелёк успешно зашифрован!",
|
||||
"enc_wait": "Пожалуйста, подождите, не закрывайте приложение.",
|
||||
"error": "Ошибка",
|
||||
"error_format": "Ошибка: %s",
|
||||
"est_time_to_block": "Расч. время до блока",
|
||||
@@ -346,10 +399,18 @@
|
||||
"export_keys_danger": "ОПАСНОСТЬ: Будут экспортированы ВСЕ приватные ключи из вашего кошелька! Любой, кто получит доступ к этому файлу, сможет украсть ваши средства. Храните его в безопасности и удалите после использования.",
|
||||
"export_keys_include_t": "Включить T-адреса (прозрачные)",
|
||||
"export_keys_include_z": "Включить Z-адреса (экранированные)",
|
||||
"export_keys_none_addrs": "Нет адресов для экспорта",
|
||||
"export_keys_none_result": "Ключи не экспортированы (0 из %d) — разблокируйте кошелёк и повторите попытку.",
|
||||
"export_keys_none_toast": "Не удалось экспортировать ключи — кошелёк разблокирован?",
|
||||
"export_keys_not_connected": "Нет подключения к демону",
|
||||
"export_keys_options": "Параметры экспорта:",
|
||||
"export_keys_partial": "Экспортировано %d из %d ключей — неполно (у некоторых нет ключа расходования или кошелёк заблокирован).",
|
||||
"export_keys_partial_toast": "Частичный экспорт: %d из %d ключей",
|
||||
"export_keys_progress": "Экспорт %d/%d...",
|
||||
"export_keys_select_type": "Выберите хотя бы один тип адреса",
|
||||
"export_keys_success": "Ключи успешно экспортированы",
|
||||
"export_keys_title": "Экспорт всех приватных ключей",
|
||||
"export_keys_write_fail": "Не удалось записать файл ключей.",
|
||||
"export_private_key": "Экспорт приватного ключа",
|
||||
"export_tx_count": "Экспортировать %zu транзакций в файл CSV.",
|
||||
"export_tx_file_fail": "Не удалось создать файл CSV",
|
||||
@@ -391,21 +452,42 @@
|
||||
"history": "История",
|
||||
"immature_type": "Незрелая",
|
||||
"import": "Импорт",
|
||||
"import_key_address": "Адрес:",
|
||||
"import_key_btn": "Импорт ключей",
|
||||
"import_key_done": "Импортировано. Кошелёк выполняет повторное сканирование.",
|
||||
"import_key_field": "Ключ",
|
||||
"import_key_formats": "Поддерживаемые форматы ключей:",
|
||||
"import_key_full_rescan": "(0 = полное сканирование)",
|
||||
"import_key_import": "Импорт",
|
||||
"import_key_label": "Приватный ключ(и):",
|
||||
"import_key_need_node": "Подключите запущенный узел, чтобы импортировать ключ.",
|
||||
"import_key_no_valid": "В введённых данных не найдено действительных ключей",
|
||||
"import_key_progress": "Импорт %d/%d...",
|
||||
"import_key_rescan": "Пересканировать блокчейн после импорта",
|
||||
"import_key_rescanning": "Импорт и повторное сканирование — это может занять несколько минут",
|
||||
"import_key_reveal_tip": "Показать/скрыть ключ",
|
||||
"import_key_start_height": "Начальная высота:",
|
||||
"import_key_success": "Ключи успешно импортированы",
|
||||
"import_key_t_format": "Приватные ключи WIF для T-адресов",
|
||||
"import_key_title": "Импорт приватного ключа",
|
||||
"import_key_tooltip": "Введите один или несколько приватных ключей, по одному на строку.\nПоддерживаются ключи z-адресов и t-адресов.\nСтроки, начинающиеся с #, считаются комментариями.",
|
||||
"import_key_type_tkey": "Прозрачный приватный ключ",
|
||||
"import_key_type_unknown": "Нераспознанный формат ключа",
|
||||
"import_key_type_zspend": "Экранированный ключ траты",
|
||||
"import_key_type_zview": "Экранированный ключ просмотра (только чтение)",
|
||||
"import_key_warn": "Импортируйте только свой ключ — он даёт доступ к его средствам.",
|
||||
"import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.",
|
||||
"import_key_wrong_type": "Похоже, это ключ просмотра. Используйте «Импорт ключа просмотра».",
|
||||
"import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)",
|
||||
"import_private_key": "Импорт приватного ключа...",
|
||||
"import_scan_hint": "0 = повторное сканирование с начала",
|
||||
"import_scan_label": "Сканировать с высоты блока (необязательно)",
|
||||
"import_scan_tip": "текущая высота",
|
||||
"import_scan_transparent": "Прозрачные ключи всегда пересканируются полностью",
|
||||
"import_viewkey_field": "Ключ просмотра",
|
||||
"import_viewkey_note": "Только чтение: ключ просмотра показывает баланс и транзакции адреса, но не может тратить его средства.",
|
||||
"import_viewkey_title": "Импорт ключа просмотра",
|
||||
"import_viewkey_wrong_type": "Похоже, это ключ расходования. Используйте «Импорт приватного ключа».",
|
||||
"incorrect_passphrase": "Неверный пароль",
|
||||
"incorrect_pin": "Неверный PIN",
|
||||
"insufficient_funds": "Недостаточно средств для этой суммы плюс комиссия.",
|
||||
@@ -414,6 +496,7 @@
|
||||
"keep": "Сохранить",
|
||||
"keep_daemon": "Оставить daemon работающим",
|
||||
"key_export_click_retrieve": "Нажмите, чтобы получить ключ из вашего кошелька",
|
||||
"key_export_failed": "Не удалось экспортировать ключ — разблокируйте кошелёк (если зашифрован) и повторите попытку.",
|
||||
"key_export_fetching": "Получение ключа из кошелька...",
|
||||
"key_export_private_key": "Приватный ключ:",
|
||||
"key_export_private_warning": "Держите этот ключ в ТАЙНЕ! Любой, кто владеет этим ключом, может потратить ваши средства. Никогда не делитесь им в интернете или с ненадёжными лицами.",
|
||||
@@ -739,6 +822,7 @@
|
||||
"output_filename": "Имя выходного файла:",
|
||||
"overview": "Обзор",
|
||||
"paste": "Вставить",
|
||||
"paste_clip_empty": "Буфер обмена пуст",
|
||||
"paste_from_clipboard": "Вставить из буфера обмена",
|
||||
"pay_from": "Оплатить с",
|
||||
"payment_request": "ЗАПРОС НА ОПЛАТУ",
|
||||
@@ -790,7 +874,14 @@
|
||||
"peers_upper": "УЗЛЫ",
|
||||
"peers_version": "Версия",
|
||||
"pending": "Ожидание",
|
||||
"pin_change_desc": "Измените PIN-код разблокировки. Вам понадобится текущий PIN-код и новый PIN-код.",
|
||||
"pin_confirm_new_label": "Подтвердите новый PIN-код:",
|
||||
"pin_current_label": "Текущий PIN-код:",
|
||||
"pin_new_label": "Новый PIN-код (4–8 цифр):",
|
||||
"pin_not_set": "PIN не установлен. Используйте пароль для разблокировки.",
|
||||
"pin_remove_desc": "Введите текущий PIN-код для подтверждения удаления. Для разблокировки вам понадобится полный пароль.",
|
||||
"pin_setup_desc": "Установите PIN-код из 4–8 цифр для быстрой разблокировки кошелька. Пароль вашего кошелька будет зашифрован этим PIN-кодом и сохранён локально.",
|
||||
"pin_wallet_passphrase": "Пароль кошелька:",
|
||||
"ping": "Пинг",
|
||||
"portfolio_add_entry": "Добавить запись",
|
||||
"portfolio_add_to": "Добавить в портфель",
|
||||
@@ -829,6 +920,9 @@
|
||||
"portfolio_remove_from": "Удалить из портфеля",
|
||||
"portfolio_revert": "Отменить изменения",
|
||||
"portfolio_save": "Сохранить",
|
||||
"portfolio_save_need_address": "Добавьте хотя бы один адрес, чтобы сохранить.",
|
||||
"portfolio_save_need_name": "Введите имя, чтобы сохранить эту группу.",
|
||||
"portfolio_save_need_price": "Введите ручную цену больше 0, чтобы сохранить.",
|
||||
"portfolio_search": "Поиск адресов…",
|
||||
"portfolio_search_icons": "Поиск иконок…",
|
||||
"portfolio_select_all": "Все",
|
||||
@@ -1064,7 +1158,8 @@
|
||||
"settings_gradient_bg": "Градиент фона",
|
||||
"settings_gradient_desc": "Заменить текстурные фоны плавными градиентами",
|
||||
"settings_idle_after": "через",
|
||||
"settings_import_key": "Импортировать ключ...",
|
||||
"settings_import_key": "Импорт приватного ключа...",
|
||||
"settings_import_viewkey": "Импортировать ключ просмотра...",
|
||||
"settings_language_note": "Примечание: Некоторый текст требует перезапуска для обновления",
|
||||
"settings_lock_now": "Заблокировать сейчас",
|
||||
"settings_locked": "Заблокирован",
|
||||
@@ -1151,6 +1246,14 @@
|
||||
"submitting_transaction": "Отправка транзакции...",
|
||||
"success": "Успешно",
|
||||
"summary": "Итоги",
|
||||
"sweep_button": "Перевести",
|
||||
"sweep_caveat": "Импортирует ключ, чтобы подписать одну транзакцию, переводящую все его средства на ваш адрес. Ключ остаётся в кошельке с нулевым балансом.",
|
||||
"sweep_dest_label": "Отправить переведённые средства на",
|
||||
"sweep_dest_new": "Новый экранированный адрес (рекомендуется)",
|
||||
"sweep_done": "Готово — средства переведены на ваш адрес.",
|
||||
"sweep_to": "Переведено на:",
|
||||
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
|
||||
"sweep_tx": "Транзакция:",
|
||||
"syncing": "Синхронизация...",
|
||||
"t_address": "T-адрес",
|
||||
"t_addresses": "T-адреса",
|
||||
@@ -1186,6 +1289,7 @@
|
||||
"transfer_to": "Перевести на:",
|
||||
"transparent": "Прозрачный",
|
||||
"transparent_address": "Прозрачный адрес",
|
||||
"try_again": "Повторить",
|
||||
"tt_addr_url": "Базовый URL для просмотра адресов в обозревателе блоков",
|
||||
"tt_address_book": "Управление сохранёнными адресами для быстрой отправки",
|
||||
"tt_auto_lock": "Заблокировать кошелёк после этого времени бездействия",
|
||||
@@ -1212,6 +1316,7 @@
|
||||
"tt_font_scale": "Масштабировать весь текст и интерфейс (1.0x = по умолчанию, до 1.5x).",
|
||||
"tt_idle_delay": "Сколько ждать перед началом майнинга",
|
||||
"tt_import_key": "Импортировать приватный ключ (zkey или tkey) в этот кошелёк",
|
||||
"tt_import_viewkey": "Импортировать экранированный ключ просмотра для наблюдения за адресом (только чтение)",
|
||||
"tt_keep_daemon": "Демон будет остановлен при запуске мастера настройки",
|
||||
"tt_language": "Язык интерфейса кошелька",
|
||||
"tt_layout_hotkey": "Горячая клавиша: стрелки влево/вправо для переключения раскладок Баланса",
|
||||
|
||||
107
res/lang/zh.json
107
res/lang/zh.json
@@ -62,6 +62,7 @@
|
||||
"backup_data": "备份与数据",
|
||||
"backup_description": "创建 wallet.dat 文件的备份。此文件包含您所有的私钥和交易历史。请将备份存放在安全的地方。",
|
||||
"backup_destination": "备份目标:",
|
||||
"backup_overwrite_confirm": "该位置已存在文件,再次保存将覆盖它。",
|
||||
"backup_source": "来源:%s",
|
||||
"backup_tip_external": "将备份存储在外部驱动器或云存储中",
|
||||
"backup_tip_multiple": "在不同位置创建多个备份",
|
||||
@@ -70,6 +71,7 @@
|
||||
"backup_title": "备份钱包",
|
||||
"backup_wallet": "备份钱包...",
|
||||
"backup_wallet_not_found": "警告:在预期位置未找到 wallet.dat",
|
||||
"backup_warn": "此文件包含您的所有私钥,请妥善保管。",
|
||||
"balance": "余额",
|
||||
"balance_history_collecting": "余额历史——正在收集数据…",
|
||||
"balance_layout": "余额布局",
|
||||
@@ -114,6 +116,10 @@
|
||||
"bootstrap_wallet_protected": "(wallet.dat 已受保护)",
|
||||
"bootstrap_warning": "现有区块数据(blocks、chainstate、notarizations)将被删除并替换。您的 wallet.dat 不会被修改或删除。",
|
||||
"cancel": "取消",
|
||||
"change_pass_confirm": "确认新密码:",
|
||||
"change_pass_current": "当前密码短语:",
|
||||
"change_pass_new": "新密码短语:",
|
||||
"change_pass_title": "更改密码短语",
|
||||
"characters": "字符",
|
||||
"chat": "聊天",
|
||||
"chat_cancel": "取消",
|
||||
@@ -180,10 +186,18 @@
|
||||
"connected_peers": "已连接节点",
|
||||
"connecting": "连接中...",
|
||||
"console": "控制台",
|
||||
"console_accents": "颜色强调",
|
||||
"console_app": "应用",
|
||||
"console_auto_scroll": "自动滚动",
|
||||
"console_available_commands": "可用命令:",
|
||||
"console_capturing_output": "正在捕获守护进程输出...",
|
||||
"console_cat_blockchain": "区块链",
|
||||
"console_cat_control": "控制",
|
||||
"console_cat_mining": "挖矿",
|
||||
"console_cat_network": "网络",
|
||||
"console_cat_raw_transactions": "原始交易",
|
||||
"console_cat_utility": "实用工具",
|
||||
"console_cat_wallet": "钱包",
|
||||
"console_clear": "清除",
|
||||
"console_clear_console": "清除控制台",
|
||||
"console_cleared": "控制台已清除",
|
||||
@@ -221,6 +235,20 @@
|
||||
"console_no_daemon": "无守护进程",
|
||||
"console_not_connected": "错误:未连接到守护进程",
|
||||
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。",
|
||||
"console_ref_builds": "生成",
|
||||
"console_ref_cancel": "取消",
|
||||
"console_ref_destructive": "谨慎",
|
||||
"console_ref_example": "示例",
|
||||
"console_ref_insert": "插入到控制台",
|
||||
"console_ref_insert_run": "插入并运行",
|
||||
"console_ref_no_match": "没有匹配的命令。",
|
||||
"console_ref_no_params": "无需参数。",
|
||||
"console_ref_optional": "可选",
|
||||
"console_ref_parameters": "参数",
|
||||
"console_ref_run": "运行",
|
||||
"console_ref_run_confirm": "立即运行 %s?这是一个有重大影响的命令。",
|
||||
"console_ref_search_hint": "按名称或用途搜索…",
|
||||
"console_ref_select_hint": "选择一个命令以查看其功能。",
|
||||
"console_rpc_reference": "RPC 命令参考",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "控制台扫描线",
|
||||
@@ -240,6 +268,9 @@
|
||||
"console_status_stopping": "停止中",
|
||||
"console_status_unknown": "未知",
|
||||
"console_tab_completion": "Tab 补全",
|
||||
"console_text_colors": "文本颜色",
|
||||
"console_toggle_accents": "切换行颜色强调",
|
||||
"console_toggle_text_color": "切换行文本颜色",
|
||||
"console_type_help": "输入 'help' 查看可用命令",
|
||||
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
|
||||
"console_zoom_in": "放大",
|
||||
@@ -302,6 +333,19 @@
|
||||
"date": "日期",
|
||||
"date_label": "日期:",
|
||||
"debug_logging": "调试日志",
|
||||
"decrypt_desc": "钱包将被导出,守护进程将使用全新的未加密钱包重新启动,并重新导入所有密钥。根据钱包大小,这可能需要几分钟。",
|
||||
"decrypt_error_title": "解密失败",
|
||||
"decrypt_step_backup": "正在备份加密钱包",
|
||||
"decrypt_step_export": "正在导出钱包密钥",
|
||||
"decrypt_step_restart": "正在重启守护进程",
|
||||
"decrypt_step_stop": "正在停止守护进程",
|
||||
"decrypt_step_unlock": "正在解锁钱包",
|
||||
"decrypt_success_desc": "您的钱包现在未加密。加密钱包的备份已保存为 wallet.dat.encrypted.bak,位于您的数据目录中。",
|
||||
"decrypt_success_title": "钱包解密成功!",
|
||||
"decrypt_title": "移除钱包加密",
|
||||
"decrypt_wait_general": "请稍候。守护进程正在导出密钥、重启并重新导入。这可能需要几分钟。",
|
||||
"decrypt_wait_restart": "正在等待守护进程完成启动...",
|
||||
"decrypt_warning": "这将移除您钱包的加密。您的私钥将以未受保护的方式存储在磁盘上。",
|
||||
"delete": "删除",
|
||||
"delete_blockchain": "删除区块链",
|
||||
"delete_blockchain_confirm": "删除并重新同步",
|
||||
@@ -314,6 +358,15 @@
|
||||
"download_bootstrap": "下载引导程序",
|
||||
"dragonx_green": "DragonX(绿色)",
|
||||
"edit": "编辑",
|
||||
"enc_confirm": "确认:",
|
||||
"enc_desc": "加密钱包会用密码短语保护您的私钥。加密后,守护进程将重新启动。",
|
||||
"enc_encrypting": "正在加密钱包...",
|
||||
"enc_pin_desc": "4-8 位 PIN 码可让您无需每次输入完整密码短语即可解锁钱包。",
|
||||
"enc_pin_set_ok": "PIN 码设置成功",
|
||||
"enc_pin_skipped": "已跳过 PIN。您可以稍后在设置中设置。",
|
||||
"enc_pin_vault_fail": "无法创建 PIN 保险库",
|
||||
"enc_success": "钱包加密成功!",
|
||||
"enc_wait": "请稍候,不要关闭应用程序。",
|
||||
"error": "错误",
|
||||
"error_format": "错误:%s",
|
||||
"est_time_to_block": "预计出块时间",
|
||||
@@ -346,10 +399,18 @@
|
||||
"export_keys_danger": "危险:这将导出您钱包中的所有私钥!任何获得此文件的人都可以窃取您的资金。请安全保管并在使用后删除。",
|
||||
"export_keys_include_t": "包含 T 地址(透明)",
|
||||
"export_keys_include_z": "包含 Z 地址(屏蔽)",
|
||||
"export_keys_none_addrs": "没有可导出的地址",
|
||||
"export_keys_none_result": "未导出任何密钥(%d 个中 0 个)——请解锁钱包后重试。",
|
||||
"export_keys_none_toast": "无法导出密钥——钱包已解锁吗?",
|
||||
"export_keys_not_connected": "未连接到守护进程",
|
||||
"export_keys_options": "导出选项:",
|
||||
"export_keys_partial": "已导出 %d/%d 个密钥——不完整(部分没有花费密钥,或钱包已锁定)。",
|
||||
"export_keys_partial_toast": "部分导出:%d/%d 个密钥",
|
||||
"export_keys_progress": "正在导出 %d/%d...",
|
||||
"export_keys_select_type": "请至少选择一种地址类型",
|
||||
"export_keys_success": "密钥导出成功",
|
||||
"export_keys_title": "导出所有私钥",
|
||||
"export_keys_write_fail": "写入密钥文件失败。",
|
||||
"export_private_key": "导出私钥",
|
||||
"export_tx_count": "导出 %zu 笔交易到 CSV 文件。",
|
||||
"export_tx_file_fail": "无法创建 CSV 文件",
|
||||
@@ -391,21 +452,42 @@
|
||||
"history": "历史",
|
||||
"immature_type": "未成熟",
|
||||
"import": "导入",
|
||||
"import_key_address": "地址:",
|
||||
"import_key_btn": "导入密钥",
|
||||
"import_key_done": "已导入。钱包正在重新扫描。",
|
||||
"import_key_field": "密钥",
|
||||
"import_key_formats": "支持的密钥格式:",
|
||||
"import_key_full_rescan": "(0 = 完整重扫)",
|
||||
"import_key_import": "导入",
|
||||
"import_key_label": "私钥:",
|
||||
"import_key_need_node": "请连接一个运行中的节点以导入密钥。",
|
||||
"import_key_no_valid": "输入中未找到有效密钥",
|
||||
"import_key_progress": "正在导入 %d/%d...",
|
||||
"import_key_rescan": "导入后重新扫描区块链",
|
||||
"import_key_rescanning": "正在导入并重新扫描——可能需要几分钟",
|
||||
"import_key_reveal_tip": "显示/隐藏密钥",
|
||||
"import_key_start_height": "起始高度:",
|
||||
"import_key_success": "密钥导入成功",
|
||||
"import_key_t_format": "T 地址 WIF 私钥",
|
||||
"import_key_title": "导入私钥",
|
||||
"import_key_tooltip": "输入一个或多个私钥,每行一个。\n支持 z 地址和 t 地址密钥。\n以 # 开头的行视为注释。",
|
||||
"import_key_type_tkey": "透明私钥",
|
||||
"import_key_type_unknown": "无法识别的密钥格式",
|
||||
"import_key_type_zspend": "屏蔽花费密钥",
|
||||
"import_key_type_zview": "屏蔽查看密钥(仅查看)",
|
||||
"import_key_warn": "只导入你自己拥有的密钥——它可访问该地址的资金。",
|
||||
"import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。",
|
||||
"import_key_wrong_type": "这看起来像查看密钥。请改用\"导入查看密钥\"。",
|
||||
"import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)",
|
||||
"import_private_key": "导入私钥...",
|
||||
"import_scan_hint": "0 = 从头重新扫描",
|
||||
"import_scan_label": "从区块高度开始扫描(可选)",
|
||||
"import_scan_tip": "当前高度",
|
||||
"import_scan_transparent": "透明密钥始终完整重新扫描",
|
||||
"import_viewkey_field": "查看密钥",
|
||||
"import_viewkey_note": "仅查看:查看密钥可显示地址的余额和交易,但无法花费其资金。",
|
||||
"import_viewkey_title": "导入查看密钥",
|
||||
"import_viewkey_wrong_type": "这看起来像花费密钥。请改用\"导入私钥\"。",
|
||||
"incorrect_passphrase": "密码错误",
|
||||
"incorrect_pin": "PIN 错误",
|
||||
"insufficient_funds": "余额不足以支付此金额加手续费。",
|
||||
@@ -414,6 +496,7 @@
|
||||
"keep": "保留",
|
||||
"keep_daemon": "保持守护进程运行",
|
||||
"key_export_click_retrieve": "点击从钱包中获取密钥",
|
||||
"key_export_failed": "无法导出密钥——请解锁钱包(如已加密)后重试。",
|
||||
"key_export_fetching": "正在从钱包获取密钥...",
|
||||
"key_export_private_key": "私钥:",
|
||||
"key_export_private_warning": "请保密此密钥!任何拥有此密钥的人都可以花费您的资金。切勿在网上或与不可信的人分享。",
|
||||
@@ -739,6 +822,7 @@
|
||||
"output_filename": "输出文件名:",
|
||||
"overview": "概览",
|
||||
"paste": "粘贴",
|
||||
"paste_clip_empty": "剪贴板为空",
|
||||
"paste_from_clipboard": "从剪贴板粘贴",
|
||||
"pay_from": "付款来源",
|
||||
"payment_request": "付款请求",
|
||||
@@ -790,7 +874,14 @@
|
||||
"peers_upper": "节点",
|
||||
"peers_version": "版本",
|
||||
"pending": "待处理",
|
||||
"pin_change_desc": "更改您的解锁 PIN 码。您需要当前 PIN 码和新 PIN 码。",
|
||||
"pin_confirm_new_label": "确认新 PIN 码:",
|
||||
"pin_current_label": "当前 PIN 码:",
|
||||
"pin_new_label": "新 PIN 码(4-8 位):",
|
||||
"pin_not_set": "未设置 PIN。使用密码解锁。",
|
||||
"pin_remove_desc": "输入当前 PIN 码以确认移除。解锁时您需要使用完整的密码短语。",
|
||||
"pin_setup_desc": "设置一个 4-8 位 PIN 码以快速解锁钱包。您的钱包密码短语将使用此 PIN 码加密并存储在本地。",
|
||||
"pin_wallet_passphrase": "钱包密码短语:",
|
||||
"ping": "延迟",
|
||||
"portfolio_add_entry": "添加条目",
|
||||
"portfolio_add_to": "添加到投资组合",
|
||||
@@ -829,6 +920,9 @@
|
||||
"portfolio_remove_from": "从投资组合中移除",
|
||||
"portfolio_revert": "还原",
|
||||
"portfolio_save": "保存",
|
||||
"portfolio_save_need_address": "添加至少一个地址以保存。",
|
||||
"portfolio_save_need_name": "输入名称以保存此分组。",
|
||||
"portfolio_save_need_price": "输入大于 0 的手动价格以保存。",
|
||||
"portfolio_search": "搜索地址…",
|
||||
"portfolio_search_icons": "搜索图标…",
|
||||
"portfolio_select_all": "全部",
|
||||
@@ -1064,7 +1158,8 @@
|
||||
"settings_gradient_bg": "渐变背景",
|
||||
"settings_gradient_desc": "用平滑渐变替换纹理背景",
|
||||
"settings_idle_after": "之后",
|
||||
"settings_import_key": "导入密钥...",
|
||||
"settings_import_key": "导入私钥...",
|
||||
"settings_import_viewkey": "导入查看密钥...",
|
||||
"settings_language_note": "注意:部分文本需要重启才能更新",
|
||||
"settings_lock_now": "立即锁定",
|
||||
"settings_locked": "已锁定",
|
||||
@@ -1151,6 +1246,14 @@
|
||||
"submitting_transaction": "正在提交交易...",
|
||||
"success": "成功",
|
||||
"summary": "摘要",
|
||||
"sweep_button": "归集",
|
||||
"sweep_caveat": "导入密钥以签署一笔将其所有资金转移到您地址的交易。密钥将保留在钱包中,余额为零。",
|
||||
"sweep_dest_label": "将归集资金发送到",
|
||||
"sweep_dest_new": "新的屏蔽地址(推荐)",
|
||||
"sweep_done": "完成 — 资金已归集到您的地址。",
|
||||
"sweep_to": "归集到:",
|
||||
"sweep_toggle": "归集到我的钱包(不保留密钥)",
|
||||
"sweep_tx": "交易:",
|
||||
"syncing": "同步中...",
|
||||
"t_address": "T 地址",
|
||||
"t_addresses": "T 地址",
|
||||
@@ -1186,6 +1289,7 @@
|
||||
"transfer_to": "转账至:",
|
||||
"transparent": "透明",
|
||||
"transparent_address": "透明地址",
|
||||
"try_again": "重试",
|
||||
"tt_addr_url": "在区块浏览器中查看地址的基础 URL",
|
||||
"tt_address_book": "管理已保存的地址以快速发送",
|
||||
"tt_auto_lock": "在此不活动时间后锁定钱包",
|
||||
@@ -1212,6 +1316,7 @@
|
||||
"tt_font_scale": "缩放所有文本和界面(1.0x = 默认,最大 1.5x)。",
|
||||
"tt_idle_delay": "开始挖矿前等待多长时间",
|
||||
"tt_import_key": "将私钥(zkey 或 tkey)导入此钱包",
|
||||
"tt_import_viewkey": "导入屏蔽查看密钥以查看某个地址(只读)",
|
||||
"tt_keep_daemon": "运行设置向导时守护进程仍会停止",
|
||||
"tt_language": "钱包界面语言",
|
||||
"tt_layout_hotkey": "快捷键:左/右箭头键切换余额布局",
|
||||
|
||||
814
src/app.cpp
814
src/app.cpp
File diff suppressed because it is too large
Load Diff
44
src/app.h
44
src/app.h
@@ -300,8 +300,18 @@ public:
|
||||
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
|
||||
// callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
|
||||
void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
|
||||
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback);
|
||||
|
||||
// callback(success, errorOrEmpty, importedAddress). address is "" on failure or when the RPC
|
||||
// returns none; the import routes to z_importviewingkey / z_importkey / importprivkey by key type.
|
||||
// startHeight > 0 rescans from that block (shielded RPCs only; ignored for transparent WIF).
|
||||
void importPrivateKey(const std::string& key, int startHeight,
|
||||
std::function<void(bool, const std::string&, const std::string&)> callback);
|
||||
|
||||
// Sweep a spending key: import it (rescan) then z_sendmany ALL its funds (balance − fee) to a
|
||||
// destination you own — a freshly generated shielded address when destMode == 0, else destExisting.
|
||||
// Drives the sweep_step_ / sweep_status_ / sweep_txid_ state; reuses the async-operation tracker.
|
||||
void sweepPrivateKey(const std::string& key, int startHeight, int destMode,
|
||||
const std::string& destExisting);
|
||||
|
||||
// Wallet backup
|
||||
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
|
||||
// Export the wallet's BIP39 seed phrase (z_exportmnemonic). The callback receives
|
||||
@@ -370,7 +380,8 @@ public:
|
||||
std::string screenshotDir() const; // <config>/screenshots (fixed; sweeps overwrite in place)
|
||||
|
||||
// Dialog triggers (used by settings page to open modal dialogs)
|
||||
void showImportKeyDialog() { show_import_key_ = true; }
|
||||
void showImportKeyDialog() { import_view_mode_ = false; show_import_key_ = true; } // spending
|
||||
void showImportViewingKeyDialog() { import_view_mode_ = true; show_import_key_ = true; } // watch-only
|
||||
void showExportKeyDialog() { show_export_key_ = true; }
|
||||
void showBackupDialog() { show_backup_ = true; }
|
||||
void showSeedBackupDialog() { show_seed_backup_ = true; }
|
||||
@@ -687,6 +698,14 @@ private:
|
||||
std::string exchange_chart_key_; // "<identifier>:<BASE>/<QUOTE>" of the loaded series ("" = none)
|
||||
bool exchange_chart_fetch_in_flight_ = false;
|
||||
std::chrono::steady_clock::time_point exchange_chart_last_fetch_{};
|
||||
// Per-pair candle cache: switching back to a recently-viewed venue loads instantly (no re-fetch)
|
||||
// instead of overwriting the single active buffer. Keyed like exchange_chart_key_.
|
||||
struct ExchangeChartCache {
|
||||
std::vector<std::pair<std::time_t, double>> closeIntraday, closeDaily;
|
||||
std::vector<data::Candle> ohlcIntraday, ohlcDaily;
|
||||
std::chrono::steady_clock::time_point fetchedAt{};
|
||||
};
|
||||
std::unordered_map<std::string, ExchangeChartCache> exchange_chart_cache_;
|
||||
util::AsyncTaskManager async_tasks_;
|
||||
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
|
||||
|
||||
@@ -789,11 +808,28 @@ private:
|
||||
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
|
||||
|
||||
// Export/Import state
|
||||
std::string export_result_;
|
||||
char export_result_[256] = {0}; // SECRET exported key — fixed buffer so it can be sodium_memzero'd
|
||||
bool export_in_progress_ = false; // async key fetch running (spinner + disable Export)
|
||||
bool export_error_ = false; // last export returned no key (locked wallet / failure)
|
||||
char import_key_input_[512] = {0};
|
||||
std::string export_address_;
|
||||
std::string import_status_;
|
||||
bool import_success_ = false;
|
||||
bool import_key_reveal_ = false; // show the key in plaintext (default masked)
|
||||
bool import_in_progress_ = false; // an import + rescan is running (disable/spinner)
|
||||
std::string import_result_address_; // address imported on success (shown as a copy field)
|
||||
bool import_view_mode_ = false; // dialog mode: false = spending key, true = viewing key
|
||||
char import_key_scan_height_[16] = {0}; // optional rescan start height (shielded spend / viewing-key imports)
|
||||
// --- Sweep: import a spending key then move all its funds to one of your own addresses, instead
|
||||
// of keeping the key in the wallet (spending-key / non-view mode only). ---
|
||||
bool import_sweep_mode_ = false; // sweep instead of a plain import
|
||||
int sweep_dest_mode_ = 0; // destination: 0 = fresh shielded address, 1 = an existing one
|
||||
char sweep_dest_pick_[128] = {0}; // chosen existing destination (sweep_dest_mode_ == 1)
|
||||
enum class SweepStep { Idle, Running, Done, Error };
|
||||
SweepStep sweep_step_ = SweepStep::Idle;
|
||||
std::string sweep_status_; // progress / error text
|
||||
std::string sweep_txid_; // sweep transaction id (on success)
|
||||
std::string sweep_dest_shown_; // the destination address the funds were swept to
|
||||
std::string backup_status_;
|
||||
bool backup_success_ = false;
|
||||
|
||||
|
||||
@@ -1806,6 +1806,19 @@ void App::refreshExchangeChart()
|
||||
if (key == exchange_chart_key_ && state_.market.exchange_chart_active &&
|
||||
std::chrono::steady_clock::now() - exchange_chart_last_fetch_ < std::chrono::minutes(30))
|
||||
return;
|
||||
// Cached from a recent view of this pair -> load instantly, no re-fetch (switching back is snappy).
|
||||
if (auto it = exchange_chart_cache_.find(key);
|
||||
it != exchange_chart_cache_.end() &&
|
||||
std::chrono::steady_clock::now() - it->second.fetchedAt < std::chrono::minutes(30)) {
|
||||
state_.market.exchange_chart_intraday = it->second.closeIntraday;
|
||||
state_.market.exchange_chart_daily = it->second.closeDaily;
|
||||
state_.market.exchange_ohlc_intraday = it->second.ohlcIntraday;
|
||||
state_.market.exchange_ohlc_daily = it->second.ohlcDaily;
|
||||
state_.market.exchange_chart_active = true;
|
||||
exchange_chart_key_ = key;
|
||||
exchange_chart_last_fetch_ = it->second.fetchedAt;
|
||||
return;
|
||||
}
|
||||
// Switching pairs: fall back to the aggregate until the new venue's candles arrive.
|
||||
if (key != exchange_chart_key_) state_.market.exchange_chart_active = false;
|
||||
|
||||
@@ -1824,18 +1837,25 @@ void App::refreshExchangeChart()
|
||||
return [this, key, ohlcIntra = std::move(ohlcIntra), ohlcDaily = std::move(ohlcDaily)]() mutable {
|
||||
exchange_chart_fetch_in_flight_ = false;
|
||||
if (!ohlcDaily.empty() || !ohlcIntra.empty()) {
|
||||
// Derive close series (line + change%) from the OHLC (candles).
|
||||
std::vector<std::pair<std::time_t, double>> closeIntra, closeDaily;
|
||||
closeIntra.reserve(ohlcIntra.size()); closeDaily.reserve(ohlcDaily.size());
|
||||
for (const auto& c : ohlcIntra) closeIntra.emplace_back(c.time, c.close);
|
||||
for (const auto& c : ohlcDaily) closeDaily.emplace_back(c.time, c.close);
|
||||
state_.market.exchange_chart_intraday = std::move(closeIntra);
|
||||
state_.market.exchange_chart_daily = std::move(closeDaily);
|
||||
state_.market.exchange_ohlc_intraday = std::move(ohlcIntra);
|
||||
state_.market.exchange_ohlc_daily = std::move(ohlcDaily);
|
||||
// Build a cache entry (OHLC + derived close series for the line/change%), publish it to
|
||||
// the active buffer, and keep it so switching back to this pair later is instant.
|
||||
ExchangeChartCache e;
|
||||
e.ohlcIntraday = std::move(ohlcIntra);
|
||||
e.ohlcDaily = std::move(ohlcDaily);
|
||||
e.closeIntraday.reserve(e.ohlcIntraday.size());
|
||||
e.closeDaily.reserve(e.ohlcDaily.size());
|
||||
for (const auto& c : e.ohlcIntraday) e.closeIntraday.emplace_back(c.time, c.close);
|
||||
for (const auto& c : e.ohlcDaily) e.closeDaily.emplace_back(c.time, c.close);
|
||||
e.fetchedAt = std::chrono::steady_clock::now();
|
||||
state_.market.exchange_chart_intraday = e.closeIntraday;
|
||||
state_.market.exchange_chart_daily = e.closeDaily;
|
||||
state_.market.exchange_ohlc_intraday = e.ohlcIntraday;
|
||||
state_.market.exchange_ohlc_daily = e.ohlcDaily;
|
||||
state_.market.exchange_chart_active = true;
|
||||
exchange_chart_key_ = key;
|
||||
exchange_chart_last_fetch_ = std::chrono::steady_clock::now();
|
||||
exchange_chart_last_fetch_ = e.fetchedAt;
|
||||
if (exchange_chart_cache_.size() >= 16) exchange_chart_cache_.clear(); // bound it (many venues)
|
||||
exchange_chart_cache_[key] = std::move(e);
|
||||
} else {
|
||||
// Both ranges empty -> the venue's API failed/changed; keep the aggregate.
|
||||
state_.market.exchange_chart_active = false;
|
||||
@@ -2901,10 +2921,11 @@ void App::exportAllKeys(std::function<void(const std::string&, int, int)> callba
|
||||
}
|
||||
}
|
||||
|
||||
void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, const std::string&)> callback)
|
||||
void App::importPrivateKey(const std::string& rawKey, int startHeight,
|
||||
std::function<void(bool, const std::string&, const std::string&)> callback)
|
||||
{
|
||||
if (!state_.connected || !rpc_ || !worker_) {
|
||||
if (callback) callback(false, "Not connected");
|
||||
if (callback) callback(false, "Not connected", "");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2914,23 +2935,35 @@ void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, c
|
||||
while (!key.empty() && (key.front()==' '||key.front()=='\t'||key.front()=='\n'||key.front()=='\r')) key.erase(key.begin());
|
||||
while (!key.empty() && (key.back()==' '||key.back()=='\t'||key.back()=='\n'||key.back()=='\r')) key.pop_back();
|
||||
|
||||
// Reject anything that doesn't look like a Z/T private key before handing it to the daemon (the
|
||||
// dialog's indicator and this guard now share isRecognizedPrivateKey, so they can't disagree).
|
||||
if (!services::WalletSecurityController::isRecognizedPrivateKey(key)) {
|
||||
if (callback) callback(false, "Unrecognized private-key format.");
|
||||
// Reject anything that isn't a recognized Z/T private key or shielded viewing key before handing
|
||||
// it to the daemon (the dialog's indicator and this guard share isRecognizedImportKey).
|
||||
if (!services::WalletSecurityController::isRecognizedImportKey(key)) {
|
||||
if (callback) callback(false, "Unrecognized key format.", "");
|
||||
return;
|
||||
}
|
||||
|
||||
const bool viewing = services::WalletSecurityController::isViewingKey(key);
|
||||
const bool shielded = services::WalletSecurityController::classifyPrivateKey(key)
|
||||
== services::WalletSecurityController::KeyKind::Shielded;
|
||||
// Run on the worker thread — import requests a full rescan (rescan=true), so the
|
||||
// synchronous curl call can take many seconds; never block the UI thread on it.
|
||||
worker_->post([this, key, shielded, callback]() -> rpc::RPCWorker::MainCb {
|
||||
std::string err;
|
||||
worker_->post([this, key, viewing, shielded, startHeight, callback]() -> rpc::RPCWorker::MainCb {
|
||||
std::string err, addr;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Import private key");
|
||||
if (shielded) rpc_->call("z_importkey", {key, "yes"}); // rescan
|
||||
else rpc_->call("importprivkey", {key, "", true}); // label "", rescan
|
||||
rpc::RPCClient::TraceScope trace("Settings / Import key");
|
||||
std::string method;
|
||||
nlohmann::json params;
|
||||
if (viewing) { method = "z_importviewingkey"; params = {key, "yes"}; } // watch-only
|
||||
else if (shielded) { method = "z_importkey"; params = {key, "yes"}; }
|
||||
else { method = "importprivkey"; params = {key, "", true}; }
|
||||
// A start height (shielded RPCs only) rescans from that block instead of genesis.
|
||||
if (startHeight > 0 && (viewing || shielded)) params.push_back(startHeight);
|
||||
nlohmann::json r = rpc_->call(method, params);
|
||||
// z_import* return {type,address}; importprivkey returns the t-address string.
|
||||
if (r.is_object() && r.contains("address") && r["address"].is_string())
|
||||
addr = r["address"].get<std::string>();
|
||||
else if (r.is_string())
|
||||
addr = r.get<std::string>();
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
} catch (...) {
|
||||
@@ -2938,16 +2971,169 @@ void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, c
|
||||
// below would never run, leaving a stuck "Importing…" spinner.
|
||||
err = "Import failed (unknown error)";
|
||||
}
|
||||
return [this, shielded, err, callback]() {
|
||||
return [this, err, addr, callback]() {
|
||||
if (!err.empty()) {
|
||||
if (callback) callback(false, err);
|
||||
if (callback) callback(false, err, "");
|
||||
return;
|
||||
}
|
||||
invalidateAddressValidationCache();
|
||||
refreshAddresses();
|
||||
if (callback) callback(true, services::WalletSecurityController::importSuccessMessage(
|
||||
shielded ? services::WalletSecurityController::KeyKind::Shielded
|
||||
: services::WalletSecurityController::KeyKind::Transparent));
|
||||
if (callback) callback(true, "", addr);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Sweep a spending key: import it (a full rescan populates its UTXOs/notes — the stock node has no
|
||||
// address index, so there is no way to enumerate them without importing) then z_sendmany ALL of the
|
||||
// key's funds (balance − fee) to a destination the user already controls. The imported key is left in
|
||||
// the wallet with an empty balance (there is no remove-key RPC) — the point is that the funds now sit
|
||||
// on the user's own key. z_sendmany (not z_mergetoaddress): it moves a single-UTXO transparent source
|
||||
// and fails loudly rather than silently leaving a remainder. Fund-moving — see the reviewed design.
|
||||
void App::sweepPrivateKey(const std::string& rawKey, int startHeight, int destMode,
|
||||
const std::string& destExisting)
|
||||
{
|
||||
if (!state_.connected || !rpc_ || !worker_) {
|
||||
sweep_status_ = "Not connected to the daemon.";
|
||||
sweep_step_ = SweepStep::Error;
|
||||
return;
|
||||
}
|
||||
// Trim (manual entry doesn't go through the dialog's Paste trimmer).
|
||||
std::string key(rawKey);
|
||||
while (!key.empty() && (key.front()==' '||key.front()=='\t'||key.front()=='\n'||key.front()=='\r')) key.erase(key.begin());
|
||||
while (!key.empty() && (key.back()==' '||key.back()=='\t'||key.back()=='\n'||key.back()=='\r')) key.pop_back();
|
||||
|
||||
// Sweeping requires a SPENDING key (transparent WIF or shielded z-spending key) — a viewing key
|
||||
// can't sign, and this must never accidentally route to z_importviewingkey.
|
||||
if (!services::WalletSecurityController::isRecognizedPrivateKey(key)) {
|
||||
sweep_status_ = "Enter a spending key (a viewing key can't move funds).";
|
||||
sweep_step_ = SweepStep::Error;
|
||||
return;
|
||||
}
|
||||
|
||||
sweep_step_ = SweepStep::Running;
|
||||
sweep_status_ = "Importing key & rescanning…";
|
||||
sweep_txid_.clear();
|
||||
sweep_dest_shown_.clear();
|
||||
|
||||
const bool shielded = services::WalletSecurityController::classifyPrivateKey(key)
|
||||
== services::WalletSecurityController::KeyKind::Shielded;
|
||||
const double fee = DRAGONX_DEFAULT_FEE;
|
||||
worker_->post([this, key, startHeight, destMode, destExisting, shielded, fee]() -> rpc::RPCWorker::MainCb {
|
||||
std::string err, dest, sourceAddr, amountStr;
|
||||
double amount = 0.0;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Sweep key");
|
||||
|
||||
// For a shielded key this daemon's z_importkey returns null (no address), so snapshot the
|
||||
// z-address set before the import and diff it after to find the newly-controlled address.
|
||||
std::vector<std::string> preZ;
|
||||
bool preZok = false;
|
||||
auto listZ = [&](std::vector<std::string>& out) {
|
||||
out.clear();
|
||||
auto la = rpc_->call("z_listaddresses");
|
||||
if (la.is_array()) for (auto& a : la) if (a.is_string()) out.push_back(a.get<std::string>());
|
||||
};
|
||||
if (shielded) { try { listZ(preZ); preZok = true; } catch (...) { preZok = false; } }
|
||||
|
||||
// 1. Import the key (blocks until the rescan completes → its funds become spendable).
|
||||
std::string method;
|
||||
nlohmann::json params;
|
||||
if (shielded) { method = "z_importkey"; params = {key, "yes"}; }
|
||||
else { method = "importprivkey"; params = {key, "", true}; }
|
||||
if (startHeight > 0 && shielded) params.push_back(startHeight);
|
||||
nlohmann::json r = rpc_->call(method, params);
|
||||
|
||||
// 2. Determine the swept address. importprivkey returns the t-address string; z_importkey
|
||||
// returns null, so diff the z-address list to find the one the key just added.
|
||||
if (shielded) {
|
||||
// A failed pre-snapshot would make the diff treat pre-existing addresses as "new" and
|
||||
// could pick the WRONG source — refuse rather than risk moving another address's funds.
|
||||
if (!preZok) throw std::runtime_error("Couldn't read the wallet's addresses to identify the swept key. Try again.");
|
||||
std::vector<std::string> postZ;
|
||||
listZ(postZ);
|
||||
std::vector<std::string> added;
|
||||
for (const auto& s : postZ)
|
||||
if (std::find(preZ.begin(), preZ.end(), s) == preZ.end()) added.push_back(s);
|
||||
if (added.size() == 1) sourceAddr = added[0];
|
||||
else if (added.empty()) throw std::runtime_error("__ALREADY__"); // key already in wallet
|
||||
else throw std::runtime_error("Could not identify the key's address after import.");
|
||||
} else {
|
||||
if (r.is_string()) sourceAddr = r.get<std::string>();
|
||||
else if (r.is_object() && r.contains("address") && r["address"].is_string())
|
||||
sourceAddr = r["address"].get<std::string>();
|
||||
}
|
||||
if (sourceAddr.empty()) throw std::runtime_error("Could not determine the key's address.");
|
||||
|
||||
// 3. Confirm the key holds spendable (confirmed) funds. z_getbalance covers t- and z-addrs.
|
||||
auto balAt = [&](int minconf) -> double {
|
||||
try {
|
||||
nlohmann::json b = rpc_->call("z_getbalance", {sourceAddr, minconf});
|
||||
return b.is_number() ? b.get<double>()
|
||||
: b.is_string() ? std::stod(b.get<std::string>()) : 0.0;
|
||||
} catch (...) { return 0.0; }
|
||||
};
|
||||
double bal = balAt(1);
|
||||
if (bal <= 0.0) {
|
||||
throw std::runtime_error(balAt(0) > 0.0 ? "__UNCONFIRMED__" : "__NOFUNDS__");
|
||||
}
|
||||
if (bal <= fee) throw std::runtime_error("__DUST__");
|
||||
|
||||
// 4. Resolve the destination — a fresh shielded address by default, else the picked one.
|
||||
if (destMode == 0) dest = rpc_->call("z_getnewaddress").get<std::string>();
|
||||
else dest = destExisting;
|
||||
if (dest.empty()) throw std::runtime_error("No destination address for the sweep.");
|
||||
|
||||
// Send everything; the fee consumes the remainder (no change/residue). Compute in integer
|
||||
// satoshis so the fixed-decimal amount is exact for all realistic balances (double bal-fee
|
||||
// is exact below 2^52 sat ≈ 45M DRGX; above that the JSON-parsed balance is itself lossy —
|
||||
// a swept key that large is implausible and would fail safe with funds left in place).
|
||||
const long long feeSats = (long long)std::llround(fee * 100000000.0);
|
||||
const long long amtSats = (long long)std::llround(bal * 100000000.0) - feeSats;
|
||||
if (amtSats <= 0) throw std::runtime_error("__DUST__");
|
||||
char amtBuf[32];
|
||||
snprintf(amtBuf, sizeof(amtBuf), "%lld.%08lld",
|
||||
amtSats / 100000000LL, amtSats % 100000000LL);
|
||||
amountStr = amtBuf;
|
||||
amount = (double)amtSats / 100000000.0; // for send bookkeeping / display only
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
} catch (...) {
|
||||
err = "Sweep failed (unknown error)";
|
||||
}
|
||||
return [this, err, sourceAddr, dest, amount, amountStr, fee]() {
|
||||
invalidateAddressValidationCache();
|
||||
refreshAddresses();
|
||||
if (!err.empty()) {
|
||||
if (err == "__NOFUNDS__") sweep_status_ = "This key holds no funds to sweep.";
|
||||
else if (err == "__UNCONFIRMED__") sweep_status_ = "This key's funds are still unconfirmed — try again shortly.";
|
||||
else if (err == "__ALREADY__") sweep_status_ = "This key is already in your wallet — use Send to move its funds.";
|
||||
else if (err == "__DUST__") sweep_status_ = "This key's balance is too small to cover the network fee.";
|
||||
else sweep_status_ = err;
|
||||
sweep_step_ = SweepStep::Error;
|
||||
return;
|
||||
}
|
||||
sweep_dest_shown_ = dest;
|
||||
sweep_status_ = "Sweeping funds to your address…";
|
||||
// Send all funds from the swept address to the destination through the tested z_sendmany
|
||||
// wrapper (fixed-decimal amount + async-op tracking). z_sendmany needs every input in one
|
||||
// transaction to cover the full balance, so it fails loudly rather than partial-sweeping.
|
||||
nlohmann::json recipients = nlohmann::json::array();
|
||||
nlohmann::json rcp;
|
||||
rcp["address"] = dest;
|
||||
rcp["amount"] = amountStr; // exact integer-satoshi fixed-decimal string
|
||||
recipients.push_back(rcp);
|
||||
submitZSendMany(sourceAddr, dest, amount, fee, "", recipients, "Settings / Sweep key",
|
||||
/*markFeeGapRetry*/ false, [this](bool ok, const std::string& result) {
|
||||
if (ok) {
|
||||
sweep_txid_ = result;
|
||||
sweep_status_.clear();
|
||||
sweep_step_ = SweepStep::Done;
|
||||
} else {
|
||||
sweep_status_ = result.empty() ? "The sweep transaction failed." : result;
|
||||
sweep_step_ = SweepStep::Error;
|
||||
}
|
||||
refreshBalance();
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1125,35 +1125,33 @@ void App::renderEncryptWalletDialog() {
|
||||
// Encrypt wallet dialog — multi-phase: passphrase → encrypting → PIN setup
|
||||
if (show_encrypt_dialog_) {
|
||||
const char* dlgTitle = (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup)
|
||||
? "Quick-Unlock PIN" : "Encrypt Wallet";
|
||||
? TR("wiz_pin_title") : TR("settings_encrypt_wallet");
|
||||
|
||||
// Prevent closing via X button while encrypting
|
||||
bool canClose = (encrypt_dialog_phase_ != EncryptDialogPhase::Encrypting);
|
||||
bool* pOpen = canClose ? &show_encrypt_dialog_ : nullptr;
|
||||
|
||||
if (BeginOverlayDialog(dlgTitle, pOpen, 460.0f, 0.94f)) {
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = dlgTitle; ov.p_open = pOpen;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 480.0f; ov.idSuffix = "encrypt";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
// ---- Phase 1: Passphrase entry ----
|
||||
if (encrypt_dialog_phase_ == EncryptDialogPhase::PassphraseEntry) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1));
|
||||
ImGui::TextWrapped(ICON_MD_WARNING
|
||||
" If you lose your passphrase, you lose access to your funds.");
|
||||
ImGui::PopStyleColor();
|
||||
DialogWarningHeader(TR("wiz_encrypt_warning"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextWrapped("Encrypting your wallet protects your private keys "
|
||||
"with a passphrase. After encryption, the daemon will restart.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", TR("enc_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Passphrase:");
|
||||
ImGui::TextUnformatted(TR("wiz_passphrase"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##enc_pass", encrypt_pass_buf_, sizeof(encrypt_pass_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("Confirm:");
|
||||
ImGui::TextUnformatted(TR("enc_confirm"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##enc_confirm", encrypt_confirm_buf_, sizeof(encrypt_confirm_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
@@ -1174,7 +1172,7 @@ void App::renderEncryptWalletDialog() {
|
||||
}
|
||||
int classes = (int)hasDigit + (int)hasLower + (int)hasUpper + (int)hasSymbol;
|
||||
|
||||
const char* strengthLabel = "Weak";
|
||||
const char* strengthLabel = TR("wiz_strength_weak");
|
||||
ImVec4 strengthCol(0.9f, 0.2f, 0.2f, 1.0f);
|
||||
float strengthPct = 0.25f;
|
||||
int tier = 0; // 0=Weak, 1=Fair, 2=Good, 3=Strong
|
||||
@@ -1183,9 +1181,9 @@ void App::renderEncryptWalletDialog() {
|
||||
else if (len >= 8) tier = 1;
|
||||
// Downgrade one tier when only a single character class is used.
|
||||
if (classes <= 1 && tier > 0) tier -= 1;
|
||||
if (tier == 3) { strengthLabel = "Strong"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; }
|
||||
else if (tier == 2) { strengthLabel = "Good"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; }
|
||||
else if (tier == 1) { strengthLabel = "Fair"; strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
|
||||
if (tier == 3) { strengthLabel = TR("wiz_strength_strong"); strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; }
|
||||
else if (tier == 2) { strengthLabel = TR("wiz_strength_good"); strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; }
|
||||
else if (tier == 1) { strengthLabel = TR("wiz_strength_fair"); strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
|
||||
|
||||
float barW = ImGui::GetContentRegionAvail().x;
|
||||
float barH = 4.0f;
|
||||
@@ -1197,7 +1195,7 @@ void App::renderEncryptWalletDialog() {
|
||||
dl->AddRectFilled(p, ImVec2(p.x + barW * strengthPct, p.y + barH),
|
||||
ImGui::ColorConvertFloat4ToU32(strengthCol), 2.0f);
|
||||
ImGui::Dummy(ImVec2(barW, barH));
|
||||
ImGui::Text("Strength: %s", strengthLabel);
|
||||
ImGui::Text(TR("wiz_strength"), strengthLabel);
|
||||
}
|
||||
|
||||
if (!encrypt_status_.empty()) {
|
||||
@@ -1210,7 +1208,7 @@ void App::renderEncryptWalletDialog() {
|
||||
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
||||
if (ui::material::TactileButton("Encrypt Wallet", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_encrypt_wallet"), ImVec2(btnW, 40))) {
|
||||
std::string pass(encrypt_pass_buf_);
|
||||
enc_dlg_saved_passphrase_ = pass;
|
||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||
@@ -1222,7 +1220,7 @@ void App::renderEncryptWalletDialog() {
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Cancel", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
||||
show_encrypt_dialog_ = false;
|
||||
@@ -1231,7 +1229,7 @@ void App::renderEncryptWalletDialog() {
|
||||
// ---- Phase 2: Encrypting in progress ----
|
||||
} else if (encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
|
||||
const char* statusTitle = encrypt_in_progress_
|
||||
? "Encrypting wallet..." : encrypt_status_.c_str();
|
||||
? TR("enc_encrypting") : encrypt_status_.c_str();
|
||||
ImGui::Text("%s", statusTitle);
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -1258,7 +1256,7 @@ void App::renderEncryptWalletDialog() {
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1,1,1,0.4f), "Please wait, do not close the application.");
|
||||
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", TR("enc_wait"));
|
||||
|
||||
// Transition to PIN phase when encryption finishes successfully
|
||||
if (!encrypt_in_progress_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
|
||||
@@ -1268,23 +1266,20 @@ void App::renderEncryptWalletDialog() {
|
||||
// ---- Phase 3: PIN setup (after successful encryption) ----
|
||||
} else if (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.9f, 0.5f, 1));
|
||||
ImGui::Text(ICON_MD_CHECK_CIRCLE " Wallet encrypted successfully!");
|
||||
ImGui::Text(ICON_MD_CHECK_CIRCLE " %s", TR("enc_success"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextWrapped("A 4-8 digit PIN lets you unlock your wallet "
|
||||
"without typing the full passphrase every time.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", TR("enc_pin_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("PIN (4-8 digits):");
|
||||
ImGui::TextUnformatted(TR("wiz_pin_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##enc_dlg_pin", enc_dlg_pin_buf_, sizeof(enc_dlg_pin_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("Confirm PIN:");
|
||||
ImGui::TextUnformatted(TR("wiz_pin_confirm"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##enc_dlg_pin_confirm", enc_dlg_pin_confirm_buf_,
|
||||
sizeof(enc_dlg_pin_confirm_buf_),
|
||||
@@ -1304,7 +1299,7 @@ void App::renderEncryptWalletDialog() {
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
|
||||
ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_);
|
||||
if (ui::material::TactileButton("Set PIN", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
enc_dlg_pin_status_.clear();
|
||||
std::string savedPass = enc_dlg_saved_passphrase_;
|
||||
@@ -1317,7 +1312,7 @@ void App::renderEncryptWalletDialog() {
|
||||
settings_->setPinEnabled(true);
|
||||
settings_->save();
|
||||
pin_in_progress_ = false;
|
||||
ui::Notifications::instance().info("PIN set successfully");
|
||||
ui::Notifications::instance().info(TR("enc_pin_set_ok"));
|
||||
// Clean up
|
||||
if (!enc_dlg_saved_passphrase_.empty()) {
|
||||
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
||||
@@ -1328,20 +1323,20 @@ void App::renderEncryptWalletDialog() {
|
||||
memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
|
||||
show_encrypt_dialog_ = false;
|
||||
} else {
|
||||
enc_dlg_pin_status_ = "Failed to create PIN vault";
|
||||
enc_dlg_pin_status_ = TR("enc_pin_vault_fail");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
enc_dlg_pin_status_ = "Failed to create PIN vault";
|
||||
enc_dlg_pin_status_ = TR("enc_pin_vault_fail");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Skip", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW, 40))) {
|
||||
if (!enc_dlg_saved_passphrase_.empty()) {
|
||||
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
||||
enc_dlg_saved_passphrase_.size());
|
||||
@@ -1350,8 +1345,7 @@ void App::renderEncryptWalletDialog() {
|
||||
memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_));
|
||||
memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
|
||||
show_encrypt_dialog_ = false;
|
||||
ui::Notifications::instance().info(
|
||||
"PIN skipped. You can set one later in Settings.");
|
||||
ui::Notifications::instance().info(TR("enc_pin_skipped"));
|
||||
}
|
||||
}
|
||||
EndOverlayDialog();
|
||||
@@ -1367,21 +1361,25 @@ void App::renderEncryptWalletDialog() {
|
||||
|
||||
// Change passphrase dialog
|
||||
if (show_change_passphrase_) {
|
||||
if (BeginOverlayDialog("Change Passphrase", &show_change_passphrase_, 440.0f, 0.94f)) {
|
||||
|
||||
ImGui::Text("Current Passphrase:");
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("change_pass_title"); ov.p_open = &show_change_passphrase_;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 460.0f; ov.idSuffix = "changepass";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
ImGui::TextUnformatted(TR("change_pass_current"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("New Passphrase:");
|
||||
ImGui::TextUnformatted(TR("change_pass_new"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##chg_new", change_new_pass_buf_, sizeof(change_new_pass_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("Confirm New:");
|
||||
ImGui::TextUnformatted(TR("change_pass_confirm"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##chg_confirm", change_confirm_buf_, sizeof(change_confirm_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
@@ -1396,13 +1394,22 @@ void App::renderEncryptWalletDialog() {
|
||||
strlen(change_new_pass_buf_) >= 8 &&
|
||||
strcmp(change_new_pass_buf_, change_confirm_buf_) == 0;
|
||||
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
||||
if (ui::material::TactileButton("Change Passphrase", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(-1, 40))) {
|
||||
changePassphrase(std::string(change_old_pass_buf_),
|
||||
std::string(change_new_pass_buf_));
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
EndOverlayDialog();
|
||||
}
|
||||
|
||||
// Wipe the passphrase buffers if the dialog was dismissed (X / Esc /
|
||||
// outside-click) without submitting. The success path already zeroes
|
||||
// them in changePassphrase(); the failure path keeps them for retry.
|
||||
if (!show_change_passphrase_) {
|
||||
memset(change_old_pass_buf_, 0, sizeof(change_old_pass_buf_));
|
||||
memset(change_new_pass_buf_, 0, sizeof(change_new_pass_buf_));
|
||||
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1427,26 +1434,21 @@ void App::renderDecryptWalletDialog() {
|
||||
bool canClose = wallet_security_workflow_.canClose();
|
||||
bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr;
|
||||
|
||||
if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) {
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("decrypt_title"); ov.p_open = pOpen;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 480.0f; ov.idSuffix = "decrypt";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
// ---- Phase 0: Passphrase entry ----
|
||||
if (decryptState.phase == DecryptPhase::PassphraseEntry) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1));
|
||||
ImGui::TextWrapped(ICON_MD_WARNING
|
||||
" This will remove encryption from your wallet. "
|
||||
"Your private keys will be stored unprotected on disk.");
|
||||
ImGui::PopStyleColor();
|
||||
DialogWarningHeader(TR("decrypt_warning"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"The wallet will be exported, the daemon restarted with a fresh "
|
||||
"unencrypted wallet, and all keys re-imported. This may take "
|
||||
"several minutes depending on wallet size.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", TR("decrypt_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Current Passphrase:");
|
||||
ImGui::TextUnformatted(TR("change_pass_current"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
bool enterPressed = ImGui::InputText("##decrypt_pass", decrypt_pass_buf_,
|
||||
sizeof(decrypt_pass_buf_), ImGuiInputTextFlags_Password |
|
||||
@@ -1462,7 +1464,7 @@ void App::renderDecryptWalletDialog() {
|
||||
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
ImGui::BeginDisabled(!valid || decryptState.inProgress);
|
||||
if (ui::material::TactileButton("Remove Encryption", ImVec2(btnW, 40)) || (enterPressed && valid)) {
|
||||
if (ui::material::TactileButton(TR("settings_remove_encryption"), ImVec2(btnW, 40)) || (enterPressed && valid)) {
|
||||
std::string passphrase(decrypt_pass_buf_);
|
||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||
wallet_security_workflow_.start(std::chrono::steady_clock::now());
|
||||
@@ -1653,7 +1655,7 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Cancel", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||
show_decrypt_dialog_ = false;
|
||||
}
|
||||
@@ -1662,11 +1664,11 @@ void App::renderDecryptWalletDialog() {
|
||||
} else if (decryptState.phase == DecryptPhase::Working) {
|
||||
// Step checklist
|
||||
const char* stepLabels[] = {
|
||||
"Unlocking wallet",
|
||||
"Exporting wallet keys",
|
||||
"Stopping daemon",
|
||||
"Backing up encrypted wallet",
|
||||
"Restarting daemon"
|
||||
TR("decrypt_step_unlock"),
|
||||
TR("decrypt_step_export"),
|
||||
TR("decrypt_step_stop"),
|
||||
TR("decrypt_step_backup"),
|
||||
TR("decrypt_step_restart")
|
||||
};
|
||||
const int numSteps = 5;
|
||||
|
||||
@@ -1743,10 +1745,9 @@ void App::renderDecryptWalletDialog() {
|
||||
|
||||
// Step-specific hints
|
||||
if (decryptState.step == DecryptStep::RestartDaemon) {
|
||||
ImGui::TextWrapped("Waiting for the daemon to finish starting up...");
|
||||
ImGui::TextWrapped("%s", TR("decrypt_wait_restart"));
|
||||
} else {
|
||||
ImGui::TextWrapped("Please wait. The daemon is exporting keys, restarting, "
|
||||
"and re-importing. This may take several minutes.");
|
||||
ImGui::TextWrapped("%s", TR("decrypt_wait_general"));
|
||||
}
|
||||
|
||||
// Total elapsed
|
||||
@@ -1763,15 +1764,13 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "Wallet decrypted successfully!");
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "%s", TR("decrypt_success_title"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped(
|
||||
"Your wallet is now unencrypted. A backup of the encrypted wallet "
|
||||
"was saved as wallet.dat.encrypted.bak in your data directory.");
|
||||
ImGui::TextWrapped("%s", TR("decrypt_success_desc"));
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ui::material::TactileButton("Close", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("close"), ImVec2(-1, 40))) {
|
||||
show_decrypt_dialog_ = false;
|
||||
}
|
||||
|
||||
@@ -1781,23 +1780,31 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Decryption failed");
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", TR("decrypt_error_title"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", decryptState.status.c_str());
|
||||
|
||||
ImGui::Spacing();
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (ui::material::TactileButton("Try Again", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("try_again"), ImVec2(btnW, 40))) {
|
||||
wallet_security_workflow_.reset();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Close", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("close"), ImVec2(btnW, 40))) {
|
||||
show_decrypt_dialog_ = false;
|
||||
}
|
||||
}
|
||||
EndOverlayDialog();
|
||||
}
|
||||
|
||||
// Wipe the passphrase buffer if the dialog was dismissed (X / Esc / outside-
|
||||
// click) without submitting. The submit and Cancel paths already memset it;
|
||||
// this covers the BlurFloat dismiss paths. (canClose is false during Working,
|
||||
// so an in-flight decrypt cannot be dismissed here.)
|
||||
if (!show_decrypt_dialog_) {
|
||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -1809,29 +1816,28 @@ void App::renderPinDialogs() {
|
||||
|
||||
// ---- Set PIN dialog ----
|
||||
if (show_pin_setup_) {
|
||||
if (BeginOverlayDialog("Set PIN", &show_pin_setup_, 420.0f, 0.94f)) {
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Set a 4-8 digit PIN for quick wallet unlock. "
|
||||
"Your wallet passphrase will be encrypted with this PIN "
|
||||
"and stored locally.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("settings_set_pin"); ov.p_open = &show_pin_setup_;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 420.0f; ov.idSuffix = "pinsetup";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
ImGui::TextWrapped("%s", TR("pin_setup_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Wallet Passphrase:");
|
||||
ImGui::TextUnformatted(TR("pin_wallet_passphrase"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_passphrase", pin_passphrase_buf_, sizeof(pin_passphrase_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("New PIN (4-8 digits):");
|
||||
ImGui::TextUnformatted(TR("pin_new_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_new", pin_buf_, sizeof(pin_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("Confirm PIN:");
|
||||
ImGui::TextUnformatted(TR("wiz_pin_confirm"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
@@ -1848,7 +1854,7 @@ void App::renderPinDialogs() {
|
||||
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
||||
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton("Set PIN", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(-1, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Verifying passphrase...";
|
||||
|
||||
@@ -1904,30 +1910,39 @@ void App::renderPinDialogs() {
|
||||
ImGui::EndDisabled();
|
||||
EndOverlayDialog();
|
||||
}
|
||||
// Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc /
|
||||
// outside-click) without submitting. The submit path already memsets them.
|
||||
if (!show_pin_setup_) {
|
||||
memset(pin_passphrase_buf_, 0, sizeof(pin_passphrase_buf_));
|
||||
memset(pin_buf_, 0, sizeof(pin_buf_));
|
||||
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Change PIN dialog ----
|
||||
if (show_pin_change_) {
|
||||
if (BeginOverlayDialog("Change PIN", &show_pin_change_, 420.0f, 0.94f)) {
|
||||
|
||||
ImGui::TextWrapped("Change your unlock PIN. You need your current PIN and a new PIN.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("settings_change_pin"); ov.p_open = &show_pin_change_;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 420.0f; ov.idSuffix = "pinchange";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
ImGui::TextWrapped("%s", TR("pin_change_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Current PIN:");
|
||||
ImGui::TextUnformatted(TR("pin_current_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_old", pin_old_buf_, sizeof(pin_old_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("New PIN (4-8 digits):");
|
||||
ImGui::TextUnformatted(TR("pin_new_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_change_new", pin_buf_, sizeof(pin_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("Confirm New PIN:");
|
||||
ImGui::TextUnformatted(TR("pin_confirm_new_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_change_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
@@ -1944,7 +1959,7 @@ void App::renderPinDialogs() {
|
||||
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
||||
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton("Change PIN", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Changing PIN...";
|
||||
std::string oldPin(pin_old_buf_);
|
||||
@@ -1977,20 +1992,26 @@ void App::renderPinDialogs() {
|
||||
ImGui::EndDisabled();
|
||||
EndOverlayDialog();
|
||||
}
|
||||
// Wipe the PIN buffers if the dialog was dismissed without submitting.
|
||||
if (!show_pin_change_) {
|
||||
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
||||
memset(pin_buf_, 0, sizeof(pin_buf_));
|
||||
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Remove PIN dialog ----
|
||||
if (show_pin_remove_) {
|
||||
if (BeginOverlayDialog("Remove PIN", &show_pin_remove_, 400.0f, 0.94f)) {
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Enter your current PIN to confirm removal. "
|
||||
"You will need to use your full passphrase to unlock.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("settings_remove_pin"); ov.p_open = &show_pin_remove_;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 400.0f; ov.idSuffix = "pinremove";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
ImGui::TextWrapped("%s", TR("pin_remove_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Current PIN:");
|
||||
ImGui::TextUnformatted(TR("pin_current_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_remove", pin_old_buf_, sizeof(pin_old_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
@@ -2003,7 +2024,7 @@ void App::renderPinDialogs() {
|
||||
ImGui::Spacing();
|
||||
bool valid = strlen(pin_old_buf_) >= 4;
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton("Remove PIN", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Verifying PIN...";
|
||||
std::string oldPin(pin_old_buf_);
|
||||
@@ -2040,6 +2061,10 @@ void App::renderPinDialogs() {
|
||||
ImGui::EndDisabled();
|
||||
EndOverlayDialog();
|
||||
}
|
||||
// Wipe the PIN buffer if the dialog was dismissed without submitting.
|
||||
if (!show_pin_remove_) {
|
||||
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#include "ui/sidebar.h"
|
||||
#include "ui/windows/send_tab.h"
|
||||
#include "ui/windows/wallets_dialog.h"
|
||||
#include "ui/windows/export_transactions_dialog.h"
|
||||
#include "ui/windows/export_all_keys_dialog.h"
|
||||
#include "ui/windows/bootstrap_download_dialog.h"
|
||||
#include "ui/windows/daemon_download_dialog.h"
|
||||
#include "ui/windows/xmrig_download_dialog.h"
|
||||
#include "ui/pages/settings_page.h"
|
||||
@@ -261,11 +264,67 @@ void App::buildSweepCatalog()
|
||||
|
||||
// Simple bool-flag modals.
|
||||
add("modal-import-key", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; });
|
||||
[](App& a) { a.import_view_mode_ = false; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; });
|
||||
add("modal-import-viewkey", ui::NavPage::Overview,
|
||||
[](App& a) { a.import_view_mode_ = true; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; a.import_view_mode_ = false; });
|
||||
add("modal-export-key", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; });
|
||||
add("modal-export-transactions", ui::NavPage::Settings,
|
||||
[](App&) { ui::ExportTransactionsDialog::show(); }, [](App&) { ui::ExportTransactionsDialog::hide(); });
|
||||
add("modal-export-all-keys", ui::NavPage::Settings,
|
||||
[](App&) { ui::ExportAllKeysDialog::show(); }, [](App&) { ui::ExportAllKeysDialog::hide(); });
|
||||
add("modal-bootstrap", ui::NavPage::Settings,
|
||||
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
|
||||
add("modal-backup", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
||||
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
|
||||
add("modal-encrypt", ui::NavPage::Settings,
|
||||
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
|
||||
[](App& a) {
|
||||
a.show_encrypt_dialog_ = false; a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
|
||||
a.encrypt_status_.clear();
|
||||
memset(a.encrypt_pass_buf_, 0, sizeof(a.encrypt_pass_buf_));
|
||||
memset(a.encrypt_confirm_buf_, 0, sizeof(a.encrypt_confirm_buf_));
|
||||
});
|
||||
add("modal-change-passphrase", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_change_passphrase_ = true; },
|
||||
[](App& a) {
|
||||
a.show_change_passphrase_ = false; a.encrypt_status_.clear();
|
||||
memset(a.change_old_pass_buf_, 0, sizeof(a.change_old_pass_buf_));
|
||||
memset(a.change_new_pass_buf_, 0, sizeof(a.change_new_pass_buf_));
|
||||
memset(a.change_confirm_buf_, 0, sizeof(a.change_confirm_buf_));
|
||||
});
|
||||
// Remove-encryption dialog — the redesigned passphrase-entry phase (reset() keeps the
|
||||
// workflow in PassphraseEntry; nothing fires the async unlock/export/restart pyramid).
|
||||
add("modal-decrypt", ui::NavPage::Settings,
|
||||
[](App& a) { a.wallet_security_workflow_.reset(); a.show_decrypt_dialog_ = true; },
|
||||
[](App& a) {
|
||||
a.show_decrypt_dialog_ = false; a.wallet_security_workflow_.reset();
|
||||
memset(a.decrypt_pass_buf_, 0, sizeof(a.decrypt_pass_buf_));
|
||||
});
|
||||
// PIN setup / change / remove dialogs (never fire the async vault store/verify).
|
||||
add("modal-pin-setup", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_pin_setup_ = true; },
|
||||
[](App& a) {
|
||||
a.show_pin_setup_ = false; a.pin_status_.clear();
|
||||
memset(a.pin_passphrase_buf_, 0, sizeof(a.pin_passphrase_buf_));
|
||||
memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
|
||||
memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
|
||||
});
|
||||
add("modal-pin-change", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_pin_change_ = true; },
|
||||
[](App& a) {
|
||||
a.show_pin_change_ = false; a.pin_status_.clear();
|
||||
memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
|
||||
memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
|
||||
memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
|
||||
});
|
||||
add("modal-pin-remove", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_pin_remove_ = true; },
|
||||
[](App& a) {
|
||||
a.show_pin_remove_ = false; a.pin_status_.clear();
|
||||
memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
|
||||
});
|
||||
add("modal-about", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
|
||||
add("modal-settings", ui::NavPage::Settings,
|
||||
|
||||
@@ -175,6 +175,10 @@ bool Settings::load(const std::string& path)
|
||||
loadScalar(j, "portfolio_style", portfolio_style_);
|
||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||
loadScalar(j, "console_line_accents", console_line_accents_);
|
||||
loadScalar(j, "console_text_color", console_text_color_);
|
||||
loadScalar(j, "console_zoom", console_zoom_);
|
||||
if (!(console_zoom_ >= 0.25f && console_zoom_ <= 4.0f)) console_zoom_ = 1.0f; // guard bad/NaN
|
||||
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
||||
hidden_addresses_.clear();
|
||||
for (const auto& a : j["hidden_addresses"])
|
||||
@@ -425,6 +429,9 @@ bool Settings::save(const std::string& path)
|
||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||
j["portfolio_style"] = portfolio_style_;
|
||||
j["scanline_enabled"] = scanline_enabled_;
|
||||
j["console_line_accents"] = console_line_accents_;
|
||||
j["console_text_color"] = console_text_color_;
|
||||
j["console_zoom"] = console_zoom_;
|
||||
j["hidden_addresses"] = json::array();
|
||||
for (const auto& addr : hidden_addresses_)
|
||||
j["hidden_addresses"].push_back(addr);
|
||||
|
||||
@@ -189,6 +189,15 @@ public:
|
||||
bool getScanlineEnabled() const { return scanline_enabled_; }
|
||||
void setScanlineEnabled(bool v) { scanline_enabled_ = v; }
|
||||
|
||||
// Console output appearance: per-line left color accent bars, and per-channel text coloring.
|
||||
// (Defaults match the ConsoleTab statics so an upgrade re-save doesn't flip visible behavior.)
|
||||
bool getConsoleLineAccents() const { return console_line_accents_; }
|
||||
void setConsoleLineAccents(bool v) { console_line_accents_ = v; }
|
||||
bool getConsoleTextColor() const { return console_text_color_; }
|
||||
void setConsoleTextColor(bool v) { console_text_color_ = v; }
|
||||
float getConsoleZoom() const { return console_zoom_; }
|
||||
void setConsoleZoom(float v) { console_zoom_ = v; }
|
||||
|
||||
// Hidden addresses (addresses hidden from the UI by the user)
|
||||
const std::set<std::string>& getHiddenAddresses() const { return hidden_addresses_; }
|
||||
bool isAddressHidden(const std::string& addr) const { return hidden_addresses_.count(addr) > 0; }
|
||||
@@ -482,6 +491,9 @@ private:
|
||||
std::string balance_layout_ = "classic";
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
||||
bool scanline_enabled_ = true;
|
||||
bool console_line_accents_ = true; // left color accent bars in console output
|
||||
bool console_text_color_ = true; // per-channel text coloring in console output
|
||||
float console_zoom_ = 1.0f; // console output font zoom factor
|
||||
std::set<std::string> hidden_addresses_;
|
||||
std::set<std::string> favorite_addresses_;
|
||||
std::map<std::string, AddressMeta> address_meta_;
|
||||
|
||||
@@ -101,10 +101,18 @@ WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(c
|
||||
// (The old `key[0]=='s'` test misrouted an uppercase "SK..." shielded key to the transparent RPC.)
|
||||
if (key.rfind("secret-extended-key-", 0) == 0) return KeyKind::Shielded;
|
||||
if (key.size() >= 2 && key[0] == 'S' && key[1] == 'K') return KeyKind::Shielded;
|
||||
if (!key.empty() && key[0] == 's') return KeyKind::Shielded;
|
||||
// A "z"-prefixed key is a shielded viewing key (zxview…); "s"-prefixed is a legacy Sprout key.
|
||||
if (!key.empty() && (key[0] == 's' || key[0] == 'z')) return KeyKind::Shielded;
|
||||
return KeyKind::Transparent;
|
||||
}
|
||||
|
||||
bool WalletSecurityController::isViewingKey(const std::string& key)
|
||||
{
|
||||
// Sapling extended full viewing key (mainnet HRP "zxviews"; "zxview" also matches the prefix the
|
||||
// lite backend recognizes). Watch-only: reveals the address's funds but cannot spend them.
|
||||
return key.rfind("zxview", 0) == 0;
|
||||
}
|
||||
|
||||
bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
|
||||
{
|
||||
if (key.rfind("secret-extended-key-", 0) == 0) return true; // Sapling z spending key
|
||||
@@ -115,6 +123,11 @@ bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WalletSecurityController::isRecognizedImportKey(const std::string& key)
|
||||
{
|
||||
return isRecognizedPrivateKey(key) || isViewingKey(key);
|
||||
}
|
||||
|
||||
const char* WalletSecurityController::importSuccessMessage(KeyKind kind)
|
||||
{
|
||||
return kind == KeyKind::Shielded
|
||||
|
||||
@@ -74,9 +74,13 @@ public:
|
||||
std::size_t minLength = 4);
|
||||
static KeyKind classifyAddress(const std::string& address);
|
||||
static KeyKind classifyPrivateKey(const std::string& key);
|
||||
// True if `key` is a shielded viewing key (extended full viewing key, "zxview…" — watch-only).
|
||||
static bool isViewingKey(const std::string& key);
|
||||
// True if `key` looks like a recognized Z (Sapling/Sprout spending) or T (WIF) private key.
|
||||
// Single source of truth for the import dialog's indicator AND its submit guard.
|
||||
static bool isRecognizedPrivateKey(const std::string& key);
|
||||
// As above, but also accepts a recognized shielded viewing key — the import dialog auto-detects
|
||||
// both, so this is the single source of truth for its indicator AND its submit guard.
|
||||
static bool isRecognizedImportKey(const std::string& key);
|
||||
static const char* importSuccessMessage(KeyKind kind);
|
||||
static std::string decryptExportFileName(std::uint64_t timestampSeconds);
|
||||
static void secureClear(std::string& value);
|
||||
|
||||
@@ -1666,11 +1666,21 @@ inline void BeginOverlayDialogFooter(float totalActionWidth, bool drawSeparator
|
||||
// TextColored(label). The label text is passed in (already translated).
|
||||
inline void DialogWarningHeader(const char* warningLabel, const ImVec4& col = WarningVec4())
|
||||
{
|
||||
ImGui::PushFont(Type().iconLarge());
|
||||
ImFont* iconF = Type().iconLarge();
|
||||
const float rowTop = ImGui::GetCursorPosY();
|
||||
const float iconH = iconF->LegacySize; // already dp-scaled
|
||||
const float textH = ImGui::GetFontSize();
|
||||
ImGui::PushFont(iconF);
|
||||
ImGui::TextColored(col, ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
// Vertically center the label with the taller warning glyph (stays within the icon's row height).
|
||||
if (iconH > textH) ImGui::SetCursorPosY(rowTop + (iconH - textH) * 0.5f);
|
||||
// Wrap so a long warning flows to a second line instead of clipping at the card edge; short
|
||||
// warnings (which fit) are unaffected.
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextColored(col, "%s", warningLabel);
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
// 50/50 Cancel / confirm footer for overlay dialogs. Sets `outCancel` /
|
||||
@@ -1700,6 +1710,70 @@ inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel,
|
||||
}
|
||||
}
|
||||
|
||||
// Reference dialog footer: a centered primary + Close (or Cancel) TactileButton pair, fixed width,
|
||||
// NO divider above (the reference dropped the separator). Sets outPrimary/outClose when the respective
|
||||
// button is clicked — the caller runs the bodies (so the primary's action can be arbitrary). The
|
||||
// primary is BeginDisabled'd when !primaryEnabled. btnW<=0 → 130·dp default.
|
||||
inline void DialogActionFooter(const char* primaryLabel, bool primaryEnabled,
|
||||
const char* closeLabel, bool& outPrimary, bool& outClose,
|
||||
float btnW = 0.0f)
|
||||
{
|
||||
const float dp = Layout::dpiScale();
|
||||
if (btnW <= 0.0f) btnW = 130.0f * dp;
|
||||
const float total = btnW * 2.0f + ImGui::GetStyle().ItemSpacing.x;
|
||||
const float off = (ImGui::GetContentRegionAvail().x - total) * 0.5f;
|
||||
if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off);
|
||||
ImGui::BeginDisabled(!primaryEnabled);
|
||||
if (TactileButton(primaryLabel, ImVec2(btnW, 0))) outPrimary = true;
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(closeLabel, ImVec2(btnW, 0))) outClose = true;
|
||||
}
|
||||
|
||||
// RAII glass sub-section for grouping an optional/advanced block inside a dialog (the reference's
|
||||
// scan-height panel). Auto-sizes to its content and paints a subtle glass panel behind it. Uses a
|
||||
// LOCAL ImDrawListSplitter — safe with BeginCombo popups, unlike the shared ChannelsSplit that
|
||||
// GlassCardScope uses. padX/padY are the interior insets (in logical px, dp-scaled here). Render the
|
||||
// section body between construction and destruction; use interiorWidth() for full-width children.
|
||||
struct GlassSectionScope {
|
||||
ImDrawListSplitter splitter;
|
||||
ImDrawList* dl;
|
||||
ImVec2 cardMin;
|
||||
float panelW, padX, padY;
|
||||
GlassPanelSpec spec;
|
||||
|
||||
explicit GlassSectionScope(float padXLogical = 12.0f, float padYLogical = 10.0f)
|
||||
{
|
||||
const float dp = Layout::dpiScale();
|
||||
padX = padXLogical * dp;
|
||||
padY = padYLogical * dp;
|
||||
spec.rounding = 8.0f * dp;
|
||||
spec.fillAlpha = 20;
|
||||
spec.borderAlpha = 30;
|
||||
dl = ImGui::GetWindowDrawList();
|
||||
panelW = ImGui::GetContentRegionAvail().x;
|
||||
cardMin = ImGui::GetCursorScreenPos();
|
||||
splitter.Split(dl, 2);
|
||||
splitter.SetCurrentChannel(dl, 1); // content above the panel background
|
||||
ImGui::Dummy(ImVec2(0, padY));
|
||||
ImGui::Indent(padX);
|
||||
}
|
||||
~GlassSectionScope()
|
||||
{
|
||||
ImGui::Unindent(padX);
|
||||
ImGui::Dummy(ImVec2(0, padY));
|
||||
const ImVec2 cardMax(cardMin.x + panelW, ImGui::GetCursorScreenPos().y);
|
||||
splitter.SetCurrentChannel(dl, 0);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, spec);
|
||||
splitter.Merge(dl);
|
||||
}
|
||||
// Interior content width (panel width minus both side insets) for full-width children.
|
||||
float interiorWidth() const { return panelW - 2.0f * padX; }
|
||||
|
||||
GlassSectionScope(const GlassSectionScope&) = delete;
|
||||
GlassSectionScope& operator=(const GlassSectionScope&) = delete;
|
||||
};
|
||||
|
||||
} // namespace material
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -492,6 +492,31 @@ static void saveSettingsPageState(config::Settings* settings) {
|
||||
settings->save();
|
||||
}
|
||||
|
||||
// Console output color toggles (accent bars + per-channel text color), shown on their own row under
|
||||
// the effects checkboxes. They mirror the console toolbar buttons and carry no GPU cost, so — unlike
|
||||
// scanline/theme-effects — they stay enabled in low-spec: the caller has an open
|
||||
// BeginDisabled(low_spec) around the effects row, so close it, draw these live-bound to the ConsoleTab
|
||||
// statics, then reopen it. Persisted immediately (the startup restore in App loads them on launch).
|
||||
static void renderConsoleColorToggles(App* app) {
|
||||
ImGui::EndDisabled();
|
||||
bool accents = ConsoleTab::s_line_accents_enabled;
|
||||
if (ImGui::Checkbox(TrId("console_accents", "con_accents").c_str(), &accents)) {
|
||||
ConsoleTab::s_line_accents_enabled = accents;
|
||||
app->settings()->setConsoleLineAccents(accents);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_accents"));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
bool textColor = ConsoleTab::s_line_text_color_enabled;
|
||||
if (ImGui::Checkbox(TrId("console_text_colors", "con_textcol").c_str(), &textColor)) {
|
||||
ConsoleTab::s_line_text_color_enabled = textColor;
|
||||
app->settings()->setConsoleTextColor(textColor);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_text_color"));
|
||||
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Settings Page Renderer
|
||||
// ============================================================================
|
||||
@@ -874,6 +899,9 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects"));
|
||||
|
||||
// Console output color toggles (own row — no GPU cost, enabled even in low-spec).
|
||||
renderConsoleColorToggles(app);
|
||||
|
||||
// Row 1: Acrylic preset slider + Noise slider (side by side, labels above)
|
||||
float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size;
|
||||
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
||||
@@ -1126,6 +1154,9 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects"));
|
||||
|
||||
// Console output color toggles (own row — no GPU cost, enabled even in low-spec).
|
||||
renderConsoleColorToggles(app);
|
||||
|
||||
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
||||
availWidth - pad * 2.0f);
|
||||
ImGui::TextUnformatted(TR("acrylic"));
|
||||
@@ -1318,12 +1349,14 @@ void RenderSettingsPage(App* app) {
|
||||
float naturalW = 0;
|
||||
for (int i = 0; i < 5; i++)
|
||||
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
|
||||
float impViewW = ImGui::CalcTextSize(TR("settings_import_viewkey")).x + btnPadX;
|
||||
naturalW += impViewW; // the extra "Import viewing key" button (rendered after Import key)
|
||||
float wizW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f;
|
||||
float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f;
|
||||
// Full-node-only "Seed phrase" + "Migrate to seed" buttons trail the Backup button.
|
||||
float seedW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_backup_button")).x + btnPadX : 0.0f;
|
||||
float migrateW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_migrate_button")).x + btnPadX : 0.0f;
|
||||
float totalW = naturalW + sp * 5;
|
||||
float totalW = naturalW + sp * 6; // 6 base buttons (incl. Import viewing key)
|
||||
if (showFullNodeLifecycleActions) totalW += wizW + bsW + seedW + migrateW + sp * 4;
|
||||
|
||||
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
|
||||
@@ -1333,6 +1366,11 @@ void RenderSettingsPage(App* app) {
|
||||
if (TactileButton(r1[0], ImVec2(0, 0), btnFont))
|
||||
app->showImportKeyDialog();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[0]);
|
||||
// Watch-only shielded viewing-key import (separate button — different key type + scan-height).
|
||||
ImGui::SameLine(0, scaledSp);
|
||||
if (TactileButton(TR("settings_import_viewkey"), ImVec2(0, 0), btnFont))
|
||||
app->showImportViewingKeyDialog();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_import_viewkey"));
|
||||
ImGui::SameLine(0, scaledSp);
|
||||
if (TactileButton(r1[1], ImVec2(0, 0), btnFont))
|
||||
app->showExportKeyDialog();
|
||||
|
||||
@@ -680,19 +680,31 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
ImGui::Separator();
|
||||
if (ImGui::BeginMenu(TR("portfolio_add_to"))) {
|
||||
const auto& pf = app->settings()->getPortfolioEntries();
|
||||
if (pf.empty()) {
|
||||
ImGui::MenuItem(TR("portfolio_no_entries"), nullptr, false, false);
|
||||
}
|
||||
const std::string activeHash = app->activeWalletIdentityHash();
|
||||
int shown = 0;
|
||||
for (int pi = 0; pi < (int)pf.size(); pi++) {
|
||||
// Only this wallet's groups (or legacy/unscoped), matching the Market summary —
|
||||
// don't offer to add this address to another wallet's group.
|
||||
if (!(pf[pi].scope.empty() || activeHash.empty() || pf[pi].scope == activeHash))
|
||||
continue;
|
||||
// PushID keeps the item ID unique even when two groups share a label (a
|
||||
// label-derived MenuItem ID would collide); a blank label shows a placeholder
|
||||
// instead of rendering as an invisible row.
|
||||
ImGui::PushID(pi);
|
||||
bool inSet = dragonx::data::PortfolioEntryContains(pf[pi].addresses, addr.address);
|
||||
if (ImGui::MenuItem(pf[pi].label.c_str(), nullptr, inSet)) {
|
||||
const char* lbl = pf[pi].label.empty() ? TR("portfolio_new_entry") : pf[pi].label.c_str();
|
||||
if (ImGui::MenuItem(lbl, nullptr, inSet)) {
|
||||
auto entries = pf; // copy, mutate, persist
|
||||
if (inSet) dragonx::data::PortfolioEntryRemove(entries[pi].addresses, addr.address);
|
||||
else dragonx::data::PortfolioEntryAdd(entries[pi].addresses, addr.address);
|
||||
app->settings()->setPortfolioEntries(entries);
|
||||
app->settings()->save();
|
||||
}
|
||||
ImGui::PopID();
|
||||
shown++;
|
||||
}
|
||||
if (shown == 0)
|
||||
ImGui::MenuItem(TR("portfolio_no_entries"), nullptr, false, false);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||
|
||||
@@ -47,6 +47,9 @@ public:
|
||||
|
||||
static bool isOpen() { return s_open; }
|
||||
|
||||
// Close the dialog (used by the UI sweep teardown).
|
||||
static void hide() { s_open = false; s_state = State::Confirm; }
|
||||
|
||||
static void render() {
|
||||
if (!s_app) return;
|
||||
if (!s_open) {
|
||||
@@ -91,21 +94,8 @@ private:
|
||||
ImGui::TextWrapped("%s", TR("bootstrap_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
// Warning card
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.9f, 0.6f, 0.0f, 0.08f));
|
||||
ImGui::BeginChild("##bsWarn", ImVec2(0, 0),
|
||||
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding,
|
||||
ImGuiWindowFlags_NoScrollbar);
|
||||
{
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
ImGui::PushFont(iconFont);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Warning()), "%s", ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextWrapped("%s", TR("bootstrap_warning"));
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
// Care header (reference style: ⚠ + Warning color, wraps if long).
|
||||
DialogWarningHeader(TR("bootstrap_warning"));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -124,9 +114,12 @@ private:
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Buttons: Download | Mirror | Cancel
|
||||
// Buttons: Download | Mirror | Cancel — centered as a group.
|
||||
float btnW = 140.0f * dp;
|
||||
float btnSm = 90.0f * dp;
|
||||
const float rowTotal = btnW * 2.0f + btnSm + ImGui::GetStyle().ItemSpacing.x * 2.0f;
|
||||
const float rowOff = (ImGui::GetContentRegionAvail().x - rowTotal) * 0.5f;
|
||||
if (rowOff > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rowOff);
|
||||
|
||||
if (TactileButton(TR("download"), ImVec2(btnW, 0))) {
|
||||
startDownload("");
|
||||
|
||||
@@ -14,106 +14,270 @@ constexpr int CountOf(const ConsoleCommandEntry (&)[N])
|
||||
}
|
||||
|
||||
const ConsoleCommandEntry kControlCommands[] = {
|
||||
{"help", "List all commands, or get help for a specified command", "[\"command\"]"},
|
||||
{"getinfo", "Get general info about the node", ""},
|
||||
{"stop", "Stop the daemon", ""},
|
||||
{"help", "List all commands, or get help for a specified command", "[\"command\"]",
|
||||
"Lists every RPC command. Pass a command name to see the node's detailed help for just that one.",
|
||||
"help \"getinfo\"", "commands list documentation what available"},
|
||||
{"getinfo", "Get general info about the node", "",
|
||||
"A quick snapshot of the node: version, block height, connection count, difficulty and wallet balance.",
|
||||
"getinfo", "status version summary overview node health"},
|
||||
{"stop", "Stop the daemon", "",
|
||||
"Shuts the DragonX node down cleanly. The wallet stays disconnected until the node is started again.",
|
||||
"stop", "quit shutdown exit close halt turn off", true},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kNetworkCommands[] = {
|
||||
{"getnetworkinfo", "Return P2P network state info", ""},
|
||||
{"getpeerinfo", "Get data about each connected peer", ""},
|
||||
{"getconnectioncount", "Get number of peer connections", ""},
|
||||
{"getnettotals", "Get network traffic statistics", ""},
|
||||
{"addnode", "Add, remove, or connect to a node", "\"node\" \"add|remove|onetry\""},
|
||||
{"setban", "Add or remove an IP/subnet from the ban list", "\"ip\" \"add|remove\" [bantime] [absolute]"},
|
||||
{"listbanned", "List all banned IPs/subnets", ""},
|
||||
{"clearbanned", "Clear all banned IPs", ""},
|
||||
{"ping", "Ping all peers to measure round-trip time", ""},
|
||||
{"getnetworkinfo", "Return P2P network state info", "",
|
||||
"The node's networking state: protocol version, which networks are active, listening addresses and relay fee.",
|
||||
"getnetworkinfo", "network p2p connection status internet"},
|
||||
{"getpeerinfo", "Get data about each connected peer", "",
|
||||
"Detailed data for every peer you are connected to \xE2\x80\x94 address, version, ping time and bytes exchanged.",
|
||||
"getpeerinfo", "peers connections nodes who connected"},
|
||||
{"getconnectioncount", "Get number of peer connections", "",
|
||||
"How many peers the node is currently connected to. Zero usually means the node is still starting or offline.",
|
||||
"getconnectioncount", "peers connections count how many"},
|
||||
{"getnettotals", "Get network traffic statistics", "",
|
||||
"Shows the total number of bytes your node has sent and received over the peer-to-peer network since it started. Useful for monitoring how much bandwidth the wallet is using.",
|
||||
"getnettotals", "bandwidth usage data traffic upload download network stats"},
|
||||
{"addnode", "Add, remove, or connect to a node", "\"node\" \"add|remove|onetry\"",
|
||||
"Manually connects your node to a specific peer by host or IP. Use 'add' to keep it in your list permanently, 'onetry' for a single connection attempt, or 'remove' to drop it. Handy when auto-discovery is not finding peers.",
|
||||
"addnode \"node.dragonx.is\" \"add\"", "connect peer manual node ip host disconnect no peers not connecting"},
|
||||
{"setban", "Add or remove an IP/subnet from the ban list", "\"ip\" \"add|remove\" [bantime] [absolute]",
|
||||
"Bans or unbans an IP or subnet from connecting. bantime is in seconds (0 = the default 24h); absolute treats it as a Unix timestamp.",
|
||||
"setban \"192.168.0.6\" \"add\" 86400", "ban block ip peer firewall reject", true},
|
||||
{"listbanned", "List all banned IPs/subnets", "",
|
||||
"Lists the IP addresses and subnets your node has banned, usually for misbehaving. Check it to see who you have blocked or to confirm before unbanning a peer.",
|
||||
"listbanned", "banned blocked ip ban list blacklist peers rejected"},
|
||||
{"clearbanned", "Clear all banned IPs", "",
|
||||
"Removes every entry from the ban list, letting all previously-banned peers connect again.",
|
||||
"clearbanned", "unban reset bans allow clear", true},
|
||||
{"ping", "Ping all peers to measure round-trip time", "",
|
||||
"Queues a ping to every connected peer to measure round-trip latency. Results are not returned here but show up in getpeerinfo; use it to check how responsive your connections are.",
|
||||
"ping", "latency ping peers round trip connection speed responsive test"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kBlockchainCommands[] = {
|
||||
{"getblockchaininfo", "Get current blockchain state", ""},
|
||||
{"getblockcount", "Get number of blocks in longest chain", ""},
|
||||
{"getbestblockhash", "Get hash of the tip block", ""},
|
||||
{"getblock", "Get block data for a given hash or height", "\"hash|height\" [verbosity]"},
|
||||
{"getblockhash", "Get block hash at a given height", "height"},
|
||||
{"getblockheader", "Get block header for a given hash", "\"hash\" [verbose]"},
|
||||
{"getdifficulty", "Get proof-of-work difficulty", ""},
|
||||
{"getrawmempool", "Get all txids in mempool", "[verbose]"},
|
||||
{"getmempoolinfo", "Get mempool state info", ""},
|
||||
{"gettxout", "Get details about an unspent output", "\"txid\" n [includemempool]"},
|
||||
{"coinsupply", "Get coin supply information", "[height]"},
|
||||
{"getchaintips", "Get all known chain tips", ""},
|
||||
{"getchaintxstats", "Get chain transaction statistics", "[nblocks] [\"blockhash\"]"},
|
||||
{"verifychain", "Verify the blockchain database", "[checklevel] [numblocks]"},
|
||||
{"kvsearch", "Search the blockchain key-value store", "\"key\""},
|
||||
{"kvupdate", "Update a key-value pair on-chain", "\"key\" \"value\" days"},
|
||||
{"getblockchaininfo", "Get current blockchain state", "",
|
||||
"State of the chain: height, best block hash, difficulty, verification progress and how far the node has synced.",
|
||||
"getblockchaininfo", "chain sync height status blockchain progress"},
|
||||
{"getblockcount", "Get number of blocks in longest chain", "",
|
||||
"The height of the longest chain \xE2\x80\x94 how many blocks the node has. Compare with the network to check sync.",
|
||||
"getblockcount", "height blocks how many chain length synced"},
|
||||
{"getbestblockhash", "Get hash of the tip block", "",
|
||||
"Shows the ID (hash) of the newest block at the very top of the blockchain. Handy to confirm your node is synced to the current chain tip.",
|
||||
"getbestblockhash", "latest block tip newest current top synced chain head"},
|
||||
{"getblock", "Get block data for a given hash or height", "\"hash|height\" [verbosity]",
|
||||
"Returns a block by its hash or height. verbosity 0 = raw hex, 1 = decoded header + txids, 2 = full transactions.",
|
||||
"getblock \"0000000000abc123\" 1", "block details transactions header"},
|
||||
{"getblockhash", "Get block hash at a given height", "height",
|
||||
"Looks up the unique ID (hash) of the block at a specific height (block number). Use it to find a block by its position, then feed the hash to other commands.",
|
||||
"getblockhash 1000000", "block by height number find block id lookup height hash"},
|
||||
{"getblockheader", "Get block header for a given hash", "\"hash\" [verbose]",
|
||||
"Returns the header (summary info) of a block, such as its time, difficulty, and links to neighboring blocks. Add true for readable fields instead of raw hex.",
|
||||
"getblockheader \"0000000000abc123\" true", "block summary header time difficulty inspect block metadata"},
|
||||
{"getdifficulty", "Get proof-of-work difficulty", "",
|
||||
"Shows how hard it currently is to mine a new block. A higher number means more mining power is competing on the network.",
|
||||
"getdifficulty", "mining difficulty how hard hashrate network pow proof of work"},
|
||||
{"getrawmempool", "Get all txids in mempool", "[verbose]",
|
||||
"Lists the transaction IDs waiting in the mempool (unconfirmed, not yet in a block). Pass true for extra details like fee and size per transaction.",
|
||||
"getrawmempool true", "pending unconfirmed transactions waiting mempool queue not confirmed"},
|
||||
{"getmempoolinfo", "Get mempool state info", "",
|
||||
"Reports the overall state of the waiting-transaction pool: how many transactions are pending and how much memory they use.",
|
||||
"getmempoolinfo", "mempool size pending count unconfirmed pool stats memory"},
|
||||
{"gettxout", "Get details about an unspent output", "\"txid\" n [includemempool]",
|
||||
"Checks a specific transaction output to see if it is still unspent and how many coins it holds. The last flag controls whether the mempool is also searched.",
|
||||
"gettxout \"0000000000abc123\" 0 true", "unspent output utxo check coins available spendable balance"},
|
||||
{"coinsupply", "Get coin supply information", "[height]",
|
||||
"Reports the coin supply at a block height (transparent, shielded, and total), or at the current tip if no height is given. Useful for checking total supply.",
|
||||
"coinsupply 1000000", "total supply circulating coins how many emission money supply shielded"},
|
||||
{"getchaintips", "Get all known chain tips", "",
|
||||
"Lists all block-chain branch tips the node knows about, including the main chain and any stale forks. Mostly useful for diagnosing chain forks or reorgs.",
|
||||
"getchaintips", "forks branches chain tips reorg orphan stale diagnose"},
|
||||
{"getchaintxstats", "Get chain transaction statistics", "[nblocks] [\"blockhash\"]",
|
||||
"Gives statistics about transactions over a window of recent blocks, like total count and average transactions per second. Useful for gauging network activity.",
|
||||
"getchaintxstats 2016", "transaction stats network activity tx rate throughput volume history"},
|
||||
{"verifychain", "Verify the blockchain database", "[checklevel] [numblocks]",
|
||||
"Runs an integrity check on your local blockchain database to confirm it is not corrupted. A higher checklevel and more blocks mean a deeper, slower check.",
|
||||
"verifychain 3 288", "check database integrity verify blockchain corruption validate"},
|
||||
{"kvsearch", "Search the blockchain key-value store", "\"key\"",
|
||||
"Advanced/developer command. Looks up a value previously stored on-chain under a given key in DragonX's key-value store.",
|
||||
"kvsearch \"mykey\"", "lookup key value store read on-chain data retrieve kv developer"},
|
||||
{"kvupdate", "Update a key-value pair on-chain", "\"key\" \"value\" days",
|
||||
"Advanced/developer command. Stores or updates a key-value entry on the blockchain for a number of days (costs a fee); an optional passphrase can protect the key. Rarely needed by wallet users.",
|
||||
"kvupdate \"mykey\" \"myvalue\" 30", "write key value store on-chain data set update kv developer publish"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kMiningCommands[] = {
|
||||
{"getmininginfo", "Get mining-related information", ""},
|
||||
{"setgenerate", "Turn mining on or off (true/false [threads])", "generate [genproclimit]"},
|
||||
{"getgenerate", "Check if the node is mining", ""},
|
||||
{"getnetworkhashps", "Get estimated network hash rate", "[blocks] [height]"},
|
||||
{"getblocksubsidy", "Get block reward at a given height", "[height]"},
|
||||
{"getblocktemplate", "Get block template for mining", "[\"jsonrequestobject\"]"},
|
||||
{"submitblock", "Submit a mined block to the network", "\"hexdata\""},
|
||||
{"getmininginfo", "Get mining-related information", "",
|
||||
"Mining status: current difficulty, estimated network hash rate and whether this node is generating blocks.",
|
||||
"getmininginfo", "mining hashrate difficulty generate"},
|
||||
{"setgenerate", "Turn mining on or off (true/false [threads])", "generate [genproclimit]",
|
||||
"Turns built-in CPU mining on or off. Pass true or false, and optionally the number of threads (-1 = all cores).",
|
||||
"setgenerate true 4", "mine mining generate cpu start stop", true},
|
||||
{"getgenerate", "Check if the node is mining", "",
|
||||
"Tells you whether the node's built-in CPU miner is currently turned on and trying to mine blocks. Handy to confirm mining is running or stopped.",
|
||||
"getgenerate", "mining on off status check am i mining is mining running generate"},
|
||||
{"getnetworkhashps", "Get estimated network hash rate", "[blocks] [height]",
|
||||
"Estimates the whole network's total mining power (solutions per second) over recent blocks. A higher number means more miners competing to find each block.",
|
||||
"getnetworkhashps 120 -1", "network hashrate mining power total speed solutions per second competition"},
|
||||
{"getblocksubsidy", "Get block reward at a given height", "[height]",
|
||||
"Shows the block reward (newly minted DRGX) paid for mining the block at a given height. Useful for checking current or future rewards; omit the height to use the current tip.",
|
||||
"getblocksubsidy 250000", "block reward mining payout coins subsidy how much earn per block"},
|
||||
{"getblocktemplate", "Get block template for mining", "[\"jsonrequestobject\"]",
|
||||
"Advanced/developer command: returns the raw data a mining program needs to assemble and work on a candidate block. Normal users mine via a pool or the built-in miner instead.",
|
||||
"getblocktemplate {\"capabilities\":[\"coinbasetxn\",\"workid\"]}", "mining template block work developer pool solo advanced candidate"},
|
||||
{"submitblock", "Submit a mined block to the network", "\"hexdata\"",
|
||||
"Advanced/developer command: submits a fully mined block (as raw hex) to the network. Only used by external mining software that built the block itself.",
|
||||
"submitblock \"0000000000abc123\"", "submit block mined broadcast solo mining developer advanced send block"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kWalletCommands[] = {
|
||||
{"getbalance", "Get wallet transparent balance", "[\"account\"] [minconf]"},
|
||||
{"z_gettotalbalance", "Get total transparent + shielded balance", "[minconf]"},
|
||||
{"z_getbalances", "Get all balances (transparent + shielded)", ""},
|
||||
{"getnewaddress", "Generate a new transparent address", ""},
|
||||
{"z_getnewaddress", "Generate a new shielded address", "[\"type\"]"},
|
||||
{"listaddresses", "List all transparent addresses", ""},
|
||||
{"z_listaddresses", "List all z-addresses", ""},
|
||||
{"sendtoaddress", "Send to a specific address", "\"address\" amount"},
|
||||
{"z_sendmany", "Send to multiple z/t-addresses with shielded support", "\"fromaddress\" [{\"address\":\"...\",\"amount\":...}]"},
|
||||
{"z_shieldcoinbase", "Shield transparent coinbase funds to a z-address", "\"fromaddress\" \"tozaddress\" [fee] [limit]"},
|
||||
{"z_mergetoaddress", "Merge multiple UTXOs/notes to one address", "[\"fromaddress\",...] \"toaddress\" [fee] [limit]"},
|
||||
{"listtransactions", "List recent wallet transactions", "[\"account\"] [count] [from]"},
|
||||
{"listunspent", "List unspent transaction outputs", "[minconf] [maxconf]"},
|
||||
{"z_listunspent", "List unspent shielded notes", "[minconf] [maxconf]"},
|
||||
{"z_getoperationstatus", "Get status of async z operations", "[\"operationid\",...]"},
|
||||
{"z_getoperationresult", "Get result of completed z operations", "[\"operationid\",...]"},
|
||||
{"z_listoperationids", "List all async z operation IDs", ""},
|
||||
{"getwalletinfo", "Get wallet state info", ""},
|
||||
{"backupwallet", "Back up wallet to a file", "\"destination\""},
|
||||
{"dumpprivkey", "Dump private key for an address", "\"address\""},
|
||||
{"importprivkey", "Import a private key into the wallet", "\"privkey\" [\"label\"] [rescan]"},
|
||||
{"dumpwallet", "Dump all wallet keys to a file", "\"filename\""},
|
||||
{"importwallet", "Import wallet from a dump file", "\"filename\""},
|
||||
{"z_exportkey", "Export spending key for a z-address", "\"zaddr\""},
|
||||
{"z_importkey", "Import a z-address spending key", "\"zkey\" [rescan] [startheight]"},
|
||||
{"z_exportviewingkey", "Export viewing key for a z-address", "\"zaddr\""},
|
||||
{"z_importviewingkey", "Import a z-address viewing key", "\"vkey\" [rescan] [startheight]"},
|
||||
{"z_exportwallet", "Export all wallet keys (including z-keys) to file", "\"filename\""},
|
||||
{"signmessage", "Sign a message with an address key", "\"address\" \"message\""},
|
||||
{"settxfee", "Set the transaction fee per kB", "amount"},
|
||||
{"walletpassphrase", "Unlock the wallet with passphrase", "\"passphrase\" timeout"},
|
||||
{"walletlock", "Lock the wallet", ""},
|
||||
{"encryptwallet", "Encrypt the wallet with a passphrase", "\"passphrase\""},
|
||||
{"getbalance", "Get wallet transparent balance", "[\"account\"] [minconf]",
|
||||
"Your confirmed transparent (t-address) balance in DRGX. Shielded funds are NOT included \xE2\x80\x94 use z_gettotalbalance for everything.",
|
||||
"getbalance", "balance money funds how much transparent"},
|
||||
{"z_gettotalbalance", "Get total transparent + shielded balance", "[minconf]",
|
||||
"Your complete balance in DRGX \xE2\x80\x94 transparent plus shielded (private) funds together.",
|
||||
"z_gettotalbalance", "balance total money funds shielded private how much"},
|
||||
{"z_getbalances", "Get all balances (transparent + shielded)", "",
|
||||
"Breaks your balance down across each transparent and shielded address the wallet holds.",
|
||||
"z_getbalances", "balance breakdown addresses funds per address"},
|
||||
{"getnewaddress", "Generate a new transparent address", "",
|
||||
"Creates a fresh transparent (t-) address you can share to receive DRGX.",
|
||||
"getnewaddress", "receive new address deposit transparent create get paid"},
|
||||
{"z_getnewaddress", "Generate a new shielded address", "[\"type\"]",
|
||||
"Creates a fresh shielded (z-) address for receiving DRGX privately.",
|
||||
"z_getnewaddress", "receive new address private shielded create"},
|
||||
{"listaddresses", "List all transparent addresses", "",
|
||||
"Lists the transparent (t-) addresses in your wallet.",
|
||||
"listaddresses", "addresses list my accounts transparent"},
|
||||
{"z_listaddresses", "List all z-addresses", "",
|
||||
"Lists the shielded (z-) addresses in your wallet.",
|
||||
"z_listaddresses", "addresses list my shielded private"},
|
||||
{"sendtoaddress", "Send to a specific address", "\"address\" amount",
|
||||
"Sends DRGX to a transparent address. amount is in DRGX. Double-check the address \xE2\x80\x94 sent transactions cannot be reversed.",
|
||||
"sendtoaddress \"RyourRecipientAddr\" 1.5", "send pay transfer money spend", true},
|
||||
{"z_sendmany", "Send to multiple z/t-addresses with shielded support", "\"fromaddress\" [{\"address\":\"...\",\"amount\":...}]",
|
||||
"Sends DRGX from one address to one or more recipients, with shielded (private) support. Amounts are in DRGX.",
|
||||
"z_sendmany \"RfromAddr\" [{\"address\":\"zs1toAddr\",\"amount\":1.0}]", "send pay private shielded transfer money", true},
|
||||
{"z_shieldcoinbase", "Shield transparent coinbase funds to a z-address", "\"fromaddress\" \"tozaddress\" [fee] [limit]",
|
||||
"Moves newly mined (coinbase) transparent funds into a private shielded z-address, since mined rewards must be shielded before they can be spent normally. Runs in the background and returns an operation id.",
|
||||
"z_shieldcoinbase \"RyourMiningAddr\" \"zs1yourShieldedAddr\"", "shield mining rewards coinbase private hide mined funds move to shielded"},
|
||||
{"z_mergetoaddress", "Merge multiple UTXOs/notes to one address", "[\"fromaddress\",...] \"toaddress\" [fee] [limit]",
|
||||
"Combines many small balances (from transparent and/or shielded addresses) into a single destination address in one transaction, to consolidate funds. Runs in the background and returns an operation id.",
|
||||
"z_mergetoaddress [\"RyourAddr\",\"zs1yourShieldedAddr\"] \"zs1destShieldedAddr\"", "merge combine consolidate funds sweep small balances into one address"},
|
||||
{"listtransactions", "List recent wallet transactions", "[\"account\"] [count] [from]",
|
||||
"Your most recent wallet transactions, newest first \xE2\x80\x94 amounts, addresses and confirmations.",
|
||||
"listtransactions", "transactions history recent payments received sent"},
|
||||
{"listunspent", "List unspent transaction outputs", "[minconf] [maxconf]",
|
||||
"Lists your spendable transparent coins (unspent outputs), showing which addresses hold funds and how many confirmations each has. Useful to see exactly what's available to spend.",
|
||||
"listunspent 1", "unspent coins utxo available balance spendable transparent outputs"},
|
||||
{"z_listunspent", "List unspent shielded notes", "[minconf] [maxconf]",
|
||||
"Lists your spendable shielded notes (private z-address funds), showing amounts and confirmations. The private-address counterpart to listunspent.",
|
||||
"z_listunspent 1", "unspent shielded notes private balance available zaddr spendable notes"},
|
||||
{"z_getoperationstatus", "Get status of async z operations", "[\"operationid\",...]",
|
||||
"Checks whether a background shielded operation (like sending or shielding from a z-address) is still running, has succeeded, or has failed. Use the operation id returned when you started it.",
|
||||
"z_getoperationstatus [\"opid-00000000-abc1-2345-6789-000000000abc\"]", "check status pending operation progress is my send done async job"},
|
||||
{"z_getoperationresult", "Get result of completed z operations", "[\"operationid\",...]",
|
||||
"Fetches the final result (transaction id or any error) of a finished background shielded operation and then removes it from memory. Use after the status shows it completed.",
|
||||
"z_getoperationresult [\"opid-00000000-abc1-2345-6789-000000000abc\"]", "operation result txid outcome finished completed error async send"},
|
||||
{"z_listoperationids", "List all async z operation IDs", "",
|
||||
"Lists the ids of all your background shielded operations known to the node, so you can look up their status or results. Takes no arguments.",
|
||||
"z_listoperationids", "list operations pending jobs opids background async queue"},
|
||||
{"getwalletinfo", "Get wallet state info", "",
|
||||
"Wallet summary: balances, transaction count, key-pool size and whether the wallet is encrypted and locked.",
|
||||
"getwalletinfo", "wallet status info balance encrypted locked"},
|
||||
{"backupwallet", "Back up wallet to a file", "\"destination\"",
|
||||
"Saves a copy of your wallet.dat to the given path. Keep the backup somewhere safe and private.",
|
||||
"backupwallet \"/home/you/drgx-backup.dat\"", "backup save wallet copy protect"},
|
||||
{"dumpprivkey", "Dump private key for an address", "\"address\"",
|
||||
"Reveals the private key for an address. Anyone with this key can spend from the address \xE2\x80\x94 never share it.",
|
||||
"dumpprivkey \"RyourAddr\"", "private key export reveal secret spend", true},
|
||||
{"importprivkey", "Import a private key into the wallet", "\"privkey\" [\"label\"] [rescan]",
|
||||
"Imports a private key so the wallet can spend from its address, then rescans the chain for its history (can be slow).",
|
||||
"importprivkey \"Uxxxxxxxxxxxx\"", "import private key restore recover add", true},
|
||||
{"dumpwallet", "Dump all wallet keys to a file", "\"filename\"",
|
||||
"Writes a human-readable text file of your wallet's transparent private keys for backup. The file is saved into the daemon's configured -exportdir folder. Anyone with it can spend your funds, so store it securely.",
|
||||
"dumpwallet \"wallet-backup\"", "backup export private keys save wallet dump keys to file", true},
|
||||
{"importwallet", "Import wallet from a dump file", "\"filename\"",
|
||||
"Loads keys from a dumpwallet text file back into your wallet and rescans the chain for their funds. Use to restore a backup or add keys from another wallet. Advanced/recovery use.",
|
||||
"importwallet \"/home/you/wallet-backup.txt\"", "restore import keys recover load wallet backup from file", true},
|
||||
{"z_exportkey", "Export spending key for a z-address", "\"zaddr\"",
|
||||
"Reveals the spending key for a shielded address. Anyone with it can spend your private funds \xE2\x80\x94 keep it secret.",
|
||||
"z_exportkey \"zs1yourAddr\"", "private key export shielded secret spend", true},
|
||||
{"z_importkey", "Import a z-address spending key", "\"zkey\" [rescan] [startheight]",
|
||||
"Adds a shielded (z-address) spending key from another wallet so you can see and spend its private funds here. By default it rescans the chain, which can take a while. Advanced/recovery use.",
|
||||
"z_importkey \"secret-extended-key-main1yourZKeyHere\"", "import shielded key restore zaddr spending key recover private funds", true},
|
||||
{"z_exportviewingkey", "Export viewing key for a z-address", "\"zaddr\"",
|
||||
"Exports a view-only key for a shielded address, letting someone see its incoming funds and balance without being able to spend. Safe to share for auditing/monitoring.",
|
||||
"z_exportviewingkey \"zs1yourShieldedAddr\"", "export viewing key watch only audit read only shielded balance"},
|
||||
{"z_importviewingkey", "Import a z-address viewing key", "\"vkey\" [rescan] [startheight]",
|
||||
"Imports a view-only key so your wallet can watch a shielded address's incoming funds without being able to spend them. By default it rescans the chain. For monitoring/auditing.",
|
||||
"z_importviewingkey \"zviews1yourViewingKeyHere\"", "import viewing key watch only monitor shielded address audit read only", true},
|
||||
{"z_exportwallet", "Export all wallet keys (including z-keys) to file", "\"filename\"",
|
||||
"Writes a text-file backup of all your keys including shielded (z-address) spending keys, unlike dumpwallet which is transparent-only. Saved into the daemon's -exportdir folder; anyone with it can spend your funds.",
|
||||
"z_exportwallet \"full-wallet-backup\"", "backup export all keys shielded zkeys save wallet full to file", true},
|
||||
{"signmessage", "Sign a message with an address key", "\"address\" \"message\"",
|
||||
"Signs a message with the private key of one of your addresses, proving you control that address.",
|
||||
"signmessage \"RyourAddr\" \"hello world\"", "sign message prove ownership"},
|
||||
{"settxfee", "Set the transaction fee per kB", "amount",
|
||||
"Sets the fee you pay per kilobyte on transparent transactions, in DRGX. A higher fee can speed confirmation; set 0 to use the default. Applies until you change it or restart.",
|
||||
"settxfee 0.0001", "set fee transaction cost per kb miner fee change fee rate"},
|
||||
{"walletpassphrase", "Unlock the wallet with passphrase", "\"passphrase\" timeout",
|
||||
"Unlocks an encrypted wallet for the given number of seconds so you can send funds. Your passphrase is typed in plain text \xE2\x80\x94 be sure no one is watching.",
|
||||
"walletpassphrase \"your passphrase\" 60", "unlock passphrase password encrypted open", true},
|
||||
{"walletlock", "Lock the wallet", "",
|
||||
"Removes the encryption key from memory, locking the wallet so its keys can't spend until you unlock it again with your passphrase. Only works on an encrypted wallet.",
|
||||
"walletlock", "lock wallet secure re-lock protect passphrase disable spending"},
|
||||
{"encryptwallet", "Encrypt the wallet with a passphrase", "\"passphrase\"",
|
||||
"Encrypts the wallet with a passphrase. The node shuts down afterward and you will need the passphrase to send. Encryption cannot be undone \xE2\x80\x94 keep the passphrase safe.",
|
||||
"encryptwallet \"a strong passphrase\"", "encrypt password protect secure lock", true},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kRawTransactionCommands[] = {
|
||||
{"createrawtransaction", "Create a raw transaction spending given inputs", "[{\"txid\":\"...\",\"vout\":n},...] {\"address\":amount,...}"},
|
||||
{"decoderawtransaction", "Decode raw transaction hex string", "\"hexstring\""},
|
||||
{"decodescript", "Decode a hex-encoded script", "\"hex\""},
|
||||
{"getrawtransaction", "Get raw transaction data by txid", "\"txid\" [verbose]"},
|
||||
{"sendrawtransaction", "Submit raw transaction to the network", "\"hexstring\" [allowhighfees]"},
|
||||
{"signrawtransaction", "Sign a raw transaction with private keys", "\"hexstring\""},
|
||||
{"fundrawtransaction", "Add inputs to meet output value", "\"hexstring\""},
|
||||
{"createrawtransaction", "Create a raw transaction spending given inputs", "[{\"txid\":\"...\",\"vout\":n},...] {\"address\":amount,...}",
|
||||
"Advanced/developer command. Builds an unsigned raw transaction from transparent inputs (txid+vout) you pick and transparent outputs you specify; returns only hex that you must still sign and broadcast. Cannot spend to or from shielded (zs1) addresses. Most users should just use the Send tab.",
|
||||
"createrawtransaction [{\"txid\":\"0000000000abc123\",\"vout\":0}] {\"RyourAddr\":1.0}", "build unsigned transaction manually craft raw tx construct spend inputs outputs advanced"},
|
||||
{"decoderawtransaction", "Decode raw transaction hex string", "\"hexstring\"",
|
||||
"Developer tool. Takes a raw transaction hex string and shows its contents in readable JSON (inputs, outputs, amounts, addresses, scripts). Useful for inspecting a transaction before you sign or broadcast it.",
|
||||
"decoderawtransaction \"0100000001abc123...\"", "inspect raw transaction hex read decode view contents parse tx examine"},
|
||||
{"decodescript", "Decode a hex-encoded script", "\"hex\"",
|
||||
"Developer tool. Turns a hex-encoded script into readable form and shows the address or spending condition it represents. Rarely needed unless you are debugging scripts, redeem scripts, or multisig setups.",
|
||||
"decodescript \"76a914abc123...88ac\"", "decode script hex readable inspect multisig p2sh redeemscript debug advanced"},
|
||||
{"getrawtransaction", "Get raw transaction data by txid", "\"txid\" [verbose]",
|
||||
"Look up a transaction by its txid and return its raw hex, or a decoded readable view if you pass 1 for verbose. Only finds a tx that is in the mempool, has an unspent output, or when the node runs with -txindex.",
|
||||
"getrawtransaction \"0000000000abc123\" 1", "lookup transaction by id fetch tx details verbose inspect view raw txindex"},
|
||||
{"sendrawtransaction", "Submit raw transaction to the network", "\"hexstring\" [allowhighfees]",
|
||||
"Advanced command. Broadcasts an already-signed raw transaction hex to the network so miners can include it in a block. This is the final step after building and signing a transaction by hand.",
|
||||
"sendrawtransaction \"0100000001abc123...\"", "broadcast submit signed transaction publish push to network relay send hex"},
|
||||
{"signrawtransaction", "Sign a raw transaction with private keys", "\"hexstring\"",
|
||||
"Advanced command. Signs a raw transaction using the private keys in your wallet so it becomes valid to broadcast. Returns the signed hex plus a 'complete' flag telling you whether it is fully signed and ready to send.",
|
||||
"signrawtransaction \"0100000001abc123...\"", "sign raw transaction authorize private key finalize validate ready to broadcast"},
|
||||
{"fundrawtransaction", "Add inputs to meet output value", "\"hexstring\"",
|
||||
"Advanced wallet command. Adds enough of your wallet's inputs (plus one change output) to a partial raw transaction so it covers its outputs and fee. The added inputs are unsigned, so you still need to sign and send it afterward.",
|
||||
"fundrawtransaction \"0100000000010000000000...\"", "add inputs fund coins cover amount select utxos change fee complete transaction"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kUtilityCommands[] = {
|
||||
{"validateaddress", "Validate a transparent address", "\"address\""},
|
||||
{"z_validateaddress", "Validate a z-address", "\"zaddr\""},
|
||||
{"estimatefee", "Estimate fee for a transaction", "nblocks"},
|
||||
{"verifymessage", "Verify a signed message", "\"address\" \"signature\" \"message\""},
|
||||
{"createmultisig", "Create a multisig address", "nrequired [\"key\",...]"},
|
||||
{"invalidateblock", "Mark a block as invalid", "\"hash\""},
|
||||
{"reconsiderblock", "Reconsider a previously invalidated block", "\"hash\""},
|
||||
{"validateaddress", "Validate a transparent address", "\"address\"",
|
||||
"Checks whether a transparent address is well-formed and whether it belongs to your wallet.",
|
||||
"validateaddress \"RsomeAddr\"", "validate check address valid mine"},
|
||||
{"z_validateaddress", "Validate a z-address", "\"zaddr\"",
|
||||
"Checks whether a shielded (zs1...) address is valid and reports its type (sapling) and, if your wallet is loaded, whether the address belongs to you. Handy for double-checking a private address before sending to it.",
|
||||
"z_validateaddress \"zs1someAddr\"", "check shielded address valid private zaddr is this address mine verify z-address sapling"},
|
||||
{"estimatefee", "Estimate fee for a transaction", "nblocks",
|
||||
"Estimates the fee (per kilobyte) a transaction likely needs to start confirming within the given number of blocks. Returns -1 if the node hasn't seen enough activity to make a guess.",
|
||||
"estimatefee 6", "estimate fee cost transaction how much confirmation speed fee per kb suggested network fee"},
|
||||
{"verifymessage", "Verify a signed message", "\"address\" \"signature\" \"message\"",
|
||||
"Confirms that a signed message really came from the owner of a transparent (R...) address. You supply the address, its base64 signature, and the exact original message; it returns true or false.",
|
||||
"verifymessage \"RyourAddr\" \"H1234base64signature==\" \"the exact signed message\"", "verify signed message check signature prove ownership authenticate confirm sender validate proof"},
|
||||
{"createmultisig", "Create a multisig address", "nrequired [\"key\",...]",
|
||||
"Advanced: builds a multi-signature address that needs several keys to approve spending (e.g. 2-of-3). Returns the address and a redeem script you must keep in order to spend from it.",
|
||||
"createmultisig 2 [\"RfirstAddr\",\"RsecondAddr\",\"RthirdAddr\"]", "multisig shared wallet multiple signatures joint account m of n co-sign approval redeem script"},
|
||||
{"invalidateblock", "Mark a block as invalid", "\"hash\"",
|
||||
"Advanced/developer: forces your node to mark a specific block (and everything built on it) as invalid, rolling your local chain back to before it. Only for troubleshooting or testing; reversible with reconsiderblock.",
|
||||
"invalidateblock \"0000000000abc123\"", "reject block roll back chain reorg force invalid remove block undo block troubleshoot fork", true},
|
||||
{"reconsiderblock", "Reconsider a previously invalidated block", "\"hash\"",
|
||||
"Advanced/developer: clears the invalid mark on a block and its descendants so your node accepts them again and rejoins the normal chain. Used to reverse a previous invalidateblock.",
|
||||
"reconsiderblock \"0000000000abc123\"", "undo invalidate accept block again re-enable block restore chain reconsider fix rollback fork", true},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -7,8 +7,14 @@ namespace ui {
|
||||
|
||||
struct ConsoleCommandEntry {
|
||||
const char* name;
|
||||
const char* desc;
|
||||
const char* params;
|
||||
const char* desc; // terse one-line summary (always set)
|
||||
const char* params; // parameter template, e.g. "\"address\" amount [comment]"
|
||||
// Optional novice-facing enrichment (C++17 aggregate defaults — entries that omit these keep
|
||||
// the empty/false fallbacks, and the command explorer falls back to `desc`):
|
||||
const char* details = ""; // longer plain-language explanation
|
||||
const char* example = ""; // one concrete, runnable example line
|
||||
const char* keywords = ""; // space-separated search synonyms ("balance money funds")
|
||||
bool destructive = false; // consequential/sensitive -> safety badge + run confirmation
|
||||
};
|
||||
|
||||
struct ConsoleCommandCategory {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
namespace dragonx {
|
||||
@@ -116,9 +117,9 @@ std::vector<std::string> FormatConsoleCompletionLines(const std::vector<std::str
|
||||
return lines;
|
||||
}
|
||||
|
||||
std::vector<std::string> ParseConsoleCommandArgs(const std::string& command)
|
||||
std::vector<ConsoleArg> ParseConsoleCommandArgsTagged(const std::string& command)
|
||||
{
|
||||
std::vector<std::string> args;
|
||||
std::vector<ConsoleArg> args;
|
||||
std::size_t index = 0;
|
||||
while (index < command.size()) {
|
||||
while (index < command.size() && (command[index] == ' ' || command[index] == '\t')) {
|
||||
@@ -126,11 +127,12 @@ std::vector<std::string> ParseConsoleCommandArgs(const std::string& command)
|
||||
}
|
||||
if (index >= command.size()) break;
|
||||
|
||||
std::string token;
|
||||
ConsoleArg arg;
|
||||
if (command[index] == '"' || command[index] == '\'') {
|
||||
arg.quoted = true;
|
||||
char quote = command[index++];
|
||||
while (index < command.size() && command[index] != quote) {
|
||||
token += command[index++];
|
||||
arg.text += command[index++];
|
||||
}
|
||||
if (index < command.size()) ++index;
|
||||
} else if (command[index] == '[' || command[index] == '{') {
|
||||
@@ -140,30 +142,69 @@ std::vector<std::string> ParseConsoleCommandArgs(const std::string& command)
|
||||
while (index < command.size()) {
|
||||
if (command[index] == open) ++depth;
|
||||
else if (command[index] == close) --depth;
|
||||
token += command[index++];
|
||||
arg.text += command[index++];
|
||||
if (depth == 0) break;
|
||||
}
|
||||
} else {
|
||||
while (index < command.size() && command[index] != ' ' && command[index] != '\t') {
|
||||
token += command[index++];
|
||||
arg.text += command[index++];
|
||||
}
|
||||
}
|
||||
if (!token.empty()) args.push_back(token);
|
||||
// A quoted empty string ("") is a real argument; only skip genuinely-empty unquoted tokens.
|
||||
if (!arg.text.empty() || arg.quoted) args.push_back(std::move(arg));
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
std::vector<std::string> ParseConsoleCommandArgs(const std::string& command)
|
||||
{
|
||||
std::vector<std::string> args;
|
||||
for (auto& a : ParseConsoleCommandArgsTagged(command)) args.push_back(std::move(a.text));
|
||||
return args;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Interpret a bare token as a JSON number only when the WHOLE token is a valid, finite number;
|
||||
// otherwise return it as a string. Guards against std::stoll silently truncating "1e9"/"123abc"
|
||||
// (it stops at the first non-digit) and std::stod yielding inf/subnormal for out-of-range input.
|
||||
nlohmann::json ParseConsoleNumberOrString(const std::string& s)
|
||||
{
|
||||
if (!s.empty()) {
|
||||
try {
|
||||
std::size_t pos = 0;
|
||||
long long ll = std::stoll(s, &pos);
|
||||
if (pos == s.size()) return ll;
|
||||
} catch (...) {}
|
||||
try {
|
||||
std::size_t pos = 0;
|
||||
double d = std::stod(s, &pos);
|
||||
if (pos == s.size() && std::isfinite(d)) return d;
|
||||
} catch (...) {}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ConsoleRpcCall BuildConsoleRpcCall(const std::string& command)
|
||||
{
|
||||
auto args = ParseConsoleCommandArgs(command);
|
||||
auto args = ParseConsoleCommandArgsTagged(command);
|
||||
ConsoleRpcCall call;
|
||||
if (args.empty()) return call;
|
||||
|
||||
call.valid = true;
|
||||
call.method = args.front();
|
||||
call.method = args.front().text;
|
||||
|
||||
for (std::size_t argIndex = 1; argIndex < args.size(); ++argIndex) {
|
||||
const std::string& arg = args[argIndex];
|
||||
const ConsoleArg& a = args[argIndex];
|
||||
const std::string& arg = a.text;
|
||||
|
||||
// A quoted token is always a string — never coerced — so a genuinely-string argument that
|
||||
// happens to look numeric (e.g. an all-digit label) is sent as a string, not a number.
|
||||
if (a.quoted) {
|
||||
call.params.push_back(arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!arg.empty() && (arg[0] == '{' || arg[0] == '[')) {
|
||||
auto parsed = nlohmann::json::parse(arg, nullptr, false);
|
||||
if (!parsed.is_discarded()) {
|
||||
@@ -181,15 +222,7 @@ ConsoleRpcCall BuildConsoleRpcCall(const std::string& command)
|
||||
} else if (arg == "false") {
|
||||
call.params.push_back(false);
|
||||
} else {
|
||||
try {
|
||||
if (arg.find('.') != std::string::npos) {
|
||||
call.params.push_back(std::stod(arg));
|
||||
} else {
|
||||
call.params.push_back(std::stoll(arg));
|
||||
}
|
||||
} catch (...) {
|
||||
call.params.push_back(arg);
|
||||
}
|
||||
call.params.push_back(ParseConsoleNumberOrString(arg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,13 @@ std::string ConsoleHistoryEntry(const std::vector<std::string>& history,
|
||||
ConsoleCompletionResult CompleteConsoleCommand(const std::string& input);
|
||||
std::vector<std::string> FormatConsoleCompletionLines(const std::vector<std::string>& matches,
|
||||
std::size_t maxLineLength = 60);
|
||||
// A tokenized console argument. `quoted` is true when the token came from a "..."/'...' literal,
|
||||
// so BuildConsoleRpcCall must send it verbatim as a JSON string (never coerce it to a number/bool).
|
||||
struct ConsoleArg {
|
||||
std::string text;
|
||||
bool quoted = false;
|
||||
};
|
||||
std::vector<ConsoleArg> ParseConsoleCommandArgsTagged(const std::string& command);
|
||||
std::vector<std::string> ParseConsoleCommandArgs(const std::string& command);
|
||||
ConsoleRpcCall BuildConsoleRpcCall(const std::string& command);
|
||||
std::vector<ConsoleResultLine> FormatConsoleRpcResultLines(const std::string& result,
|
||||
|
||||
@@ -44,6 +44,8 @@ ImU32 ConsoleTab::COLOR_DAEMON = IM_COL32(160, 160, 160, 180);
|
||||
ImU32 ConsoleTab::COLOR_INFO = IM_COL32(191, 209, 229, 255);
|
||||
ImU32 ConsoleTab::COLOR_RPC = IM_COL32(120, 180, 255, 210);
|
||||
bool ConsoleTab::s_scanline_enabled = true;
|
||||
bool ConsoleTab::s_line_accents_enabled = true;
|
||||
bool ConsoleTab::s_line_text_color_enabled = true;
|
||||
float ConsoleTab::s_console_zoom = 1.0f;
|
||||
bool ConsoleTab::s_daemon_messages_enabled = true;
|
||||
bool ConsoleTab::s_errors_only_enabled = false;
|
||||
@@ -57,15 +59,16 @@ ConsoleTab* s_rpc_trace_console = nullptr;
|
||||
|
||||
std::string rpcTraceTimestamp()
|
||||
{
|
||||
// Called on RPC worker threads. std::localtime shares a process-wide static tm, so a private
|
||||
// mutex here can't stop another thread's localtime call from clobbering it between the call and
|
||||
// the copy. Use the reentrant variant into a local tm instead (matches the codebase pattern).
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm localTime{};
|
||||
static std::mutex timeMutex;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(timeMutex);
|
||||
if (const std::tm* current = std::localtime(&now)) {
|
||||
localTime = *current;
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
localtime_s(&localTime, &now);
|
||||
#else
|
||||
localtime_r(&now, &localTime);
|
||||
#endif
|
||||
|
||||
char buffer[16];
|
||||
std::strftime(buffer, sizeof(buffer), "%H:%M:%S", &localTime);
|
||||
@@ -188,6 +191,10 @@ void ConsoleTab::refreshColors()
|
||||
ImU32 ConsoleTab::channelTextColor(ConsoleChannel channel) const
|
||||
{
|
||||
using namespace material;
|
||||
// Monochrome mode: collapse every channel (and JSON syntax role) to the neutral result-body
|
||||
// color. COLOR_RESULT is theme-correct (contrast-floored on light terminals in refreshColors),
|
||||
// so text stays readable. The left accent bars are gated separately and stay independent.
|
||||
if (!s_line_text_color_enabled) return COLOR_RESULT;
|
||||
switch (channel) {
|
||||
// COLOR_* channels are already palette-derived + contrast-floored in refreshColors(). The
|
||||
// roles below are computed live from the theme palette, so floor them to a readable contrast on
|
||||
@@ -235,12 +242,11 @@ ConsoleTab::ConsoleTab()
|
||||
s_rpc_trace_console = this;
|
||||
}
|
||||
rpc::RPCClient::setTraceCallback([](const std::string& source, const std::string& method) {
|
||||
ConsoleTab* console = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
|
||||
console = s_rpc_trace_console;
|
||||
}
|
||||
if (console) console->addRpcTraceLine(source, method);
|
||||
// Dereference under the lock, not after releasing it: ~ConsoleTab clears
|
||||
// s_rpc_trace_console under the same lock, so holding it here blocks destruction until the
|
||||
// call returns — closing the shutdown use-after-free window (this fires on RPC worker threads).
|
||||
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
|
||||
if (s_rpc_trace_console) s_rpc_trace_console->addRpcTraceLine(source, method);
|
||||
});
|
||||
rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
|
||||
|
||||
@@ -278,6 +284,14 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
}
|
||||
}
|
||||
|
||||
// Run a command the RPC-reference modal's "Insert & run" queued (the modal has no executor, so it
|
||||
// defers to here). Done before drain so the echoed "> cmd" line surfaces this frame.
|
||||
if (!pending_submit_.empty()) {
|
||||
std::string cmd;
|
||||
cmd.swap(pending_submit_);
|
||||
submitConsoleCommand(exec, cmd);
|
||||
}
|
||||
|
||||
// Pull passive log lines (daemon/xmrig output, or the lite diagnostics ring) and any
|
||||
// completed command results from the backend executor.
|
||||
exec.pollLogLines([this](const std::string& l, ConsoleChannel c) { addLine(l, c); });
|
||||
@@ -292,6 +306,10 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
// model_) and the selection/scroll state are touched only on this (main) thread.
|
||||
drainModel();
|
||||
|
||||
// Compute the filtered visible-line set once per frame, BEFORE the toolbar — so its "<N> matches"
|
||||
// label reflects the current frame (not last frame's stale count). renderOutput reuses the result.
|
||||
computeVisibleLines(has_text_filter_, filter_lower_);
|
||||
|
||||
// Main console layout
|
||||
ImGui::BeginChild("ConsoleContainer", ImVec2(0, 0), false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
@@ -526,6 +544,7 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
// Commands reference button (full-node RPC reference only)
|
||||
if (exec.hasRpcReference()) {
|
||||
if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||
command_search_[0] = '\0'; // fresh search each open (dismiss paths don't all reset it)
|
||||
show_commands_popup_ = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
@@ -544,6 +563,33 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
// Output filter input
|
||||
drawFilterInput();
|
||||
|
||||
// Color-accent toggle: hide/show the per-line left accent bars (appearance, grouped with zoom).
|
||||
ImGui::SameLine();
|
||||
ImGui::Spacing();
|
||||
ImGui::SameLine();
|
||||
{
|
||||
float btnSz = ImGui::GetFrameHeight();
|
||||
const char* icon = s_line_accents_enabled ? ICON_MD_FORMAT_COLOR_FILL : ICON_MD_FORMAT_COLOR_RESET;
|
||||
if (!s_line_accents_enabled)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
if (TactileButton(icon, ImVec2(btnSz, btnSz), Type().iconMed()))
|
||||
s_line_accents_enabled = !s_line_accents_enabled;
|
||||
if (!s_line_accents_enabled) ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_accents"));
|
||||
}
|
||||
|
||||
// Text-color toggle: colored per-channel text vs. monochrome (grouped with the accent toggle).
|
||||
ImGui::SameLine();
|
||||
{
|
||||
float btnSz = ImGui::GetFrameHeight();
|
||||
if (!s_line_text_color_enabled)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
if (TactileButton(ICON_MD_FORMAT_COLOR_TEXT, ImVec2(btnSz, btnSz), Type().iconMed()))
|
||||
s_line_text_color_enabled = !s_line_text_color_enabled;
|
||||
if (!s_line_text_color_enabled) ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_text_color"));
|
||||
}
|
||||
|
||||
// Zoom +/- buttons (right side of toolbar)
|
||||
ImGui::SameLine();
|
||||
ImGui::Spacing();
|
||||
@@ -724,11 +770,8 @@ void ConsoleTab::renderOutput()
|
||||
output_scroll_y_ = ImGui::GetScrollY();
|
||||
scanline_rows_.clear();
|
||||
|
||||
// Build the filtered visible-line index list BEFORE mouse handling (screenToTextPos maps
|
||||
// through visible_indices_). Also yields the text-filter state used for highlighting.
|
||||
bool has_text_filter = false;
|
||||
std::string filter_lower;
|
||||
computeVisibleLines(has_text_filter, filter_lower);
|
||||
// visible_indices_ / has_text_filter_ / filter_lower_ were already computed once at the top of
|
||||
// render() (before the toolbar). screenToTextPos maps through visible_indices_, so it's ready.
|
||||
|
||||
// Calculate wrapped heights AND build sub-row segments for each visible line. Each
|
||||
// segment records which bytes of the source text appear on that visual row, so
|
||||
@@ -748,12 +791,15 @@ void ConsoleTab::renderOutput()
|
||||
win_min.y + ImGui::GetWindowHeight());
|
||||
bool mouse_in_output = (mouse_pos.x >= win_min.x && mouse_pos.x < win_max.x &&
|
||||
mouse_pos.y >= win_min.y && mouse_pos.y < win_max.y &&
|
||||
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopup));
|
||||
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopup) &&
|
||||
// The RPC reference is a BeginOverlayDialog (not an ImGui popup), so it
|
||||
// isn't caught above; suppress the output's text cursor + selection under it.
|
||||
!show_commands_popup_);
|
||||
handleOutputInteraction(mouse_pos, mouse_in_output);
|
||||
|
||||
// Draw the visible lines: accent bars, JSON indent guides, selection + filter highlight,
|
||||
// scanline capture, and the text itself.
|
||||
drawVisibleLines(padX, line_height, has_text_filter, filter_lower);
|
||||
drawVisibleLines(padX, line_height, has_text_filter_, filter_lower_);
|
||||
|
||||
ImGui::Unindent(padX);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -776,7 +822,7 @@ void ConsoleTab::renderOutput()
|
||||
}
|
||||
|
||||
// Filter indicator (text filter only — daemon toggle is already visible in toolbar)
|
||||
if (has_text_filter) {
|
||||
if (has_text_filter_) {
|
||||
char filterBuf[128];
|
||||
snprintf(filterBuf, sizeof(filterBuf), TR("console_showing_lines"),
|
||||
static_cast<int>(visible_indices_.size()), model_.size());
|
||||
@@ -815,6 +861,10 @@ void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLow
|
||||
bool has_filter = hasTextFilter || !outputFilter.daemonMessagesEnabled ||
|
||||
!outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled ||
|
||||
outputFilter.errorsOnly;
|
||||
// Folding is only honored in the unfiltered view; the filtered view is flat. Record it so the
|
||||
// fold triangles aren't drawn/clickable while filtering (else clicks would silently mutate the
|
||||
// collapsed flag with no visible effect and the glyph would disagree with the rendered block).
|
||||
folding_active_ = !has_filter;
|
||||
visible_indices_.clear();
|
||||
const int n = static_cast<int>(model_.size());
|
||||
if (has_filter) {
|
||||
@@ -850,8 +900,10 @@ void ConsoleTab::handleOutputInteraction(ImVec2 mousePos, bool mouseInOutput)
|
||||
if (mouseInOutput) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
|
||||
}
|
||||
// Selection drag lifecycle (continues even if the mouse leaves the window).
|
||||
if (mouseInOutput && io.MouseClicked[0]) {
|
||||
// Selection drag lifecycle (continues even if the mouse leaves the window). Ignore clicks in the
|
||||
// left gutter (< output_origin_.x) — that strip holds the accent bar + fold triangles, so a fold
|
||||
// toggle there shouldn't begin (and thus clear) a text selection.
|
||||
if (mouseInOutput && io.MouseClicked[0] && mousePos.x >= output_origin_.x) {
|
||||
selection_.beginDrag(screenToTextPos(mousePos));
|
||||
}
|
||||
if (selection_.dragging() && io.MouseDown[0]) {
|
||||
@@ -940,7 +992,8 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt
|
||||
|
||||
// Left-edge channel accent bar, drawn in the padX margin so it never overlaps
|
||||
// the text or the selection highlight. Same color that tints the toolbar toggle.
|
||||
if (line.channel != ConsoleChannel::None) {
|
||||
// Suppressed when the toolbar's color-accent toggle is off (cleaner monochrome gutter).
|
||||
if (s_line_accents_enabled && line.channel != ConsoleChannel::None) {
|
||||
ImU32 barCol = channelAccentColor(line.channel);
|
||||
if (barCol != 0) {
|
||||
float barW = 3.0f * Layout::hScale();
|
||||
@@ -961,16 +1014,19 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt
|
||||
for (size_t c = 2; c < leading; c += 2) {
|
||||
float gx = lineOrigin.x + static_cast<float>(c) * spaceW;
|
||||
dl->AddLine(ImVec2(gx, lineOrigin.y), ImVec2(gx, lineOrigin.y + totalH),
|
||||
IM_COL32(255, 255, 255, 20), 1.0f);
|
||||
IM_COL32(255, 255, 255, 20), 1.0f * Layout::dpiScale());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JSON fold toggle — a small triangle in the left gutter for block-opener lines.
|
||||
// Openers carry no accent bar (result/JSON channels), so the gutter is free.
|
||||
if (line.foldSpan > 0) {
|
||||
float sz = fontSize * 0.30f;
|
||||
float cx = output_origin_.x - padX + sz + 1.0f * Layout::hScale();
|
||||
// Openers carry no accent bar (result/JSON channels), so the gutter is free. Only shown in the
|
||||
// unfiltered view, where folding actually applies (see folding_active_ in computeVisibleLines).
|
||||
if (folding_active_ && line.foldSpan > 0) {
|
||||
// Size from the (DPI/density-scaled) gutter width, not the zoomed font, and center it in
|
||||
// the gutter band [origin-padX, origin) so the glyph and its clickable cell stay aligned.
|
||||
float sz = std::min(fontSize * 0.30f, padX * 0.34f);
|
||||
float cx = output_origin_.x - padX * 0.5f;
|
||||
float cy = lineOrigin.y + lineHeight * 0.5f;
|
||||
ImU32 triCol = WithAlpha(OnSurfaceMedium(), 210);
|
||||
if (line.collapsed) {
|
||||
@@ -1140,18 +1196,21 @@ void ConsoleTab::drawNewOutputIndicator()
|
||||
// "New output" indicator when the user is scrolled up and new lines arrived.
|
||||
if (scroll_.autoScroll() || scroll_.newLines() <= 0) return;
|
||||
|
||||
float indicW = 140.0f;
|
||||
float indicH = 24.0f;
|
||||
// The box geometry is hand-drawn absolute px, so scale it by dpiScale() (the font metrics below
|
||||
// are already scaled — left alone). Without this the pill renders native-size on a HiDPI display.
|
||||
float dp = Layout::dpiScale();
|
||||
float indicW = 140.0f * dp;
|
||||
float indicH = 24.0f * dp;
|
||||
ImDrawList* dlInd = ImGui::GetWindowDrawList();
|
||||
ImVec2 wMin = ImGui::GetWindowPos();
|
||||
ImVec2 wSize = ImGui::GetWindowSize();
|
||||
float ix = wMin.x + (wSize.x - indicW) * 0.5f;
|
||||
float iy = wMin.y + wSize.y - indicH - 8.0f;
|
||||
float iy = wMin.y + wSize.y - indicH - 8.0f * dp;
|
||||
ImVec2 iMin(ix, iy);
|
||||
ImVec2 iMax(ix + indicW, iy + indicH);
|
||||
|
||||
dlInd->AddRectFilled(iMin, iMax, IM_COL32(40, 40, 40, 220), 12.0f);
|
||||
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f);
|
||||
dlInd->AddRectFilled(iMin, iMax, IM_COL32(40, 40, 40, 220), 12.0f * dp);
|
||||
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f * dp, 0, 1.0f * dp);
|
||||
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), TR("console_new_lines"), scroll_.newLines());
|
||||
@@ -1357,9 +1416,10 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
|
||||
std::transform(first.begin(), first.end(), first.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
}
|
||||
// 'stop' shuts down the node — require a confirming second 'stop'; any other command clears the pending state.
|
||||
static bool stopConfirmPending = false;
|
||||
if (first != "stop") stopConfirmPending = false;
|
||||
// 'stop' shuts down the node — require a confirming second 'stop'; any other command clears the
|
||||
// pending state. Member (not a function-local static) so clear() can also cancel it — otherwise a
|
||||
// toolbar/context-menu clear between the two 'stop's would leave a stale arm and skip the warning.
|
||||
if (first != "stop") stop_confirm_pending_ = false;
|
||||
auto add = [this](const std::string& l, ConsoleChannel c) { addLine(l, c); };
|
||||
if (first == "clear" || first == "cls") {
|
||||
// View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history).
|
||||
@@ -1370,12 +1430,12 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
|
||||
} else if (first == "quit" || first == "exit") {
|
||||
addLine(TR("console_quit_note"), ConsoleChannel::Info);
|
||||
} else if (first == "stop") {
|
||||
if (!stopConfirmPending) {
|
||||
stopConfirmPending = true;
|
||||
if (!stop_confirm_pending_) {
|
||||
stop_confirm_pending_ = true;
|
||||
addLine("'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm.",
|
||||
ConsoleChannel::Warning);
|
||||
} else {
|
||||
stopConfirmPending = false;
|
||||
stop_confirm_pending_ = false;
|
||||
if (!exec.isReady()) addLine(TR("console_not_connected"), ConsoleChannel::Error);
|
||||
else exec.submit(cmd);
|
||||
}
|
||||
@@ -1390,195 +1450,476 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
|
||||
namespace {
|
||||
// True if any of a command's name/desc/params contains `filterLower` (already lowercased).
|
||||
// An empty filter matches everything.
|
||||
bool consoleCommandMatchesFilter(const char* name, const char* desc, const char* params,
|
||||
const std::string& filterLower)
|
||||
static bool lcContains(const char* s, const std::string& needleLower)
|
||||
{
|
||||
if (filterLower.empty()) return true;
|
||||
auto contains = [&filterLower](const char* s) {
|
||||
std::string v(s);
|
||||
std::transform(v.begin(), v.end(), v.begin(), ::tolower);
|
||||
return v.find(filterLower) != std::string::npos;
|
||||
};
|
||||
return contains(name) || contains(desc) || contains(params);
|
||||
std::string v(s);
|
||||
std::transform(v.begin(), v.end(), v.begin(), ::tolower);
|
||||
return v.find(needleLower) != std::string::npos;
|
||||
}
|
||||
static bool lcStartsWith(const char* s, const std::string& needleLower)
|
||||
{
|
||||
std::string v(s);
|
||||
std::transform(v.begin(), v.end(), v.begin(), ::tolower);
|
||||
return v.rfind(needleLower, 0) == 0;
|
||||
}
|
||||
|
||||
// Draw a command's parameter string into the current table cell, dimming optional [params].
|
||||
void drawConsoleCommandParams(const char* params)
|
||||
// Search relevance of a command for a lowercased query. -1 = no match; higher = better. Keywords
|
||||
// let novices find a command by intent ("balance" -> getbalance) even without the exact name.
|
||||
int consoleCommandRank(const ConsoleCommandEntry& cmd, const std::string& q)
|
||||
{
|
||||
using namespace material;
|
||||
const char* p = params;
|
||||
bool first = true;
|
||||
while (*p) {
|
||||
const char* bracketStart = strchr(p, '[');
|
||||
if (bracketStart) {
|
||||
// Draw the required part before the bracket.
|
||||
if (bracketStart > p) {
|
||||
if (!first) ImGui::SameLine(0, 0);
|
||||
std::string req(p, bracketStart);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", req.c_str());
|
||||
first = false;
|
||||
}
|
||||
const char* bracketEnd = strchr(bracketStart, ']');
|
||||
if (bracketEnd) {
|
||||
if (!first) ImGui::SameLine(0, 0);
|
||||
std::string opt(bracketStart, bracketEnd + 1);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", opt.c_str());
|
||||
first = false;
|
||||
p = bracketEnd + 1;
|
||||
} else {
|
||||
if (!first) ImGui::SameLine(0, 0);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", bracketStart);
|
||||
first = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (!first) ImGui::SameLine(0, 0);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", p);
|
||||
first = false;
|
||||
break;
|
||||
}
|
||||
if (q.empty()) return 0;
|
||||
if (lcStartsWith(cmd.name, q)) return 100;
|
||||
if (lcContains(cmd.name, q)) return 60;
|
||||
if (cmd.keywords[0] && lcContains(cmd.keywords, q)) return 40;
|
||||
if (lcContains(cmd.desc, q)) return 25;
|
||||
if (cmd.details[0] && lcContains(cmd.details, q)) return 15;
|
||||
if (lcContains(cmd.params, q)) return 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Split a parameter template into top-level tokens, respecting quote/bracket nesting so a space
|
||||
// inside "..." or [{...}] doesn't split (e.g. `"address" [{"a":1}]` -> {`"address"`, `[{"a":1}]`}).
|
||||
std::vector<std::string> splitParamTemplate(const char* params)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
std::string tok;
|
||||
int depth = 0;
|
||||
char q = 0;
|
||||
for (const char* p = params; *p; ++p) {
|
||||
char c = *p;
|
||||
if (q) { tok += c; if (c == q) q = 0; continue; }
|
||||
if (c == '"' || c == '\'') { q = c; tok += c; continue; }
|
||||
if (c == '[' || c == '{') { depth++; tok += c; continue; }
|
||||
if (c == ']' || c == '}') { if (depth > 0) depth--; tok += c; continue; }
|
||||
if (c == ' ' && depth == 0) { if (!tok.empty()) { out.push_back(tok); tok.clear(); } continue; }
|
||||
tok += c;
|
||||
}
|
||||
if (!tok.empty()) out.push_back(tok);
|
||||
return out;
|
||||
}
|
||||
|
||||
// A parsed parameter for the builder form. type in {string, number, json}; `raw` is the original
|
||||
// template token (used as a placeholder when a required field is left empty).
|
||||
struct ConsoleParamSpec {
|
||||
std::string label;
|
||||
std::string type;
|
||||
bool optional = false;
|
||||
std::string raw;
|
||||
};
|
||||
|
||||
// Best-effort parse of a param template into fillable fields. The templates are human-readable, not
|
||||
// a strict schema, so heuristics: [word] / ["word"] = an optional scalar; a [ / { with JSON content
|
||||
// (a brace or comma) = a JSON field the user pastes; otherwise a required string/number.
|
||||
std::vector<ConsoleParamSpec> parseParamSpecs(const char* params)
|
||||
{
|
||||
std::vector<ConsoleParamSpec> out;
|
||||
for (const std::string& t : splitParamTemplate(params)) {
|
||||
ConsoleParamSpec p;
|
||||
p.raw = t;
|
||||
std::string inner = t;
|
||||
if (inner.size() >= 2 && inner.front() == '[' && inner.back() == ']') {
|
||||
std::string body = inner.substr(1, inner.size() - 2);
|
||||
bool looksJson = body.find('{') != std::string::npos ||
|
||||
body.find(',') != std::string::npos ||
|
||||
(!body.empty() && body.front() == '[');
|
||||
if (!looksJson) { p.optional = true; inner = body; } // optional scalar wrapper
|
||||
}
|
||||
char c0 = inner.empty() ? 0 : inner.front();
|
||||
if (c0 == '"' || c0 == '\'') {
|
||||
p.type = "string";
|
||||
if (inner.size() >= 2 && inner.back() == c0) inner = inner.substr(1, inner.size() - 2);
|
||||
p.label = inner;
|
||||
} else if (c0 == '{' || c0 == '[') {
|
||||
p.type = "json";
|
||||
p.label = "json";
|
||||
} else {
|
||||
p.type = "number";
|
||||
p.label = inner;
|
||||
}
|
||||
out.push_back(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Translate a command-category name from the static reference tables (English) for display. The
|
||||
// per-command descriptions stay in English (technical RPC docs); only the 7 category labels are i18n'd.
|
||||
const char* consoleCategoryLabel(const char* name)
|
||||
{
|
||||
if (!std::strcmp(name, "Control")) return TR("console_cat_control");
|
||||
if (!std::strcmp(name, "Network")) return TR("console_cat_network");
|
||||
if (!std::strcmp(name, "Blockchain")) return TR("console_cat_blockchain");
|
||||
if (!std::strcmp(name, "Mining")) return TR("console_cat_mining");
|
||||
if (!std::strcmp(name, "Wallet")) return TR("console_cat_wallet");
|
||||
if (!std::strcmp(name, "Raw Transactions")) return TR("console_cat_raw_transactions");
|
||||
if (!std::strcmp(name, "Utility")) return TR("console_cat_utility");
|
||||
return name;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ConsoleTab::insertCommandToInput(const ConsoleCommandEntry& cmd)
|
||||
{
|
||||
// Fill the console input with the command (+ its param template) and close the modal — the user
|
||||
// reviews/edits it and presses Enter to run.
|
||||
if (cmd.params[0] != '\0')
|
||||
snprintf(input_buffer_, sizeof(input_buffer_), "%s %s", cmd.name, cmd.params);
|
||||
else {
|
||||
strncpy(input_buffer_, cmd.name, sizeof(input_buffer_) - 1);
|
||||
input_buffer_[sizeof(input_buffer_) - 1] = '\0';
|
||||
}
|
||||
command_search_[0] = '\0';
|
||||
run_confirm_cmd_ = nullptr;
|
||||
show_commands_popup_ = false;
|
||||
}
|
||||
|
||||
void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel)
|
||||
{
|
||||
using namespace material;
|
||||
float dp = Layout::dpiScale();
|
||||
ImFont* mono = Type().mono();
|
||||
|
||||
// Heading: command name, then category + a safety badge for consequential commands.
|
||||
ImGui::PushFont(Type().subtitle1());
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", cmd.name);
|
||||
ImGui::PopFont();
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", catLabel);
|
||||
if (cmd.destructive) {
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
ImVec4 warn = ImGui::ColorConvertU32ToFloat4(Warning());
|
||||
ImGui::PushFont(Type().iconSmall());
|
||||
ImGui::TextColored(warn, ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
ImGui::TextColored(warn, "%s", TR("console_ref_destructive"));
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Plain-language explanation (falls back to the terse summary when not enriched).
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurface()), "%s",
|
||||
cmd.details[0] ? cmd.details : cmd.desc);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
// Parameters — an editable form that assembles the command. Reset the fields whenever the
|
||||
// selected command changes so values don't carry across commands.
|
||||
if (cmd_param_owner_ != &cmd) {
|
||||
for (auto& b : cmd_param_bufs_) b[0] = '\0';
|
||||
cmd_param_owner_ = &cmd;
|
||||
}
|
||||
std::vector<ConsoleParamSpec> specs = parseParamSpecs(cmd.params);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_parameters"));
|
||||
if (specs.empty()) {
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", TR("console_ref_no_params"));
|
||||
} else {
|
||||
float labelW = 110.0f * dp;
|
||||
ImGui::Indent(Layout::spacingSm());
|
||||
for (size_t k = 0; k < specs.size() && k < 6; k++) {
|
||||
const ConsoleParamSpec& s = specs[k];
|
||||
ImGui::PushID((int)k);
|
||||
ImGui::PushFont(mono);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(s.optional ? OnSurfaceMedium() : OnSurface()),
|
||||
"%s", s.label.c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(labelW);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
std::string hint = s.optional
|
||||
? (s.type + " \xC2\xB7 " + std::string(TR("console_ref_optional"))) : s.type;
|
||||
ImGui::InputTextWithHint("##pv", hint.c_str(), cmd_param_bufs_[k], sizeof(cmd_param_bufs_[k]));
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::Unindent(Layout::spacingSm());
|
||||
}
|
||||
|
||||
// Assemble the command from the filled fields. A required field left empty keeps its placeholder
|
||||
// token (so Insert still shows what's needed) and marks the command incomplete (Insert & run
|
||||
// disabled). String values are auto-quoted; trailing empty optionals are omitted.
|
||||
auto trimStr = [](const std::string& s) -> std::string {
|
||||
size_t a = s.find_first_not_of(" \t");
|
||||
if (a == std::string::npos) return std::string();
|
||||
return s.substr(a, s.find_last_not_of(" \t") - a + 1);
|
||||
};
|
||||
// Include fields up to the last one that is filled OR required; trailing empty optionals are
|
||||
// dropped. An empty field that must still be included (a blank required field, or a gap before a
|
||||
// later filled field — positional args can't skip a middle slot) keeps its placeholder and marks
|
||||
// the command incomplete, so Insert & run stays disabled and no typed value is silently lost.
|
||||
int lastNeeded = -1;
|
||||
for (size_t k = 0; k < specs.size() && k < 6; k++)
|
||||
if (!trimStr(cmd_param_bufs_[k]).empty() || !specs[k].optional) lastNeeded = (int)k;
|
||||
bool complete = specs.size() <= 6;
|
||||
std::string built = cmd.name;
|
||||
for (int k = 0; k <= lastNeeded; k++) {
|
||||
std::string val = trimStr(cmd_param_bufs_[k]);
|
||||
if (val.empty()) {
|
||||
built += " " + specs[k].raw;
|
||||
complete = false;
|
||||
} else {
|
||||
if (specs[k].type == "string" && val.front() != '"' && val.front() != '\'' &&
|
||||
val.front() != '[' && val.front() != '{')
|
||||
val = "\"" + val + "\"";
|
||||
built += " " + val;
|
||||
}
|
||||
}
|
||||
|
||||
// Live "Builds" preview when the command takes parameters.
|
||||
if (!specs.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_builds"));
|
||||
ImGui::PushFont(mono);
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(FloorLight(IM_COL32(150, 200, 150, 255))), "%s", built.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopFont();
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
// Example (curated commands only) — reference alongside the builder.
|
||||
if (cmd.example[0]) {
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_example"));
|
||||
ImGui::PushFont(mono);
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(FloorLight(IM_COL32(150, 200, 150, 255))), "%s", cmd.example);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopFont();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
}
|
||||
|
||||
// Actions (or the destructive run confirmation) — Insert/run use the assembled command.
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
if (run_confirm_cmd_ == &cmd) {
|
||||
ImVec4 warn = ImGui::ColorConvertU32ToFloat4(Warning());
|
||||
ImGui::PushFont(Type().iconSmall());
|
||||
ImGui::TextColored(warn, ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
ImGui::TextColored(warn, TR("console_ref_run_confirm"), cmd.name);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
float cbw = 120.0f * dp;
|
||||
if (material::TactileButton(TR("console_ref_cancel"), ImVec2(cbw, 0))) run_confirm_cmd_ = nullptr;
|
||||
ImGui::SameLine();
|
||||
if (material::TactileButton(TR("console_ref_run"), ImVec2(cbw, 0))) {
|
||||
pending_submit_ = built;
|
||||
command_search_[0] = '\0';
|
||||
run_confirm_cmd_ = nullptr;
|
||||
show_commands_popup_ = false;
|
||||
}
|
||||
} else {
|
||||
if (material::TactileButton(TR("console_ref_insert"), ImVec2(200.0f * dp, 0))) {
|
||||
snprintf(input_buffer_, sizeof(input_buffer_), "%s", built.c_str());
|
||||
command_search_[0] = '\0';
|
||||
run_confirm_cmd_ = nullptr;
|
||||
show_commands_popup_ = false;
|
||||
}
|
||||
// Insert & run: enabled once every required field is filled (a template with unfilled
|
||||
// placeholders would just error).
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!complete);
|
||||
if (material::TactileButton(TR("console_ref_insert_run"), ImVec2(140.0f * dp, 0))) {
|
||||
if (cmd.destructive) {
|
||||
run_confirm_cmd_ = &cmd;
|
||||
} else {
|
||||
pending_submit_ = built;
|
||||
command_search_[0] = '\0';
|
||||
show_commands_popup_ = false;
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
void ConsoleTab::renderCommandsPopup()
|
||||
{
|
||||
using namespace material;
|
||||
|
||||
float popW = std::min(schema::UI().drawElement("tabs.console", "popup-max-width").size, ImGui::GetMainViewport()->Size.x * schema::UI().drawElement("tabs.console", "popup-width-ratio").size);
|
||||
float dp = Layout::dpiScale();
|
||||
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("console_rpc_reference"); ov.p_open = &show_commands_popup_;
|
||||
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
|
||||
ov.cardWidth = popW; ov.cardBottomViewportRatio = 0.94f; // keep authored width
|
||||
// FIXED height (fills most of the viewport) instead of auto-height: the command list's
|
||||
// fill-height child reads GetContentRegionAvail() inside the dialog child, which is only stable
|
||||
// when the child is fixed-height. Auto-height made it self-referential and it slid content
|
||||
// off-screen after a monitor move (same class of bug as the Wallets modal).
|
||||
ov.cardHeight = ImGui::GetMainViewport()->Size.y * 0.84f / Layout::dpiScale();
|
||||
if (!material::BeginOverlayDialog(ov)) {
|
||||
return;
|
||||
ov.title = TR("console_rpc_reference");
|
||||
ov.p_open = &show_commands_popup_;
|
||||
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
|
||||
ov.cardWidth = 960.0f; // wide enough for two panes
|
||||
ov.cardHeight = ImGui::GetMainViewport()->Size.y * 0.74f / dp; // fixed; both panes fill it
|
||||
ov.idSuffix = "cmdref";
|
||||
if (!material::BeginOverlayDialog(ov)) return;
|
||||
|
||||
// Esc dismisses (a first Esc cancels a pending run-confirm). Not built into the overlay.
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
if (run_confirm_cmd_) run_confirm_cmd_ = nullptr;
|
||||
else show_commands_popup_ = false;
|
||||
}
|
||||
|
||||
// Search filter
|
||||
static char cmdFilter[128] = {0};
|
||||
// Search box — auto-focus on open so the user can type immediately. Enter inserts the selected
|
||||
// command's template; reset the param-builder fields on open.
|
||||
if (ImGui::IsWindowAppearing()) { ImGui::SetKeyboardFocusHere(); cmd_param_owner_ = nullptr; }
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##CmdSearch", TR("console_search_commands"), cmdFilter, sizeof(cmdFilter));
|
||||
bool searchEnter = ImGui::InputTextWithHint("##CmdSearch", TR("console_ref_search_hint"),
|
||||
command_search_, sizeof(command_search_),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
bool searchChanged = ImGui::IsItemEdited();
|
||||
bool searchActive = ImGui::IsItemActive();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
|
||||
std::string q(command_search_);
|
||||
std::transform(q.begin(), q.end(), q.begin(), ::tolower);
|
||||
const bool searching = !q.empty();
|
||||
|
||||
const auto& categories = consoleCommandCategories();
|
||||
|
||||
std::string filter(cmdFilter);
|
||||
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
|
||||
|
||||
ImGui::BeginChild("CmdListScroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - Layout::spacingXs()),
|
||||
false);
|
||||
|
||||
ImGui::PushFont(Type().caption());
|
||||
|
||||
float cmdMinWidth = schema::UI().drawElement("tabs.console", "cmd-min-width").sizeOr(500.0f);
|
||||
float popupInnerW = ImGui::GetContentRegionAvail().x;
|
||||
bool showParams = popupInnerW >= cmdMinWidth;
|
||||
int catIdx = 0;
|
||||
|
||||
for (const auto& cat : categories) {
|
||||
// Count matching commands in this category
|
||||
int matchCount = 0;
|
||||
if (filter.empty()) {
|
||||
matchCount = cat.count;
|
||||
} else {
|
||||
for (int i = 0; i < cat.count; i++) {
|
||||
if (consoleCommandMatchesFilter(cat.commands[i].name, cat.commands[i].desc,
|
||||
cat.commands[i].params, filter)) {
|
||||
matchCount++;
|
||||
}
|
||||
// Flat display order of (cat,idx): ranked when searching, category order when browsing. Drives
|
||||
// keyboard nav + auto-selection; the browse view still renders grouped headers below.
|
||||
std::vector<std::pair<int, int>> order;
|
||||
if (searching) {
|
||||
std::vector<std::pair<int, std::pair<int, int>>> scored; // (score, (cat,idx))
|
||||
for (int c = 0; c < (int)categories.size(); c++)
|
||||
for (int i = 0; i < categories[c].count; i++) {
|
||||
int s = consoleCommandRank(categories[c].commands[i], q);
|
||||
if (s >= 0) scored.push_back({s, {c, i}});
|
||||
}
|
||||
std::stable_sort(scored.begin(), scored.end(),
|
||||
[](const auto& a, const auto& b) { return a.first > b.first; });
|
||||
for (auto& s : scored) order.push_back(s.second);
|
||||
} else {
|
||||
for (int c = 0; c < (int)categories.size(); c++)
|
||||
for (int i = 0; i < categories[c].count; i++) order.push_back({c, i});
|
||||
}
|
||||
|
||||
// Keep a valid selection: reset to the top when the query changes or the current selection falls
|
||||
// out of the visible set, so the detail pane is always populated.
|
||||
int selPos = -1;
|
||||
for (int k = 0; k < (int)order.size(); k++)
|
||||
if (order[k].first == cmd_sel_cat_ && order[k].second == cmd_sel_idx_) { selPos = k; break; }
|
||||
if (searchChanged || selPos < 0) {
|
||||
if (!order.empty()) { cmd_sel_cat_ = order[0].first; cmd_sel_idx_ = order[0].second; selPos = 0; }
|
||||
else { cmd_sel_cat_ = cmd_sel_idx_ = -1; }
|
||||
}
|
||||
|
||||
// Keyboard nav from the SEARCH box only (so typing in a param field doesn't move the selection):
|
||||
// Up/Down move, Enter inserts the selected command's template. Disabled while a run-confirm shows.
|
||||
if (!run_confirm_cmd_ && searchActive && !order.empty() && selPos >= 0) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && selPos + 1 < (int)order.size()) selPos++;
|
||||
else if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && selPos > 0) selPos--;
|
||||
cmd_sel_cat_ = order[selPos].first;
|
||||
cmd_sel_idx_ = order[selPos].second;
|
||||
}
|
||||
if (searchEnter && !run_confirm_cmd_ && cmd_sel_cat_ >= 0)
|
||||
insertCommandToInput(categories[cmd_sel_cat_].commands[cmd_sel_idx_]);
|
||||
|
||||
// Two-pane body sized above the footer.
|
||||
float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingXs();
|
||||
float bodyH = std::max(120.0f, ImGui::GetContentRegionAvail().y - footerH);
|
||||
float contentW = ImGui::GetContentRegionAvail().x;
|
||||
float gap = Layout::spacingLg();
|
||||
float masterW = std::min(std::max(contentW * 0.34f, 280.0f * dp), 380.0f * dp);
|
||||
float detailW = contentW - masterW - gap;
|
||||
|
||||
ImFont* mono = Type().mono();
|
||||
// The panes are theme-adaptive glass (light on light skins), so floor the link-blue to a readable
|
||||
// contrast on light surfaces (FloorLight is a no-op on dark themes).
|
||||
ImU32 nameCol = FloorLight(IM_COL32(100, 180, 255, 255));
|
||||
float rowH = std::max(20.0f * dp, mono->LegacySize + 6.0f * dp);
|
||||
|
||||
// One master row: mono command name + a warning dot for consequential commands, with a soft
|
||||
// rounded selection/hover fill (Material, not the default sharp Selectable highlight). Click
|
||||
// selects (drives the detail pane); double-click inserts.
|
||||
auto drawRow = [&](int c, int i) {
|
||||
const ConsoleCommandEntry& cmd = categories[c].commands[i];
|
||||
bool sel = (cmd_sel_cat_ == c && cmd_sel_idx_ == i);
|
||||
ImGui::PushID(c * 1000 + i);
|
||||
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0)); // we draw our own fill
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0, 0, 0, 0));
|
||||
bool clicked = ImGui::Selectable("##cmdrow", sel, ImGuiSelectableFlags_SpanAvailWidth,
|
||||
ImVec2(0, rowH));
|
||||
ImGui::PopStyleColor(3);
|
||||
bool hov = ImGui::IsItemHovered();
|
||||
if (clicked) { cmd_sel_cat_ = c; cmd_sel_idx_ = i; }
|
||||
if (hov) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) insertCommandToInput(cmd);
|
||||
}
|
||||
if (matchCount == 0) { catIdx++; continue; }
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
float rowW = ImGui::GetItemRectSize().x;
|
||||
if (sel || hov)
|
||||
dl->AddRectFilled(ImVec2(rmn.x, rmn.y + 1.0f * dp),
|
||||
ImVec2(rmn.x + rowW, rmn.y + rowH - 1.0f * dp),
|
||||
sel ? WithAlpha(Primary(), 52) : WithAlpha(OnSurface(), 16), 6.0f * dp);
|
||||
float tx = rmn.x + Layout::spacingSm();
|
||||
if (cmd.destructive) {
|
||||
float r = 3.0f * dp;
|
||||
dl->AddCircleFilled(ImVec2(tx + r, rmn.y + rowH * 0.5f), r, Warning());
|
||||
tx += r * 2.0f + Layout::spacingXs();
|
||||
}
|
||||
dl->AddText(mono, mono->LegacySize, ImVec2(tx, rmn.y + (rowH - mono->LegacySize) * 0.5f),
|
||||
sel ? OnSurface() : nameCol, cmd.name);
|
||||
ImGui::PopID();
|
||||
};
|
||||
|
||||
// Default-open only the first category (Control); collapse the rest
|
||||
ImGuiTreeNodeFlags headerFlags = (catIdx == 0) ? ImGuiTreeNodeFlags_DefaultOpen : 0;
|
||||
// When filtering, open all matching categories
|
||||
if (!filter.empty()) headerFlags = ImGuiTreeNodeFlags_DefaultOpen;
|
||||
// Both panes sit on soft Material glass surfaces (no hard 1px child border) with inner padding.
|
||||
GlassPanelSpec paneGlass;
|
||||
paneGlass.rounding = 14.0f;
|
||||
paneGlass.fillAlpha = 30;
|
||||
paneGlass.borderAlpha = 30;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
|
||||
// Show match count badge when filtering
|
||||
char headerLabel[128];
|
||||
if (!filter.empty()) {
|
||||
snprintf(headerLabel, sizeof(headerLabel), "%s (%d)", cat.name, matchCount);
|
||||
// MASTER pane.
|
||||
{
|
||||
ImVec2 mMin = ImGui::GetCursorScreenPos();
|
||||
DrawGlassPanel(ImGui::GetWindowDrawList(), mMin, ImVec2(mMin.x + masterW, mMin.y + bodyH), paneGlass);
|
||||
}
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingSm()));
|
||||
ImGui::BeginChild("##cmdMaster", ImVec2(masterW, bodyH), ImGuiChildFlags_AlwaysUseWindowPadding);
|
||||
{
|
||||
if (order.empty()) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_ref_no_match"));
|
||||
} else if (searching) {
|
||||
for (auto& p : order) drawRow(p.first, p.second);
|
||||
} else {
|
||||
snprintf(headerLabel, sizeof(headerLabel), "%s", cat.name);
|
||||
}
|
||||
bool open = ImGui::CollapsingHeader(headerLabel, headerFlags);
|
||||
ImGui::PopStyleColor();
|
||||
catIdx++;
|
||||
|
||||
if (open) {
|
||||
float nameColW = schema::UI().drawElement("tabs.console", "cmd-name-col-width").size * Layout::hScale();
|
||||
float paramsColW = schema::UI().drawElement("tabs.console", "cmd-params-col-width").size * Layout::hScale();
|
||||
int numCols = showParams ? 3 : 2;
|
||||
if (ImGui::BeginTable("##cmds", numCols, ImGuiTableFlags_None)) {
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, nameColW);
|
||||
if (showParams)
|
||||
ImGui::TableSetupColumn("Parameters", ImGuiTableColumnFlags_WidthFixed, paramsColW);
|
||||
ImGui::TableSetupColumn("Desc", ImGuiTableColumnFlags_WidthStretch);
|
||||
|
||||
for (int i = 0; i < cat.count; i++) {
|
||||
const auto& cmd = cat.commands[i];
|
||||
if (!consoleCommandMatchesFilter(cmd.name, cmd.desc, cmd.params, filter))
|
||||
continue;
|
||||
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImGui::ColorConvertU32ToFloat4(IM_COL32(100, 180, 255, 255)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.2f, 0.4f, 0.6f, 0.3f));
|
||||
char selId[128];
|
||||
snprintf(selId, sizeof(selId), "%s##cmdRef", cmd.name);
|
||||
if (ImGui::Selectable(selId, false)) {
|
||||
if (cmd.params[0] != '\0') {
|
||||
snprintf(input_buffer_, sizeof(input_buffer_), "%s %s", cmd.name, cmd.params);
|
||||
} else {
|
||||
strncpy(input_buffer_, cmd.name, sizeof(input_buffer_) - 1);
|
||||
input_buffer_[sizeof(input_buffer_) - 1] = '\0';
|
||||
}
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (cmd.params[0] != '\0')
|
||||
material::Tooltip(TR("console_click_insert_params"), cmd.name, cmd.params);
|
||||
else
|
||||
material::Tooltip(TR("console_click_insert"), cmd.name);
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
if (showParams) {
|
||||
ImGui::TableNextColumn();
|
||||
drawConsoleCommandParams(cmd.params);
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextColored(
|
||||
ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()),
|
||||
"%s", cmd.desc);
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
for (int c = 0; c < (int)categories.size(); c++) {
|
||||
// Subtle accent-labelled section header (no heavy filled bar).
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
|
||||
ImGui::ColorConvertU32ToFloat4(WithAlpha(OnSurface(), 14)));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive,
|
||||
ImGui::ColorConvertU32ToFloat4(WithAlpha(OnSurface(), 20)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
|
||||
bool open = ImGui::CollapsingHeader(consoleCategoryLabel(categories[c].name),
|
||||
ImGuiTreeNodeFlags_DefaultOpen);
|
||||
ImGui::PopStyleColor(4);
|
||||
if (open)
|
||||
for (int i = 0; i < categories[c].count; i++) drawRow(c, i);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopFont();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::SameLine(0, gap);
|
||||
|
||||
// DETAIL pane.
|
||||
{
|
||||
ImVec2 dMin = ImGui::GetCursorScreenPos();
|
||||
DrawGlassPanel(ImGui::GetWindowDrawList(), dMin, ImVec2(dMin.x + detailW, dMin.y + bodyH), paneGlass);
|
||||
}
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingMd()));
|
||||
ImGui::BeginChild("##cmdDetail", ImVec2(detailW, bodyH), ImGuiChildFlags_AlwaysUseWindowPadding);
|
||||
if (cmd_sel_cat_ >= 0 && cmd_sel_cat_ < (int)categories.size() &&
|
||||
cmd_sel_idx_ >= 0 && cmd_sel_idx_ < categories[cmd_sel_cat_].count) {
|
||||
renderCommandDetail(categories[cmd_sel_cat_].commands[cmd_sel_idx_],
|
||||
consoleCategoryLabel(categories[cmd_sel_cat_].name));
|
||||
} else {
|
||||
ImVec2 av = ImGui::GetContentRegionAvail();
|
||||
ImGui::SetCursorPosY(av.y * 0.4f);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_ref_select_hint"));
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
// Footer.
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Close button
|
||||
if (material::TactileButton(TR("console_close"), ImVec2(-1, 0))) {
|
||||
cmdFilter[0] = '\0';
|
||||
command_search_[0] = '\0';
|
||||
run_confirm_cmd_ = nullptr;
|
||||
show_commands_popup_ = false;
|
||||
}
|
||||
|
||||
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
@@ -1639,8 +1980,17 @@ void ConsoleTab::drainModel()
|
||||
// with them so the highlight stays on the text the user selected.
|
||||
selection_.shiftForEviction(static_cast<int>(dr.popped));
|
||||
|
||||
// Track new output that arrived while the user is scrolled up (for the indicator).
|
||||
scroll_.onLinesAdded(static_cast<int>(dr.added));
|
||||
// Track new output that arrived while the user is scrolled up (for the "N new lines" indicator).
|
||||
// Count only the newly-added lines that pass the active filter, so the indicator isn't inflated
|
||||
// by lines the current filter / errors-only view hides — the user wouldn't see those on jumping
|
||||
// to the bottom. (The new lines are the last dr.added entries; eviction is from the front.)
|
||||
ConsoleOutputFilter f{filter_text_, s_daemon_messages_enabled, s_errors_only_enabled,
|
||||
s_rpc_trace_enabled, s_app_messages_enabled};
|
||||
const int n = static_cast<int>(model_.size());
|
||||
int visibleAdded = 0;
|
||||
for (int i = std::max(0, n - static_cast<int>(dr.added)); i < n; i++)
|
||||
if (consoleLinePassesFilter(model_[i].text, model_[i].channel, f)) ++visibleAdded;
|
||||
scroll_.onLinesAdded(visibleAdded);
|
||||
}
|
||||
|
||||
void ConsoleTab::addRpcTraceLine(const std::string& source, const std::string& method)
|
||||
@@ -1654,6 +2004,7 @@ void ConsoleTab::clear()
|
||||
// View-only clear (main thread). The executor keeps its own log cursors, so new output
|
||||
// still appends. The "cleared" line is ingested and appears on the next frame's drain.
|
||||
model_.clear();
|
||||
stop_confirm_pending_ = false; // a pending 'stop' confirmation is cancelled by clearing
|
||||
addLine(TR("console_cleared"), ConsoleChannel::Info);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace ui {
|
||||
|
||||
class ConsoleCommandExecutor;
|
||||
struct ConsoleLogFilterCaps;
|
||||
struct ConsoleCommandEntry;
|
||||
|
||||
/**
|
||||
* @brief Console tab — a rich terminal shared by the full-node and lite variants.
|
||||
@@ -69,6 +70,12 @@ public:
|
||||
// Console output zoom factor (1.0 = default caption font size)
|
||||
static float s_console_zoom;
|
||||
|
||||
// Draw the per-line left color accent bars (channel-colored). Toggled from the toolbar.
|
||||
static bool s_line_accents_enabled;
|
||||
|
||||
// Color output text per channel. When false the console is monochrome text. Toolbar-toggled.
|
||||
static bool s_line_text_color_enabled;
|
||||
|
||||
// Show/hide daemon output messages
|
||||
static bool s_daemon_messages_enabled;
|
||||
|
||||
@@ -109,6 +116,8 @@ private:
|
||||
void addFormattedResult(const std::string& result, bool is_error);
|
||||
void renderStatusHeader(ConsoleCommandExecutor& exec);
|
||||
void renderCommandsPopup();
|
||||
void renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel); // right pane
|
||||
void insertCommandToInput(const ConsoleCommandEntry& cmd); // fill input + close the modal
|
||||
|
||||
// renderToolbar() draws the top bar; these are its sub-steps:
|
||||
void renderToolbar(ConsoleCommandExecutor& exec);
|
||||
@@ -144,6 +153,7 @@ private:
|
||||
std::vector<std::string> command_history_;
|
||||
int history_index_ = -1;
|
||||
char input_buffer_[4096] = {0};
|
||||
bool stop_confirm_pending_ = false; // 'stop' typed once, awaiting a confirming second 'stop'
|
||||
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)
|
||||
|
||||
// Auto-scroll state machine (pin-to-bottom, wheel-up cooldown, new-line backlog count).
|
||||
@@ -167,14 +177,24 @@ private:
|
||||
mutable int filter_match_count_ = 0; // lines matching the text filter (for the toolbar)
|
||||
std::string context_token_; // hash/address under the cursor at right-click
|
||||
mutable std::vector<int> visible_indices_; // Cached for selection mapping
|
||||
bool folding_active_ = true; // fold UI shown only in the unfiltered (foldable) view
|
||||
bool has_text_filter_ = false; // computed once per frame (before the toolbar draws it)
|
||||
std::string filter_lower_; // lowercased filter needle for match highlighting
|
||||
|
||||
// Wrap layout for the visible lines (segments + per-line heights + cumulative Y),
|
||||
// recomputed each frame by the pure BuildConsoleLayout (console_text_layout.h) and
|
||||
// consumed by the renderer + hit-testing.
|
||||
mutable ConsoleLayout layout_;
|
||||
|
||||
// Commands popup
|
||||
// Commands popup (RPC command explorer)
|
||||
bool show_commands_popup_ = false;
|
||||
char command_search_[128] = {0}; // RPC-reference search filter (cleared when the modal opens)
|
||||
int cmd_sel_cat_ = -1; // detail-pane selection: category index into consoleCommandCategories()
|
||||
int cmd_sel_idx_ = -1; // detail-pane selection: command index within that category
|
||||
std::string pending_submit_; // command to run next frame (deferred so the modal needs no executor)
|
||||
const ConsoleCommandEntry* run_confirm_cmd_ = nullptr; // destructive "Insert & run" awaiting confirmation
|
||||
char cmd_param_bufs_[6][256] = {{0}}; // parameter-builder input fields
|
||||
const ConsoleCommandEntry* cmd_param_owner_ = nullptr; // command the param buffers currently hold
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
#include "../widgets/copy_field.h"
|
||||
#include "../theme.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "imgui.h"
|
||||
@@ -29,7 +30,8 @@ using json = nlohmann::json;
|
||||
// Static state
|
||||
static bool s_open = false;
|
||||
static bool s_exporting = false;
|
||||
static std::string s_status;
|
||||
static std::string s_status; // error / partial message (non-empty => red text)
|
||||
static std::string s_saved_path; // full path on full success (=> green check + copy field)
|
||||
static std::string s_exported_keys;
|
||||
static int s_total_addresses = 0;
|
||||
static int s_exported_count = 0;
|
||||
@@ -42,6 +44,7 @@ void ExportAllKeysDialog::show()
|
||||
s_open = true;
|
||||
s_exporting = false;
|
||||
s_status.clear();
|
||||
s_saved_path.clear();
|
||||
s_exported_keys.clear();
|
||||
s_total_addresses = 0;
|
||||
s_exported_count = 0;
|
||||
@@ -60,74 +63,77 @@ bool ExportAllKeysDialog::isOpen()
|
||||
return s_open;
|
||||
}
|
||||
|
||||
void ExportAllKeysDialog::hide()
|
||||
{
|
||||
s_open = false;
|
||||
s_status.clear();
|
||||
s_saved_path.clear();
|
||||
}
|
||||
|
||||
void ExportAllKeysDialog::render(App* app)
|
||||
{
|
||||
if (!s_open) return;
|
||||
namespace m = material;
|
||||
|
||||
auto& S = schema::UI();
|
||||
auto win = S.window("dialogs.export-all-keys");
|
||||
auto exportBtn = S.button("dialogs.export-all-keys", "export-button");
|
||||
auto closeBtn = S.button("dialogs.export-all-keys", "close-button");
|
||||
m::OverlayDialogSpec ov;
|
||||
ov.title = TR("export_keys_title");
|
||||
ov.p_open = &s_open;
|
||||
ov.style = m::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 600.0f;
|
||||
ov.idSuffix = "exportallkeys";
|
||||
if (m::BeginOverlayDialog(ov)) {
|
||||
m::DialogWarningHeader(TR("export_keys_danger"));
|
||||
ImGui::Spacing();
|
||||
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("export_keys_title"); ov.p_open = &s_open;
|
||||
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
|
||||
ov.cardWidth = win.width; ov.cardBottomViewportRatio = 0.94f; // keep authored width
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
// Warning
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
ImGui::Text(ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f);
|
||||
ImGui::TextWrapped("%s", TR("export_keys_danger"));
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (s_exporting) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
// Options
|
||||
ImGui::Text("%s", TR("export_keys_options"));
|
||||
ImGui::BeginDisabled(s_exporting);
|
||||
ImGui::TextUnformatted(TR("export_keys_options"));
|
||||
ImGui::Checkbox(TR("export_keys_include_z"), &s_include_z);
|
||||
ImGui::Checkbox(TR("export_keys_include_t"), &s_include_t);
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Filename
|
||||
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
||||
|
||||
m::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("%s", TR("file_save_location"));
|
||||
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("file_save_location"));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
// Progress / result.
|
||||
if (s_exporting) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::Primary()), TR("export_keys_progress"),
|
||||
s_exported_count, s_total_addresses);
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::Primary()), "%s", m::LoadingDots());
|
||||
} else if (!s_saved_path.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushFont(m::Type().iconSmall());
|
||||
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f * Layout::dpiScale());
|
||||
ImGui::TextColored(m::SuccessVec4(), "%s", TR("export_keys_success"));
|
||||
widgets::AddressCopyField("##exportedkeyspath", s_saved_path);
|
||||
} else if (!s_status.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", s_status.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Export button
|
||||
if (s_exporting) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
if (material::StyledButton(TR("export_keys_btn"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
||||
|
||||
bool doExport = false, doClose = false;
|
||||
m::DialogActionFooter(TR("export_keys_btn"), !s_exporting, TR("close"), doExport, doClose);
|
||||
if (doClose) s_open = false;
|
||||
if (doExport) {
|
||||
if (!s_include_z && !s_include_t) {
|
||||
Notifications::instance().warning("Select at least one address type");
|
||||
Notifications::instance().warning(TR("export_keys_select_type"));
|
||||
} else if (!app->rpc() || !app->rpc()->isConnected()) {
|
||||
Notifications::instance().error("Not connected to daemon");
|
||||
Notifications::instance().error(TR("export_keys_not_connected"));
|
||||
} else {
|
||||
s_exporting = true;
|
||||
s_exported_keys.clear();
|
||||
s_exported_count = 0;
|
||||
s_status = "Exporting keys...";
|
||||
|
||||
s_status.clear();
|
||||
s_saved_path.clear();
|
||||
|
||||
const auto& state = app->getWalletState();
|
||||
|
||||
// Count total addresses to export
|
||||
@@ -137,7 +143,8 @@ void ExportAllKeysDialog::render(App* app)
|
||||
|
||||
if (s_total_addresses == 0) {
|
||||
s_exporting = false;
|
||||
s_status = "No addresses to export";
|
||||
s_status = TR("export_keys_none_addrs");
|
||||
m::EndOverlayDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -216,21 +223,21 @@ void ExportAllKeysDialog::render(App* app)
|
||||
return [exported, total, filepath, writeOk]() {
|
||||
s_exported_count = exported;
|
||||
s_exporting = false;
|
||||
char buf[320];
|
||||
if (exported == 0) {
|
||||
s_status = "No keys exported (0 of " + std::to_string(total) +
|
||||
") — unlock the wallet (if encrypted) and try again.";
|
||||
Notifications::instance().error("No keys could be exported — is the wallet unlocked?");
|
||||
snprintf(buf, sizeof(buf), TR("export_keys_none_result"), total);
|
||||
s_status = buf;
|
||||
Notifications::instance().error(TR("export_keys_none_toast"));
|
||||
} else if (!writeOk) {
|
||||
s_status = "Failed to write file";
|
||||
Notifications::instance().error("Failed to save key file");
|
||||
s_status = TR("export_keys_write_fail");
|
||||
Notifications::instance().error(TR("export_keys_write_fail"));
|
||||
} else if (exported < total) {
|
||||
s_status = "Exported " + std::to_string(exported) + " of " +
|
||||
std::to_string(total) + " keys to: " + filepath +
|
||||
" (INCOMPLETE — some addresses had no spending key or the wallet is locked)";
|
||||
Notifications::instance().warning("Partial export: " + std::to_string(exported) +
|
||||
" of " + std::to_string(total) + " keys");
|
||||
snprintf(buf, sizeof(buf), TR("export_keys_partial"), exported, total);
|
||||
s_status = buf;
|
||||
snprintf(buf, sizeof(buf), TR("export_keys_partial_toast"), exported, total);
|
||||
Notifications::instance().warning(buf);
|
||||
} else {
|
||||
s_status = "Exported to: " + filepath;
|
||||
s_saved_path = filepath;
|
||||
Notifications::instance().success(TR("export_keys_success"), 5.0f);
|
||||
}
|
||||
};
|
||||
@@ -238,24 +245,8 @@ void ExportAllKeysDialog::render(App* app)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s_exporting) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled(TR("export_keys_progress"), s_exported_count, s_total_addresses);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
// Status
|
||||
if (!s_status.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", s_status.c_str());
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
|
||||
m::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ public:
|
||||
|
||||
// Check if dialog is open
|
||||
static bool isOpen();
|
||||
|
||||
// Close the dialog (used by the UI sweep teardown)
|
||||
static void hide();
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
#include "../notifications.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
#include "../widgets/copy_field.h"
|
||||
#include "../theme.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <string>
|
||||
@@ -24,7 +27,8 @@ namespace ui {
|
||||
// Static state
|
||||
static bool s_open = false;
|
||||
static char s_filename[256] = "";
|
||||
static std::string s_status;
|
||||
static std::string s_saved_path; // full path on success (shown as a copy field); empty otherwise
|
||||
static std::string s_error; // error message on failure; empty otherwise
|
||||
// Re-entrancy guard: true while a CSV write is in flight (disables the
|
||||
// Export button so a second synchronous write can't be kicked off).
|
||||
static bool s_exporting = false;
|
||||
@@ -52,8 +56,9 @@ static std::string escapeCSV(const std::string& field)
|
||||
void ExportTransactionsDialog::show()
|
||||
{
|
||||
s_open = true;
|
||||
s_status.clear();
|
||||
|
||||
s_saved_path.clear();
|
||||
s_error.clear();
|
||||
|
||||
// Generate default filename with timestamp
|
||||
std::time_t now = std::time(nullptr);
|
||||
char timebuf[32];
|
||||
@@ -66,104 +71,96 @@ bool ExportTransactionsDialog::isOpen()
|
||||
return s_open;
|
||||
}
|
||||
|
||||
void ExportTransactionsDialog::hide()
|
||||
{
|
||||
s_open = false;
|
||||
s_saved_path.clear();
|
||||
s_error.clear();
|
||||
}
|
||||
|
||||
void ExportTransactionsDialog::render(App* app)
|
||||
{
|
||||
if (!s_open) return;
|
||||
namespace m = material;
|
||||
|
||||
auto& S = schema::UI();
|
||||
auto win = S.window("dialogs.export-transactions");
|
||||
auto exportBtn = S.button("dialogs.export-transactions", "export-button");
|
||||
auto closeBtn = S.button("dialogs.export-transactions", "close-button");
|
||||
m::OverlayDialogSpec ov;
|
||||
ov.title = TR("export_tx_title");
|
||||
ov.p_open = &s_open;
|
||||
ov.style = m::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 520.0f;
|
||||
ov.idSuffix = "exporttx";
|
||||
if (!m::BeginOverlayDialog(ov)) return;
|
||||
|
||||
if (material::BeginOverlayDialog(TR("export_tx_title"), &s_open, win.width, 0.94f)) {
|
||||
const auto& state = app->getWalletState();
|
||||
|
||||
ImGui::Text(TR("export_tx_count"), state.transactions.size());
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Filename
|
||||
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("%s", TR("file_save_location"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Export button (disabled while a write is already in flight)
|
||||
ImGui::BeginDisabled(s_exporting);
|
||||
if (material::StyledButton(TR("export"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
||||
if (state.transactions.empty()) {
|
||||
Notifications::instance().warning(TR("export_tx_none"));
|
||||
} else {
|
||||
s_exporting = true;
|
||||
std::string configDir = util::Platform::getConfigDir();
|
||||
std::string filepath = configDir + "/" + s_filename;
|
||||
const auto& state = app->getWalletState();
|
||||
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
s_status = "Failed to create file";
|
||||
Notifications::instance().error(TR("export_tx_file_fail"));
|
||||
} else {
|
||||
// Write CSV header
|
||||
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
|
||||
|
||||
// Write transactions
|
||||
for (const auto& tx : state.transactions) {
|
||||
// Date
|
||||
std::time_t t = static_cast<std::time_t>(tx.timestamp);
|
||||
char datebuf[32];
|
||||
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
||||
file << datebuf << ",";
|
||||
|
||||
// Type
|
||||
file << escapeCSV(tx.type) << ",";
|
||||
|
||||
// Amount
|
||||
std::ostringstream amt;
|
||||
amt << std::fixed << std::setprecision(8) << tx.amount;
|
||||
file << amt.str() << ",";
|
||||
|
||||
// Address
|
||||
file << escapeCSV(tx.address) << ",";
|
||||
|
||||
// TXID
|
||||
file << escapeCSV(tx.txid) << ",";
|
||||
|
||||
// Confirmations
|
||||
file << tx.confirmations << ",";
|
||||
|
||||
// Memo
|
||||
file << escapeCSV(tx.memo) << "\n";
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
s_status = "Exported " + std::to_string(state.transactions.size()) +
|
||||
" transactions to: " + filepath;
|
||||
Notifications::instance().success(TR("export_tx_success"), 5.0f);
|
||||
}
|
||||
s_exporting = false;
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Text(TR("export_tx_count"), state.transactions.size());
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
// Status
|
||||
if (!s_status.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", s_status.c_str());
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
// Filename + save-location hint.
|
||||
m::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("file_save_location"));
|
||||
|
||||
// Result — success shows the saved path as a copy field; failure shows a red line.
|
||||
if (!s_saved_path.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushFont(m::Type().iconSmall());
|
||||
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f * Layout::dpiScale());
|
||||
ImGui::TextColored(m::SuccessVec4(), "%s", TR("export_tx_success"));
|
||||
widgets::AddressCopyField("##exportedpath", s_saved_path);
|
||||
} else if (!s_error.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", s_error.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
bool doExport = false, doClose = false;
|
||||
m::DialogActionFooter(TR("export"), !s_exporting, TR("close"), doExport, doClose);
|
||||
if (doClose) s_open = false;
|
||||
if (doExport) {
|
||||
s_saved_path.clear();
|
||||
s_error.clear();
|
||||
if (state.transactions.empty()) {
|
||||
Notifications::instance().warning(TR("export_tx_none"));
|
||||
} else {
|
||||
s_exporting = true;
|
||||
std::string configDir = util::Platform::getConfigDir();
|
||||
std::string filepath = configDir + "/" + s_filename;
|
||||
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
s_error = TR("export_tx_file_fail");
|
||||
Notifications::instance().error(TR("export_tx_file_fail"));
|
||||
} else {
|
||||
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
|
||||
for (const auto& tx : state.transactions) {
|
||||
std::time_t t = static_cast<std::time_t>(tx.timestamp);
|
||||
char datebuf[32];
|
||||
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
||||
file << datebuf << ",";
|
||||
file << escapeCSV(tx.type) << ",";
|
||||
std::ostringstream amt;
|
||||
amt << std::fixed << std::setprecision(8) << tx.amount;
|
||||
file << amt.str() << ",";
|
||||
file << escapeCSV(tx.address) << ",";
|
||||
file << escapeCSV(tx.txid) << ",";
|
||||
file << tx.confirmations << ",";
|
||||
file << escapeCSV(tx.memo) << "\n";
|
||||
}
|
||||
file.close();
|
||||
s_saved_path = filepath;
|
||||
Notifications::instance().success(TR("export_tx_success"), 5.0f);
|
||||
}
|
||||
s_exporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
m::EndOverlayDialog();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -25,6 +25,9 @@ public:
|
||||
|
||||
// Check if dialog is open
|
||||
static bool isOpen();
|
||||
|
||||
// Close the dialog (used by the UI sweep teardown)
|
||||
static void hide();
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -293,12 +293,29 @@ static void PortfolioBeginEdit(App* app, int index)
|
||||
// ---- Portfolio-editor persistence helpers (operate on the s_pfEdit working state) ----
|
||||
|
||||
// Build a PortfolioEntry from the working set, preserving off-form fields (grid geometry) from base.
|
||||
// Normalized views of the two free-text buffers, applied identically by pfBuildWorking (what we store)
|
||||
// and pfWorkingMatches (the dirty check) so the two can never disagree: a trailing space in the label
|
||||
// or an emptied currency field must not leave the group perpetually "dirty" after a save.
|
||||
static std::string pfEditLabel() // label with leading/trailing whitespace trimmed
|
||||
{
|
||||
std::string s(s_pfEdit.label);
|
||||
size_t i = 0, j = s.size();
|
||||
while (i < j && (unsigned char)s[i] <= ' ') i++;
|
||||
while (j > i && (unsigned char)s[j - 1] <= ' ') j--;
|
||||
return s.substr(i, j - i);
|
||||
}
|
||||
static std::string pfEditCurrency() // currency, defaulting an empty buffer to USD
|
||||
{
|
||||
return s_pfEdit.manualCcy[0] != '\0' ? std::string(s_pfEdit.manualCcy) : std::string("USD");
|
||||
}
|
||||
|
||||
static config::Settings::PortfolioEntry pfBuildWorking(const config::Settings::PortfolioEntry& base)
|
||||
{
|
||||
config::Settings::PortfolioEntry e = base;
|
||||
e.label = s_pfEdit.label; e.addresses = s_pfEdit.addrs; e.icon = s_pfEdit.icon;
|
||||
e.label = pfEditLabel(); e.addresses = s_pfEdit.addrs; e.icon = s_pfEdit.icon;
|
||||
e.color = s_pfEdit.color; e.outlineOpacity = s_pfEdit.outlineOpacity;
|
||||
e.priceBasis = s_pfEdit.priceBasis; e.manualPrice = s_pfEdit.manualPrice; e.manualCurrency = s_pfEdit.manualCcy;
|
||||
e.priceBasis = s_pfEdit.priceBasis; e.manualPrice = s_pfEdit.manualPrice;
|
||||
e.manualCurrency = pfEditCurrency();
|
||||
e.showDrgx = s_pfEdit.showDrgx; e.showValue = s_pfEdit.showValue; e.show24h = s_pfEdit.show24h;
|
||||
e.showSparkline = s_pfEdit.showSparkline; e.sparklineInterval = s_pfEdit.sparkInterval;
|
||||
return e;
|
||||
@@ -307,14 +324,24 @@ static config::Settings::PortfolioEntry pfBuildWorking(const config::Settings::P
|
||||
// True when the working set matches a stored entry (i.e. there are no uncommitted edits).
|
||||
static bool pfWorkingMatches(const config::Settings::PortfolioEntry& e)
|
||||
{
|
||||
return e.label == std::string(s_pfEdit.label) && e.addresses == s_pfEdit.addrs && e.icon == s_pfEdit.icon
|
||||
return e.label == pfEditLabel() && e.addresses == s_pfEdit.addrs && e.icon == s_pfEdit.icon
|
||||
&& e.color == s_pfEdit.color && e.outlineOpacity == s_pfEdit.outlineOpacity
|
||||
&& e.priceBasis == s_pfEdit.priceBasis && e.manualPrice == s_pfEdit.manualPrice
|
||||
&& e.manualCurrency == std::string(s_pfEdit.manualCcy)
|
||||
&& e.manualCurrency == pfEditCurrency()
|
||||
&& e.showDrgx == s_pfEdit.showDrgx && e.showValue == s_pfEdit.showValue && e.show24h == s_pfEdit.show24h
|
||||
&& e.showSparkline == s_pfEdit.showSparkline && e.sparklineInterval == s_pfEdit.sparkInterval;
|
||||
}
|
||||
|
||||
// A group is only worth persisting when it has a name, at least one address (else it's always $0), and
|
||||
// — on the Manual price basis — a positive price (0 would render as "unavailable"). Gates both the Save
|
||||
// button and the commit path (Close/switch commit too, so validating only the button wouldn't be enough).
|
||||
static bool pfWorkingValid()
|
||||
{
|
||||
return !pfEditLabel().empty() // a whitespace-only label is treated as empty (invisible group)
|
||||
&& !s_pfEdit.addrs.empty()
|
||||
&& !(s_pfEdit.priceBasis == 3 && s_pfEdit.manualPrice <= 0.0);
|
||||
}
|
||||
|
||||
static void pfPersist(config::Settings* settings, const std::vector<config::Settings::PortfolioEntry>& entries)
|
||||
{
|
||||
settings->setPortfolioEntries(entries);
|
||||
@@ -322,23 +349,48 @@ static void pfPersist(config::Settings* settings, const std::vector<config::Sett
|
||||
}
|
||||
|
||||
// Persist the working group when it is named and actually changed; unnamed drafts are dropped.
|
||||
// New groups default to the active wallet's scope (per-wallet visibility); existing groups keep
|
||||
// whatever scope they already had.
|
||||
// New groups adopt the active wallet's scope (per-wallet visibility). An existing group with no scope
|
||||
// — a placeholder added before the wallet identity resolved, or a pre-scoping legacy group — is also
|
||||
// claimed for the active wallet on edit, so it can't linger in the global (empty-scope) bucket and
|
||||
// leak into every wallet's list. No-op when the identity is unavailable (stays unscoped until edited
|
||||
// with a live identity).
|
||||
static void pfCommitIfNeeded(App* app)
|
||||
{
|
||||
config::Settings* settings = app->settings();
|
||||
if (s_pfEdit.label[0] == '\0') return;
|
||||
if (!pfWorkingValid()) return; // drop invalid drafts (unnamed / no addresses / manual price <= 0)
|
||||
auto entries = settings->getPortfolioEntries();
|
||||
bool existing = (s_pfEdit.sel >= 0 && s_pfEdit.sel < (int)entries.size());
|
||||
config::Settings::PortfolioEntry base;
|
||||
if (existing) { base = entries[s_pfEdit.sel]; if (pfWorkingMatches(base)) return; }
|
||||
auto e = pfBuildWorking(base);
|
||||
if (!existing) e.scope = app->activeWalletIdentityHash();
|
||||
if (e.scope.empty()) {
|
||||
const std::string h = app->activeWalletIdentityHash();
|
||||
if (!h.empty()) e.scope = h;
|
||||
}
|
||||
if (existing) entries[s_pfEdit.sel] = e;
|
||||
else { entries.push_back(e); s_pfEdit.sel = (int)entries.size() - 1; }
|
||||
pfPersist(settings, entries);
|
||||
}
|
||||
|
||||
// A group is visible to the active wallet when it carries the wallet's scope, is unscoped (legacy —
|
||||
// shown everywhere), or the wallet identity isn't resolved yet. The editor mirrors the Market summary's
|
||||
// scope filter so it only lists/edits the current wallet's groups.
|
||||
static bool pfIndexVisible(App* app, int i)
|
||||
{
|
||||
const auto& es = app->settings()->getPortfolioEntries();
|
||||
if (i < 0 || i >= (int)es.size()) return false;
|
||||
const std::string h = app->activeWalletIdentityHash();
|
||||
return es[i].scope.empty() || h.empty() || es[i].scope == h;
|
||||
}
|
||||
static int pfFirstVisibleIndex(App* app)
|
||||
{
|
||||
const auto& es = app->settings()->getPortfolioEntries();
|
||||
const std::string h = app->activeWalletIdentityHash();
|
||||
for (int i = 0; i < (int)es.size(); i++)
|
||||
if (es[i].scope.empty() || h.empty() || es[i].scope == h) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---- Detail-pane section renderers (drawn inside ##pfDetailBody; operate on s_pfEdit) ----
|
||||
|
||||
// Appearance: accent-color picker (+ custom), outline-opacity slider, icon grid.
|
||||
@@ -513,7 +565,8 @@ static void pfDrawPriceSection()
|
||||
float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f;
|
||||
ImGui::SetNextItemWidth(half);
|
||||
ImGui::InputDouble("##pfManPrice", &s_pfEdit.manualPrice, 0.0, 0.0, "%.6f");
|
||||
if (s_pfEdit.manualPrice < 0.0) s_pfEdit.manualPrice = 0.0;
|
||||
// Reject negatives and non-finite input (InputDouble parses "inf"/"nan"; neither is < 0).
|
||||
if (!std::isfinite(s_pfEdit.manualPrice) || s_pfEdit.manualPrice < 0.0) s_pfEdit.manualPrice = 0.0;
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pfEdit.manualCcy, sizeof(s_pfEdit.manualCcy));
|
||||
@@ -635,8 +688,8 @@ static void pfDrawAddressSection(App* app)
|
||||
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rmx(rmn.x + lw, rmn.y + rowH);
|
||||
bool rhov = ImGui::IsMouseHoveringRect(rmn, rmx);
|
||||
if (inSet) ldl->AddRectFilled(rmn, rmx, WithAlpha(Primary(), 22), 4.0f);
|
||||
else if (rhov) ldl->AddRectFilled(rmn, rmx, IM_COL32(255, 255, 255, 14), 4.0f);
|
||||
if (inSet) ldl->AddRectFilled(rmn, rmx, WithAlpha(Primary(), 22), 4.0f * dp);
|
||||
else if (rhov) ldl->AddRectFilled(rmn, rmx, IM_COL32(255, 255, 255, 14), 4.0f * dp);
|
||||
float rcy = rmn.y + rowH * 0.5f;
|
||||
float rx = rmn.x + Layout::spacingSm();
|
||||
float cb = 15.0f * dp;
|
||||
@@ -654,9 +707,9 @@ static void pfDrawAddressSection(App* app)
|
||||
bool isZ = (a.type == "shielded");
|
||||
const char* chip = isZ ? "Z" : "T";
|
||||
ImU32 chipCol = isZ ? Success() : Warning();
|
||||
ImVec2 chipPos(rx, rcy - capF->LegacySize * 0.5f - 2.0f);
|
||||
ImVec2 chipPos(rx, rcy - capF->LegacySize * 0.5f - 2.0f * dp);
|
||||
float chipW = material::DrawPill(ldl, chipPos, chip, capF, chipCol,
|
||||
WithAlpha(chipCol, 40), 0, ImVec2(4.0f * dp, 2.0f), 4.0f * dp).x;
|
||||
WithAlpha(chipCol, 40), 0, ImVec2(4.0f * dp, 2.0f * dp), 4.0f * dp).x;
|
||||
rx += chipW + Layout::spacingSm();
|
||||
std::string aicon = app->getAddressIcon(a.address);
|
||||
if (!aicon.empty()) {
|
||||
@@ -706,9 +759,9 @@ static void RenderPortfolioEditor(App* app)
|
||||
|
||||
// ---------------- Full-window blur overlay (shared framework) ----------------
|
||||
// Backdrop + capture-once, floating card, plain heading, outside-click dismiss and effect
|
||||
// suppression are owned by material::BeginOverlayDialog (BlurFloat). Esc still closes here;
|
||||
// Esc/outside-click discard (only Close commits); LatchBlurOverlayActive (App::render) handles
|
||||
// the acrylic re-capture on close.
|
||||
// suppression are owned by material::BeginOverlayDialog (BlurFloat). Every dismiss gesture
|
||||
// (Close button, Esc, outside-click, switching/adding/deleting groups) auto-saves the current
|
||||
// group via pfCommitIfNeeded; LatchBlurOverlayActive (App::render) does the acrylic re-capture.
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("portfolio_manage_title");
|
||||
ov.p_open = &s_pfEdit.open;
|
||||
@@ -717,11 +770,28 @@ static void RenderPortfolioEditor(App* app)
|
||||
ov.cardHeight = 760.0f;
|
||||
ov.idSuffix = "pf";
|
||||
if (!material::BeginOverlayDialog(ov)) return;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_pfEdit.open = false;
|
||||
// Outside-click dismiss is handled inside BeginOverlayDialog, which clears s_pfEdit.open mid-frame
|
||||
// (the card still draws one final frame). Auto-save now, before that frame: the placeholder GC in
|
||||
// mktDrawPortfolio already ran this frame while open was still true, so entry indices haven't
|
||||
// shifted under us and pfCommitIfNeeded targets the right slot.
|
||||
if (!s_pfEdit.open) pfCommitIfNeeded(app);
|
||||
// A first Escape should dismiss only an open popup (e.g. the custom-color picker); don't also tear
|
||||
// down the whole editor while a popup is capturing the key. Otherwise Escape auto-saves and closes.
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape) &&
|
||||
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) {
|
||||
pfCommitIfNeeded(app);
|
||||
s_pfEdit.open = false;
|
||||
}
|
||||
|
||||
{ // clamp/validate the selection against the current entry list
|
||||
{ // Clamp/validate the selection. The in-editor delete already reloads the working state, but if the
|
||||
// list shrank or the selection landed on a group outside this wallet's scope via any other path,
|
||||
// reset to the first visible group and reload so a later commit can't overlay stale fields.
|
||||
const auto& entriesRO = settings->getPortfolioEntries();
|
||||
if (s_pfEdit.sel >= (int)entriesRO.size()) s_pfEdit.sel = entriesRO.empty() ? -1 : 0;
|
||||
if (s_pfEdit.sel >= (int)entriesRO.size() ||
|
||||
(s_pfEdit.sel >= 0 && !pfIndexVisible(app, s_pfEdit.sel))) {
|
||||
s_pfEdit.sel = pfFirstVisibleIndex(app);
|
||||
PortfolioBeginEdit(app, s_pfEdit.sel);
|
||||
}
|
||||
}
|
||||
|
||||
float footerH = 56.0f * dp; // room for the 40px buttons + separator so they aren't clipped
|
||||
@@ -732,8 +802,8 @@ static void RenderPortfolioEditor(App* app)
|
||||
float detailW = contentW - masterW - gap;
|
||||
|
||||
// Does the selected group have uncommitted edits? (Only the selected group can differ from the
|
||||
// stored copy — others were committed on deselect.) Drives the master-list "unsaved" dot and the
|
||||
// detail-pane Revert/Save enable state.
|
||||
// stored copy — the working buffer only ever holds the selected group; switching reloads it and
|
||||
// discards any uncommitted edits.) Drives the master-list "unsaved" dot and Revert/Save state.
|
||||
bool selDirty = false;
|
||||
{
|
||||
const auto& es = settings->getPortfolioEntries();
|
||||
@@ -748,12 +818,20 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImGui::BeginChild("##pfMasterList", ImVec2(masterW, std::max(48.0f, bodyH - addH - Layout::spacingSm())), false);
|
||||
{
|
||||
ImDrawList* mdl = ImGui::GetWindowDrawList();
|
||||
if (entries.empty())
|
||||
// Only list groups scoped to the active wallet (or legacy/unscoped) — same filter the
|
||||
// Market summary uses. `i` stays the storage index (selection/delete operate on storage).
|
||||
const std::string activeHash = app->activeWalletIdentityHash();
|
||||
std::vector<int> vis;
|
||||
for (int i = 0; i < (int)entries.size(); i++)
|
||||
if (entries[i].scope.empty() || activeHash.empty() || entries[i].scope == activeHash)
|
||||
vis.push_back(i);
|
||||
if (vis.empty())
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||
float rowH = 46.0f * dp;
|
||||
float delSlot = 36.0f * dp; // reserved trailing space for the delete icon + margin
|
||||
int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop
|
||||
for (int i = 0; i < (int)entries.size(); i++) {
|
||||
for (int vi = 0; vi < (int)vis.size(); vi++) {
|
||||
int i = vis[vi];
|
||||
ImGui::PushID(i);
|
||||
const auto& en = entries[i];
|
||||
bool selRow = (s_pfEdit.sel == i);
|
||||
@@ -808,7 +886,9 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (delRow >= 0) {
|
||||
// Operates on the stored entries; any uncommitted working edits are discarded.
|
||||
// Auto-save edits to a *different* selected group before mutating the list; only the
|
||||
// deleted row's own working state is dropped. (Re-read entries after the commit.)
|
||||
if (delRow != s_pfEdit.sel) pfCommitIfNeeded(app);
|
||||
auto es = settings->getPortfolioEntries();
|
||||
if (delRow < (int)es.size()) {
|
||||
es.erase(es.begin() + delRow);
|
||||
@@ -819,14 +899,16 @@ static void RenderPortfolioEditor(App* app)
|
||||
PortfolioBeginEdit(app, s_pfEdit.sel);
|
||||
}
|
||||
} else if (clickedSel != -999) {
|
||||
PortfolioBeginEdit(app, clickedSel); // switching groups discards uncommitted edits
|
||||
pfCommitIfNeeded(app); // auto-save the current group before switching
|
||||
PortfolioBeginEdit(app, clickedSel);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
// Add immediately creates a persisted "Untitled" group and selects it for editing.
|
||||
if (material::TactileButton(TR("portfolio_add_entry"), ImVec2(masterW, addH))) {
|
||||
// Adding switches to a new group; uncommitted edits to the current one are discarded.
|
||||
// Auto-save the current group, then start a fresh one selected for editing.
|
||||
pfCommitIfNeeded(app);
|
||||
auto es = settings->getPortfolioEntries();
|
||||
config::Settings::PortfolioEntry ne;
|
||||
ne.label = TR("portfolio_untitled");
|
||||
@@ -869,7 +951,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
DrawGlassPanel(pdl, pMin, pMax, g);
|
||||
if (accent) {
|
||||
int oa = (int)(std::max(0, std::min(100, s_pfEdit.outlineOpacity)) * 2.55f + 0.5f);
|
||||
pdl->AddRect(pMin, pMax, WithAlpha(accent, oa), 10.0f, 0, 2.0f);
|
||||
pdl->AddRect(pMin, pMax, WithAlpha(accent, oa), 10.0f * dp, 0, 2.0f * dp);
|
||||
}
|
||||
if (s_pfEdit.showSparkline && (s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1)) {
|
||||
std::vector<double> h = data::sparklineSeries(state.market, s_pfEdit.sparkInterval);
|
||||
@@ -956,10 +1038,17 @@ static void RenderPortfolioEditor(App* app)
|
||||
if (material::TactileButton(TR("portfolio_revert"), ImVec2(bw, addH)))
|
||||
PortfolioBeginEdit(app, s_pfEdit.sel); // reload the working set from the stored entry
|
||||
ImGui::SameLine(0, sp);
|
||||
ImGui::BeginDisabled(s_pfEdit.label[0] == '\0');
|
||||
ImGui::BeginDisabled(!pfWorkingValid());
|
||||
if (material::TactileButton(TR("portfolio_save"), ImVec2(bw, addH))) pfCommitIfNeeded(app);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndDisabled();
|
||||
// Explain why Save is disabled — but only for a validation gap, not the "nothing changed"
|
||||
// (!selDirty) case, which is self-evident. Priority mirrors pfWorkingValid's checks.
|
||||
if (!pfWorkingValid() && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||
if (pfEditLabel().empty()) material::Tooltip("%s", TR("portfolio_save_need_name"));
|
||||
else if (s_pfEdit.addrs.empty()) material::Tooltip("%s", TR("portfolio_save_need_address"));
|
||||
else material::Tooltip("%s", TR("portfolio_save_need_price"));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild(); // ##pfDetail
|
||||
@@ -1406,7 +1495,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
bool bhov = material::IsRectHovered(bmn, bmx);
|
||||
ImU32 bg = sel ? WithAlpha(Primary(), 200)
|
||||
: (bhov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 18));
|
||||
dl->AddRectFilled(bmn, bmx, bg, 4.0f);
|
||||
dl->AddRectFilled(bmn, bmx, bg, 4.0f * mktDp);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(bx + Layout::spacingSm(), textY),
|
||||
sel ? IM_COL32(255, 255, 255, 255) : OnSurface(), kIvs[b].lbl);
|
||||
ImGui::SetCursorScreenPos(bmn);
|
||||
@@ -1429,7 +1518,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
const char* styleIcon = isCandle ? ICON_MD_CANDLESTICK_CHART : ICON_MD_SHOW_CHART;
|
||||
ImVec2 tmn(bx, rowTop), tmx(bx + pillH, rowTop + pillH);
|
||||
bool thov = material::IsRectHovered(tmn, tmx);
|
||||
if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f);
|
||||
if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f * mktDp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
|
||||
ImVec2 tiSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, styleIcon);
|
||||
dl->AddText(icoF, icoF->LegacySize,
|
||||
@@ -1450,7 +1539,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
ImFont* iconSmall = material::Typography::instance().iconSmall();
|
||||
ImVec2 rbMin(rEdge - pillH, rowTop), rbMax(rEdge, rowTop + pillH);
|
||||
bool refreshHov = material::IsRectHovered(rbMin, rbMax);
|
||||
if (refreshHov) { dl->AddRectFilled(rbMin, rbMax, IM_COL32(255, 255, 255, 20), 4.0f);
|
||||
if (refreshHov) { dl->AddRectFilled(rbMin, rbMax, IM_COL32(255, 255, 255, 20), 4.0f * mktDp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
|
||||
ImVec2 icSz = iconSmall->CalcTextSizeA(iconSmall->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH);
|
||||
dl->AddText(iconSmall, iconSmall->LegacySize,
|
||||
@@ -1754,6 +1843,20 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
float ratioBarH = cx.ratioBarH, pfSummaryH = cx.pfSummaryH, portfolioH = cx.portfolioH;
|
||||
char buf[128];
|
||||
|
||||
// Garbage-collect abandoned placeholders. "Add entry" persists an empty group up front so it shows
|
||||
// in the editor list; if the user closes the editor without ever adding an address, that phantom
|
||||
// "$0 · 0" group would linger. A group with no addresses can't be Saved (the editor forbids it), so
|
||||
// it is always junk — prune it, but only while the editor is closed so we never delete one the user
|
||||
// is actively filling in.
|
||||
if (!s_pfEdit.open) {
|
||||
auto es = app->settings()->getPortfolioEntries();
|
||||
size_t before = es.size();
|
||||
es.erase(std::remove_if(es.begin(), es.end(),
|
||||
[](const config::Settings::PortfolioEntry& e) { return e.addresses.empty(); }),
|
||||
es.end());
|
||||
if (es.size() != before) pfPersist(app->settings(), es);
|
||||
}
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
||||
// "Manage…" button, right-aligned on the header row — opens the portfolio editor.
|
||||
{
|
||||
@@ -1762,8 +1865,9 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
ImGui::SameLine();
|
||||
material::RightAlignX(mBtnW);
|
||||
if (material::TactileButton(ml, ImVec2(mBtnW, 0))) {
|
||||
// Open the combined editor selecting the first group (or the empty state if none).
|
||||
PortfolioBeginEdit(app, app->settings()->getPortfolioEntries().empty() ? -1 : 0);
|
||||
// Open the editor on the first group visible to this wallet (or the empty state if none) —
|
||||
// never a raw index 0 that might belong to a different wallet.
|
||||
PortfolioBeginEdit(app, pfFirstVisibleIndex(app));
|
||||
s_pfEdit.open = true;
|
||||
}
|
||||
}
|
||||
@@ -1797,12 +1901,16 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
chgCol, chg);
|
||||
}
|
||||
|
||||
double portfolio_btc = total_balance * market.price_btc;
|
||||
snprintf(buf, sizeof(buf), "\xE2\x89\x88 %.8f BTC", portfolio_btc); // "≈ <n> BTC"
|
||||
float btcW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - btcW, cy + (sub1->LegacySize - capFont->LegacySize)),
|
||||
OnSurfaceMedium(), buf);
|
||||
// Only show the BTC equivalent when a BTC price is actually available — otherwise it would read a
|
||||
// misleading "≈ 0.00000000 BTC" (price_usd can be present while price_btc is still 0).
|
||||
if (market.price_btc > 0) {
|
||||
double portfolio_btc = total_balance * market.price_btc;
|
||||
snprintf(buf, sizeof(buf), "\xE2\x89\x88 %.8f BTC", portfolio_btc); // "≈ <n> BTC"
|
||||
float btcW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - btcW, cy + (sub1->LegacySize - capFont->LegacySize)),
|
||||
OnSurfaceMedium(), buf);
|
||||
}
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx0, cy), OnSurfaceDisabled(), TR("market_no_price"));
|
||||
}
|
||||
@@ -1814,7 +1922,7 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
snprintf(buf, sizeof(buf), "Z %.4f \xC2\xB7 T %.4f", private_balance, transparent_balance);
|
||||
float brkW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - brkW, cy + 2), OnSurfaceDisabled(), buf);
|
||||
ImVec2(rightEdge - brkW, cy + 2.0f * mktDp), OnSurfaceDisabled(), buf);
|
||||
cy += body2->LegacySize + Layout::spacingSm();
|
||||
|
||||
// Full-width shielded/transparent ratio bar + % label.
|
||||
@@ -1832,16 +1940,16 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
const bool barLight = IsLightTheme();
|
||||
const int fillA = barLight ? 205 : 165;
|
||||
dl->AddRectFilled(barStart, ImVec2(barStart.x + barW, barStart.y + ratioBarH),
|
||||
barLight ? IM_COL32(0, 0, 0, 20) : IM_COL32(255, 255, 255, 10), 3.0f);
|
||||
barLight ? IM_COL32(0, 0, 0, 20) : IM_COL32(255, 255, 255, 10), 3.0f * mktDp);
|
||||
if (shieldedW > 0.5f)
|
||||
dl->AddRectFilled(barStart, ImVec2(barStart.x + shieldedW, barStart.y + ratioBarH),
|
||||
WithAlpha(Success(), fillA),
|
||||
transpW > 0.5f ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, 3.0f);
|
||||
transpW > 0.5f ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, 3.0f * mktDp);
|
||||
if (transpW > 0.5f)
|
||||
dl->AddRectFilled(ImVec2(barStart.x + shieldedW, barStart.y),
|
||||
ImVec2(barStart.x + barW, barStart.y + ratioBarH),
|
||||
WithAlpha(Warning(), fillA),
|
||||
shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f);
|
||||
shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f * mktDp);
|
||||
|
||||
// market_pct_shielded is "%.0f%% Shielded" — pass a double (an int to %f is UB).
|
||||
snprintf(buf, sizeof(buf), TR("market_pct_shielded"), static_cast<double>(shieldedRatio) * 100.0);
|
||||
|
||||
@@ -2,21 +2,14 @@
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Shared "Browse all releases" picker, used by both the miner updater (XmrigDownloadDialog) and the
|
||||
// node updater (DaemonUpdateDialog). Renders a scrollable list of releases (tag, title, date, with
|
||||
// pre-release / installed badges) inside the current overlay dialog; the caller maps its updater's
|
||||
// release list into ReleaseRow values and acts on the clicked index.
|
||||
// Shared release-row model for the updater dialogs. Both the miner updater (XmrigDownloadDialog) and
|
||||
// the node updater (DaemonUpdateDialog) map their updater's release list into ReleaseRow values to
|
||||
// drive their two-pane version picker (tag + title + date, with pre-release / installed badges). The
|
||||
// two-pane list is the "browse all releases" UI; each dialog renders its own tactile Material cards.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../../util/i18n.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
@@ -30,58 +23,5 @@ struct ReleaseRow {
|
||||
bool installed = false; // matches the currently-installed version (Install -> Reinstall)
|
||||
};
|
||||
|
||||
// Renders the picker. Returns the row index whose Install button was clicked this frame, or -1.
|
||||
// Sets *back when the Back button is clicked. If `globalDisabledTooltip` is non-null, every Install
|
||||
// button is disabled and shows that tooltip (e.g. "stop mining before updating the miner").
|
||||
inline int RenderReleaseList(const std::vector<ReleaseRow>& rows, float dp, bool* back,
|
||||
const char* globalDisabledTooltip = nullptr) {
|
||||
using namespace material;
|
||||
int clicked = -1;
|
||||
|
||||
Type().text(TypeStyle::Subtitle2, TR("upd_select_version"));
|
||||
ImGui::Spacing();
|
||||
|
||||
const float listH = 300.0f * dp;
|
||||
if (ImGui::BeginChild("##release_list", ImVec2(0, listH), true)) {
|
||||
const ImVec4 dim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
|
||||
const ImVec4 warn = ImVec4(1.0f, 0.78f, 0.25f, 1.0f);
|
||||
const ImVec4 succ = ImGui::ColorConvertU32ToFloat4(Success());
|
||||
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
|
||||
const ReleaseRow& r = rows[i];
|
||||
ImGui::PushID(i);
|
||||
|
||||
ImGui::TextUnformatted(r.tag.c_str());
|
||||
if (r.prerelease) { ImGui::SameLine(); ImGui::TextColored(warn, "[%s]", TR("upd_prerelease")); }
|
||||
if (r.installed) { ImGui::SameLine(); ImGui::TextColored(succ, "[%s]", TR("upd_installed_badge")); }
|
||||
|
||||
if (!r.title.empty() || !r.date.empty()) {
|
||||
std::string meta = r.title;
|
||||
if (!r.title.empty() && !r.date.empty()) meta += " · ";
|
||||
meta += r.date;
|
||||
ImGui::TextColored(dim, "%s", meta.c_str());
|
||||
}
|
||||
|
||||
const char* lbl = r.installed ? TR("upd_reinstall") : TR("upd_install");
|
||||
const bool disabled = !r.hasAsset || globalDisabledTooltip != nullptr;
|
||||
ImGui::BeginDisabled(disabled);
|
||||
if (TactileButton(lbl, ImVec2(140.0f * dp, 0))) clicked = i;
|
||||
ImGui::EndDisabled();
|
||||
if (disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
material::Tooltip("%s", globalDisabledTooltip ? globalDisabledTooltip
|
||||
: TR("upd_no_build_platform"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("upd_back"), ImVec2(ImGui::GetContentRegionAvail().x, 0))) *back = true;
|
||||
return clicked;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -371,7 +371,7 @@ public:
|
||||
dl->AddText(metaFont, metaFont->LegacySize, ImVec2(cp.x + 8.0f * dp, cp.y + 3.0f * dp), Success(), al);
|
||||
} else {
|
||||
ImGui::SetCursorScreenPos(ImVec2(btnX, midY - btnH * 0.5f));
|
||||
if (StyledButton(TR("wallets_open"), ImVec2(btnW, btnH))) {
|
||||
if (TactileButton(TR("wallets_open"), ImVec2(btnW, btnH))) {
|
||||
if (r.inDatadir) { app->switchToWallet(r.fileName); s_open = false; }
|
||||
else { openInPlace(app, r); }
|
||||
}
|
||||
@@ -419,7 +419,7 @@ public:
|
||||
ImGui::SetNextItemWidth(listW - createBtnW - style.ItemSpacing.x);
|
||||
ImGui::InputTextWithHint("##newWalletName", TR("wallets_new_hint"), s_newName, sizeof(s_newName));
|
||||
ImGui::SameLine();
|
||||
if (StyledButton(TR("wallets_create"), ImVec2(createBtnW, 0))) {
|
||||
if (TactileButton(TR("wallets_create"), ImVec2(createBtnW, 0))) {
|
||||
std::string name = normalizeWalletName(s_newName);
|
||||
std::error_code ec;
|
||||
if (name.empty() || isLinkName(name)) { // reserve the in-place-link prefix
|
||||
@@ -439,7 +439,7 @@ public:
|
||||
// ---- Scan another folder — always visible, full-width, opens the in-app folder picker -
|
||||
// Start in the DRAGONX data directory (where the datadir wallets live) so the user can
|
||||
// navigate up/out from a familiar anchor to find wallets in other folders.
|
||||
if (StyledButton(TR("wallets_scan_folder"), ImVec2(listW, 0))) {
|
||||
if (TactileButton(TR("wallets_scan_folder"), ImVec2(listW, 0))) {
|
||||
FolderPicker::open(util::Platform::getDragonXDataDir(), [app](const std::string& dir) {
|
||||
std::error_code ec;
|
||||
if (!dir.empty() && std::filesystem::is_directory(dir, ec)) {
|
||||
@@ -497,7 +497,6 @@ public:
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::Separator();
|
||||
// ---- Refresh: icon-only, de-emphasized next to the text actions -----------------------
|
||||
{
|
||||
float bh = ImGui::GetFrameHeight();
|
||||
@@ -510,10 +509,10 @@ public:
|
||||
s_needScan = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (StyledButton(TR("wallets_reveal"), ImVec2(150.0f * dp, 0)))
|
||||
if (TactileButton(TR("wallets_reveal"), ImVec2(150.0f * dp, 0)))
|
||||
util::Platform::openFolder(util::Platform::getDragonXDataDir());
|
||||
ImGui::SameLine();
|
||||
if (StyledButton(TR("close"), ImVec2(110.0f * dp, 0))) s_open = false;
|
||||
if (TactileButton(TR("close"), ImVec2(110.0f * dp, 0))) s_open = false;
|
||||
|
||||
EndOverlayDialog();
|
||||
}
|
||||
|
||||
@@ -428,7 +428,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["settings_shield_mining"] = "Shield Mining...";
|
||||
strings_["settings_merge_to_address"] = "Merge to Address...";
|
||||
strings_["settings_clear_ztx"] = "Clear Z-Tx History";
|
||||
strings_["settings_import_key"] = "Import Key...";
|
||||
strings_["settings_import_key"] = "Import Private Key...";
|
||||
strings_["settings_import_viewkey"] = "Import Viewing Key...";
|
||||
strings_["settings_export_key"] = "Export Key...";
|
||||
strings_["settings_export_all"] = "Export All...";
|
||||
strings_["settings_backup"] = "Backup...";
|
||||
@@ -507,6 +508,43 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["wiz_encrypt_desc"] = "Encrypt your wallet to protect private keys with a passphrase.";
|
||||
strings_["wiz_encrypt_warning"] = "If you lose your passphrase, you lose access to your funds.";
|
||||
strings_["wiz_passphrase"] = "Passphrase:";
|
||||
// --- Encrypt / change-passphrase dialogs (settings) ---
|
||||
strings_["enc_desc"] = "Encrypting your wallet protects your private keys with a passphrase. After encryption, the daemon will restart.";
|
||||
strings_["enc_confirm"] = "Confirm:";
|
||||
strings_["enc_encrypting"] = "Encrypting wallet...";
|
||||
strings_["enc_wait"] = "Please wait, do not close the application.";
|
||||
strings_["enc_success"] = "Wallet encrypted successfully!";
|
||||
strings_["enc_pin_desc"] = "A 4-8 digit PIN lets you unlock your wallet without typing the full passphrase every time.";
|
||||
strings_["enc_pin_set_ok"] = "PIN set successfully";
|
||||
strings_["enc_pin_vault_fail"] = "Failed to create PIN vault";
|
||||
strings_["enc_pin_skipped"] = "PIN skipped. You can set one later in Settings.";
|
||||
strings_["change_pass_title"] = "Change Passphrase";
|
||||
strings_["change_pass_current"] = "Current Passphrase:";
|
||||
strings_["change_pass_new"] = "New Passphrase:";
|
||||
strings_["change_pass_confirm"] = "Confirm New:";
|
||||
// --- Remove-encryption (decrypt) dialog (settings) ---
|
||||
strings_["decrypt_title"] = "Remove Wallet Encryption";
|
||||
strings_["decrypt_warning"] = "This will remove encryption from your wallet. Your private keys will be stored unprotected on disk.";
|
||||
strings_["decrypt_desc"] = "The wallet will be exported, the daemon restarted with a fresh unencrypted wallet, and all keys re-imported. This may take several minutes depending on wallet size.";
|
||||
strings_["decrypt_step_unlock"] = "Unlocking wallet";
|
||||
strings_["decrypt_step_export"] = "Exporting wallet keys";
|
||||
strings_["decrypt_step_stop"] = "Stopping daemon";
|
||||
strings_["decrypt_step_backup"] = "Backing up encrypted wallet";
|
||||
strings_["decrypt_step_restart"] = "Restarting daemon";
|
||||
strings_["decrypt_wait_restart"] = "Waiting for the daemon to finish starting up...";
|
||||
strings_["decrypt_wait_general"] = "Please wait. The daemon is exporting keys, restarting, and re-importing. This may take several minutes.";
|
||||
strings_["decrypt_success_title"] = "Wallet decrypted successfully!";
|
||||
strings_["decrypt_success_desc"] = "Your wallet is now unencrypted. A backup of the encrypted wallet was saved as wallet.dat.encrypted.bak in your data directory.";
|
||||
strings_["decrypt_error_title"] = "Decryption failed";
|
||||
strings_["try_again"] = "Try Again";
|
||||
// --- PIN setup / change / remove dialogs (settings) ---
|
||||
strings_["pin_setup_desc"] = "Set a 4-8 digit PIN for quick wallet unlock. Your wallet passphrase will be encrypted with this PIN and stored locally.";
|
||||
strings_["pin_wallet_passphrase"] = "Wallet Passphrase:";
|
||||
strings_["pin_new_label"] = "New PIN (4-8 digits):";
|
||||
strings_["pin_change_desc"] = "Change your unlock PIN. You need your current PIN and a new PIN.";
|
||||
strings_["pin_current_label"] = "Current PIN:";
|
||||
strings_["pin_confirm_new_label"] = "Confirm New PIN:";
|
||||
strings_["pin_remove_desc"] = "Enter your current PIN to confirm removal. You will need to use your full passphrase to unlock.";
|
||||
strings_["wiz_confirm"] = "Confirm:";
|
||||
strings_["wiz_strength_weak"] = "Weak";
|
||||
strings_["wiz_strength_fair"] = "Fair";
|
||||
@@ -731,6 +769,7 @@ void I18n::loadBuiltinEnglish()
|
||||
|
||||
// Settings: additional tooltips (keys/data row)
|
||||
strings_["tt_import_key"] = "Import a private key (zkey or tkey) into this wallet";
|
||||
strings_["tt_import_viewkey"] = "Import a shielded viewing key to watch an address (read-only)";
|
||||
strings_["tt_export_key"] = "Export the private key for the selected address";
|
||||
strings_["tt_export_all"] = "Export all private keys to a file";
|
||||
strings_["tt_backup"] = "Create a backup of your wallet.dat file";
|
||||
@@ -1200,6 +1239,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["backup_created"] = "Wallet backup created";
|
||||
strings_["backup_description"] = "Create a backup of your wallet.dat file. This file contains all your private keys and transaction history. Store the backup in a secure location.";
|
||||
strings_["backup_destination"] = "Backup destination:";
|
||||
strings_["backup_warn"] = "This file holds all your private keys \xE2\x80\x94 keep it somewhere safe.";
|
||||
strings_["backup_overwrite_confirm"] = "A file already exists there \xE2\x80\x94 Save again to overwrite it.";
|
||||
strings_["backup_tip_external"] = "Store backups on external drives or cloud storage";
|
||||
strings_["backup_tip_multiple"] = "Create multiple backups in different locations";
|
||||
strings_["backup_tip_test"] = "Test restoring from backup periodically";
|
||||
@@ -1290,6 +1331,31 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["console_welcome"] = "Welcome to ObsidianDragon Console";
|
||||
strings_["console_zoom_in"] = "Zoom in";
|
||||
strings_["console_zoom_out"] = "Zoom out";
|
||||
strings_["console_toggle_accents"] = "Toggle line color accents";
|
||||
strings_["console_toggle_text_color"] = "Toggle line text colors";
|
||||
strings_["console_accents"] = "Color accents";
|
||||
strings_["console_text_colors"] = "Text colors";
|
||||
strings_["console_cat_control"] = "Control";
|
||||
strings_["console_cat_network"] = "Network";
|
||||
strings_["console_cat_blockchain"] = "Blockchain";
|
||||
strings_["console_cat_mining"] = "Mining";
|
||||
strings_["console_cat_wallet"] = "Wallet";
|
||||
strings_["console_cat_raw_transactions"] = "Raw Transactions";
|
||||
strings_["console_cat_utility"] = "Utility";
|
||||
strings_["console_ref_search_hint"] = "Search by name or task\xE2\x80\xA6";
|
||||
strings_["console_ref_parameters"] = "Parameters";
|
||||
strings_["console_ref_no_params"] = "Takes no parameters.";
|
||||
strings_["console_ref_optional"] = "optional";
|
||||
strings_["console_ref_example"] = "Example";
|
||||
strings_["console_ref_builds"] = "Builds";
|
||||
strings_["console_ref_destructive"] = "Consequential";
|
||||
strings_["console_ref_run_confirm"] = "Run %s now? This is a consequential command.";
|
||||
strings_["console_ref_cancel"] = "Cancel";
|
||||
strings_["console_ref_run"] = "Run";
|
||||
strings_["console_ref_insert"] = "Insert into console";
|
||||
strings_["console_ref_insert_run"] = "Insert & run";
|
||||
strings_["console_ref_select_hint"] = "Select a command to see what it does.";
|
||||
strings_["console_ref_no_match"] = "No commands match.";
|
||||
|
||||
// --- Export All Keys Dialog ---
|
||||
strings_["export_keys_btn"] = "Export Keys";
|
||||
@@ -1298,6 +1364,14 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["export_keys_include_z"] = "Include Z-addresses (shielded)";
|
||||
strings_["export_keys_options"] = "Export options:";
|
||||
strings_["export_keys_success"] = "Keys exported successfully";
|
||||
strings_["export_keys_select_type"] = "Select at least one address type";
|
||||
strings_["export_keys_not_connected"] = "Not connected to the daemon";
|
||||
strings_["export_keys_none_addrs"] = "No addresses to export";
|
||||
strings_["export_keys_none_result"] = "No keys exported (0 of %d) \xE2\x80\x94 unlock the wallet and try again.";
|
||||
strings_["export_keys_none_toast"] = "No keys could be exported \xE2\x80\x94 is the wallet unlocked?";
|
||||
strings_["export_keys_write_fail"] = "Failed to write the key file.";
|
||||
strings_["export_keys_partial"] = "Exported %d of %d keys \xE2\x80\x94 incomplete (some had no spending key, or the wallet is locked).";
|
||||
strings_["export_keys_partial_toast"] = "Partial export: %d of %d keys";
|
||||
strings_["export_keys_title"] = "Export All Private Keys";
|
||||
|
||||
// --- Export Transactions Dialog ---
|
||||
@@ -1321,9 +1395,42 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["import_key_tooltip"] = "Enter one or more private keys, one per line.\nSupports both z-address and t-address keys.\nLines starting with # are treated as comments.";
|
||||
strings_["import_key_warning"] = "Warning: Never share your private keys! Importing keys from untrusted sources can compromise your wallet.";
|
||||
strings_["import_key_z_format"] = "Z-address spending keys (secret-extended-key-...)";
|
||||
strings_["import_key_warn"] = "Only import a key you own \xE2\x80\x94 it grants access to its funds.";
|
||||
strings_["import_key_need_node"] = "Connect a running node to import a key.";
|
||||
strings_["import_key_field"] = "Key";
|
||||
strings_["import_key_reveal_tip"] = "Show/hide the key";
|
||||
strings_["import_key_type_tkey"] = "Transparent private key";
|
||||
strings_["import_key_type_zspend"] = "Shielded spending key";
|
||||
strings_["import_key_type_zview"] = "Shielded viewing key (watch-only)";
|
||||
strings_["import_key_type_unknown"] = "Unrecognized key format";
|
||||
strings_["import_key_rescanning"] = "Importing & rescanning \xE2\x80\x94 this can take several minutes";
|
||||
strings_["import_key_done"] = "Imported. Wallet is rescanning.";
|
||||
strings_["import_key_address"] = "Address:";
|
||||
strings_["import_key_import"] = "Import";
|
||||
// Viewing-key (watch-only) import — separate button + dialog mode.
|
||||
strings_["import_viewkey_title"] = "Import Viewing Key";
|
||||
strings_["import_viewkey_note"] = "Watch-only: a viewing key reveals an address's balance and transactions but cannot spend its funds.";
|
||||
strings_["import_viewkey_field"] = "Viewing key";
|
||||
strings_["import_key_wrong_type"] = "This looks like a viewing key. Use \"Import Viewing Key\" instead.";
|
||||
strings_["import_viewkey_wrong_type"] = "This looks like a spending key. Use \"Import Private Key\" instead.";
|
||||
strings_["import_scan_label"] = "Scan from block height (optional)";
|
||||
strings_["import_scan_hint"] = "0 = rescan from the start";
|
||||
strings_["import_scan_transparent"] = "Transparent keys always rescan fully";
|
||||
strings_["import_scan_tip"] = "current height";
|
||||
strings_["paste_clip_empty"] = "Clipboard is empty";
|
||||
// Sweep (import a spending key, then move all its funds to your own address).
|
||||
strings_["sweep_toggle"] = "Sweep to my wallet (don't keep the key)";
|
||||
strings_["sweep_caveat"] = "Imports the key to sign one transaction moving all its funds to your address. The key stays in your wallet with an empty balance.";
|
||||
strings_["sweep_dest_label"] = "Send swept funds to";
|
||||
strings_["sweep_dest_new"] = "New shielded address (recommended)";
|
||||
strings_["sweep_button"] = "Sweep";
|
||||
strings_["sweep_done"] = "Done \xE2\x80\x94 funds swept to your address.";
|
||||
strings_["sweep_to"] = "Swept to:";
|
||||
strings_["sweep_tx"] = "Transaction:";
|
||||
|
||||
// --- Key Export Dialog ---
|
||||
strings_["key_export_fetching"] = "Fetching key from wallet...";
|
||||
strings_["key_export_failed"] = "Couldn't export the key \xE2\x80\x94 unlock the wallet (if encrypted) and try again.";
|
||||
strings_["key_export_private_key"] = "Private Key:";
|
||||
strings_["key_export_private_warning"] = "Keep this key SECRET! Anyone with this key can spend your funds. Never share it online or with untrusted parties.";
|
||||
strings_["key_export_reveal"] = "Reveal Key";
|
||||
@@ -1405,6 +1512,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["portfolio_cancel"] = "Cancel";
|
||||
strings_["portfolio_revert"] = "Revert";
|
||||
strings_["portfolio_close"] = "Close";
|
||||
strings_["portfolio_save_need_name"] = "Enter a name to save this group.";
|
||||
strings_["portfolio_save_need_address"] = "Add at least one address to save.";
|
||||
strings_["portfolio_save_need_price"] = "Enter a manual price above 0 to save.";
|
||||
strings_["portfolio_untitled"] = "Untitled";
|
||||
strings_["portfolio_detail_empty"] = "Select a group on the left, or add one, to edit it.";
|
||||
strings_["portfolio_add_to"] = "Add to portfolio";
|
||||
|
||||
@@ -3200,6 +3200,23 @@ void testConsoleInputModel()
|
||||
EXPECT_TRUE(call.params[0].get<bool>());
|
||||
EXPECT_EQ(call.params[1].get<long long>(), 4LL);
|
||||
|
||||
// Quoted numeric-looking args must stay strings (not coerced to numbers).
|
||||
auto qcall = dragonx::ui::BuildConsoleRpcCall("setaccount \"123\" \"456.7\"");
|
||||
EXPECT_TRUE(qcall.valid);
|
||||
EXPECT_EQ(qcall.params.size(), static_cast<size_t>(2));
|
||||
EXPECT_TRUE(qcall.params[0].is_string());
|
||||
EXPECT_EQ(qcall.params[0].get<std::string>(), std::string("123"));
|
||||
EXPECT_TRUE(qcall.params[1].is_string());
|
||||
// Bare finite numbers become numbers; non-finite / non-whole-token tokens fall back to strings
|
||||
// (no std::stoll truncation of "1e999" to 1, no inf).
|
||||
auto ncall = dragonx::ui::BuildConsoleRpcCall("foo 42 3.5 1e999 123abc");
|
||||
EXPECT_TRUE(ncall.params[0].is_number_integer());
|
||||
EXPECT_EQ(ncall.params[0].get<long long>(), 42LL);
|
||||
EXPECT_TRUE(ncall.params[1].is_number_float());
|
||||
EXPECT_TRUE(ncall.params[2].is_string());
|
||||
EXPECT_EQ(ncall.params[2].get<std::string>(), std::string("1e999"));
|
||||
EXPECT_TRUE(ncall.params[3].is_string());
|
||||
|
||||
auto resultLines = dragonx::ui::FormatConsoleRpcResultLines("{\n \"balance\": 12,\n \"ok\": true\n}", false);
|
||||
EXPECT_EQ(resultLines.size(), static_cast<size_t>(4));
|
||||
EXPECT_EQ(resultLines[0].role, dragonx::ui::ConsoleResultLineRole::JsonBrace);
|
||||
|
||||
Reference in New Issue
Block a user