Compare commits
40 Commits
e66f2b6c8c
...
3a9edf2200
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a9edf2200 | |||
| c25a4e4150 | |||
| 54945c94bf | |||
| 96a1c54556 | |||
| bedb482855 | |||
| c352364bdc | |||
| 6b7438b529 | |||
| c6624913de | |||
| e6bea8eb04 | |||
| 5e5f167fb0 | |||
| 1fd794ccc1 | |||
| ed4d61c364 | |||
| 3df516be70 | |||
| f8fbc32463 | |||
| 06c8b99bce | |||
| 5eff3adc3c | |||
| e010822a74 | |||
| b2882095bb | |||
| a80feb708e | |||
| 010465f835 | |||
| eb08b81706 | |||
| 8cbd0cab9d | |||
| a9f0d544cc | |||
| 13ff20d791 | |||
| d8cc1e9ee6 | |||
| e7a3f90102 | |||
| b70f1ae643 | |||
| 76a2ef51f3 | |||
| 8846a96e3d | |||
| 5db7941fbc | |||
| a09adb14fb | |||
| d23ee9b75f | |||
| e8a8ce68b2 | |||
| 72bf59149d | |||
| 82e61da62f | |||
| 48c79567d7 | |||
| 6c19fac6f5 | |||
| 0d9908019d | |||
| a86cb8f0f3 | |||
| 5228da707f |
@@ -89,6 +89,14 @@ Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…
|
||||
|
||||
**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`).
|
||||
|
||||
## Seed phrase & migrate-to-seed (full node)
|
||||
|
||||
Full-node wallets are **BIP39-mnemonic-backed** and can **migrate a legacy (non-mnemonic) wallet into a seed wallet**. All of this **requires the `hd-transparent-keys`/`dev` daemon** (`z_exportmnemonic` + `-usemnemonic`); the older bundled binary lacks those RPCs, so these features degrade gracefully (chat falls back to a `z_exportkey` identity; the backup screen shows a "legacy wallet" note). The daemon source is vendored at `external/dragonx/` (build with its own `./build.sh`); deploy the built `dragonxd`/`dragonx-cli`/`dragonx-tx` into the wallet's daemon dir (`build/*/bin`) or via the daemon updater.
|
||||
|
||||
- **New wallets get a phrase.** `EmbeddedDaemon::getChainParams()` always passes `-usemnemonic=1`. The daemon reads it **only inside `GenerateNewSeed()` when a wallet has no seed yet**, so it is inert on existing wallets (safe to pass unconditionally) and makes every fresh wallet mnemonic-backed.
|
||||
- **Back up seed phrase.** Settings → Backup & Data → "Seed phrase" opens `renderSeedBackupDialog` (`App::exportSeedPhrase` → `z_exportmnemonic`, wiped via `sodium_memzero`). A one-time nudge (`maybeRemindSeedBackup`, settings flag `seed_backup_reminded`) reminds mnemonic-wallet users to back up.
|
||||
- **Migrate-to-seed** (`showSeedMigrationDialog` / `renderSeedMigrationDialog`, state machine `SeedMigrationStep`). **Phase 1 — create:** `daemon::SeedWalletCreator` runs a **second, isolated `dragonxd`** on its own port + throwaway datadir (`<config>/seed-migrate/DRAGONX`, basename MUST be the acname; `-usemnemonic=1 -connect=0`; RPC is plaintext http, not TLS), mints a mnemonic wallet, exports its seed + a sweep-target z-address, then stops. Uses the one-shot `EmbeddedDaemon::setNextStartOverride` + `setSkipPortCheck`. **Phase 2 — sweep + adopt:** `z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"]` sweeps all funds to the new address; a **Confirming gate** (`pollSweepStatus`) only allows adopt once the sweep tx is **mined (≥1 conf) AND the legacy balance is ~0** (offering a remainder re-sweep); adopt (`beginAdoptSeedWallet`, background) stops the daemon, moves `wallet.dat` aside to a **timestamped `.bak` (never deleted, restored on failure)**, installs the new wallet, and restarts with `-rescan`. Fund-moving code — **two rounds of adversarial review + a live mainnet run** gate it; the pending stage persists (`seed_migration_*` settings) so a restart resumes at sweep/confirm.
|
||||
|
||||
## Versioning
|
||||
|
||||
The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` plus `DRAGONX_VERSION_SUFFIX` in `CMakeLists.txt`. CMake generates `build/.../generated/dragonx_generated_version.h` from `src/config/version.h.in`. Do not hand-edit generated version output or hardcode version strings — bump the `project()` version in `CMakeLists.txt`.
|
||||
@@ -98,5 +106,6 @@ The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` p
|
||||
- **C++17.** Match the surrounding code's style per file.
|
||||
- **Icons:** use the Material Design icon font defines (`ICON_MD_*`); never raw Unicode glyphs.
|
||||
- **UI layout values** belong in `res/themes/ui.toml`, read via `schema::UI()` — do not hardcode pixel sizes/offsets in code.
|
||||
- **DPI / display scaling:** `schema::UI()` returns **logical (raw) px**; `Layout::dpiScale()` (= OS DPI × the in-app font-scale) is the single scale factor. The `Layout::k*()` helpers and `BeginOverlayDialog` already fold it in, and ImGui auto-layout scales via the DPI-rebuilt font atlas + `ScaleAllSizes` — so tabs/standard widgets scale for free. But **hand-drawn absolute geometry** (`dl->AddText` at manual `cy += 24.0f` offsets, `SetNextWindowSize`, explicit `ImVec2(width,0)` button sizes, `SameLine(x)` column strides) is immune to all of that and must be multiplied by `ui::Layout::dpiScale()` yourself, or it renders native-size (tiny) on a HiDPI display. Font metrics (`font->LegacySize`, `CalcTextSize`) are already scaled — don't double-scale those. Verify at scale with a full sweep run at `font_scale: 1.5` (same `dpiScale()==1.5` code path as OS 150%).
|
||||
- **i18n:** user-facing strings are translated via `src/util/i18n`; the English source of truth is the `strings_[...]` map in `src/util/i18n.cpp`, and the per-language translations live as the **source of truth** in `res/lang/` (`de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, `zh`). **Edit those JSONs directly and additively** — write with `json.dump(..., indent=4, sort_keys=True, ensure_ascii=False)`; never bulk-regenerate/overwrite a whole file (that path silently dropped ~285 keys/language before). `scripts/add_missing_translations.py` back-fills only the keys missing from a JSON (non-destructive), and `scripts/build_cjk_subset.py` rebuilds the CJK subset font (`res/fonts/NotoSansCJK-Subset.ttf`) after new CJK glyphs are added.
|
||||
- **Commits:** the history uses Conventional Commits (`feat(scope): …`, `fix(scope): …`). PRs target `master`.
|
||||
|
||||
@@ -404,6 +404,7 @@ set(APP_SOURCES
|
||||
src/app.cpp
|
||||
src/app_network.cpp
|
||||
src/app_security.cpp
|
||||
src/app_sweep.cpp
|
||||
src/app_wizard.cpp
|
||||
src/services/network_refresh_service.cpp
|
||||
src/services/refresh_scheduler.cpp
|
||||
@@ -489,6 +490,7 @@ set(APP_SOURCES
|
||||
src/rpc/connection.cpp
|
||||
src/config/settings.cpp
|
||||
src/data/address_book.cpp
|
||||
src/data/wallet_index.cpp
|
||||
src/data/exchange_info.cpp
|
||||
src/util/logger.cpp
|
||||
src/util/async_task_manager.cpp
|
||||
@@ -1061,6 +1063,8 @@ if(BUILD_TESTING)
|
||||
src/util/text_format.cpp
|
||||
src/data/wallet_state.cpp
|
||||
src/data/transaction_history_cache.cpp
|
||||
src/data/address_book.cpp
|
||||
src/data/wallet_index.cpp
|
||||
src/daemon/lifecycle_adapters.cpp
|
||||
src/rpc/connection.cpp
|
||||
src/config/settings.cpp
|
||||
|
||||
Binary file not shown.
412
res/lang/de.json
412
res/lang/de.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "Adresse hinzufügen",
|
||||
"address_book_add_new": "Neue hinzufügen",
|
||||
"address_book_added": "Adresse zum Buch hinzugefügt",
|
||||
"address_book_confirm_delete": "Löschen bestätigen?",
|
||||
"address_book_count": "%zu Adressen gespeichert",
|
||||
"address_book_deleted": "Eintrag gelöscht",
|
||||
"address_book_edit": "Adresse bearbeiten",
|
||||
@@ -39,6 +40,7 @@
|
||||
"address_copied": "Adresse in Zwischenablage kopiert",
|
||||
"address_details": "Adressdetails",
|
||||
"address_label": "Adresse:",
|
||||
"address_reorder_hint": "Am Rand einer Zeile ablegen zum Umsortieren, oder in deren Mitte zum Übertragen",
|
||||
"address_upper": "ADRESSE",
|
||||
"address_url": "Adress-URL",
|
||||
"addresses_appear_here": "Ihre Empfangsadressen erscheinen hier, sobald Sie verbunden sind.",
|
||||
@@ -69,7 +71,10 @@
|
||||
"backup_wallet": "Wallet sichern...",
|
||||
"backup_wallet_not_found": "Warnung: wallet.dat nicht am erwarteten Speicherort gefunden",
|
||||
"balance": "Guthaben",
|
||||
"balance_history_collecting": "Guthabenverlauf — Daten werden gesammelt...",
|
||||
"balance_layout": "Guthaben-Layout",
|
||||
"balance_shielded_fmt": "Abgeschirmt: %.8f",
|
||||
"balance_transparent_fmt": "Transparent: %.8f",
|
||||
"ban": "Sperren",
|
||||
"banned_peers": "Gesperrte Peers",
|
||||
"block": "Block",
|
||||
@@ -151,6 +156,20 @@
|
||||
"confirm_delete_blockchain_msg": "Dies stoppt den Daemon, löscht alle Blockchain-Daten (blocks, chainstate, peers) und startet eine neue Synchronisierung. Dies kann mehrere Stunden dauern.",
|
||||
"confirm_delete_blockchain_safe": "Ihre wallet.dat, Konfiguration und Transaktionshistorie sind sicher und werden nicht gelöscht.",
|
||||
"confirm_delete_blockchain_title": "Blockchain-Daten löschen",
|
||||
"confirm_lite_redownload_msg": "Dies löscht die von der Wallet heruntergeladenen Blockdaten und lädt jeden Block erneut vom Lite-Server herunter und scannt ihn neu. Dies kann eine Weile dauern; die Wallet zeigt den Synchronisierungsfortschritt bis zum Abschluss an.",
|
||||
"confirm_lite_redownload_safe": "Ihre Wallet, Ihre Schlüssel und Ihr Seed sind nicht betroffen — nur die Blockdaten werden erneut heruntergeladen.",
|
||||
"confirm_lite_redownload_title": "Blöcke erneut herunterladen",
|
||||
"confirm_reinstall_daemon_msg": "Dies stoppt den Daemon, überschreibt den installierten dragonxd (sowie dragonx-cli/dragonx-tx) mit den in diesem Wallet-Build gebündelten Versionen und startet den Node anschließend neu. Verwenden Sie dies, um die Node-Programmdatei wiederherzustellen oder zu aktualisieren.",
|
||||
"confirm_reinstall_daemon_safe": "Ihre Wallet, Ihre Schlüssel und Ihre Blockchain-Daten bleiben unberührt — nur die Programmdateien des Daemons werden ersetzt.",
|
||||
"confirm_reinstall_daemon_title": "Gebündelten Daemon installieren",
|
||||
"confirm_repair_wallet_msg": "Dies startet den Daemon mit -zapwallettxes=2 neu: Alle Transaktions- und Notiz-Einträge der Wallet werden gelöscht und anschließend aus der Blockchain neu aufgebaut. Verwenden Sie dies, wenn Transaktionen nicht erstellt werden können („Invalid sapling spend proof“ / „shielded requirements not met“), selbst nach einem vollständigen Neuscan. Es dauert lange und die Wallet bleibt offline, bis es abgeschlossen ist.",
|
||||
"confirm_repair_wallet_safe": "Ihre Schlüssel, Adressen und Ihr Guthaben bleiben erhalten — nur die zwischengespeicherten Transaktionsdatensätze werden neu aufgebaut.",
|
||||
"confirm_repair_wallet_title": "Wallet reparieren",
|
||||
"confirm_rescan_msg": "Dies startet den Daemon neu und scannt die gesamte Blockchain erneut nach den Transaktionen Ihrer Wallet. Das kann lange dauern, und die Wallet bleibt offline, bis der Vorgang abgeschlossen ist.",
|
||||
"confirm_rescan_safe": "Ihre wallet.dat und Blockchain-Daten werden nicht gelöscht — nur neu gescannt.",
|
||||
"confirm_rescan_title": "Blockchain neu scannen",
|
||||
"confirm_restart_daemon_msg": "Dadurch wird der Daemon gestoppt und neu gestartet, um die geänderten Optionen anzuwenden. Die Wallet trennt die Verbindung kurz und stellt sie wieder her.",
|
||||
"confirm_restart_daemon_title": "Daemon neu starten",
|
||||
"confirm_send": "Senden bestätigen",
|
||||
"confirm_transaction": "Transaktion bestätigen",
|
||||
"confirm_transfer": "Überweisung bestätigen",
|
||||
@@ -161,6 +180,7 @@
|
||||
"connected_peers": "Verbundene Peers",
|
||||
"connecting": "Verbinde...",
|
||||
"console": "Konsole",
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Automatisch scrollen",
|
||||
"console_available_commands": "Verfügbare Befehle:",
|
||||
"console_capturing_output": "Erfasse Daemon-Ausgabe...",
|
||||
@@ -177,6 +197,7 @@
|
||||
"console_connected": "Verbunden mit Daemon",
|
||||
"console_copy_all": "Alles kopieren",
|
||||
"console_copy_selected": "Kopieren",
|
||||
"console_copy_value": "Kopieren",
|
||||
"console_daemon": "Daemon",
|
||||
"console_daemon_error": "Daemon-Fehler!",
|
||||
"console_daemon_started": "Daemon gestartet",
|
||||
@@ -195,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - Mining steuern",
|
||||
"console_help_stop": " stop - Daemon stoppen",
|
||||
"console_line_count": "%zu Zeilen",
|
||||
"console_matches": "Treffer",
|
||||
"console_new_lines": "%d neue Zeilen",
|
||||
"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_rpc_reference": "RPC-Befehlsreferenz",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Konsolen-Scanline",
|
||||
"console_search_commands": "Befehle suchen...",
|
||||
"console_select_all": "Alles auswählen",
|
||||
"console_show_app_output": "[App]-Wallet-Protokollzeilen anzeigen",
|
||||
"console_show_daemon_output": "Daemon-Ausgabe anzeigen",
|
||||
"console_show_errors_only": "Nur Fehler anzeigen",
|
||||
"console_show_rpc_ref": "RPC-Befehlsreferenz anzeigen",
|
||||
"console_show_rpc_trace": "App-RPC-Aufrufe anzeigen",
|
||||
"console_showing_lines": "Zeige %zu von %zu Zeilen",
|
||||
"console_starting_node": "Knoten wird gestartet...",
|
||||
"console_status_error": "Fehler",
|
||||
@@ -218,6 +244,12 @@
|
||||
"console_welcome": "Willkommen bei ObsidianDragon Konsole",
|
||||
"console_zoom_in": "Vergrößern",
|
||||
"console_zoom_out": "Verkleinern",
|
||||
"contact_global": "In jeder Wallet anzeigen (globaler Kontakt)",
|
||||
"contact_global_badge_tt": "Globaler Kontakt — in jeder Wallet sichtbar",
|
||||
"contact_global_tt": "Ein: Dieser Kontakt bleibt sichtbar, egal welche Wallet Sie laden. Aus: Er gehört nur zur aktuellen Wallet.",
|
||||
"contacts": "Kontakte",
|
||||
"contacts_search_no_match": "Keine passenden Kontakte",
|
||||
"contacts_search_placeholder": "Kontakte durchsuchen...",
|
||||
"copied": "Kopiert!",
|
||||
"copy": "Kopieren",
|
||||
"copy_address": "Vollständige Adresse kopieren",
|
||||
@@ -227,6 +259,44 @@
|
||||
"copy_uri": "URI kopieren",
|
||||
"current_price": "Aktueller Preis",
|
||||
"custom_fees": "Benutzerdefinierte Gebühren",
|
||||
"daemon_binary": "Daemon-Binärdatei",
|
||||
"daemon_bundled": "Gebündelt",
|
||||
"daemon_install_bundled": "Gebündelten installieren",
|
||||
"daemon_installed": "Installiert",
|
||||
"daemon_none_bundled": "keiner in diesem Build",
|
||||
"daemon_not_installed": "nicht installiert",
|
||||
"daemon_status_differ": "Installierte Binärdatei unterscheidet sich von der gebündelten Version.",
|
||||
"daemon_status_match": "Installierte Binärdatei entspricht der gebündelten Version.",
|
||||
"daemon_status_missing": "Kein Daemon installiert — installieren Sie die mitgelieferte Version.",
|
||||
"daemon_update_available": "Eine neue Node-Version ist verfügbar",
|
||||
"daemon_update_body": "Dieser Wallet-Build enthält einen neueren DragonX-Node als den derzeit installierten. Beim Aktualisieren wird der installierte dragonxd (sowie dragonx-cli/dragonx-tx) ersetzt und der Node anschließend gestoppt und neu gestartet, damit die neue Version wirksam wird. Empfohlen — ein neuerer Node kann Funktionen (z. B. Unterstützung für Wiederherstellungsphrasen) bieten, die der alte nicht hat.",
|
||||
"daemon_update_browse": "Alle Releases durchsuchen…",
|
||||
"daemon_update_check": "Nach Updates suchen…",
|
||||
"daemon_update_checking": "Suche nach dem neuesten Node…",
|
||||
"daemon_update_downgrade_note": "Ältere Versionen sind möglicherweise nicht mit Ihren aktuellen Chain-Daten kompatibel. Die Installation einer anderen Version wird nach einem Neustart des Daemons wirksam.",
|
||||
"daemon_update_download_install": "Herunterladen & installieren",
|
||||
"daemon_update_downloading": "Wird heruntergeladen…",
|
||||
"daemon_update_failed": "Update fehlgeschlagen",
|
||||
"daemon_update_installed": "Installiert:",
|
||||
"daemon_update_installed_ok": "Node installiert",
|
||||
"daemon_update_installing": "Wird installiert…",
|
||||
"daemon_update_keep": "Aktuellen behalten",
|
||||
"daemon_update_later": "Später",
|
||||
"daemon_update_latest": "Neueste:",
|
||||
"daemon_update_loading": "Releases werden geladen…",
|
||||
"daemon_update_now": "Jetzt aktualisieren",
|
||||
"daemon_update_reinstall": "Neu installieren",
|
||||
"daemon_update_restart_note": "Starten Sie den Daemon neu, um die neue Version auszuführen.",
|
||||
"daemon_update_restart_now": "Daemon jetzt neu starten",
|
||||
"daemon_update_safe": "Ihre Wallet, Ihre Schlüssel und Ihre Blockchain-Daten bleiben unberührt — nur die Programmdateien des Daemons werden ersetzt. Wenn Sie bewusst einen eigenen Node betreiben, wählen Sie „Aktuellen behalten“.",
|
||||
"daemon_update_title": "Node-Daemon aktualisieren?",
|
||||
"daemon_update_unavailable_body": "Für diese Plattform ist kein Node-Build verfügbar.",
|
||||
"daemon_update_unavailable_title": "Node-Updates nicht verfügbar",
|
||||
"daemon_update_unknown_error": "Unbekannter Fehler.",
|
||||
"daemon_update_up_to_date": "Der Node ist aktuell",
|
||||
"daemon_update_verify_note": "Der Download wird vor der Installation anhand der veröffentlichten SHA-256-Prüfsumme des Releases und einer fest hinterlegten ed25519-Signatur verifiziert.",
|
||||
"daemon_update_verifying": "Wird verifiziert…",
|
||||
"daemon_update_version": "Version:",
|
||||
"daemon_version": "Daemon",
|
||||
"dark": "Dunkel",
|
||||
"date": "Datum",
|
||||
@@ -257,10 +327,14 @@
|
||||
"explorer_block_time": "Zeit",
|
||||
"explorer_block_txs": "Transaktionen",
|
||||
"explorer_chain_stats": "Kette",
|
||||
"explorer_hash_not_found": "Kein Block und keine Transaktion für diesen Hash gefunden",
|
||||
"explorer_invalid_query": "Geben Sie eine Blockhöhe oder einen 64-stelligen Hash ein",
|
||||
"explorer_invalid_response": "Ungültige Antwort vom Daemon",
|
||||
"explorer_mempool": "Mempool",
|
||||
"explorer_mempool_size": "Größe",
|
||||
"explorer_mempool_txs": "Transaktionen",
|
||||
"explorer_no_results": "Keine passenden zwischengespeicherten Blöcke",
|
||||
"explorer_not_connected": "Nicht mit dem Daemon verbunden — Block- oder Transaktions-Hash kann nicht nachgeschlagen werden",
|
||||
"explorer_recent_blocks": "Letzte Blöcke",
|
||||
"explorer_search": "Suchen",
|
||||
"explorer_section": "EXPLORER",
|
||||
@@ -312,6 +386,7 @@
|
||||
"hidden_tag": " (versteckt)",
|
||||
"hide": "Ausblenden",
|
||||
"hide_address": "Adresse ausblenden",
|
||||
"hide_qr": "QR ausblenden",
|
||||
"hide_zero_balances": "Nullsalden ausblenden",
|
||||
"history": "Verlauf",
|
||||
"immature_type": "Unreif",
|
||||
@@ -350,19 +425,120 @@
|
||||
"label_placeholder": "z.B. Ersparnisse, Mining...",
|
||||
"language": "Sprache",
|
||||
"light": "Hell",
|
||||
"lite_account_label": "Konto",
|
||||
"lite_action": "Aktion",
|
||||
"lite_backup_keys": "Sicherung & Schlüssel",
|
||||
"lite_birthday_backup": "Geburtstag: %llu (auch diesen sichern)",
|
||||
"lite_birthday_hint": "Blockhöhe, ab der gescannt werden soll. Bei 0 belassen, falls unbekannt (langsamerer vollständiger Scan).",
|
||||
"lite_birthday_label": "Geburtsblock",
|
||||
"lite_console_help_passthrough": "Jede andere Eingabe wird als Lite-Wallet-Konsolenbefehl ausgeführt.",
|
||||
"lite_copy": "Kopieren",
|
||||
"lite_could_not_write": "Konnte nicht schreiben ",
|
||||
"lite_encrypt_wallet": "Wallet verschlüsseln",
|
||||
"lite_encryption_removed": "Verschlüsselung entfernt",
|
||||
"lite_hide_wipe": "Ausblenden & löschen",
|
||||
"lite_import": "Importieren",
|
||||
"lite_import_key_label": "Schlüssel importieren",
|
||||
"lite_key_imported": "Schlüssel importiert — führen Sie eine Synchronisierung durch, um den Verlauf zu scannen",
|
||||
"lite_lock_failed": "Sperren fehlgeschlagen",
|
||||
"lite_lock_now": "Jetzt sperren",
|
||||
"lite_maintenance": "Wartung",
|
||||
"lite_net_add": "Hinzufügen",
|
||||
"lite_net_add_label_hint": "Bezeichnung (optional)",
|
||||
"lite_net_add_url_hint": "https://ihr-lite-server",
|
||||
"lite_net_checking": "wird geprüft…",
|
||||
"lite_net_connected": "Verbunden",
|
||||
"lite_net_custom": "Benutzerdefiniert",
|
||||
"lite_net_disconnected": "Nicht verbunden",
|
||||
"lite_net_hidden_section": "Ausgeblendete Server",
|
||||
"lite_net_hide": "Ausblenden",
|
||||
"lite_net_in_use": "In Verwendung",
|
||||
"lite_net_intro": "Wählen Sie einen zu verwendenden Server oder lassen Sie die Wallet einen zufällig auswählen. Änderungen werden sofort wirksam.",
|
||||
"lite_net_invalid_url": "Geben Sie eine gültige http(s)://-URL ein.",
|
||||
"lite_net_no_wallet": "Keine Wallet geöffnet",
|
||||
"lite_net_official": "Offiziell",
|
||||
"lite_net_offline": "offline",
|
||||
"lite_net_random_active": "Zufällige Serverauswahl ist aktiv.",
|
||||
"lite_net_random_server": "Zufälliger Server",
|
||||
"lite_net_refresh": "Aktualisieren",
|
||||
"lite_net_show_hidden": "Ausgeblendete Server anzeigen",
|
||||
"lite_net_sync_label": "Sync",
|
||||
"lite_net_synced": "Synchronisiert",
|
||||
"lite_net_syncing": "Synchronisiert",
|
||||
"lite_net_title": "Lite-Server",
|
||||
"lite_net_unhide": "Einblenden",
|
||||
"lite_net_use_random": "Jedes Mal einen zufälligen Server verwenden",
|
||||
"lite_no_wallet": "Keine Wallet geöffnet — erstellen oder öffnen Sie eine in den Einstellungen",
|
||||
"lite_no_wallet_short": "Keine Wallet geöffnet",
|
||||
"lite_op_create": "Erstellen",
|
||||
"lite_op_open": "Öffnen",
|
||||
"lite_op_restore": "Wiederherstellen",
|
||||
"lite_overwrite": "Überschreiben",
|
||||
"lite_passphrase_label": "Passphrase",
|
||||
"lite_private_keys_warning": "Private Schlüssel — wer sie besitzt, kann Ihre Gelder ausgeben",
|
||||
"lite_redownload_blocks": "Blöcke erneut herunterladen",
|
||||
"lite_redownload_desc": "Alle Blöcke erneut vom Lite-Server abrufen (verwenden, wenn Ihr Guthaben oder Ihr Verlauf falsch aussieht).",
|
||||
"lite_redownload_running": "Blöcke werden erneut heruntergeladen…",
|
||||
"lite_remove_encryption": "Verschlüsselung entfernen",
|
||||
"lite_restore_birthday_label": "Geburtsblock (optional — 0 scannt von Anfang an, langsamer)",
|
||||
"lite_restore_btn": "Wallet wiederherstellen",
|
||||
"lite_restore_intro": "Geben Sie Ihre Wiederherstellungsphrase ein. Die Wallet wird wiederhergestellt und anschließend vom Lite-Server synchronisiert.",
|
||||
"lite_restore_ok": "Wallet wiederhergestellt — Synchronisierung vom Lite-Server läuft…",
|
||||
"lite_restore_seed_label": "Seed-Phrase (Wörter durch Leerzeichen getrennt)",
|
||||
"lite_restore_title": "Aus Wiederherstellungsphrase wiederherstellen",
|
||||
"lite_save_to_file": "In Datei speichern",
|
||||
"lite_saved_to": "Gespeichert (Klartext, nur für Besitzer) unter ",
|
||||
"lite_security": "Sicherheit",
|
||||
"lite_seed_label": "Seed",
|
||||
"lite_seed_warning": "Wiederherstellungsphrase — der EINZIGE Weg, Ihre Wallet wiederherzustellen. Schreiben Sie sie auf, bewahren Sie sie offline auf und teilen Sie sie niemals.",
|
||||
"lite_servers_network_tab": "Lite-Server werden im Netzwerk-Tab verwaltet.",
|
||||
"lite_show_private_keys": "Private Schlüssel anzeigen",
|
||||
"lite_show_seed": "Seed anzeigen",
|
||||
"lite_unlock": "Entsperren",
|
||||
"lite_unlock_btn": "Entsperren",
|
||||
"lite_unlock_failed": "Entsperren fehlgeschlagen — falsche Passphrase?",
|
||||
"lite_unlock_msg": "Geben Sie Ihre Passphrase ein, um die Wallet zum Ausgeben zu entsperren.",
|
||||
"lite_unlock_ok": "Wallet entsperrt — Sie können jetzt senden",
|
||||
"lite_unlock_title": "Wallet entsperren",
|
||||
"lite_validate": "Prüfen",
|
||||
"lite_wallet_encrypted": "Wallet verschlüsselt",
|
||||
"lite_wallet_label": "Wallet",
|
||||
"lite_wallet_locked": "Wallet gesperrt",
|
||||
"lite_wallet_ready": "Wallet bereit",
|
||||
"lite_wallet_request": "Lite-Wallet-Anfrage",
|
||||
"lite_wallet_unlocked": "Wallet entsperrt",
|
||||
"lite_welcome_create": "Neue Wallet erstellen",
|
||||
"lite_welcome_create_failed": "Wallet konnte nicht erstellt werden",
|
||||
"lite_welcome_created": "Wallet erstellt — sichern Sie jetzt Ihre Wiederherstellungsphrase unter Einstellungen → Sicherung & Schlüssel",
|
||||
"lite_welcome_later": "Später",
|
||||
"lite_welcome_msg": "Sie haben noch keine Wallet. Erstellen Sie eine neue oder stellen Sie sie aus einer Wiederherstellungsphrase wieder her.",
|
||||
"lite_welcome_restore": "Aus Seed wiederherstellen",
|
||||
"lite_welcome_restore_hint": "Stellen Sie Ihre Wallet unter Einstellungen → Lite-Wallet-Anfrage wieder her",
|
||||
"lite_welcome_title": "Willkommen bei ObsidianDragon Lite",
|
||||
"lite_word_count": "%d / 24 Wörter",
|
||||
"lite_working": "In Arbeit…",
|
||||
"loading": "Laden...",
|
||||
"loading_addresses": "Adressen werden geladen...",
|
||||
"loading_transactions": "Transaktionen werden geladen",
|
||||
"local_hashrate": "Lokale Hashrate",
|
||||
"low_spec_mode": "Energiesparmodus",
|
||||
"mark_mining_address": "Als Mining-Adresse markieren",
|
||||
"market": "Markt",
|
||||
"market_12h": "12h",
|
||||
"market_18h": "18h",
|
||||
"market_24h": "24h",
|
||||
"market_24h_change": "24h",
|
||||
"market_24h_volume": "24H VOLUMEN",
|
||||
"market_6h": "6h",
|
||||
"market_attribution": "Preisdaten von NonKYC",
|
||||
"market_btc_price": "BTC PREIS",
|
||||
"market_cap": "Marktkapitalisierung",
|
||||
"market_cap_short": "Kap.",
|
||||
"market_iv_1d": "1T",
|
||||
"market_iv_1h": "1S",
|
||||
"market_iv_1m": "1M",
|
||||
"market_iv_1w": "1W",
|
||||
"market_iv_live": "Live",
|
||||
"market_no_history": "Kein Preisverlauf verfügbar",
|
||||
"market_no_price": "Keine Preisdaten",
|
||||
"market_now": "Jetzt",
|
||||
@@ -372,6 +548,8 @@
|
||||
"market_price_unavailable": "Preisdaten nicht verfügbar",
|
||||
"market_refresh_price": "Preisdaten aktualisieren",
|
||||
"market_trade_on": "Handeln auf %s",
|
||||
"market_updated": "\\xc2\\xb7 Aktualisiert %s",
|
||||
"market_vol_short": "Vol.",
|
||||
"mature": "Reif",
|
||||
"max": "Max",
|
||||
"memo": "Memo (optional, verschlüsselt)",
|
||||
@@ -381,8 +559,52 @@
|
||||
"memo_z_only": "Hinweis: Memos sind nur beim Senden an abgeschirmte (z) Adressen verfügbar",
|
||||
"merge_description": "Mehrere UTXOs zu einer einzelnen abgeschirmten Adresse zusammenführen. Dies kann die Wallet-Größe reduzieren und die Privatsphäre verbessern.",
|
||||
"merge_funds": "Gelder zusammenführen",
|
||||
"merge_send_failed": "Zusammenführen fehlgeschlagen: ",
|
||||
"merge_started": "Zusammenführung gestartet",
|
||||
"merge_title": "An Adresse zusammenführen",
|
||||
"mig_adopt": "Dies zu meiner Wallet machen",
|
||||
"mig_adopt_now": "Seed-Wallet übernehmen",
|
||||
"mig_adopting": "Ihre neue Wallet wird installiert und neu gescannt…",
|
||||
"mig_adopting_note": "Der Node startet mit Ihrer neuen Wallet neu und scannt die Chain neu — das kann einige Minuten dauern. Lassen Sie die Wallet laufen; sie verbindet sich automatisch wieder.",
|
||||
"mig_already_mnemonic": "Ihre Wallet hat bereits eine 24-Wort-Wiederherstellungsphrase — es gibt nichts zu migrieren. Sichern Sie sie einfach und bewahren Sie die Wörter an einem sicheren Ort auf.",
|
||||
"mig_backed_up": "Ich habe meine Wiederherstellungsphrase aufgeschrieben",
|
||||
"mig_backup_instead": "Wiederherstellungsphrase sichern",
|
||||
"mig_balance": "Aktuelles Wallet-Guthaben: %.8f DRGX (abzüglich einer kleinen Gebühr)",
|
||||
"mig_check_failed": "Ihre Wallet konnte nicht geprüft werden. Stellen Sie sicher, dass sie entsperrt und verbunden ist, und prüfen Sie erneut.",
|
||||
"mig_checking_balance": "Ihr Guthaben wird geprüft",
|
||||
"mig_confirming": "Es wird auf die Bestätigung der Übertragung in der Blockchain gewartet, bevor die Wallet gewechselt wird — dies garantiert, dass Ihre Gelder tatsächlich verschoben wurden.",
|
||||
"mig_confs": "Übertragungsbestätigungen: %d",
|
||||
"mig_continue_sweep": "Weiter zur Übertragung",
|
||||
"mig_copy_seed": "Phrase kopieren",
|
||||
"mig_create": "Seed-Wallet erstellen",
|
||||
"mig_daemon_too_old": "Ihr DragonX-Node ist zu alt, um eine Seed-Wallet zu erstellen — ihm fehlt die dafür nötige Unterstützung für Wiederherstellungsphrasen. Aktualisieren Sie den Node (Einstellungen → Node & Sicherheit → Gebündelten installieren, oder Nach Updates suchen) und versuchen Sie es erneut.",
|
||||
"mig_discard": "Migration verwerfen",
|
||||
"mig_done": "Migration abgeschlossen. Ihre Wallet ist nun durch Ihre Wiederherstellungsphrase gesichert.",
|
||||
"mig_done_btn": "Fertig",
|
||||
"mig_done_detail": "Der Daemon führt einen Neuscan durch, um Ihre Gelder anzuzeigen — das kann einige Minuten dauern. Ihre vorherige Wallet wurde als .bak im Datenordner gespeichert.",
|
||||
"mig_intro": "Dies erstellt eine brandneue Wallet, die durch eine 24-Wort-Wiederherstellungsphrase gesichert ist, in einem isolierten Knoten, damit Sie Ihre Gelder dorthin übertragen können. Ihre aktuelle Wallet wird NICHT verändert und in diesem Schritt werden KEINE Gelder verschoben — Sie sichern zunächst die neue Wiederherstellungsphrase und übertragen Ihre Gelder dann in einem separaten, bestätigten Schritt.",
|
||||
"mig_later": "Später",
|
||||
"mig_no_funds": "Ihre Wallet hat keine Gelder zum Migrieren. Sie können die neue Seed-Wallet direkt übernehmen — sie ersetzt Ihre aktuelle (leere) Wallet durch die seed-gesicherte. Ihre alte Wallet wird für alle Fälle dennoch in eine mit Zeitstempel versehene Sicherung beiseitegelegt.",
|
||||
"mig_nofunds_confirm": "Ich verstehe, dass dies meine aktuelle Wallet durch die neue Seed-Wallet ersetzt",
|
||||
"mig_not_connected": "Verbinden Sie sich zuerst mit Ihrem Node und öffnen Sie dies dann erneut, um zu migrieren.",
|
||||
"mig_precheck_checking": "Ihre Wallet wird geprüft",
|
||||
"mig_receive_addr": "Empfangsadresse der neuen Wallet:",
|
||||
"mig_recheck": "Erneut prüfen",
|
||||
"mig_remainder": "Einige Gelder befinden sich noch in Ihrer alten Wallet (zu viele Eingänge für eine einzelne Transaktion) — übertragen Sie den Rest, bevor Sie wechseln.",
|
||||
"mig_remaining": "Verbleibend in alter Wallet: %.8f DRGX",
|
||||
"mig_seed_warning": "Schreiben Sie diese 24 Wörter der Reihe nach auf und bewahren Sie sie offline auf. Sie sind die einzige Sicherung Ihrer neuen Wallet — wenn Sie sie verlieren, sind die migrierten Gelder für immer verloren.",
|
||||
"mig_step1_done": "Schritt 1 von 2 abgeschlossen — Ihre Gelder wurden noch NICHT verschoben. Sie in diese neue Wallet zu übertragen ist der nächste Schritt.",
|
||||
"mig_sweep_all": "Alle Gelder übertragen",
|
||||
"mig_sweep_intro": "Übertragen Sie nun alle Gelder aus Ihrer aktuellen Wallet in die neue Seed-Wallet. Dies sendet eine einzelne Transaktion an die Adresse der neuen Wallet; Ihre Wiederherstellungsphrase (bereits gesichert) kontrolliert die Gelder von nun an.",
|
||||
"mig_sweep_remaining": "Rest übertragen",
|
||||
"mig_sweeping": "Ihre Gelder werden in die neue Wallet übertragen…",
|
||||
"mig_title": "Zu einer Seed-Wallet migrieren",
|
||||
"mig_to": "An: ",
|
||||
"mig_txid": "txid: ",
|
||||
"mig_unlock_first": "Entsperren Sie zuerst Ihre Wallet, um daraus auszugeben.",
|
||||
"mig_unlock_to_migrate": "Entsperren Sie zuerst Ihre Wallet und öffnen Sie dies dann erneut, um zu migrieren.",
|
||||
"mig_waiting_mine": "Es wird darauf gewartet, dass die Übertragungstransaktion geschürft wird…",
|
||||
"mig_working": "Ihre neue Seed-Wallet wird in einem isolierten Knoten erstellt — das kann eine Minute dauern. Ihre Haupt-Wallet läuft weiter.",
|
||||
"mine_when_idle": "Im Leerlauf minen",
|
||||
"mined": "gemined",
|
||||
"mined_filter": "Gemined",
|
||||
@@ -394,6 +616,8 @@
|
||||
"mining_address_copied": "Mining-Adresse kopiert",
|
||||
"mining_all_time": "Gesamt",
|
||||
"mining_already_saved": "Pool-URL bereits gespeichert",
|
||||
"mining_auto_balance_desc": "Verteilt die Miner nach Hashrate auf die offiziellen Pools, um das Netzwerk zu dezentralisieren. Prüft jeden Pool regelmäßig.",
|
||||
"mining_auto_balanced_to": "Mining automatisch angepasst auf",
|
||||
"mining_benchmark_cancel": "Benchmark abbrechen",
|
||||
"mining_benchmark_cooling": "Abkühlen",
|
||||
"mining_benchmark_dismiss": "Schließen",
|
||||
@@ -430,8 +654,10 @@
|
||||
"mining_idle_threads_active_tooltip": "Threads bei Benutzeraktivität",
|
||||
"mining_idle_threads_idle_tooltip": "Threads im Leerlauf",
|
||||
"mining_local_hashrate": "Lokale Hashrate",
|
||||
"mining_manual_desc": "Manuell: an den Pool minen, den Sie aus der Liste unten auswählen.",
|
||||
"mining_mine": "Minen",
|
||||
"mining_mining_addr": "Mining-Adr.",
|
||||
"mining_mining_here": "mint hier",
|
||||
"mining_network": "Netzwerk",
|
||||
"mining_no_blocks_yet": "Noch keine Blöcke gefunden",
|
||||
"mining_no_payouts_yet": "Noch keine Pool-Auszahlungen",
|
||||
@@ -441,18 +667,24 @@
|
||||
"mining_on": "Mining ist AN",
|
||||
"mining_open_in_explorer": "Im Explorer öffnen",
|
||||
"mining_payout_address": "Auszahlungsadresse",
|
||||
"mining_payout_foreign": "⚠ Diese Auszahlungsadresse befindet sich nicht in Ihrer aktuellen Wallet — geschürfte Belohnungen würden an eine andere Wallet gehen. Aktualisieren Sie sie, wenn Sie die Wallet gewechselt haben.",
|
||||
"mining_payout_tooltip": "Adresse für Mining-Belohnungen",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_fee": "Gebühr",
|
||||
"mining_pool_hashrate": "Pool-Hashrate",
|
||||
"mining_pool_url": "Pool-URL",
|
||||
"mining_pools_header": "POOLS",
|
||||
"mining_recent_blocks": "LETZTE BLÖCKE",
|
||||
"mining_recent_payouts": "LETZTE POOL-AUSZAHLUNGEN",
|
||||
"mining_refresh": "Aktualisieren",
|
||||
"mining_remove": "Entfernen",
|
||||
"mining_reset_defaults": "Standardwerte zurücksetzen",
|
||||
"mining_save_payout_address": "Auszahlungsadresse speichern",
|
||||
"mining_save_pool_url": "Pool-URL speichern",
|
||||
"mining_saved_addresses": "Gespeicherte Adressen:",
|
||||
"mining_saved_pools": "Gespeicherte Pools:",
|
||||
"mining_select_auto": "Auto-Ausgleich",
|
||||
"mining_select_manual": "Manuell",
|
||||
"mining_shares": "Shares",
|
||||
"mining_show_chart": "Diagramm",
|
||||
"mining_show_log": "Protokoll",
|
||||
@@ -465,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "Solo-Mining stoppen um Pool-Einstellungen zu ändern",
|
||||
"mining_stopping": "Stoppt...",
|
||||
"mining_stopping_tooltip": "Miner stoppt...",
|
||||
"mining_suggested_pools": "Vorgeschlagene Pools",
|
||||
"mining_syncing_tooltip": "Blockchain synchronisiert...",
|
||||
"mining_tag": " · Mining",
|
||||
"mining_threads": "Mining-Threads",
|
||||
"mining_to_save": "zum Speichern",
|
||||
"mining_today": "Heute",
|
||||
@@ -486,10 +720,12 @@
|
||||
"no_addresses_match": "Keine Adressen passen zum Filter",
|
||||
"no_addresses_with_balance": "Keine Adressen mit Guthaben",
|
||||
"no_addresses_yet": "Noch keine Adressen",
|
||||
"no_icons_found": "Keine Symbole entsprechen Ihrer Suche.",
|
||||
"no_matching": "Keine passenden Transaktionen",
|
||||
"no_recent_receives": "Keine kürzlichen Empfänge",
|
||||
"no_recent_sends": "Keine kürzlichen Sendungen",
|
||||
"no_transactions": "Keine Transaktionen gefunden",
|
||||
"no_transactions_yet": "Noch keine Transaktionen",
|
||||
"node": "KNOTEN",
|
||||
"node_security": "KNOTEN & SICHERHEIT",
|
||||
"noise": "Rauschen",
|
||||
@@ -553,7 +789,66 @@
|
||||
"pending": "Ausstehend",
|
||||
"pin_not_set": "PIN nicht gesetzt. Verwenden Sie das Passwort zum Entsperren.",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "Eintrag hinzufügen",
|
||||
"portfolio_add_to": "Zum Portfolio hinzufügen",
|
||||
"portfolio_addresses_hdr": "Adressen",
|
||||
"portfolio_addresses_sel": "%d ausgewählt",
|
||||
"portfolio_all_funds": "Alle Gelder",
|
||||
"portfolio_all_shielded": "Alle abgeschirmt",
|
||||
"portfolio_all_transparent": "Alle transparent",
|
||||
"portfolio_appearance": "Erscheinungsbild",
|
||||
"portfolio_cancel": "Abbrechen",
|
||||
"portfolio_clear_sel": "Leeren",
|
||||
"portfolio_close": "Schließen",
|
||||
"portfolio_color": "Farbe",
|
||||
"portfolio_currency": "Währung",
|
||||
"portfolio_custom_color": "Eigene Farbe…",
|
||||
"portfolio_delete": "Löschen",
|
||||
"portfolio_detail_empty": "Wählen Sie links eine Gruppe aus oder fügen Sie eine hinzu, um sie zu bearbeiten.",
|
||||
"portfolio_edit": "Bearbeiten",
|
||||
"portfolio_funded": "Finanziert",
|
||||
"portfolio_group_name": "Gruppenname",
|
||||
"portfolio_icon": "Symbol",
|
||||
"portfolio_label": "Bezeichnung",
|
||||
"portfolio_manage": "Verwalten\\xE2\\x80\\xA6",
|
||||
"portfolio_manage_title": "Portfolio verwalten",
|
||||
"portfolio_manual_price": "Preis / DRGX",
|
||||
"portfolio_new_entry": "Neuer Eintrag",
|
||||
"portfolio_no_addr_match": "Keine passenden Adressen",
|
||||
"portfolio_no_entries": "Noch keine benutzerdefinierten Einträge. Fügen Sie einen hinzu, um eine Gruppe von Adressen zu verfolgen.",
|
||||
"portfolio_no_icon": "Keines",
|
||||
"portfolio_outline_opacity": "Konturdeckkraft",
|
||||
"portfolio_price": "Preis",
|
||||
"portfolio_price_btc": "Markt \\xC2\\xB7 BTC",
|
||||
"portfolio_price_drgx": "Nur DRGX",
|
||||
"portfolio_price_manual": "Manuell",
|
||||
"portfolio_price_usd": "Markt · USD",
|
||||
"portfolio_remove_from": "Aus Portfolio entfernen",
|
||||
"portfolio_revert": "Zurücksetzen",
|
||||
"portfolio_save": "Speichern",
|
||||
"portfolio_search": "Adressen durchsuchen…",
|
||||
"portfolio_search_icons": "Symbole suchen…",
|
||||
"portfolio_select_all": "Alle",
|
||||
"portfolio_select_shown": "Alle auswählen",
|
||||
"portfolio_show": "Anzeigen:",
|
||||
"portfolio_show_24h": "24h",
|
||||
"portfolio_show_sparkline": "Sparkline",
|
||||
"portfolio_show_value": "Wert",
|
||||
"portfolio_spark_day": "Tag",
|
||||
"portfolio_spark_hour": "Stunde",
|
||||
"portfolio_spark_min": "Minute",
|
||||
"portfolio_spark_month": "Monat",
|
||||
"portfolio_spark_week": "Woche",
|
||||
"portfolio_style_compact": "Kompakte Zeilen",
|
||||
"portfolio_style_detailed": "Detaillierte Zeilen",
|
||||
"portfolio_style_featured": "Hervorgehobene Zeilen",
|
||||
"portfolio_style_label": "Portfolio-Stil",
|
||||
"portfolio_untitled": "Ohne Titel",
|
||||
"price_chart": "Preisdiagramm",
|
||||
"privacy_great": "Großartige Privatsphäre!",
|
||||
"privacy_low": "Geringe Privatsphäre — Gelder abschirmen",
|
||||
"privacy_medium": "Erwägen Sie, mehr abzuschirmen",
|
||||
"processing_transaction": "Transaktion wird verarbeitet...",
|
||||
"qr_code": "QR-Code",
|
||||
"qr_failed": "QR-Code konnte nicht generiert werden",
|
||||
"qr_title": "QR-Code",
|
||||
@@ -571,6 +866,7 @@
|
||||
"receiving_addresses": "Ihre Empfangsadressen",
|
||||
"recent_received": "KÜRZLICH EMPFANGEN",
|
||||
"recent_sends": "KÜRZLICH GESENDET",
|
||||
"recent_transactions": "LETZTE TRANSAKTIONEN",
|
||||
"recipient": "EMPFÄNGER",
|
||||
"recipient_balance": "Empfänger: %.8f → %.8f DRGX",
|
||||
"recv_type": "Empf.",
|
||||
@@ -578,6 +874,7 @@
|
||||
"refresh": "Aktualisieren",
|
||||
"refresh_now": "Jetzt aktualisieren",
|
||||
"remove_favorite": "Favorit entfernen",
|
||||
"repair_wallet": "Wallet reparieren",
|
||||
"report_bug": "Fehler melden",
|
||||
"request_amount": "Betrag (optional):",
|
||||
"request_copy_uri": "URI kopieren",
|
||||
@@ -593,12 +890,16 @@
|
||||
"request_transparent_addrs": "-- Transparente Adressen --",
|
||||
"request_uri_copied": "Zahlungs-URI in Zwischenablage kopiert",
|
||||
"rescan": "Neu scannen",
|
||||
"rescan_bootstrapped_msg": "Ihr Node wurde per Bootstrap eingerichtet, daher liegen Blöcke unterhalb des Snapshots nicht auf der Festplatte und ein Neuscan ab dem Genesis-Block würde fehlschlagen. Führen Sie den Neuscan ab einer Höhe durch, die Ihr Snapshot umfasst, um das ausgegebene Guthaben Ihrer Wallet abzugleichen. Ihre wallet.dat und Ihre Blockchain-Daten werden nicht gelöscht.",
|
||||
"rescan_detecting": "Prüfe, welche Blöcke Ihr Node auf der Festplatte hat…",
|
||||
"rescan_from_height": "Neu scannen ab Blockhöhe:",
|
||||
"reset_to_defaults": "Standardwerte zurücksetzen",
|
||||
"restarting_after_encryption": "Daemon wird nach Verschlüsselung neu gestartet...",
|
||||
"restore_address": "Adresse wiederherstellen",
|
||||
"result_preview": "Ergebnisvorschau",
|
||||
"retry": "Wiederholen",
|
||||
"review_send": "Senden prüfen",
|
||||
"rpc_connection": "RPC-Verbindung...",
|
||||
"rpc_host": "RPC-Host",
|
||||
"rpc_pass": "Passwort",
|
||||
"rpc_port": "Port",
|
||||
@@ -608,12 +909,15 @@
|
||||
"save_z_transactions": "Z-Tx in Tx-Liste speichern",
|
||||
"sb_auth_failed": "Authentifizierung fehlgeschlagen — rpcuser/rpcpassword prüfen",
|
||||
"sb_block": "Block: %d",
|
||||
"sb_building_witnesses": "Zeugen werden erstellt",
|
||||
"sb_building_witnesses_pct": "Witnesses werden neu aufgebaut %.0f%%",
|
||||
"sb_connecting_daemon": "Verbindung zu dragonxd...",
|
||||
"sb_connecting_err": "Verbindung zum Daemon — %s",
|
||||
"sb_connecting_external": "Verbindung zu externem Daemon...",
|
||||
"sb_connecting_generic": "Verbindung zum Daemon...",
|
||||
"sb_daemon_crashed": "Daemon ist %d mal abgestürzt",
|
||||
"sb_daemon_not_found": "Daemon nicht gefunden",
|
||||
"sb_daemon_start_failed": "dragonxd konnte nicht gestartet werden",
|
||||
"sb_dragonxd_running": "dragonxd läuft",
|
||||
"sb_dragonxd_stopped": "dragonxd gestoppt",
|
||||
"sb_dragonxd_stopping": "dragonxd wird gestoppt...",
|
||||
@@ -639,6 +943,12 @@
|
||||
"sb_waiting_daemon": "Warten auf dragonxd...",
|
||||
"sb_waiting_daemon_err": "Warten auf dragonxd — %s",
|
||||
"sb_warming_up": "Aufwärmen...",
|
||||
"sb_witness_cache": "Zeugen werden neu aufgebaut",
|
||||
"screenshot_open_dir": "Speicherort öffnen",
|
||||
"screenshot_sweep": "Screenshot-Durchlauf ausführen",
|
||||
"screenshot_sweep_desc": "Durchläuft jedes Design über jeden Tab und speichert von jedem einen Screenshot in tab-spezifischen Unterordnern im Screenshots-Ordner des Konfigurationsverzeichnisses (überschreibt den vorherigen Durchlauf). Läuft einige Sekunden.",
|
||||
"screenshot_sweep_full": "Vollständiger UI-Durchlauf",
|
||||
"search_icons": "Symbole suchen...",
|
||||
"search_placeholder": "Suchen...",
|
||||
"security": "SICHERHEIT",
|
||||
"seed_backup_button": "Wiederherstellungsphrase",
|
||||
@@ -655,6 +965,7 @@
|
||||
"seed_backup_saved": "Gespeichert unter ",
|
||||
"seed_backup_title": "Wiederherstellungsphrase sichern",
|
||||
"seed_backup_warning": "Schreiben Sie sie der Reihe nach auf, bewahren Sie sie offline auf und teilen oder fotografieren Sie sie niemals. Wenn Sie sie verlieren, können Ihre Gelder nicht wiederhergestellt werden.",
|
||||
"seed_migrate_button": "Zu Seed-Wallet migrieren…",
|
||||
"select_address": "Adresse auswählen...",
|
||||
"select_receiving_address": "Empfangsadresse auswählen...",
|
||||
"select_source_address": "Quelladresse auswählen...",
|
||||
@@ -663,8 +974,10 @@
|
||||
"send_amount_details": "BETRAGSDETAILS",
|
||||
"send_amount_upper": "BETRAG",
|
||||
"send_clear_fields": "Alle Formularfelder leeren?",
|
||||
"send_contacts_button": "Aus Kontakten auswählen",
|
||||
"send_copy_error": "Fehler kopieren",
|
||||
"send_dismiss": "Verwerfen",
|
||||
"send_err_needs_rescan": "Die abgeschirmten Notizdaten Ihrer Wallet sind nicht mehr aktuell mit der Blockchain (dies passiert nach einem Bootstrap oder Reindex). Führen Sie einen vollständigen Rescan über Einstellungen -> Blockchain neu scannen durch und lassen Sie ihn vollständig abschließen, und versuchen Sie dann erneut zu senden.",
|
||||
"send_error_copied": "Fehler in Zwischenablage kopiert",
|
||||
"send_error_prefix": "Fehler: %s",
|
||||
"send_exceeds_available": "Übersteigt verfügbar (%.8f)",
|
||||
@@ -683,6 +996,7 @@
|
||||
"send_recipient": "EMPFÄNGER",
|
||||
"send_select_source": "Quelladresse auswählen...",
|
||||
"send_sending_from": "SENDEN VON",
|
||||
"send_status_unconfirmed": "Transaktionsstatus konnte nicht bestätigt werden",
|
||||
"send_submitting": "Transaktion wird übermittelt...",
|
||||
"send_switch_to_receive": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
|
||||
"send_to": "Senden an",
|
||||
@@ -755,6 +1069,8 @@
|
||||
"settings_noise_opacity": "Rauschdichte:",
|
||||
"settings_not_encrypted": "Nicht verschlüsselt",
|
||||
"settings_not_found": "Nicht gefunden",
|
||||
"settings_open_app_dir": "App-Ordner öffnen",
|
||||
"settings_open_data_dir": "Datenordner öffnen",
|
||||
"settings_other": "Sonstiges",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "Datenschutz",
|
||||
@@ -788,16 +1104,24 @@
|
||||
"shield_check_status": "Status prüfen",
|
||||
"shield_completed": "Vorgang erfolgreich abgeschlossen!",
|
||||
"shield_description": "Schirmen Sie Ihre Mining-Belohnungen ab, indem Sie Coinbase-Ausgaben von transparenten Adressen an eine abgeschirmte Adresse senden. Dies verbessert die Privatsphäre, indem Ihre Mining-Einkünfte verborgen werden.",
|
||||
"shield_error_prefix": "Fehler: ",
|
||||
"shield_from_address": "Von Adresse:",
|
||||
"shield_funds": "Gelder abschirmen",
|
||||
"shield_in_progress": "Vorgang läuft...",
|
||||
"shield_max_utxos": "Max. UTXOs pro Vorgang",
|
||||
"shield_merge_done": "Abschirmung/Zusammenführung abgeschlossen!",
|
||||
"shield_op_failed": "Vorgang fehlgeschlagen: ",
|
||||
"shield_op_submitted": "Vorgang übermittelt: ",
|
||||
"shield_operation_id": "Vorgangs-ID: %s",
|
||||
"shield_select_z": "z-Adresse auswählen...",
|
||||
"shield_send_failed": "Abschirmen fehlgeschlagen: ",
|
||||
"shield_started": "Abschirmvorgang gestartet",
|
||||
"shield_status_check_error": "Fehler beim Prüfen des Status: ",
|
||||
"shield_status_label": "Status: ",
|
||||
"shield_submitting": "Vorgang wird übermittelt...",
|
||||
"shield_title": "Coinbase-Belohnungen abschirmen",
|
||||
"shield_to_address": "An Adresse (Abgeschirmt):",
|
||||
"shield_unknown_error": "Unbekannter Fehler",
|
||||
"shield_utxo_limit": "UTXO-Limit:",
|
||||
"shield_wildcard_hint": "Verwenden Sie '*' um von allen transparenten Adressen abzuschirmen",
|
||||
"shielded": "Abgeschirmt",
|
||||
@@ -807,11 +1131,16 @@
|
||||
"shielding_notice": "Hinweis: Dies wird Gelder von einer transparenten (T) Adresse auf eine private (Z) Adresse schirmen.",
|
||||
"show": "Anzeigen",
|
||||
"show_hidden": "Ausgeblendete anzeigen (%d)",
|
||||
"show_qr": "QR anzeigen",
|
||||
"show_qr_code": "QR-Code anzeigen",
|
||||
"showing_transactions": "Zeige %d–%d von %d Transaktionen (gesamt: %zu)",
|
||||
"showing_x_of_y": "%d von %d Adressen angezeigt",
|
||||
"simple_background": "Einfacher Hintergrund",
|
||||
"slider_off": "Aus",
|
||||
"sort_amount_high": "Größter Betrag",
|
||||
"sort_amount_low": "Kleinster Betrag",
|
||||
"sort_date_newest": "Neueste zuerst",
|
||||
"sort_date_oldest": "Älteste zuerst",
|
||||
"start_mining": "Mining starten",
|
||||
"status": "Status",
|
||||
"stop_external": "Externen Daemon stoppen",
|
||||
@@ -866,6 +1195,8 @@
|
||||
"tt_clear_ztx": "Lokal zwischengespeicherten Z-Transaktionsverlauf löschen",
|
||||
"tt_custom_fees": "Manuelle Gebühreneingabe beim Senden von Transaktionen aktivieren",
|
||||
"tt_custom_theme": "Benutzerdefiniertes Theme aktiv",
|
||||
"tt_daemon_install_bundled": "Node stoppen, den installierten dragonxd mit der in diesem Wallet-Build enthaltenen Version überschreiben und dann neu starten",
|
||||
"tt_daemon_update_check": "Den neuesten dragonxd-Full-Node vom Projekt-Gitea herunterladen und verifizieren, dann zum Anwenden neu starten",
|
||||
"tt_debug_collapse": "Debug-Protokollierungsoptionen einklappen",
|
||||
"tt_debug_expand": "Debug-Protokollierungsoptionen ausklappen",
|
||||
"tt_delete_blockchain": "Alle Blockchain-Daten löschen und neu synchronisieren. wallet.dat und Konfiguration bleiben erhalten.",
|
||||
@@ -881,15 +1212,19 @@
|
||||
"tt_keep_daemon": "Der Daemon wird beim Ausführen des Einrichtungsassistenten gestoppt",
|
||||
"tt_language": "Schnittstellensprache der Wallet-UI",
|
||||
"tt_layout_hotkey": "Hotkey: Links-/Rechts-Pfeiltasten zum Wechseln der Balance-Layouts",
|
||||
"tt_lite_redownload": "Alle Blöcke erneut vom Lite-Server herunterladen und neu scannen",
|
||||
"tt_lock": "Die Wallet sofort sperren",
|
||||
"tt_low_spec": "Alle aufwendigen visuellen Effekte deaktivieren\\nHotkey: Ctrl+Shift+Down",
|
||||
"tt_merge": "Mehrere UTXOs einer Adresse zusammenführen",
|
||||
"tt_mine_idle": "Mining automatisch starten, wenn das\\nSystem inaktiv ist (keine Tastatur-/Mauseingabe)",
|
||||
"tt_noise": "Körnungstextur-Intensität (0%% = aus, 100%% = maximum)",
|
||||
"tt_open_app_dir": "Den ObsidianDragon-Ordner (Einstellungen, Themes, Logs) im Dateimanager öffnen",
|
||||
"tt_open_data_dir": "Den Ordner mit Ihren Wallet- und Blockchain-Daten im Dateimanager öffnen",
|
||||
"tt_open_dir": "Klicken, um im Dateimanager zu öffnen",
|
||||
"tt_reduce_motion": "Animierte Übergänge und Saldo-Lerp für Barrierefreiheit deaktivieren",
|
||||
"tt_remove_encrypt": "Verschlüsselung entfernen und Wallet ungeschützt speichern",
|
||||
"tt_remove_pin": "PIN entfernen und Passphrase zum Entsperren erfordern",
|
||||
"tt_repair_wallet": "Die Transaktionsdatensätze der Wallet löschen und aus der Blockchain neu aufbauen (behebt Notes, die nach einem Neu-Scan nicht gesendet werden können)",
|
||||
"tt_report_bug": "Ein Problem im Projekt-Tracker melden",
|
||||
"tt_request_payment": "Eine Zahlungsanforderung mit QR-Code generieren",
|
||||
"tt_rescan": "Blockchain nach fehlenden Transaktionen neu scannen",
|
||||
@@ -904,6 +1239,7 @@
|
||||
"tt_scan_themes": "Nach neuen Themes suchen.\\nTheme-Ordner ablegen in:\\n%s",
|
||||
"tt_scanline": "CRT-Scanlinieneffekt in der Konsole",
|
||||
"tt_seed_backup": "Die 24-Wort-Wiederherstellungsphrase Ihrer Wallet anzeigen und sichern",
|
||||
"tt_seed_migrate": "Eine neue Wallet mit Wiederherstellungsphrase erstellen und Ihre Gelder dorthin übertragen",
|
||||
"tt_set_pin": "Eine 4-8-stellige PIN für schnelles Entsperren festlegen",
|
||||
"tt_shield_mining": "Transparente Mining-Belohnungen an eine geschirmte Adresse verschieben",
|
||||
"tt_simple_bg": "Einen einfachen Verlauf für den Hintergrund verwenden\\nHotkey: Ctrl+Up",
|
||||
@@ -917,6 +1253,7 @@
|
||||
"tt_ui_opacity": "Karten- und Seitenleisten-Deckkraft (100%% = vollständig undurchsichtig, niedriger = durchsichtiger)",
|
||||
"tt_validate": "Prüfen, ob eine DragonX-Adresse gültig ist",
|
||||
"tt_verbose": "Detaillierte Verbindungsdiagnosen,\\nDaemon-Status und Port-Besitzer-Info\\nin der Konsolen-Registerkarte protokollieren",
|
||||
"tt_wallets_button": "Ihre Wallet-Dateien auflisten und zwischen ihnen wechseln",
|
||||
"tt_website": "Die DragonX-Website öffnen",
|
||||
"tt_window_opacity": "Hintergrund-Deckkraft (niedriger = Desktop durch Fenster sichtbar)",
|
||||
"tt_wizard": "Den Ersteinrichtungsassistenten erneut ausführen\\nDer Daemon wird neu gestartet",
|
||||
@@ -925,7 +1262,18 @@
|
||||
"tx_from_address": "Von Adresse:",
|
||||
"tx_id_label": "Transaktions-ID:",
|
||||
"tx_immature": "UNREIF",
|
||||
"tx_loading_enriching_sends": "Details gesendeter Transaktionen werden geprüft (%d)",
|
||||
"tx_loading_fetching_transparent": "Transparenter Verlauf wird abgerufen",
|
||||
"tx_loading_history_progress": "Ältere Historie wird geladen (%d%%)",
|
||||
"tx_loading_queued": "Transaktionsaktualisierung in der Warteschlange",
|
||||
"tx_loading_refreshing_cached": "Wallet-Verlauf wird aktualisiert (%d zwischengespeichert)",
|
||||
"tx_loading_scanning_shielded": "Abgeschirmter Verlauf wird gescannt (%d Adressen)",
|
||||
"tx_mined": "GEMINED",
|
||||
"tx_progress_balances": "Guthaben wird aktualisiert",
|
||||
"tx_progress_finalizing": "Transaktion wird abgeschlossen",
|
||||
"tx_progress_history": "Verlauf wird aktualisiert",
|
||||
"tx_progress_submitting": "Transaktion wird an den Daemon übermittelt",
|
||||
"tx_progress_waiting_ops": "Warten auf Operation (%d)",
|
||||
"tx_received": "EMPFANGEN",
|
||||
"tx_sent": "GESENDET",
|
||||
"tx_to_address": "An Adresse:",
|
||||
@@ -937,6 +1285,14 @@
|
||||
"unconfirmed": "Unbestätigt",
|
||||
"undo_clear": "Leeren rückgängig",
|
||||
"unknown": "Unbekannt",
|
||||
"unmark_mining_address": "Mining-Adresse-Markierung aufheben",
|
||||
"upd_back": "Zurück",
|
||||
"upd_install": "Installieren",
|
||||
"upd_installed_badge": "installiert",
|
||||
"upd_no_build_platform": "Kein Build für diese Plattform",
|
||||
"upd_prerelease": "Vorabversion",
|
||||
"upd_reinstall": "Neu installieren",
|
||||
"upd_select_version": "Wählen Sie eine zu installierende Version aus",
|
||||
"use_embedded_daemon": "Eingebetteten dragonxd verwenden",
|
||||
"use_tor": "Tor verwenden",
|
||||
"validate_btn": "Validieren",
|
||||
@@ -956,17 +1312,73 @@
|
||||
"verbose_logging": "Ausführliches Logging",
|
||||
"version": "Version",
|
||||
"view": "Ansicht",
|
||||
"view_all": "Alle anzeigen",
|
||||
"view_details": "Details anzeigen",
|
||||
"view_on_explorer": "Im Explorer anzeigen",
|
||||
"waiting_for_daemon": "Warte auf Daemon-Verbindung...",
|
||||
"wallet": "WALLET",
|
||||
"wallet_empty": "Ihre Wallet ist leer",
|
||||
"wallet_empty_hint": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
|
||||
"wallets_active": "Aktiv",
|
||||
"wallets_add": "Ordner hinzufügen",
|
||||
"wallets_add_folder": "Auch einen weiteren Ordner nach Wallet-Dateien durchsuchen:",
|
||||
"wallets_add_folder_toggle": "+ Weiteren Ordner nach Wallets durchsuchen…",
|
||||
"wallets_button": "Wallets…",
|
||||
"wallets_col_addresses": "Adressen",
|
||||
"wallets_col_balance": "Guthaben",
|
||||
"wallets_col_name": "Wallet",
|
||||
"wallets_col_opened": "Zuletzt geöffnet",
|
||||
"wallets_col_size": "Größe",
|
||||
"wallets_create": "Wallet erstellen",
|
||||
"wallets_creating": "Wallet wird erstellt — der Node wird neu gestartet…",
|
||||
"wallets_current": "aktuell",
|
||||
"wallets_exists": "Eine Wallet mit diesem Namen existiert bereits.",
|
||||
"wallets_external_tt": "Außerhalb deines Datenverzeichnisses — mit Import hineinkopieren.",
|
||||
"wallets_folder_hint": "/pfad/zum/ordner mit .dat-Wallet-Dateien",
|
||||
"wallets_folder_invalid": "Dieser Ordner existiert nicht.",
|
||||
"wallets_import": "Importieren",
|
||||
"wallets_import_failed": "Diese Wallet-Datei konnte nicht importiert werden.",
|
||||
"wallets_import_hint": "Zum Öffnen importieren",
|
||||
"wallets_import_tt": "Diese Wallet befindet sich außerhalb des Datenverzeichnisses. Beim Öffnen wird sie zuerst hineinkopiert (demnächst verfügbar).",
|
||||
"wallets_imported": "Wallet importiert — wird gewechselt…",
|
||||
"wallets_intro": "Wallet-Dateien in Ihrem Datenverzeichnis (und in beliebigen Ordnern, die Sie hinzufügen). Öffnen Sie eine, um zu wechseln — der Node wird neu gestartet, um sie zu laden, und jede Wallet behält ihre eigenen Daten.",
|
||||
"wallets_name_invalid": "Bitte geben Sie einen gültigen Wallet-Namen ein.",
|
||||
"wallets_never": "Nie",
|
||||
"wallets_new_hint": "Name (z. B. Ersparnisse)",
|
||||
"wallets_new_label": "Eine neue Wallet erstellen:",
|
||||
"wallets_open": "Öffnen",
|
||||
"wallets_reveal": "Ordner anzeigen",
|
||||
"wallets_title": "Wallets",
|
||||
"warning": "Warnung",
|
||||
"warning_upper": "WARNUNG!",
|
||||
"website": "Webseite",
|
||||
"window_opacity": "Fenster-Transparenz",
|
||||
"wizard_daemon_start_failed": "Daemon-Start fehlgeschlagen — wird automatisch wiederholt",
|
||||
"xmrig_browse_releases": "Alle Releases durchsuchen…",
|
||||
"xmrig_checking": "Nach dem neuesten Miner wird gesucht…",
|
||||
"xmrig_current": "Aktuell:",
|
||||
"xmrig_download_install": "Herunterladen & installieren",
|
||||
"xmrig_downloading": "Wird heruntergeladen…",
|
||||
"xmrig_installed": "Installiert:",
|
||||
"xmrig_installed_ok": "Miner installiert",
|
||||
"xmrig_installing": "Wird installiert…",
|
||||
"xmrig_latest": "Neueste:",
|
||||
"xmrig_loading_releases": "Releases werden geladen…",
|
||||
"xmrig_none": "keiner",
|
||||
"xmrig_reinstall": "Neu installieren",
|
||||
"xmrig_stop_mining_first": "Stoppen Sie das Mining, bevor Sie den Miner aktualisieren.",
|
||||
"xmrig_unavailable_body": "Für diese Plattform ist kein Miner-Build verfügbar.",
|
||||
"xmrig_unavailable_title": "Miner-Updates nicht verfügbar",
|
||||
"xmrig_unknown_error": "Unbekannter Fehler.",
|
||||
"xmrig_up_to_date": "Der Miner ist auf dem neuesten Stand",
|
||||
"xmrig_update_available": "Ein neuer Miner ist verfügbar",
|
||||
"xmrig_update_button": "Miner aktualisieren…",
|
||||
"xmrig_update_failed": "Update fehlgeschlagen",
|
||||
"xmrig_update_short": "Aktualisieren",
|
||||
"xmrig_update_title": "Miner aktualisieren",
|
||||
"xmrig_verify_note": "Der Download wird vor der Installation anhand der veröffentlichten SHA-256-Prüfsumme des Releases überprüft.",
|
||||
"xmrig_verifying": "Wird überprüft…",
|
||||
"xmrig_version": "Version:",
|
||||
"yes_clear": "Ja, leeren",
|
||||
"your_addresses": "Ihre Adressen",
|
||||
"z_address": "Z-Adresse",
|
||||
|
||||
412
res/lang/es.json
412
res/lang/es.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "Agregar Dirección",
|
||||
"address_book_add_new": "Agregar Nueva",
|
||||
"address_book_added": "Dirección agregada a la libreta",
|
||||
"address_book_confirm_delete": "¿Confirmar eliminación?",
|
||||
"address_book_count": "%zu direcciones guardadas",
|
||||
"address_book_deleted": "Entrada eliminada",
|
||||
"address_book_edit": "Editar Dirección",
|
||||
@@ -39,6 +40,7 @@
|
||||
"address_copied": "Dirección copiada al portapapeles",
|
||||
"address_details": "Detalles de Dirección",
|
||||
"address_label": "Dirección:",
|
||||
"address_reorder_hint": "Suelta en el borde de una fila para reordenar, o en su centro para transferir",
|
||||
"address_upper": "DIRECCIÓN",
|
||||
"address_url": "URL de Dirección",
|
||||
"addresses_appear_here": "Tus direcciones de recepción aparecerán aquí una vez conectado.",
|
||||
@@ -69,7 +71,10 @@
|
||||
"backup_wallet": "Respaldar Cartera...",
|
||||
"backup_wallet_not_found": "Advertencia: wallet.dat no encontrado en la ubicación esperada",
|
||||
"balance": "Saldo",
|
||||
"balance_history_collecting": "Historial de saldo — recopilando datos...",
|
||||
"balance_layout": "Diseño de Saldo",
|
||||
"balance_shielded_fmt": "Protegido: %.8f",
|
||||
"balance_transparent_fmt": "Transparente: %.8f",
|
||||
"ban": "Bloquear",
|
||||
"banned_peers": "Nodos Bloqueados",
|
||||
"block": "Bloque",
|
||||
@@ -151,6 +156,20 @@
|
||||
"confirm_delete_blockchain_msg": "Esto detendrá el daemon, eliminará todos los datos de la blockchain (blocks, chainstate, peers) y comenzará una nueva sincronización desde cero. Esto puede tardar varias horas.",
|
||||
"confirm_delete_blockchain_safe": "Su wallet.dat, configuración e historial de transacciones están seguros y no se eliminarán.",
|
||||
"confirm_delete_blockchain_title": "Eliminar Datos de Blockchain",
|
||||
"confirm_lite_redownload_msg": "Esto borra los datos de bloques descargados por la cartera y vuelve a obtener y reescanear cada bloque desde el servidor lite. Puede tardar un rato; la cartera muestra el progreso de sincronización hasta que termina.",
|
||||
"confirm_lite_redownload_safe": "Tu cartera, tus claves y tu frase de recuperación no se ven afectados: solo se vuelven a descargar los datos de bloques.",
|
||||
"confirm_lite_redownload_title": "Volver a descargar bloques",
|
||||
"confirm_reinstall_daemon_msg": "Esto detiene el daemon, sobrescribe el dragonxd instalado (y dragonx-cli/dragonx-tx) con las versiones incluidas en esta compilación de la cartera, y luego reinicia el nodo. Úsalo para recuperar o actualizar el binario del nodo.",
|
||||
"confirm_reinstall_daemon_safe": "Tu cartera, tus claves y los datos de la blockchain no se ven afectados: solo se reemplazan los archivos del programa daemon.",
|
||||
"confirm_reinstall_daemon_title": "Instalar daemon incluido",
|
||||
"confirm_repair_wallet_msg": "Esto reinicia el daemon con -zapwallettxes=2: elimina todos los registros de transacciones y notas de la cartera y luego los reconstruye a partir de la blockchain. Úsalo cuando las transacciones no se puedan construir (\"Invalid sapling spend proof\" / \"shielded requirements not met\") incluso después de un reescaneo completo. Tarda mucho y la cartera permanece sin conexión hasta que termina.",
|
||||
"confirm_repair_wallet_safe": "Tus claves, direcciones y saldo se conservan — solo se reconstruyen los registros de transacciones en caché.",
|
||||
"confirm_repair_wallet_title": "Reparar cartera",
|
||||
"confirm_rescan_msg": "Esto reinicia el daemon y vuelve a escanear toda la blockchain en busca de las transacciones de tu cartera. Puede tardar mucho tiempo y la cartera permanece sin conexión hasta que termine.",
|
||||
"confirm_rescan_safe": "Tu wallet.dat y los datos de la blockchain no se eliminan, solo se reescanean.",
|
||||
"confirm_rescan_title": "Reescanear la blockchain",
|
||||
"confirm_restart_daemon_msg": "Esto detiene y reinicia el daemon para aplicar las opciones cambiadas. La cartera se desconectará y reconectará brevemente.",
|
||||
"confirm_restart_daemon_title": "Reiniciar daemon",
|
||||
"confirm_send": "Confirmar Envío",
|
||||
"confirm_transaction": "Confirmar Transacción",
|
||||
"confirm_transfer": "Confirmar Transferencia",
|
||||
@@ -161,6 +180,7 @@
|
||||
"connected_peers": "Nodos Conectados",
|
||||
"connecting": "Conectando...",
|
||||
"console": "Consola",
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Auto-desplazamiento",
|
||||
"console_available_commands": "Comandos disponibles:",
|
||||
"console_capturing_output": "Capturando salida del daemon...",
|
||||
@@ -177,6 +197,7 @@
|
||||
"console_connected": "Conectado al daemon",
|
||||
"console_copy_all": "Copiar Todo",
|
||||
"console_copy_selected": "Copiar",
|
||||
"console_copy_value": "Copiar",
|
||||
"console_daemon": "Daemon",
|
||||
"console_daemon_error": "¡Error del daemon!",
|
||||
"console_daemon_started": "Daemon iniciado",
|
||||
@@ -195,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - Controlar minería",
|
||||
"console_help_stop": " stop - Detener el daemon",
|
||||
"console_line_count": "%zu líneas",
|
||||
"console_matches": "coincidencias",
|
||||
"console_new_lines": "%d nuevas líneas",
|
||||
"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_rpc_reference": "Referencia de Comandos RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Líneas de consola",
|
||||
"console_search_commands": "Buscar comandos...",
|
||||
"console_select_all": "Seleccionar Todo",
|
||||
"console_show_app_output": "Mostrar las líneas de registro de la cartera [app]",
|
||||
"console_show_daemon_output": "Mostrar salida del daemon",
|
||||
"console_show_errors_only": "Mostrar solo errores",
|
||||
"console_show_rpc_ref": "Mostrar referencia de comandos RPC",
|
||||
"console_show_rpc_trace": "Mostrar llamadas RPC de la app",
|
||||
"console_showing_lines": "Mostrando %zu de %zu líneas",
|
||||
"console_starting_node": "Iniciando nodo...",
|
||||
"console_status_error": "Error",
|
||||
@@ -218,6 +244,12 @@
|
||||
"console_welcome": "Bienvenido a la Consola de ObsidianDragon",
|
||||
"console_zoom_in": "Acercar",
|
||||
"console_zoom_out": "Alejar",
|
||||
"contact_global": "Mostrar en todas las carteras (contacto global)",
|
||||
"contact_global_badge_tt": "Contacto global — visible en todas las carteras",
|
||||
"contact_global_tt": "Activado: este contacto permanece visible sin importar qué cartera cargues. Desactivado: pertenece solo a la cartera actual.",
|
||||
"contacts": "Contactos",
|
||||
"contacts_search_no_match": "No hay contactos coincidentes",
|
||||
"contacts_search_placeholder": "Buscar contactos...",
|
||||
"copied": "¡Copiado!",
|
||||
"copy": "Copiar",
|
||||
"copy_address": "Copiar Dirección Completa",
|
||||
@@ -227,6 +259,44 @@
|
||||
"copy_uri": "Copiar URI",
|
||||
"current_price": "Precio Actual",
|
||||
"custom_fees": "Comisiones personalizadas",
|
||||
"daemon_binary": "Binario del daemon",
|
||||
"daemon_bundled": "Incluido",
|
||||
"daemon_install_bundled": "Instalar integrado",
|
||||
"daemon_installed": "Instalado",
|
||||
"daemon_none_bundled": "ninguno en esta compilación",
|
||||
"daemon_not_installed": "no instalado",
|
||||
"daemon_status_differ": "El binario instalado difiere de la versión incluida.",
|
||||
"daemon_status_match": "El binario instalado coincide con la versión integrada.",
|
||||
"daemon_status_missing": "No hay ningún daemon instalado — instala la versión incluida.",
|
||||
"daemon_update_available": "Hay una nueva versión del nodo disponible",
|
||||
"daemon_update_body": "Esta versión de la cartera incluye un nodo DragonX más reciente que el que está instalado actualmente. Al actualizar se reemplaza el dragonxd instalado (y dragonx-cli/dragonx-tx), y luego se detiene y reinicia el nodo para que la nueva versión surta efecto. Recomendado: un nodo más reciente puede añadir funciones (p. ej. compatibilidad con frase de recuperación) que el antiguo no tiene.",
|
||||
"daemon_update_browse": "Explorar todas las versiones…",
|
||||
"daemon_update_check": "Buscar actualizaciones…",
|
||||
"daemon_update_checking": "Buscando el nodo más reciente…",
|
||||
"daemon_update_downgrade_note": "Las versiones anteriores pueden ser incompatibles con tus datos de blockchain actuales. Instalar una versión diferente surte efecto tras reiniciar el daemon.",
|
||||
"daemon_update_download_install": "Descargar e instalar",
|
||||
"daemon_update_downloading": "Descargando…",
|
||||
"daemon_update_failed": "Error en la actualización",
|
||||
"daemon_update_installed": "Instalado:",
|
||||
"daemon_update_installed_ok": "Nodo instalado",
|
||||
"daemon_update_installing": "Instalando…",
|
||||
"daemon_update_keep": "Mantener actual",
|
||||
"daemon_update_later": "Más tarde",
|
||||
"daemon_update_latest": "Más reciente:",
|
||||
"daemon_update_loading": "Cargando versiones…",
|
||||
"daemon_update_now": "Actualizar ahora",
|
||||
"daemon_update_reinstall": "Reinstalar",
|
||||
"daemon_update_restart_note": "Reinicia el daemon para empezar a ejecutar la nueva versión.",
|
||||
"daemon_update_restart_now": "Reiniciar el daemon ahora",
|
||||
"daemon_update_safe": "Tu cartera, tus claves y los datos de la blockchain no se ven afectados: solo se reemplazan los archivos del programa daemon. Si ejecutas deliberadamente un nodo personalizado, elige Mantener actual.",
|
||||
"daemon_update_title": "¿Actualizar el daemon del nodo?",
|
||||
"daemon_update_unavailable_body": "No hay una compilación del nodo disponible para esta plataforma.",
|
||||
"daemon_update_unavailable_title": "Actualizaciones del nodo no disponibles",
|
||||
"daemon_update_unknown_error": "Error desconocido.",
|
||||
"daemon_update_up_to_date": "El nodo está actualizado",
|
||||
"daemon_update_verify_note": "La descarga se verifica frente al SHA-256 publicado de la versión y una firma ed25519 fijada antes de instalarla.",
|
||||
"daemon_update_verifying": "Verificando…",
|
||||
"daemon_update_version": "Versión:",
|
||||
"daemon_version": "Daemon",
|
||||
"dark": "Oscuro",
|
||||
"date": "Fecha",
|
||||
@@ -257,10 +327,14 @@
|
||||
"explorer_block_time": "Hora",
|
||||
"explorer_block_txs": "Transacciones",
|
||||
"explorer_chain_stats": "Cadena",
|
||||
"explorer_hash_not_found": "No se encontró ningún bloque ni transacción para este hash",
|
||||
"explorer_invalid_query": "Ingrese una altura de bloque o un hash de 64 caracteres",
|
||||
"explorer_invalid_response": "Respuesta no válida del daemon",
|
||||
"explorer_mempool": "Mempool",
|
||||
"explorer_mempool_size": "Tamaño",
|
||||
"explorer_mempool_txs": "Transacciones",
|
||||
"explorer_no_results": "No hay bloques en caché que coincidan",
|
||||
"explorer_not_connected": "Sin conexión al daemon: no se puede buscar un bloque ni un hash de transacción",
|
||||
"explorer_recent_blocks": "Bloques Recientes",
|
||||
"explorer_search": "Buscar",
|
||||
"explorer_section": "EXPLORADOR",
|
||||
@@ -312,6 +386,7 @@
|
||||
"hidden_tag": " (oculto)",
|
||||
"hide": "Ocultar",
|
||||
"hide_address": "Ocultar dirección",
|
||||
"hide_qr": "Ocultar QR",
|
||||
"hide_zero_balances": "Ocultar saldos 0",
|
||||
"history": "Historial",
|
||||
"immature_type": "Inmaduro",
|
||||
@@ -350,19 +425,120 @@
|
||||
"label_placeholder": "ej. Ahorros, Minería...",
|
||||
"language": "Idioma",
|
||||
"light": "Claro",
|
||||
"lite_account_label": "Cuenta",
|
||||
"lite_action": "Acción",
|
||||
"lite_backup_keys": "Respaldo y claves",
|
||||
"lite_birthday_backup": "Cumpleaños: %llu (respalda esto también)",
|
||||
"lite_birthday_hint": "Altura de bloque desde la que empezar a escanear. Deja 0 si se desconoce (escaneo completo más lento).",
|
||||
"lite_birthday_label": "Fecha de creación",
|
||||
"lite_console_help_passthrough": "Cualquier otra entrada se ejecuta como un comando de consola de la cartera lite.",
|
||||
"lite_copy": "Copiar",
|
||||
"lite_could_not_write": "No se pudo escribir ",
|
||||
"lite_encrypt_wallet": "Cifrar cartera",
|
||||
"lite_encryption_removed": "Cifrado eliminado",
|
||||
"lite_hide_wipe": "Ocultar y borrar",
|
||||
"lite_import": "Importar",
|
||||
"lite_import_key_label": "Importar clave",
|
||||
"lite_key_imported": "Clave importada: ejecuta una sincronización para escanear su historial",
|
||||
"lite_lock_failed": "Error al bloquear",
|
||||
"lite_lock_now": "Bloquear ahora",
|
||||
"lite_maintenance": "Mantenimiento",
|
||||
"lite_net_add": "Agregar",
|
||||
"lite_net_add_label_hint": "Etiqueta (opcional)",
|
||||
"lite_net_add_url_hint": "https://tu-servidor-lite",
|
||||
"lite_net_checking": "comprobando…",
|
||||
"lite_net_connected": "Conectado",
|
||||
"lite_net_custom": "Personalizado",
|
||||
"lite_net_disconnected": "No conectado",
|
||||
"lite_net_hidden_section": "Servidores ocultos",
|
||||
"lite_net_hide": "Ocultar",
|
||||
"lite_net_in_use": "En uso",
|
||||
"lite_net_intro": "Elige un servidor para usar, o deja que la cartera elija uno al azar. Los cambios se aplican de inmediato.",
|
||||
"lite_net_invalid_url": "Introduce una URL http(s):// válida.",
|
||||
"lite_net_no_wallet": "No hay ninguna cartera abierta",
|
||||
"lite_net_official": "Oficial",
|
||||
"lite_net_offline": "sin conexión",
|
||||
"lite_net_random_active": "La selección aleatoria de servidor está activa.",
|
||||
"lite_net_random_server": "Servidor aleatorio",
|
||||
"lite_net_refresh": "Actualizar",
|
||||
"lite_net_show_hidden": "Mostrar servidores ocultos",
|
||||
"lite_net_sync_label": "Sincronización",
|
||||
"lite_net_synced": "Sincronizado",
|
||||
"lite_net_syncing": "Sincronizando",
|
||||
"lite_net_title": "Servidores Lite",
|
||||
"lite_net_unhide": "Mostrar",
|
||||
"lite_net_use_random": "Usar un servidor aleatorio cada vez",
|
||||
"lite_no_wallet": "No hay ninguna cartera abierta: crea o abre una en Configuración",
|
||||
"lite_no_wallet_short": "No hay ninguna cartera abierta",
|
||||
"lite_op_create": "Crear",
|
||||
"lite_op_open": "Abrir",
|
||||
"lite_op_restore": "Restaurar",
|
||||
"lite_overwrite": "Sobrescribir",
|
||||
"lite_passphrase_label": "Contraseña",
|
||||
"lite_private_keys_warning": "Claves privadas — cualquiera que las tenga puede gastar tus fondos",
|
||||
"lite_redownload_blocks": "Volver a descargar bloques",
|
||||
"lite_redownload_desc": "Vuelve a obtener todos los bloques del servidor lite (úsalo si tu saldo o historial parece incorrecto).",
|
||||
"lite_redownload_running": "Volviendo a descargar bloques…",
|
||||
"lite_remove_encryption": "Quitar cifrado",
|
||||
"lite_restore_birthday_label": "Bloque de creación (opcional — 0 escanea desde el principio, más lento)",
|
||||
"lite_restore_btn": "Restaurar cartera",
|
||||
"lite_restore_intro": "Introduce tu frase de recuperación. La cartera se restaurará y luego se sincronizará desde el servidor lite.",
|
||||
"lite_restore_ok": "Cartera restaurada: sincronizando desde el servidor lite…",
|
||||
"lite_restore_seed_label": "Frase de recuperación (palabras separadas por espacios)",
|
||||
"lite_restore_title": "Restaurar desde frase de recuperación",
|
||||
"lite_save_to_file": "Guardar en archivo",
|
||||
"lite_saved_to": "Guardado (texto sin cifrar, solo para el propietario) en ",
|
||||
"lite_security": "Seguridad",
|
||||
"lite_seed_label": "Frase",
|
||||
"lite_seed_warning": "Frase de recuperación — la ÚNICA forma de restaurar tu cartera. Anótala, guárdala sin conexión y nunca la compartas.",
|
||||
"lite_servers_network_tab": "Los servidores lite se gestionan en la pestaña Red.",
|
||||
"lite_show_private_keys": "Mostrar claves privadas",
|
||||
"lite_show_seed": "Mostrar frase de recuperación",
|
||||
"lite_unlock": "Desbloquear",
|
||||
"lite_unlock_btn": "Desbloquear",
|
||||
"lite_unlock_failed": "Error al desbloquear — ¿frase de contraseña incorrecta?",
|
||||
"lite_unlock_msg": "Introduce tu contraseña para desbloquear la cartera y poder gastar.",
|
||||
"lite_unlock_ok": "Cartera desbloqueada: ya puedes enviar",
|
||||
"lite_unlock_title": "Desbloquear cartera",
|
||||
"lite_validate": "Validar",
|
||||
"lite_wallet_encrypted": "Cartera cifrada",
|
||||
"lite_wallet_label": "Cartera",
|
||||
"lite_wallet_locked": "Cartera bloqueada",
|
||||
"lite_wallet_ready": "Cartera lista",
|
||||
"lite_wallet_request": "Solicitud de cartera lite",
|
||||
"lite_wallet_unlocked": "Cartera desbloqueada",
|
||||
"lite_welcome_create": "Crear nueva cartera",
|
||||
"lite_welcome_create_failed": "No se pudo crear la cartera",
|
||||
"lite_welcome_created": "Cartera creada: respalda ahora tu frase de recuperación en Configuración → Respaldo y claves",
|
||||
"lite_welcome_later": "Más tarde",
|
||||
"lite_welcome_msg": "Aún no tienes una cartera. Crea una nueva o restaura desde una frase de recuperación.",
|
||||
"lite_welcome_restore": "Restaurar desde frase",
|
||||
"lite_welcome_restore_hint": "Restaura tu cartera en Configuración → Solicitud de cartera lite",
|
||||
"lite_welcome_title": "Bienvenido a ObsidianDragon Lite",
|
||||
"lite_word_count": "%d / 24 palabras",
|
||||
"lite_working": "Trabajando…",
|
||||
"loading": "Cargando...",
|
||||
"loading_addresses": "Cargando direcciones...",
|
||||
"loading_transactions": "Cargando transacciones",
|
||||
"local_hashrate": "Tasa Hash Local",
|
||||
"low_spec_mode": "Modo bajo rendimiento",
|
||||
"mark_mining_address": "Marcar como dirección de minería",
|
||||
"market": "Mercado",
|
||||
"market_12h": "12h",
|
||||
"market_18h": "18h",
|
||||
"market_24h": "24h",
|
||||
"market_24h_change": "24h",
|
||||
"market_24h_volume": "VOLUMEN 24H",
|
||||
"market_6h": "6h",
|
||||
"market_attribution": "Datos de precios de NonKYC",
|
||||
"market_btc_price": "PRECIO BTC",
|
||||
"market_cap": "Cap. de Mercado",
|
||||
"market_cap_short": "Cap.",
|
||||
"market_iv_1d": "1D",
|
||||
"market_iv_1h": "1H",
|
||||
"market_iv_1m": "1M",
|
||||
"market_iv_1w": "1S",
|
||||
"market_iv_live": "En vivo",
|
||||
"market_no_history": "No hay historial de precios disponible",
|
||||
"market_no_price": "Sin datos de precio",
|
||||
"market_now": "Ahora",
|
||||
@@ -372,6 +548,8 @@
|
||||
"market_price_unavailable": "Datos de precio no disponibles",
|
||||
"market_refresh_price": "Actualizar datos de precio",
|
||||
"market_trade_on": "Operar en %s",
|
||||
"market_updated": "\\xc2\\xb7 Actualizado %s",
|
||||
"market_vol_short": "Vol",
|
||||
"mature": "Maduro",
|
||||
"max": "Máximo",
|
||||
"memo": "Memo (opcional, encriptado)",
|
||||
@@ -381,8 +559,52 @@
|
||||
"memo_z_only": "Nota: Los memos solo están disponibles al enviar a direcciones blindadas (z)",
|
||||
"merge_description": "Fusiona múltiples UTXOs en una sola dirección protegida. Esto puede ayudar a reducir el tamaño de la cartera y mejorar la privacidad.",
|
||||
"merge_funds": "Fusionar Fondos",
|
||||
"merge_send_failed": "Error al fusionar: ",
|
||||
"merge_started": "Operación de fusión iniciada",
|
||||
"merge_title": "Fusionar a Dirección",
|
||||
"mig_adopt": "Hacer que esta sea mi cartera",
|
||||
"mig_adopt_now": "Adoptar cartera con frase",
|
||||
"mig_adopting": "Instalando tu nueva cartera y reescaneando…",
|
||||
"mig_adopting_note": "El nodo se está reiniciando con tu nueva cartera y reescaneará la cadena: esto puede tardar varios minutos. Deja la cartera en funcionamiento; se reconecta automáticamente.",
|
||||
"mig_already_mnemonic": "Tu cartera ya tiene una frase de recuperación de 24 palabras: no hay nada que migrar. Simplemente respáldala y guarda las palabras en un lugar seguro.",
|
||||
"mig_backed_up": "He anotado mi frase de recuperación",
|
||||
"mig_backup_instead": "Respaldar frase de recuperación",
|
||||
"mig_balance": "Saldo de la cartera actual: %.8f DRGX (menos una pequeña comisión)",
|
||||
"mig_check_failed": "No se pudo comprobar tu cartera. Asegúrate de que esté desbloqueada y conectada, y luego vuelve a comprobar.",
|
||||
"mig_checking_balance": "Comprobando tu saldo",
|
||||
"mig_confirming": "Esperando a que el barrido se confirme en la cadena antes de cambiar de cartera: esto garantiza que tus fondos realmente se han movido.",
|
||||
"mig_confs": "Confirmaciones del barrido: %d",
|
||||
"mig_continue_sweep": "Continuar al barrido",
|
||||
"mig_copy_seed": "Copiar frase",
|
||||
"mig_create": "Crear cartera con frase",
|
||||
"mig_daemon_too_old": "Tu nodo DragonX es demasiado antiguo para crear una cartera con frase de recuperación: le falta la compatibilidad con mnemónicos que esto necesita. Actualiza el nodo (Configuración → NODO Y SEGURIDAD → Instalar integrado, o Buscar actualizaciones) y vuelve a intentarlo.",
|
||||
"mig_discard": "Descartar migración",
|
||||
"mig_done": "Migración completada. Tu cartera ahora está respaldada por tu frase de recuperación.",
|
||||
"mig_done_btn": "Listo",
|
||||
"mig_done_detail": "El daemon está reescaneando para mostrar tus fondos: esto puede tardar unos minutos. Tu cartera anterior se guardó como un .bak en la carpeta de datos.",
|
||||
"mig_intro": "Esto crea una cartera totalmente nueva respaldada por una frase de recuperación de 24 palabras, en un nodo aislado, para que puedas trasladar tus fondos a ella. Tu cartera actual NO se modifica y NO se mueve ningún fondo en este paso: primero respaldarás la nueva frase de recuperación y luego barrerás tus fondos hacia ella en un paso aparte y confirmado.",
|
||||
"mig_later": "Más tarde",
|
||||
"mig_no_funds": "Tu cartera no tiene fondos para migrar. Puedes adoptar directamente la nueva cartera con frase de recuperación: reemplaza tu cartera actual (vacía) por la respaldada con la frase. Tu cartera anterior se guarda de todos modos aparte, en un respaldo con marca de tiempo, por si acaso.",
|
||||
"mig_nofunds_confirm": "Entiendo que esto reemplaza mi cartera actual por la nueva cartera con frase de recuperación",
|
||||
"mig_not_connected": "Conéctate primero a tu nodo y luego vuelve a abrir esto para migrar.",
|
||||
"mig_precheck_checking": "Comprobando tu cartera",
|
||||
"mig_receive_addr": "Dirección de recepción de la nueva cartera:",
|
||||
"mig_recheck": "Volver a comprobar",
|
||||
"mig_remainder": "Aún quedan algunos fondos en tu cartera antigua (demasiadas entradas para una sola transacción): barre el resto antes de cambiar.",
|
||||
"mig_remaining": "Restante en la cartera antigua: %.8f DRGX",
|
||||
"mig_seed_warning": "Anota estas 24 palabras en orden y guárdalas sin conexión. Son el único respaldo de tu nueva cartera: si las pierdes, los fondos que migres se perderán para siempre.",
|
||||
"mig_step1_done": "Paso 1 de 2 completado: tus fondos AÚN NO se han movido. Barrerlos hacia esta nueva cartera es el siguiente paso.",
|
||||
"mig_sweep_all": "Barrer todos los fondos",
|
||||
"mig_sweep_intro": "Ahora barre todos los fondos de tu cartera actual hacia la nueva cartera con frase de recuperación. Esto envía una única transacción a la dirección de la nueva cartera; a partir de ahora, tu frase de recuperación (ya respaldada) controla los fondos.",
|
||||
"mig_sweep_remaining": "Barrer el resto",
|
||||
"mig_sweeping": "Barriendo tus fondos hacia la nueva cartera…",
|
||||
"mig_title": "Migrar a una cartera con frase de recuperación",
|
||||
"mig_to": "Para: ",
|
||||
"mig_txid": "txid: ",
|
||||
"mig_unlock_first": "Desbloquea primero tu cartera para poder gastar desde ella.",
|
||||
"mig_unlock_to_migrate": "Desbloquea primero tu cartera y luego vuelve a abrir esto para migrar.",
|
||||
"mig_waiting_mine": "Esperando a que se mine la transacción de barrido…",
|
||||
"mig_working": "Creando tu nueva cartera con frase de recuperación en un nodo aislado: esto puede tardar un minuto. Tu cartera principal sigue funcionando.",
|
||||
"mine_when_idle": "Minar en reposo",
|
||||
"mined": "minado",
|
||||
"mined_filter": "Minado",
|
||||
@@ -394,6 +616,8 @@
|
||||
"mining_address_copied": "Dirección de minería copiada",
|
||||
"mining_all_time": "Todo el Tiempo",
|
||||
"mining_already_saved": "URL del pool ya guardada",
|
||||
"mining_auto_balance_desc": "Distribuye los mineros entre los pools oficiales según el hashrate para ayudar a descentralizar la red. Comprueba cada pool periódicamente.",
|
||||
"mining_auto_balanced_to": "Minería autoequilibrada a",
|
||||
"mining_benchmark_cancel": "Cancelar benchmark",
|
||||
"mining_benchmark_cooling": "Enfriando",
|
||||
"mining_benchmark_dismiss": "Cerrar",
|
||||
@@ -430,8 +654,10 @@
|
||||
"mining_idle_threads_active_tooltip": "Hilos cuando el usuario está activo",
|
||||
"mining_idle_threads_idle_tooltip": "Hilos cuando el sistema está inactivo",
|
||||
"mining_local_hashrate": "Hashrate Local",
|
||||
"mining_manual_desc": "Manual: mina al pool que elijas de la lista de abajo.",
|
||||
"mining_mine": "Minar",
|
||||
"mining_mining_addr": "Dir. Minería",
|
||||
"mining_mining_here": "minando aquí",
|
||||
"mining_network": "Red",
|
||||
"mining_no_blocks_yet": "Aún no se han encontrado bloques",
|
||||
"mining_no_payouts_yet": "Aún no hay pagos del pool",
|
||||
@@ -441,18 +667,24 @@
|
||||
"mining_on": "La minería está ENCENDIDA",
|
||||
"mining_open_in_explorer": "Abrir en explorador",
|
||||
"mining_payout_address": "Dirección de Pago",
|
||||
"mining_payout_foreign": "⚠ Esta dirección de pago no está en tu cartera actual — las recompensas minadas irían a otra cartera. Actualízala si cambiaste de cartera.",
|
||||
"mining_payout_tooltip": "Dirección para recibir recompensas de minería",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_fee": "Comisión",
|
||||
"mining_pool_hashrate": "Hashrate del Pool",
|
||||
"mining_pool_url": "URL del Pool",
|
||||
"mining_pools_header": "POOLS",
|
||||
"mining_recent_blocks": "BLOQUES RECIENTES",
|
||||
"mining_recent_payouts": "PAGOS DE POOL RECIENTES",
|
||||
"mining_refresh": "Actualizar",
|
||||
"mining_remove": "Eliminar",
|
||||
"mining_reset_defaults": "Restablecer Valores",
|
||||
"mining_save_payout_address": "Guardar dirección de pago",
|
||||
"mining_save_pool_url": "Guardar URL del pool",
|
||||
"mining_saved_addresses": "Direcciones Guardadas:",
|
||||
"mining_saved_pools": "Pools Guardados:",
|
||||
"mining_select_auto": "Auto-balance",
|
||||
"mining_select_manual": "Manual",
|
||||
"mining_shares": "Acciones",
|
||||
"mining_show_chart": "Gráfico",
|
||||
"mining_show_log": "Registro",
|
||||
@@ -465,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "Detener minería solo para cambiar configuración del pool",
|
||||
"mining_stopping": "Deteniendo...",
|
||||
"mining_stopping_tooltip": "El minero está deteniéndose...",
|
||||
"mining_suggested_pools": "Pools sugeridos",
|
||||
"mining_syncing_tooltip": "El blockchain está sincronizando...",
|
||||
"mining_tag": " · Minería",
|
||||
"mining_threads": "Hilos de Minería",
|
||||
"mining_to_save": "para guardar",
|
||||
"mining_today": "Hoy",
|
||||
@@ -486,10 +720,12 @@
|
||||
"no_addresses_match": "No hay direcciones que coincidan con el filtro",
|
||||
"no_addresses_with_balance": "No hay direcciones con saldo",
|
||||
"no_addresses_yet": "Aún no hay direcciones",
|
||||
"no_icons_found": "Ningún icono coincide con tu búsqueda.",
|
||||
"no_matching": "No hay transacciones coincidentes",
|
||||
"no_recent_receives": "No hay recepciones recientes",
|
||||
"no_recent_sends": "No hay envíos recientes",
|
||||
"no_transactions": "No se encontraron transacciones",
|
||||
"no_transactions_yet": "Aún no hay transacciones",
|
||||
"node": "NODO",
|
||||
"node_security": "NODO Y SEGURIDAD",
|
||||
"noise": "Ruido",
|
||||
@@ -553,7 +789,66 @@
|
||||
"pending": "Pendiente",
|
||||
"pin_not_set": "PIN no configurado. Use la contraseña para desbloquear.",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "Añadir entrada",
|
||||
"portfolio_add_to": "Agregar a la cartera de inversiones",
|
||||
"portfolio_addresses_hdr": "Direcciones",
|
||||
"portfolio_addresses_sel": "%d seleccionadas",
|
||||
"portfolio_all_funds": "Todos los fondos",
|
||||
"portfolio_all_shielded": "Todo protegido",
|
||||
"portfolio_all_transparent": "Todo transparente",
|
||||
"portfolio_appearance": "Apariencia",
|
||||
"portfolio_cancel": "Cancelar",
|
||||
"portfolio_clear_sel": "Limpiar",
|
||||
"portfolio_close": "Cerrar",
|
||||
"portfolio_color": "Color",
|
||||
"portfolio_currency": "Moneda",
|
||||
"portfolio_custom_color": "Color personalizado…",
|
||||
"portfolio_delete": "Eliminar",
|
||||
"portfolio_detail_empty": "Selecciona un grupo a la izquierda, o añade uno, para editarlo.",
|
||||
"portfolio_edit": "Editar",
|
||||
"portfolio_funded": "Con fondos",
|
||||
"portfolio_group_name": "Nombre del grupo",
|
||||
"portfolio_icon": "Icono",
|
||||
"portfolio_label": "Etiqueta",
|
||||
"portfolio_manage": "Gestionar…",
|
||||
"portfolio_manage_title": "Gestionar portafolio",
|
||||
"portfolio_manual_price": "Precio / DRGX",
|
||||
"portfolio_new_entry": "Nueva entrada",
|
||||
"portfolio_no_addr_match": "Ninguna dirección coincide",
|
||||
"portfolio_no_entries": "Aún no hay entradas personalizadas. Agrega una para seguir un grupo de direcciones.",
|
||||
"portfolio_no_icon": "Ninguno",
|
||||
"portfolio_outline_opacity": "Opacidad del contorno",
|
||||
"portfolio_price": "Precio",
|
||||
"portfolio_price_btc": "Mercado \\xC2\\xB7 BTC",
|
||||
"portfolio_price_drgx": "Solo DRGX",
|
||||
"portfolio_price_manual": "Manual",
|
||||
"portfolio_price_usd": "Mercado · USD",
|
||||
"portfolio_remove_from": "Quitar del portafolio",
|
||||
"portfolio_revert": "Revertir",
|
||||
"portfolio_save": "Guardar",
|
||||
"portfolio_search": "Buscar direcciones…",
|
||||
"portfolio_search_icons": "Buscar iconos…",
|
||||
"portfolio_select_all": "Todos",
|
||||
"portfolio_select_shown": "Seleccionar todo",
|
||||
"portfolio_show": "Mostrar:",
|
||||
"portfolio_show_24h": "24h",
|
||||
"portfolio_show_sparkline": "Minigráfico",
|
||||
"portfolio_show_value": "Valor",
|
||||
"portfolio_spark_day": "Día",
|
||||
"portfolio_spark_hour": "Hora",
|
||||
"portfolio_spark_min": "Minuto",
|
||||
"portfolio_spark_month": "Mes",
|
||||
"portfolio_spark_week": "Semana",
|
||||
"portfolio_style_compact": "Filas compactas",
|
||||
"portfolio_style_detailed": "Filas detalladas",
|
||||
"portfolio_style_featured": "Filas destacadas",
|
||||
"portfolio_style_label": "Estilo de cartera",
|
||||
"portfolio_untitled": "Sin título",
|
||||
"price_chart": "Gráfico de Precios",
|
||||
"privacy_great": "¡Excelente privacidad!",
|
||||
"privacy_low": "Privacidad baja: protege los fondos",
|
||||
"privacy_medium": "Considera proteger más fondos",
|
||||
"processing_transaction": "Procesando transacción...",
|
||||
"qr_code": "Código QR",
|
||||
"qr_failed": "Error al generar código QR",
|
||||
"qr_title": "Código QR",
|
||||
@@ -571,6 +866,7 @@
|
||||
"receiving_addresses": "Sus Direcciones de Recepción",
|
||||
"recent_received": "RECIBIDOS RECIENTES",
|
||||
"recent_sends": "ENVÍOS RECIENTES",
|
||||
"recent_transactions": "TRANSACCIONES RECIENTES",
|
||||
"recipient": "DESTINATARIO",
|
||||
"recipient_balance": "Destinatario: %.8f → %.8f DRGX",
|
||||
"recv_type": "Recibido",
|
||||
@@ -578,6 +874,7 @@
|
||||
"refresh": "Actualizar",
|
||||
"refresh_now": "Actualizar Ahora",
|
||||
"remove_favorite": "Quitar favorito",
|
||||
"repair_wallet": "Reparar cartera",
|
||||
"report_bug": "Reportar Error",
|
||||
"request_amount": "Cantidad (opcional):",
|
||||
"request_copy_uri": "Copiar URI",
|
||||
@@ -593,12 +890,16 @@
|
||||
"request_transparent_addrs": "-- Direcciones Transparentes --",
|
||||
"request_uri_copied": "URI de pago copiada al portapapeles",
|
||||
"rescan": "Re-escanear",
|
||||
"rescan_bootstrapped_msg": "Tu nodo se inicializó con un bootstrap, por lo que los bloques anteriores a la instantánea no están en disco y un reescaneo desde el génesis fallaría. Reescanea desde una altura que incluya tu instantánea para reconciliar el saldo gastado de tu cartera. Tu wallet.dat y los datos de la cadena no se eliminan.",
|
||||
"rescan_detecting": "Comprobando qué bloques tiene tu nodo en disco…",
|
||||
"rescan_from_height": "Reescanear desde la altura de bloque:",
|
||||
"reset_to_defaults": "Restablecer Valores",
|
||||
"restarting_after_encryption": "Reiniciando daemon después del cifrado...",
|
||||
"restore_address": "Restaurar dirección",
|
||||
"result_preview": "Vista previa del resultado",
|
||||
"retry": "Reintentar",
|
||||
"review_send": "Revisar Envío",
|
||||
"rpc_connection": "Conexión RPC...",
|
||||
"rpc_host": "Host RPC",
|
||||
"rpc_pass": "Contraseña",
|
||||
"rpc_port": "Puerto",
|
||||
@@ -608,12 +909,15 @@
|
||||
"save_z_transactions": "Guardar Z-tx en lista",
|
||||
"sb_auth_failed": "Autenticación fallida — verifique rpcuser/rpcpassword",
|
||||
"sb_block": "Bloque: %d",
|
||||
"sb_building_witnesses": "Estableciendo testigos",
|
||||
"sb_building_witnesses_pct": "Reconstruyendo testigos %.0f%%",
|
||||
"sb_connecting_daemon": "Conectando a dragonxd...",
|
||||
"sb_connecting_err": "Conectando al daemon — %s",
|
||||
"sb_connecting_external": "Conectando a daemon externo...",
|
||||
"sb_connecting_generic": "Conectando al daemon...",
|
||||
"sb_daemon_crashed": "El daemon se bloqueó %d veces",
|
||||
"sb_daemon_not_found": "Daemon no encontrado",
|
||||
"sb_daemon_start_failed": "No se pudo iniciar dragonxd",
|
||||
"sb_dragonxd_running": "dragonxd ejecutándose",
|
||||
"sb_dragonxd_stopped": "dragonxd detenido",
|
||||
"sb_dragonxd_stopping": "Deteniendo dragonxd...",
|
||||
@@ -639,6 +943,12 @@
|
||||
"sb_waiting_daemon": "Esperando a dragonxd...",
|
||||
"sb_waiting_daemon_err": "Esperando a dragonxd — %s",
|
||||
"sb_warming_up": "Calentando...",
|
||||
"sb_witness_cache": "Reconstruyendo testigos",
|
||||
"screenshot_open_dir": "Abrir ubicación",
|
||||
"screenshot_sweep": "Ejecutar barrido de capturas",
|
||||
"screenshot_sweep_desc": "Recorre cada tema en cada pestaña y guarda una captura de pantalla de cada una en subcarpetas por pestaña dentro de la carpeta de capturas del directorio de configuración (sobrescribiendo el barrido anterior). Se ejecuta durante unos segundos.",
|
||||
"screenshot_sweep_full": "Barrido completo de la interfaz",
|
||||
"search_icons": "Buscar iconos...",
|
||||
"search_placeholder": "Buscar...",
|
||||
"security": "SEGURIDAD",
|
||||
"seed_backup_button": "Frase de recuperación",
|
||||
@@ -655,6 +965,7 @@
|
||||
"seed_backup_saved": "Guardado en ",
|
||||
"seed_backup_title": "Respaldar frase de recuperación",
|
||||
"seed_backup_warning": "Anótalas en orden, guárdalas sin conexión y nunca las compartas ni las fotografíes. Si las pierdes, tus fondos no se podrán recuperar.",
|
||||
"seed_migrate_button": "Migrar a una cartera con frase…",
|
||||
"select_address": "Seleccionar dirección...",
|
||||
"select_receiving_address": "Seleccionar dirección de recepción...",
|
||||
"select_source_address": "Seleccionar dirección de origen...",
|
||||
@@ -663,8 +974,10 @@
|
||||
"send_amount_details": "DETALLES DE CANTIDAD",
|
||||
"send_amount_upper": "CANTIDAD",
|
||||
"send_clear_fields": "¿Limpiar todos los campos del formulario?",
|
||||
"send_contacts_button": "Elegir de contactos",
|
||||
"send_copy_error": "Copiar Error",
|
||||
"send_dismiss": "Descartar",
|
||||
"send_err_needs_rescan": "Los datos de las notas blindadas de tu cartera están desactualizados respecto a la blockchain (esto ocurre tras un bootstrap o un reindexado). Ejecuta un reescaneo completo desde Configuración -> Reescanear blockchain y deja que termine por completo, luego intenta enviar de nuevo.",
|
||||
"send_error_copied": "Error copiado al portapapeles",
|
||||
"send_error_prefix": "Error: %s",
|
||||
"send_exceeds_available": "Excede disponible (%.8f)",
|
||||
@@ -683,6 +996,7 @@
|
||||
"send_recipient": "DESTINATARIO",
|
||||
"send_select_source": "Seleccionar dirección de origen...",
|
||||
"send_sending_from": "ENVIANDO DESDE",
|
||||
"send_status_unconfirmed": "No se pudo confirmar el estado de la transacción",
|
||||
"send_submitting": "Enviando transacción...",
|
||||
"send_switch_to_receive": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
|
||||
"send_to": "Enviar a",
|
||||
@@ -755,6 +1069,8 @@
|
||||
"settings_noise_opacity": "Opacidad de ruido:",
|
||||
"settings_not_encrypted": "Sin cifrar",
|
||||
"settings_not_found": "No encontrado",
|
||||
"settings_open_app_dir": "Abrir carpeta de la aplicación",
|
||||
"settings_open_data_dir": "Abrir carpeta de datos",
|
||||
"settings_other": "Otros",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "Privacidad",
|
||||
@@ -788,16 +1104,24 @@
|
||||
"shield_check_status": "Verificar Estado",
|
||||
"shield_completed": "¡Operación completada exitosamente!",
|
||||
"shield_description": "Protege tus recompensas de minería enviando salidas coinbase de direcciones transparentes a una dirección protegida. Esto mejora la privacidad ocultando tus ingresos de minería.",
|
||||
"shield_error_prefix": "Error: ",
|
||||
"shield_from_address": "Dirección de Origen:",
|
||||
"shield_funds": "Proteger Fondos",
|
||||
"shield_in_progress": "Operación en progreso...",
|
||||
"shield_max_utxos": "UTXOs máximos por operación",
|
||||
"shield_merge_done": "¡Protección/fusión completada!",
|
||||
"shield_op_failed": "Operación fallida: ",
|
||||
"shield_op_submitted": "Operación enviada: ",
|
||||
"shield_operation_id": "ID de operación: %s",
|
||||
"shield_select_z": "Seleccionar dirección z...",
|
||||
"shield_send_failed": "Error al proteger: ",
|
||||
"shield_started": "Operación de protección iniciada",
|
||||
"shield_status_check_error": "Error al comprobar el estado: ",
|
||||
"shield_status_label": "Estado: ",
|
||||
"shield_submitting": "Enviando operación...",
|
||||
"shield_title": "Proteger Recompensas de Coinbase",
|
||||
"shield_to_address": "Dirección Destino (Protegida):",
|
||||
"shield_unknown_error": "Error desconocido",
|
||||
"shield_utxo_limit": "Límite UTXO:",
|
||||
"shield_wildcard_hint": "Usa '*' para proteger desde todas las direcciones transparentes",
|
||||
"shielded": "Blindada",
|
||||
@@ -807,11 +1131,16 @@
|
||||
"shielding_notice": "Nota: Esto blindará fondos de una dirección transparente (T) a una dirección privada (Z).",
|
||||
"show": "Mostrar",
|
||||
"show_hidden": "Mostrar ocultos (%d)",
|
||||
"show_qr": "Mostrar QR",
|
||||
"show_qr_code": "Mostrar Código QR",
|
||||
"showing_transactions": "Mostrando %d–%d de %d transacciones (total: %zu)",
|
||||
"showing_x_of_y": "Mostrando %d de %d direcciones",
|
||||
"simple_background": "Fondo simple",
|
||||
"slider_off": "Apagado",
|
||||
"sort_amount_high": "Mayor importe",
|
||||
"sort_amount_low": "Menor cantidad",
|
||||
"sort_date_newest": "Más recientes primero",
|
||||
"sort_date_oldest": "Más antiguas primero",
|
||||
"start_mining": "Iniciar Minería",
|
||||
"status": "Estado",
|
||||
"stop_external": "Detener daemon externo",
|
||||
@@ -866,6 +1195,8 @@
|
||||
"tt_clear_ztx": "Eliminar historial de z-transacciones en caché local",
|
||||
"tt_custom_fees": "Habilitar entrada manual de comisiones al enviar transacciones",
|
||||
"tt_custom_theme": "Tema personalizado activo",
|
||||
"tt_daemon_install_bundled": "Detiene el nodo, sobrescribe el dragonxd instalado con la versión incluida en esta compilación de la cartera y luego lo reinicia",
|
||||
"tt_daemon_update_check": "Descarga y verifica el nodo completo dragonxd más reciente desde el Gitea del proyecto, y luego reinicia para aplicarlo",
|
||||
"tt_debug_collapse": "Colapsar opciones de registro de depuración",
|
||||
"tt_debug_expand": "Expandir opciones de registro de depuración",
|
||||
"tt_delete_blockchain": "Eliminar todos los datos de la blockchain e iniciar una nueva sincronización. Se preservan wallet.dat y la configuración.",
|
||||
@@ -881,15 +1212,19 @@
|
||||
"tt_keep_daemon": "El daemon se detendrá cuando ejecute el asistente de configuración",
|
||||
"tt_language": "Idioma de la interfaz de la billetera",
|
||||
"tt_layout_hotkey": "Atajo: teclas de flecha izquierda/derecha para cambiar diseños de Balance",
|
||||
"tt_lite_redownload": "Volver a descargar y re-escanear todos los bloques del servidor lite",
|
||||
"tt_lock": "Bloquear la billetera inmediatamente",
|
||||
"tt_low_spec": "Desactivar todos los efectos visuales pesados\\nAtajo: Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolidar múltiples UTXOs en una dirección",
|
||||
"tt_mine_idle": "Iniciar minería automáticamente cuando el\\nsistema esté inactivo (sin entrada de teclado/ratón)",
|
||||
"tt_noise": "Intensidad de textura granulada (0%% = apagado, 100%% = máximo)",
|
||||
"tt_open_app_dir": "Abrir la carpeta de ObsidianDragon (configuración, temas, registros) en el explorador de archivos",
|
||||
"tt_open_data_dir": "Abre en el gestor de archivos la carpeta con los datos de tu cartera y de la blockchain",
|
||||
"tt_open_dir": "Clic para abrir en explorador de archivos",
|
||||
"tt_reduce_motion": "Desactivar transiciones animadas y lerp de saldo para accesibilidad",
|
||||
"tt_remove_encrypt": "Quitar cifrado y almacenar la billetera sin protección",
|
||||
"tt_remove_pin": "Quitar PIN y requerir contraseña para desbloquear",
|
||||
"tt_repair_wallet": "Borra y reconstruye los registros de transacciones de la cartera a partir de la blockchain (corrige notas que no se envían tras un re-escaneo)",
|
||||
"tt_report_bug": "Reportar un problema en el rastreador del proyecto",
|
||||
"tt_request_payment": "Generar una solicitud de pago con código QR",
|
||||
"tt_rescan": "Reescanear la cadena de bloques en busca de transacciones faltantes",
|
||||
@@ -904,6 +1239,7 @@
|
||||
"tt_scan_themes": "Buscar nuevos temas.\\nColoque carpetas de temas en:\\n%s",
|
||||
"tt_scanline": "Efecto de líneas de escaneo CRT en la consola",
|
||||
"tt_seed_backup": "Muestra y respalda la frase de recuperación de 24 palabras de tu cartera",
|
||||
"tt_seed_migrate": "Crea una nueva cartera con frase de recuperación y traslada tus fondos a ella",
|
||||
"tt_set_pin": "Establecer un PIN de 4-8 dígitos para desbloqueo rápido",
|
||||
"tt_shield_mining": "Mover recompensas de minería transparentes a una dirección blindada",
|
||||
"tt_simple_bg": "Usar un gradiente simple para el fondo\\nAtajo: Ctrl+Up",
|
||||
@@ -917,6 +1253,7 @@
|
||||
"tt_ui_opacity": "Opacidad de tarjetas y barra lateral (100%% = totalmente opaco, menor = más transparente)",
|
||||
"tt_validate": "Comprobar si una dirección DragonX es válida",
|
||||
"tt_verbose": "Registrar diagnósticos detallados de conexión,\\nestado del daemon e info de propietario de puerto\\nen la pestaña de Consola",
|
||||
"tt_wallets_button": "Enumera tus archivos de cartera y cambia entre ellos",
|
||||
"tt_website": "Abrir el sitio web de DragonX",
|
||||
"tt_window_opacity": "Opacidad del fondo (menor = escritorio visible a través de la ventana)",
|
||||
"tt_wizard": "Volver a ejecutar el asistente de configuración inicial\\nEl daemon será reiniciado",
|
||||
@@ -925,7 +1262,18 @@
|
||||
"tx_from_address": "Dirección Origen:",
|
||||
"tx_id_label": "ID de Transacción:",
|
||||
"tx_immature": "INMADURO",
|
||||
"tx_loading_enriching_sends": "Comprobando detalles de transacciones enviadas (%d)",
|
||||
"tx_loading_fetching_transparent": "Obteniendo historial transparente",
|
||||
"tx_loading_history_progress": "Cargando historial más antiguo (%d%%)",
|
||||
"tx_loading_queued": "Actualización de transacciones en cola",
|
||||
"tx_loading_refreshing_cached": "Actualizando el historial de la cartera (%d en caché)",
|
||||
"tx_loading_scanning_shielded": "Escaneando historial protegido (%d direcciones)",
|
||||
"tx_mined": "MINADO",
|
||||
"tx_progress_balances": "Actualizando saldos",
|
||||
"tx_progress_finalizing": "Finalizando transacción",
|
||||
"tx_progress_history": "Actualizando el historial",
|
||||
"tx_progress_submitting": "Enviando transacción al daemon",
|
||||
"tx_progress_waiting_ops": "Esperando la operación (%d)",
|
||||
"tx_received": "RECIBIDO",
|
||||
"tx_sent": "ENVIADO",
|
||||
"tx_to_address": "Dirección Destino:",
|
||||
@@ -937,6 +1285,14 @@
|
||||
"unconfirmed": "Sin confirmar",
|
||||
"undo_clear": "Deshacer Limpieza",
|
||||
"unknown": "Desconocido",
|
||||
"unmark_mining_address": "Desmarcar dirección de minería",
|
||||
"upd_back": "Atrás",
|
||||
"upd_install": "Instalar",
|
||||
"upd_installed_badge": "instalado",
|
||||
"upd_no_build_platform": "No hay compilación para esta plataforma",
|
||||
"upd_prerelease": "prelanzamiento",
|
||||
"upd_reinstall": "Reinstalar",
|
||||
"upd_select_version": "Selecciona una versión para instalar",
|
||||
"use_embedded_daemon": "Usar dragonxd integrado",
|
||||
"use_tor": "Usar Tor",
|
||||
"validate_btn": "Validar",
|
||||
@@ -956,17 +1312,73 @@
|
||||
"verbose_logging": "Registro detallado",
|
||||
"version": "Versión",
|
||||
"view": "Ver",
|
||||
"view_all": "Ver todo",
|
||||
"view_details": "Ver Detalles",
|
||||
"view_on_explorer": "Ver en Explorador",
|
||||
"waiting_for_daemon": "Esperando conexión al daemon...",
|
||||
"wallet": "CARTERA",
|
||||
"wallet_empty": "Tu cartera está vacía",
|
||||
"wallet_empty_hint": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
|
||||
"wallets_active": "Activa",
|
||||
"wallets_add": "Agregar carpeta",
|
||||
"wallets_add_folder": "Escanear también otra carpeta en busca de archivos de cartera:",
|
||||
"wallets_add_folder_toggle": "+ Buscar wallets en otra carpeta…",
|
||||
"wallets_button": "Carteras…",
|
||||
"wallets_col_addresses": "Direcciones",
|
||||
"wallets_col_balance": "Saldo",
|
||||
"wallets_col_name": "Cartera",
|
||||
"wallets_col_opened": "Última apertura",
|
||||
"wallets_col_size": "Tamaño",
|
||||
"wallets_create": "Crear cartera",
|
||||
"wallets_creating": "Creando cartera — el nodo se reiniciará…",
|
||||
"wallets_current": "actual",
|
||||
"wallets_exists": "Ya existe una cartera con ese nombre.",
|
||||
"wallets_external_tt": "Fuera de tu directorio de datos — usa Importar para copiarlo.",
|
||||
"wallets_folder_hint": "/ruta/a/carpeta con archivos .dat de cartera",
|
||||
"wallets_folder_invalid": "Esa carpeta no existe.",
|
||||
"wallets_import": "Importar",
|
||||
"wallets_import_failed": "No se pudo importar ese archivo de cartera.",
|
||||
"wallets_import_hint": "Importar para abrir",
|
||||
"wallets_import_tt": "Esta cartera está fuera del directorio de datos. Al abrirla se copiará primero dentro (próximamente).",
|
||||
"wallets_imported": "Cartera importada — cambiando…",
|
||||
"wallets_intro": "Archivos de cartera en tu directorio de datos (y en las carpetas que agregues). Abre uno para cambiar: el nodo se reinicia para cargarlo y cada cartera conserva sus propios datos.",
|
||||
"wallets_name_invalid": "Introduce un nombre de cartera válido.",
|
||||
"wallets_never": "Nunca",
|
||||
"wallets_new_hint": "Nombre (p. ej. ahorros)",
|
||||
"wallets_new_label": "Crear una nueva cartera:",
|
||||
"wallets_open": "Abrir",
|
||||
"wallets_reveal": "Mostrar carpeta",
|
||||
"wallets_title": "Carteras",
|
||||
"warning": "Advertencia",
|
||||
"warning_upper": "¡ADVERTENCIA!",
|
||||
"website": "Sitio Web",
|
||||
"window_opacity": "Opacidad de ventana",
|
||||
"wizard_daemon_start_failed": "Error al iniciar el daemon — se reintentará automáticamente",
|
||||
"xmrig_browse_releases": "Ver todos los lanzamientos…",
|
||||
"xmrig_checking": "Buscando el minero más reciente…",
|
||||
"xmrig_current": "Actual:",
|
||||
"xmrig_download_install": "Descargar e instalar",
|
||||
"xmrig_downloading": "Descargando…",
|
||||
"xmrig_installed": "Instalado:",
|
||||
"xmrig_installed_ok": "Minero instalado",
|
||||
"xmrig_installing": "Instalando…",
|
||||
"xmrig_latest": "Más reciente:",
|
||||
"xmrig_loading_releases": "Cargando versiones…",
|
||||
"xmrig_none": "ninguno",
|
||||
"xmrig_reinstall": "Reinstalar",
|
||||
"xmrig_stop_mining_first": "Detén la minería antes de actualizar el minero.",
|
||||
"xmrig_unavailable_body": "No hay ninguna versión del minero disponible para esta plataforma.",
|
||||
"xmrig_unavailable_title": "Actualizaciones del minero no disponibles",
|
||||
"xmrig_unknown_error": "Error desconocido.",
|
||||
"xmrig_up_to_date": "El minero está actualizado",
|
||||
"xmrig_update_available": "Hay un nuevo minero disponible",
|
||||
"xmrig_update_button": "Actualizar minero…",
|
||||
"xmrig_update_failed": "Error en la actualización",
|
||||
"xmrig_update_short": "Actualizar",
|
||||
"xmrig_update_title": "Actualizar minero",
|
||||
"xmrig_verify_note": "La descarga se comprueba con la suma de verificación SHA-256 publicada de la versión antes de instalar.",
|
||||
"xmrig_verifying": "Verificando…",
|
||||
"xmrig_version": "Versión:",
|
||||
"yes_clear": "Sí, Limpiar",
|
||||
"your_addresses": "Sus Direcciones",
|
||||
"z_address": "Dirección Z",
|
||||
|
||||
412
res/lang/fr.json
412
res/lang/fr.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "Ajouter une adresse",
|
||||
"address_book_add_new": "Ajouter",
|
||||
"address_book_added": "Adresse ajoutée au carnet",
|
||||
"address_book_confirm_delete": "Confirmer la suppression ?",
|
||||
"address_book_count": "%zu adresses enregistrées",
|
||||
"address_book_deleted": "Entrée supprimée",
|
||||
"address_book_edit": "Modifier l'adresse",
|
||||
@@ -39,6 +40,7 @@
|
||||
"address_copied": "Adresse copiée dans le presse-papiers",
|
||||
"address_details": "Détails de l'adresse",
|
||||
"address_label": "Adresse :",
|
||||
"address_reorder_hint": "Déposez sur le bord d'une ligne pour réorganiser, ou en son centre pour transférer",
|
||||
"address_upper": "ADRESSE",
|
||||
"address_url": "URL de l'adresse",
|
||||
"addresses_appear_here": "Vos adresses de réception apparaîtront ici une fois connecté.",
|
||||
@@ -69,7 +71,10 @@
|
||||
"backup_wallet": "Sauvegarder le portefeuille...",
|
||||
"backup_wallet_not_found": "Attention : wallet.dat introuvable à l'emplacement prévu",
|
||||
"balance": "Solde",
|
||||
"balance_history_collecting": "Historique du solde — collecte des données...",
|
||||
"balance_layout": "Disposition du solde",
|
||||
"balance_shielded_fmt": "Blindé : %.8f",
|
||||
"balance_transparent_fmt": "Transparent : %.8f",
|
||||
"ban": "Bannir",
|
||||
"banned_peers": "Pairs bannis",
|
||||
"block": "Bloc",
|
||||
@@ -151,6 +156,20 @@
|
||||
"confirm_delete_blockchain_msg": "Cela arrêtera le daemon, supprimera toutes les données de la blockchain (blocks, chainstate, peers) et démarrera une nouvelle synchronisation. Cela peut prendre plusieurs heures.",
|
||||
"confirm_delete_blockchain_safe": "Votre wallet.dat, votre configuration et votre historique de transactions sont en sécurité et ne seront pas supprimés.",
|
||||
"confirm_delete_blockchain_title": "Supprimer les données Blockchain",
|
||||
"confirm_lite_redownload_msg": "Cela efface les données de blocs téléchargées par le portefeuille, puis récupère et re-scanne chaque bloc depuis le serveur lite. Cela peut prendre un moment ; le portefeuille affiche la progression de la synchronisation jusqu'à la fin.",
|
||||
"confirm_lite_redownload_safe": "Votre portefeuille, vos clés et votre phrase de récupération ne sont pas affectés — seules les données de blocs sont retéléchargées.",
|
||||
"confirm_lite_redownload_title": "Retélécharger les blocs",
|
||||
"confirm_reinstall_daemon_msg": "Ceci arrête le daemon, remplace le dragonxd installé (ainsi que dragonx-cli/dragonx-tx) par les versions incluses dans cette version du portefeuille, puis redémarre le nœud. À utiliser pour réparer ou mettre à jour le binaire du nœud.",
|
||||
"confirm_reinstall_daemon_safe": "Votre portefeuille, vos clés et vos données de blockchain ne sont pas touchés — seuls les fichiers du programme daemon sont remplacés.",
|
||||
"confirm_reinstall_daemon_title": "Installer le daemon intégré",
|
||||
"confirm_repair_wallet_msg": "Ceci redémarre le daemon avec -zapwallettxes=2 : il supprime tous les enregistrements de transactions et de notes du portefeuille, puis les reconstruit à partir de la blockchain. Utilisez-le lorsque les transactions échouent à se construire (« Invalid sapling spend proof » / « shielded requirements not met ») même après un rescan complet. Cela prend beaucoup de temps et le portefeuille reste hors ligne jusqu'à la fin.",
|
||||
"confirm_repair_wallet_safe": "Vos clés, adresses et solde sont préservés — seuls les enregistrements de transactions en cache sont reconstruits.",
|
||||
"confirm_repair_wallet_title": "Réparer le portefeuille",
|
||||
"confirm_rescan_msg": "Cela redémarre le daemon et re-scanne toute la blockchain à la recherche des transactions de votre portefeuille. Cela peut prendre beaucoup de temps et le portefeuille reste hors ligne jusqu'à la fin.",
|
||||
"confirm_rescan_safe": "Votre wallet.dat et vos données de blockchain ne sont pas supprimés — ils sont seulement re-scannés.",
|
||||
"confirm_rescan_title": "Rescan de la blockchain",
|
||||
"confirm_restart_daemon_msg": "Ceci arrête et redémarre le daemon pour appliquer les options modifiées. Le portefeuille se déconnectera puis se reconnectera brièvement.",
|
||||
"confirm_restart_daemon_title": "Redémarrer le daemon",
|
||||
"confirm_send": "Confirmer l'envoi",
|
||||
"confirm_transaction": "Confirmer la transaction",
|
||||
"confirm_transfer": "Confirmer le transfert",
|
||||
@@ -161,6 +180,7 @@
|
||||
"connected_peers": "Pairs connectés",
|
||||
"connecting": "Connexion...",
|
||||
"console": "Console",
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Défilement auto",
|
||||
"console_available_commands": "Commandes disponibles :",
|
||||
"console_capturing_output": "Capture de la sortie du daemon...",
|
||||
@@ -177,6 +197,7 @@
|
||||
"console_connected": "Connecté au daemon",
|
||||
"console_copy_all": "Tout copier",
|
||||
"console_copy_selected": "Copier",
|
||||
"console_copy_value": "Copier",
|
||||
"console_daemon": "Daemon",
|
||||
"console_daemon_error": "Erreur du daemon !",
|
||||
"console_daemon_started": "Daemon démarré",
|
||||
@@ -195,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - Contrôler le minage",
|
||||
"console_help_stop": " stop - Arrêter le daemon",
|
||||
"console_line_count": "%zu lignes",
|
||||
"console_matches": "correspondances",
|
||||
"console_new_lines": "%d nouvelles lignes",
|
||||
"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_rpc_reference": "Référence des commandes RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Scanline de la console",
|
||||
"console_search_commands": "Rechercher des commandes...",
|
||||
"console_select_all": "Tout sélectionner",
|
||||
"console_show_app_output": "Afficher les lignes du journal du portefeuille [app]",
|
||||
"console_show_daemon_output": "Afficher la sortie du daemon",
|
||||
"console_show_errors_only": "Afficher uniquement les erreurs",
|
||||
"console_show_rpc_ref": "Afficher la référence des commandes RPC",
|
||||
"console_show_rpc_trace": "Afficher les appels RPC de l'application",
|
||||
"console_showing_lines": "Affichage de %zu sur %zu lignes",
|
||||
"console_starting_node": "Démarrage du nœud...",
|
||||
"console_status_error": "Erreur",
|
||||
@@ -218,6 +244,12 @@
|
||||
"console_welcome": "Bienvenue dans la console ObsidianDragon",
|
||||
"console_zoom_in": "Agrandir",
|
||||
"console_zoom_out": "Réduire",
|
||||
"contact_global": "Afficher dans tous les portefeuilles (contact global)",
|
||||
"contact_global_badge_tt": "Contact global — visible dans tous les portefeuilles",
|
||||
"contact_global_tt": "Activé : ce contact reste visible quel que soit le portefeuille chargé. Désactivé : il appartient au portefeuille actuel uniquement.",
|
||||
"contacts": "Contacts",
|
||||
"contacts_search_no_match": "Aucun contact correspondant",
|
||||
"contacts_search_placeholder": "Rechercher des contacts...",
|
||||
"copied": "Copié !",
|
||||
"copy": "Copier",
|
||||
"copy_address": "Copier l'adresse complète",
|
||||
@@ -227,6 +259,44 @@
|
||||
"copy_uri": "Copier l'URI",
|
||||
"current_price": "Prix actuel",
|
||||
"custom_fees": "Frais personnalisés",
|
||||
"daemon_binary": "Binaire du daemon",
|
||||
"daemon_bundled": "Intégré",
|
||||
"daemon_install_bundled": "Installer la version intégrée",
|
||||
"daemon_installed": "Installé",
|
||||
"daemon_none_bundled": "aucun dans cette version",
|
||||
"daemon_not_installed": "non installé",
|
||||
"daemon_status_differ": "Le binaire installé diffère de la version intégrée.",
|
||||
"daemon_status_match": "Le binaire installé correspond à la version intégrée.",
|
||||
"daemon_status_missing": "Aucun daemon installé — installez la version intégrée.",
|
||||
"daemon_update_available": "Une nouvelle version du nœud est disponible",
|
||||
"daemon_update_body": "Cette version du portefeuille inclut un nœud DragonX plus récent que celui actuellement installé. La mise à jour remplace le dragonxd installé (ainsi que dragonx-cli/dragonx-tx), puis arrête et redémarre le nœud pour que la nouvelle version prenne effet. Recommandé — un nœud plus récent peut ajouter des fonctionnalités (par exemple la prise en charge des phrases de récupération) que l'ancien n'a pas.",
|
||||
"daemon_update_browse": "Parcourir toutes les versions…",
|
||||
"daemon_update_check": "Vérifier les mises à jour…",
|
||||
"daemon_update_checking": "Recherche du nœud le plus récent…",
|
||||
"daemon_update_downgrade_note": "Les versions plus anciennes peuvent être incompatibles avec vos données de chaîne actuelles. L'installation d'une version différente prend effet après un redémarrage du daemon.",
|
||||
"daemon_update_download_install": "Télécharger et installer",
|
||||
"daemon_update_downloading": "Téléchargement…",
|
||||
"daemon_update_failed": "Échec de la mise à jour",
|
||||
"daemon_update_installed": "Installé :",
|
||||
"daemon_update_installed_ok": "Nœud installé",
|
||||
"daemon_update_installing": "Installation…",
|
||||
"daemon_update_keep": "Conserver l'actuel",
|
||||
"daemon_update_later": "Plus tard",
|
||||
"daemon_update_latest": "Dernière :",
|
||||
"daemon_update_loading": "Chargement des versions…",
|
||||
"daemon_update_now": "Mettre à jour",
|
||||
"daemon_update_reinstall": "Réinstaller",
|
||||
"daemon_update_restart_note": "Redémarrez le daemon pour lancer la nouvelle version.",
|
||||
"daemon_update_restart_now": "Redémarrer le daemon maintenant",
|
||||
"daemon_update_safe": "Votre portefeuille, vos clés et vos données de blockchain ne sont pas touchés — seuls les fichiers du programme daemon sont remplacés. Si vous exécutez délibérément un nœud personnalisé, choisissez Conserver l'actuel.",
|
||||
"daemon_update_title": "Mettre à jour le daemon du nœud ?",
|
||||
"daemon_update_unavailable_body": "Aucune version du nœud n'est disponible pour cette plateforme.",
|
||||
"daemon_update_unavailable_title": "Mises à jour du nœud indisponibles",
|
||||
"daemon_update_unknown_error": "Erreur inconnue.",
|
||||
"daemon_update_up_to_date": "Le nœud est à jour",
|
||||
"daemon_update_verify_note": "Le téléchargement est vérifié par rapport au SHA-256 publié de la version et à une signature ed25519 épinglée avant l'installation.",
|
||||
"daemon_update_verifying": "Vérification…",
|
||||
"daemon_update_version": "Version :",
|
||||
"daemon_version": "Daemon",
|
||||
"dark": "Sombre",
|
||||
"date": "Date",
|
||||
@@ -257,10 +327,14 @@
|
||||
"explorer_block_time": "Heure",
|
||||
"explorer_block_txs": "Transactions",
|
||||
"explorer_chain_stats": "Chaîne",
|
||||
"explorer_hash_not_found": "Aucun bloc ou transaction trouvé pour ce hash",
|
||||
"explorer_invalid_query": "Entrez une hauteur de bloc ou un hash de 64 caractères",
|
||||
"explorer_invalid_response": "Réponse invalide du daemon",
|
||||
"explorer_mempool": "Mempool",
|
||||
"explorer_mempool_size": "Taille",
|
||||
"explorer_mempool_txs": "Transactions",
|
||||
"explorer_no_results": "Aucun bloc en cache correspondant",
|
||||
"explorer_not_connected": "Non connecté au daemon — impossible de rechercher un hash de bloc ou de transaction",
|
||||
"explorer_recent_blocks": "Blocs récents",
|
||||
"explorer_search": "Rechercher",
|
||||
"explorer_section": "EXPLORATEUR",
|
||||
@@ -312,6 +386,7 @@
|
||||
"hidden_tag": " (masqué)",
|
||||
"hide": "Masquer",
|
||||
"hide_address": "Masquer l'adresse",
|
||||
"hide_qr": "Masquer le QR",
|
||||
"hide_zero_balances": "Masquer les soldes à 0",
|
||||
"history": "Historique",
|
||||
"immature_type": "Immature",
|
||||
@@ -350,19 +425,120 @@
|
||||
"label_placeholder": "ex. Épargne, Minage...",
|
||||
"language": "Langue",
|
||||
"light": "Clair",
|
||||
"lite_account_label": "Compte",
|
||||
"lite_action": "Action",
|
||||
"lite_backup_keys": "Sauvegarde et clés",
|
||||
"lite_birthday_backup": "Date de création : %llu (à sauvegarder également)",
|
||||
"lite_birthday_hint": "Hauteur de bloc à partir de laquelle commencer l'analyse. Laissez 0 si inconnue (analyse complète plus lente).",
|
||||
"lite_birthday_label": "Bloc de création",
|
||||
"lite_console_help_passthrough": "Toute autre entrée est exécutée comme une commande de la console du portefeuille lite.",
|
||||
"lite_copy": "Copier",
|
||||
"lite_could_not_write": "Impossible d'écrire ",
|
||||
"lite_encrypt_wallet": "Chiffrer le portefeuille",
|
||||
"lite_encryption_removed": "Chiffrement supprimé",
|
||||
"lite_hide_wipe": "Masquer et effacer",
|
||||
"lite_import": "Importer",
|
||||
"lite_import_key_label": "Importer une clé",
|
||||
"lite_key_imported": "Clé importée — lancez une synchronisation pour analyser son historique",
|
||||
"lite_lock_failed": "Échec du verrouillage",
|
||||
"lite_lock_now": "Verrouiller maintenant",
|
||||
"lite_maintenance": "Maintenance",
|
||||
"lite_net_add": "Ajouter",
|
||||
"lite_net_add_label_hint": "Libellé (facultatif)",
|
||||
"lite_net_add_url_hint": "https://votre-serveur-lite",
|
||||
"lite_net_checking": "vérification…",
|
||||
"lite_net_connected": "Connecté",
|
||||
"lite_net_custom": "Personnalisé",
|
||||
"lite_net_disconnected": "Non connecté",
|
||||
"lite_net_hidden_section": "Serveurs masqués",
|
||||
"lite_net_hide": "Masquer",
|
||||
"lite_net_in_use": "Utilisé",
|
||||
"lite_net_intro": "Choisissez un serveur à utiliser, ou laissez le portefeuille en sélectionner un au hasard. Les changements s'appliquent immédiatement.",
|
||||
"lite_net_invalid_url": "Saisissez une URL http(s):// valide.",
|
||||
"lite_net_no_wallet": "Aucun portefeuille ouvert",
|
||||
"lite_net_official": "Officiel",
|
||||
"lite_net_offline": "hors ligne",
|
||||
"lite_net_random_active": "La sélection aléatoire du serveur est active.",
|
||||
"lite_net_random_server": "Serveur aléatoire",
|
||||
"lite_net_refresh": "Actualiser",
|
||||
"lite_net_show_hidden": "Afficher les serveurs masqués",
|
||||
"lite_net_sync_label": "Synchro",
|
||||
"lite_net_synced": "Synchronisé",
|
||||
"lite_net_syncing": "Synchronisation",
|
||||
"lite_net_title": "Serveurs Lite",
|
||||
"lite_net_unhide": "Afficher",
|
||||
"lite_net_use_random": "Utiliser un serveur aléatoire à chaque fois",
|
||||
"lite_no_wallet": "Aucun portefeuille ouvert — créez-en un ou ouvrez-en un dans Paramètres",
|
||||
"lite_no_wallet_short": "Aucun portefeuille ouvert",
|
||||
"lite_op_create": "Créer",
|
||||
"lite_op_open": "Ouvrir",
|
||||
"lite_op_restore": "Restaurer",
|
||||
"lite_overwrite": "Écraser",
|
||||
"lite_passphrase_label": "Phrase secrète",
|
||||
"lite_private_keys_warning": "Clés privées — quiconque les possède peut dépenser vos fonds",
|
||||
"lite_redownload_blocks": "Retélécharger les blocs",
|
||||
"lite_redownload_desc": "Re-télécharger tous les blocs depuis le serveur lite (à utiliser si votre solde ou votre historique semble erroné).",
|
||||
"lite_redownload_running": "Re-téléchargement des blocs…",
|
||||
"lite_remove_encryption": "Supprimer le chiffrement",
|
||||
"lite_restore_birthday_label": "Bloc de création (facultatif — 0 analyse depuis le début, plus lent)",
|
||||
"lite_restore_btn": "Restaurer le portefeuille",
|
||||
"lite_restore_intro": "Saisissez votre phrase de récupération. Le portefeuille sera restauré puis synchronisé depuis le serveur lite.",
|
||||
"lite_restore_ok": "Portefeuille restauré — synchronisation depuis le serveur lite…",
|
||||
"lite_restore_seed_label": "Phrase de récupération (mots séparés par des espaces)",
|
||||
"lite_restore_title": "Restaurer depuis la phrase de récupération",
|
||||
"lite_save_to_file": "Enregistrer dans un fichier",
|
||||
"lite_saved_to": "Enregistré (en clair, propriétaire uniquement) dans ",
|
||||
"lite_security": "Sécurité",
|
||||
"lite_seed_label": "Phrase de récupération",
|
||||
"lite_seed_warning": "Phrase de récupération — le SEUL moyen de restaurer votre portefeuille. Notez-la, conservez-la hors ligne, ne la partagez jamais.",
|
||||
"lite_servers_network_tab": "Les serveurs lite sont gérés dans l'onglet Réseau.",
|
||||
"lite_show_private_keys": "Afficher les clés privées",
|
||||
"lite_show_seed": "Afficher la phrase de récupération",
|
||||
"lite_unlock": "Déverrouiller",
|
||||
"lite_unlock_btn": "Déverrouiller",
|
||||
"lite_unlock_failed": "Échec du déverrouillage — mauvaise phrase secrète ?",
|
||||
"lite_unlock_msg": "Saisissez votre phrase secrète pour déverrouiller le portefeuille et pouvoir dépenser.",
|
||||
"lite_unlock_ok": "Portefeuille déverrouillé — vous pouvez maintenant envoyer",
|
||||
"lite_unlock_title": "Déverrouiller le portefeuille",
|
||||
"lite_validate": "Valider",
|
||||
"lite_wallet_encrypted": "Portefeuille chiffré",
|
||||
"lite_wallet_label": "Portefeuille",
|
||||
"lite_wallet_locked": "Portefeuille verrouillé",
|
||||
"lite_wallet_ready": "Portefeuille prêt",
|
||||
"lite_wallet_request": "Requête du portefeuille Lite",
|
||||
"lite_wallet_unlocked": "Portefeuille déverrouillé",
|
||||
"lite_welcome_create": "Créer un nouveau portefeuille",
|
||||
"lite_welcome_create_failed": "Impossible de créer le portefeuille",
|
||||
"lite_welcome_created": "Portefeuille créé — sauvegardez votre phrase de récupération maintenant dans Paramètres → Sauvegarde et clés",
|
||||
"lite_welcome_later": "Plus tard",
|
||||
"lite_welcome_msg": "Vous n'avez pas encore de portefeuille. Créez-en un nouveau ou restaurez-le à partir d'une phrase de récupération.",
|
||||
"lite_welcome_restore": "Restaurer depuis une phrase",
|
||||
"lite_welcome_restore_hint": "Restaurez votre portefeuille dans Paramètres → Portefeuille lite",
|
||||
"lite_welcome_title": "Bienvenue dans ObsidianDragon Lite",
|
||||
"lite_word_count": "%d / 24 mots",
|
||||
"lite_working": "En cours…",
|
||||
"loading": "Chargement...",
|
||||
"loading_addresses": "Chargement des adresses...",
|
||||
"loading_transactions": "Chargement des transactions",
|
||||
"local_hashrate": "Hashrate local",
|
||||
"low_spec_mode": "Mode économie",
|
||||
"mark_mining_address": "Marquer comme adresse de minage",
|
||||
"market": "Marché",
|
||||
"market_12h": "12h",
|
||||
"market_18h": "18h",
|
||||
"market_24h": "24h",
|
||||
"market_24h_change": "24 h",
|
||||
"market_24h_volume": "VOLUME 24H",
|
||||
"market_6h": "6h",
|
||||
"market_attribution": "Données de prix de NonKYC",
|
||||
"market_btc_price": "PRIX BTC",
|
||||
"market_cap": "Capitalisation",
|
||||
"market_cap_short": "Cap.",
|
||||
"market_iv_1d": "1J",
|
||||
"market_iv_1h": "1H",
|
||||
"market_iv_1m": "1M",
|
||||
"market_iv_1w": "1 s",
|
||||
"market_iv_live": "En direct",
|
||||
"market_no_history": "Aucun historique de prix disponible",
|
||||
"market_no_price": "Pas de données de prix",
|
||||
"market_now": "Maintenant",
|
||||
@@ -372,6 +548,8 @@
|
||||
"market_price_unavailable": "Données de prix indisponibles",
|
||||
"market_refresh_price": "Actualiser les données de prix",
|
||||
"market_trade_on": "Échanger sur %s",
|
||||
"market_updated": "\\xc2\\xb7 Mis à jour %s",
|
||||
"market_vol_short": "Vol",
|
||||
"mature": "Mature",
|
||||
"max": "Max",
|
||||
"memo": "Mémo (optionnel, chiffré)",
|
||||
@@ -381,8 +559,52 @@
|
||||
"memo_z_only": "Note : Les mémos ne sont disponibles que lors de l'envoi vers des adresses blindées (z)",
|
||||
"merge_description": "Fusionnez plusieurs UTXOs en une seule adresse blindée. Cela peut réduire la taille du portefeuille et améliorer la confidentialité.",
|
||||
"merge_funds": "Fusionner les fonds",
|
||||
"merge_send_failed": "Échec de la fusion : ",
|
||||
"merge_started": "Opération de fusion démarrée",
|
||||
"merge_title": "Fusionner vers une adresse",
|
||||
"mig_adopt": "Faire de ceci mon portefeuille",
|
||||
"mig_adopt_now": "Adopter le portefeuille à phrase",
|
||||
"mig_adopting": "Installation de votre nouveau portefeuille et rescan en cours…",
|
||||
"mig_adopting_note": "Le nœud redémarre sur votre nouveau portefeuille et va rescanner la chaîne — cela peut prendre plusieurs minutes. Laissez le portefeuille en marche ; il se reconnecte automatiquement.",
|
||||
"mig_already_mnemonic": "Votre portefeuille possède déjà une phrase de récupération de 24 mots — il n'y a rien à migrer. Sauvegardez-la simplement et conservez les mots en lieu sûr.",
|
||||
"mig_backed_up": "J'ai noté ma phrase de récupération",
|
||||
"mig_backup_instead": "Sauvegarder la phrase de récupération",
|
||||
"mig_balance": "Solde du portefeuille actuel : %.8f DRGX (moins de faibles frais)",
|
||||
"mig_check_failed": "Impossible de vérifier votre portefeuille. Assurez-vous qu'il est déverrouillé et connecté, puis relancez la vérification.",
|
||||
"mig_checking_balance": "Vérification de votre solde",
|
||||
"mig_confirming": "Attente de la confirmation du balayage sur la chaîne avant de changer de portefeuille — cela garantit que vos fonds ont bien bougé.",
|
||||
"mig_confs": "Confirmations du balayage : %d",
|
||||
"mig_continue_sweep": "Continuer vers le balayage",
|
||||
"mig_copy_seed": "Copier la phrase",
|
||||
"mig_create": "Créer le portefeuille à phrase",
|
||||
"mig_daemon_too_old": "Votre nœud DragonX est trop ancien pour créer un portefeuille à phrase — il ne prend pas en charge les phrases mnémoniques nécessaires. Mettez à jour le nœud (Paramètres → NŒUD & SÉCURITÉ → Installer la version intégrée, ou Rechercher des mises à jour), puis réessayez.",
|
||||
"mig_discard": "Abandonner la migration",
|
||||
"mig_done": "Migration terminée. Votre portefeuille est désormais protégé par votre phrase de récupération.",
|
||||
"mig_done_btn": "Terminé",
|
||||
"mig_done_detail": "Le daemon effectue un rescan pour afficher vos fonds — cela peut prendre quelques minutes. Votre portefeuille précédent a été enregistré sous forme de fichier .bak dans le dossier de données.",
|
||||
"mig_intro": "Ceci crée un tout nouveau portefeuille protégé par une phrase de récupération de 24 mots, dans un nœud isolé, afin que vous puissiez y transférer vos fonds. Votre portefeuille actuel n'est PAS modifié et AUCUN fonds ne bouge à cette étape — vous sauvegarderez d'abord la nouvelle phrase, puis vous y balaierez vos fonds lors d'une étape distincte et confirmée.",
|
||||
"mig_later": "Plus tard",
|
||||
"mig_no_funds": "Votre portefeuille n'a aucun fonds à migrer. Vous pouvez adopter directement le nouveau portefeuille à phrase — il remplace votre portefeuille actuel (vide) par celui protégé par une phrase de récupération. Votre ancien portefeuille est tout de même mis de côté dans une sauvegarde horodatée, au cas où.",
|
||||
"mig_nofunds_confirm": "Je comprends que ceci remplace mon portefeuille actuel par le nouveau portefeuille à phrase",
|
||||
"mig_not_connected": "Connectez-vous d'abord à votre nœud, puis rouvrez cette fenêtre pour migrer.",
|
||||
"mig_precheck_checking": "Vérification de votre portefeuille",
|
||||
"mig_receive_addr": "Adresse de réception du nouveau portefeuille :",
|
||||
"mig_recheck": "Revérifier",
|
||||
"mig_remainder": "Certains fonds sont encore dans votre ancien portefeuille (trop d'entrées pour une seule transaction) — balayez le reste avant de changer.",
|
||||
"mig_remaining": "Restant dans l'ancien portefeuille : %.8f DRGX",
|
||||
"mig_seed_warning": "Notez ces 24 mots dans l'ordre et conservez-les hors ligne. Ils constituent l'unique sauvegarde de votre nouveau portefeuille — si vous les perdez, les fonds que vous migrez sont perdus à jamais.",
|
||||
"mig_step1_done": "Étape 1 sur 2 terminée — vos fonds n'ont PAS encore bougé. Les balayer vers ce nouveau portefeuille est l'étape suivante.",
|
||||
"mig_sweep_all": "Balayer tous les fonds",
|
||||
"mig_sweep_intro": "Balayez maintenant tous les fonds de votre portefeuille actuel vers le nouveau portefeuille à phrase. Ceci envoie une seule transaction vers l'adresse du nouveau portefeuille ; votre phrase de récupération (déjà sauvegardée) contrôle les fonds à partir de maintenant.",
|
||||
"mig_sweep_remaining": "Balayer le reste",
|
||||
"mig_sweeping": "Balayage de vos fonds vers le nouveau portefeuille…",
|
||||
"mig_title": "Migrer vers un portefeuille à phrase de récupération",
|
||||
"mig_to": "Vers : ",
|
||||
"mig_txid": "txid : ",
|
||||
"mig_unlock_first": "Déverrouillez d'abord votre portefeuille pour y dépenser des fonds.",
|
||||
"mig_unlock_to_migrate": "Déverrouillez d'abord votre portefeuille, puis rouvrez cette fenêtre pour migrer.",
|
||||
"mig_waiting_mine": "Attente du minage de la transaction de balayage…",
|
||||
"mig_working": "Création de votre nouveau portefeuille à phrase dans un nœud isolé — cela peut prendre une minute. Votre portefeuille principal continue de fonctionner.",
|
||||
"mine_when_idle": "Miner au repos",
|
||||
"mined": "miné",
|
||||
"mined_filter": "Miné",
|
||||
@@ -394,6 +616,8 @@
|
||||
"mining_address_copied": "Adresse de minage copiée",
|
||||
"mining_all_time": "Tout le temps",
|
||||
"mining_already_saved": "URL du pool déjà enregistrée",
|
||||
"mining_auto_balance_desc": "Répartit les mineurs sur les pools officiels en fonction du hashrate pour aider à décentraliser le réseau. Vérifie chaque pool périodiquement.",
|
||||
"mining_auto_balanced_to": "Minage équilibré automatiquement à",
|
||||
"mining_benchmark_cancel": "Annuler le benchmark",
|
||||
"mining_benchmark_cooling": "Refroidissement",
|
||||
"mining_benchmark_dismiss": "Fermer",
|
||||
@@ -430,8 +654,10 @@
|
||||
"mining_idle_threads_active_tooltip": "Threads quand l'utilisateur est actif",
|
||||
"mining_idle_threads_idle_tooltip": "Threads quand le système est inactif",
|
||||
"mining_local_hashrate": "Hashrate local",
|
||||
"mining_manual_desc": "Manuel : minez vers le pool que vous choisissez dans la liste ci-dessous.",
|
||||
"mining_mine": "Miner",
|
||||
"mining_mining_addr": "Adr. minage",
|
||||
"mining_mining_here": "minage ici",
|
||||
"mining_network": "Réseau",
|
||||
"mining_no_blocks_yet": "Aucun bloc trouvé pour l'instant",
|
||||
"mining_no_payouts_yet": "Aucun paiement de pool pour l'instant",
|
||||
@@ -441,18 +667,24 @@
|
||||
"mining_on": "Le minage est ACTIVÉ",
|
||||
"mining_open_in_explorer": "Ouvrir dans l'explorateur",
|
||||
"mining_payout_address": "Adresse de paiement",
|
||||
"mining_payout_foreign": "⚠ Cette adresse de paiement ne fait pas partie de votre portefeuille actuel — les récompenses minées iraient vers un autre portefeuille. Mettez-la à jour si vous avez changé de portefeuille.",
|
||||
"mining_payout_tooltip": "Adresse pour recevoir les récompenses de minage",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_fee": "Frais",
|
||||
"mining_pool_hashrate": "Hashrate du pool",
|
||||
"mining_pool_url": "URL du pool",
|
||||
"mining_pools_header": "POOLS",
|
||||
"mining_recent_blocks": "BLOCS RÉCENTS",
|
||||
"mining_recent_payouts": "PAIEMENTS DE POOL RÉCENTS",
|
||||
"mining_refresh": "Actualiser",
|
||||
"mining_remove": "Supprimer",
|
||||
"mining_reset_defaults": "Réinitialiser les paramètres",
|
||||
"mining_save_payout_address": "Enregistrer l'adresse de paiement",
|
||||
"mining_save_pool_url": "Enregistrer l'URL du pool",
|
||||
"mining_saved_addresses": "Adresses enregistrées :",
|
||||
"mining_saved_pools": "Pools enregistrés :",
|
||||
"mining_select_auto": "Équilibrage auto",
|
||||
"mining_select_manual": "Manuel",
|
||||
"mining_shares": "Parts",
|
||||
"mining_show_chart": "Graphique",
|
||||
"mining_show_log": "Journal",
|
||||
@@ -465,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "Arrêtez le minage solo pour modifier les paramètres du pool",
|
||||
"mining_stopping": "Arrêt...",
|
||||
"mining_stopping_tooltip": "Le mineur s'arrête...",
|
||||
"mining_suggested_pools": "Pools suggérés",
|
||||
"mining_syncing_tooltip": "La blockchain se synchronise...",
|
||||
"mining_tag": " · Minage",
|
||||
"mining_threads": "Threads de minage",
|
||||
"mining_to_save": "pour enregistrer",
|
||||
"mining_today": "Aujourd'hui",
|
||||
@@ -486,10 +720,12 @@
|
||||
"no_addresses_match": "Aucune adresse ne correspond au filtre",
|
||||
"no_addresses_with_balance": "Aucune adresse avec solde",
|
||||
"no_addresses_yet": "Pas encore d'adresses",
|
||||
"no_icons_found": "Aucune icône ne correspond à votre recherche.",
|
||||
"no_matching": "Aucune transaction correspondante",
|
||||
"no_recent_receives": "Aucune réception récente",
|
||||
"no_recent_sends": "Aucun envoi récent",
|
||||
"no_transactions": "Aucune transaction trouvée",
|
||||
"no_transactions_yet": "Aucune transaction pour le moment",
|
||||
"node": "NŒUD",
|
||||
"node_security": "NŒUD & SÉCURITÉ",
|
||||
"noise": "Bruit",
|
||||
@@ -553,7 +789,66 @@
|
||||
"pending": "En attente",
|
||||
"pin_not_set": "PIN non défini. Utilisez le mot de passe pour déverrouiller.",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "Ajouter une entrée",
|
||||
"portfolio_add_to": "Ajouter au portefeuille",
|
||||
"portfolio_addresses_hdr": "Adresses",
|
||||
"portfolio_addresses_sel": "%d sélectionnées",
|
||||
"portfolio_all_funds": "Tous les fonds",
|
||||
"portfolio_all_shielded": "Tout blindé",
|
||||
"portfolio_all_transparent": "Tout transparent",
|
||||
"portfolio_appearance": "Apparence",
|
||||
"portfolio_cancel": "Annuler",
|
||||
"portfolio_clear_sel": "Effacer",
|
||||
"portfolio_close": "Fermer",
|
||||
"portfolio_color": "Couleur",
|
||||
"portfolio_currency": "Devise",
|
||||
"portfolio_custom_color": "Couleur personnalisée\\xE2\\x80\\xA6",
|
||||
"portfolio_delete": "Supprimer",
|
||||
"portfolio_detail_empty": "Sélectionnez un groupe à gauche, ou ajoutez-en un, pour le modifier.",
|
||||
"portfolio_edit": "Modifier",
|
||||
"portfolio_funded": "Approvisionné",
|
||||
"portfolio_group_name": "Nom du groupe",
|
||||
"portfolio_icon": "Icône",
|
||||
"portfolio_label": "Étiquette",
|
||||
"portfolio_manage": "Gérer…",
|
||||
"portfolio_manage_title": "Gérer le portefeuille",
|
||||
"portfolio_manual_price": "Prix / DRGX",
|
||||
"portfolio_new_entry": "Nouvelle entrée",
|
||||
"portfolio_no_addr_match": "Aucune adresse ne correspond",
|
||||
"portfolio_no_entries": "Aucune entrée personnalisée pour l'instant. Ajoutez-en une pour suivre un groupe d'adresses.",
|
||||
"portfolio_no_icon": "Aucun",
|
||||
"portfolio_outline_opacity": "Opacité du contour",
|
||||
"portfolio_price": "Prix",
|
||||
"portfolio_price_btc": "Marché \\xC2\\xB7 BTC",
|
||||
"portfolio_price_drgx": "DRGX uniquement",
|
||||
"portfolio_price_manual": "Manuel",
|
||||
"portfolio_price_usd": "Marché \\xC2\\xB7 USD",
|
||||
"portfolio_remove_from": "Retirer du portefeuille d'actifs",
|
||||
"portfolio_revert": "Annuler",
|
||||
"portfolio_save": "Enregistrer",
|
||||
"portfolio_search": "Rechercher des adresses\\xE2\\x80\\xA6",
|
||||
"portfolio_search_icons": "Rechercher des icônes\\xE2\\x80\\xA6",
|
||||
"portfolio_select_all": "Tout",
|
||||
"portfolio_select_shown": "Tout sélectionner",
|
||||
"portfolio_show": "Afficher :",
|
||||
"portfolio_show_24h": "24 h",
|
||||
"portfolio_show_sparkline": "Sparkline",
|
||||
"portfolio_show_value": "Valeur",
|
||||
"portfolio_spark_day": "Jour",
|
||||
"portfolio_spark_hour": "Heure",
|
||||
"portfolio_spark_min": "Minute",
|
||||
"portfolio_spark_month": "Mois",
|
||||
"portfolio_spark_week": "Semaine",
|
||||
"portfolio_style_compact": "Lignes compactes",
|
||||
"portfolio_style_detailed": "Lignes détaillées",
|
||||
"portfolio_style_featured": "Lignes en vedette",
|
||||
"portfolio_style_label": "Style du portefeuille",
|
||||
"portfolio_untitled": "Sans titre",
|
||||
"price_chart": "Graphique des prix",
|
||||
"privacy_great": "Excellente confidentialité !",
|
||||
"privacy_low": "Faible confidentialité — blindez vos fonds",
|
||||
"privacy_medium": "Envisagez de blinder davantage",
|
||||
"processing_transaction": "Traitement de la transaction...",
|
||||
"qr_code": "Code QR",
|
||||
"qr_failed": "Échec de la génération du code QR",
|
||||
"qr_title": "Code QR",
|
||||
@@ -571,6 +866,7 @@
|
||||
"receiving_addresses": "Vos adresses de réception",
|
||||
"recent_received": "REÇUS RÉCENTS",
|
||||
"recent_sends": "ENVOIS RÉCENTS",
|
||||
"recent_transactions": "TRANSACTIONS RÉCENTES",
|
||||
"recipient": "DESTINATAIRE",
|
||||
"recipient_balance": "Destinataire : %.8f → %.8f DRGX",
|
||||
"recv_type": "Reçu",
|
||||
@@ -578,6 +874,7 @@
|
||||
"refresh": "Actualiser",
|
||||
"refresh_now": "Actualiser maintenant",
|
||||
"remove_favorite": "Retirer des favoris",
|
||||
"repair_wallet": "Réparer le portefeuille",
|
||||
"report_bug": "Signaler un bug",
|
||||
"request_amount": "Montant (optionnel) :",
|
||||
"request_copy_uri": "Copier l'URI",
|
||||
@@ -593,12 +890,16 @@
|
||||
"request_transparent_addrs": "-- Adresses transparentes --",
|
||||
"request_uri_copied": "URI de paiement copiée dans le presse-papiers",
|
||||
"rescan": "Re-scanner",
|
||||
"rescan_bootstrapped_msg": "Votre nœud a été initialisé par bootstrap, les blocs situés en dessous de l'instantané ne sont donc pas sur le disque et un rescan depuis la genèse échouerait. Effectuez un rescan à partir d'une hauteur incluse dans votre instantané pour réconcilier le solde dépensé de votre portefeuille. Votre fichier wallet.dat et vos données de chaîne ne sont pas supprimés.",
|
||||
"rescan_detecting": "Vérification des blocs présents sur le disque de votre nœud…",
|
||||
"rescan_from_height": "Rescanner à partir de la hauteur de bloc :",
|
||||
"reset_to_defaults": "Réinitialiser les paramètres",
|
||||
"restarting_after_encryption": "Redémarrage du daemon après chiffrement...",
|
||||
"restore_address": "Restaurer l'adresse",
|
||||
"result_preview": "Aperçu du résultat",
|
||||
"retry": "Réessayer",
|
||||
"review_send": "Vérifier l'envoi",
|
||||
"rpc_connection": "Connexion RPC...",
|
||||
"rpc_host": "Hôte RPC",
|
||||
"rpc_pass": "Mot de passe",
|
||||
"rpc_port": "Port",
|
||||
@@ -608,12 +909,15 @@
|
||||
"save_z_transactions": "Enregistrer les Z-tx dans la liste",
|
||||
"sb_auth_failed": "Authentification échouée — vérifiez rpcuser/rpcpassword",
|
||||
"sb_block": "Bloc : %d",
|
||||
"sb_building_witnesses": "Configuration des témoins",
|
||||
"sb_building_witnesses_pct": "Reconstruction des témoins %.0f%%",
|
||||
"sb_connecting_daemon": "Connexion à dragonxd...",
|
||||
"sb_connecting_err": "Connexion au daemon — %s",
|
||||
"sb_connecting_external": "Connexion au daemon externe...",
|
||||
"sb_connecting_generic": "Connexion au daemon...",
|
||||
"sb_daemon_crashed": "Le daemon a planté %d fois",
|
||||
"sb_daemon_not_found": "Daemon introuvable",
|
||||
"sb_daemon_start_failed": "Impossible de démarrer dragonxd",
|
||||
"sb_dragonxd_running": "dragonxd en cours",
|
||||
"sb_dragonxd_stopped": "dragonxd arrêté",
|
||||
"sb_dragonxd_stopping": "Arrêt de dragonxd...",
|
||||
@@ -639,6 +943,12 @@
|
||||
"sb_waiting_daemon": "En attente de dragonxd...",
|
||||
"sb_waiting_daemon_err": "En attente de dragonxd — %s",
|
||||
"sb_warming_up": "Démarrage...",
|
||||
"sb_witness_cache": "Reconstruction des témoins",
|
||||
"screenshot_open_dir": "Ouvrir l'emplacement",
|
||||
"screenshot_sweep": "Lancer la capture d'écran",
|
||||
"screenshot_sweep_desc": "Parcourt chaque thème sur chaque onglet et enregistre une capture d'écran de chacun dans des sous-dossiers par onglet, sous le dossier screenshots du répertoire de configuration (en écrasant le balayage précédent). Dure quelques secondes.",
|
||||
"screenshot_sweep_full": "Balayage complet de l'interface",
|
||||
"search_icons": "Rechercher des icônes...",
|
||||
"search_placeholder": "Rechercher...",
|
||||
"security": "SÉCURITÉ",
|
||||
"seed_backup_button": "Phrase de récupération",
|
||||
@@ -655,6 +965,7 @@
|
||||
"seed_backup_saved": "Enregistré dans ",
|
||||
"seed_backup_title": "Sauvegarder la phrase de récupération",
|
||||
"seed_backup_warning": "Notez-les dans l'ordre, conservez-les hors ligne, et ne les partagez ni ne les photographiez jamais. Si vous les perdez, vos fonds ne pourront pas être récupérés.",
|
||||
"seed_migrate_button": "Migrer vers un portefeuille à phrase…",
|
||||
"select_address": "Sélectionner une adresse...",
|
||||
"select_receiving_address": "Sélectionner une adresse de réception...",
|
||||
"select_source_address": "Sélectionner une adresse source...",
|
||||
@@ -663,8 +974,10 @@
|
||||
"send_amount_details": "DÉTAILS DU MONTANT",
|
||||
"send_amount_upper": "MONTANT",
|
||||
"send_clear_fields": "Effacer tous les champs du formulaire ?",
|
||||
"send_contacts_button": "Choisir parmi les contacts",
|
||||
"send_copy_error": "Copier l'erreur",
|
||||
"send_dismiss": "Ignorer",
|
||||
"send_err_needs_rescan": "Les données des notes blindées de votre portefeuille ne sont plus à jour avec la blockchain (cela arrive après un bootstrap ou une réindexation). Lancez un rescan complet via Paramètres -> Re-scanner la blockchain et laissez-le se terminer entièrement, puis réessayez l'envoi.",
|
||||
"send_error_copied": "Erreur copiée dans le presse-papiers",
|
||||
"send_error_prefix": "Erreur : %s",
|
||||
"send_exceeds_available": "Dépasse le disponible (%.8f)",
|
||||
@@ -683,6 +996,7 @@
|
||||
"send_recipient": "DESTINATAIRE",
|
||||
"send_select_source": "Sélectionner une adresse source...",
|
||||
"send_sending_from": "ENVOI DEPUIS",
|
||||
"send_status_unconfirmed": "Le statut de la transaction n'a pas pu être confirmé",
|
||||
"send_submitting": "Soumission de la transaction...",
|
||||
"send_switch_to_receive": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
|
||||
"send_to": "Envoyer à",
|
||||
@@ -755,6 +1069,8 @@
|
||||
"settings_noise_opacity": "Opacité du bruit :",
|
||||
"settings_not_encrypted": "Non chiffré",
|
||||
"settings_not_found": "Non trouvé",
|
||||
"settings_open_app_dir": "Ouvrir le dossier de l'application",
|
||||
"settings_open_data_dir": "Ouvrir le dossier de données",
|
||||
"settings_other": "Autres",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "Confidentialité",
|
||||
@@ -788,16 +1104,24 @@
|
||||
"shield_check_status": "Vérifier le statut",
|
||||
"shield_completed": "Opération terminée avec succès !",
|
||||
"shield_description": "Blindez vos récompenses de minage en envoyant les sorties coinbase des adresses transparentes vers une adresse blindée. Cela améliore la confidentialité en masquant vos revenus de minage.",
|
||||
"shield_error_prefix": "Erreur : ",
|
||||
"shield_from_address": "Depuis l'adresse :",
|
||||
"shield_funds": "Blinder les fonds",
|
||||
"shield_in_progress": "Opération en cours...",
|
||||
"shield_max_utxos": "UTXOs max par opération",
|
||||
"shield_merge_done": "Blindage/fusion terminé !",
|
||||
"shield_op_failed": "Échec de l'opération : ",
|
||||
"shield_op_submitted": "Opération soumise : ",
|
||||
"shield_operation_id": "ID d'opération : %s",
|
||||
"shield_select_z": "Sélectionner une z-adresse...",
|
||||
"shield_send_failed": "Échec du blindage : ",
|
||||
"shield_started": "Opération de blindage démarrée",
|
||||
"shield_status_check_error": "Erreur lors de la vérification du statut : ",
|
||||
"shield_status_label": "Statut : ",
|
||||
"shield_submitting": "Envoi de l'opération...",
|
||||
"shield_title": "Blinder les récompenses coinbase",
|
||||
"shield_to_address": "Vers l'adresse (blindée) :",
|
||||
"shield_unknown_error": "Erreur inconnue",
|
||||
"shield_utxo_limit": "Limite UTXO :",
|
||||
"shield_wildcard_hint": "Utilisez '*' pour blinder depuis toutes les adresses transparentes",
|
||||
"shielded": "Blindé",
|
||||
@@ -807,11 +1131,16 @@
|
||||
"shielding_notice": "Note : Cela blindera des fonds d'une adresse transparente (T) vers une adresse privée (Z).",
|
||||
"show": "Afficher",
|
||||
"show_hidden": "Afficher masqués (%d)",
|
||||
"show_qr": "Afficher le QR",
|
||||
"show_qr_code": "Afficher le code QR",
|
||||
"showing_transactions": "Affichage %d–%d sur %d transactions (total : %zu)",
|
||||
"showing_x_of_y": "Affichage de %d sur %d adresses",
|
||||
"simple_background": "Arrière-plan simple",
|
||||
"slider_off": "Désactivé",
|
||||
"sort_amount_high": "Montant le plus élevé",
|
||||
"sort_amount_low": "Plus petit montant",
|
||||
"sort_date_newest": "Plus récent d'abord",
|
||||
"sort_date_oldest": "Plus ancien d'abord",
|
||||
"start_mining": "Démarrer le minage",
|
||||
"status": "Statut",
|
||||
"stop_external": "Arrêter le daemon externe",
|
||||
@@ -866,6 +1195,8 @@
|
||||
"tt_clear_ztx": "Supprimer l'historique des z-transactions mis en cache localement",
|
||||
"tt_custom_fees": "Activer la saisie manuelle des frais lors de l'envoi de transactions",
|
||||
"tt_custom_theme": "Thème personnalisé actif",
|
||||
"tt_daemon_install_bundled": "Arrêter le nœud, remplacer le dragonxd installé par la version intégrée dans cette version du portefeuille, puis redémarrer",
|
||||
"tt_daemon_update_check": "Télécharger et vérifier le dernier nœud complet dragonxd depuis le Gitea du projet, puis redémarrer pour l'appliquer",
|
||||
"tt_debug_collapse": "Réduire les options de journalisation de débogage",
|
||||
"tt_debug_expand": "Développer les options de journalisation de débogage",
|
||||
"tt_delete_blockchain": "Supprimer toutes les données de la blockchain et démarrer une nouvelle synchronisation. wallet.dat et la configuration sont préservés.",
|
||||
@@ -881,15 +1212,19 @@
|
||||
"tt_keep_daemon": "Le daemon s'arrêtera lors de l'exécution de l'assistant de configuration",
|
||||
"tt_language": "Langue de l'interface du portefeuille",
|
||||
"tt_layout_hotkey": "Raccourci : touches fléchées gauche/droite pour changer les dispositions de Balance",
|
||||
"tt_lite_redownload": "Re-télécharger et re-scanner tous les blocs depuis le serveur lite",
|
||||
"tt_lock": "Verrouiller le portefeuille immédiatement",
|
||||
"tt_low_spec": "Désactiver tous les effets visuels lourds\\nRaccourci : Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolider plusieurs UTXOs vers une adresse",
|
||||
"tt_mine_idle": "Démarrer le minage automatiquement quand le\\nsystème est inactif (aucune entrée clavier/souris)",
|
||||
"tt_noise": "Intensité de texture grainée (0%% = désactivé, 100%% = maximum)",
|
||||
"tt_open_app_dir": "Ouvrir le dossier ObsidianDragon (paramètres, thèmes, journaux) dans le gestionnaire de fichiers",
|
||||
"tt_open_data_dir": "Ouvrir le dossier contenant les données de votre portefeuille et de la blockchain dans le gestionnaire de fichiers",
|
||||
"tt_open_dir": "Cliquer pour ouvrir dans l'explorateur de fichiers",
|
||||
"tt_reduce_motion": "Désactiver les transitions animées et le lerp de solde pour l'accessibilité",
|
||||
"tt_remove_encrypt": "Supprimer le chiffrement et stocker le portefeuille sans protection",
|
||||
"tt_remove_pin": "Supprimer le PIN et exiger la phrase secrète pour déverrouiller",
|
||||
"tt_repair_wallet": "Effacer et reconstruire les enregistrements de transactions du portefeuille depuis la blockchain (corrige les notes qui échouent à l'envoi après un rescan)",
|
||||
"tt_report_bug": "Signaler un problème dans le suivi de projet",
|
||||
"tt_request_payment": "Générer une demande de paiement avec code QR",
|
||||
"tt_rescan": "Rescanner la blockchain pour les transactions manquantes",
|
||||
@@ -904,6 +1239,7 @@
|
||||
"tt_scan_themes": "Rechercher de nouveaux thèmes.\\nPlacez les dossiers de thèmes dans :\\n%s",
|
||||
"tt_scanline": "Effet de lignes de balayage CRT dans la console",
|
||||
"tt_seed_backup": "Afficher et sauvegarder la phrase de récupération de 24 mots de votre portefeuille",
|
||||
"tt_seed_migrate": "Créer un nouveau portefeuille à phrase de récupération et y transférer vos fonds",
|
||||
"tt_set_pin": "Définir un PIN de 4-8 chiffres pour un déverrouillage rapide",
|
||||
"tt_shield_mining": "Déplacer les récompenses de minage transparentes vers une adresse blindée",
|
||||
"tt_simple_bg": "Utiliser un dégradé simple pour l'arrière-plan\\nRaccourci : Ctrl+Up",
|
||||
@@ -917,6 +1253,7 @@
|
||||
"tt_ui_opacity": "Opacité des cartes et de la barre latérale (100%% = entièrement opaque, plus bas = plus transparent)",
|
||||
"tt_validate": "Vérifier si une adresse DragonX est valide",
|
||||
"tt_verbose": "Journaliser les diagnostics de connexion détaillés,\\nl'état du daemon et les informations de propriétaire de port\\ndans l'onglet Console",
|
||||
"tt_wallets_button": "Répertoriez vos fichiers de portefeuille et passez de l'un à l'autre",
|
||||
"tt_website": "Ouvrir le site web DragonX",
|
||||
"tt_window_opacity": "Opacité de l'arrière-plan (plus bas = bureau visible à travers la fenêtre)",
|
||||
"tt_wizard": "Relancer l'assistant de configuration initiale\\nLe daemon sera redémarré",
|
||||
@@ -925,7 +1262,18 @@
|
||||
"tx_from_address": "Adresse d'origine :",
|
||||
"tx_id_label": "ID de transaction :",
|
||||
"tx_immature": "IMMATURE",
|
||||
"tx_loading_enriching_sends": "Vérification des détails des transactions envoyées (%d)",
|
||||
"tx_loading_fetching_transparent": "Récupération de l'historique transparent",
|
||||
"tx_loading_history_progress": "Chargement de l'historique plus ancien (%d%%)",
|
||||
"tx_loading_queued": "Actualisation des transactions en file d'attente",
|
||||
"tx_loading_refreshing_cached": "Actualisation de l'historique du portefeuille (%d en cache)",
|
||||
"tx_loading_scanning_shielded": "Analyse de l'historique blindé (%d adresses)",
|
||||
"tx_mined": "MINÉ",
|
||||
"tx_progress_balances": "Actualisation des soldes",
|
||||
"tx_progress_finalizing": "Finalisation de la transaction",
|
||||
"tx_progress_history": "Actualisation de l'historique",
|
||||
"tx_progress_submitting": "Soumission de la transaction au daemon",
|
||||
"tx_progress_waiting_ops": "En attente de l'opération (%d)",
|
||||
"tx_received": "REÇU",
|
||||
"tx_sent": "ENVOYÉ",
|
||||
"tx_to_address": "Adresse de destination :",
|
||||
@@ -937,6 +1285,14 @@
|
||||
"unconfirmed": "Non confirmé",
|
||||
"undo_clear": "Annuler l'effacement",
|
||||
"unknown": "Inconnu",
|
||||
"unmark_mining_address": "Retirer la marque d'adresse de minage",
|
||||
"upd_back": "Retour",
|
||||
"upd_install": "Installer",
|
||||
"upd_installed_badge": "installé",
|
||||
"upd_no_build_platform": "Aucune version pour cette plateforme",
|
||||
"upd_prerelease": "pré-version",
|
||||
"upd_reinstall": "Réinstaller",
|
||||
"upd_select_version": "Sélectionnez une version à installer",
|
||||
"use_embedded_daemon": "Utiliser le dragonxd intégré",
|
||||
"use_tor": "Utiliser Tor",
|
||||
"validate_btn": "Valider",
|
||||
@@ -956,17 +1312,73 @@
|
||||
"verbose_logging": "Journalisation détaillée",
|
||||
"version": "Version",
|
||||
"view": "Afficher",
|
||||
"view_all": "Tout afficher",
|
||||
"view_details": "Voir les détails",
|
||||
"view_on_explorer": "Voir dans l'explorateur",
|
||||
"waiting_for_daemon": "En attente de la connexion au daemon...",
|
||||
"wallet": "PORTEFEUILLE",
|
||||
"wallet_empty": "Votre portefeuille est vide",
|
||||
"wallet_empty_hint": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
|
||||
"wallets_active": "Actif",
|
||||
"wallets_add": "Ajouter le dossier",
|
||||
"wallets_add_folder": "Analyser aussi un autre dossier à la recherche de fichiers de portefeuille :",
|
||||
"wallets_add_folder_toggle": "+ Analyser un autre dossier pour les portefeuilles…",
|
||||
"wallets_button": "Portefeuilles…",
|
||||
"wallets_col_addresses": "Adresses",
|
||||
"wallets_col_balance": "Solde",
|
||||
"wallets_col_name": "Portefeuille",
|
||||
"wallets_col_opened": "Dernière ouverture",
|
||||
"wallets_col_size": "Taille",
|
||||
"wallets_create": "Créer le portefeuille",
|
||||
"wallets_creating": "Création du portefeuille — le nœud va redémarrer…",
|
||||
"wallets_current": "actuel",
|
||||
"wallets_exists": "Un portefeuille portant ce nom existe déjà.",
|
||||
"wallets_external_tt": "Hors de votre répertoire de données — Importer pour le copier.",
|
||||
"wallets_folder_hint": "/chemin/vers/le/dossier contenant les fichiers .dat",
|
||||
"wallets_folder_invalid": "Ce dossier n'existe pas.",
|
||||
"wallets_import": "Importer",
|
||||
"wallets_import_failed": "Impossible d'importer ce fichier de portefeuille.",
|
||||
"wallets_import_hint": "Importer pour ouvrir",
|
||||
"wallets_import_tt": "Ce portefeuille se trouve hors du répertoire de données. L'ouvrir le copiera d'abord dedans (bientôt disponible).",
|
||||
"wallets_imported": "Portefeuille importé — changement en cours…",
|
||||
"wallets_intro": "Fichiers de portefeuille dans votre répertoire de données (et tout dossier que vous ajoutez). Ouvrez-en un pour changer — le nœud redémarre pour le charger, et chaque portefeuille conserve ses propres données.",
|
||||
"wallets_name_invalid": "Veuillez saisir un nom de portefeuille valide.",
|
||||
"wallets_never": "Jamais",
|
||||
"wallets_new_hint": "Nom (par ex. épargne)",
|
||||
"wallets_new_label": "Créer un nouveau portefeuille :",
|
||||
"wallets_open": "Ouvrir",
|
||||
"wallets_reveal": "Afficher le dossier",
|
||||
"wallets_title": "Portefeuilles",
|
||||
"warning": "Attention",
|
||||
"warning_upper": "ATTENTION !",
|
||||
"website": "Site web",
|
||||
"window_opacity": "Opacité de la fenêtre",
|
||||
"wizard_daemon_start_failed": "Échec du démarrage du daemon — sera réessayé automatiquement",
|
||||
"xmrig_browse_releases": "Parcourir toutes les versions…",
|
||||
"xmrig_checking": "Recherche du dernier mineur…",
|
||||
"xmrig_current": "Actuel :",
|
||||
"xmrig_download_install": "Télécharger et installer",
|
||||
"xmrig_downloading": "Téléchargement…",
|
||||
"xmrig_installed": "Installé :",
|
||||
"xmrig_installed_ok": "Mineur installé",
|
||||
"xmrig_installing": "Installation…",
|
||||
"xmrig_latest": "Dernière :",
|
||||
"xmrig_loading_releases": "Chargement des versions…",
|
||||
"xmrig_none": "aucun",
|
||||
"xmrig_reinstall": "Réinstaller",
|
||||
"xmrig_stop_mining_first": "Arrêtez le minage avant de mettre à jour le mineur.",
|
||||
"xmrig_unavailable_body": "Aucune version du mineur n'est disponible pour cette plateforme.",
|
||||
"xmrig_unavailable_title": "Mises à jour du mineur indisponibles",
|
||||
"xmrig_unknown_error": "Erreur inconnue.",
|
||||
"xmrig_up_to_date": "Le mineur est à jour",
|
||||
"xmrig_update_available": "Un nouveau mineur est disponible",
|
||||
"xmrig_update_button": "Mettre à jour le mineur…",
|
||||
"xmrig_update_failed": "Échec de la mise à jour",
|
||||
"xmrig_update_short": "Mettre à jour",
|
||||
"xmrig_update_title": "Mettre à jour le mineur",
|
||||
"xmrig_verify_note": "Le téléchargement est vérifié par rapport à la somme de contrôle SHA-256 publiée de la version avant l'installation.",
|
||||
"xmrig_verifying": "Vérification…",
|
||||
"xmrig_version": "Version :",
|
||||
"yes_clear": "Oui, effacer",
|
||||
"your_addresses": "Vos adresses",
|
||||
"z_address": "Adresse Z",
|
||||
|
||||
412
res/lang/ja.json
412
res/lang/ja.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "アドレスを追加",
|
||||
"address_book_add_new": "新規追加",
|
||||
"address_book_added": "アドレスをアドレス帳に追加しました",
|
||||
"address_book_confirm_delete": "削除しますか?",
|
||||
"address_book_count": "%zu 件のアドレスを保存済み",
|
||||
"address_book_deleted": "エントリを削除しました",
|
||||
"address_book_edit": "アドレスを編集",
|
||||
@@ -39,6 +40,7 @@
|
||||
"address_copied": "アドレスをクリップボードにコピーしました",
|
||||
"address_details": "アドレス詳細",
|
||||
"address_label": "アドレス:",
|
||||
"address_reorder_hint": "行の端にドロップで並べ替え、中央にドロップで移動します",
|
||||
"address_upper": "アドレス",
|
||||
"address_url": "アドレスURL",
|
||||
"addresses_appear_here": "接続後、受信アドレスがここに表示されます。",
|
||||
@@ -69,7 +71,10 @@
|
||||
"backup_wallet": "ウォレットをバックアップ...",
|
||||
"backup_wallet_not_found": "警告:予想される場所にwallet.datが見つかりません",
|
||||
"balance": "残高",
|
||||
"balance_history_collecting": "残高履歴 — データを収集中...",
|
||||
"balance_layout": "残高レイアウト",
|
||||
"balance_shielded_fmt": "シールド: %.8f",
|
||||
"balance_transparent_fmt": "透明: %.8f",
|
||||
"ban": "ブロック",
|
||||
"banned_peers": "ブロック済みピア",
|
||||
"block": "ブロック",
|
||||
@@ -151,6 +156,20 @@
|
||||
"confirm_delete_blockchain_msg": "デーモンを停止し、すべてのブロックチェーンデータ(blocks、chainstate、peers)を削除して、最初から再同期を開始します。数時間かかる場合があります。",
|
||||
"confirm_delete_blockchain_safe": "wallet.dat、設定、トランザクション履歴は安全で削除されません。",
|
||||
"confirm_delete_blockchain_title": "ブロックチェーンデータを削除",
|
||||
"confirm_lite_redownload_msg": "これにより、ウォレットにダウンロード済みのブロックデータがクリアされ、ライトサーバーからすべてのブロックを再取得して再スキャンします。しばらく時間がかかる場合があり、完了するまでウォレットに同期の進行状況が表示されます。",
|
||||
"confirm_lite_redownload_safe": "ウォレット、鍵、シードには影響しません — 再ダウンロードされるのはブロックデータのみです。",
|
||||
"confirm_lite_redownload_title": "ブロックを再ダウンロード",
|
||||
"confirm_reinstall_daemon_msg": "デーモンを停止し、インストール済みの dragonxd(および dragonx-cli/dragonx-tx)をこのウォレットビルドにバンドルされているバージョンで上書きしてから、ノードを再起動します。ノードのバイナリを復旧または更新する場合に使用してください。",
|
||||
"confirm_reinstall_daemon_safe": "ウォレット、鍵、ブロックチェーンデータには影響しません — 置き換えられるのはデーモンのプログラムファイルのみです。",
|
||||
"confirm_reinstall_daemon_title": "バンドル版デーモンをインストール",
|
||||
"confirm_repair_wallet_msg": "デーモンを -zapwallettxes=2 で再起動します:ウォレットのすべての取引およびノート記録を削除し、その後ブロックチェーンから再構築します。完全な再スキャン後でも取引の作成に失敗する(「Invalid sapling spend proof」/「shielded requirements not met」)場合に使用してください。長い時間がかかり、完了するまでウォレットはオフラインのままになります。",
|
||||
"confirm_repair_wallet_safe": "鍵、アドレス、残高は保持されます — 再構築されるのはキャッシュされたトランザクション記録のみです。",
|
||||
"confirm_repair_wallet_title": "ウォレットを修復",
|
||||
"confirm_rescan_msg": "デーモンを再起動し、ウォレットのトランザクションを探してブロックチェーン全体を再スキャンします。長時間かかる場合があり、完了するまでウォレットはオフラインのままになります。",
|
||||
"confirm_rescan_safe": "wallet.dat とブロックチェーンデータは削除されません — 再スキャンされるだけです。",
|
||||
"confirm_rescan_title": "ブロックチェーンを再スキャン",
|
||||
"confirm_restart_daemon_msg": "変更したオプションを適用するため、デーモンを停止して再起動します。ウォレットは一時的に切断され、再接続します。",
|
||||
"confirm_restart_daemon_title": "デーモンを再起動",
|
||||
"confirm_send": "送金を確認",
|
||||
"confirm_transaction": "取引を確認",
|
||||
"confirm_transfer": "送金を確認",
|
||||
@@ -161,6 +180,7 @@
|
||||
"connected_peers": "接続中のピア",
|
||||
"connecting": "接続中...",
|
||||
"console": "コンソール",
|
||||
"console_app": "アプリ",
|
||||
"console_auto_scroll": "自動スクロール",
|
||||
"console_available_commands": "利用可能なコマンド:",
|
||||
"console_capturing_output": "デーモン出力をキャプチャ中...",
|
||||
@@ -177,6 +197,7 @@
|
||||
"console_connected": "デーモンに接続済み",
|
||||
"console_copy_all": "すべてコピー",
|
||||
"console_copy_selected": "コピー",
|
||||
"console_copy_value": "コピー",
|
||||
"console_daemon": "デーモン",
|
||||
"console_daemon_error": "デーモンエラー!",
|
||||
"console_daemon_started": "デーモンが起動しました",
|
||||
@@ -195,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - マイニングを制御",
|
||||
"console_help_stop": " stop - デーモンを停止",
|
||||
"console_line_count": "%zu 行",
|
||||
"console_matches": "件一致",
|
||||
"console_new_lines": "%d 新しい行",
|
||||
"console_no_daemon": "デーモンなし",
|
||||
"console_not_connected": "エラー:デーモンに接続されていません",
|
||||
"console_quit_note": "ここでは 'quit'/'exit' は不要です — ウィンドウを閉じるだけで構いません。",
|
||||
"console_rpc_reference": "RPCコマンドリファレンス",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "コンソールスキャンライン",
|
||||
"console_search_commands": "コマンドを検索...",
|
||||
"console_select_all": "すべて選択",
|
||||
"console_show_app_output": "[app] ウォレットのログ行を表示",
|
||||
"console_show_daemon_output": "デーモン出力を表示",
|
||||
"console_show_errors_only": "エラーのみ表示",
|
||||
"console_show_rpc_ref": "RPCコマンドリファレンスを表示",
|
||||
"console_show_rpc_trace": "アプリのRPC呼び出しを表示",
|
||||
"console_showing_lines": "%zu / %zu 行を表示中",
|
||||
"console_starting_node": "ノードを起動中...",
|
||||
"console_status_error": "エラー",
|
||||
@@ -218,6 +244,12 @@
|
||||
"console_welcome": "ObsidianDragonコンソールへようこそ",
|
||||
"console_zoom_in": "拡大",
|
||||
"console_zoom_out": "縮小",
|
||||
"contact_global": "すべてのウォレットで表示(グローバル連絡先)",
|
||||
"contact_global_badge_tt": "グローバル連絡先 — すべてのウォレットで表示",
|
||||
"contact_global_tt": "オン:この連絡先はどのウォレットを読み込んでも表示されます。オフ:現在のウォレットにのみ属します。",
|
||||
"contacts": "連絡先",
|
||||
"contacts_search_no_match": "一致する連絡先がありません",
|
||||
"contacts_search_placeholder": "連絡先を検索...",
|
||||
"copied": "コピーしました!",
|
||||
"copy": "コピー",
|
||||
"copy_address": "完全なアドレスをコピー",
|
||||
@@ -227,6 +259,44 @@
|
||||
"copy_uri": "URIをコピー",
|
||||
"current_price": "現在の価格",
|
||||
"custom_fees": "カスタム手数料",
|
||||
"daemon_binary": "デーモンバイナリ",
|
||||
"daemon_bundled": "バンドル版",
|
||||
"daemon_install_bundled": "バンドル版をインストール",
|
||||
"daemon_installed": "インストール済み",
|
||||
"daemon_none_bundled": "このビルドにはなし",
|
||||
"daemon_not_installed": "未インストール",
|
||||
"daemon_status_differ": "インストール済みのバイナリはバンドル版と異なります。",
|
||||
"daemon_status_match": "インストール済みのバイナリはバンドル版と一致しています。",
|
||||
"daemon_status_missing": "デーモンがインストールされていません — バンドル版をインストールしてください。",
|
||||
"daemon_update_available": "新しいノードバージョンが利用可能です",
|
||||
"daemon_update_body": "このウォレットビルドには、現在インストールされているものよりも新しいDragonXノードがバンドルされています。更新すると、インストール済みのdragonxd(およびdragonx-cli/dragonx-tx)が置き換えられ、その後ノードを停止して再起動し、新しいバージョンが有効になります。新しいノードには、古いものにない機能(例:シードフレーズのサポート)が追加されている場合があるため、更新を推奨します。",
|
||||
"daemon_update_browse": "すべてのリリースを表示…",
|
||||
"daemon_update_check": "更新を確認…",
|
||||
"daemon_update_checking": "最新ノードを確認中…",
|
||||
"daemon_update_downgrade_note": "古いバージョンは現在のチェーンデータと互換性がない場合があります。別のバージョンをインストールすると、デーモンの再起動後に有効になります。",
|
||||
"daemon_update_download_install": "ダウンロードしてインストール",
|
||||
"daemon_update_downloading": "ダウンロード中…",
|
||||
"daemon_update_failed": "更新に失敗しました",
|
||||
"daemon_update_installed": "インストール済み:",
|
||||
"daemon_update_installed_ok": "ノードをインストールしました",
|
||||
"daemon_update_installing": "インストール中…",
|
||||
"daemon_update_keep": "現在のまま使用",
|
||||
"daemon_update_later": "後で",
|
||||
"daemon_update_latest": "最新:",
|
||||
"daemon_update_loading": "リリースを読み込み中…",
|
||||
"daemon_update_now": "今すぐ更新",
|
||||
"daemon_update_reinstall": "再インストール",
|
||||
"daemon_update_restart_note": "新しいバージョンを実行するにはデーモンを再起動してください。",
|
||||
"daemon_update_restart_now": "今すぐデーモンを再起動",
|
||||
"daemon_update_safe": "ウォレット、鍵、ブロックチェーンデータには影響しません — 置き換えられるのはデーモンのプログラムファイルのみです。意図的にカスタムノードを実行している場合は、「現在のまま使用」を選択してください。",
|
||||
"daemon_update_title": "ノードデーモンを更新しますか?",
|
||||
"daemon_update_unavailable_body": "このプラットフォーム向けのノードビルドはありません。",
|
||||
"daemon_update_unavailable_title": "ノードの更新は利用できません",
|
||||
"daemon_update_unknown_error": "不明なエラーです。",
|
||||
"daemon_update_up_to_date": "ノードは最新です",
|
||||
"daemon_update_verify_note": "ダウンロードは、インストール前にリリースで公開された SHA-256 と固定された ed25519 署名で検証されます。",
|
||||
"daemon_update_verifying": "検証中…",
|
||||
"daemon_update_version": "バージョン:",
|
||||
"daemon_version": "デーモン",
|
||||
"dark": "ダーク",
|
||||
"date": "日付",
|
||||
@@ -257,10 +327,14 @@
|
||||
"explorer_block_time": "時刻",
|
||||
"explorer_block_txs": "トランザクション",
|
||||
"explorer_chain_stats": "チェーン",
|
||||
"explorer_hash_not_found": "このハッシュに対応するブロックまたはトランザクションが見つかりません",
|
||||
"explorer_invalid_query": "ブロック高さまたは64文字のハッシュを入力してください",
|
||||
"explorer_invalid_response": "デーモンからの応答が無効です",
|
||||
"explorer_mempool": "メモリプール",
|
||||
"explorer_mempool_size": "サイズ",
|
||||
"explorer_mempool_txs": "トランザクション",
|
||||
"explorer_no_results": "一致するキャッシュ済みブロックがありません",
|
||||
"explorer_not_connected": "デーモンに未接続 — ブロックまたはトランザクションハッシュを検索できません",
|
||||
"explorer_recent_blocks": "最近のブロック",
|
||||
"explorer_search": "検索",
|
||||
"explorer_section": "エクスプローラー",
|
||||
@@ -312,6 +386,7 @@
|
||||
"hidden_tag": " (非表示)",
|
||||
"hide": "非表示",
|
||||
"hide_address": "アドレスを非表示",
|
||||
"hide_qr": "QRを非表示",
|
||||
"hide_zero_balances": "残高0を非表示",
|
||||
"history": "履歴",
|
||||
"immature_type": "未成熟",
|
||||
@@ -350,19 +425,120 @@
|
||||
"label_placeholder": "例: 貯金、マイニング...",
|
||||
"language": "言語",
|
||||
"light": "ライト",
|
||||
"lite_account_label": "アカウント",
|
||||
"lite_action": "アクション",
|
||||
"lite_backup_keys": "バックアップと鍵",
|
||||
"lite_birthday_backup": "誕生日:%llu (これもバックアップしてください)",
|
||||
"lite_birthday_hint": "スキャンを開始するブロック高。不明な場合は0のままにしてください(完全スキャンが遅くなります)。",
|
||||
"lite_birthday_label": "バースデー",
|
||||
"lite_console_help_passthrough": "その他の入力はライトウォレットのコンソールコマンドとして実行されます。",
|
||||
"lite_copy": "コピー",
|
||||
"lite_could_not_write": "書き込めませんでした ",
|
||||
"lite_encrypt_wallet": "ウォレットを暗号化",
|
||||
"lite_encryption_removed": "暗号化を解除しました",
|
||||
"lite_hide_wipe": "非表示にして消去",
|
||||
"lite_import": "インポート",
|
||||
"lite_import_key_label": "鍵をインポート",
|
||||
"lite_key_imported": "鍵をインポートしました — 履歴をスキャンするには同期を実行してください",
|
||||
"lite_lock_failed": "ロックに失敗しました",
|
||||
"lite_lock_now": "今すぐロック",
|
||||
"lite_maintenance": "メンテナンス",
|
||||
"lite_net_add": "追加",
|
||||
"lite_net_add_label_hint": "ラベル(任意)",
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "確認中…",
|
||||
"lite_net_connected": "接続済み",
|
||||
"lite_net_custom": "カスタム",
|
||||
"lite_net_disconnected": "未接続",
|
||||
"lite_net_hidden_section": "非表示のサーバー",
|
||||
"lite_net_hide": "非表示",
|
||||
"lite_net_in_use": "使用中",
|
||||
"lite_net_intro": "使用するサーバーを選ぶか、ウォレットにランダムで選ばせることができます。変更はすぐに反映されます。",
|
||||
"lite_net_invalid_url": "有効な http(s):// URL を入力してください。",
|
||||
"lite_net_no_wallet": "ウォレットが開かれていません",
|
||||
"lite_net_official": "公式",
|
||||
"lite_net_offline": "オフライン",
|
||||
"lite_net_random_active": "ランダムなサーバー選択が有効です。",
|
||||
"lite_net_random_server": "ランダムサーバー",
|
||||
"lite_net_refresh": "更新",
|
||||
"lite_net_show_hidden": "非表示のサーバーを表示",
|
||||
"lite_net_sync_label": "同期",
|
||||
"lite_net_synced": "同期済み",
|
||||
"lite_net_syncing": "同期中",
|
||||
"lite_net_title": "Liteサーバー",
|
||||
"lite_net_unhide": "再表示",
|
||||
"lite_net_use_random": "毎回ランダムなサーバーを使用",
|
||||
"lite_no_wallet": "開いているウォレットがありません — 設定で作成または開いてください",
|
||||
"lite_no_wallet_short": "ウォレットが開かれていません",
|
||||
"lite_op_create": "作成",
|
||||
"lite_op_open": "開く",
|
||||
"lite_op_restore": "復元",
|
||||
"lite_overwrite": "上書き",
|
||||
"lite_passphrase_label": "パスフレーズ",
|
||||
"lite_private_keys_warning": "秘密鍵 — これらを持つ者は誰でもあなたの資金を送金できます",
|
||||
"lite_redownload_blocks": "ブロックを再ダウンロード",
|
||||
"lite_redownload_desc": "ライトサーバーからすべてのブロックを再取得します(残高や履歴がおかしい場合に使用)。",
|
||||
"lite_redownload_running": "ブロックを再ダウンロード中…",
|
||||
"lite_remove_encryption": "暗号化を解除",
|
||||
"lite_restore_birthday_label": "バースデーブロック(任意 — 0で最初からスキャンし、遅くなります)",
|
||||
"lite_restore_btn": "ウォレットを復元",
|
||||
"lite_restore_intro": "復元用シードフレーズを入力してください。ウォレットが復元され、その後ライトサーバーから同期されます。",
|
||||
"lite_restore_ok": "ウォレットを復元しました — ライトサーバーから同期中…",
|
||||
"lite_restore_seed_label": "シードフレーズ(単語をスペースで区切る)",
|
||||
"lite_restore_title": "シードフレーズから復元",
|
||||
"lite_save_to_file": "ファイルに保存",
|
||||
"lite_saved_to": "保存しました(平文、所有者のみ):",
|
||||
"lite_security": "セキュリティ",
|
||||
"lite_seed_label": "シード",
|
||||
"lite_seed_warning": "シードフレーズ — ウォレットを復元する唯一の方法です。書き留めてオフラインで保管し、決して共有しないでください。",
|
||||
"lite_servers_network_tab": "ライトサーバーはネットワークタブで管理します。",
|
||||
"lite_show_private_keys": "秘密鍵を表示",
|
||||
"lite_show_seed": "シードを表示",
|
||||
"lite_unlock": "ロック解除",
|
||||
"lite_unlock_btn": "ロック解除",
|
||||
"lite_unlock_failed": "アンロックに失敗しました — パスフレーズが間違っていませんか?",
|
||||
"lite_unlock_msg": "送金のためにウォレットのロックを解除するには、パスフレーズを入力してください。",
|
||||
"lite_unlock_ok": "ウォレットのロックを解除しました — 送金できます",
|
||||
"lite_unlock_title": "ウォレットのロックを解除",
|
||||
"lite_validate": "検証",
|
||||
"lite_wallet_encrypted": "ウォレットを暗号化しました",
|
||||
"lite_wallet_label": "ウォレット",
|
||||
"lite_wallet_locked": "ウォレットはロックされています",
|
||||
"lite_wallet_ready": "ウォレット準備完了",
|
||||
"lite_wallet_request": "Liteウォレットのリクエスト",
|
||||
"lite_wallet_unlocked": "ウォレットのロックを解除しました",
|
||||
"lite_welcome_create": "新しいウォレットを作成",
|
||||
"lite_welcome_create_failed": "ウォレットを作成できませんでした",
|
||||
"lite_welcome_created": "ウォレットを作成しました — 今すぐ 設定 → バックアップと鍵 で復元フレーズをバックアップしてください",
|
||||
"lite_welcome_later": "後で",
|
||||
"lite_welcome_msg": "まだウォレットがありません。新規に作成するか、復元フレーズから復元してください。",
|
||||
"lite_welcome_restore": "シードから復元",
|
||||
"lite_welcome_restore_hint": "設定 → ライトウォレットリクエスト でウォレットを復元してください",
|
||||
"lite_welcome_title": "ObsidianDragon Lite へようこそ",
|
||||
"lite_word_count": "%d / 24 単語",
|
||||
"lite_working": "処理中…",
|
||||
"loading": "読み込み中...",
|
||||
"loading_addresses": "アドレスを読み込み中...",
|
||||
"loading_transactions": "トランザクションを読み込み中",
|
||||
"local_hashrate": "ローカルハッシュレート",
|
||||
"low_spec_mode": "省電力モード",
|
||||
"mark_mining_address": "マイニングアドレスとしてマーク",
|
||||
"market": "市場",
|
||||
"market_12h": "12時間",
|
||||
"market_18h": "18時間",
|
||||
"market_24h": "24時間",
|
||||
"market_24h_change": "24時間",
|
||||
"market_24h_volume": "24時間出来高",
|
||||
"market_6h": "6時間",
|
||||
"market_attribution": "価格データ:NonKYC提供",
|
||||
"market_btc_price": "BTC価格",
|
||||
"market_cap": "時価総額",
|
||||
"market_cap_short": "時価総額",
|
||||
"market_iv_1d": "1日",
|
||||
"market_iv_1h": "1時間",
|
||||
"market_iv_1m": "1ヶ月",
|
||||
"market_iv_1w": "1週",
|
||||
"market_iv_live": "ライブ",
|
||||
"market_no_history": "価格履歴がありません",
|
||||
"market_no_price": "価格データなし",
|
||||
"market_now": "現在",
|
||||
@@ -372,6 +548,8 @@
|
||||
"market_price_unavailable": "価格データが利用できません",
|
||||
"market_refresh_price": "価格データを更新",
|
||||
"market_trade_on": "%s で取引",
|
||||
"market_updated": "\\xc2\\xb7 更新: %s",
|
||||
"market_vol_short": "出来高",
|
||||
"mature": "成熟済み",
|
||||
"max": "最大",
|
||||
"memo": "メモ(任意、暗号化)",
|
||||
@@ -381,8 +559,52 @@
|
||||
"memo_z_only": "注:メモはシールド (z) アドレスへの送金時のみ利用可能です",
|
||||
"merge_description": "複数のUTXOを単一のシールドアドレスに統合します。ウォレットサイズの縮小とプライバシーの向上に役立ちます。",
|
||||
"merge_funds": "資金を統合",
|
||||
"merge_send_failed": "マージに失敗しました: ",
|
||||
"merge_started": "統合操作を開始しました",
|
||||
"merge_title": "アドレスに統合",
|
||||
"mig_adopt": "これを自分のウォレットにする",
|
||||
"mig_adopt_now": "シードウォレットを採用",
|
||||
"mig_adopting": "新しいウォレットをインストールして再スキャン中…",
|
||||
"mig_adopting_note": "ノードは新しいウォレットで再起動し、チェーンを再スキャンします — これには数分かかる場合があります。ウォレットは実行したままにしてください。自動的に再接続します。",
|
||||
"mig_already_mnemonic": "このウォレットにはすでに24単語のシードフレーズがあります — 移行するものはありません。バックアップして、単語を安全な場所に保管してください。",
|
||||
"mig_backed_up": "シードフレーズを書き写しました",
|
||||
"mig_backup_instead": "シードフレーズをバックアップ",
|
||||
"mig_balance": "現在のウォレット残高:%.8f DRGX(少額の手数料を除く)",
|
||||
"mig_check_failed": "ウォレットを確認できませんでした。ロックが解除され、接続されていることを確認してから、再確認してください。",
|
||||
"mig_checking_balance": "残高を確認中",
|
||||
"mig_confirming": "ウォレットを切り替える前に、スイープがオンチェーンで確認されるのを待っています — これにより資金が実際に移動したことが保証されます。",
|
||||
"mig_confs": "スイープの確認数:%d",
|
||||
"mig_continue_sweep": "スイープに進む",
|
||||
"mig_copy_seed": "シードをコピー",
|
||||
"mig_create": "シードウォレットを作成",
|
||||
"mig_daemon_too_old": "DragonXノードが古すぎるため、シードウォレットを作成できません — これに必要なニーモニックのサポートがありません。ノードを更新してから(設定 → ノードとセキュリティ → バンドル版をインストール、または更新を確認)、もう一度お試しください。",
|
||||
"mig_discard": "移行を破棄",
|
||||
"mig_done": "移行が完了しました。ウォレットはシードフレーズで保護されるようになりました。",
|
||||
"mig_done_btn": "完了",
|
||||
"mig_done_detail": "デーモンが資金を表示するために再スキャンしています — 数分かかる場合があります。以前のウォレットはデータフォルダに .bak として保存されました。",
|
||||
"mig_intro": "これは24単語のシードフレーズで保護された新規ウォレットを隔離されたノード内に作成し、資金をそこへ移動できるようにします。現在のウォレットには一切変更を加えず、このステップでは資金は移動しません — まず新しいシードをバックアップし、その後、別の確定ステップとして資金をスイープします。",
|
||||
"mig_later": "後で",
|
||||
"mig_no_funds": "このウォレットには移行する資金がありません。新しいシードウォレットを直接採用できます — 現在の(空の)ウォレットをシードフレーズで保護されたものに置き換えます。念のため、古いウォレットはタイムスタンプ付きのバックアップとして退避されます。",
|
||||
"mig_nofunds_confirm": "現在のウォレットが新しいシードウォレットに置き換えられることを理解しました",
|
||||
"mig_not_connected": "先にノードへ接続してから、これを再度開いて移行してください。",
|
||||
"mig_precheck_checking": "ウォレットを確認中",
|
||||
"mig_receive_addr": "新しいウォレットの受信アドレス:",
|
||||
"mig_recheck": "再確認",
|
||||
"mig_remainder": "一部の資金がまだ旧ウォレットに残っています(1回のトランザクションには入力が多すぎます) — 切り替える前に残りをスイープしてください。",
|
||||
"mig_remaining": "旧ウォレットの残り:%.8f DRGX",
|
||||
"mig_seed_warning": "これら24単語を順番どおりに書き写し、オフラインで保管してください。これは新しいウォレットの唯一のバックアップです — 紛失すると、移行した資金は永久に失われます。",
|
||||
"mig_step1_done": "2ステップ中1ステップが完了 — 資金はまだ移動していません。この新しいウォレットへ資金をスイープするのが次のステップです。",
|
||||
"mig_sweep_all": "すべての資金をスイープ",
|
||||
"mig_sweep_intro": "次に、現在のウォレットからすべての資金を新しいシードウォレットへスイープします。これは新しいウォレットのアドレスへ1件のトランザクションを送信します。これ以降は(バックアップ済みの)シードフレーズが資金を管理します。",
|
||||
"mig_sweep_remaining": "残りをスイープ",
|
||||
"mig_sweeping": "新しいウォレットへ資金をスイープ中…",
|
||||
"mig_title": "シードウォレットへ移行",
|
||||
"mig_to": "宛先: ",
|
||||
"mig_txid": "txid: ",
|
||||
"mig_unlock_first": "送金するには、まずウォレットのロックを解除してください。",
|
||||
"mig_unlock_to_migrate": "先にウォレットのロックを解除してから、これを再度開いて移行してください。",
|
||||
"mig_waiting_mine": "スイープトランザクションがマイニングされるのを待っています…",
|
||||
"mig_working": "隔離されたノード内に新しいシードウォレットを作成中です — 数分かかる場合があります。メインウォレットは動作を続けます。",
|
||||
"mine_when_idle": "アイドル時にマイニング",
|
||||
"mined": "採掘済み",
|
||||
"mined_filter": "採掘済み",
|
||||
@@ -394,6 +616,8 @@
|
||||
"mining_address_copied": "マイニングアドレスをコピーしました",
|
||||
"mining_all_time": "全期間",
|
||||
"mining_already_saved": "プールURLは既に保存済みです",
|
||||
"mining_auto_balance_desc": "ネットワークの分散化を促すため、ハッシュレートに応じて公式プール間にマイナーを分散します。各プールを定期的にチェックします。",
|
||||
"mining_auto_balanced_to": "マイニングを自動調整しました:",
|
||||
"mining_benchmark_cancel": "ベンチマークをキャンセル",
|
||||
"mining_benchmark_cooling": "クーリング",
|
||||
"mining_benchmark_dismiss": "閉じる",
|
||||
@@ -430,8 +654,10 @@
|
||||
"mining_idle_threads_active_tooltip": "ユーザーアクティブ時のスレッド数",
|
||||
"mining_idle_threads_idle_tooltip": "システムアイドル時のスレッド数",
|
||||
"mining_local_hashrate": "ローカルハッシュレート",
|
||||
"mining_manual_desc": "手動: 下のリストから選んだプールにマイニングします。",
|
||||
"mining_mine": "マイニング",
|
||||
"mining_mining_addr": "マイニングアドレス",
|
||||
"mining_mining_here": "ここでマイニング中",
|
||||
"mining_network": "ネットワーク",
|
||||
"mining_no_blocks_yet": "まだブロックが見つかっていません",
|
||||
"mining_no_payouts_yet": "まだプール支払いがありません",
|
||||
@@ -441,18 +667,24 @@
|
||||
"mining_on": "マイニングはオンです",
|
||||
"mining_open_in_explorer": "エクスプローラーで開く",
|
||||
"mining_payout_address": "支払いアドレス",
|
||||
"mining_payout_foreign": "⚠ この支払いアドレスは現在のウォレットに含まれていません — マイニング報酬が別のウォレットに送られます。ウォレットを切り替えた場合は更新してください。",
|
||||
"mining_payout_tooltip": "マイニング報酬の受取アドレス",
|
||||
"mining_pool": "プール",
|
||||
"mining_pool_fee": "手数料",
|
||||
"mining_pool_hashrate": "プールハッシュレート",
|
||||
"mining_pool_url": "プールURL",
|
||||
"mining_pools_header": "プール",
|
||||
"mining_recent_blocks": "最近のブロック",
|
||||
"mining_recent_payouts": "最近のプール支払い",
|
||||
"mining_refresh": "更新",
|
||||
"mining_remove": "削除",
|
||||
"mining_reset_defaults": "デフォルトにリセット",
|
||||
"mining_save_payout_address": "支払いアドレスを保存",
|
||||
"mining_save_pool_url": "プールURLを保存",
|
||||
"mining_saved_addresses": "保存済みアドレス:",
|
||||
"mining_saved_pools": "保存済みプール:",
|
||||
"mining_select_auto": "自動バランス",
|
||||
"mining_select_manual": "手動",
|
||||
"mining_shares": "シェア",
|
||||
"mining_show_chart": "チャート",
|
||||
"mining_show_log": "ログ",
|
||||
@@ -465,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "プール設定を変更するにはソロマイニングを停止してください",
|
||||
"mining_stopping": "停止中...",
|
||||
"mining_stopping_tooltip": "マイナーを停止中...",
|
||||
"mining_suggested_pools": "推奨プール",
|
||||
"mining_syncing_tooltip": "ブロックチェーン同期中...",
|
||||
"mining_tag": " · マイニング",
|
||||
"mining_threads": "マイニングスレッド",
|
||||
"mining_to_save": "保存する",
|
||||
"mining_today": "今日",
|
||||
@@ -486,10 +720,12 @@
|
||||
"no_addresses_match": "フィルタに一致するアドレスがありません",
|
||||
"no_addresses_with_balance": "残高のあるアドレスがありません",
|
||||
"no_addresses_yet": "アドレスがまだありません",
|
||||
"no_icons_found": "検索に一致するアイコンがありません。",
|
||||
"no_matching": "一致する取引がありません",
|
||||
"no_recent_receives": "最近の受信がありません",
|
||||
"no_recent_sends": "最近の送信がありません",
|
||||
"no_transactions": "取引が見つかりません",
|
||||
"no_transactions_yet": "まだ取引がありません",
|
||||
"node": "ノード",
|
||||
"node_security": "ノードとセキュリティ",
|
||||
"noise": "ノイズ",
|
||||
@@ -553,7 +789,66 @@
|
||||
"pending": "保留中",
|
||||
"pin_not_set": "PINが設定されていません。パスフレーズで解除してください。",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "エントリを追加",
|
||||
"portfolio_add_to": "ポートフォリオに追加",
|
||||
"portfolio_addresses_hdr": "アドレス",
|
||||
"portfolio_addresses_sel": "%d 件選択中",
|
||||
"portfolio_all_funds": "すべての資金",
|
||||
"portfolio_all_shielded": "すべてシールド",
|
||||
"portfolio_all_transparent": "すべて透明",
|
||||
"portfolio_appearance": "外観",
|
||||
"portfolio_cancel": "キャンセル",
|
||||
"portfolio_clear_sel": "クリア",
|
||||
"portfolio_close": "閉じる",
|
||||
"portfolio_color": "色",
|
||||
"portfolio_currency": "通貨",
|
||||
"portfolio_custom_color": "カスタムカラー…",
|
||||
"portfolio_delete": "削除",
|
||||
"portfolio_detail_empty": "左側でグループを選択するか、追加して編集してください。",
|
||||
"portfolio_edit": "編集",
|
||||
"portfolio_funded": "資金あり",
|
||||
"portfolio_group_name": "グループ名",
|
||||
"portfolio_icon": "アイコン",
|
||||
"portfolio_label": "ラベル",
|
||||
"portfolio_manage": "管理…",
|
||||
"portfolio_manage_title": "ポートフォリオを管理",
|
||||
"portfolio_manual_price": "価格 / DRGX",
|
||||
"portfolio_new_entry": "新規エントリ",
|
||||
"portfolio_no_addr_match": "一致するアドレスがありません",
|
||||
"portfolio_no_entries": "カスタムエントリーはまだありません。アドレスのグループを追跡するには1つ追加してください。",
|
||||
"portfolio_no_icon": "なし",
|
||||
"portfolio_outline_opacity": "アウトラインの不透明度",
|
||||
"portfolio_price": "価格",
|
||||
"portfolio_price_btc": "マーケット · BTC",
|
||||
"portfolio_price_drgx": "DRGXのみ",
|
||||
"portfolio_price_manual": "手動",
|
||||
"portfolio_price_usd": "市場 · USD",
|
||||
"portfolio_remove_from": "ポートフォリオから削除",
|
||||
"portfolio_revert": "元に戻す",
|
||||
"portfolio_save": "保存",
|
||||
"portfolio_search": "アドレスを検索…",
|
||||
"portfolio_search_icons": "アイコンを検索…",
|
||||
"portfolio_select_all": "すべて",
|
||||
"portfolio_select_shown": "すべて選択",
|
||||
"portfolio_show": "表示:",
|
||||
"portfolio_show_24h": "24時間",
|
||||
"portfolio_show_sparkline": "スパークライン",
|
||||
"portfolio_show_value": "評価額",
|
||||
"portfolio_spark_day": "日",
|
||||
"portfolio_spark_hour": "時間",
|
||||
"portfolio_spark_min": "分",
|
||||
"portfolio_spark_month": "月",
|
||||
"portfolio_spark_week": "週",
|
||||
"portfolio_style_compact": "コンパクト行",
|
||||
"portfolio_style_detailed": "詳細行",
|
||||
"portfolio_style_featured": "注目行",
|
||||
"portfolio_style_label": "ポートフォリオスタイル",
|
||||
"portfolio_untitled": "無題",
|
||||
"price_chart": "価格チャート",
|
||||
"privacy_great": "優れたプライバシーです!",
|
||||
"privacy_low": "プライバシーが低い — 資金をシールドしてください",
|
||||
"privacy_medium": "さらにシールドすることを検討してください",
|
||||
"processing_transaction": "取引を処理中...",
|
||||
"qr_code": "QRコード",
|
||||
"qr_failed": "QRコードの生成に失敗しました",
|
||||
"qr_title": "QRコード",
|
||||
@@ -571,6 +866,7 @@
|
||||
"receiving_addresses": "あなたの受信アドレス",
|
||||
"recent_received": "最近の受信",
|
||||
"recent_sends": "最近の送信",
|
||||
"recent_transactions": "最近のトランザクション",
|
||||
"recipient": "受取人",
|
||||
"recipient_balance": "受取人: %.8f → %.8f DRGX",
|
||||
"recv_type": "受信",
|
||||
@@ -578,6 +874,7 @@
|
||||
"refresh": "更新",
|
||||
"refresh_now": "今すぐ更新",
|
||||
"remove_favorite": "お気に入りを削除",
|
||||
"repair_wallet": "ウォレットを修復",
|
||||
"report_bug": "バグを報告",
|
||||
"request_amount": "金額(任意):",
|
||||
"request_copy_uri": "URIをコピー",
|
||||
@@ -593,12 +890,16 @@
|
||||
"request_transparent_addrs": "-- 透明アドレス --",
|
||||
"request_uri_copied": "支払いURIをクリップボードにコピーしました",
|
||||
"rescan": "再スキャン",
|
||||
"rescan_bootstrapped_msg": "ノードはブートストラップされているため、スナップショットより下のブロックはディスク上になく、ジェネシスからの再スキャンは失敗します。ウォレットの使用済み残高を照合するには、スナップショットに含まれる高さから再スキャンしてください。wallet.dat とチェーンデータは削除されません。",
|
||||
"rescan_detecting": "ノードがディスク上に保有しているブロックを確認中…",
|
||||
"rescan_from_height": "ブロック高から再スキャン:",
|
||||
"reset_to_defaults": "デフォルトにリセット",
|
||||
"restarting_after_encryption": "暗号化後にデーモンを再起動中...",
|
||||
"restore_address": "アドレスを復元",
|
||||
"result_preview": "結果プレビュー",
|
||||
"retry": "再試行",
|
||||
"review_send": "送金を確認",
|
||||
"rpc_connection": "RPC接続...",
|
||||
"rpc_host": "RPCホスト",
|
||||
"rpc_pass": "パスワード",
|
||||
"rpc_port": "ポート",
|
||||
@@ -608,12 +909,15 @@
|
||||
"save_z_transactions": "Z取引を取引リストに保存",
|
||||
"sb_auth_failed": "認証失敗 — rpcuser/rpcpassword を確認してください",
|
||||
"sb_block": "ブロック: %d",
|
||||
"sb_building_witnesses": "証人を設定中",
|
||||
"sb_building_witnesses_pct": "ウィットネスを再構築中 %.0f%%",
|
||||
"sb_connecting_daemon": "dragonxd に接続中...",
|
||||
"sb_connecting_err": "デーモンに接続中 — %s",
|
||||
"sb_connecting_external": "外部デーモンに接続中...",
|
||||
"sb_connecting_generic": "デーモンに接続中...",
|
||||
"sb_daemon_crashed": "デーモンが %d 回クラッシュしました",
|
||||
"sb_daemon_not_found": "デーモンが見つかりません",
|
||||
"sb_daemon_start_failed": "dragonxd を起動できませんでした",
|
||||
"sb_dragonxd_running": "dragonxd 実行中",
|
||||
"sb_dragonxd_stopped": "dragonxd 停止",
|
||||
"sb_dragonxd_stopping": "dragonxd を停止中...",
|
||||
@@ -639,6 +943,12 @@
|
||||
"sb_waiting_daemon": "dragonxd を待機中...",
|
||||
"sb_waiting_daemon_err": "dragonxd を待機中 — %s",
|
||||
"sb_warming_up": "ウォームアップ中...",
|
||||
"sb_witness_cache": "ウィットネスを再構築中",
|
||||
"screenshot_open_dir": "場所を開く",
|
||||
"screenshot_sweep": "スクリーンショットスイープを実行",
|
||||
"screenshot_sweep_desc": "すべてのテーマをすべてのタブで巡回し、それぞれのスクリーンショットを設定ディレクトリの screenshots フォルダ内のタブごとのサブフォルダに保存します(前回のスイープを上書きします)。数秒間実行されます。",
|
||||
"screenshot_sweep_full": "UI全体スイープ",
|
||||
"search_icons": "アイコンを検索...",
|
||||
"search_placeholder": "検索...",
|
||||
"security": "セキュリティ",
|
||||
"seed_backup_button": "シードフレーズ",
|
||||
@@ -655,6 +965,7 @@
|
||||
"seed_backup_saved": "保存先: ",
|
||||
"seed_backup_title": "シードフレーズをバックアップ",
|
||||
"seed_backup_warning": "順番どおりに書き写し、オフラインで保管してください。決して共有や撮影をしないでください。紛失すると資金を復元できません。",
|
||||
"seed_migrate_button": "シードウォレットへ移行…",
|
||||
"select_address": "アドレスを選択...",
|
||||
"select_receiving_address": "受信アドレスを選択...",
|
||||
"select_source_address": "送信元アドレスを選択...",
|
||||
@@ -663,8 +974,10 @@
|
||||
"send_amount_details": "金額の詳細",
|
||||
"send_amount_upper": "金額",
|
||||
"send_clear_fields": "すべてのフォームフィールドをクリアしますか?",
|
||||
"send_contacts_button": "連絡先から選択",
|
||||
"send_copy_error": "エラーをコピー",
|
||||
"send_dismiss": "閉じる",
|
||||
"send_err_needs_rescan": "ウォレットのシールドノートデータがブロックチェーンと同期していません(ブートストラップや再インデックスの後に発生します)。設定 → ブロックチェーンを再スキャン から完全な再スキャンを実行し、最後まで完了させてから、もう一度送金をお試しください。",
|
||||
"send_error_copied": "エラーをクリップボードにコピーしました",
|
||||
"send_error_prefix": "エラー:%s",
|
||||
"send_exceeds_available": "利用可能額を超過 (%.8f)",
|
||||
@@ -683,6 +996,7 @@
|
||||
"send_recipient": "受取人",
|
||||
"send_select_source": "送信元アドレスを選択...",
|
||||
"send_sending_from": "送信元",
|
||||
"send_status_unconfirmed": "取引のステータスを確認できませんでした",
|
||||
"send_submitting": "取引を送信中...",
|
||||
"send_switch_to_receive": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
|
||||
"send_to": "送金先",
|
||||
@@ -755,6 +1069,8 @@
|
||||
"settings_noise_opacity": "ノイズ不透明度:",
|
||||
"settings_not_encrypted": "暗号化されていません",
|
||||
"settings_not_found": "見つかりません",
|
||||
"settings_open_app_dir": "アプリフォルダを開く",
|
||||
"settings_open_data_dir": "データフォルダを開く",
|
||||
"settings_other": "その他",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "プライバシー",
|
||||
@@ -788,16 +1104,24 @@
|
||||
"shield_check_status": "ステータスを確認",
|
||||
"shield_completed": "操作が正常に完了しました!",
|
||||
"shield_description": "透明アドレスのcoinbase出力をシールドアドレスに送信して、マイニング報酬をシールドします。マイニング収入を隠すことでプライバシーが向上します。",
|
||||
"shield_error_prefix": "エラー: ",
|
||||
"shield_from_address": "送信元アドレス:",
|
||||
"shield_funds": "資金をシールド",
|
||||
"shield_in_progress": "操作進行中...",
|
||||
"shield_max_utxos": "1回の操作あたりの最大UTXO数",
|
||||
"shield_merge_done": "シールド/統合が完了しました!",
|
||||
"shield_op_failed": "操作に失敗しました: ",
|
||||
"shield_op_submitted": "操作を送信しました: ",
|
||||
"shield_operation_id": "オペレーション ID:%s",
|
||||
"shield_select_z": "zアドレスを選択...",
|
||||
"shield_send_failed": "シールドに失敗しました: ",
|
||||
"shield_started": "シールド操作を開始しました",
|
||||
"shield_status_check_error": "状態の確認エラー: ",
|
||||
"shield_status_label": "ステータス: ",
|
||||
"shield_submitting": "操作を送信中...",
|
||||
"shield_title": "Coinbase報酬をシールド",
|
||||
"shield_to_address": "送信先アドレス(シールド):",
|
||||
"shield_unknown_error": "不明なエラー",
|
||||
"shield_utxo_limit": "UTXO制限:",
|
||||
"shield_wildcard_hint": "'*' を使用してすべての透明アドレスからシールド",
|
||||
"shielded": "シールド",
|
||||
@@ -807,11 +1131,16 @@
|
||||
"shielding_notice": "注意:トランスペアレント (T) アドレスからプライベート (Z) アドレスへ資金をシールドします。",
|
||||
"show": "表示",
|
||||
"show_hidden": "非表示を表示 (%d)",
|
||||
"show_qr": "QRを表示",
|
||||
"show_qr_code": "QRコードを表示",
|
||||
"showing_transactions": "%d–%d / %d 件の取引を表示中(合計:%zu)",
|
||||
"showing_x_of_y": "%d / %d アドレスを表示",
|
||||
"simple_background": "シンプル背景",
|
||||
"slider_off": "オフ",
|
||||
"sort_amount_high": "金額が大きい順",
|
||||
"sort_amount_low": "金額が小さい順",
|
||||
"sort_date_newest": "新しい順",
|
||||
"sort_date_oldest": "古い順",
|
||||
"start_mining": "マイニング開始",
|
||||
"status": "ステータス",
|
||||
"stop_external": "外部デーモンを停止",
|
||||
@@ -866,6 +1195,8 @@
|
||||
"tt_clear_ztx": "ローカルにキャッシュされた z-トランザクション履歴を削除",
|
||||
"tt_custom_fees": "トランザクション送信時に手動手数料入力を有効化",
|
||||
"tt_custom_theme": "カスタムテーマがアクティブ",
|
||||
"tt_daemon_install_bundled": "ノードを停止し、インストール済みの dragonxd をこのウォレットビルドにバンドルされたバージョンで上書きしてから再起動します",
|
||||
"tt_daemon_update_check": "プロジェクトの Gitea から最新の dragonxd フルノードをダウンロードして検証し、再起動して適用します",
|
||||
"tt_debug_collapse": "デバッグログオプションを折りたたむ",
|
||||
"tt_debug_expand": "デバッグログオプションを展開",
|
||||
"tt_delete_blockchain": "すべてのブロックチェーンデータを削除して新規同期を開始します。wallet.dat と設定は保持されます。",
|
||||
@@ -881,15 +1212,19 @@
|
||||
"tt_keep_daemon": "セットアップウィザード実行時にデーモンは停止します",
|
||||
"tt_language": "ウォレット UI のインターフェース言語",
|
||||
"tt_layout_hotkey": "ホットキー:左右矢印キーでバランスレイアウトを切り替え",
|
||||
"tt_lite_redownload": "ライトサーバーからすべてのブロックを再ダウンロードして再スキャン",
|
||||
"tt_lock": "ウォレットを即座にロック",
|
||||
"tt_low_spec": "すべての重い視覚効果を無効化\\nホットキー:Ctrl+Shift+Down",
|
||||
"tt_merge": "複数の UTXO を一つのアドレスに統合",
|
||||
"tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始",
|
||||
"tt_noise": "グレインテクスチャ強度(0%% = オフ、100%% = 最大)",
|
||||
"tt_open_app_dir": "ObsidianDragon フォルダ(設定、テーマ、ログ)をファイルマネージャーで開く",
|
||||
"tt_open_data_dir": "ファイルマネージャーでウォレットとブロックチェーンデータのフォルダを開きます",
|
||||
"tt_open_dir": "クリックしてファイルエクスプローラーで開く",
|
||||
"tt_reduce_motion": "アクセシビリティのためにアニメーション遷移と残高補間を無効にする",
|
||||
"tt_remove_encrypt": "暗号化を解除してウォレットを保護なしで保存",
|
||||
"tt_remove_pin": "PIN を削除しアンロックにパスフレーズを要求",
|
||||
"tt_repair_wallet": "ウォレットのトランザクション記録をブロックチェーンから消去して再構築します(再スキャン後に送信に失敗するノートを修復します)",
|
||||
"tt_report_bug": "プロジェクトトラッカーで問題を報告",
|
||||
"tt_request_payment": "QR コード付きの支払い請求を生成",
|
||||
"tt_rescan": "欠落したトランザクションのためにブロックチェーンを再スキャン",
|
||||
@@ -904,6 +1239,7 @@
|
||||
"tt_scan_themes": "新しいテーマをスキャン。\\nテーマフォルダーをここに配置:\\n%s",
|
||||
"tt_scanline": "コンソールでの CRT スキャンライン効果",
|
||||
"tt_seed_backup": "ウォレットの24単語の復元シードフレーズを表示してバックアップします",
|
||||
"tt_seed_migrate": "新しいシードフレーズウォレットを作成し、資金をそこへ移動します",
|
||||
"tt_set_pin": "クイックアンロック用の 4-8 桁 PIN を設定",
|
||||
"tt_shield_mining": "透明マイニング報酬をシールドアドレスに移動",
|
||||
"tt_simple_bg": "背景にシンプルなグラデーションを使用\\nホットキー:Ctrl+Up",
|
||||
@@ -917,6 +1253,7 @@
|
||||
"tt_ui_opacity": "カードとサイドバーの不透明度(100%% = 完全不透明、低い = より透過)",
|
||||
"tt_validate": "DragonX アドレスが有効かどうかを確認",
|
||||
"tt_verbose": "詳細な接続診断、デーモン状態、\\nポート所有者情報をコンソールタブに記録",
|
||||
"tt_wallets_button": "ウォレットファイルを一覧表示して切り替えます",
|
||||
"tt_website": "DragonX ウェブサイトを開く",
|
||||
"tt_window_opacity": "背景の不透明度(低い = デスクトップがウィンドウ越しに見える)",
|
||||
"tt_wizard": "初期セットアップウィザードを再実行\\nデーモンは再起動されます",
|
||||
@@ -925,7 +1262,18 @@
|
||||
"tx_from_address": "送信元アドレス:",
|
||||
"tx_id_label": "取引ID:",
|
||||
"tx_immature": "未成熟",
|
||||
"tx_loading_enriching_sends": "送信取引の詳細を確認中 (%d)",
|
||||
"tx_loading_fetching_transparent": "透明履歴を取得中",
|
||||
"tx_loading_history_progress": "過去の履歴を読み込み中 (%d%%)",
|
||||
"tx_loading_queued": "取引の更新を待機中",
|
||||
"tx_loading_refreshing_cached": "ウォレット履歴を更新中(%d 件キャッシュ済み)",
|
||||
"tx_loading_scanning_shielded": "シールド履歴をスキャン中 (%d アドレス)",
|
||||
"tx_mined": "採掘済み",
|
||||
"tx_progress_balances": "残高を更新中",
|
||||
"tx_progress_finalizing": "トランザクションを確定中",
|
||||
"tx_progress_history": "履歴を更新中",
|
||||
"tx_progress_submitting": "トランザクションをデーモンに送信中",
|
||||
"tx_progress_waiting_ops": "操作を待機中 (%d)",
|
||||
"tx_received": "受信済み",
|
||||
"tx_sent": "送信済み",
|
||||
"tx_to_address": "送信先アドレス:",
|
||||
@@ -937,6 +1285,14 @@
|
||||
"unconfirmed": "未確認",
|
||||
"undo_clear": "クリアを元に戻す",
|
||||
"unknown": "不明",
|
||||
"unmark_mining_address": "マイニングアドレスのマークを解除",
|
||||
"upd_back": "戻る",
|
||||
"upd_install": "インストール",
|
||||
"upd_installed_badge": "インストール済み",
|
||||
"upd_no_build_platform": "このプラットフォーム向けのビルドがありません",
|
||||
"upd_prerelease": "プレリリース",
|
||||
"upd_reinstall": "再インストール",
|
||||
"upd_select_version": "インストールするバージョンを選択",
|
||||
"use_embedded_daemon": "内蔵dragonxdを使用",
|
||||
"use_tor": "Torを使用",
|
||||
"validate_btn": "検証",
|
||||
@@ -956,17 +1312,73 @@
|
||||
"verbose_logging": "詳細ログ",
|
||||
"version": "バージョン",
|
||||
"view": "表示",
|
||||
"view_all": "すべて表示",
|
||||
"view_details": "詳細を表示",
|
||||
"view_on_explorer": "エクスプローラーで表示",
|
||||
"waiting_for_daemon": "デーモン接続を待機中...",
|
||||
"wallet": "ウォレット",
|
||||
"wallet_empty": "ウォレットは空です",
|
||||
"wallet_empty_hint": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
|
||||
"wallets_active": "アクティブ",
|
||||
"wallets_add": "フォルダを追加",
|
||||
"wallets_add_folder": "ウォレットファイルを検索する別のフォルダを追加:",
|
||||
"wallets_add_folder_toggle": "+ 別のフォルダーをウォレット検索…",
|
||||
"wallets_button": "ウォレット…",
|
||||
"wallets_col_addresses": "アドレス",
|
||||
"wallets_col_balance": "残高",
|
||||
"wallets_col_name": "ウォレット",
|
||||
"wallets_col_opened": "最終使用",
|
||||
"wallets_col_size": "サイズ",
|
||||
"wallets_create": "ウォレットを作成",
|
||||
"wallets_creating": "ウォレットを作成中 — ノードが再起動します…",
|
||||
"wallets_current": "現在",
|
||||
"wallets_exists": "その名前のウォレットは既に存在します。",
|
||||
"wallets_external_tt": "データディレクトリの外にあります — インポートでコピーします。",
|
||||
"wallets_folder_hint": "wallet .dat ファイルのあるフォルダのパス",
|
||||
"wallets_folder_invalid": "そのフォルダは存在しません。",
|
||||
"wallets_import": "インポート",
|
||||
"wallets_import_failed": "そのウォレットファイルをインポートできませんでした。",
|
||||
"wallets_import_hint": "インポートして開く",
|
||||
"wallets_import_tt": "このウォレットはデータディレクトリの外にあります。開くには先にコピーが必要です(近日対応予定)。",
|
||||
"wallets_imported": "ウォレットをインポートしました — 切り替え中…",
|
||||
"wallets_intro": "データディレクトリ(および追加したフォルダ)内のウォレットファイルです。開いて切り替えると、それを読み込むためにノードが再起動します。各ウォレットは個別のデータを保持します。",
|
||||
"wallets_name_invalid": "有効なウォレット名を入力してください。",
|
||||
"wallets_never": "なし",
|
||||
"wallets_new_hint": "名前(例:savings)",
|
||||
"wallets_new_label": "新しいウォレットを作成:",
|
||||
"wallets_open": "開く",
|
||||
"wallets_reveal": "フォルダを開く",
|
||||
"wallets_title": "ウォレット",
|
||||
"warning": "警告",
|
||||
"warning_upper": "警告!",
|
||||
"website": "ウェブサイト",
|
||||
"window_opacity": "ウィンドウ透明度",
|
||||
"wizard_daemon_start_failed": "デーモンの起動に失敗しました — 自動的に再試行されます",
|
||||
"xmrig_browse_releases": "すべてのリリースを表示…",
|
||||
"xmrig_checking": "最新のマイナーを確認中…",
|
||||
"xmrig_current": "現在:",
|
||||
"xmrig_download_install": "ダウンロードしてインストール",
|
||||
"xmrig_downloading": "ダウンロード中…",
|
||||
"xmrig_installed": "インストール済み:",
|
||||
"xmrig_installed_ok": "マイナーをインストールしました",
|
||||
"xmrig_installing": "インストール中…",
|
||||
"xmrig_latest": "最新:",
|
||||
"xmrig_loading_releases": "リリースを読み込み中…",
|
||||
"xmrig_none": "なし",
|
||||
"xmrig_reinstall": "再インストール",
|
||||
"xmrig_stop_mining_first": "マイナーを更新する前にマイニングを停止してください。",
|
||||
"xmrig_unavailable_body": "このプラットフォーム向けのマイナービルドは利用できません。",
|
||||
"xmrig_unavailable_title": "マイナーの更新は利用できません",
|
||||
"xmrig_unknown_error": "不明なエラー。",
|
||||
"xmrig_up_to_date": "マイナーは最新です",
|
||||
"xmrig_update_available": "新しいマイナーが利用可能です",
|
||||
"xmrig_update_button": "マイナーを更新…",
|
||||
"xmrig_update_failed": "更新に失敗しました",
|
||||
"xmrig_update_short": "更新",
|
||||
"xmrig_update_title": "マイナーを更新",
|
||||
"xmrig_verify_note": "ダウンロードは、インストール前にリリースで公開されている SHA-256 チェックサムと照合されます。",
|
||||
"xmrig_verifying": "検証中…",
|
||||
"xmrig_version": "バージョン:",
|
||||
"yes_clear": "はい、クリア",
|
||||
"your_addresses": "あなたのアドレス",
|
||||
"z_address": "Zアドレス",
|
||||
|
||||
412
res/lang/ko.json
412
res/lang/ko.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "주소 추가",
|
||||
"address_book_add_new": "새로 추가",
|
||||
"address_book_added": "주소록에 주소를 추가했습니다",
|
||||
"address_book_confirm_delete": "삭제하시겠습니까?",
|
||||
"address_book_count": "저장된 주소 %zu개",
|
||||
"address_book_deleted": "항목이 삭제되었습니다",
|
||||
"address_book_edit": "주소 편집",
|
||||
@@ -39,6 +40,7 @@
|
||||
"address_copied": "주소가 클립보드에 복사되었습니다",
|
||||
"address_details": "주소 상세",
|
||||
"address_label": "주소:",
|
||||
"address_reorder_hint": "행의 가장자리에 놓으면 순서를 바꾸고, 가운데에 놓으면 이동합니다",
|
||||
"address_upper": "주소",
|
||||
"address_url": "주소 URL",
|
||||
"addresses_appear_here": "연결 후 수신 주소가 여기에 표시됩니다.",
|
||||
@@ -69,7 +71,10 @@
|
||||
"backup_wallet": "지갑 백업...",
|
||||
"backup_wallet_not_found": "경고: 예상 위치에서 wallet.dat를 찾을 수 없습니다",
|
||||
"balance": "잔액",
|
||||
"balance_history_collecting": "잔액 내역 — 데이터 수집 중...",
|
||||
"balance_layout": "잔액 레이아웃",
|
||||
"balance_shielded_fmt": "차폐: %.8f",
|
||||
"balance_transparent_fmt": "투명: %.8f",
|
||||
"ban": "차단",
|
||||
"banned_peers": "차단된 피어",
|
||||
"block": "블록",
|
||||
@@ -151,6 +156,20 @@
|
||||
"confirm_delete_blockchain_msg": "데몬을 중지하고 모든 블록체인 데이터(blocks, chainstate, peers)를 삭제한 후 처음부터 다시 동기화합니다. 몇 시간이 걸릴 수 있습니다.",
|
||||
"confirm_delete_blockchain_safe": "wallet.dat, 설정 및 거래 내역은 안전하며 삭제되지 않습니다.",
|
||||
"confirm_delete_blockchain_title": "블록체인 데이터 삭제",
|
||||
"confirm_lite_redownload_msg": "지갑에 다운로드된 블록 데이터를 지우고, 라이트 서버에서 모든 블록을 다시 가져와 재스캔합니다. 시간이 걸릴 수 있으며, 완료될 때까지 지갑에 동기화 진행률이 표시됩니다.",
|
||||
"confirm_lite_redownload_safe": "지갑, 키, 시드는 영향을 받지 않으며 — 블록 데이터만 다시 다운로드됩니다.",
|
||||
"confirm_lite_redownload_title": "블록 다시 다운로드",
|
||||
"confirm_reinstall_daemon_msg": "데몬을 중지하고, 설치된 dragonxd(및 dragonx-cli/dragonx-tx)를 이 지갑 빌드에 번들된 버전으로 덮어쓴 다음, 노드를 재시작합니다. 노드 바이너리를 복구하거나 업데이트할 때 사용하세요.",
|
||||
"confirm_reinstall_daemon_safe": "지갑, 키, 블록체인 데이터는 그대로이며 — 데몬 프로그램 파일만 교체됩니다.",
|
||||
"confirm_reinstall_daemon_title": "번들 데몬 설치",
|
||||
"confirm_repair_wallet_msg": "데몬을 -zapwallettxes=2 옵션으로 재시작합니다. 지갑의 모든 거래 및 노트 기록을 삭제한 뒤 블록체인에서 다시 생성합니다. 전체 재스캔 후에도 거래 생성이 실패할 때(\"Invalid sapling spend proof\" / \"shielded requirements not met\") 사용하세요. 시간이 오래 걸리며 완료될 때까지 지갑은 오프라인 상태로 유지됩니다.",
|
||||
"confirm_repair_wallet_safe": "키, 주소, 잔액은 그대로 유지되며 — 캐시된 거래 기록만 다시 만들어집니다.",
|
||||
"confirm_repair_wallet_title": "지갑 복구",
|
||||
"confirm_rescan_msg": "이 작업은 데몬을 다시 시작하고 지갑의 거래를 찾기 위해 전체 블록체인을 다시 스캔합니다. 오래 걸릴 수 있으며 완료될 때까지 지갑은 오프라인 상태로 유지됩니다.",
|
||||
"confirm_rescan_safe": "wallet.dat와 블록체인 데이터는 삭제되지 않고 — 재스캔만 수행됩니다.",
|
||||
"confirm_rescan_title": "블록체인 재스캔",
|
||||
"confirm_restart_daemon_msg": "변경된 옵션을 적용하기 위해 데몬을 중지했다가 다시 시작합니다. 지갑이 잠시 연결이 끊겼다가 다시 연결됩니다.",
|
||||
"confirm_restart_daemon_title": "데몬 재시작",
|
||||
"confirm_send": "전송 확인",
|
||||
"confirm_transaction": "거래 확인",
|
||||
"confirm_transfer": "이체 확인",
|
||||
@@ -161,6 +180,7 @@
|
||||
"connected_peers": "연결된 피어",
|
||||
"connecting": "연결 중...",
|
||||
"console": "콘솔",
|
||||
"console_app": "앱",
|
||||
"console_auto_scroll": "자동 스크롤",
|
||||
"console_available_commands": "사용 가능한 명령어:",
|
||||
"console_capturing_output": "데몬 출력 캡처 중...",
|
||||
@@ -177,6 +197,7 @@
|
||||
"console_connected": "데몬에 연결됨",
|
||||
"console_copy_all": "모두 복사",
|
||||
"console_copy_selected": "복사",
|
||||
"console_copy_value": "복사",
|
||||
"console_daemon": "데몬",
|
||||
"console_daemon_error": "데몬 오류!",
|
||||
"console_daemon_started": "데몬이 시작되었습니다",
|
||||
@@ -195,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - 채굴 제어",
|
||||
"console_help_stop": " stop - 데몬 중지",
|
||||
"console_line_count": "%zu줄",
|
||||
"console_matches": "일치",
|
||||
"console_new_lines": "%d 새 줄",
|
||||
"console_no_daemon": "데몬 없음",
|
||||
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
|
||||
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.",
|
||||
"console_rpc_reference": "RPC 명령어 참조",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "콘솔 스캔라인",
|
||||
"console_search_commands": "명령어 검색...",
|
||||
"console_select_all": "모두 선택",
|
||||
"console_show_app_output": "[app] 지갑 로그 줄 표시",
|
||||
"console_show_daemon_output": "데몬 출력 표시",
|
||||
"console_show_errors_only": "오류만 표시",
|
||||
"console_show_rpc_ref": "RPC 명령어 참조 표시",
|
||||
"console_show_rpc_trace": "앱 RPC 호출 표시",
|
||||
"console_showing_lines": "%zu / %zu줄 표시 중",
|
||||
"console_starting_node": "노드 시작 중...",
|
||||
"console_status_error": "오류",
|
||||
@@ -218,6 +244,12 @@
|
||||
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
|
||||
"console_zoom_in": "확대",
|
||||
"console_zoom_out": "축소",
|
||||
"contact_global": "모든 지갑에 표시(전역 연락처)",
|
||||
"contact_global_badge_tt": "전역 연락처 — 모든 지갑에서 표시됨",
|
||||
"contact_global_tt": "켜짐: 어떤 지갑을 불러오든 이 연락처가 계속 표시됩니다. 꺼짐: 현재 지갑에만 속합니다.",
|
||||
"contacts": "연락처",
|
||||
"contacts_search_no_match": "일치하는 연락처 없음",
|
||||
"contacts_search_placeholder": "연락처 검색...",
|
||||
"copied": "복사됨!",
|
||||
"copy": "복사",
|
||||
"copy_address": "전체 주소 복사",
|
||||
@@ -227,6 +259,44 @@
|
||||
"copy_uri": "URI 복사",
|
||||
"current_price": "현재 가격",
|
||||
"custom_fees": "사용자 정의 수수료",
|
||||
"daemon_binary": "데몬 바이너리",
|
||||
"daemon_bundled": "번들",
|
||||
"daemon_install_bundled": "번들 버전 설치",
|
||||
"daemon_installed": "설치됨",
|
||||
"daemon_none_bundled": "이 빌드에 없음",
|
||||
"daemon_not_installed": "설치되지 않음",
|
||||
"daemon_status_differ": "설치된 바이너리가 번들 버전과 다릅니다.",
|
||||
"daemon_status_match": "설치된 바이너리가 번들 버전과 일치합니다.",
|
||||
"daemon_status_missing": "설치된 데몬 없음 — 번들 버전을 설치하세요.",
|
||||
"daemon_update_available": "새 노드 버전이 있습니다",
|
||||
"daemon_update_body": "이 지갑 빌드에는 현재 설치된 것보다 새로운 DragonX 노드가 번들로 포함되어 있습니다. 업데이트하면 설치된 dragonxd(및 dragonx-cli/dragonx-tx)가 교체된 후, 새 버전이 적용되도록 노드를 중지했다가 다시 시작합니다. 새 노드는 기존 노드에 없는 기능(예: 시드 문구 지원)을 추가할 수 있으므로 권장합니다.",
|
||||
"daemon_update_browse": "모든 릴리스 보기…",
|
||||
"daemon_update_check": "업데이트 확인…",
|
||||
"daemon_update_checking": "최신 노드 확인 중…",
|
||||
"daemon_update_downgrade_note": "이전 버전은 현재 체인 데이터와 호환되지 않을 수 있습니다. 다른 버전을 설치하면 데몬을 재시작한 후 적용됩니다.",
|
||||
"daemon_update_download_install": "다운로드 및 설치",
|
||||
"daemon_update_downloading": "다운로드 중…",
|
||||
"daemon_update_failed": "업데이트 실패",
|
||||
"daemon_update_installed": "설치됨:",
|
||||
"daemon_update_installed_ok": "노드가 설치되었습니다",
|
||||
"daemon_update_installing": "설치 중…",
|
||||
"daemon_update_keep": "현재 버전 유지",
|
||||
"daemon_update_later": "나중에",
|
||||
"daemon_update_latest": "최신:",
|
||||
"daemon_update_loading": "릴리스 불러오는 중…",
|
||||
"daemon_update_now": "지금 업데이트",
|
||||
"daemon_update_reinstall": "다시 설치",
|
||||
"daemon_update_restart_note": "새 버전을 실행하려면 데몬을 재시작하세요.",
|
||||
"daemon_update_restart_now": "지금 데몬 재시작",
|
||||
"daemon_update_safe": "지갑, 키, 블록체인 데이터는 그대로이며 — 데몬 프로그램 파일만 교체됩니다. 커스텀 노드를 일부러 실행 중이라면 현재 버전 유지를 선택하세요.",
|
||||
"daemon_update_title": "노드 데몬을 업데이트할까요?",
|
||||
"daemon_update_unavailable_body": "이 플랫폼에 사용할 수 있는 노드 빌드가 없습니다.",
|
||||
"daemon_update_unavailable_title": "노드 업데이트를 사용할 수 없음",
|
||||
"daemon_update_unknown_error": "알 수 없는 오류입니다.",
|
||||
"daemon_update_up_to_date": "노드가 최신 상태입니다",
|
||||
"daemon_update_verify_note": "다운로드는 설치 전에 릴리스에 게시된 SHA-256과 고정된 ed25519 서명으로 검증됩니다.",
|
||||
"daemon_update_verifying": "확인 중…",
|
||||
"daemon_update_version": "버전:",
|
||||
"daemon_version": "데몬",
|
||||
"dark": "다크",
|
||||
"date": "날짜",
|
||||
@@ -257,10 +327,14 @@
|
||||
"explorer_block_time": "시간",
|
||||
"explorer_block_txs": "트랜잭션",
|
||||
"explorer_chain_stats": "체인",
|
||||
"explorer_hash_not_found": "이 해시에 해당하는 블록이나 트랜잭션을 찾을 수 없습니다",
|
||||
"explorer_invalid_query": "블록 높이 또는 64자 해시를 입력하세요",
|
||||
"explorer_invalid_response": "데몬으로부터 잘못된 응답",
|
||||
"explorer_mempool": "멤풀",
|
||||
"explorer_mempool_size": "크기",
|
||||
"explorer_mempool_txs": "트랜잭션",
|
||||
"explorer_no_results": "일치하는 캐시된 블록이 없습니다",
|
||||
"explorer_not_connected": "데몬에 연결되지 않음 — 블록 또는 트랜잭션 해시를 조회할 수 없습니다",
|
||||
"explorer_recent_blocks": "최근 블록",
|
||||
"explorer_search": "검색",
|
||||
"explorer_section": "탐색기",
|
||||
@@ -312,6 +386,7 @@
|
||||
"hidden_tag": " (숨김)",
|
||||
"hide": "숨기기",
|
||||
"hide_address": "주소 숨기기",
|
||||
"hide_qr": "QR 숨기기",
|
||||
"hide_zero_balances": "잔액 0 숨기기",
|
||||
"history": "내역",
|
||||
"immature_type": "미성숙",
|
||||
@@ -350,19 +425,120 @@
|
||||
"label_placeholder": "예: 저축, 채굴...",
|
||||
"language": "언어",
|
||||
"light": "라이트",
|
||||
"lite_account_label": "계정",
|
||||
"lite_action": "작업",
|
||||
"lite_backup_keys": "백업 및 키",
|
||||
"lite_birthday_backup": "생성 블록: %llu (이 값도 백업하세요)",
|
||||
"lite_birthday_hint": "스캔을 시작할 블록 높이입니다. 모르면 0으로 두세요(전체 스캔이 느려짐).",
|
||||
"lite_birthday_label": "생일 블록",
|
||||
"lite_console_help_passthrough": "그 외 입력은 라이트 지갑 콘솔 명령으로 실행됩니다.",
|
||||
"lite_copy": "복사",
|
||||
"lite_could_not_write": "쓸 수 없습니다: ",
|
||||
"lite_encrypt_wallet": "지갑 암호화",
|
||||
"lite_encryption_removed": "암호화가 제거되었습니다",
|
||||
"lite_hide_wipe": "숨기고 삭제",
|
||||
"lite_import": "가져오기",
|
||||
"lite_import_key_label": "키 가져오기",
|
||||
"lite_key_imported": "키를 가져왔습니다 — 동기화를 실행하여 기록을 스캔하세요",
|
||||
"lite_lock_failed": "잠금 실패",
|
||||
"lite_lock_now": "지금 잠그기",
|
||||
"lite_maintenance": "유지 관리",
|
||||
"lite_net_add": "추가",
|
||||
"lite_net_add_label_hint": "레이블(선택 사항)",
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "확인 중…",
|
||||
"lite_net_connected": "연결됨",
|
||||
"lite_net_custom": "사용자 지정",
|
||||
"lite_net_disconnected": "연결되지 않음",
|
||||
"lite_net_hidden_section": "숨겨진 서버",
|
||||
"lite_net_hide": "숨기기",
|
||||
"lite_net_in_use": "사용 중",
|
||||
"lite_net_intro": "사용할 서버를 선택하거나, 지갑이 무작위로 선택하도록 하세요. 변경 사항은 즉시 적용됩니다.",
|
||||
"lite_net_invalid_url": "유효한 http(s):// URL을 입력하세요.",
|
||||
"lite_net_no_wallet": "열린 지갑 없음",
|
||||
"lite_net_official": "공식",
|
||||
"lite_net_offline": "오프라인",
|
||||
"lite_net_random_active": "무작위 서버 선택이 활성화되어 있습니다.",
|
||||
"lite_net_random_server": "무작위 서버",
|
||||
"lite_net_refresh": "새로고침",
|
||||
"lite_net_show_hidden": "숨긴 서버 표시",
|
||||
"lite_net_sync_label": "동기화",
|
||||
"lite_net_synced": "동기화됨",
|
||||
"lite_net_syncing": "동기화 중",
|
||||
"lite_net_title": "라이트 서버",
|
||||
"lite_net_unhide": "숨김 해제",
|
||||
"lite_net_use_random": "매번 무작위 서버 사용",
|
||||
"lite_no_wallet": "열린 지갑 없음 — 설정에서 지갑을 만들거나 여세요",
|
||||
"lite_no_wallet_short": "열린 지갑 없음",
|
||||
"lite_op_create": "생성",
|
||||
"lite_op_open": "열기",
|
||||
"lite_op_restore": "복원",
|
||||
"lite_overwrite": "덮어쓰기",
|
||||
"lite_passphrase_label": "암호",
|
||||
"lite_private_keys_warning": "개인 키 — 이 키를 가진 사람은 누구나 당신의 자금을 사용할 수 있습니다",
|
||||
"lite_redownload_blocks": "블록 다시 다운로드",
|
||||
"lite_redownload_desc": "라이트 서버에서 모든 블록을 다시 가져옵니다(잔액이나 내역이 잘못 보일 때 사용).",
|
||||
"lite_redownload_running": "블록 다시 다운로드 중…",
|
||||
"lite_remove_encryption": "암호화 제거",
|
||||
"lite_restore_birthday_label": "생일 블록 (선택 사항 — 0은 처음부터 스캔하며 느림)",
|
||||
"lite_restore_btn": "지갑 복원",
|
||||
"lite_restore_intro": "복구 시드 문구를 입력하세요. 지갑이 복원된 후 라이트 서버에서 동기화됩니다.",
|
||||
"lite_restore_ok": "지갑이 복원되었습니다 — 라이트 서버에서 동기화 중…",
|
||||
"lite_restore_seed_label": "시드 문구(단어를 공백으로 구분)",
|
||||
"lite_restore_title": "시드 문구로 복구",
|
||||
"lite_save_to_file": "파일로 저장",
|
||||
"lite_saved_to": "저장됨(평문, 소유자 전용): ",
|
||||
"lite_security": "보안",
|
||||
"lite_seed_label": "시드",
|
||||
"lite_seed_warning": "시드 문구 — 지갑을 복구할 수 있는 유일한 방법입니다. 종이에 적어 오프라인으로 보관하고 절대 공유하지 마세요.",
|
||||
"lite_servers_network_tab": "라이트 서버는 네트워크 탭에서 관리합니다.",
|
||||
"lite_show_private_keys": "개인 키 표시",
|
||||
"lite_show_seed": "시드 표시",
|
||||
"lite_unlock": "잠금 해제",
|
||||
"lite_unlock_btn": "잠금 해제",
|
||||
"lite_unlock_failed": "잠금 해제 실패 — 암호가 틀렸나요?",
|
||||
"lite_unlock_msg": "지출을 위해 지갑의 잠금을 해제하려면 암호를 입력하세요.",
|
||||
"lite_unlock_ok": "지갑 잠금이 해제되었습니다 — 이제 전송할 수 있습니다",
|
||||
"lite_unlock_title": "지갑 잠금 해제",
|
||||
"lite_validate": "확인",
|
||||
"lite_wallet_encrypted": "지갑이 암호화되었습니다",
|
||||
"lite_wallet_label": "지갑",
|
||||
"lite_wallet_locked": "지갑 잠김",
|
||||
"lite_wallet_ready": "지갑 준비됨",
|
||||
"lite_wallet_request": "라이트 지갑 요청",
|
||||
"lite_wallet_unlocked": "지갑 잠금이 해제되었습니다",
|
||||
"lite_welcome_create": "새 지갑 만들기",
|
||||
"lite_welcome_create_failed": "지갑을 만들 수 없습니다",
|
||||
"lite_welcome_created": "지갑이 생성되었습니다 — 지금 설정 → 백업 및 키에서 복구 문구를 백업하세요",
|
||||
"lite_welcome_later": "나중에",
|
||||
"lite_welcome_msg": "아직 지갑이 없습니다. 새로 만들거나 복구 문구로 복원하세요.",
|
||||
"lite_welcome_restore": "시드로 복원",
|
||||
"lite_welcome_restore_hint": "설정 → 라이트 지갑 요청에서 지갑을 복원하세요",
|
||||
"lite_welcome_title": "ObsidianDragon Lite에 오신 것을 환영합니다",
|
||||
"lite_word_count": "%d / 24 단어",
|
||||
"lite_working": "작업 중…",
|
||||
"loading": "로딩 중...",
|
||||
"loading_addresses": "주소 로딩 중...",
|
||||
"loading_transactions": "거래를 불러오는 중",
|
||||
"local_hashrate": "로컬 해시레이트",
|
||||
"low_spec_mode": "저사양 모드",
|
||||
"mark_mining_address": "채굴 주소로 표시",
|
||||
"market": "시장",
|
||||
"market_12h": "12시간",
|
||||
"market_18h": "18시간",
|
||||
"market_24h": "24시간",
|
||||
"market_24h_change": "24시간",
|
||||
"market_24h_volume": "24시간 거래량",
|
||||
"market_6h": "6시간",
|
||||
"market_attribution": "가격 데이터: NonKYC 제공",
|
||||
"market_btc_price": "BTC 가격",
|
||||
"market_cap": "시가총액",
|
||||
"market_cap_short": "시총",
|
||||
"market_iv_1d": "1일",
|
||||
"market_iv_1h": "1시간",
|
||||
"market_iv_1m": "1개월",
|
||||
"market_iv_1w": "1주",
|
||||
"market_iv_live": "실시간",
|
||||
"market_no_history": "가격 내역 없음",
|
||||
"market_no_price": "가격 데이터 없음",
|
||||
"market_now": "현재",
|
||||
@@ -372,6 +548,8 @@
|
||||
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
|
||||
"market_refresh_price": "가격 데이터 새로고침",
|
||||
"market_trade_on": "%s에서 거래",
|
||||
"market_updated": "\\xc2\\xb7 업데이트됨 %s",
|
||||
"market_vol_short": "거래량",
|
||||
"mature": "성숙됨",
|
||||
"max": "최대",
|
||||
"memo": "메모 (선택, 암호화)",
|
||||
@@ -381,8 +559,52 @@
|
||||
"memo_z_only": "참고: 메모는 차폐 (z) 주소로 전송할 때만 사용할 수 있습니다",
|
||||
"merge_description": "여러 UTXO를 단일 차폐 주소로 통합합니다. 지갑 크기를 줄이고 프라이버시를 향상시킵니다.",
|
||||
"merge_funds": "자금 통합",
|
||||
"merge_send_failed": "통합 실패: ",
|
||||
"merge_started": "통합 작업이 시작되었습니다",
|
||||
"merge_title": "주소로 통합",
|
||||
"mig_adopt": "이 지갑을 내 지갑으로 사용",
|
||||
"mig_adopt_now": "시드 지갑 사용",
|
||||
"mig_adopting": "새 지갑을 설치하고 재스캔하는 중…",
|
||||
"mig_adopting_note": "노드가 새 지갑으로 다시 시작되어 체인을 재스캔합니다 — 몇 분 정도 걸릴 수 있습니다. 지갑을 계속 실행해 두세요. 자동으로 다시 연결됩니다.",
|
||||
"mig_already_mnemonic": "지갑에 이미 24단어 시드 문구가 있어 이전할 것이 없습니다. 백업해서 단어들을 안전한 곳에 보관하기만 하면 됩니다.",
|
||||
"mig_backed_up": "시드 문구를 적어 두었습니다",
|
||||
"mig_backup_instead": "시드 문구 백업",
|
||||
"mig_balance": "현재 지갑 잔액: %.8f DRGX (소액 수수료 제외)",
|
||||
"mig_check_failed": "지갑을 확인할 수 없습니다. 잠금이 해제되고 연결되어 있는지 확인한 다음 다시 확인하세요.",
|
||||
"mig_checking_balance": "잔액을 확인하는 중",
|
||||
"mig_confirming": "지갑을 전환하기 전에 쓸어 담기가 온체인에서 확인되기를 기다리는 중입니다 — 이는 자금이 실제로 이동했음을 보장합니다.",
|
||||
"mig_confs": "쓸어 담기 확인 수: %d",
|
||||
"mig_continue_sweep": "쓸어 담기로 계속",
|
||||
"mig_copy_seed": "시드 복사",
|
||||
"mig_create": "시드 지갑 생성",
|
||||
"mig_daemon_too_old": "DragonX 노드가 너무 오래되어 시드 지갑을 만들 수 없습니다 — 여기에 필요한 니모닉 지원이 없습니다. 노드를 업데이트한 뒤(설정 → 노드 및 보안 → 번들 설치, 또는 업데이트 확인) 다시 시도하세요.",
|
||||
"mig_discard": "이전 취소",
|
||||
"mig_done": "이전이 완료되었습니다. 이제 지갑이 시드 문구로 백업됩니다.",
|
||||
"mig_done_btn": "완료",
|
||||
"mig_done_detail": "데몬이 자금을 표시하기 위해 재스캔하는 중입니다 — 몇 분 정도 걸릴 수 있습니다. 이전 지갑은 데이터 폴더에 .bak 파일로 저장되었습니다.",
|
||||
"mig_intro": "24단어 시드 문구로 백업되는 완전히 새로운 지갑을 격리된 노드에 생성하여 자금을 그곳으로 옮길 수 있습니다. 현재 지갑은 그대로 유지되며 이 단계에서는 어떤 자금도 이동하지 않습니다 — 먼저 새 시드를 백업한 다음, 별도의 확인된 단계로 자금을 그곳에 쓸어 담습니다.",
|
||||
"mig_later": "나중에",
|
||||
"mig_no_funds": "지갑에 이전할 자금이 없습니다. 새 시드 지갑을 바로 사용할 수 있습니다 — 현재의 (비어 있는) 지갑을 시드로 백업되는 지갑으로 교체합니다. 만일을 대비해 이전 지갑은 타임스탬프가 붙은 백업으로 별도 보관됩니다.",
|
||||
"mig_nofunds_confirm": "현재 지갑이 새 시드 지갑으로 교체된다는 것을 이해합니다",
|
||||
"mig_not_connected": "먼저 노드에 연결한 다음, 이 화면을 다시 열어 이전하세요.",
|
||||
"mig_precheck_checking": "지갑을 확인하는 중",
|
||||
"mig_receive_addr": "새 지갑 수신 주소:",
|
||||
"mig_recheck": "다시 확인",
|
||||
"mig_remainder": "일부 자금이 아직 이전 지갑에 남아 있습니다 (한 거래에 담기에는 입력이 너무 많음) — 전환하기 전에 나머지를 쓸어 담으세요.",
|
||||
"mig_remaining": "이전 지갑에 남은 잔액: %.8f DRGX",
|
||||
"mig_seed_warning": "이 24개의 단어를 순서대로 종이에 적어 오프라인으로 보관하세요. 이 단어들은 새 지갑의 유일한 백업입니다 — 잃어버리면 이전한 자금을 영원히 복구할 수 없습니다.",
|
||||
"mig_step1_done": "2단계 중 1단계 완료 — 아직 자금이 이동하지 않았습니다. 이 새 지갑으로 자금을 쓸어 담는 것이 다음 단계입니다.",
|
||||
"mig_sweep_all": "모든 자금 쓸어 담기",
|
||||
"mig_sweep_intro": "이제 현재 지갑의 모든 자금을 새 시드 지갑으로 쓸어 담습니다. 새 지갑 주소로 단일 거래를 전송하며, 이후로는 (이미 백업한) 시드 문구가 자금을 관리합니다.",
|
||||
"mig_sweep_remaining": "나머지 쓸어 담기",
|
||||
"mig_sweeping": "새 지갑으로 자금을 쓸어 담는 중…",
|
||||
"mig_title": "시드 지갑으로 이전",
|
||||
"mig_to": "받는 곳: ",
|
||||
"mig_txid": "txid: ",
|
||||
"mig_unlock_first": "지갑에서 자금을 전송하려면 먼저 잠금을 해제하세요.",
|
||||
"mig_unlock_to_migrate": "먼저 지갑의 잠금을 해제한 다음, 이 화면을 다시 열어 이전하세요.",
|
||||
"mig_waiting_mine": "쓸어 담기 거래가 채굴되기를 기다리는 중…",
|
||||
"mig_working": "격리된 노드에 새 시드 지갑을 생성하는 중입니다 — 1분 정도 걸릴 수 있습니다. 메인 지갑은 계속 실행됩니다.",
|
||||
"mine_when_idle": "유휴 시 채굴",
|
||||
"mined": "채굴됨",
|
||||
"mined_filter": "채굴됨",
|
||||
@@ -394,6 +616,8 @@
|
||||
"mining_address_copied": "채굴 주소가 복사되었습니다",
|
||||
"mining_all_time": "전체 기간",
|
||||
"mining_already_saved": "풀 URL이 이미 저장되어 있습니다",
|
||||
"mining_auto_balance_desc": "네트워크 분산화를 돕기 위해 해시레이트에 따라 채굴기를 공식 풀에 분산합니다. 각 풀을 주기적으로 확인합니다.",
|
||||
"mining_auto_balanced_to": "채굴을 자동 조정한 대상",
|
||||
"mining_benchmark_cancel": "벤치마크 취소",
|
||||
"mining_benchmark_cooling": "쿨링",
|
||||
"mining_benchmark_dismiss": "닫기",
|
||||
@@ -430,8 +654,10 @@
|
||||
"mining_idle_threads_active_tooltip": "사용자 활성 시 스레드",
|
||||
"mining_idle_threads_idle_tooltip": "시스템 유휴 시 스레드",
|
||||
"mining_local_hashrate": "로컬 해시레이트",
|
||||
"mining_manual_desc": "수동: 아래 목록에서 선택한 풀로 채굴합니다.",
|
||||
"mining_mine": "채굴",
|
||||
"mining_mining_addr": "채굴 주소",
|
||||
"mining_mining_here": "여기서 채굴 중",
|
||||
"mining_network": "네트워크",
|
||||
"mining_no_blocks_yet": "아직 블록을 찾지 못했습니다",
|
||||
"mining_no_payouts_yet": "아직 풀 지급이 없습니다",
|
||||
@@ -441,18 +667,24 @@
|
||||
"mining_on": "채굴이 켜져 있습니다",
|
||||
"mining_open_in_explorer": "탐색기에서 열기",
|
||||
"mining_payout_address": "지급 주소",
|
||||
"mining_payout_foreign": "⚠ 이 지급 주소는 현재 지갑에 없습니다 — 채굴한 보상이 다른 지갑으로 전송됩니다. 지갑을 전환했다면 주소를 업데이트하세요.",
|
||||
"mining_payout_tooltip": "채굴 보상 수신 주소",
|
||||
"mining_pool": "풀",
|
||||
"mining_pool_fee": "수수료",
|
||||
"mining_pool_hashrate": "풀 해시레이트",
|
||||
"mining_pool_url": "풀 URL",
|
||||
"mining_pools_header": "풀",
|
||||
"mining_recent_blocks": "최근 블록",
|
||||
"mining_recent_payouts": "최근 풀 지급",
|
||||
"mining_refresh": "새로고침",
|
||||
"mining_remove": "제거",
|
||||
"mining_reset_defaults": "기본값으로 재설정",
|
||||
"mining_save_payout_address": "지급 주소 저장",
|
||||
"mining_save_pool_url": "풀 URL 저장",
|
||||
"mining_saved_addresses": "저장된 주소:",
|
||||
"mining_saved_pools": "저장된 풀:",
|
||||
"mining_select_auto": "자동 분산",
|
||||
"mining_select_manual": "수동",
|
||||
"mining_shares": "셰어",
|
||||
"mining_show_chart": "차트",
|
||||
"mining_show_log": "로그",
|
||||
@@ -465,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "풀 설정을 변경하려면 솔로 채굴을 중지하세요",
|
||||
"mining_stopping": "중지 중...",
|
||||
"mining_stopping_tooltip": "채굴기 중지 중...",
|
||||
"mining_suggested_pools": "추천 풀",
|
||||
"mining_syncing_tooltip": "블록체인 동기화 중...",
|
||||
"mining_tag": " · 채굴",
|
||||
"mining_threads": "채굴 스레드",
|
||||
"mining_to_save": "저장하려면",
|
||||
"mining_today": "오늘",
|
||||
@@ -486,10 +720,12 @@
|
||||
"no_addresses_match": "필터와 일치하는 주소가 없습니다",
|
||||
"no_addresses_with_balance": "잔액이 있는 주소가 없습니다",
|
||||
"no_addresses_yet": "아직 주소가 없습니다",
|
||||
"no_icons_found": "검색과 일치하는 아이콘이 없습니다.",
|
||||
"no_matching": "일치하는 거래가 없습니다",
|
||||
"no_recent_receives": "최근 수신 내역 없음",
|
||||
"no_recent_sends": "최근 전송 내역 없음",
|
||||
"no_transactions": "거래 내역이 없습니다",
|
||||
"no_transactions_yet": "아직 거래 내역이 없습니다",
|
||||
"node": "노드",
|
||||
"node_security": "노드 및 보안",
|
||||
"noise": "노이즈",
|
||||
@@ -553,7 +789,66 @@
|
||||
"pending": "대기 중",
|
||||
"pin_not_set": "PIN이 설정되지 않았습니다. 암호를 사용하여 잠금 해제하세요.",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "항목 추가",
|
||||
"portfolio_add_to": "포트폴리오에 추가",
|
||||
"portfolio_addresses_hdr": "주소",
|
||||
"portfolio_addresses_sel": "%d개 선택됨",
|
||||
"portfolio_all_funds": "모든 자금",
|
||||
"portfolio_all_shielded": "모두 차폐",
|
||||
"portfolio_all_transparent": "전체 투명",
|
||||
"portfolio_appearance": "외관",
|
||||
"portfolio_cancel": "취소",
|
||||
"portfolio_clear_sel": "지우기",
|
||||
"portfolio_close": "닫기",
|
||||
"portfolio_color": "색상",
|
||||
"portfolio_currency": "통화",
|
||||
"portfolio_custom_color": "사용자 지정 색상\\xE2\\x80\\xA6",
|
||||
"portfolio_delete": "삭제",
|
||||
"portfolio_detail_empty": "왼쪽에서 그룹을 선택하거나 새로 추가하여 편집하세요.",
|
||||
"portfolio_edit": "편집",
|
||||
"portfolio_funded": "자금 보유",
|
||||
"portfolio_group_name": "그룹 이름",
|
||||
"portfolio_icon": "아이콘",
|
||||
"portfolio_label": "라벨",
|
||||
"portfolio_manage": "관리…",
|
||||
"portfolio_manage_title": "포트폴리오 관리",
|
||||
"portfolio_manual_price": "가격 / DRGX",
|
||||
"portfolio_new_entry": "새 항목",
|
||||
"portfolio_no_addr_match": "일치하는 주소 없음",
|
||||
"portfolio_no_entries": "아직 사용자 지정 항목이 없습니다. 주소 그룹을 추적하려면 항목을 추가하세요.",
|
||||
"portfolio_no_icon": "없음",
|
||||
"portfolio_outline_opacity": "외곽선 불투명도",
|
||||
"portfolio_price": "가격",
|
||||
"portfolio_price_btc": "시장 · BTC",
|
||||
"portfolio_price_drgx": "DRGX만",
|
||||
"portfolio_price_manual": "수동",
|
||||
"portfolio_price_usd": "시장 \\xC2\\xB7 USD",
|
||||
"portfolio_remove_from": "포트폴리오에서 제거",
|
||||
"portfolio_revert": "되돌리기",
|
||||
"portfolio_save": "저장",
|
||||
"portfolio_search": "주소 검색…",
|
||||
"portfolio_search_icons": "아이콘 검색\\xE2\\x80\\xA6",
|
||||
"portfolio_select_all": "전체",
|
||||
"portfolio_select_shown": "모두 선택",
|
||||
"portfolio_show": "표시:",
|
||||
"portfolio_show_24h": "24시간",
|
||||
"portfolio_show_sparkline": "스파크라인",
|
||||
"portfolio_show_value": "가치",
|
||||
"portfolio_spark_day": "일",
|
||||
"portfolio_spark_hour": "시간",
|
||||
"portfolio_spark_min": "분",
|
||||
"portfolio_spark_month": "월",
|
||||
"portfolio_spark_week": "주",
|
||||
"portfolio_style_compact": "간결한 행",
|
||||
"portfolio_style_detailed": "상세 행",
|
||||
"portfolio_style_featured": "강조 행",
|
||||
"portfolio_style_label": "포트폴리오 스타일",
|
||||
"portfolio_untitled": "제목 없음",
|
||||
"price_chart": "가격 차트",
|
||||
"privacy_great": "프라이버시가 우수합니다!",
|
||||
"privacy_low": "낮은 프라이버시 — 자금을 차폐하세요",
|
||||
"privacy_medium": "더 많은 자금을 차폐하는 것을 고려하세요",
|
||||
"processing_transaction": "거래 처리 중...",
|
||||
"qr_code": "QR 코드",
|
||||
"qr_failed": "QR 코드 생성 실패",
|
||||
"qr_title": "QR 코드",
|
||||
@@ -571,6 +866,7 @@
|
||||
"receiving_addresses": "수신 주소",
|
||||
"recent_received": "최근 수신",
|
||||
"recent_sends": "최근 전송",
|
||||
"recent_transactions": "최근 거래",
|
||||
"recipient": "수신자",
|
||||
"recipient_balance": "수신자: %.8f → %.8f DRGX",
|
||||
"recv_type": "수신",
|
||||
@@ -578,6 +874,7 @@
|
||||
"refresh": "새로고침",
|
||||
"refresh_now": "지금 새로고침",
|
||||
"remove_favorite": "즐겨찾기 제거",
|
||||
"repair_wallet": "지갑 복구",
|
||||
"report_bug": "버그 신고",
|
||||
"request_amount": "금액 (선택):",
|
||||
"request_copy_uri": "URI 복사",
|
||||
@@ -593,12 +890,16 @@
|
||||
"request_transparent_addrs": "-- 투명 주소 --",
|
||||
"request_uri_copied": "결제 URI가 클립보드에 복사되었습니다",
|
||||
"rescan": "재스캔",
|
||||
"rescan_bootstrapped_msg": "노드가 부트스트랩되어 스냅샷 이전 블록은 디스크에 없으므로 제네시스부터의 재스캔은 실패합니다. 지갑의 사용된 잔액을 조정하려면 스냅샷에 포함된 높이부터 재스캔하세요. wallet.dat과 체인 데이터는 삭제되지 않습니다.",
|
||||
"rescan_detecting": "노드가 디스크에 어떤 블록을 가지고 있는지 확인하는 중…",
|
||||
"rescan_from_height": "재스캔 시작 블록 높이:",
|
||||
"reset_to_defaults": "기본값으로 재설정",
|
||||
"restarting_after_encryption": "암호화 후 데몬 재시작 중...",
|
||||
"restore_address": "주소 복원",
|
||||
"result_preview": "결과 미리보기",
|
||||
"retry": "재시도",
|
||||
"review_send": "전송 검토",
|
||||
"rpc_connection": "RPC 연결...",
|
||||
"rpc_host": "RPC 호스트",
|
||||
"rpc_pass": "비밀번호",
|
||||
"rpc_port": "포트",
|
||||
@@ -608,12 +909,15 @@
|
||||
"save_z_transactions": "Z 거래를 거래 목록에 저장",
|
||||
"sb_auth_failed": "인증 실패 — rpcuser/rpcpassword를 확인하세요",
|
||||
"sb_block": "블록: %d",
|
||||
"sb_building_witnesses": "증인 설정 중",
|
||||
"sb_building_witnesses_pct": "위트니스 재생성 중 %.0f%%",
|
||||
"sb_connecting_daemon": "dragonxd에 연결 중...",
|
||||
"sb_connecting_err": "데몬 연결 중 — %s",
|
||||
"sb_connecting_external": "외부 데몬에 연결 중...",
|
||||
"sb_connecting_generic": "데몬에 연결 중...",
|
||||
"sb_daemon_crashed": "데몬이 %d회 충돌함",
|
||||
"sb_daemon_not_found": "데몬을 찾을 수 없음",
|
||||
"sb_daemon_start_failed": "dragonxd를 시작할 수 없습니다",
|
||||
"sb_dragonxd_running": "dragonxd 실행 중",
|
||||
"sb_dragonxd_stopped": "dragonxd 중지됨",
|
||||
"sb_dragonxd_stopping": "dragonxd 중지 중...",
|
||||
@@ -639,6 +943,12 @@
|
||||
"sb_waiting_daemon": "dragonxd 대기 중...",
|
||||
"sb_waiting_daemon_err": "dragonxd 대기 중 — %s",
|
||||
"sb_warming_up": "워밍업 중...",
|
||||
"sb_witness_cache": "증인 재구축 중",
|
||||
"screenshot_open_dir": "위치 열기",
|
||||
"screenshot_sweep": "스크린샷 스윕 실행",
|
||||
"screenshot_sweep_desc": "모든 탭에 대해 모든 테마를 순회하며 각각의 스크린샷을 설정 디렉터리의 screenshots 폴더 아래 탭별 하위 폴더에 저장합니다(이전 스윕을 덮어씀). 몇 초 동안 실행됩니다.",
|
||||
"screenshot_sweep_full": "전체 UI 스윕",
|
||||
"search_icons": "아이콘 검색...",
|
||||
"search_placeholder": "검색...",
|
||||
"security": "보안",
|
||||
"seed_backup_button": "시드 문구",
|
||||
@@ -655,6 +965,7 @@
|
||||
"seed_backup_saved": "저장됨: ",
|
||||
"seed_backup_title": "시드 문구 백업",
|
||||
"seed_backup_warning": "순서대로 종이에 적어 오프라인으로 보관하고, 절대 공유하거나 사진으로 찍지 마세요. 이 단어들을 잃어버리면 자금을 복구할 수 없습니다.",
|
||||
"seed_migrate_button": "시드 지갑으로 이전…",
|
||||
"select_address": "주소 선택...",
|
||||
"select_receiving_address": "수신 주소 선택...",
|
||||
"select_source_address": "보낼 주소 선택...",
|
||||
@@ -663,8 +974,10 @@
|
||||
"send_amount_details": "금액 상세",
|
||||
"send_amount_upper": "금액",
|
||||
"send_clear_fields": "모든 양식 필드를 지우시겠습니까?",
|
||||
"send_contacts_button": "연락처에서 선택",
|
||||
"send_copy_error": "오류 복사",
|
||||
"send_dismiss": "닫기",
|
||||
"send_err_needs_rescan": "지갑의 차폐 노트 데이터가 블록체인과 일치하지 않습니다(부트스트랩 또는 재색인 후에 발생합니다). 설정 → 블록체인 재스캔에서 전체 재스캔을 실행하고 완전히 끝날 때까지 기다린 뒤 다시 전송해 보세요.",
|
||||
"send_error_copied": "오류가 클립보드에 복사되었습니다",
|
||||
"send_error_prefix": "오류: %s",
|
||||
"send_exceeds_available": "사용 가능 금액 초과 (%.8f)",
|
||||
@@ -683,6 +996,7 @@
|
||||
"send_recipient": "수신자",
|
||||
"send_select_source": "보낼 주소 선택...",
|
||||
"send_sending_from": "보내는 곳",
|
||||
"send_status_unconfirmed": "거래 상태를 확인할 수 없습니다",
|
||||
"send_submitting": "거래 제출 중...",
|
||||
"send_switch_to_receive": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
|
||||
"send_to": "받는 곳",
|
||||
@@ -755,6 +1069,8 @@
|
||||
"settings_noise_opacity": "노이즈 불투명도:",
|
||||
"settings_not_encrypted": "암호화되지 않음",
|
||||
"settings_not_found": "찾을 수 없음",
|
||||
"settings_open_app_dir": "앱 폴더 열기",
|
||||
"settings_open_data_dir": "데이터 폴더 열기",
|
||||
"settings_other": "기타",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "개인 정보",
|
||||
@@ -788,16 +1104,24 @@
|
||||
"shield_check_status": "상태 확인",
|
||||
"shield_completed": "작업이 성공적으로 완료되었습니다!",
|
||||
"shield_description": "투명 주소의 코인베이스 출력을 차폐 주소로 전송하여 채굴 보상을 차폐합니다. 채굴 수입을 숨겨 프라이버시가 향상됩니다.",
|
||||
"shield_error_prefix": "오류: ",
|
||||
"shield_from_address": "보내는 주소:",
|
||||
"shield_funds": "자금 차폐",
|
||||
"shield_in_progress": "작업 진행 중...",
|
||||
"shield_max_utxos": "작업당 최대 UTXO 수",
|
||||
"shield_merge_done": "차폐/통합이 완료되었습니다!",
|
||||
"shield_op_failed": "작업 실패: ",
|
||||
"shield_op_submitted": "작업이 제출되었습니다: ",
|
||||
"shield_operation_id": "작업 ID: %s",
|
||||
"shield_select_z": "z 주소 선택...",
|
||||
"shield_send_failed": "차폐 실패: ",
|
||||
"shield_started": "차폐 작업이 시작되었습니다",
|
||||
"shield_status_check_error": "상태 확인 오류: ",
|
||||
"shield_status_label": "상태: ",
|
||||
"shield_submitting": "작업 제출 중...",
|
||||
"shield_title": "코인베이스 보상 차폐",
|
||||
"shield_to_address": "받는 주소 (차폐):",
|
||||
"shield_unknown_error": "알 수 없는 오류",
|
||||
"shield_utxo_limit": "UTXO 제한:",
|
||||
"shield_wildcard_hint": "'*'를 사용하여 모든 투명 주소에서 차폐",
|
||||
"shielded": "차폐",
|
||||
@@ -807,11 +1131,16 @@
|
||||
"shielding_notice": "참고: 투명 (T) 주소에서 프라이빗 (Z) 주소로 자금을 실딩합니다.",
|
||||
"show": "표시",
|
||||
"show_hidden": "숨겨진 항목 표시 (%d)",
|
||||
"show_qr": "QR 표시",
|
||||
"show_qr_code": "QR 코드 표시",
|
||||
"showing_transactions": "%d–%d / %d건의 거래 표시 중 (총: %zu)",
|
||||
"showing_x_of_y": "%d / %d 주소 표시",
|
||||
"simple_background": "단순 배경",
|
||||
"slider_off": "끔",
|
||||
"sort_amount_high": "금액 큰 순",
|
||||
"sort_amount_low": "금액 적은 순",
|
||||
"sort_date_newest": "최신순",
|
||||
"sort_date_oldest": "오래된 순",
|
||||
"start_mining": "채굴 시작",
|
||||
"status": "상태",
|
||||
"stop_external": "외부 데몬 중지",
|
||||
@@ -866,6 +1195,8 @@
|
||||
"tt_clear_ztx": "로컬에 캐시된 z-트랜잭션 기록 삭제",
|
||||
"tt_custom_fees": "거래 전송 시 수동 수수료 입력 활성화",
|
||||
"tt_custom_theme": "사용자 지정 테마 활성화됨",
|
||||
"tt_daemon_install_bundled": "노드를 중지하고 설치된 dragonxd를 이 지갑 빌드에 번들된 버전으로 덮어쓴 다음 재시작합니다",
|
||||
"tt_daemon_update_check": "프로젝트 Gitea에서 최신 dragonxd 풀 노드를 다운로드하고 검증한 다음, 재시작하여 적용합니다",
|
||||
"tt_debug_collapse": "디버그 로깅 옵션 접기",
|
||||
"tt_debug_expand": "디버그 로깅 옵션 펼치기",
|
||||
"tt_delete_blockchain": "모든 블록체인 데이터를 삭제하고 새로 동기화합니다. wallet.dat 및 설정은 보존됩니다.",
|
||||
@@ -881,15 +1212,19 @@
|
||||
"tt_keep_daemon": "설정 마법사를 실행하면 데몬이 여전히 중지됩니다",
|
||||
"tt_language": "지갑 UI 인터페이스 언어",
|
||||
"tt_layout_hotkey": "단축키: 좌/우 화살표 키로 잔액 레이아웃 전환",
|
||||
"tt_lite_redownload": "라이트 서버에서 모든 블록을 다시 다운로드하고 다시 스캔합니다",
|
||||
"tt_lock": "지갑 즉시 잠금",
|
||||
"tt_low_spec": "모든 고부하 시각 효과 비활성화\\n단축키: Ctrl+Shift+Down",
|
||||
"tt_merge": "여러 UTXO를 하나의 주소로 통합",
|
||||
"tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작",
|
||||
"tt_noise": "그레인 텍스처 강도 (0%% = 끔, 100%% = 최대)",
|
||||
"tt_open_app_dir": "파일 관리자에서 ObsidianDragon 폴더(설정, 테마, 로그)를 엽니다",
|
||||
"tt_open_data_dir": "지갑 및 블록체인 데이터가 있는 폴더를 파일 탐색기에서 엽니다",
|
||||
"tt_open_dir": "파일 탐색기에서 열려면 클릭",
|
||||
"tt_reduce_motion": "접근성을 위해 애니메이션 전환 및 잔액 보간 비활성화",
|
||||
"tt_remove_encrypt": "암호화를 제거하고 지갑을 보호 없이 저장",
|
||||
"tt_remove_pin": "PIN을 제거하고 잠금 해제 시 비밀번호 요구",
|
||||
"tt_repair_wallet": "지갑의 거래 기록을 블록체인에서 지우고 다시 만듭니다 (재스캔 후 전송에 실패하는 노트를 수정)",
|
||||
"tt_report_bug": "프로젝트 트래커에서 문제 보고",
|
||||
"tt_request_payment": "QR 코드가 포함된 결제 요청 생성",
|
||||
"tt_rescan": "누락된 거래를 찾기 위해 블록체인 재스캔",
|
||||
@@ -904,6 +1239,7 @@
|
||||
"tt_scan_themes": "새 테마 검색.\\n테마 폴더를 여기에 배치:\\n%s",
|
||||
"tt_scanline": "콘솔에서 CRT 스캔라인 효과",
|
||||
"tt_seed_backup": "지갑의 24단어 복구 시드 문구를 표시하고 백업합니다",
|
||||
"tt_seed_migrate": "새 시드 문구 지갑을 만들고 자금을 그곳으로 옮깁니다",
|
||||
"tt_set_pin": "빠른 잠금 해제를 위한 4-8자리 PIN 설정",
|
||||
"tt_shield_mining": "투명 채굴 보상을 차폐 주소로 이동",
|
||||
"tt_simple_bg": "배경에 단순 그라데이션 사용\\n단축키: Ctrl+Up",
|
||||
@@ -917,6 +1253,7 @@
|
||||
"tt_ui_opacity": "카드 및 사이드바 불투명도 (100%% = 완전 불투명, 낮을수록 더 투명)",
|
||||
"tt_validate": "DragonX 주소가 유효한지 확인",
|
||||
"tt_verbose": "콘솔 탭에 상세 연결 진단,\\n데몬 상태 및 포트 소유자 정보 기록",
|
||||
"tt_wallets_button": "지갑 파일 목록을 보고 전환합니다",
|
||||
"tt_website": "DragonX 웹사이트 열기",
|
||||
"tt_window_opacity": "배경 불투명도 (낮을수록 = 창을 통해 바탕 화면이 보임)",
|
||||
"tt_wizard": "초기 설정 마법사 다시 실행\\n데몬이 재시작됩니다",
|
||||
@@ -925,7 +1262,18 @@
|
||||
"tx_from_address": "보낸 주소:",
|
||||
"tx_id_label": "거래 ID:",
|
||||
"tx_immature": "미성숙",
|
||||
"tx_loading_enriching_sends": "보낸 거래 세부 정보 확인 중 (%d)",
|
||||
"tx_loading_fetching_transparent": "투명 내역을 가져오는 중",
|
||||
"tx_loading_history_progress": "이전 기록 불러오는 중 (%d%%)",
|
||||
"tx_loading_queued": "거래 새로고침 대기 중",
|
||||
"tx_loading_refreshing_cached": "지갑 내역을 새로고침하는 중 (%d개 캐시됨)",
|
||||
"tx_loading_scanning_shielded": "차폐 내역 스캔 중 (%d개 주소)",
|
||||
"tx_mined": "채굴됨",
|
||||
"tx_progress_balances": "잔액을 새로 고치는 중",
|
||||
"tx_progress_finalizing": "트랜잭션 마무리 중",
|
||||
"tx_progress_history": "기록 새로고침 중",
|
||||
"tx_progress_submitting": "데몬에 트랜잭션을 제출하는 중",
|
||||
"tx_progress_waiting_ops": "작업 대기 중 (%d)",
|
||||
"tx_received": "수신됨",
|
||||
"tx_sent": "전송됨",
|
||||
"tx_to_address": "받는 주소:",
|
||||
@@ -937,6 +1285,14 @@
|
||||
"unconfirmed": "미확인",
|
||||
"undo_clear": "지우기 취소",
|
||||
"unknown": "알 수 없음",
|
||||
"unmark_mining_address": "채굴 주소 표시 해제",
|
||||
"upd_back": "뒤로",
|
||||
"upd_install": "설치",
|
||||
"upd_installed_badge": "설치됨",
|
||||
"upd_no_build_platform": "이 플랫폼용 빌드가 없습니다",
|
||||
"upd_prerelease": "사전 릴리스",
|
||||
"upd_reinstall": "재설치",
|
||||
"upd_select_version": "설치할 버전을 선택하세요",
|
||||
"use_embedded_daemon": "내장 dragonxd 사용",
|
||||
"use_tor": "Tor 사용",
|
||||
"validate_btn": "검증",
|
||||
@@ -956,17 +1312,73 @@
|
||||
"verbose_logging": "상세 로깅",
|
||||
"version": "버전",
|
||||
"view": "보기",
|
||||
"view_all": "모두 보기",
|
||||
"view_details": "상세 보기",
|
||||
"view_on_explorer": "탐색기에서 보기",
|
||||
"waiting_for_daemon": "데몬 연결 대기 중...",
|
||||
"wallet": "지갑",
|
||||
"wallet_empty": "지갑이 비어 있습니다",
|
||||
"wallet_empty_hint": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
|
||||
"wallets_active": "활성",
|
||||
"wallets_add": "폴더 추가",
|
||||
"wallets_add_folder": "지갑 파일을 검색할 폴더를 추가로 지정:",
|
||||
"wallets_add_folder_toggle": "+ 다른 폴더에서 지갑 검색…",
|
||||
"wallets_button": "지갑…",
|
||||
"wallets_col_addresses": "주소",
|
||||
"wallets_col_balance": "잔액",
|
||||
"wallets_col_name": "지갑",
|
||||
"wallets_col_opened": "마지막 열람",
|
||||
"wallets_col_size": "크기",
|
||||
"wallets_create": "지갑 만들기",
|
||||
"wallets_creating": "지갑을 만드는 중 — 노드가 재시작됩니다…",
|
||||
"wallets_current": "현재",
|
||||
"wallets_exists": "같은 이름의 지갑이 이미 존재합니다.",
|
||||
"wallets_external_tt": "데이터 디렉터리 외부에 있음 — 가져오기로 복사합니다.",
|
||||
"wallets_folder_hint": "wallet-*.dat 파일이 있는 폴더 경로",
|
||||
"wallets_folder_invalid": "해당 폴더가 존재하지 않습니다.",
|
||||
"wallets_import": "가져오기",
|
||||
"wallets_import_failed": "해당 지갑 파일을 가져올 수 없습니다.",
|
||||
"wallets_import_hint": "가져와서 열기",
|
||||
"wallets_import_tt": "이 지갑은 데이터 디렉터리 밖에 있습니다. 열면 먼저 안으로 복사됩니다(출시 예정).",
|
||||
"wallets_imported": "지갑을 가져왔습니다 — 전환 중…",
|
||||
"wallets_intro": "데이터 디렉터리(및 추가한 폴더)에 있는 지갑 파일입니다. 하나를 열면 전환됩니다 — 노드가 재시작되어 해당 지갑을 불러오며, 각 지갑은 고유한 데이터를 유지합니다.",
|
||||
"wallets_name_invalid": "유효한 지갑 이름을 입력하세요.",
|
||||
"wallets_never": "없음",
|
||||
"wallets_new_hint": "이름(예: savings)",
|
||||
"wallets_new_label": "새 지갑 만들기:",
|
||||
"wallets_open": "열기",
|
||||
"wallets_reveal": "폴더 열기",
|
||||
"wallets_title": "지갑",
|
||||
"warning": "경고",
|
||||
"warning_upper": "경고!",
|
||||
"website": "웹사이트",
|
||||
"window_opacity": "창 투명도",
|
||||
"wizard_daemon_start_failed": "데몬 시작 실패 — 자동으로 재시도됩니다",
|
||||
"xmrig_browse_releases": "모든 릴리스 보기…",
|
||||
"xmrig_checking": "최신 채굴기를 확인하는 중…",
|
||||
"xmrig_current": "현재:",
|
||||
"xmrig_download_install": "다운로드 및 설치",
|
||||
"xmrig_downloading": "다운로드 중…",
|
||||
"xmrig_installed": "설치됨:",
|
||||
"xmrig_installed_ok": "채굴기가 설치되었습니다",
|
||||
"xmrig_installing": "설치 중…",
|
||||
"xmrig_latest": "최신:",
|
||||
"xmrig_loading_releases": "릴리스를 불러오는 중…",
|
||||
"xmrig_none": "없음",
|
||||
"xmrig_reinstall": "재설치",
|
||||
"xmrig_stop_mining_first": "채굴기를 업데이트하기 전에 채굴을 중지하세요.",
|
||||
"xmrig_unavailable_body": "이 플랫폼에서 사용 가능한 채굴기 빌드가 없습니다.",
|
||||
"xmrig_unavailable_title": "채굴기 업데이트를 사용할 수 없습니다",
|
||||
"xmrig_unknown_error": "알 수 없는 오류.",
|
||||
"xmrig_up_to_date": "채굴기가 최신 상태입니다",
|
||||
"xmrig_update_available": "새 채굴기를 사용할 수 있습니다",
|
||||
"xmrig_update_button": "채굴기 업데이트…",
|
||||
"xmrig_update_failed": "업데이트 실패",
|
||||
"xmrig_update_short": "업데이트",
|
||||
"xmrig_update_title": "채굴기 업데이트",
|
||||
"xmrig_verify_note": "다운로드는 설치 전에 릴리스에 게시된 SHA-256 체크섬과 대조하여 검증됩니다.",
|
||||
"xmrig_verifying": "확인 중…",
|
||||
"xmrig_version": "버전:",
|
||||
"yes_clear": "예, 지우기",
|
||||
"your_addresses": "내 주소",
|
||||
"z_address": "Z 주소",
|
||||
|
||||
412
res/lang/pt.json
412
res/lang/pt.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "Adicionar Endereço",
|
||||
"address_book_add_new": "Adicionar Novo",
|
||||
"address_book_added": "Endereço adicionado ao livro",
|
||||
"address_book_confirm_delete": "Confirmar exclusão?",
|
||||
"address_book_count": "%zu endereços salvos",
|
||||
"address_book_deleted": "Entrada excluída",
|
||||
"address_book_edit": "Editar Endereço",
|
||||
@@ -39,6 +40,7 @@
|
||||
"address_copied": "Endereço copiado para a área de transferência",
|
||||
"address_details": "Detalhes do Endereço",
|
||||
"address_label": "Endereço:",
|
||||
"address_reorder_hint": "Solte na borda de uma linha para reordenar, ou no centro dela para transferir",
|
||||
"address_upper": "ENDEREÇO",
|
||||
"address_url": "URL do Endereço",
|
||||
"addresses_appear_here": "Seus endereços de recebimento aparecerão aqui após a conexão.",
|
||||
@@ -69,7 +71,10 @@
|
||||
"backup_wallet": "Fazer Backup da Carteira...",
|
||||
"backup_wallet_not_found": "Aviso: wallet.dat não encontrado no local esperado",
|
||||
"balance": "Saldo",
|
||||
"balance_history_collecting": "Histórico de saldo — coletando dados...",
|
||||
"balance_layout": "Layout do Saldo",
|
||||
"balance_shielded_fmt": "Blindado: %.8f",
|
||||
"balance_transparent_fmt": "Transparente: %.8f",
|
||||
"ban": "Banir",
|
||||
"banned_peers": "Pares Banidos",
|
||||
"block": "Bloco",
|
||||
@@ -151,6 +156,20 @@
|
||||
"confirm_delete_blockchain_msg": "Isso irá parar o daemon, excluir todos os dados da blockchain (blocks, chainstate, peers) e iniciar uma nova sincronização do zero. Isso pode levar várias horas.",
|
||||
"confirm_delete_blockchain_safe": "Seu wallet.dat, configuração e histórico de transações estão seguros e não serão excluídos.",
|
||||
"confirm_delete_blockchain_title": "Excluir Dados da Blockchain",
|
||||
"confirm_lite_redownload_msg": "Isso limpa os dados de blocos baixados da carteira e busca e reescaneia novamente todos os blocos do servidor lite. Pode demorar um pouco; a carteira mostra o progresso da sincronização até terminar.",
|
||||
"confirm_lite_redownload_safe": "Sua carteira, chaves e frase de recuperação não são afetadas — apenas os dados dos blocos são baixados novamente.",
|
||||
"confirm_lite_redownload_title": "Baixar Blocos Novamente",
|
||||
"confirm_reinstall_daemon_msg": "Isto para o daemon, substitui o dragonxd instalado (e o dragonx-cli/dragonx-tx) pelas versões incluídas nesta build da carteira e, em seguida, reinicia o nó. Use isto para recuperar ou atualizar o binário do nó.",
|
||||
"confirm_reinstall_daemon_safe": "Sua carteira, chaves e dados da blockchain não são tocados — apenas os arquivos de programa do daemon são substituídos.",
|
||||
"confirm_reinstall_daemon_title": "Instalar Daemon Empacotado",
|
||||
"confirm_repair_wallet_msg": "Isso reinicia o daemon com -zapwallettxes=2: ele exclui todos os registros de transações e notas da carteira e depois os reconstrói a partir da blockchain. Use isso quando as transações falharem ao serem construídas (\"Invalid sapling spend proof\" / \"shielded requirements not met\") mesmo após um reescaneamento completo. Leva muito tempo e a carteira fica offline até terminar.",
|
||||
"confirm_repair_wallet_safe": "Suas chaves, endereços e saldo são preservados — apenas os registros de transações em cache são reconstruídos.",
|
||||
"confirm_repair_wallet_title": "Reparar Carteira",
|
||||
"confirm_rescan_msg": "Isto reinicia o daemon e reescaneia toda a blockchain em busca das transações da sua carteira. Pode demorar bastante e a carteira permanece offline até terminar.",
|
||||
"confirm_rescan_safe": "Seu wallet.dat e os dados da blockchain não são excluídos — apenas reescaneados.",
|
||||
"confirm_rescan_title": "Reescanear Blockchain",
|
||||
"confirm_restart_daemon_msg": "Isso para e reinicia o daemon para aplicar as opções alteradas. A carteira será brevemente desconectada e reconectada.",
|
||||
"confirm_restart_daemon_title": "Reiniciar Daemon",
|
||||
"confirm_send": "Confirmar Envio",
|
||||
"confirm_transaction": "Confirmar Transação",
|
||||
"confirm_transfer": "Confirmar Transferência",
|
||||
@@ -161,6 +180,7 @@
|
||||
"connected_peers": "Pares Conectados",
|
||||
"connecting": "Conectando...",
|
||||
"console": "Console",
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Rolagem automática",
|
||||
"console_available_commands": "Comandos disponíveis:",
|
||||
"console_capturing_output": "Capturando saída do daemon...",
|
||||
@@ -177,6 +197,7 @@
|
||||
"console_connected": "Conectado ao daemon",
|
||||
"console_copy_all": "Copiar Tudo",
|
||||
"console_copy_selected": "Copiar",
|
||||
"console_copy_value": "Copiar",
|
||||
"console_daemon": "Daemon",
|
||||
"console_daemon_error": "Erro do daemon!",
|
||||
"console_daemon_started": "Daemon iniciado",
|
||||
@@ -195,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - Controlar mineração",
|
||||
"console_help_stop": " stop - Parar o daemon",
|
||||
"console_line_count": "%zu linhas",
|
||||
"console_matches": "correspondências",
|
||||
"console_new_lines": "%d novas linhas",
|
||||
"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_rpc_reference": "Referência de Comandos RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Scanline do console",
|
||||
"console_search_commands": "Pesquisar comandos...",
|
||||
"console_select_all": "Selecionar Tudo",
|
||||
"console_show_app_output": "Mostrar linhas do log da carteira [app]",
|
||||
"console_show_daemon_output": "Mostrar saída do daemon",
|
||||
"console_show_errors_only": "Mostrar apenas erros",
|
||||
"console_show_rpc_ref": "Mostrar referência de comandos RPC",
|
||||
"console_show_rpc_trace": "Mostrar chamadas RPC do app",
|
||||
"console_showing_lines": "Mostrando %zu de %zu linhas",
|
||||
"console_starting_node": "Iniciando nó...",
|
||||
"console_status_error": "Erro",
|
||||
@@ -218,6 +244,12 @@
|
||||
"console_welcome": "Bem-vindo ao Console ObsidianDragon",
|
||||
"console_zoom_in": "Aumentar zoom",
|
||||
"console_zoom_out": "Diminuir zoom",
|
||||
"contact_global": "Mostrar em todas as carteiras (contato global)",
|
||||
"contact_global_badge_tt": "Contato global — visível em todas as carteiras",
|
||||
"contact_global_tt": "Ativado: este contato permanece visível em qualquer carteira que você carregar. Desativado: ele pertence apenas à carteira atual.",
|
||||
"contacts": "Contatos",
|
||||
"contacts_search_no_match": "Nenhum contato correspondente",
|
||||
"contacts_search_placeholder": "Pesquisar contatos...",
|
||||
"copied": "Copiado!",
|
||||
"copy": "Copiar",
|
||||
"copy_address": "Copiar Endereço Completo",
|
||||
@@ -227,6 +259,44 @@
|
||||
"copy_uri": "Copiar URI",
|
||||
"current_price": "Preço Atual",
|
||||
"custom_fees": "Taxas personalizadas",
|
||||
"daemon_binary": "Binário do daemon",
|
||||
"daemon_bundled": "Empacotado",
|
||||
"daemon_install_bundled": "Instalar incluído",
|
||||
"daemon_installed": "Instalado",
|
||||
"daemon_none_bundled": "nenhum nesta build",
|
||||
"daemon_not_installed": "não instalado",
|
||||
"daemon_status_differ": "O binário instalado difere da versão empacotada.",
|
||||
"daemon_status_match": "O binário instalado corresponde à versão incluída.",
|
||||
"daemon_status_missing": "Nenhum daemon instalado — instale a versão incluída.",
|
||||
"daemon_update_available": "Uma nova versão do nó está disponível",
|
||||
"daemon_update_body": "Esta versão da carteira inclui um nó DragonX mais recente do que o atualmente instalado. A atualização substitui o dragonxd instalado (e o dragonx-cli/dragonx-tx), depois para e reinicia o nó para que a nova versão entre em vigor. Recomendado — um nó mais recente pode adicionar recursos (por exemplo, suporte a frase de recuperação) que o antigo não tem.",
|
||||
"daemon_update_browse": "Ver todos os lançamentos…",
|
||||
"daemon_update_check": "Verificar atualizações…",
|
||||
"daemon_update_checking": "Verificando o nó mais recente…",
|
||||
"daemon_update_downgrade_note": "Versões mais antigas podem ser incompatíveis com seus dados de cadeia atuais. Instalar uma versão diferente entra em vigor após reiniciar o daemon.",
|
||||
"daemon_update_download_install": "Baixar e instalar",
|
||||
"daemon_update_downloading": "Baixando…",
|
||||
"daemon_update_failed": "Falha na atualização",
|
||||
"daemon_update_installed": "Instalado:",
|
||||
"daemon_update_installed_ok": "Nó instalado",
|
||||
"daemon_update_installing": "Instalando…",
|
||||
"daemon_update_keep": "Manter atual",
|
||||
"daemon_update_later": "Depois",
|
||||
"daemon_update_latest": "Mais recente:",
|
||||
"daemon_update_loading": "Carregando versões…",
|
||||
"daemon_update_now": "Atualizar agora",
|
||||
"daemon_update_reinstall": "Reinstalar",
|
||||
"daemon_update_restart_note": "Reinicie o daemon para começar a executar a nova versão.",
|
||||
"daemon_update_restart_now": "Reiniciar daemon agora",
|
||||
"daemon_update_safe": "Sua carteira, chaves e dados da blockchain não são tocados — apenas os arquivos de programa do daemon são substituídos. Se você executa deliberadamente um nó personalizado, escolha Manter atual.",
|
||||
"daemon_update_title": "Atualizar o daemon do nó?",
|
||||
"daemon_update_unavailable_body": "Nenhuma versão do nó está disponível para esta plataforma.",
|
||||
"daemon_update_unavailable_title": "Atualizações do nó indisponíveis",
|
||||
"daemon_update_unknown_error": "Erro desconhecido.",
|
||||
"daemon_update_up_to_date": "O nó está atualizado",
|
||||
"daemon_update_verify_note": "O download é verificado contra o SHA-256 publicado do lançamento e uma assinatura ed25519 fixada antes da instalação.",
|
||||
"daemon_update_verifying": "Verificando…",
|
||||
"daemon_update_version": "Versão:",
|
||||
"daemon_version": "Daemon",
|
||||
"dark": "Escuro",
|
||||
"date": "Data",
|
||||
@@ -257,10 +327,14 @@
|
||||
"explorer_block_time": "Hora",
|
||||
"explorer_block_txs": "Transações",
|
||||
"explorer_chain_stats": "Cadeia",
|
||||
"explorer_hash_not_found": "Nenhum bloco ou transação encontrado para este hash",
|
||||
"explorer_invalid_query": "Insira uma altura de bloco ou um hash de 64 caracteres",
|
||||
"explorer_invalid_response": "Resposta inválida do daemon",
|
||||
"explorer_mempool": "Mempool",
|
||||
"explorer_mempool_size": "Tamanho",
|
||||
"explorer_mempool_txs": "Transações",
|
||||
"explorer_no_results": "Nenhum bloco em cache correspondente",
|
||||
"explorer_not_connected": "Não conectado ao daemon — não é possível procurar um bloco ou hash de transação",
|
||||
"explorer_recent_blocks": "Blocos Recentes",
|
||||
"explorer_search": "Pesquisar",
|
||||
"explorer_section": "EXPLORADOR",
|
||||
@@ -312,6 +386,7 @@
|
||||
"hidden_tag": " (oculto)",
|
||||
"hide": "Ocultar",
|
||||
"hide_address": "Ocultar endereço",
|
||||
"hide_qr": "Ocultar QR",
|
||||
"hide_zero_balances": "Ocultar saldos zero",
|
||||
"history": "Histórico",
|
||||
"immature_type": "Imaturo",
|
||||
@@ -350,19 +425,120 @@
|
||||
"label_placeholder": "ex. Poupança, Mineração...",
|
||||
"language": "Idioma",
|
||||
"light": "Claro",
|
||||
"lite_account_label": "Conta",
|
||||
"lite_action": "Ação",
|
||||
"lite_backup_keys": "Backup e chaves",
|
||||
"lite_birthday_backup": "Aniversário: %llu (faça o backup disto também)",
|
||||
"lite_birthday_hint": "Altura do bloco a partir da qual começar a escanear. Deixe 0 se desconhecida (escaneamento completo mais lento).",
|
||||
"lite_birthday_label": "Data de nascimento",
|
||||
"lite_console_help_passthrough": "Qualquer outra entrada é executada como um comando de console da carteira leve.",
|
||||
"lite_copy": "Copiar",
|
||||
"lite_could_not_write": "Não foi possível gravar ",
|
||||
"lite_encrypt_wallet": "Criptografar carteira",
|
||||
"lite_encryption_removed": "Criptografia removida",
|
||||
"lite_hide_wipe": "Ocultar e apagar",
|
||||
"lite_import": "Importar",
|
||||
"lite_import_key_label": "Importar chave",
|
||||
"lite_key_imported": "Chave importada — execute uma sincronização para escanear seu histórico",
|
||||
"lite_lock_failed": "Falha ao bloquear",
|
||||
"lite_lock_now": "Bloquear agora",
|
||||
"lite_maintenance": "Manutenção",
|
||||
"lite_net_add": "Adicionar",
|
||||
"lite_net_add_label_hint": "Rótulo (opcional)",
|
||||
"lite_net_add_url_hint": "https://seu-servidor-lite",
|
||||
"lite_net_checking": "verificando…",
|
||||
"lite_net_connected": "Conectado",
|
||||
"lite_net_custom": "Personalizado",
|
||||
"lite_net_disconnected": "Não conectado",
|
||||
"lite_net_hidden_section": "Servidores ocultos",
|
||||
"lite_net_hide": "Ocultar",
|
||||
"lite_net_in_use": "Em uso",
|
||||
"lite_net_intro": "Escolha um servidor para usar, ou deixe a carteira escolher um aleatoriamente. As alterações são aplicadas imediatamente.",
|
||||
"lite_net_invalid_url": "Insira uma URL http(s):// válida.",
|
||||
"lite_net_no_wallet": "Nenhuma carteira aberta",
|
||||
"lite_net_official": "Oficial",
|
||||
"lite_net_offline": "offline",
|
||||
"lite_net_random_active": "A seleção aleatória de servidor está ativa.",
|
||||
"lite_net_random_server": "Servidor aleatório",
|
||||
"lite_net_refresh": "Atualizar",
|
||||
"lite_net_show_hidden": "Mostrar servidores ocultos",
|
||||
"lite_net_sync_label": "Sincronização",
|
||||
"lite_net_synced": "Sincronizado",
|
||||
"lite_net_syncing": "Sincronizando",
|
||||
"lite_net_title": "Servidores Lite",
|
||||
"lite_net_unhide": "Reexibir",
|
||||
"lite_net_use_random": "Usar um servidor aleatório a cada vez",
|
||||
"lite_no_wallet": "Nenhuma carteira aberta — crie ou abra uma em Configurações",
|
||||
"lite_no_wallet_short": "Nenhuma carteira aberta",
|
||||
"lite_op_create": "Criar",
|
||||
"lite_op_open": "Abrir",
|
||||
"lite_op_restore": "Restaurar",
|
||||
"lite_overwrite": "Sobrescrever",
|
||||
"lite_passphrase_label": "Senha",
|
||||
"lite_private_keys_warning": "Chaves privadas — qualquer pessoa que as tenha pode gastar seus fundos",
|
||||
"lite_redownload_blocks": "Rebaixar blocos",
|
||||
"lite_redownload_desc": "Rebaixar todos os blocos do servidor lite (use se o seu saldo ou histórico parecer incorreto).",
|
||||
"lite_redownload_running": "Rebaixando blocos…",
|
||||
"lite_remove_encryption": "Remover criptografia",
|
||||
"lite_restore_birthday_label": "Bloco de nascimento (opcional — 0 escaneia desde o início, mais lento)",
|
||||
"lite_restore_btn": "Restaurar carteira",
|
||||
"lite_restore_intro": "Digite sua frase de recuperação. A carteira será restaurada e depois sincronizada a partir do servidor lite.",
|
||||
"lite_restore_ok": "Carteira restaurada — sincronizando a partir do servidor lite…",
|
||||
"lite_restore_seed_label": "Frase de recuperação (palavras separadas por espaços)",
|
||||
"lite_restore_title": "Restaurar a partir da frase de recuperação",
|
||||
"lite_save_to_file": "Salvar em arquivo",
|
||||
"lite_saved_to": "Salvo (texto simples, apenas para o dono) em ",
|
||||
"lite_security": "Segurança",
|
||||
"lite_seed_label": "Semente",
|
||||
"lite_seed_warning": "Frase de recuperação — a ÚNICA forma de restaurar sua carteira. Anote-a, guarde-a offline, nunca a compartilhe.",
|
||||
"lite_servers_network_tab": "Os servidores leves são gerenciados na aba Rede.",
|
||||
"lite_show_private_keys": "Mostrar chaves privadas",
|
||||
"lite_show_seed": "Mostrar semente",
|
||||
"lite_unlock": "Desbloquear",
|
||||
"lite_unlock_btn": "Desbloquear",
|
||||
"lite_unlock_failed": "Falha ao desbloquear — senha incorreta?",
|
||||
"lite_unlock_msg": "Digite sua senha para desbloquear a carteira para gastos.",
|
||||
"lite_unlock_ok": "Carteira desbloqueada — você já pode enviar",
|
||||
"lite_unlock_title": "Desbloquear carteira",
|
||||
"lite_validate": "Validar",
|
||||
"lite_wallet_encrypted": "Carteira criptografada",
|
||||
"lite_wallet_label": "Carteira",
|
||||
"lite_wallet_locked": "Carteira bloqueada",
|
||||
"lite_wallet_ready": "Carteira pronta",
|
||||
"lite_wallet_request": "Solicitação da carteira lite",
|
||||
"lite_wallet_unlocked": "Carteira desbloqueada",
|
||||
"lite_welcome_create": "Criar nova carteira",
|
||||
"lite_welcome_create_failed": "Não foi possível criar a carteira",
|
||||
"lite_welcome_created": "Carteira criada — faça o backup da sua frase de recuperação agora em Configurações → Backup e chaves",
|
||||
"lite_welcome_later": "Mais tarde",
|
||||
"lite_welcome_msg": "Você ainda não tem uma carteira. Crie uma nova ou restaure a partir de uma frase de recuperação.",
|
||||
"lite_welcome_restore": "Restaurar a partir da semente",
|
||||
"lite_welcome_restore_hint": "Restaure sua carteira em Configurações → Carteira lite",
|
||||
"lite_welcome_title": "Bem-vindo ao ObsidianDragon Lite",
|
||||
"lite_word_count": "%d / 24 palavras",
|
||||
"lite_working": "Processando…",
|
||||
"loading": "Carregando...",
|
||||
"loading_addresses": "Carregando endereços...",
|
||||
"loading_transactions": "Carregando transações",
|
||||
"local_hashrate": "Hashrate Local",
|
||||
"low_spec_mode": "Modo econômico",
|
||||
"mark_mining_address": "Marcar como endereço de mineração",
|
||||
"market": "Mercado",
|
||||
"market_12h": "12h",
|
||||
"market_18h": "18h",
|
||||
"market_24h": "24h",
|
||||
"market_24h_change": "24h",
|
||||
"market_24h_volume": "VOLUME 24H",
|
||||
"market_6h": "6h",
|
||||
"market_attribution": "Dados de preço do NonKYC",
|
||||
"market_btc_price": "PREÇO BTC",
|
||||
"market_cap": "Capitalização",
|
||||
"market_cap_short": "Cap.",
|
||||
"market_iv_1d": "1D",
|
||||
"market_iv_1h": "1H",
|
||||
"market_iv_1m": "1M",
|
||||
"market_iv_1w": "1S",
|
||||
"market_iv_live": "Ao vivo",
|
||||
"market_no_history": "Nenhum histórico de preços disponível",
|
||||
"market_no_price": "Sem dados de preço",
|
||||
"market_now": "Agora",
|
||||
@@ -372,6 +548,8 @@
|
||||
"market_price_unavailable": "Dados de preço indisponíveis",
|
||||
"market_refresh_price": "Atualizar dados de preço",
|
||||
"market_trade_on": "Negociar no %s",
|
||||
"market_updated": "\\xc2\\xb7 Atualizado %s",
|
||||
"market_vol_short": "Vol",
|
||||
"mature": "Maduro",
|
||||
"max": "Máx",
|
||||
"memo": "Memo (opcional, criptografado)",
|
||||
@@ -381,8 +559,52 @@
|
||||
"memo_z_only": "Nota: Memos só estão disponíveis ao enviar para endereços blindados (z)",
|
||||
"merge_description": "Fundir múltiplos UTXOs em um único endereço blindado. Isso pode ajudar a reduzir o tamanho da carteira e melhorar a privacidade.",
|
||||
"merge_funds": "Fundir Fundos",
|
||||
"merge_send_failed": "Falha na fusão: ",
|
||||
"merge_started": "Operação de fusão iniciada",
|
||||
"merge_title": "Fundir para Endereço",
|
||||
"mig_adopt": "Tornar esta minha carteira",
|
||||
"mig_adopt_now": "Adotar carteira semente",
|
||||
"mig_adopting": "Instalando sua nova carteira e reescaneando…",
|
||||
"mig_adopting_note": "O nó está reiniciando com sua nova carteira e irá reescanear a blockchain — isso pode levar alguns minutos. Deixe a carteira em execução; ela reconecta automaticamente.",
|
||||
"mig_already_mnemonic": "Sua carteira já tem uma frase de recuperação de 24 palavras — não há nada a migrar. Basta fazer o backup dela e guardar as palavras em um local seguro.",
|
||||
"mig_backed_up": "Anotei minha frase de recuperação",
|
||||
"mig_backup_instead": "Fazer backup da frase de recuperação",
|
||||
"mig_balance": "Saldo da carteira atual: %.8f DRGX (menos uma pequena taxa)",
|
||||
"mig_check_failed": "Não foi possível verificar sua carteira. Verifique se ela está desbloqueada e conectada e depois verifique novamente.",
|
||||
"mig_checking_balance": "Verificando seu saldo",
|
||||
"mig_confirming": "Aguardando a varredura ser confirmada na blockchain antes de trocar de carteira — isso garante que seus fundos realmente foram movidos.",
|
||||
"mig_confs": "Confirmações da varredura: %d",
|
||||
"mig_continue_sweep": "Continuar para a varredura",
|
||||
"mig_copy_seed": "Copiar frase de recuperação",
|
||||
"mig_create": "Criar carteira semente",
|
||||
"mig_daemon_too_old": "Seu nó DragonX é muito antigo para criar uma carteira semente — ele não tem o suporte a frase de recuperação necessário para isso. Atualize o nó (Ajustes -> NÓ & SEGURANÇA -> Instalar incluído, ou Verificar atualizações) e tente novamente.",
|
||||
"mig_discard": "Descartar migração",
|
||||
"mig_done": "Migração concluída. Sua carteira agora é protegida pela sua frase de recuperação.",
|
||||
"mig_done_btn": "Concluído",
|
||||
"mig_done_detail": "O daemon está reescaneando para exibir seus fundos — isso pode levar alguns minutos. Sua carteira anterior foi salva como um arquivo .bak na pasta de dados.",
|
||||
"mig_intro": "Isso cria uma carteira totalmente nova, protegida por uma frase de recuperação de 24 palavras, em um nó isolado, para que você possa mover seus fundos para ela. Sua carteira atual NÃO é alterada e NENHUM fundo é movido nesta etapa — primeiro você fará o backup da nova frase de recuperação e, em seguida, varrerá seus fundos para ela como uma etapa separada e confirmada.",
|
||||
"mig_later": "Depois",
|
||||
"mig_no_funds": "Sua carteira não tem fundos para migrar. Você pode adotar a nova carteira semente diretamente — ela substitui sua carteira atual (vazia) pela carteira protegida por frase de recuperação. Sua carteira antiga ainda é movida para um backup com data e hora, por precaução.",
|
||||
"mig_nofunds_confirm": "Entendo que isso substitui minha carteira atual pela nova carteira semente",
|
||||
"mig_not_connected": "Conecte-se ao seu nó primeiro e depois reabra esta janela para migrar.",
|
||||
"mig_precheck_checking": "Verificando sua carteira",
|
||||
"mig_receive_addr": "Endereço de recebimento da nova carteira:",
|
||||
"mig_recheck": "Verificar novamente",
|
||||
"mig_remainder": "Alguns fundos ainda estão na sua carteira antiga (entradas demais para uma única transação) — varra o restante antes de trocar.",
|
||||
"mig_remaining": "Restante na carteira antiga: %.8f DRGX",
|
||||
"mig_seed_warning": "Anote estas 24 palavras em ordem e guarde-as offline. Elas são o único backup da sua nova carteira — se você as perder, os fundos que migrar estarão perdidos para sempre.",
|
||||
"mig_step1_done": "Etapa 1 de 2 concluída — seus fundos AINDA NÃO foram movidos. Varrê-los para esta nova carteira é a próxima etapa.",
|
||||
"mig_sweep_all": "Varrer todos os fundos",
|
||||
"mig_sweep_intro": "Agora varra todos os fundos da sua carteira atual para a nova carteira semente. Isso envia uma única transação para o endereço da nova carteira; sua frase de recuperação (já salva em backup) controla os fundos daqui em diante.",
|
||||
"mig_sweep_remaining": "Varrer o restante",
|
||||
"mig_sweeping": "Varrendo seus fundos para a nova carteira…",
|
||||
"mig_title": "Migrar para uma carteira semente",
|
||||
"mig_to": "Para: ",
|
||||
"mig_txid": "txid: ",
|
||||
"mig_unlock_first": "Desbloqueie sua carteira primeiro para gastar a partir dela.",
|
||||
"mig_unlock_to_migrate": "Desbloqueie sua carteira primeiro e depois reabra esta janela para migrar.",
|
||||
"mig_waiting_mine": "Aguardando a transação de varredura ser minerada…",
|
||||
"mig_working": "Criando sua nova carteira semente em um nó isolado — isso pode levar um minuto. Sua carteira principal continua em execução.",
|
||||
"mine_when_idle": "Minerar quando ocioso",
|
||||
"mined": "minerado",
|
||||
"mined_filter": "Minerado",
|
||||
@@ -394,6 +616,8 @@
|
||||
"mining_address_copied": "Endereço de mineração copiado",
|
||||
"mining_all_time": "Todo o Tempo",
|
||||
"mining_already_saved": "URL do pool já salva",
|
||||
"mining_auto_balance_desc": "Distribui os mineradores pelos pools oficiais por hashrate para ajudar a descentralizar a rede. Verifica cada pool periodicamente.",
|
||||
"mining_auto_balanced_to": "Mineração autobalanceada para",
|
||||
"mining_benchmark_cancel": "Cancelar benchmark",
|
||||
"mining_benchmark_cooling": "Resfriando",
|
||||
"mining_benchmark_dismiss": "Fechar",
|
||||
@@ -430,8 +654,10 @@
|
||||
"mining_idle_threads_active_tooltip": "Threads quando o usuário está ativo",
|
||||
"mining_idle_threads_idle_tooltip": "Threads quando o sistema está ocioso",
|
||||
"mining_local_hashrate": "Hashrate Local",
|
||||
"mining_manual_desc": "Manual: minere no pool que você escolher da lista abaixo.",
|
||||
"mining_mine": "Minerar",
|
||||
"mining_mining_addr": "End. Mineração",
|
||||
"mining_mining_here": "minerando aqui",
|
||||
"mining_network": "Rede",
|
||||
"mining_no_blocks_yet": "Nenhum bloco encontrado ainda",
|
||||
"mining_no_payouts_yet": "Nenhum pagamento de pool ainda",
|
||||
@@ -441,18 +667,24 @@
|
||||
"mining_on": "Mineração está LIGADA",
|
||||
"mining_open_in_explorer": "Abrir no explorador",
|
||||
"mining_payout_address": "Endereço de Pagamento",
|
||||
"mining_payout_foreign": "⚠ Este endereço de pagamento não está na sua carteira atual — as recompensas mineradas iriam para uma carteira diferente. Atualize-o se você trocou de carteira.",
|
||||
"mining_payout_tooltip": "Endereço para receber recompensas de mineração",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_fee": "Taxa",
|
||||
"mining_pool_hashrate": "Hashrate do Pool",
|
||||
"mining_pool_url": "URL do Pool",
|
||||
"mining_pools_header": "POOLS",
|
||||
"mining_recent_blocks": "BLOCOS RECENTES",
|
||||
"mining_recent_payouts": "PAGAMENTOS DE POOL RECENTES",
|
||||
"mining_refresh": "Atualizar",
|
||||
"mining_remove": "Remover",
|
||||
"mining_reset_defaults": "Redefinir Padrões",
|
||||
"mining_save_payout_address": "Salvar endereço de pagamento",
|
||||
"mining_save_pool_url": "Salvar URL do pool",
|
||||
"mining_saved_addresses": "Endereços Salvos:",
|
||||
"mining_saved_pools": "Pools Salvos:",
|
||||
"mining_select_auto": "Balanceamento automático",
|
||||
"mining_select_manual": "Manual",
|
||||
"mining_shares": "Shares",
|
||||
"mining_show_chart": "Gráfico",
|
||||
"mining_show_log": "Log",
|
||||
@@ -465,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "Pare a mineração solo para alterar as configurações do pool",
|
||||
"mining_stopping": "Parando...",
|
||||
"mining_stopping_tooltip": "Minerador está parando...",
|
||||
"mining_suggested_pools": "Pools sugeridos",
|
||||
"mining_syncing_tooltip": "Blockchain está sincronizando...",
|
||||
"mining_tag": " · Mineração",
|
||||
"mining_threads": "Threads de Mineração",
|
||||
"mining_to_save": "para salvar",
|
||||
"mining_today": "Hoje",
|
||||
@@ -486,10 +720,12 @@
|
||||
"no_addresses_match": "Nenhum endereço corresponde ao filtro",
|
||||
"no_addresses_with_balance": "Nenhum endereço com saldo",
|
||||
"no_addresses_yet": "Nenhum endereço ainda",
|
||||
"no_icons_found": "Nenhum ícone corresponde à sua pesquisa.",
|
||||
"no_matching": "Nenhuma transação correspondente",
|
||||
"no_recent_receives": "Nenhum recebimento recente",
|
||||
"no_recent_sends": "Nenhum envio recente",
|
||||
"no_transactions": "Nenhuma transação encontrada",
|
||||
"no_transactions_yet": "Nenhuma transação ainda",
|
||||
"node": "NÓ",
|
||||
"node_security": "NÓ & SEGURANÇA",
|
||||
"noise": "Ruído",
|
||||
@@ -553,7 +789,66 @@
|
||||
"pending": "Pendente",
|
||||
"pin_not_set": "PIN não definido. Use a senha para desbloquear.",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "Adicionar entrada",
|
||||
"portfolio_add_to": "Adicionar ao portfólio",
|
||||
"portfolio_addresses_hdr": "Endereços",
|
||||
"portfolio_addresses_sel": "%d selecionado(s)",
|
||||
"portfolio_all_funds": "Todos os fundos",
|
||||
"portfolio_all_shielded": "Todos blindados",
|
||||
"portfolio_all_transparent": "Todos transparentes",
|
||||
"portfolio_appearance": "Aparência",
|
||||
"portfolio_cancel": "Cancelar",
|
||||
"portfolio_clear_sel": "Limpar",
|
||||
"portfolio_close": "Fechar",
|
||||
"portfolio_color": "Cor",
|
||||
"portfolio_currency": "Moeda",
|
||||
"portfolio_custom_color": "Cor personalizada\\xE2\\x80\\xA6",
|
||||
"portfolio_delete": "Excluir",
|
||||
"portfolio_detail_empty": "Selecione um grupo à esquerda, ou adicione um, para editá-lo.",
|
||||
"portfolio_edit": "Editar",
|
||||
"portfolio_funded": "Financiado",
|
||||
"portfolio_group_name": "Nome do grupo",
|
||||
"portfolio_icon": "Ícone",
|
||||
"portfolio_label": "Rótulo",
|
||||
"portfolio_manage": "Gerenciar…",
|
||||
"portfolio_manage_title": "Gerenciar portfólio",
|
||||
"portfolio_manual_price": "Preço / DRGX",
|
||||
"portfolio_new_entry": "Nova entrada",
|
||||
"portfolio_no_addr_match": "Nenhum endereço corresponde",
|
||||
"portfolio_no_entries": "Nenhuma entrada personalizada ainda. Adicione uma para acompanhar um grupo de endereços.",
|
||||
"portfolio_no_icon": "Nenhum",
|
||||
"portfolio_outline_opacity": "Opacidade do contorno",
|
||||
"portfolio_price": "Preço",
|
||||
"portfolio_price_btc": "Mercado \\xC2\\xB7 BTC",
|
||||
"portfolio_price_drgx": "Apenas DRGX",
|
||||
"portfolio_price_manual": "Manual",
|
||||
"portfolio_price_usd": "Mercado \\xC2\\xB7 USD",
|
||||
"portfolio_remove_from": "Remover do portfólio",
|
||||
"portfolio_revert": "Reverter",
|
||||
"portfolio_save": "Salvar",
|
||||
"portfolio_search": "Pesquisar endereços…",
|
||||
"portfolio_search_icons": "Pesquisar ícones\\xE2\\x80\\xA6",
|
||||
"portfolio_select_all": "Todos",
|
||||
"portfolio_select_shown": "Selecionar todos",
|
||||
"portfolio_show": "Mostrar:",
|
||||
"portfolio_show_24h": "24h",
|
||||
"portfolio_show_sparkline": "Minigráfico",
|
||||
"portfolio_show_value": "Valor",
|
||||
"portfolio_spark_day": "Dia",
|
||||
"portfolio_spark_hour": "Hora",
|
||||
"portfolio_spark_min": "Minuto",
|
||||
"portfolio_spark_month": "Mês",
|
||||
"portfolio_spark_week": "Semana",
|
||||
"portfolio_style_compact": "Linhas compactas",
|
||||
"portfolio_style_detailed": "Linhas detalhadas",
|
||||
"portfolio_style_featured": "Linhas em destaque",
|
||||
"portfolio_style_label": "Estilo do portfólio",
|
||||
"portfolio_untitled": "Sem título",
|
||||
"price_chart": "Gráfico de Preços",
|
||||
"privacy_great": "Ótima privacidade!",
|
||||
"privacy_low": "Baixa privacidade — blinde os fundos",
|
||||
"privacy_medium": "Considere blindar mais",
|
||||
"processing_transaction": "Processando transação...",
|
||||
"qr_code": "Código QR",
|
||||
"qr_failed": "Falha ao gerar código QR",
|
||||
"qr_title": "Código QR",
|
||||
@@ -571,6 +866,7 @@
|
||||
"receiving_addresses": "Seus Endereços de Recebimento",
|
||||
"recent_received": "RECEBIDOS RECENTES",
|
||||
"recent_sends": "ENVIOS RECENTES",
|
||||
"recent_transactions": "TRANSAÇÕES RECENTES",
|
||||
"recipient": "DESTINATÁRIO",
|
||||
"recipient_balance": "Destinatário: %.8f → %.8f DRGX",
|
||||
"recv_type": "Receb.",
|
||||
@@ -578,6 +874,7 @@
|
||||
"refresh": "Atualizar",
|
||||
"refresh_now": "Atualizar Agora",
|
||||
"remove_favorite": "Remover favorito",
|
||||
"repair_wallet": "Reparar Carteira",
|
||||
"report_bug": "Reportar Bug",
|
||||
"request_amount": "Valor (opcional):",
|
||||
"request_copy_uri": "Copiar URI",
|
||||
@@ -593,12 +890,16 @@
|
||||
"request_transparent_addrs": "-- Endereços Transparentes --",
|
||||
"request_uri_copied": "URI de pagamento copiada para a área de transferência",
|
||||
"rescan": "Reescanear",
|
||||
"rescan_bootstrapped_msg": "Seu nó foi inicializado por bootstrap, então os blocos abaixo do snapshot não estão em disco e um reescaneamento a partir do gênesis falharia. Reescaneie a partir de uma altura que seu snapshot inclua para reconciliar o saldo gasto da sua carteira. Seu wallet.dat e os dados da cadeia não são excluídos.",
|
||||
"rescan_detecting": "Verificando quais blocos seu nó tem em disco…",
|
||||
"rescan_from_height": "Reescanear a partir da altura do bloco:",
|
||||
"reset_to_defaults": "Redefinir Padrões",
|
||||
"restarting_after_encryption": "Reiniciando daemon após criptografia...",
|
||||
"restore_address": "Restaurar endereço",
|
||||
"result_preview": "Pré-visualização do resultado",
|
||||
"retry": "Tentar novamente",
|
||||
"review_send": "Revisar Envio",
|
||||
"rpc_connection": "Conexão RPC...",
|
||||
"rpc_host": "Host RPC",
|
||||
"rpc_pass": "Senha",
|
||||
"rpc_port": "Porta",
|
||||
@@ -608,12 +909,15 @@
|
||||
"save_z_transactions": "Salvar Z-tx na lista de tx",
|
||||
"sb_auth_failed": "Autenticação falhou — verifique rpcuser/rpcpassword",
|
||||
"sb_block": "Bloco: %d",
|
||||
"sb_building_witnesses": "Definindo testemunhas",
|
||||
"sb_building_witnesses_pct": "Reconstruindo testemunhas %.0f%%",
|
||||
"sb_connecting_daemon": "Conectando ao dragonxd...",
|
||||
"sb_connecting_err": "Conectando ao daemon — %s",
|
||||
"sb_connecting_external": "Conectando ao daemon externo...",
|
||||
"sb_connecting_generic": "Conectando ao daemon...",
|
||||
"sb_daemon_crashed": "O daemon travou %d vezes",
|
||||
"sb_daemon_not_found": "Daemon não encontrado",
|
||||
"sb_daemon_start_failed": "Não foi possível iniciar o dragonxd",
|
||||
"sb_dragonxd_running": "dragonxd em execução",
|
||||
"sb_dragonxd_stopped": "dragonxd parado",
|
||||
"sb_dragonxd_stopping": "Parando dragonxd...",
|
||||
@@ -639,6 +943,12 @@
|
||||
"sb_waiting_daemon": "Aguardando dragonxd...",
|
||||
"sb_waiting_daemon_err": "Aguardando dragonxd — %s",
|
||||
"sb_warming_up": "Aquecendo...",
|
||||
"sb_witness_cache": "Reconstruindo testemunhas",
|
||||
"screenshot_open_dir": "Abrir local",
|
||||
"screenshot_sweep": "Executar varredura de capturas de tela",
|
||||
"screenshot_sweep_desc": "Percorre cada tema em cada aba e salva uma captura de tela de cada um em subpastas por aba dentro da pasta de capturas de tela do diretório de configuração (sobrescrevendo a varredura anterior). É executado por alguns segundos.",
|
||||
"screenshot_sweep_full": "Varredura completa da interface",
|
||||
"search_icons": "Pesquisar ícones...",
|
||||
"search_placeholder": "Pesquisar...",
|
||||
"security": "SEGURANÇA",
|
||||
"seed_backup_button": "Frase de recuperação",
|
||||
@@ -655,6 +965,7 @@
|
||||
"seed_backup_saved": "Salvo em ",
|
||||
"seed_backup_title": "Backup da frase de recuperação",
|
||||
"seed_backup_warning": "Anote-as em ordem, guarde-as offline e nunca as compartilhe ou fotografe. Se você as perder, seus fundos não poderão ser recuperados.",
|
||||
"seed_migrate_button": "Migrar para carteira semente…",
|
||||
"select_address": "Selecionar endereço...",
|
||||
"select_receiving_address": "Selecionar endereço de recebimento...",
|
||||
"select_source_address": "Selecionar endereço de origem...",
|
||||
@@ -663,8 +974,10 @@
|
||||
"send_amount_details": "DETALHES DO VALOR",
|
||||
"send_amount_upper": "VALOR",
|
||||
"send_clear_fields": "Limpar todos os campos do formulário?",
|
||||
"send_contacts_button": "Escolher dos contatos",
|
||||
"send_copy_error": "Copiar Erro",
|
||||
"send_dismiss": "Dispensar",
|
||||
"send_err_needs_rescan": "Os dados de notas blindadas da sua carteira estão desatualizados em relação à blockchain (isso acontece após um bootstrap ou reindexação). Execute um reescaneamento completo em Configurações -> Reescanear Blockchain e deixe-o terminar por completo, depois tente enviar novamente.",
|
||||
"send_error_copied": "Erro copiado para a área de transferência",
|
||||
"send_error_prefix": "Erro: %s",
|
||||
"send_exceeds_available": "Excede o disponível (%.8f)",
|
||||
@@ -683,6 +996,7 @@
|
||||
"send_recipient": "DESTINATÁRIO",
|
||||
"send_select_source": "Selecionar endereço de origem...",
|
||||
"send_sending_from": "ENVIANDO DE",
|
||||
"send_status_unconfirmed": "Não foi possível confirmar o status da transação",
|
||||
"send_submitting": "Enviando transação...",
|
||||
"send_switch_to_receive": "Mude para Receber para obter seu endereço e começar a receber fundos.",
|
||||
"send_to": "Enviar para",
|
||||
@@ -755,6 +1069,8 @@
|
||||
"settings_noise_opacity": "Opacidade do ruído:",
|
||||
"settings_not_encrypted": "Não encriptado",
|
||||
"settings_not_found": "Não encontrado",
|
||||
"settings_open_app_dir": "Abrir pasta do aplicativo",
|
||||
"settings_open_data_dir": "Abrir pasta de dados",
|
||||
"settings_other": "Outros",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "Privacidade",
|
||||
@@ -788,16 +1104,24 @@
|
||||
"shield_check_status": "Verificar Status",
|
||||
"shield_completed": "Operação concluída com sucesso!",
|
||||
"shield_description": "Blinde suas recompensas de mineração enviando saídas coinbase de endereços transparentes para um endereço blindado. Isso melhora a privacidade ocultando sua renda de mineração.",
|
||||
"shield_error_prefix": "Erro: ",
|
||||
"shield_from_address": "Do Endereço:",
|
||||
"shield_funds": "Blindar Fundos",
|
||||
"shield_in_progress": "Operação em andamento...",
|
||||
"shield_max_utxos": "Máx. UTXOs por operação",
|
||||
"shield_merge_done": "Blindagem/fusão concluída!",
|
||||
"shield_op_failed": "Falha na operação: ",
|
||||
"shield_op_submitted": "Operação enviada: ",
|
||||
"shield_operation_id": "ID da operação: %s",
|
||||
"shield_select_z": "Selecionar z-endereço...",
|
||||
"shield_send_failed": "Falha na blindagem: ",
|
||||
"shield_started": "Operação de blindagem iniciada",
|
||||
"shield_status_check_error": "Erro ao verificar status: ",
|
||||
"shield_status_label": "Status: ",
|
||||
"shield_submitting": "Enviando operação...",
|
||||
"shield_title": "Blindar Recompensas Coinbase",
|
||||
"shield_to_address": "Para Endereço (Blindado):",
|
||||
"shield_unknown_error": "Erro desconhecido",
|
||||
"shield_utxo_limit": "Limite UTXO:",
|
||||
"shield_wildcard_hint": "Use '*' para blindar de todos os endereços transparentes",
|
||||
"shielded": "Blindado",
|
||||
@@ -807,11 +1131,16 @@
|
||||
"shielding_notice": "Nota: Isso blindará fundos de um endereço transparente (T) para um endereço privado (Z).",
|
||||
"show": "Mostrar",
|
||||
"show_hidden": "Mostrar ocultos (%d)",
|
||||
"show_qr": "Mostrar QR",
|
||||
"show_qr_code": "Mostrar Código QR",
|
||||
"showing_transactions": "Mostrando %d–%d de %d transações (total: %zu)",
|
||||
"showing_x_of_y": "Mostrando %d de %d endereços",
|
||||
"simple_background": "Fundo simples",
|
||||
"slider_off": "Desligado",
|
||||
"sort_amount_high": "Maior valor",
|
||||
"sort_amount_low": "Menor valor",
|
||||
"sort_date_newest": "Mais recentes primeiro",
|
||||
"sort_date_oldest": "Mais antigas primeiro",
|
||||
"start_mining": "Iniciar Mineração",
|
||||
"status": "Status",
|
||||
"stop_external": "Parar daemon externo",
|
||||
@@ -866,6 +1195,8 @@
|
||||
"tt_clear_ztx": "Excluir histórico de z-transações em cache local",
|
||||
"tt_custom_fees": "Ativar entrada manual de taxas ao enviar transações",
|
||||
"tt_custom_theme": "Tema personalizado ativo",
|
||||
"tt_daemon_install_bundled": "Parar o nó, sobrescrever o dragonxd instalado com a versão incluída nesta compilação da carteira e reiniciar",
|
||||
"tt_daemon_update_check": "Baixe e verifique o nó completo dragonxd mais recente do Gitea do projeto e, em seguida, reinicie para aplicar",
|
||||
"tt_debug_collapse": "Recolher opções de registro de depuração",
|
||||
"tt_debug_expand": "Expandir opções de registro de depuração",
|
||||
"tt_delete_blockchain": "Excluir todos os dados da blockchain e iniciar uma nova sincronização. wallet.dat e configuração são preservados.",
|
||||
@@ -881,15 +1212,19 @@
|
||||
"tt_keep_daemon": "O daemon será parado ao executar o assistente de configuração",
|
||||
"tt_language": "Idioma da interface da carteira",
|
||||
"tt_layout_hotkey": "Atalho: teclas de seta esquerda/direita para alternar layouts de Saldo",
|
||||
"tt_lite_redownload": "Rebaixar e reescanear todos os blocos do servidor lite",
|
||||
"tt_lock": "Bloquear a carteira imediatamente",
|
||||
"tt_low_spec": "Desativar todos os efeitos visuais pesados\\nAtalho: Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolidar múltiplos UTXOs em um endereço",
|
||||
"tt_mine_idle": "Iniciar mineração automaticamente quando o\\nsistema estiver ocioso (sem entrada de teclado/mouse)",
|
||||
"tt_noise": "Intensidade de textura granulada (0%% = desligado, 100%% = máximo)",
|
||||
"tt_open_app_dir": "Abrir a pasta ObsidianDragon (configurações, temas, logs) no gerenciador de arquivos",
|
||||
"tt_open_data_dir": "Abrir a pasta com os dados da sua carteira e da blockchain no gerenciador de arquivos",
|
||||
"tt_open_dir": "Clique para abrir no explorador de arquivos",
|
||||
"tt_reduce_motion": "Desativar transições animadas e lerp de saldo para acessibilidade",
|
||||
"tt_remove_encrypt": "Remover encriptação e armazenar a carteira desprotegida",
|
||||
"tt_remove_pin": "Remover PIN e exigir frase secreta para desbloquear",
|
||||
"tt_repair_wallet": "Apagar e reconstruir os registros de transações da carteira a partir da blockchain (corrige notas que falham ao enviar após uma reescaneada)",
|
||||
"tt_report_bug": "Reportar um problema no rastreador do projeto",
|
||||
"tt_request_payment": "Gerar uma solicitação de pagamento com código QR",
|
||||
"tt_rescan": "Reescanear a blockchain em busca de transações ausentes",
|
||||
@@ -904,6 +1239,7 @@
|
||||
"tt_scan_themes": "Procurar novos temas.\\nColoque pastas de temas em:\\n%s",
|
||||
"tt_scanline": "Efeito de linhas de varredura CRT no console",
|
||||
"tt_seed_backup": "Mostrar e fazer backup da frase de recuperação de 24 palavras da sua carteira",
|
||||
"tt_seed_migrate": "Criar uma nova carteira com frase de recuperação e mover seus fundos para ela",
|
||||
"tt_set_pin": "Definir um PIN de 4-8 dígitos para desbloqueio rápido",
|
||||
"tt_shield_mining": "Mover recompensas de mineração transparentes para um endereço blindado",
|
||||
"tt_simple_bg": "Usar um gradiente simples para o fundo\\nAtalho: Ctrl+Up",
|
||||
@@ -917,6 +1253,7 @@
|
||||
"tt_ui_opacity": "Opacidade de cartões e barra lateral (100%% = totalmente opaco, menor = mais transparente)",
|
||||
"tt_validate": "Verificar se um endereço DragonX é válido",
|
||||
"tt_verbose": "Registrar diagnósticos detalhados de conexão,\\nestado do daemon e info de proprietário de porta\\nna aba Console",
|
||||
"tt_wallets_button": "Liste os arquivos de carteira e alterne entre eles",
|
||||
"tt_website": "Abrir o site do DragonX",
|
||||
"tt_window_opacity": "Opacidade do fundo (menor = área de trabalho visível através da janela)",
|
||||
"tt_wizard": "Executar novamente o assistente de configuração inicial\\nO daemon será reiniciado",
|
||||
@@ -925,7 +1262,18 @@
|
||||
"tx_from_address": "Endereço de Origem:",
|
||||
"tx_id_label": "ID da Transação:",
|
||||
"tx_immature": "IMATURO",
|
||||
"tx_loading_enriching_sends": "Verificando detalhes das transações enviadas (%d)",
|
||||
"tx_loading_fetching_transparent": "Buscando histórico transparente",
|
||||
"tx_loading_history_progress": "Carregando histórico antigo (%d%%)",
|
||||
"tx_loading_queued": "Atualização de transação na fila",
|
||||
"tx_loading_refreshing_cached": "Atualizando histórico da carteira (%d em cache)",
|
||||
"tx_loading_scanning_shielded": "Verificando histórico blindado (%d endereços)",
|
||||
"tx_mined": "MINERADO",
|
||||
"tx_progress_balances": "Atualizando saldos",
|
||||
"tx_progress_finalizing": "Finalizando transação",
|
||||
"tx_progress_history": "Atualizando histórico",
|
||||
"tx_progress_submitting": "Enviando transação para o daemon",
|
||||
"tx_progress_waiting_ops": "Aguardando operação (%d)",
|
||||
"tx_received": "RECEBIDO",
|
||||
"tx_sent": "ENVIADO",
|
||||
"tx_to_address": "Endereço de Destino:",
|
||||
@@ -937,6 +1285,14 @@
|
||||
"unconfirmed": "Não confirmado",
|
||||
"undo_clear": "Desfazer Limpeza",
|
||||
"unknown": "Desconhecido",
|
||||
"unmark_mining_address": "Desmarcar endereço de mineração",
|
||||
"upd_back": "Voltar",
|
||||
"upd_install": "Instalar",
|
||||
"upd_installed_badge": "instalado",
|
||||
"upd_no_build_platform": "Nenhuma build para esta plataforma",
|
||||
"upd_prerelease": "pré-lançamento",
|
||||
"upd_reinstall": "Reinstalar",
|
||||
"upd_select_version": "Selecione uma versão para instalar",
|
||||
"use_embedded_daemon": "Usar dragonxd integrado",
|
||||
"use_tor": "Usar Tor",
|
||||
"validate_btn": "Validar",
|
||||
@@ -956,17 +1312,73 @@
|
||||
"verbose_logging": "Log detalhado",
|
||||
"version": "Versão",
|
||||
"view": "Visualizar",
|
||||
"view_all": "Ver Tudo",
|
||||
"view_details": "Ver Detalhes",
|
||||
"view_on_explorer": "Ver no Explorador",
|
||||
"waiting_for_daemon": "Aguardando conexão com o daemon...",
|
||||
"wallet": "CARTEIRA",
|
||||
"wallet_empty": "Sua carteira está vazia",
|
||||
"wallet_empty_hint": "Mude para Receber para obter seu endereço e começar a receber fundos.",
|
||||
"wallets_active": "Ativa",
|
||||
"wallets_add": "Adicionar pasta",
|
||||
"wallets_add_folder": "Verificar também outra pasta em busca de arquivos de carteira:",
|
||||
"wallets_add_folder_toggle": "+ Procurar carteiras noutra pasta…",
|
||||
"wallets_button": "Carteiras…",
|
||||
"wallets_col_addresses": "Endereços",
|
||||
"wallets_col_balance": "Saldo",
|
||||
"wallets_col_name": "Carteira",
|
||||
"wallets_col_opened": "Aberta pela última vez",
|
||||
"wallets_col_size": "Tamanho",
|
||||
"wallets_create": "Criar carteira",
|
||||
"wallets_creating": "Criando carteira — o nó será reiniciado…",
|
||||
"wallets_current": "atual",
|
||||
"wallets_exists": "Já existe uma carteira com esse nome.",
|
||||
"wallets_external_tt": "Fora do seu diretório de dados — use Importar para copiá-lo.",
|
||||
"wallets_folder_hint": "/caminho/para/pasta com arquivos .dat de carteira",
|
||||
"wallets_folder_invalid": "Essa pasta não existe.",
|
||||
"wallets_import": "Importar",
|
||||
"wallets_import_failed": "Não foi possível importar esse arquivo de carteira.",
|
||||
"wallets_import_hint": "Importar para abrir",
|
||||
"wallets_import_tt": "Esta carteira está fora do diretório de dados. Ao abri-la, ela será copiada para lá primeiro (em breve).",
|
||||
"wallets_imported": "Carteira importada — alternando…",
|
||||
"wallets_intro": "Arquivos de carteira no seu diretório de dados (e em quaisquer pastas que você adicionar). Abra um para alternar — o nó reinicia para carregá-lo, e cada carteira mantém seus próprios dados.",
|
||||
"wallets_name_invalid": "Insira um nome de carteira válido.",
|
||||
"wallets_never": "Nunca",
|
||||
"wallets_new_hint": "Nome (ex.: poupança)",
|
||||
"wallets_new_label": "Criar uma nova carteira:",
|
||||
"wallets_open": "Abrir",
|
||||
"wallets_reveal": "Mostrar pasta",
|
||||
"wallets_title": "Carteiras",
|
||||
"warning": "Aviso",
|
||||
"warning_upper": "AVISO!",
|
||||
"website": "Website",
|
||||
"window_opacity": "Opacidade da Janela",
|
||||
"wizard_daemon_start_failed": "Falha ao iniciar o daemon — será tentado novamente automaticamente",
|
||||
"xmrig_browse_releases": "Navegar por todos os lançamentos…",
|
||||
"xmrig_checking": "Verificando o minerador mais recente…",
|
||||
"xmrig_current": "Atual:",
|
||||
"xmrig_download_install": "Baixar e instalar",
|
||||
"xmrig_downloading": "Baixando…",
|
||||
"xmrig_installed": "Instalado:",
|
||||
"xmrig_installed_ok": "Minerador instalado",
|
||||
"xmrig_installing": "Instalando…",
|
||||
"xmrig_latest": "Mais recente:",
|
||||
"xmrig_loading_releases": "Carregando versões…",
|
||||
"xmrig_none": "nenhum",
|
||||
"xmrig_reinstall": "Reinstalar",
|
||||
"xmrig_stop_mining_first": "Pare a mineração antes de atualizar o minerador.",
|
||||
"xmrig_unavailable_body": "Nenhuma versão do minerador está disponível para esta plataforma.",
|
||||
"xmrig_unavailable_title": "Atualizações do minerador indisponíveis",
|
||||
"xmrig_unknown_error": "Erro desconhecido.",
|
||||
"xmrig_up_to_date": "O minerador está atualizado",
|
||||
"xmrig_update_available": "Um novo minerador está disponível",
|
||||
"xmrig_update_button": "Atualizar minerador…",
|
||||
"xmrig_update_failed": "Falha na atualização",
|
||||
"xmrig_update_short": "Atualizar",
|
||||
"xmrig_update_title": "Atualizar Minerador",
|
||||
"xmrig_verify_note": "O download é verificado contra o checksum SHA-256 publicado do lançamento antes da instalação.",
|
||||
"xmrig_verifying": "Verificando…",
|
||||
"xmrig_version": "Versão:",
|
||||
"yes_clear": "Sim, Limpar",
|
||||
"your_addresses": "Seus Endereços",
|
||||
"z_address": "Endereço Z",
|
||||
|
||||
412
res/lang/ru.json
412
res/lang/ru.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "Добавить адрес",
|
||||
"address_book_add_new": "Добавить новый",
|
||||
"address_book_added": "Адрес добавлен в книгу",
|
||||
"address_book_confirm_delete": "Подтвердить удаление?",
|
||||
"address_book_count": "%zu адресов сохранено",
|
||||
"address_book_deleted": "Запись удалена",
|
||||
"address_book_edit": "Редактировать адрес",
|
||||
@@ -39,6 +40,7 @@
|
||||
"address_copied": "Адрес скопирован в буфер обмена",
|
||||
"address_details": "Детали адреса",
|
||||
"address_label": "Адрес:",
|
||||
"address_reorder_hint": "Перетащите на край строки для изменения порядка или в её центр для переноса",
|
||||
"address_upper": "АДРЕС",
|
||||
"address_url": "URL адреса",
|
||||
"addresses_appear_here": "Ваши адреса для получения появятся здесь после подключения.",
|
||||
@@ -69,7 +71,10 @@
|
||||
"backup_wallet": "Создать резервную копию...",
|
||||
"backup_wallet_not_found": "Предупреждение: wallet.dat не найден в ожидаемом расположении",
|
||||
"balance": "Баланс",
|
||||
"balance_history_collecting": "История баланса — сбор данных...",
|
||||
"balance_layout": "Макет баланса",
|
||||
"balance_shielded_fmt": "Экранировано: %.8f",
|
||||
"balance_transparent_fmt": "Прозрачный: %.8f",
|
||||
"ban": "Заблокировать",
|
||||
"banned_peers": "Заблокированные узлы",
|
||||
"block": "Блок",
|
||||
@@ -151,6 +156,20 @@
|
||||
"confirm_delete_blockchain_msg": "Это остановит демон, удалит все данные блокчейна (blocks, chainstate, peers) и начнёт синхронизацию с нуля. Это может занять несколько часов.",
|
||||
"confirm_delete_blockchain_safe": "Ваш wallet.dat, конфигурация и история транзакций в безопасности и не будут удалены.",
|
||||
"confirm_delete_blockchain_title": "Удалить данные блокчейна",
|
||||
"confirm_lite_redownload_msg": "Это удалит загруженные данные блоков кошелька и заново загрузит и пересканирует каждый блок с лёгкого сервера. Это может занять некоторое время; кошелёк будет показывать прогресс синхронизации до завершения.",
|
||||
"confirm_lite_redownload_safe": "Ваш кошелёк, ключи и сид-фраза не затрагиваются — заново загружаются только данные блоков.",
|
||||
"confirm_lite_redownload_title": "Повторно загрузить блоки",
|
||||
"confirm_reinstall_daemon_msg": "Это остановит daemon, заменит установленный dragonxd (а также dragonx-cli/dragonx-tx) версиями, встроенными в эту сборку кошелька, затем перезапустит узел. Используйте это, чтобы восстановить или обновить бинарный файл узла.",
|
||||
"confirm_reinstall_daemon_safe": "Ваш кошелёк, ключи и данные блокчейна не затрагиваются — заменяются только программные файлы демона.",
|
||||
"confirm_reinstall_daemon_title": "Установить встроенный демон",
|
||||
"confirm_repair_wallet_msg": "Это перезапустит демон с -zapwallettxes=2: он удалит все записи о транзакциях и заметках кошелька, а затем пересоберёт их из блокчейна. Используйте это, когда транзакции не удаётся собрать («Invalid sapling spend proof» / «shielded requirements not met») даже после полного пересканирования. Это занимает много времени, и кошелёк остаётся офлайн до завершения.",
|
||||
"confirm_repair_wallet_safe": "Ваши ключи, адреса и баланс сохраняются — перестраиваются только кешированные записи транзакций.",
|
||||
"confirm_repair_wallet_title": "Восстановить кошелёк",
|
||||
"confirm_rescan_msg": "Это перезапускает демон и заново сканирует весь блокчейн в поиске транзакций вашего кошелька. Процесс может занять много времени, и до его завершения кошелёк остаётся офлайн.",
|
||||
"confirm_rescan_safe": "Ваш wallet.dat и данные блокчейна не удаляются — только пересканируются.",
|
||||
"confirm_rescan_title": "Пересканировать блокчейн",
|
||||
"confirm_restart_daemon_msg": "Это остановит и перезапустит демон, чтобы применить изменённые настройки. Кошелёк ненадолго отключится и подключится снова.",
|
||||
"confirm_restart_daemon_title": "Перезапустить Daemon",
|
||||
"confirm_send": "Подтвердить отправку",
|
||||
"confirm_transaction": "Подтвердить транзакцию",
|
||||
"confirm_transfer": "Подтвердить перевод",
|
||||
@@ -161,6 +180,7 @@
|
||||
"connected_peers": "Подключённые узлы",
|
||||
"connecting": "Подключение...",
|
||||
"console": "Консоль",
|
||||
"console_app": "Прил.",
|
||||
"console_auto_scroll": "Авто-прокрутка",
|
||||
"console_available_commands": "Доступные команды:",
|
||||
"console_capturing_output": "Захват вывода daemon...",
|
||||
@@ -177,6 +197,7 @@
|
||||
"console_connected": "Подключено к daemon",
|
||||
"console_copy_all": "Копировать всё",
|
||||
"console_copy_selected": "Копировать",
|
||||
"console_copy_value": "Копировать",
|
||||
"console_daemon": "Daemon",
|
||||
"console_daemon_error": "Ошибка daemon!",
|
||||
"console_daemon_started": "Daemon запущен",
|
||||
@@ -195,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - Управление майнингом",
|
||||
"console_help_stop": " stop - Остановить daemon",
|
||||
"console_line_count": "%zu строк",
|
||||
"console_matches": "совпадений",
|
||||
"console_new_lines": "%d новых строк",
|
||||
"console_no_daemon": "Нет daemon",
|
||||
"console_not_connected": "Ошибка: Не подключено к daemon",
|
||||
"console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.",
|
||||
"console_rpc_reference": "Справочник RPC-команд",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Скан-линия консоли",
|
||||
"console_search_commands": "Поиск команд...",
|
||||
"console_select_all": "Выбрать всё",
|
||||
"console_show_app_output": "Показать строки журнала кошелька [app]",
|
||||
"console_show_daemon_output": "Показать вывод daemon",
|
||||
"console_show_errors_only": "Показать только ошибки",
|
||||
"console_show_rpc_ref": "Показать справочник RPC-команд",
|
||||
"console_show_rpc_trace": "Показать RPC-вызовы приложения",
|
||||
"console_showing_lines": "Показано %zu из %zu строк",
|
||||
"console_starting_node": "Запуск узла...",
|
||||
"console_status_error": "Ошибка",
|
||||
@@ -218,6 +244,12 @@
|
||||
"console_welcome": "Добро пожаловать в консоль ObsidianDragon",
|
||||
"console_zoom_in": "Увеличить",
|
||||
"console_zoom_out": "Уменьшить",
|
||||
"contact_global": "Показывать во всех кошельках (глобальный контакт)",
|
||||
"contact_global_badge_tt": "Глобальный контакт — виден во всех кошельках",
|
||||
"contact_global_tt": "Вкл.: этот контакт остаётся видимым, какой бы кошелёк вы ни загрузили. Выкл.: он принадлежит только текущему кошельку.",
|
||||
"contacts": "Контакты",
|
||||
"contacts_search_no_match": "Совпадающих контактов нет",
|
||||
"contacts_search_placeholder": "Поиск контактов...",
|
||||
"copied": "Скопировано!",
|
||||
"copy": "Копировать",
|
||||
"copy_address": "Копировать полный адрес",
|
||||
@@ -227,6 +259,44 @@
|
||||
"copy_uri": "Копировать URI",
|
||||
"current_price": "Текущая цена",
|
||||
"custom_fees": "Пользовательские комиссии",
|
||||
"daemon_binary": "Бинарный файл демона",
|
||||
"daemon_bundled": "Встроенный",
|
||||
"daemon_install_bundled": "Установить встроенную",
|
||||
"daemon_installed": "Установлено",
|
||||
"daemon_none_bundled": "нет в этой сборке",
|
||||
"daemon_not_installed": "не установлен",
|
||||
"daemon_status_differ": "Установленный бинарный файл отличается от встроенной версии.",
|
||||
"daemon_status_match": "Установленный бинарник соответствует встроенной версии.",
|
||||
"daemon_status_missing": "Демон не установлен — установите встроенную версию.",
|
||||
"daemon_update_available": "Доступна новая версия узла",
|
||||
"daemon_update_body": "В эту сборку кошелька встроена более новая версия узла DragonX, чем установленная сейчас. При обновлении установленный dragonxd (а также dragonx-cli/dragonx-tx) заменяется, после чего узел останавливается и перезапускается, чтобы новая версия вступила в силу. Рекомендуется — более новый узел может добавить возможности (например, поддержку сид-фразы), которых нет в старом.",
|
||||
"daemon_update_browse": "Просмотреть все релизы…",
|
||||
"daemon_update_check": "Проверить обновления…",
|
||||
"daemon_update_checking": "Проверка последней версии узла…",
|
||||
"daemon_update_downgrade_note": "Старые версии могут быть несовместимы с вашими текущими данными блокчейна. Установка другой версии вступает в силу после перезапуска демона.",
|
||||
"daemon_update_download_install": "Скачать и установить",
|
||||
"daemon_update_downloading": "Загрузка…",
|
||||
"daemon_update_failed": "Ошибка обновления",
|
||||
"daemon_update_installed": "Установлено:",
|
||||
"daemon_update_installed_ok": "Узел установлен",
|
||||
"daemon_update_installing": "Установка…",
|
||||
"daemon_update_keep": "Оставить текущий",
|
||||
"daemon_update_later": "Позже",
|
||||
"daemon_update_latest": "Последняя:",
|
||||
"daemon_update_loading": "Загрузка релизов…",
|
||||
"daemon_update_now": "Обновить сейчас",
|
||||
"daemon_update_reinstall": "Переустановить",
|
||||
"daemon_update_restart_note": "Перезапустите daemon, чтобы начать работу с новой версией.",
|
||||
"daemon_update_restart_now": "Перезапустить демон сейчас",
|
||||
"daemon_update_safe": "Ваш кошелёк, ключи и данные блокчейна не затрагиваются — заменяются только программные файлы демона. Если вы намеренно используете собственный узел, выберите «Оставить текущий».",
|
||||
"daemon_update_title": "Обновить демон узла?",
|
||||
"daemon_update_unavailable_body": "Для этой платформы нет доступной сборки узла.",
|
||||
"daemon_update_unavailable_title": "Обновления узла недоступны",
|
||||
"daemon_update_unknown_error": "Неизвестная ошибка.",
|
||||
"daemon_update_up_to_date": "Узел обновлён до последней версии",
|
||||
"daemon_update_verify_note": "Перед установкой загрузка проверяется по опубликованному для релиза SHA-256 и закреплённой подписи ed25519.",
|
||||
"daemon_update_verifying": "Проверка…",
|
||||
"daemon_update_version": "Версия:",
|
||||
"daemon_version": "Демон",
|
||||
"dark": "Тёмная",
|
||||
"date": "Дата",
|
||||
@@ -257,10 +327,14 @@
|
||||
"explorer_block_time": "Время",
|
||||
"explorer_block_txs": "Транзакции",
|
||||
"explorer_chain_stats": "Цепочка",
|
||||
"explorer_hash_not_found": "Для этого хеша не найдено ни блока, ни транзакции",
|
||||
"explorer_invalid_query": "Введите высоту блока или 64-символьный хеш",
|
||||
"explorer_invalid_response": "Неверный ответ от daemon",
|
||||
"explorer_mempool": "Мемпул",
|
||||
"explorer_mempool_size": "Размер",
|
||||
"explorer_mempool_txs": "Транзакции",
|
||||
"explorer_no_results": "Нет подходящих кэшированных блоков",
|
||||
"explorer_not_connected": "Нет подключения к демону — невозможно найти хеш блока или транзакции",
|
||||
"explorer_recent_blocks": "Последние блоки",
|
||||
"explorer_search": "Поиск",
|
||||
"explorer_section": "ОБОЗРЕВАТЕЛЬ",
|
||||
@@ -312,6 +386,7 @@
|
||||
"hidden_tag": " (скрыт)",
|
||||
"hide": "Скрыть",
|
||||
"hide_address": "Скрыть адрес",
|
||||
"hide_qr": "Скрыть QR",
|
||||
"hide_zero_balances": "Скрыть нулевые балансы",
|
||||
"history": "История",
|
||||
"immature_type": "Незрелая",
|
||||
@@ -350,19 +425,120 @@
|
||||
"label_placeholder": "напр. Накопления, Майнинг...",
|
||||
"language": "Язык",
|
||||
"light": "Светлая",
|
||||
"lite_account_label": "Аккаунт",
|
||||
"lite_action": "Действие",
|
||||
"lite_backup_keys": "Резервная копия и ключи",
|
||||
"lite_birthday_backup": "Дата рождения: %llu (сохраните её тоже)",
|
||||
"lite_birthday_hint": "Высота блока, с которой начинать сканирование. Оставьте 0, если неизвестно (медленное полное сканирование).",
|
||||
"lite_birthday_label": "Дата рождения",
|
||||
"lite_console_help_passthrough": "Любой другой ввод выполняется как команда консоли лайт-кошелька.",
|
||||
"lite_copy": "Копировать",
|
||||
"lite_could_not_write": "Не удалось записать ",
|
||||
"lite_encrypt_wallet": "Зашифровать кошелёк",
|
||||
"lite_encryption_removed": "Шифрование удалено",
|
||||
"lite_hide_wipe": "Скрыть и стереть",
|
||||
"lite_import": "Импорт",
|
||||
"lite_import_key_label": "Импортировать ключ",
|
||||
"lite_key_imported": "Ключ импортирован — выполните синхронизацию, чтобы просканировать его историю",
|
||||
"lite_lock_failed": "Не удалось заблокировать",
|
||||
"lite_lock_now": "Заблокировать сейчас",
|
||||
"lite_maintenance": "Обслуживание",
|
||||
"lite_net_add": "Добавить",
|
||||
"lite_net_add_label_hint": "Метка (необязательно)",
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "проверка…",
|
||||
"lite_net_connected": "Подключено",
|
||||
"lite_net_custom": "Свой",
|
||||
"lite_net_disconnected": "Не подключено",
|
||||
"lite_net_hidden_section": "Скрытые серверы",
|
||||
"lite_net_hide": "Скрыть",
|
||||
"lite_net_in_use": "Используется",
|
||||
"lite_net_intro": "Выберите сервер для использования или позвольте кошельку выбрать один случайным образом. Изменения применяются немедленно.",
|
||||
"lite_net_invalid_url": "Введите корректный URL http(s)://.",
|
||||
"lite_net_no_wallet": "Нет открытого кошелька",
|
||||
"lite_net_official": "Официальный",
|
||||
"lite_net_offline": "офлайн",
|
||||
"lite_net_random_active": "Активен случайный выбор сервера.",
|
||||
"lite_net_random_server": "Случайный сервер",
|
||||
"lite_net_refresh": "Обновить",
|
||||
"lite_net_show_hidden": "Показать скрытые серверы",
|
||||
"lite_net_sync_label": "Синхронизация",
|
||||
"lite_net_synced": "Синхронизировано",
|
||||
"lite_net_syncing": "Синхронизация",
|
||||
"lite_net_title": "Lite-серверы",
|
||||
"lite_net_unhide": "Показать",
|
||||
"lite_net_use_random": "Каждый раз использовать случайный сервер",
|
||||
"lite_no_wallet": "Нет открытого кошелька — создайте или откройте его в Настройках",
|
||||
"lite_no_wallet_short": "Нет открытого кошелька",
|
||||
"lite_op_create": "Создать",
|
||||
"lite_op_open": "Открыть",
|
||||
"lite_op_restore": "Восстановить",
|
||||
"lite_overwrite": "Перезаписать",
|
||||
"lite_passphrase_label": "Пароль",
|
||||
"lite_private_keys_warning": "Приватные ключи — любой, у кого они есть, может потратить ваши средства",
|
||||
"lite_redownload_blocks": "Перезагрузить блоки",
|
||||
"lite_redownload_desc": "Заново загрузить все блоки с лёгкого сервера (используйте, если ваш баланс или история выглядят неверно).",
|
||||
"lite_redownload_running": "Повторная загрузка блоков…",
|
||||
"lite_remove_encryption": "Убрать шифрование",
|
||||
"lite_restore_birthday_label": "Блок рождения (необязательно — 0 сканирует с начала, медленнее)",
|
||||
"lite_restore_btn": "Восстановить кошелёк",
|
||||
"lite_restore_intro": "Введите свою сид-фразу восстановления. Кошелёк будет восстановлен и затем синхронизирован с лёгкого сервера.",
|
||||
"lite_restore_ok": "Кошелёк восстановлен — синхронизация с лёгкого сервера…",
|
||||
"lite_restore_seed_label": "Сид-фраза (слова, разделённые пробелами)",
|
||||
"lite_restore_title": "Восстановление из сид-фразы",
|
||||
"lite_save_to_file": "Сохранить в файл",
|
||||
"lite_saved_to": "Сохранено (открытым текстом, доступ только владельцу) в ",
|
||||
"lite_security": "Безопасность",
|
||||
"lite_seed_label": "Сид",
|
||||
"lite_seed_warning": "Сид-фраза — ЕДИНСТВЕННЫЙ способ восстановить ваш кошелёк. Запишите её, храните офлайн, никогда не делитесь ею.",
|
||||
"lite_servers_network_tab": "Лайт-серверы управляются на вкладке «Сеть».",
|
||||
"lite_show_private_keys": "Показать приватные ключи",
|
||||
"lite_show_seed": "Показать сид-фразу",
|
||||
"lite_unlock": "Разблокировать",
|
||||
"lite_unlock_btn": "Разблокировать",
|
||||
"lite_unlock_failed": "Не удалось разблокировать — неверный пароль?",
|
||||
"lite_unlock_msg": "Введите парольную фразу, чтобы разблокировать кошелёк для расходования.",
|
||||
"lite_unlock_ok": "Кошелёк разблокирован — теперь можно отправлять",
|
||||
"lite_unlock_title": "Разблокировать кошелёк",
|
||||
"lite_validate": "Проверить",
|
||||
"lite_wallet_encrypted": "Кошелёк зашифрован",
|
||||
"lite_wallet_label": "Кошелёк",
|
||||
"lite_wallet_locked": "Кошелёк заблокирован",
|
||||
"lite_wallet_ready": "Кошелёк готов",
|
||||
"lite_wallet_request": "Запрос lite-кошелька",
|
||||
"lite_wallet_unlocked": "Кошелёк разблокирован",
|
||||
"lite_welcome_create": "Создать новый кошелёк",
|
||||
"lite_welcome_create_failed": "Не удалось создать кошелёк",
|
||||
"lite_welcome_created": "Кошелёк создан — создайте резервную копию сид-фразы прямо сейчас в Настройки → Резервная копия и ключи",
|
||||
"lite_welcome_later": "Позже",
|
||||
"lite_welcome_msg": "У вас пока нет кошелька. Создайте новый или восстановите из фразы восстановления.",
|
||||
"lite_welcome_restore": "Восстановить из сид-фразы",
|
||||
"lite_welcome_restore_hint": "Восстановите свой кошелёк в разделе Настройки → Лёгкий кошелёк",
|
||||
"lite_welcome_title": "Добро пожаловать в ObsidianDragon Lite",
|
||||
"lite_word_count": "%d / 24 слов",
|
||||
"lite_working": "Обработка…",
|
||||
"loading": "Загрузка...",
|
||||
"loading_addresses": "Загрузка адресов...",
|
||||
"loading_transactions": "Загрузка транзакций",
|
||||
"local_hashrate": "Локальный хешрейт",
|
||||
"low_spec_mode": "Режим экономии",
|
||||
"mark_mining_address": "Отметить как адрес майнинга",
|
||||
"market": "Рынок",
|
||||
"market_12h": "12ч",
|
||||
"market_18h": "18ч",
|
||||
"market_24h": "24ч",
|
||||
"market_24h_change": "24ч",
|
||||
"market_24h_volume": "ОБЪЁМ 24Ч",
|
||||
"market_6h": "6ч",
|
||||
"market_attribution": "Данные о ценах с NonKYC",
|
||||
"market_btc_price": "ЦЕНА BTC",
|
||||
"market_cap": "Рыночная капитализация",
|
||||
"market_cap_short": "Кап.",
|
||||
"market_iv_1d": "1Д",
|
||||
"market_iv_1h": "1Ч",
|
||||
"market_iv_1m": "1М",
|
||||
"market_iv_1w": "1Н",
|
||||
"market_iv_live": "В реальном времени",
|
||||
"market_no_history": "Нет истории цен",
|
||||
"market_no_price": "Нет данных о ценах",
|
||||
"market_now": "Сейчас",
|
||||
@@ -372,6 +548,8 @@
|
||||
"market_price_unavailable": "Данные о ценах недоступны",
|
||||
"market_refresh_price": "Обновить данные о ценах",
|
||||
"market_trade_on": "Торговать на %s",
|
||||
"market_updated": "\\xc2\\xb7 Обновлено %s",
|
||||
"market_vol_short": "Объём",
|
||||
"mature": "Зрелая",
|
||||
"max": "Макс",
|
||||
"memo": "Заметка (необязательно, зашифровано)",
|
||||
@@ -381,8 +559,52 @@
|
||||
"memo_z_only": "Примечание: Заметки доступны только при отправке на экранированные (z) адреса",
|
||||
"merge_description": "Объедините несколько UTXO в один экранированный адрес. Это может уменьшить размер кошелька и улучшить конфиденциальность.",
|
||||
"merge_funds": "Объединить средства",
|
||||
"merge_send_failed": "Ошибка объединения: ",
|
||||
"merge_started": "Операция объединения начата",
|
||||
"merge_title": "Объединить на адрес",
|
||||
"mig_adopt": "Сделать это моим кошельком",
|
||||
"mig_adopt_now": "Принять сид-кошелёк",
|
||||
"mig_adopting": "Установка вашего нового кошелька и пересканирование…",
|
||||
"mig_adopting_note": "Узел перезапускается на вашем новом кошельке и выполнит пересканирование цепочки — это может занять несколько минут. Оставьте кошелёк работающим; он подключится автоматически.",
|
||||
"mig_already_mnemonic": "У вашего кошелька уже есть сид-фраза из 24 слов — переносить нечего. Просто сделайте её резервную копию и храните слова в безопасном месте.",
|
||||
"mig_backed_up": "Я записал свою сид-фразу",
|
||||
"mig_backup_instead": "Сделать резервную копию сид-фразы",
|
||||
"mig_balance": "Баланс текущего кошелька: %.8f DRGX (за вычетом небольшой комиссии)",
|
||||
"mig_check_failed": "Не удалось проверить ваш кошелёк. Убедитесь, что он разблокирован и подключён, затем выполните повторную проверку.",
|
||||
"mig_checking_balance": "Проверка вашего баланса",
|
||||
"mig_confirming": "Ожидание подтверждения перевода в сети перед переключением кошельков — это гарантирует, что ваши средства действительно перемещены.",
|
||||
"mig_confs": "Подтверждения перевода: %d",
|
||||
"mig_continue_sweep": "Перейти к переводу",
|
||||
"mig_copy_seed": "Копировать сид-фразу",
|
||||
"mig_create": "Создать сид-кошелёк",
|
||||
"mig_daemon_too_old": "Ваш узел DragonX слишком стар, чтобы создать сид-кошелёк — в нём нет необходимой для этого поддержки мнемоники. Обновите узел (Настройки → Узел и безопасность → Установить встроенный или Проверить обновления), затем повторите попытку.",
|
||||
"mig_discard": "Отменить миграцию",
|
||||
"mig_done": "Миграция завершена. Ваш кошелёк теперь защищён вашей сид-фразой.",
|
||||
"mig_done_btn": "Готово",
|
||||
"mig_done_detail": "Демон выполняет пересканирование, чтобы показать ваши средства — это может занять несколько минут. Ваш предыдущий кошелёк сохранён как .bak в папке с данными.",
|
||||
"mig_intro": "Это создаёт совершенно новый кошелёк, защищённый сид-фразой из 24 слов, в изолированном узле, чтобы вы могли перевести в него свои средства. Ваш текущий кошелёк НЕ затрагивается, и на этом шаге НИКАКИЕ средства не перемещаются — сначала вы сделаете резервную копию новой сид-фразы, а затем отдельным, подтверждённым шагом переведёте в неё свои средства.",
|
||||
"mig_later": "Позже",
|
||||
"mig_no_funds": "В вашем кошельке нет средств для переноса. Вы можете сразу принять новый сид-кошелёк — он заменит ваш текущий (пустой) кошелёк на кошелёк с сид-фразой. Ваш старый кошелёк на всякий случай всё равно будет сохранён в резервную копию с меткой времени.",
|
||||
"mig_nofunds_confirm": "Я понимаю, что это заменит мой текущий кошелёк новым сид-кошельком",
|
||||
"mig_not_connected": "Сначала подключитесь к своему узлу, затем снова откройте это окно для миграции.",
|
||||
"mig_precheck_checking": "Проверка вашего кошелька",
|
||||
"mig_receive_addr": "Адрес получения нового кошелька:",
|
||||
"mig_recheck": "Проверить снова",
|
||||
"mig_remainder": "Часть средств всё ещё находится в вашем старом кошельке (слишком много входов для одной транзакции) — переведите остаток перед переключением.",
|
||||
"mig_remaining": "Осталось в старом кошельке: %.8f DRGX",
|
||||
"mig_seed_warning": "Запишите эти 24 слова по порядку и храните их офлайн. Это единственная резервная копия вашего нового кошелька — если вы их потеряете, средства, которые вы перенесёте, будут утрачены навсегда.",
|
||||
"mig_step1_done": "Шаг 1 из 2 завершён — ваши средства пока НЕ перемещены. Следующий шаг — перевод их в этот новый кошелёк.",
|
||||
"mig_sweep_all": "Перевести все средства",
|
||||
"mig_sweep_intro": "Теперь переведите все средства из вашего текущего кошелька в новый сид-кошелёк. Это отправит одну транзакцию на адрес нового кошелька; ваша сид-фраза (уже сохранённая в резервной копии) с этого момента управляет средствами.",
|
||||
"mig_sweep_remaining": "Перевести остаток",
|
||||
"mig_sweeping": "Перевод ваших средств в новый кошелёк…",
|
||||
"mig_title": "Переход на сид-кошелёк",
|
||||
"mig_to": "Кому: ",
|
||||
"mig_txid": "txid: ",
|
||||
"mig_unlock_first": "Сначала разблокируйте кошелёк, чтобы тратить с него.",
|
||||
"mig_unlock_to_migrate": "Сначала разблокируйте кошелёк, затем снова откройте это окно для миграции.",
|
||||
"mig_waiting_mine": "Ожидание включения транзакции перевода в блок…",
|
||||
"mig_working": "Создание вашего нового сид-кошелька в изолированном узле — это может занять минуту. Ваш основной кошелёк продолжает работать.",
|
||||
"mine_when_idle": "Майнить в простое",
|
||||
"mined": "добыто",
|
||||
"mined_filter": "Добытые",
|
||||
@@ -394,6 +616,8 @@
|
||||
"mining_address_copied": "Адрес майнинга скопирован",
|
||||
"mining_all_time": "За всё время",
|
||||
"mining_already_saved": "URL пула уже сохранён",
|
||||
"mining_auto_balance_desc": "Распределяет майнеры по официальным пулам в зависимости от хешрейта, чтобы помочь децентрализовать сеть. Периодически проверяет каждый пул.",
|
||||
"mining_auto_balanced_to": "Автобалансировка майнинга на",
|
||||
"mining_benchmark_cancel": "Отменить тест",
|
||||
"mining_benchmark_cooling": "Охлаждение",
|
||||
"mining_benchmark_dismiss": "Закрыть",
|
||||
@@ -430,8 +654,10 @@
|
||||
"mining_idle_threads_active_tooltip": "Потоки при активности пользователя",
|
||||
"mining_idle_threads_idle_tooltip": "Потоки при простое системы",
|
||||
"mining_local_hashrate": "Локальный хешрейт",
|
||||
"mining_manual_desc": "Вручную: майнинг в пул, который вы выбираете из списка ниже.",
|
||||
"mining_mine": "Майнить",
|
||||
"mining_mining_addr": "Адрес майн.",
|
||||
"mining_mining_here": "майнинг здесь",
|
||||
"mining_network": "Сеть",
|
||||
"mining_no_blocks_yet": "Блоки пока не найдены",
|
||||
"mining_no_payouts_yet": "Выплат пула пока нет",
|
||||
@@ -441,18 +667,24 @@
|
||||
"mining_on": "Майнинг ВКЛЮЧЁН",
|
||||
"mining_open_in_explorer": "Открыть в обозревателе",
|
||||
"mining_payout_address": "Адрес выплат",
|
||||
"mining_payout_foreign": "⚠ Этот адрес выплат отсутствует в вашем текущем кошельке — намайненные вознаграждения будут отправлены в другой кошелёк. Обновите его, если вы сменили кошелёк.",
|
||||
"mining_payout_tooltip": "Адрес для получения вознаграждений за майнинг",
|
||||
"mining_pool": "Пул",
|
||||
"mining_pool_fee": "Комиссия",
|
||||
"mining_pool_hashrate": "Хешрейт пула",
|
||||
"mining_pool_url": "URL пула",
|
||||
"mining_pools_header": "ПУЛЫ",
|
||||
"mining_recent_blocks": "ПОСЛЕДНИЕ БЛОКИ",
|
||||
"mining_recent_payouts": "ПОСЛЕДНИЕ ВЫПЛАТЫ ПУЛА",
|
||||
"mining_refresh": "Обновить",
|
||||
"mining_remove": "Удалить",
|
||||
"mining_reset_defaults": "Сбросить настройки",
|
||||
"mining_save_payout_address": "Сохранить адрес выплат",
|
||||
"mining_save_pool_url": "Сохранить URL пула",
|
||||
"mining_saved_addresses": "Сохранённые адреса:",
|
||||
"mining_saved_pools": "Сохранённые пулы:",
|
||||
"mining_select_auto": "Авто-балансировка",
|
||||
"mining_select_manual": "Вручную",
|
||||
"mining_shares": "Шары",
|
||||
"mining_show_chart": "График",
|
||||
"mining_show_log": "Журнал",
|
||||
@@ -465,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "Остановите соло-майнинг для изменения настроек пула",
|
||||
"mining_stopping": "Остановка...",
|
||||
"mining_stopping_tooltip": "Майнер останавливается...",
|
||||
"mining_suggested_pools": "Рекомендуемые пулы",
|
||||
"mining_syncing_tooltip": "Блокчейн синхронизируется...",
|
||||
"mining_tag": " · Майнинг",
|
||||
"mining_threads": "Потоки майнинга",
|
||||
"mining_to_save": "для сохранения",
|
||||
"mining_today": "Сегодня",
|
||||
@@ -486,10 +720,12 @@
|
||||
"no_addresses_match": "Нет адресов, соответствующих фильтру",
|
||||
"no_addresses_with_balance": "Нет адресов с балансом",
|
||||
"no_addresses_yet": "Пока нет адресов",
|
||||
"no_icons_found": "Нет иконок, соответствующих запросу.",
|
||||
"no_matching": "Нет подходящих транзакций",
|
||||
"no_recent_receives": "Нет недавних получений",
|
||||
"no_recent_sends": "Нет недавних отправлений",
|
||||
"no_transactions": "Транзакции не найдены",
|
||||
"no_transactions_yet": "Транзакций пока нет",
|
||||
"node": "УЗЕЛ",
|
||||
"node_security": "УЗЕЛ И БЕЗОПАСНОСТЬ",
|
||||
"noise": "Шум",
|
||||
@@ -553,7 +789,66 @@
|
||||
"pending": "Ожидание",
|
||||
"pin_not_set": "PIN не установлен. Используйте пароль для разблокировки.",
|
||||
"ping": "Пинг",
|
||||
"portfolio_add_entry": "Добавить запись",
|
||||
"portfolio_add_to": "Добавить в портфель",
|
||||
"portfolio_addresses_hdr": "Адреса",
|
||||
"portfolio_addresses_sel": "выбрано: %d",
|
||||
"portfolio_all_funds": "Все средства",
|
||||
"portfolio_all_shielded": "Всё экранировано",
|
||||
"portfolio_all_transparent": "Все прозрачные",
|
||||
"portfolio_appearance": "Оформление",
|
||||
"portfolio_cancel": "Отмена",
|
||||
"portfolio_clear_sel": "Очистить",
|
||||
"portfolio_close": "Закрыть",
|
||||
"portfolio_color": "Цвет",
|
||||
"portfolio_currency": "Валюта",
|
||||
"portfolio_custom_color": "Свой цвет…",
|
||||
"portfolio_delete": "Удалить",
|
||||
"portfolio_detail_empty": "Выберите группу слева или добавьте новую, чтобы её редактировать.",
|
||||
"portfolio_edit": "Редактировать",
|
||||
"portfolio_funded": "С балансом",
|
||||
"portfolio_group_name": "Название группы",
|
||||
"portfolio_icon": "Значок",
|
||||
"portfolio_label": "Метка",
|
||||
"portfolio_manage": "Управление…",
|
||||
"portfolio_manage_title": "Управление портфелем",
|
||||
"portfolio_manual_price": "Цена / DRGX",
|
||||
"portfolio_new_entry": "Новая запись",
|
||||
"portfolio_no_addr_match": "Нет совпадающих адресов",
|
||||
"portfolio_no_entries": "Пока нет пользовательских записей. Добавьте одну, чтобы отслеживать группу адресов.",
|
||||
"portfolio_no_icon": "Нет",
|
||||
"portfolio_outline_opacity": "Непрозрачность контура",
|
||||
"portfolio_price": "Цена",
|
||||
"portfolio_price_btc": "Рынок · BTC",
|
||||
"portfolio_price_drgx": "Только DRGX",
|
||||
"portfolio_price_manual": "Вручную",
|
||||
"portfolio_price_usd": "Рынок · USD",
|
||||
"portfolio_remove_from": "Удалить из портфеля",
|
||||
"portfolio_revert": "Отменить изменения",
|
||||
"portfolio_save": "Сохранить",
|
||||
"portfolio_search": "Поиск адресов…",
|
||||
"portfolio_search_icons": "Поиск иконок…",
|
||||
"portfolio_select_all": "Все",
|
||||
"portfolio_select_shown": "Выбрать все",
|
||||
"portfolio_show": "Показать:",
|
||||
"portfolio_show_24h": "24ч",
|
||||
"portfolio_show_sparkline": "График",
|
||||
"portfolio_show_value": "Стоимость",
|
||||
"portfolio_spark_day": "День",
|
||||
"portfolio_spark_hour": "Час",
|
||||
"portfolio_spark_min": "Минута",
|
||||
"portfolio_spark_month": "Месяц",
|
||||
"portfolio_spark_week": "Неделя",
|
||||
"portfolio_style_compact": "Компактные строки",
|
||||
"portfolio_style_detailed": "Подробные строки",
|
||||
"portfolio_style_featured": "Избранные строки",
|
||||
"portfolio_style_label": "Стиль портфеля",
|
||||
"portfolio_untitled": "Без названия",
|
||||
"price_chart": "График цен",
|
||||
"privacy_great": "Отличная конфиденциальность!",
|
||||
"privacy_low": "Низкая конфиденциальность — экранируйте средства",
|
||||
"privacy_medium": "Рассмотрите возможность экранировать больше",
|
||||
"processing_transaction": "Обработка транзакции...",
|
||||
"qr_code": "QR-код",
|
||||
"qr_failed": "Не удалось сгенерировать QR-код",
|
||||
"qr_title": "QR-код",
|
||||
@@ -571,6 +866,7 @@
|
||||
"receiving_addresses": "Ваши адреса для получения",
|
||||
"recent_received": "НЕДАВНО ПОЛУЧЕНО",
|
||||
"recent_sends": "НЕДАВНО ОТПРАВЛЕНО",
|
||||
"recent_transactions": "ПОСЛЕДНИЕ ТРАНЗАКЦИИ",
|
||||
"recipient": "ПОЛУЧАТЕЛЬ",
|
||||
"recipient_balance": "Получатель: %.8f → %.8f DRGX",
|
||||
"recv_type": "Получ.",
|
||||
@@ -578,6 +874,7 @@
|
||||
"refresh": "Обновить",
|
||||
"refresh_now": "Обновить сейчас",
|
||||
"remove_favorite": "Удалить из избранного",
|
||||
"repair_wallet": "Восстановить кошелёк",
|
||||
"report_bug": "Сообщить об ошибке",
|
||||
"request_amount": "Сумма (необязательно):",
|
||||
"request_copy_uri": "Копировать URI",
|
||||
@@ -593,12 +890,16 @@
|
||||
"request_transparent_addrs": "-- Прозрачные адреса --",
|
||||
"request_uri_copied": "URI платежа скопирован в буфер обмена",
|
||||
"rescan": "Пересканировать",
|
||||
"rescan_bootstrapped_msg": "Ваш узел был инициализирован из бутстрапа, поэтому блоки ниже снимка отсутствуют на диске, и пересканирование с генезиса не удастся. Пересканируйте с высоты, входящей в ваш снимок, чтобы согласовать потраченный баланс кошелька. Ваш wallet.dat и данные блокчейна не удаляются.",
|
||||
"rescan_detecting": "Проверка, какие блоки уже есть на диске вашего узла…",
|
||||
"rescan_from_height": "Пересканировать с высоты блока:",
|
||||
"reset_to_defaults": "Сбросить настройки",
|
||||
"restarting_after_encryption": "Перезапуск демона после шифрования...",
|
||||
"restore_address": "Восстановить адрес",
|
||||
"result_preview": "Предпросмотр результата",
|
||||
"retry": "Повторить",
|
||||
"review_send": "Проверить отправку",
|
||||
"rpc_connection": "RPC-подключение...",
|
||||
"rpc_host": "RPC-хост",
|
||||
"rpc_pass": "Пароль",
|
||||
"rpc_port": "Порт",
|
||||
@@ -608,12 +909,15 @@
|
||||
"save_z_transactions": "Сохранять Z-tx в списке транзакций",
|
||||
"sb_auth_failed": "Ошибка авторизации — проверьте rpcuser/rpcpassword",
|
||||
"sb_block": "Блок: %d",
|
||||
"sb_building_witnesses": "Установка свидетелей",
|
||||
"sb_building_witnesses_pct": "Перестроение свидетелей %.0f%%",
|
||||
"sb_connecting_daemon": "Подключение к dragonxd...",
|
||||
"sb_connecting_err": "Подключение к демону — %s",
|
||||
"sb_connecting_external": "Подключение к внешнему демону...",
|
||||
"sb_connecting_generic": "Подключение к демону...",
|
||||
"sb_daemon_crashed": "Демон упал %d раз",
|
||||
"sb_daemon_not_found": "Демон не найден",
|
||||
"sb_daemon_start_failed": "Не удалось запустить dragonxd",
|
||||
"sb_dragonxd_running": "dragonxd запущен",
|
||||
"sb_dragonxd_stopped": "dragonxd остановлен",
|
||||
"sb_dragonxd_stopping": "Остановка dragonxd...",
|
||||
@@ -639,6 +943,12 @@
|
||||
"sb_waiting_daemon": "Ожидание dragonxd...",
|
||||
"sb_waiting_daemon_err": "Ожидание dragonxd — %s",
|
||||
"sb_warming_up": "Прогрев...",
|
||||
"sb_witness_cache": "Перестроение свидетелей",
|
||||
"screenshot_open_dir": "Открыть расположение",
|
||||
"screenshot_sweep": "Запустить прогон скриншотов",
|
||||
"screenshot_sweep_desc": "Перебирает каждую тему по всем вкладкам и сохраняет скриншот каждой в подпапки по вкладкам внутри папки screenshots в каталоге конфигурации (перезаписывая предыдущий проход). Выполняется несколько секунд.",
|
||||
"screenshot_sweep_full": "Полный обход интерфейса",
|
||||
"search_icons": "Поиск значков...",
|
||||
"search_placeholder": "Поиск...",
|
||||
"security": "БЕЗОПАСНОСТЬ",
|
||||
"seed_backup_button": "Сид-фраза",
|
||||
@@ -655,6 +965,7 @@
|
||||
"seed_backup_saved": "Сохранено в ",
|
||||
"seed_backup_title": "Резервная копия сид-фразы",
|
||||
"seed_backup_warning": "Запишите их по порядку, храните офлайн и никогда не делитесь ими и не фотографируйте их. Если вы их потеряете, ваши средства не удастся восстановить.",
|
||||
"seed_migrate_button": "Перейти на сид-кошелёк…",
|
||||
"select_address": "Выбрать адрес...",
|
||||
"select_receiving_address": "Выбрать адрес получения...",
|
||||
"select_source_address": "Выбрать адрес-источник...",
|
||||
@@ -663,8 +974,10 @@
|
||||
"send_amount_details": "ДЕТАЛИ СУММЫ",
|
||||
"send_amount_upper": "СУММА",
|
||||
"send_clear_fields": "Очистить все поля формы?",
|
||||
"send_contacts_button": "Выбрать из контактов",
|
||||
"send_copy_error": "Копировать ошибку",
|
||||
"send_dismiss": "Отклонить",
|
||||
"send_err_needs_rescan": "Данные экранированных заметок вашего кошелька устарели относительно блокчейна (это происходит после бутстрапа или переиндексации). Выполните полное пересканирование через Настройки → Пересканировать блокчейн и дайте ему полностью завершиться, затем повторите попытку отправки.",
|
||||
"send_error_copied": "Ошибка скопирована в буфер обмена",
|
||||
"send_error_prefix": "Ошибка: %s",
|
||||
"send_exceeds_available": "Превышает доступное (%.8f)",
|
||||
@@ -683,6 +996,7 @@
|
||||
"send_recipient": "ПОЛУЧАТЕЛЬ",
|
||||
"send_select_source": "Выбрать адрес-источник...",
|
||||
"send_sending_from": "ОТПРАВКА С",
|
||||
"send_status_unconfirmed": "Не удалось подтвердить статус транзакции",
|
||||
"send_submitting": "Отправка транзакции...",
|
||||
"send_switch_to_receive": "Перейдите к получению, чтобы получить свой адрес и начать получать средства.",
|
||||
"send_to": "Отправить на",
|
||||
@@ -755,6 +1069,8 @@
|
||||
"settings_noise_opacity": "Непрозрачность шума:",
|
||||
"settings_not_encrypted": "Не зашифрован",
|
||||
"settings_not_found": "Не найден",
|
||||
"settings_open_app_dir": "Открыть папку приложения",
|
||||
"settings_open_data_dir": "Открыть папку данных",
|
||||
"settings_other": "Прочее",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "Конфиденциальность",
|
||||
@@ -788,16 +1104,24 @@
|
||||
"shield_check_status": "Проверить статус",
|
||||
"shield_completed": "Операция успешно завершена!",
|
||||
"shield_description": "Экранируйте вознаграждения за майнинг, отправив coinbase-выходы с прозрачных адресов на экранированный адрес. Это улучшает конфиденциальность, скрывая ваш доход от майнинга.",
|
||||
"shield_error_prefix": "Ошибка: ",
|
||||
"shield_from_address": "С адреса:",
|
||||
"shield_funds": "Экранировать средства",
|
||||
"shield_in_progress": "Операция выполняется...",
|
||||
"shield_max_utxos": "Макс. UTXO за операцию",
|
||||
"shield_merge_done": "Экранирование/объединение завершено!",
|
||||
"shield_op_failed": "Ошибка операции: ",
|
||||
"shield_op_submitted": "Операция отправлена: ",
|
||||
"shield_operation_id": "ID операции: %s",
|
||||
"shield_select_z": "Выбрать z-адрес...",
|
||||
"shield_send_failed": "Не удалось экранировать: ",
|
||||
"shield_started": "Операция экранирования начата",
|
||||
"shield_status_check_error": "Ошибка при проверке статуса: ",
|
||||
"shield_status_label": "Статус: ",
|
||||
"shield_submitting": "Отправка операции...",
|
||||
"shield_title": "Экранировать вознаграждения coinbase",
|
||||
"shield_to_address": "На адрес (экранированный):",
|
||||
"shield_unknown_error": "Неизвестная ошибка",
|
||||
"shield_utxo_limit": "Лимит UTXO:",
|
||||
"shield_wildcard_hint": "Используйте '*' для экранирования со всех прозрачных адресов",
|
||||
"shielded": "Экранированный",
|
||||
@@ -807,11 +1131,16 @@
|
||||
"shielding_notice": "Примечание: это переведёт средства с прозрачного (T) адреса на приватный (Z) адрес.",
|
||||
"show": "Показать",
|
||||
"show_hidden": "Показать скрытые (%d)",
|
||||
"show_qr": "Показать QR",
|
||||
"show_qr_code": "Показать QR-код",
|
||||
"showing_transactions": "Показано %d–%d из %d транзакций (всего: %zu)",
|
||||
"showing_x_of_y": "Показано %d из %d адресов",
|
||||
"simple_background": "Простой фон",
|
||||
"slider_off": "Выкл",
|
||||
"sort_amount_high": "Наибольшая сумма",
|
||||
"sort_amount_low": "Наименьшая сумма",
|
||||
"sort_date_newest": "Сначала новые",
|
||||
"sort_date_oldest": "Сначала старые",
|
||||
"start_mining": "Начать майнинг",
|
||||
"status": "Статус",
|
||||
"stop_external": "Остановить внешний daemon",
|
||||
@@ -866,6 +1195,8 @@
|
||||
"tt_clear_ztx": "Удалить локально кешированную историю z-транзакций",
|
||||
"tt_custom_fees": "Включить ручной ввод комиссий при отправке транзакций",
|
||||
"tt_custom_theme": "Пользовательская тема активна",
|
||||
"tt_daemon_install_bundled": "Остановить узел, перезаписать установленный dragonxd версией, встроенной в эту сборку кошелька, затем перезапустить",
|
||||
"tt_daemon_update_check": "Скачать и проверить последний полный узел dragonxd из проектного Gitea, затем перезапустить для применения",
|
||||
"tt_debug_collapse": "Свернуть параметры журнала отладки",
|
||||
"tt_debug_expand": "Развернуть параметры журнала отладки",
|
||||
"tt_delete_blockchain": "Удалить все данные блокчейна и начать синхронизацию заново. wallet.dat и конфигурация сохраняются.",
|
||||
@@ -881,15 +1212,19 @@
|
||||
"tt_keep_daemon": "Демон будет остановлен при запуске мастера настройки",
|
||||
"tt_language": "Язык интерфейса кошелька",
|
||||
"tt_layout_hotkey": "Горячая клавиша: стрелки влево/вправо для переключения раскладок Баланса",
|
||||
"tt_lite_redownload": "Заново загрузить и пересканировать все блоки с лёгкого сервера",
|
||||
"tt_lock": "Немедленно заблокировать кошелёк",
|
||||
"tt_low_spec": "Отключить все тяжёлые визуальные эффекты\\nГорячая клавиша: Ctrl+Shift+Down",
|
||||
"tt_merge": "Объединить несколько UTXO в один адрес",
|
||||
"tt_mine_idle": "Автоматически начать майнинг при\\nпростое системы (нет ввода с клавиатуры/мыши)",
|
||||
"tt_noise": "Интенсивность зернистой текстуры (0%% = выкл., 100%% = максимум)",
|
||||
"tt_open_app_dir": "Открыть папку ObsidianDragon (настройки, темы, логи) в файловом менеджере",
|
||||
"tt_open_data_dir": "Открыть в файловом менеджере папку с данными кошелька и блокчейна",
|
||||
"tt_open_dir": "Нажмите, чтобы открыть в проводнике",
|
||||
"tt_reduce_motion": "Отключить анимированные переходы и плавное изменение баланса для доступности",
|
||||
"tt_remove_encrypt": "Удалить шифрование и хранить кошелёк без защиты",
|
||||
"tt_remove_pin": "Удалить PIN и требовать пароль для разблокировки",
|
||||
"tt_repair_wallet": "Очистить и перестроить записи транзакций кошелька из блокчейна (исправляет заметки, которые не отправляются после пересканирования)",
|
||||
"tt_report_bug": "Сообщить о проблеме в трекере проекта",
|
||||
"tt_request_payment": "Сгенерировать запрос на оплату с QR-кодом",
|
||||
"tt_rescan": "Пересканировать блокчейн для поиска пропущенных транзакций",
|
||||
@@ -904,6 +1239,7 @@
|
||||
"tt_scan_themes": "Поиск новых тем.\\nРазместите папки тем в:\\n%s",
|
||||
"tt_scanline": "Эффект развёртки ЭЛТ в консоли",
|
||||
"tt_seed_backup": "Показать и создать резервную копию сид-фразы восстановления вашего кошелька из 24 слов",
|
||||
"tt_seed_migrate": "Создать новый кошелёк с сид-фразой и перевести в него ваши средства",
|
||||
"tt_set_pin": "Установить 4-8-значный PIN для быстрой разблокировки",
|
||||
"tt_shield_mining": "Перевести прозрачные вознаграждения за майнинг на экранированный адрес",
|
||||
"tt_simple_bg": "Использовать простой градиент для фона\\nГорячая клавиша: Ctrl+Up",
|
||||
@@ -917,6 +1253,7 @@
|
||||
"tt_ui_opacity": "Непрозрачность карточек и боковой панели (100%% = полностью непрозрачно, ниже = прозрачнее)",
|
||||
"tt_validate": "Проверить, действителен ли адрес DragonX",
|
||||
"tt_verbose": "Записывать подробную диагностику подключений,\\nсостояние демона и информацию о владельце порта\\nна вкладке Консоль",
|
||||
"tt_wallets_button": "Показать файлы кошельков и переключаться между ними",
|
||||
"tt_website": "Открыть сайт DragonX",
|
||||
"tt_window_opacity": "Непрозрачность фона (ниже = рабочий стол виден сквозь окно)",
|
||||
"tt_wizard": "Повторно запустить мастер начальной настройки\\nДемон будет перезапущен",
|
||||
@@ -925,7 +1262,18 @@
|
||||
"tx_from_address": "Адрес отправителя:",
|
||||
"tx_id_label": "ID транзакции:",
|
||||
"tx_immature": "НЕЗРЕЛАЯ",
|
||||
"tx_loading_enriching_sends": "Проверка деталей отправленных транзакций (%d)",
|
||||
"tx_loading_fetching_transparent": "Получение прозрачной истории",
|
||||
"tx_loading_history_progress": "Загрузка старой истории (%d%%)",
|
||||
"tx_loading_queued": "Обновление транзакций поставлено в очередь",
|
||||
"tx_loading_refreshing_cached": "Обновление истории кошелька (в кеше: %d)",
|
||||
"tx_loading_scanning_shielded": "Сканирование экранированной истории (%d адресов)",
|
||||
"tx_mined": "ДОБЫТА",
|
||||
"tx_progress_balances": "Обновление балансов",
|
||||
"tx_progress_finalizing": "Завершение транзакции",
|
||||
"tx_progress_history": "Обновление истории",
|
||||
"tx_progress_submitting": "Отправка транзакции в демон",
|
||||
"tx_progress_waiting_ops": "Ожидание операции (%d)",
|
||||
"tx_received": "ПОЛУЧЕНО",
|
||||
"tx_sent": "ОТПРАВЛЕНО",
|
||||
"tx_to_address": "Адрес получателя:",
|
||||
@@ -937,6 +1285,14 @@
|
||||
"unconfirmed": "Не подтверждено",
|
||||
"undo_clear": "Отменить очистку",
|
||||
"unknown": "Неизвестно",
|
||||
"unmark_mining_address": "Снять отметку с адреса майнинга",
|
||||
"upd_back": "Назад",
|
||||
"upd_install": "Установить",
|
||||
"upd_installed_badge": "установлено",
|
||||
"upd_no_build_platform": "Нет сборки для этой платформы",
|
||||
"upd_prerelease": "предварительный релиз",
|
||||
"upd_reinstall": "Переустановить",
|
||||
"upd_select_version": "Выберите версию для установки",
|
||||
"use_embedded_daemon": "Использовать встроенный dragonxd",
|
||||
"use_tor": "Использовать Tor",
|
||||
"validate_btn": "Проверить",
|
||||
@@ -956,17 +1312,73 @@
|
||||
"verbose_logging": "Подробное логирование",
|
||||
"version": "Версия",
|
||||
"view": "Просмотр",
|
||||
"view_all": "Показать всё",
|
||||
"view_details": "Подробнее",
|
||||
"view_on_explorer": "Посмотреть в обозревателе",
|
||||
"waiting_for_daemon": "Ожидание подключения к daemon...",
|
||||
"wallet": "КОШЕЛЁК",
|
||||
"wallet_empty": "Ваш кошелёк пуст",
|
||||
"wallet_empty_hint": "Перейдите к получению, чтобы получить свой адрес и начать получать средства.",
|
||||
"wallets_active": "Активен",
|
||||
"wallets_add": "Добавить папку",
|
||||
"wallets_add_folder": "Также сканировать другую папку на наличие файлов кошельков:",
|
||||
"wallets_add_folder_toggle": "+ Искать кошельки в другой папке…",
|
||||
"wallets_button": "Кошельки…",
|
||||
"wallets_col_addresses": "Адреса",
|
||||
"wallets_col_balance": "Баланс",
|
||||
"wallets_col_name": "Кошелёк",
|
||||
"wallets_col_opened": "Последнее открытие",
|
||||
"wallets_col_size": "Размер",
|
||||
"wallets_create": "Создать кошелёк",
|
||||
"wallets_creating": "Создание кошелька — узел будет перезапущен…",
|
||||
"wallets_current": "текущий",
|
||||
"wallets_exists": "Кошелёк с таким именем уже существует.",
|
||||
"wallets_external_tt": "Вне вашего каталога данных — «Импорт» скопирует его.",
|
||||
"wallets_folder_hint": "/путь/к/папке с файлами кошельков .dat",
|
||||
"wallets_folder_invalid": "Такой папки не существует.",
|
||||
"wallets_import": "Импорт",
|
||||
"wallets_import_failed": "Не удалось импортировать этот файл кошелька.",
|
||||
"wallets_import_hint": "Импортировать для открытия",
|
||||
"wallets_import_tt": "Этот кошелёк находится вне папки данных. При открытии он сначала будет скопирован в неё (скоро).",
|
||||
"wallets_imported": "Кошелёк импортирован — переключение…",
|
||||
"wallets_intro": "Файлы кошельков в вашей папке данных (и в любых добавленных вами папках). Откройте один, чтобы переключиться — узел перезапустится, чтобы загрузить его, и у каждого кошелька свои данные.",
|
||||
"wallets_name_invalid": "Введите допустимое имя кошелька.",
|
||||
"wallets_never": "Никогда",
|
||||
"wallets_new_hint": "Имя (например, savings)",
|
||||
"wallets_new_label": "Создать новый кошелёк:",
|
||||
"wallets_open": "Открыть",
|
||||
"wallets_reveal": "Открыть папку",
|
||||
"wallets_title": "Кошельки",
|
||||
"warning": "Предупреждение",
|
||||
"warning_upper": "ПРЕДУПРЕЖДЕНИЕ!",
|
||||
"website": "Веб-сайт",
|
||||
"window_opacity": "Прозрачность окна",
|
||||
"wizard_daemon_start_failed": "Не удалось запустить демон — будет повторено автоматически",
|
||||
"xmrig_browse_releases": "Просмотреть все релизы…",
|
||||
"xmrig_checking": "Проверка наличия последней версии майнера…",
|
||||
"xmrig_current": "Текущая:",
|
||||
"xmrig_download_install": "Скачать и установить",
|
||||
"xmrig_downloading": "Загрузка…",
|
||||
"xmrig_installed": "Установлено:",
|
||||
"xmrig_installed_ok": "Майнер установлен",
|
||||
"xmrig_installing": "Установка…",
|
||||
"xmrig_latest": "Последняя:",
|
||||
"xmrig_loading_releases": "Загрузка релизов…",
|
||||
"xmrig_none": "нет",
|
||||
"xmrig_reinstall": "Переустановить",
|
||||
"xmrig_stop_mining_first": "Остановите майнинг перед обновлением майнера.",
|
||||
"xmrig_unavailable_body": "Для этой платформы нет доступной сборки майнера.",
|
||||
"xmrig_unavailable_title": "Обновления майнера недоступны",
|
||||
"xmrig_unknown_error": "Неизвестная ошибка.",
|
||||
"xmrig_up_to_date": "Майнер обновлён до последней версии",
|
||||
"xmrig_update_available": "Доступен новый майнер",
|
||||
"xmrig_update_button": "Обновить майнер…",
|
||||
"xmrig_update_failed": "Ошибка обновления",
|
||||
"xmrig_update_short": "Обновить",
|
||||
"xmrig_update_title": "Обновить майнер",
|
||||
"xmrig_verify_note": "Загрузка проверяется по опубликованной для релиза контрольной сумме SHA-256 перед установкой.",
|
||||
"xmrig_verifying": "Проверка…",
|
||||
"xmrig_version": "Версия:",
|
||||
"yes_clear": "Да, очистить",
|
||||
"your_addresses": "Ваши адреса",
|
||||
"z_address": "Z-адрес",
|
||||
|
||||
412
res/lang/zh.json
412
res/lang/zh.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "添加地址",
|
||||
"address_book_add_new": "添加新地址",
|
||||
"address_book_added": "地址已添加到通讯录",
|
||||
"address_book_confirm_delete": "确认删除?",
|
||||
"address_book_count": "已保存 %zu 个地址",
|
||||
"address_book_deleted": "条目已删除",
|
||||
"address_book_edit": "编辑地址",
|
||||
@@ -39,6 +40,7 @@
|
||||
"address_copied": "地址已复制到剪贴板",
|
||||
"address_details": "地址详情",
|
||||
"address_label": "地址:",
|
||||
"address_reorder_hint": "拖放到某行的边缘可重新排序,拖到中间可转移",
|
||||
"address_upper": "地址",
|
||||
"address_url": "地址 URL",
|
||||
"addresses_appear_here": "连接后,您的接收地址将显示在此处。",
|
||||
@@ -69,7 +71,10 @@
|
||||
"backup_wallet": "备份钱包...",
|
||||
"backup_wallet_not_found": "警告:在预期位置未找到 wallet.dat",
|
||||
"balance": "余额",
|
||||
"balance_history_collecting": "余额历史——正在收集数据…",
|
||||
"balance_layout": "余额布局",
|
||||
"balance_shielded_fmt": "屏蔽:%.8f",
|
||||
"balance_transparent_fmt": "透明:%.8f",
|
||||
"ban": "封禁",
|
||||
"banned_peers": "已封禁节点",
|
||||
"block": "区块",
|
||||
@@ -151,6 +156,20 @@
|
||||
"confirm_delete_blockchain_msg": "这将停止守护进程,删除所有区块链数据(blocks、chainstate、peers),并从头开始重新同步。这可能需要几个小时。",
|
||||
"confirm_delete_blockchain_safe": "您的 wallet.dat、配置和交易历史是安全的,不会被删除。",
|
||||
"confirm_delete_blockchain_title": "删除区块链数据",
|
||||
"confirm_lite_redownload_msg": "这将清除钱包已下载的区块数据,并从轻钱包服务器重新获取并重新扫描每个区块。可能需要一段时间;在完成之前,钱包会显示同步进度。",
|
||||
"confirm_lite_redownload_safe": "您的钱包、密钥和助记词不受影响——仅重新下载区块数据。",
|
||||
"confirm_lite_redownload_title": "重新下载区块",
|
||||
"confirm_reinstall_daemon_msg": "此操作会停止守护进程,用本钱包版本内置的版本覆盖已安装的 dragonxd(以及 dragonx-cli/dragonx-tx),然后重启节点。用于恢复或更新节点程序文件。",
|
||||
"confirm_reinstall_daemon_safe": "您的钱包、密钥和区块链数据不会被改动——仅替换守护进程的程序文件。",
|
||||
"confirm_reinstall_daemon_title": "安装内置守护进程",
|
||||
"confirm_repair_wallet_msg": "这将以 -zapwallettxes=2 重启守护进程:它会删除钱包的所有交易和备注记录,然后从区块链重建它们。当即使完整重新扫描后交易仍无法构建(“Invalid sapling spend proof”/“shielded requirements not met”)时,请使用此功能。它耗时较长,钱包在完成前将保持离线。",
|
||||
"confirm_repair_wallet_safe": "您的密钥、地址和余额将被保留——仅重建缓存的交易记录。",
|
||||
"confirm_repair_wallet_title": "修复钱包",
|
||||
"confirm_rescan_msg": "此操作会重启守护进程并重新扫描整个区块链以查找您钱包的交易。可能需要很长时间,且钱包在完成前将保持离线。",
|
||||
"confirm_rescan_safe": "您的 wallet.dat 和区块链数据不会被删除——仅重新扫描。",
|
||||
"confirm_rescan_title": "重新扫描区块链",
|
||||
"confirm_restart_daemon_msg": "这将停止并重启守护进程以应用更改的选项。钱包将短暂断开连接后重新连接。",
|
||||
"confirm_restart_daemon_title": "重启守护进程",
|
||||
"confirm_send": "确认发送",
|
||||
"confirm_transaction": "确认交易",
|
||||
"confirm_transfer": "确认转账",
|
||||
@@ -161,6 +180,7 @@
|
||||
"connected_peers": "已连接节点",
|
||||
"connecting": "连接中...",
|
||||
"console": "控制台",
|
||||
"console_app": "应用",
|
||||
"console_auto_scroll": "自动滚动",
|
||||
"console_available_commands": "可用命令:",
|
||||
"console_capturing_output": "正在捕获守护进程输出...",
|
||||
@@ -177,6 +197,7 @@
|
||||
"console_connected": "已连接到守护进程",
|
||||
"console_copy_all": "全部复制",
|
||||
"console_copy_selected": "复制",
|
||||
"console_copy_value": "复制",
|
||||
"console_daemon": "守护进程",
|
||||
"console_daemon_error": "守护进程错误!",
|
||||
"console_daemon_started": "守护进程已启动",
|
||||
@@ -195,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - 控制挖矿",
|
||||
"console_help_stop": " stop - 停止守护进程",
|
||||
"console_line_count": "%zu 行",
|
||||
"console_matches": "个匹配",
|
||||
"console_new_lines": "%d 新行",
|
||||
"console_no_daemon": "无守护进程",
|
||||
"console_not_connected": "错误:未连接到守护进程",
|
||||
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。",
|
||||
"console_rpc_reference": "RPC 命令参考",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "控制台扫描线",
|
||||
"console_search_commands": "搜索命令...",
|
||||
"console_select_all": "全选",
|
||||
"console_show_app_output": "显示[应用]钱包日志行",
|
||||
"console_show_daemon_output": "显示守护进程输出",
|
||||
"console_show_errors_only": "仅显示错误",
|
||||
"console_show_rpc_ref": "显示 RPC 命令参考",
|
||||
"console_show_rpc_trace": "显示应用 RPC 调用",
|
||||
"console_showing_lines": "显示 %zu / %zu 行",
|
||||
"console_starting_node": "正在启动节点...",
|
||||
"console_status_error": "错误",
|
||||
@@ -218,6 +244,12 @@
|
||||
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
|
||||
"console_zoom_in": "放大",
|
||||
"console_zoom_out": "缩小",
|
||||
"contact_global": "在每个钱包中显示(全局联系人)",
|
||||
"contact_global_badge_tt": "全局联系人——在每个钱包中可见",
|
||||
"contact_global_tt": "开启:无论您加载哪个钱包,此联系人都保持可见。关闭:它仅属于当前钱包。",
|
||||
"contacts": "联系人",
|
||||
"contacts_search_no_match": "没有匹配的联系人",
|
||||
"contacts_search_placeholder": "搜索联系人...",
|
||||
"copied": "已复制!",
|
||||
"copy": "复制",
|
||||
"copy_address": "复制完整地址",
|
||||
@@ -227,6 +259,44 @@
|
||||
"copy_uri": "复制 URI",
|
||||
"current_price": "当前价格",
|
||||
"custom_fees": "自定义手续费",
|
||||
"daemon_binary": "守护进程二进制文件",
|
||||
"daemon_bundled": "内置",
|
||||
"daemon_install_bundled": "安装内置版本",
|
||||
"daemon_installed": "已安装",
|
||||
"daemon_none_bundled": "此版本未内置",
|
||||
"daemon_not_installed": "未安装",
|
||||
"daemon_status_differ": "已安装的程序文件与内置版本不同。",
|
||||
"daemon_status_match": "已安装的程序与内置版本一致。",
|
||||
"daemon_status_missing": "未安装守护进程——请安装内置版本。",
|
||||
"daemon_update_available": "有新的节点版本可用",
|
||||
"daemon_update_body": "此钱包版本内置的 DragonX 节点比当前已安装的更新。更新会替换已安装的 dragonxd(以及 dragonx-cli/dragonx-tx),然后停止并重启节点以使新版本生效。推荐更新——更新的节点可能增加旧版本所缺少的功能(例如助记词支持)。",
|
||||
"daemon_update_browse": "浏览所有版本…",
|
||||
"daemon_update_check": "检查更新…",
|
||||
"daemon_update_checking": "正在检查最新节点…",
|
||||
"daemon_update_downgrade_note": "较旧的版本可能与您当前的链数据不兼容。安装其他版本将在守护进程重启后生效。",
|
||||
"daemon_update_download_install": "下载并安装",
|
||||
"daemon_update_downloading": "正在下载…",
|
||||
"daemon_update_failed": "更新失败",
|
||||
"daemon_update_installed": "已安装:",
|
||||
"daemon_update_installed_ok": "节点已安装",
|
||||
"daemon_update_installing": "正在安装…",
|
||||
"daemon_update_keep": "保持当前版本",
|
||||
"daemon_update_later": "稍后",
|
||||
"daemon_update_latest": "最新:",
|
||||
"daemon_update_loading": "正在加载版本…",
|
||||
"daemon_update_now": "立即更新",
|
||||
"daemon_update_reinstall": "重新安装",
|
||||
"daemon_update_restart_note": "重启守护进程以开始运行新版本。",
|
||||
"daemon_update_restart_now": "立即重启守护进程",
|
||||
"daemon_update_safe": "您的钱包、密钥和区块链数据不会被改动——仅替换守护进程的程序文件。如果您是有意运行自定义节点,请选择“保持当前版本”。",
|
||||
"daemon_update_title": "更新节点守护进程?",
|
||||
"daemon_update_unavailable_body": "此平台没有可用的节点版本。",
|
||||
"daemon_update_unavailable_title": "节点更新不可用",
|
||||
"daemon_update_unknown_error": "未知错误。",
|
||||
"daemon_update_up_to_date": "节点已是最新版本",
|
||||
"daemon_update_verify_note": "在安装前,会根据该版本发布的 SHA-256 和固定的 ed25519 签名对下载内容进行校验。",
|
||||
"daemon_update_verifying": "正在验证…",
|
||||
"daemon_update_version": "版本:",
|
||||
"daemon_version": "守护进程",
|
||||
"dark": "深色",
|
||||
"date": "日期",
|
||||
@@ -257,10 +327,14 @@
|
||||
"explorer_block_time": "时间",
|
||||
"explorer_block_txs": "交易",
|
||||
"explorer_chain_stats": "链",
|
||||
"explorer_hash_not_found": "未找到此哈希对应的区块或交易",
|
||||
"explorer_invalid_query": "输入区块高度或64位哈希",
|
||||
"explorer_invalid_response": "守护进程返回的响应无效",
|
||||
"explorer_mempool": "内存池",
|
||||
"explorer_mempool_size": "大小",
|
||||
"explorer_mempool_txs": "交易",
|
||||
"explorer_no_results": "没有匹配的缓存区块",
|
||||
"explorer_not_connected": "未连接到守护进程——无法查询区块或交易哈希",
|
||||
"explorer_recent_blocks": "最近区块",
|
||||
"explorer_search": "搜索",
|
||||
"explorer_section": "浏览器",
|
||||
@@ -312,6 +386,7 @@
|
||||
"hidden_tag": " (已隐藏)",
|
||||
"hide": "隐藏",
|
||||
"hide_address": "隐藏地址",
|
||||
"hide_qr": "隐藏二维码",
|
||||
"hide_zero_balances": "隐藏零余额",
|
||||
"history": "历史",
|
||||
"immature_type": "未成熟",
|
||||
@@ -350,19 +425,120 @@
|
||||
"label_placeholder": "例如 储蓄、挖矿...",
|
||||
"language": "语言",
|
||||
"light": "浅色",
|
||||
"lite_account_label": "账户",
|
||||
"lite_action": "操作",
|
||||
"lite_backup_keys": "备份与密钥",
|
||||
"lite_birthday_backup": "生日区块:%llu (也请一并备份)",
|
||||
"lite_birthday_hint": "开始扫描的区块高度。如未知请保留 0(完整扫描更慢)。",
|
||||
"lite_birthday_label": "诞生区块",
|
||||
"lite_console_help_passthrough": "其他任何输入都将作为轻钱包控制台命令运行。",
|
||||
"lite_copy": "复制",
|
||||
"lite_could_not_write": "无法写入 ",
|
||||
"lite_encrypt_wallet": "加密钱包",
|
||||
"lite_encryption_removed": "已移除加密",
|
||||
"lite_hide_wipe": "隐藏并清除",
|
||||
"lite_import": "导入",
|
||||
"lite_import_key_label": "导入密钥",
|
||||
"lite_key_imported": "密钥已导入——运行同步以扫描其历史记录",
|
||||
"lite_lock_failed": "锁定失败",
|
||||
"lite_lock_now": "立即锁定",
|
||||
"lite_maintenance": "维护",
|
||||
"lite_net_add": "添加",
|
||||
"lite_net_add_label_hint": "标签(可选)",
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "检查中…",
|
||||
"lite_net_connected": "已连接",
|
||||
"lite_net_custom": "自定义",
|
||||
"lite_net_disconnected": "未连接",
|
||||
"lite_net_hidden_section": "隐藏的服务器",
|
||||
"lite_net_hide": "隐藏",
|
||||
"lite_net_in_use": "使用中",
|
||||
"lite_net_intro": "选择一个要使用的服务器,或让钱包随机选择一个。更改会立即生效。",
|
||||
"lite_net_invalid_url": "请输入有效的 http(s):// URL。",
|
||||
"lite_net_no_wallet": "没有打开的钱包",
|
||||
"lite_net_official": "官方",
|
||||
"lite_net_offline": "离线",
|
||||
"lite_net_random_active": "已启用随机服务器选择。",
|
||||
"lite_net_random_server": "随机服务器",
|
||||
"lite_net_refresh": "刷新",
|
||||
"lite_net_show_hidden": "显示已隐藏的服务器",
|
||||
"lite_net_sync_label": "同步",
|
||||
"lite_net_synced": "已同步",
|
||||
"lite_net_syncing": "正在同步",
|
||||
"lite_net_title": "Lite 服务器",
|
||||
"lite_net_unhide": "取消隐藏",
|
||||
"lite_net_use_random": "每次使用随机服务器",
|
||||
"lite_no_wallet": "未打开钱包——请在设置中创建或打开一个",
|
||||
"lite_no_wallet_short": "没有打开的钱包",
|
||||
"lite_op_create": "创建",
|
||||
"lite_op_open": "打开",
|
||||
"lite_op_restore": "恢复",
|
||||
"lite_overwrite": "覆盖",
|
||||
"lite_passphrase_label": "密码短语",
|
||||
"lite_private_keys_warning": "私钥——任何拥有它们的人都能花费您的资金",
|
||||
"lite_redownload_blocks": "重新下载区块",
|
||||
"lite_redownload_desc": "从轻钱包服务器重新获取所有区块(如果您的余额或历史记录看起来有误,请使用此功能)。",
|
||||
"lite_redownload_running": "正在重新下载区块…",
|
||||
"lite_remove_encryption": "移除加密",
|
||||
"lite_restore_birthday_label": "诞生区块(可选——0 表示从头扫描,速度较慢)",
|
||||
"lite_restore_btn": "恢复钱包",
|
||||
"lite_restore_intro": "输入您的恢复助记词。钱包将被恢复,然后从轻钱包服务器同步。",
|
||||
"lite_restore_ok": "钱包已恢复——正在从轻钱包服务器同步…",
|
||||
"lite_restore_seed_label": "助记词(单词以空格分隔)",
|
||||
"lite_restore_title": "从助记词恢复",
|
||||
"lite_save_to_file": "保存到文件",
|
||||
"lite_saved_to": "已保存(明文,仅所有者可读)至 ",
|
||||
"lite_security": "安全",
|
||||
"lite_seed_label": "助记词",
|
||||
"lite_seed_warning": "助记词——恢复钱包的唯一方式。请抄写下来,离线保存,切勿分享。",
|
||||
"lite_servers_network_tab": "轻钱包服务器在网络标签页中管理。",
|
||||
"lite_show_private_keys": "显示私钥",
|
||||
"lite_show_seed": "显示助记词",
|
||||
"lite_unlock": "解锁",
|
||||
"lite_unlock_btn": "解锁",
|
||||
"lite_unlock_failed": "解锁失败——密码错误?",
|
||||
"lite_unlock_msg": "输入您的密码短语以解锁钱包进行支付。",
|
||||
"lite_unlock_ok": "钱包已解锁——您现在可以发送了",
|
||||
"lite_unlock_title": "解锁钱包",
|
||||
"lite_validate": "验证",
|
||||
"lite_wallet_encrypted": "钱包已加密",
|
||||
"lite_wallet_label": "钱包",
|
||||
"lite_wallet_locked": "钱包已锁定",
|
||||
"lite_wallet_ready": "钱包就绪",
|
||||
"lite_wallet_request": "Lite 钱包请求",
|
||||
"lite_wallet_unlocked": "钱包已解锁",
|
||||
"lite_welcome_create": "创建新钱包",
|
||||
"lite_welcome_create_failed": "无法创建钱包",
|
||||
"lite_welcome_created": "钱包已创建——请立即在“设置 → 备份与密钥”中备份您的恢复助记词",
|
||||
"lite_welcome_later": "稍后",
|
||||
"lite_welcome_msg": "您还没有钱包。创建一个新钱包或从恢复助记词恢复。",
|
||||
"lite_welcome_restore": "从助记词恢复",
|
||||
"lite_welcome_restore_hint": "在设置 → 轻钱包请求中恢复您的钱包",
|
||||
"lite_welcome_title": "欢迎使用 ObsidianDragon Lite",
|
||||
"lite_word_count": "%d / 24 词",
|
||||
"lite_working": "处理中…",
|
||||
"loading": "加载中...",
|
||||
"loading_addresses": "正在加载地址...",
|
||||
"loading_transactions": "正在加载交易",
|
||||
"local_hashrate": "本地算力",
|
||||
"low_spec_mode": "低配模式",
|
||||
"mark_mining_address": "标记为挖矿地址",
|
||||
"market": "市场",
|
||||
"market_12h": "12小时",
|
||||
"market_18h": "18小时",
|
||||
"market_24h": "24小时",
|
||||
"market_24h_change": "24小时",
|
||||
"market_24h_volume": "24小时交易量",
|
||||
"market_6h": "6小时",
|
||||
"market_attribution": "价格数据来自 NonKYC",
|
||||
"market_btc_price": "BTC 价格",
|
||||
"market_cap": "市值",
|
||||
"market_cap_short": "市值",
|
||||
"market_iv_1d": "1天",
|
||||
"market_iv_1h": "1时",
|
||||
"market_iv_1m": "1M",
|
||||
"market_iv_1w": "1周",
|
||||
"market_iv_live": "实时",
|
||||
"market_no_history": "无价格历史",
|
||||
"market_no_price": "无价格数据",
|
||||
"market_now": "现在",
|
||||
@@ -372,6 +548,8 @@
|
||||
"market_price_unavailable": "价格数据不可用",
|
||||
"market_refresh_price": "刷新价格数据",
|
||||
"market_trade_on": "在 %s 交易",
|
||||
"market_updated": "\\xc2\\xb7 已更新 %s",
|
||||
"market_vol_short": "成交量",
|
||||
"mature": "已成熟",
|
||||
"max": "最大",
|
||||
"memo": "备注(可选,加密)",
|
||||
@@ -381,8 +559,52 @@
|
||||
"memo_z_only": "注意:备注仅在发送到屏蔽 (z) 地址时可用",
|
||||
"merge_description": "将多个 UTXO 合并到一个屏蔽地址。这可以帮助减小钱包大小并提高隐私性。",
|
||||
"merge_funds": "合并资金",
|
||||
"merge_send_failed": "合并失败:",
|
||||
"merge_started": "合并操作已开始",
|
||||
"merge_title": "合并到地址",
|
||||
"mig_adopt": "将其设为我的钱包",
|
||||
"mig_adopt_now": "采用助记词钱包",
|
||||
"mig_adopting": "正在安装您的新钱包并重新扫描…",
|
||||
"mig_adopting_note": "节点正在您的新钱包上重启,并将重新扫描区块链——这可能需要几分钟。请让钱包保持运行;它会自动重新连接。",
|
||||
"mig_already_mnemonic": "您的钱包已经有 24 词助记词——无需迁移。只需备份它并将这些词妥善保存在安全的地方即可。",
|
||||
"mig_backed_up": "我已抄写下我的助记词",
|
||||
"mig_backup_instead": "备份助记词",
|
||||
"mig_balance": "当前钱包余额:%.8f DRGX(扣除少量手续费)",
|
||||
"mig_check_failed": "无法检查您的钱包。请确保它已解锁并已连接,然后重新检查。",
|
||||
"mig_checking_balance": "正在检查您的余额",
|
||||
"mig_confirming": "正在等待此次扫入在链上确认,然后再切换钱包——这可确保您的资金确实已经转移。",
|
||||
"mig_confs": "扫入确认数:%d",
|
||||
"mig_continue_sweep": "继续扫入",
|
||||
"mig_copy_seed": "复制助记词",
|
||||
"mig_create": "创建助记词钱包",
|
||||
"mig_daemon_too_old": "您的 DragonX 节点版本太旧,无法创建助记词钱包——它缺少此功能所需的助记词支持。请更新节点(设置 → 节点与安全 → 安装内置版本,或检查更新),然后重试。",
|
||||
"mig_discard": "放弃迁移",
|
||||
"mig_done": "迁移完成。您的钱包现在由您的助记词支持。",
|
||||
"mig_done_btn": "完成",
|
||||
"mig_done_detail": "守护进程正在重新扫描以显示您的资金——这可能需要几分钟。您之前的钱包已作为 .bak 保存在数据文件夹中。",
|
||||
"mig_intro": "这会在一个隔离的节点中创建一个由 24 词助记词支持的全新钱包,以便您将资金转入其中。您当前的钱包不会受到影响,本步骤也不会转移任何资金——您将先备份新助记词,然后再作为单独的、经确认的步骤把资金扫入其中。",
|
||||
"mig_later": "稍后",
|
||||
"mig_no_funds": "您的钱包没有可迁移的资金。您可以直接采用新的助记词钱包——它会用由助记词支持的钱包替换您当前的(空)钱包。以防万一,您的旧钱包仍会移到一个带时间戳的备份中。",
|
||||
"mig_nofunds_confirm": "我明白这会用新的助记词钱包替换我当前的钱包",
|
||||
"mig_not_connected": "请先连接到您的节点,然后重新打开此界面进行迁移。",
|
||||
"mig_precheck_checking": "正在检查您的钱包",
|
||||
"mig_receive_addr": "新钱包接收地址:",
|
||||
"mig_recheck": "重新检查",
|
||||
"mig_remainder": "您的旧钱包中仍有部分资金(一笔交易的输入过多)——请在切换前扫入剩余部分。",
|
||||
"mig_remaining": "旧钱包中剩余:%.8f DRGX",
|
||||
"mig_seed_warning": "请按顺序将这 24 个词抄写下来并离线保存。它们是您新钱包唯一的备份——一旦丢失,您迁移的资金将永久无法恢复。",
|
||||
"mig_step1_done": "2 步中的第 1 步已完成——您的资金尚未转移。将它们扫入这个新钱包是下一步。",
|
||||
"mig_sweep_all": "扫入全部资金",
|
||||
"mig_sweep_intro": "现在将您当前钱包中的所有资金扫入新的助记词钱包。这会向新钱包的地址发送一笔交易;从此以后由您的助记词(已备份)控制这些资金。",
|
||||
"mig_sweep_remaining": "扫入剩余部分",
|
||||
"mig_sweeping": "正在将您的资金扫入新钱包…",
|
||||
"mig_title": "迁移到助记词钱包",
|
||||
"mig_to": "至:",
|
||||
"mig_txid": "txid: ",
|
||||
"mig_unlock_first": "请先解锁您的钱包才能从中花费。",
|
||||
"mig_unlock_to_migrate": "请先解锁您的钱包,然后重新打开此界面进行迁移。",
|
||||
"mig_waiting_mine": "正在等待扫入交易被打包…",
|
||||
"mig_working": "正在隔离的节点中创建您的新助记词钱包——这可能需要一分钟。您的主钱包将继续运行。",
|
||||
"mine_when_idle": "空闲时挖矿",
|
||||
"mined": "已挖得",
|
||||
"mined_filter": "已挖得",
|
||||
@@ -394,6 +616,8 @@
|
||||
"mining_address_copied": "挖矿地址已复制",
|
||||
"mining_all_time": "所有时间",
|
||||
"mining_already_saved": "矿池 URL 已保存",
|
||||
"mining_auto_balance_desc": "按算力将矿工分布到各官方矿池,以帮助去中心化网络。定期检查每个矿池。",
|
||||
"mining_auto_balanced_to": "已自动平衡挖矿至",
|
||||
"mining_benchmark_cancel": "取消基准测试",
|
||||
"mining_benchmark_cooling": "冷却中",
|
||||
"mining_benchmark_dismiss": "关闭",
|
||||
@@ -430,8 +654,10 @@
|
||||
"mining_idle_threads_active_tooltip": "用户活跃时的线程数",
|
||||
"mining_idle_threads_idle_tooltip": "系统空闲时的线程数",
|
||||
"mining_local_hashrate": "本地算力",
|
||||
"mining_manual_desc": "手动:挖矿到您从下方列表选择的矿池。",
|
||||
"mining_mine": "挖矿",
|
||||
"mining_mining_addr": "挖矿地址",
|
||||
"mining_mining_here": "在此挖矿",
|
||||
"mining_network": "网络",
|
||||
"mining_no_blocks_yet": "尚未找到区块",
|
||||
"mining_no_payouts_yet": "尚无矿池支付",
|
||||
@@ -441,18 +667,24 @@
|
||||
"mining_on": "挖矿已开启",
|
||||
"mining_open_in_explorer": "在浏览器中打开",
|
||||
"mining_payout_address": "支付地址",
|
||||
"mining_payout_foreign": "⚠ 此支付地址不在您当前的钱包中——挖矿奖励将进入另一个钱包。如果您切换过钱包,请更新它。",
|
||||
"mining_payout_tooltip": "接收挖矿奖励的地址",
|
||||
"mining_pool": "矿池",
|
||||
"mining_pool_fee": "费用",
|
||||
"mining_pool_hashrate": "矿池算力",
|
||||
"mining_pool_url": "矿池 URL",
|
||||
"mining_pools_header": "矿池",
|
||||
"mining_recent_blocks": "最近区块",
|
||||
"mining_recent_payouts": "最近矿池支付",
|
||||
"mining_refresh": "刷新",
|
||||
"mining_remove": "移除",
|
||||
"mining_reset_defaults": "重置默认值",
|
||||
"mining_save_payout_address": "保存支付地址",
|
||||
"mining_save_pool_url": "保存矿池 URL",
|
||||
"mining_saved_addresses": "已保存地址:",
|
||||
"mining_saved_pools": "已保存矿池:",
|
||||
"mining_select_auto": "自动平衡",
|
||||
"mining_select_manual": "手动",
|
||||
"mining_shares": "份额",
|
||||
"mining_show_chart": "图表",
|
||||
"mining_show_log": "日志",
|
||||
@@ -465,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "请停止单人挖矿以更改矿池设置",
|
||||
"mining_stopping": "停止中...",
|
||||
"mining_stopping_tooltip": "矿工正在停止...",
|
||||
"mining_suggested_pools": "推荐矿池",
|
||||
"mining_syncing_tooltip": "区块链同步中...",
|
||||
"mining_tag": " · 挖矿",
|
||||
"mining_threads": "挖矿线程",
|
||||
"mining_to_save": "保存",
|
||||
"mining_today": "今天",
|
||||
@@ -486,10 +720,12 @@
|
||||
"no_addresses_match": "没有匹配过滤器的地址",
|
||||
"no_addresses_with_balance": "没有有余额的地址",
|
||||
"no_addresses_yet": "暂无地址",
|
||||
"no_icons_found": "没有与您的搜索匹配的图标。",
|
||||
"no_matching": "没有匹配的交易",
|
||||
"no_recent_receives": "没有最近的接收",
|
||||
"no_recent_sends": "没有最近的发送",
|
||||
"no_transactions": "未找到交易",
|
||||
"no_transactions_yet": "尚无交易",
|
||||
"node": "节点",
|
||||
"node_security": "节点与安全",
|
||||
"noise": "噪点",
|
||||
@@ -553,7 +789,66 @@
|
||||
"pending": "待处理",
|
||||
"pin_not_set": "未设置 PIN。使用密码解锁。",
|
||||
"ping": "延迟",
|
||||
"portfolio_add_entry": "添加条目",
|
||||
"portfolio_add_to": "添加到投资组合",
|
||||
"portfolio_addresses_hdr": "地址",
|
||||
"portfolio_addresses_sel": "已选 %d 个",
|
||||
"portfolio_all_funds": "全部资金",
|
||||
"portfolio_all_shielded": "全部屏蔽",
|
||||
"portfolio_all_transparent": "全部透明",
|
||||
"portfolio_appearance": "外观",
|
||||
"portfolio_cancel": "取消",
|
||||
"portfolio_clear_sel": "清除",
|
||||
"portfolio_close": "关闭",
|
||||
"portfolio_color": "颜色",
|
||||
"portfolio_currency": "货币",
|
||||
"portfolio_custom_color": "自定义颜色…",
|
||||
"portfolio_delete": "删除",
|
||||
"portfolio_detail_empty": "在左侧选择一个分组,或添加一个分组进行编辑。",
|
||||
"portfolio_edit": "编辑",
|
||||
"portfolio_funded": "已注资",
|
||||
"portfolio_group_name": "分组名称",
|
||||
"portfolio_icon": "图标",
|
||||
"portfolio_label": "标签",
|
||||
"portfolio_manage": "管理…",
|
||||
"portfolio_manage_title": "管理投资组合",
|
||||
"portfolio_manual_price": "价格 / DRGX",
|
||||
"portfolio_new_entry": "新条目",
|
||||
"portfolio_no_addr_match": "没有匹配的地址",
|
||||
"portfolio_no_entries": "尚无自定义条目。添加一个以跟踪一组地址。",
|
||||
"portfolio_no_icon": "无",
|
||||
"portfolio_outline_opacity": "描边不透明度",
|
||||
"portfolio_price": "价格",
|
||||
"portfolio_price_btc": "市场 · BTC",
|
||||
"portfolio_price_drgx": "仅 DRGX",
|
||||
"portfolio_price_manual": "手动",
|
||||
"portfolio_price_usd": "市场 · 美元",
|
||||
"portfolio_remove_from": "从投资组合中移除",
|
||||
"portfolio_revert": "还原",
|
||||
"portfolio_save": "保存",
|
||||
"portfolio_search": "搜索地址…",
|
||||
"portfolio_search_icons": "搜索图标…",
|
||||
"portfolio_select_all": "全部",
|
||||
"portfolio_select_shown": "全选",
|
||||
"portfolio_show": "显示:",
|
||||
"portfolio_show_24h": "24小时",
|
||||
"portfolio_show_sparkline": "迷你走势图",
|
||||
"portfolio_show_value": "价值",
|
||||
"portfolio_spark_day": "日",
|
||||
"portfolio_spark_hour": "小时",
|
||||
"portfolio_spark_min": "分钟",
|
||||
"portfolio_spark_month": "月",
|
||||
"portfolio_spark_week": "周",
|
||||
"portfolio_style_compact": "紧凑行",
|
||||
"portfolio_style_detailed": "详细行",
|
||||
"portfolio_style_featured": "特色行",
|
||||
"portfolio_style_label": "投资组合样式",
|
||||
"portfolio_untitled": "未命名",
|
||||
"price_chart": "价格图表",
|
||||
"privacy_great": "隐私性极佳!",
|
||||
"privacy_low": "隐私性低——请屏蔽资金",
|
||||
"privacy_medium": "考虑屏蔽更多资金",
|
||||
"processing_transaction": "正在处理交易...",
|
||||
"qr_code": "二维码",
|
||||
"qr_failed": "无法生成二维码",
|
||||
"qr_title": "二维码",
|
||||
@@ -571,6 +866,7 @@
|
||||
"receiving_addresses": "您的接收地址",
|
||||
"recent_received": "最近接收",
|
||||
"recent_sends": "最近发送",
|
||||
"recent_transactions": "最近交易",
|
||||
"recipient": "收款方",
|
||||
"recipient_balance": "接收方: %.8f → %.8f DRGX",
|
||||
"recv_type": "接收",
|
||||
@@ -578,6 +874,7 @@
|
||||
"refresh": "刷新",
|
||||
"refresh_now": "立即刷新",
|
||||
"remove_favorite": "移除收藏",
|
||||
"repair_wallet": "修复钱包",
|
||||
"report_bug": "报告错误",
|
||||
"request_amount": "金额(可选):",
|
||||
"request_copy_uri": "复制 URI",
|
||||
@@ -593,12 +890,16 @@
|
||||
"request_transparent_addrs": "-- 透明地址 --",
|
||||
"request_uri_copied": "付款 URI 已复制到剪贴板",
|
||||
"rescan": "重新扫描",
|
||||
"rescan_bootstrapped_msg": "您的节点使用了引导程序,因此快照以下的区块不在磁盘上,从创世区块重新扫描将失败。请从快照所包含的高度开始重新扫描,以核对钱包的已花费余额。您的 wallet.dat 和区块链数据不会被删除。",
|
||||
"rescan_detecting": "正在检查您的节点在磁盘上已有哪些区块…",
|
||||
"rescan_from_height": "从区块高度重新扫描:",
|
||||
"reset_to_defaults": "重置为默认值",
|
||||
"restarting_after_encryption": "加密后重启守护进程...",
|
||||
"restore_address": "恢复地址",
|
||||
"result_preview": "结果预览",
|
||||
"retry": "重试",
|
||||
"review_send": "审核发送",
|
||||
"rpc_connection": "RPC 连接...",
|
||||
"rpc_host": "RPC 主机",
|
||||
"rpc_pass": "密码",
|
||||
"rpc_port": "端口",
|
||||
@@ -608,12 +909,15 @@
|
||||
"save_z_transactions": "将 Z 交易保存到列表",
|
||||
"sb_auth_failed": "认证失败 — 请检查 rpcuser/rpcpassword",
|
||||
"sb_block": "区块: %d",
|
||||
"sb_building_witnesses": "正在设置见证",
|
||||
"sb_building_witnesses_pct": "正在重建见证 %.0f%%",
|
||||
"sb_connecting_daemon": "正在连接 dragonxd...",
|
||||
"sb_connecting_err": "连接守护进程 — %s",
|
||||
"sb_connecting_external": "正在连接外部守护进程...",
|
||||
"sb_connecting_generic": "正在连接守护进程...",
|
||||
"sb_daemon_crashed": "守护进程崩溃 %d 次",
|
||||
"sb_daemon_not_found": "未找到守护进程",
|
||||
"sb_daemon_start_failed": "无法启动 dragonxd",
|
||||
"sb_dragonxd_running": "dragonxd 运行中",
|
||||
"sb_dragonxd_stopped": "dragonxd 已停止",
|
||||
"sb_dragonxd_stopping": "正在停止 dragonxd...",
|
||||
@@ -639,6 +943,12 @@
|
||||
"sb_waiting_daemon": "等待 dragonxd 启动...",
|
||||
"sb_waiting_daemon_err": "等待 dragonxd — %s",
|
||||
"sb_warming_up": "正在预热...",
|
||||
"sb_witness_cache": "正在重建见证",
|
||||
"screenshot_open_dir": "打开位置",
|
||||
"screenshot_sweep": "运行截图批处理",
|
||||
"screenshot_sweep_desc": "遍历每个标签页的每一种主题,并将每一个的截图保存到配置目录 screenshots 文件夹下的各标签页子文件夹中(覆盖上一次的遍历)。运行几秒钟。",
|
||||
"screenshot_sweep_full": "完整 UI 遍历",
|
||||
"search_icons": "搜索图标...",
|
||||
"search_placeholder": "搜索...",
|
||||
"security": "安全",
|
||||
"seed_backup_button": "助记词",
|
||||
@@ -655,6 +965,7 @@
|
||||
"seed_backup_saved": "已保存到 ",
|
||||
"seed_backup_title": "备份助记词",
|
||||
"seed_backup_warning": "请按顺序将它们抄写下来,离线保存,切勿分享或拍照。一旦丢失,您的资金将无法恢复。",
|
||||
"seed_migrate_button": "迁移到助记词钱包…",
|
||||
"select_address": "选择地址...",
|
||||
"select_receiving_address": "选择接收地址...",
|
||||
"select_source_address": "选择来源地址...",
|
||||
@@ -663,8 +974,10 @@
|
||||
"send_amount_details": "金额详情",
|
||||
"send_amount_upper": "金额",
|
||||
"send_clear_fields": "清除所有表单字段?",
|
||||
"send_contacts_button": "从联系人中选择",
|
||||
"send_copy_error": "复制错误",
|
||||
"send_dismiss": "关闭",
|
||||
"send_err_needs_rescan": "您钱包的屏蔽备注数据与区块链不同步(这通常发生在引导或重建索引之后)。请通过 设置 → 重新扫描区块链 运行一次完整的重新扫描并让其彻底完成,然后再次尝试发送。",
|
||||
"send_error_copied": "错误已复制到剪贴板",
|
||||
"send_error_prefix": "错误:%s",
|
||||
"send_exceeds_available": "超过可用额 (%.8f)",
|
||||
@@ -683,6 +996,7 @@
|
||||
"send_recipient": "收款方",
|
||||
"send_select_source": "选择来源地址...",
|
||||
"send_sending_from": "发送来源",
|
||||
"send_status_unconfirmed": "无法确认交易状态",
|
||||
"send_submitting": "正在提交交易...",
|
||||
"send_switch_to_receive": "切换到接收页面获取您的地址并开始接收资金。",
|
||||
"send_to": "发送至",
|
||||
@@ -755,6 +1069,8 @@
|
||||
"settings_noise_opacity": "噪点不透明度:",
|
||||
"settings_not_encrypted": "未加密",
|
||||
"settings_not_found": "未找到",
|
||||
"settings_open_app_dir": "打开应用文件夹",
|
||||
"settings_open_data_dir": "打开数据文件夹",
|
||||
"settings_other": "其他",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "隐私",
|
||||
@@ -788,16 +1104,24 @@
|
||||
"shield_check_status": "检查状态",
|
||||
"shield_completed": "操作成功完成!",
|
||||
"shield_description": "通过将透明地址的 coinbase 输出发送到屏蔽地址来屏蔽您的挖矿奖励。这可以隐藏您的挖矿收入,提高隐私性。",
|
||||
"shield_error_prefix": "错误:",
|
||||
"shield_from_address": "从地址:",
|
||||
"shield_funds": "屏蔽资金",
|
||||
"shield_in_progress": "操作进行中...",
|
||||
"shield_max_utxos": "每次操作最大 UTXO 数",
|
||||
"shield_merge_done": "屏蔽/合并完成!",
|
||||
"shield_op_failed": "操作失败:",
|
||||
"shield_op_submitted": "操作已提交:",
|
||||
"shield_operation_id": "操作 ID:%s",
|
||||
"shield_select_z": "选择 z 地址...",
|
||||
"shield_send_failed": "屏蔽失败:",
|
||||
"shield_started": "屏蔽操作已开始",
|
||||
"shield_status_check_error": "检查状态出错:",
|
||||
"shield_status_label": "状态:",
|
||||
"shield_submitting": "正在提交操作...",
|
||||
"shield_title": "屏蔽 Coinbase 奖励",
|
||||
"shield_to_address": "至地址(屏蔽):",
|
||||
"shield_unknown_error": "未知错误",
|
||||
"shield_utxo_limit": "UTXO 限制:",
|
||||
"shield_wildcard_hint": "使用 '*' 从所有透明地址屏蔽",
|
||||
"shielded": "屏蔽",
|
||||
@@ -807,11 +1131,16 @@
|
||||
"shielding_notice": "注意:这将把资金从透明 (T) 地址转移到隐私 (Z) 地址。",
|
||||
"show": "显示",
|
||||
"show_hidden": "显示已隐藏 (%d)",
|
||||
"show_qr": "显示二维码",
|
||||
"show_qr_code": "显示二维码",
|
||||
"showing_transactions": "显示第 %d–%d 笔,共 %d 笔交易(总计:%zu)",
|
||||
"showing_x_of_y": "显示 %d / %d 个地址",
|
||||
"simple_background": "简单背景",
|
||||
"slider_off": "关闭",
|
||||
"sort_amount_high": "金额最大",
|
||||
"sort_amount_low": "金额最小",
|
||||
"sort_date_newest": "最新优先",
|
||||
"sort_date_oldest": "最早优先",
|
||||
"start_mining": "开始挖矿",
|
||||
"status": "状态",
|
||||
"stop_external": "停止外部守护进程",
|
||||
@@ -866,6 +1195,8 @@
|
||||
"tt_clear_ztx": "删除本地缓存的 z-交易历史",
|
||||
"tt_custom_fees": "发送交易时启用手动费用输入",
|
||||
"tt_custom_theme": "自定义主题已激活",
|
||||
"tt_daemon_install_bundled": "停止节点,用此钱包版本内置的 dragonxd 覆盖已安装的版本,然后重启",
|
||||
"tt_daemon_update_check": "从项目 Gitea 下载并验证最新的 dragonxd 全节点,然后重启以应用",
|
||||
"tt_debug_collapse": "折叠调试日志选项",
|
||||
"tt_debug_expand": "展开调试日志选项",
|
||||
"tt_delete_blockchain": "删除所有区块链数据并重新同步。wallet.dat 和配置将被保留。",
|
||||
@@ -881,15 +1212,19 @@
|
||||
"tt_keep_daemon": "运行设置向导时守护进程仍会停止",
|
||||
"tt_language": "钱包界面语言",
|
||||
"tt_layout_hotkey": "快捷键:左/右箭头键切换余额布局",
|
||||
"tt_lite_redownload": "从轻钱包服务器重新下载并重新扫描所有区块",
|
||||
"tt_lock": "立即锁定钱包",
|
||||
"tt_low_spec": "禁用所有重度视觉效果\\n快捷键:Ctrl+Shift+Down",
|
||||
"tt_merge": "将多个 UTXO 合并到一个地址",
|
||||
"tt_mine_idle": "系统空闲时自动开始挖矿\\n(无键盘/鼠标输入)",
|
||||
"tt_noise": "颗粒纹理强度(0%% = 关闭,100%% = 最大)",
|
||||
"tt_open_app_dir": "在文件管理器中打开 ObsidianDragon 文件夹(设置、主题、日志)",
|
||||
"tt_open_data_dir": "在文件管理器中打开包含您钱包和区块链数据的文件夹",
|
||||
"tt_open_dir": "点击在文件管理器中打开",
|
||||
"tt_reduce_motion": "禁用动画过渡和余额渐变以提高无障碍性",
|
||||
"tt_remove_encrypt": "移除加密并以未受保护状态存储钱包",
|
||||
"tt_remove_pin": "移除 PIN 并要求密码解锁",
|
||||
"tt_repair_wallet": "从区块链清除并重建钱包的交易记录(修复重扫描后无法发送的 note)",
|
||||
"tt_report_bug": "在项目跟踪器中报告问题",
|
||||
"tt_request_payment": "生成带二维码的付款请求",
|
||||
"tt_rescan": "重新扫描区块链以查找丢失的交易",
|
||||
@@ -904,6 +1239,7 @@
|
||||
"tt_scan_themes": "扫描新主题。\\n将主题文件夹放在:\\n%s",
|
||||
"tt_scanline": "控制台中的 CRT 扫描线效果",
|
||||
"tt_seed_backup": "显示并备份您钱包的 24 词恢复助记词",
|
||||
"tt_seed_migrate": "创建一个新的助记词钱包并将您的资金转入其中",
|
||||
"tt_set_pin": "设置 4-8 位 PIN 以快速解锁",
|
||||
"tt_shield_mining": "将透明挖矿奖励转移到屏蔽地址",
|
||||
"tt_simple_bg": "使用简单渐变作为背景\\n快捷键:Ctrl+Up",
|
||||
@@ -917,6 +1253,7 @@
|
||||
"tt_ui_opacity": "卡片和侧边栏不透明度(100%% = 完全不透明,越低越透明)",
|
||||
"tt_validate": "检查 DragonX 地址是否有效",
|
||||
"tt_verbose": "将详细连接诊断、守护进程状态\\n和端口所有者信息记录到控制台选项卡",
|
||||
"tt_wallets_button": "列出您的钱包文件并在它们之间切换",
|
||||
"tt_website": "打开 DragonX 网站",
|
||||
"tt_window_opacity": "背景不透明度(越低 = 桌面透过窗口可见)",
|
||||
"tt_wizard": "重新运行初始设置向导\\n守护进程将被重启",
|
||||
@@ -925,7 +1262,18 @@
|
||||
"tx_from_address": "发送地址:",
|
||||
"tx_id_label": "交易 ID:",
|
||||
"tx_immature": "未成熟",
|
||||
"tx_loading_enriching_sends": "正在检查已发送交易详情(%d)",
|
||||
"tx_loading_fetching_transparent": "正在获取透明交易历史",
|
||||
"tx_loading_history_progress": "正在加载较早的历史记录(%d%%)",
|
||||
"tx_loading_queued": "已排队的交易刷新",
|
||||
"tx_loading_refreshing_cached": "正在刷新钱包历史(已缓存 %d 条)",
|
||||
"tx_loading_scanning_shielded": "正在扫描屏蔽历史(%d 个地址)",
|
||||
"tx_mined": "已挖得",
|
||||
"tx_progress_balances": "正在刷新余额",
|
||||
"tx_progress_finalizing": "正在完成交易",
|
||||
"tx_progress_history": "正在刷新历史记录",
|
||||
"tx_progress_submitting": "正在向守护进程提交交易",
|
||||
"tx_progress_waiting_ops": "正在等待操作(%d)",
|
||||
"tx_received": "已接收",
|
||||
"tx_sent": "已发送",
|
||||
"tx_to_address": "接收地址:",
|
||||
@@ -937,6 +1285,14 @@
|
||||
"unconfirmed": "未确认",
|
||||
"undo_clear": "撤销清除",
|
||||
"unknown": "未知",
|
||||
"unmark_mining_address": "取消标记挖矿地址",
|
||||
"upd_back": "返回",
|
||||
"upd_install": "安装",
|
||||
"upd_installed_badge": "已安装",
|
||||
"upd_no_build_platform": "没有适用于此平台的版本",
|
||||
"upd_prerelease": "预发布",
|
||||
"upd_reinstall": "重新安装",
|
||||
"upd_select_version": "选择要安装的版本",
|
||||
"use_embedded_daemon": "使用内置 dragonxd",
|
||||
"use_tor": "使用 Tor",
|
||||
"validate_btn": "验证",
|
||||
@@ -956,17 +1312,73 @@
|
||||
"verbose_logging": "详细日志",
|
||||
"version": "版本",
|
||||
"view": "查看",
|
||||
"view_all": "查看全部",
|
||||
"view_details": "查看详情",
|
||||
"view_on_explorer": "在浏览器中查看",
|
||||
"waiting_for_daemon": "等待守护进程连接...",
|
||||
"wallet": "钱包",
|
||||
"wallet_empty": "您的钱包是空的",
|
||||
"wallet_empty_hint": "切换到接收页面获取您的地址并开始接收资金。",
|
||||
"wallets_active": "活跃",
|
||||
"wallets_add": "添加文件夹",
|
||||
"wallets_add_folder": "同时扫描另一个文件夹中的钱包文件:",
|
||||
"wallets_add_folder_toggle": "+ 扫描其他文件夹中的钱包…",
|
||||
"wallets_button": "钱包…",
|
||||
"wallets_col_addresses": "地址",
|
||||
"wallets_col_balance": "余额",
|
||||
"wallets_col_name": "钱包",
|
||||
"wallets_col_opened": "上次打开",
|
||||
"wallets_col_size": "大小",
|
||||
"wallets_create": "创建钱包",
|
||||
"wallets_creating": "正在创建钱包——节点将重启…",
|
||||
"wallets_current": "当前",
|
||||
"wallets_exists": "已存在同名钱包。",
|
||||
"wallets_external_tt": "位于数据目录之外 — 导入以将其复制进来。",
|
||||
"wallets_folder_hint": "/含 .dat 钱包文件的文件夹路径",
|
||||
"wallets_folder_invalid": "该文件夹不存在。",
|
||||
"wallets_import": "导入",
|
||||
"wallets_import_failed": "无法导入该钱包文件。",
|
||||
"wallets_import_hint": "导入以打开",
|
||||
"wallets_import_tt": "此钱包位于数据目录之外。打开它会先将其复制进来(即将推出)。",
|
||||
"wallets_imported": "钱包已导入——正在切换…",
|
||||
"wallets_intro": "位于您数据目录(以及您添加的任何文件夹)中的钱包文件。打开一个即可切换——节点将重启以加载它,且每个钱包各自保留其数据。",
|
||||
"wallets_name_invalid": "请输入有效的钱包名称。",
|
||||
"wallets_never": "从未",
|
||||
"wallets_new_hint": "名称(例如 savings)",
|
||||
"wallets_new_label": "创建新钱包:",
|
||||
"wallets_open": "打开",
|
||||
"wallets_reveal": "打开文件夹",
|
||||
"wallets_title": "钱包",
|
||||
"warning": "警告",
|
||||
"warning_upper": "警告!",
|
||||
"website": "网站",
|
||||
"window_opacity": "窗口透明度",
|
||||
"wizard_daemon_start_failed": "启动守护进程失败 — 将自动重试",
|
||||
"xmrig_browse_releases": "浏览所有版本…",
|
||||
"xmrig_checking": "正在检查最新的矿工程序…",
|
||||
"xmrig_current": "当前:",
|
||||
"xmrig_download_install": "下载并安装",
|
||||
"xmrig_downloading": "正在下载…",
|
||||
"xmrig_installed": "已安装:",
|
||||
"xmrig_installed_ok": "矿工程序已安装",
|
||||
"xmrig_installing": "正在安装…",
|
||||
"xmrig_latest": "最新:",
|
||||
"xmrig_loading_releases": "正在加载发行版…",
|
||||
"xmrig_none": "无",
|
||||
"xmrig_reinstall": "重新安装",
|
||||
"xmrig_stop_mining_first": "更新矿工程序前请先停止挖矿。",
|
||||
"xmrig_unavailable_body": "此平台没有可用的矿工构建版本。",
|
||||
"xmrig_unavailable_title": "矿工更新不可用",
|
||||
"xmrig_unknown_error": "未知错误。",
|
||||
"xmrig_up_to_date": "矿工程序已是最新版本",
|
||||
"xmrig_update_available": "有新的矿工程序可用",
|
||||
"xmrig_update_button": "更新矿工…",
|
||||
"xmrig_update_failed": "更新失败",
|
||||
"xmrig_update_short": "更新",
|
||||
"xmrig_update_title": "更新矿工程序",
|
||||
"xmrig_verify_note": "下载内容会在安装前与发布版本公布的 SHA-256 校验和进行核对。",
|
||||
"xmrig_verifying": "正在验证…",
|
||||
"xmrig_version": "版本:",
|
||||
"yes_clear": "是,清除",
|
||||
"your_addresses": "您的地址",
|
||||
"z_address": "Z 地址",
|
||||
|
||||
336
src/app.cpp
336
src/app.cpp
@@ -54,6 +54,7 @@
|
||||
#include "ui/windows/bootstrap_download_dialog.h"
|
||||
#include "ui/windows/xmrig_download_dialog.h"
|
||||
#include "ui/windows/daemon_download_dialog.h"
|
||||
#include "ui/windows/wallets_dialog.h"
|
||||
#include "ui/windows/console_tab.h"
|
||||
#include "ui/pages/settings_page.h"
|
||||
#include "ui/theme.h"
|
||||
@@ -467,6 +468,15 @@ bool App::init()
|
||||
// Idempotent; a missing file is fine (returns true). Purely local — no daemon/RPC dependency.
|
||||
address_book_.load();
|
||||
|
||||
// Load the wallet-files metadata index (wallets.json); populated after each wallet load. Local
|
||||
// only, missing file is fine.
|
||||
wallet_index_.load();
|
||||
|
||||
// If this wallet build bundles a newer daemon than the one already installed, offer to update it
|
||||
// (once per version). Fixes the trap where a wallet update carrying a newer node silently kept
|
||||
// the old binary — e.g. an old daemon lacking z_exportmnemonic blocking migrate-to-seed.
|
||||
maybeOfferDaemonUpdate();
|
||||
|
||||
DEBUG_LOGF("Initialization complete\n");
|
||||
return true;
|
||||
}
|
||||
@@ -627,7 +637,13 @@ void App::update()
|
||||
{
|
||||
PERF_SCOPE("Update.Total");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
|
||||
// Full UI screenshot sweep: demo state is injected once and must stay frozen. Skip every live
|
||||
// op (refresh/connect/pumps) so a real daemon can't clobber it — on Windows a running node's
|
||||
// refresh set connected=false (CDB error) and blanked the Send/Receive/History tabs mid-sweep.
|
||||
// capture_mode_ is only set for the offline full sweep, so the live tab-only sweep is unaffected.
|
||||
if (capture_mode_) return;
|
||||
|
||||
// Track user interaction for auto-lock
|
||||
if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
|
||||
io.MouseClicked[0] || io.MouseClicked[1] ||
|
||||
@@ -1272,6 +1288,11 @@ void App::handleGlobalShortcuts()
|
||||
|
||||
void App::render()
|
||||
{
|
||||
// Advance the screenshot sweep FIRST — before the first-run-wizard early-return below — so the
|
||||
// sweep keeps progressing (and re-forces its target's state) even while a wizard step is shown.
|
||||
// (Pins current_page_ too, before the sidebar reads it further down.)
|
||||
updateScreenshotSweep();
|
||||
|
||||
// First-run wizard gate — blocks all normal UI
|
||||
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
|
||||
renderFirstRunWizard();
|
||||
@@ -1303,9 +1324,6 @@ void App::render()
|
||||
}
|
||||
}
|
||||
|
||||
// Debug screenshot sweep — pins the current (skin,page) and arms capture once settled. Must run
|
||||
// before the sidebar reads current_page_ (below) so the forced page is reflected.
|
||||
updateScreenshotSweep();
|
||||
|
||||
// While any full-window BLUR overlay is open: (1) suppress panel theme-effects for the whole
|
||||
// frame so their foreground-draw-list borders (sidebar included) don't bleed over the overlay;
|
||||
@@ -1923,6 +1941,10 @@ void App::render()
|
||||
renderSeedMigrationDialog();
|
||||
}
|
||||
|
||||
if (show_daemon_update_prompt_ && !capture_mode_) {
|
||||
renderDaemonUpdatePrompt();
|
||||
}
|
||||
|
||||
// Security overlay dialogs (encrypt, decrypt, PIN) are rendered AFTER ImGui::End()
|
||||
// to ensure they appear on top of all other content
|
||||
|
||||
@@ -1969,6 +1991,7 @@ void App::render()
|
||||
ui::BootstrapDownloadDialog::render();
|
||||
ui::XmrigDownloadDialog::render();
|
||||
ui::DaemonUpdateDialog::render();
|
||||
ui::WalletsDialog::render();
|
||||
|
||||
// Windows Defender antivirus help dialog
|
||||
renderAntivirusHelpDialog();
|
||||
@@ -2720,8 +2743,6 @@ void App::renderImportKeyDialog()
|
||||
return;
|
||||
}
|
||||
|
||||
ui::material::Type().text(ui::material::TypeStyle::H6, "Import Private Key");
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
ImGui::TextWrapped("Enter a private key to import. The wallet will rescan the blockchain for transactions.");
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -2824,9 +2845,7 @@ void App::renderExportKeyDialog()
|
||||
return;
|
||||
}
|
||||
|
||||
ui::material::Type().text(ui::material::TypeStyle::H6, "Export Private Key");
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f),
|
||||
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f),
|
||||
"WARNING: Anyone with this key can spend your coins!");
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -2899,8 +2918,6 @@ void App::renderBackupDialog()
|
||||
return;
|
||||
}
|
||||
|
||||
ui::material::Type().text(ui::material::TypeStyle::H6, "Backup Wallet");
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
ImGui::TextWrapped("Export all private keys to a file. Keep this file secure!");
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -2955,6 +2972,50 @@ void App::renderBackupDialog()
|
||||
ui::material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
void App::maybeOfferDaemonUpdate()
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) return; // lite / no embedded-daemon build
|
||||
if (capture_mode_ || !settings_) return; // never during a UI sweep
|
||||
auto inst = resources::getInstalledDaemonInfo();
|
||||
auto bun = resources::getBundledDaemonInfo();
|
||||
// Only when a daemon is ALREADY installed and its size differs from the bundled one — an absent
|
||||
// daemon is extracted normally, and a same-size match needs nothing. Fire once per bundled
|
||||
// version (never re-nag a user running a custom node): key on the bundled size last offered.
|
||||
if (!inst.exists || !bun.available || inst.size == bun.size) return;
|
||||
if ((unsigned long long)settings_->getDaemonUpdatePromptedSize() == bun.size) return;
|
||||
daemon_update_bundled_size_ = bun.size;
|
||||
show_daemon_update_prompt_ = true;
|
||||
}
|
||||
|
||||
void App::renderDaemonUpdatePrompt()
|
||||
{
|
||||
if (!ui::material::BeginOverlayDialog(TR("daemon_update_title"), &show_daemon_update_prompt_, 520.0f, 0.94f))
|
||||
return;
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
ImGui::TextWrapped("%s", TR("daemon_update_body"));
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
||||
ImGui::TextWrapped("%s", TR("daemon_update_safe"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
if (ui::material::StyledButton(TR("daemon_update_now"), ImVec2(170 * dp, 0))) {
|
||||
show_daemon_update_prompt_ = false;
|
||||
reinstallBundledDaemon(); // stops the node (managed or external), overwrites, restarts
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton(TR("daemon_update_keep"), ImVec2(150 * dp, 0))) {
|
||||
show_daemon_update_prompt_ = false;
|
||||
}
|
||||
ui::material::EndOverlayDialog();
|
||||
// Any close path (Update / Keep / Esc / X): record the bundled size so we don't prompt again for
|
||||
// this wallet version — a user keeping a custom daemon is asked at most once per version.
|
||||
if (!show_daemon_update_prompt_ && settings_) {
|
||||
settings_->setDaemonUpdatePromptedSize((long long)daemon_update_bundled_size_);
|
||||
settings_->save();
|
||||
}
|
||||
}
|
||||
|
||||
void App::renderSeedBackupDialog()
|
||||
{
|
||||
// Wipe the revealed phrase and reset the dialog's state.
|
||||
@@ -2969,8 +3030,9 @@ void App::renderSeedBackupDialog()
|
||||
show_seed_backup_ = false;
|
||||
};
|
||||
|
||||
// Kick off the async z_exportmnemonic fetch once, and only while unlocked.
|
||||
if (!seed_backup_fetch_started_ && !state_.isLocked()) {
|
||||
// Kick off the async z_exportmnemonic fetch once, and only while unlocked. Never during a UI
|
||||
// sweep — the sweep pre-sets fetch_started_ + a demo phrase, but guard the RPC too.
|
||||
if (!seed_backup_fetch_started_ && !state_.isLocked() && !capture_mode_) {
|
||||
seed_backup_fetch_started_ = true;
|
||||
seed_backup_loading_ = true;
|
||||
seed_backup_no_mnemonic_ = false;
|
||||
@@ -3001,8 +3063,9 @@ void App::renderSeedBackupDialog()
|
||||
return;
|
||||
}
|
||||
|
||||
ui::material::Type().text(ui::material::TypeStyle::H6, TR("seed_backup_title"));
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
// Button widths are logical px; BeginOverlayDialog scales the card by dpiScale() so scale these
|
||||
// to match (raw widths render small/left-packed at higher DPI / font scale).
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
|
||||
if (state_.isLocked()) {
|
||||
ImGui::TextWrapped("%s", TR("seed_backup_locked"));
|
||||
@@ -3024,11 +3087,11 @@ void App::renderSeedBackupDialog()
|
||||
ImGui::Spacing();
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ui::material::StyledButton(TR("seed_backup_copy"), ImVec2(120, 0))) {
|
||||
if (ui::material::StyledButton(TR("seed_backup_copy"), ImVec2(120 * dp, 0))) {
|
||||
copySecretToClipboard(seed_backup_phrase_);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton(TR("seed_backup_save"), ImVec2(150, 0))) {
|
||||
if (ui::material::StyledButton(TR("seed_backup_save"), ImVec2(150 * dp, 0))) {
|
||||
std::string path = util::Platform::getConfigDir() + "/dragonx-seed-backup.txt";
|
||||
std::string content = seed_backup_phrase_ + "\n";
|
||||
const bool ok = util::Platform::writeFileAtomically(path, content,
|
||||
@@ -3038,7 +3101,7 @@ void App::renderSeedBackupDialog()
|
||||
: std::string(TR("seed_backup_save_failed"))) + path;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120, 0))) {
|
||||
if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120 * dp, 0))) {
|
||||
closeAndWipe();
|
||||
ui::material::EndOverlayDialog();
|
||||
return;
|
||||
@@ -3051,7 +3114,7 @@ void App::renderSeedBackupDialog()
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120, 0))) {
|
||||
if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120 * dp, 0))) {
|
||||
closeAndWipe();
|
||||
}
|
||||
ui::material::EndOverlayDialog();
|
||||
@@ -3072,7 +3135,7 @@ void App::renderSeedMigrationDialog()
|
||||
|| seed_migration_step_ == SeedMigrationStep::Sweeping
|
||||
|| seed_migration_step_ == SeedMigrationStep::Adopting;
|
||||
|
||||
if (!ui::material::BeginOverlayDialog("Migrate to a seed wallet", &show_seed_migration_, 580.0f, 0.94f)) {
|
||||
if (!ui::material::BeginOverlayDialog(TR("mig_title"), &show_seed_migration_, 580.0f, 0.94f)) {
|
||||
if (!busy) wipeSeed();
|
||||
return;
|
||||
}
|
||||
@@ -3081,75 +3144,121 @@ void App::renderSeedMigrationDialog()
|
||||
// the post-EndOverlayDialog wipe below.
|
||||
if (!show_seed_migration_ && busy) show_seed_migration_ = true;
|
||||
|
||||
ui::material::Type().text(ui::material::TypeStyle::H6, "Migrate to a seed wallet");
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
const ImVec4 kMedium = ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium());
|
||||
// BeginOverlayDialog scales the card by dpiScale(); the button widths below are authored in
|
||||
// logical px and must be scaled the same way or they render small/left-packed at higher DPI.
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
|
||||
switch (seed_migration_step_) {
|
||||
case SeedMigrationStep::Intro:
|
||||
ImGui::TextWrapped(
|
||||
"This creates a brand-new wallet backed by a 24-word seed phrase, in an isolated node, "
|
||||
"so you can move your funds into it. Your current wallet is NOT touched and NO funds "
|
||||
"move in this step — you'll back up the new seed first, then sweep your funds into it "
|
||||
"as a separate, confirmed step.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
if (ui::material::StyledButton("Create seed wallet", ImVec2(180, 0)))
|
||||
beginCreateSeedWallet();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton("Cancel", ImVec2(120, 0)))
|
||||
close();
|
||||
case SeedMigrationStep::Intro: {
|
||||
// Pre-flight needs a connected, unlocked wallet — gate on that, then branch on what the
|
||||
// probe found so we never offer a pointless (already-seeded) or impossible (old daemon) run.
|
||||
if (!isConnected()) {
|
||||
ImGui::TextWrapped("%s", TR("mig_not_connected"));
|
||||
ImGui::Spacing(); ImGui::Separator();
|
||||
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close();
|
||||
break;
|
||||
}
|
||||
if (state_.isLocked()) {
|
||||
ImGui::TextWrapped("%s", TR("mig_unlock_to_migrate"));
|
||||
ImGui::Spacing(); ImGui::Separator();
|
||||
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close();
|
||||
break;
|
||||
}
|
||||
if (!seed_migration_precheck_started_ && !capture_mode_) beginSeedMigrationPrecheck();
|
||||
|
||||
switch (seed_migration_precheck_) {
|
||||
case SeedMigrationPrecheck::Pending:
|
||||
ImGui::TextColored(kMedium, "%s%s", TR("mig_precheck_checking"), ui::material::LoadingDots());
|
||||
break;
|
||||
case SeedMigrationPrecheck::AlreadyMnemonic:
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.8f, 0.3f, 1.0f));
|
||||
ImGui::TextWrapped("%s", TR("mig_already_mnemonic"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing(); ImGui::Separator();
|
||||
if (ui::material::StyledButton(TR("mig_backup_instead"), ImVec2(200 * dp, 0))) {
|
||||
close();
|
||||
showSeedBackupDialog();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close();
|
||||
break;
|
||||
case SeedMigrationPrecheck::DaemonTooOld:
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
|
||||
ImGui::TextWrapped("%s", TR("mig_daemon_too_old"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing(); ImGui::Separator();
|
||||
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close();
|
||||
break;
|
||||
case SeedMigrationPrecheck::CheckFailed:
|
||||
ImGui::TextWrapped("%s", TR("mig_check_failed"));
|
||||
ImGui::Spacing(); ImGui::Separator();
|
||||
if (ui::material::StyledButton(TR("mig_recheck"), ImVec2(120 * dp, 0)))
|
||||
seed_migration_precheck_started_ = false; // re-probe next frame
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close();
|
||||
break;
|
||||
case SeedMigrationPrecheck::Legacy:
|
||||
default:
|
||||
ImGui::TextWrapped("%s", TR("mig_intro"));
|
||||
ImGui::Spacing(); ImGui::Separator();
|
||||
if (ui::material::StyledButton(TR("mig_create"), ImVec2(180 * dp, 0)))
|
||||
beginCreateSeedWallet();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton(TR("cancel"), ImVec2(120 * dp, 0)))
|
||||
close();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SeedMigrationStep::Working:
|
||||
ImGui::TextWrapped("Creating your new seed wallet in an isolated node — this can take a "
|
||||
"minute. Your main wallet keeps running.");
|
||||
ImGui::TextWrapped("%s", TR("mig_working"));
|
||||
ImGui::Spacing();
|
||||
if (!seed_migration_status_.empty())
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium()), "%s", seed_migration_status_.c_str());
|
||||
ImGui::TextColored(kMedium, "%s%s", seed_migration_status_.c_str(), ui::material::LoadingDots());
|
||||
break;
|
||||
|
||||
case SeedMigrationStep::ShowSeed: {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
|
||||
ImGui::TextWrapped("Write these 24 words down in order and store them offline. They are the "
|
||||
"only backup of your new wallet — if you lose them, the funds you migrate "
|
||||
"are gone forever.");
|
||||
ImGui::TextWrapped("%s", TR("mig_seed_warning"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_migration_seed_));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium()), "New wallet receive address:");
|
||||
ImGui::TextColored(kMedium, "%s", TR("mig_receive_addr"));
|
||||
ImGui::TextWrapped("%s", seed_migration_dest_.c_str());
|
||||
ImGui::Spacing();
|
||||
if (ui::material::StyledButton("Copy seed", ImVec2(120, 0)))
|
||||
if (ui::material::StyledButton(TR("mig_copy_seed"), ImVec2(120 * dp, 0)))
|
||||
copySecretToClipboard(seed_migration_seed_);
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton("Save to file", ImVec2(130, 0))) {
|
||||
if (ui::material::StyledButton(TR("seed_backup_save"), ImVec2(130 * dp, 0))) {
|
||||
std::string path = util::Platform::getConfigDir() + "/dragonx-seed-wallet-backup.txt";
|
||||
std::string content = seed_migration_seed_ + "\nAddress: " + seed_migration_dest_ + "\n";
|
||||
const bool ok = util::Platform::writeFileAtomically(path, content, /*restrict=*/true);
|
||||
sodium_memzero(&content[0], content.size());
|
||||
seed_migration_status_ = (ok ? "Saved to " : "Could not write ") + path;
|
||||
seed_migration_status_ = std::string(ok ? TR("seed_backup_saved") : TR("seed_backup_save_failed")) + path;
|
||||
}
|
||||
if (!seed_migration_status_.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", seed_migration_status_.c_str());
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::Checkbox("I've written down my seed phrase", &seed_migration_backed_up_);
|
||||
ImGui::Checkbox(TR("mig_backed_up"), &seed_migration_backed_up_);
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
||||
ImGui::TextWrapped("Step 1 of 2 complete — your funds have NOT moved yet. Sweeping them into "
|
||||
"this new wallet is the next step.");
|
||||
ImGui::TextWrapped("%s", TR("mig_step1_done"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Separator();
|
||||
if (!seed_migration_backed_up_) ImGui::BeginDisabled();
|
||||
if (ui::material::StyledButton("Continue to sweep", ImVec2(170, 0))) {
|
||||
if (ui::material::StyledButton(TR("mig_continue_sweep"), ImVec2(170 * dp, 0))) {
|
||||
wipeSeed(); // the sweep/adopt steps only use the address, never the seed itself
|
||||
seed_migration_step_ = SeedMigrationStep::Sweep;
|
||||
seed_migration_balance_loaded_ = false;
|
||||
seed_migration_nofunds_confirmed_ = false;
|
||||
refreshSeedMigrationBalance();
|
||||
}
|
||||
if (!seed_migration_backed_up_) ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton("Discard migration", ImVec2(160, 0))) {
|
||||
if (ui::material::StyledButton(TR("mig_discard"), ImVec2(160 * dp, 0))) {
|
||||
// Throw away the isolated new wallet + clear the pending state.
|
||||
if (!seed_migration_temp_dir_.empty()) {
|
||||
std::error_code ec;
|
||||
@@ -3168,18 +3277,47 @@ void App::renderSeedMigrationDialog()
|
||||
break;
|
||||
}
|
||||
|
||||
case SeedMigrationStep::Sweep:
|
||||
ImGui::TextWrapped("Now sweep all funds from your current wallet into the new seed wallet. "
|
||||
"This sends a single transaction to the new wallet's address; your seed "
|
||||
"phrase (already backed up) controls the funds from here on.");
|
||||
case SeedMigrationStep::Sweep: {
|
||||
// Wait for the balance to load before deciding funds vs no-funds (0.0 is also the initial
|
||||
// value, so gate on the loaded flag to avoid flashing "no funds" while it's still fetching).
|
||||
if (!seed_migration_balance_loaded_) {
|
||||
ImGui::TextColored(kMedium, "%s%s", TR("mig_checking_balance"), ui::material::LoadingDots());
|
||||
ImGui::Spacing(); ImGui::Separator();
|
||||
if (ui::material::StyledButton(TR("mig_later"), ImVec2(100 * dp, 0))) close();
|
||||
break;
|
||||
}
|
||||
if (seed_migration_balance_ <= DRAGONX_DEFAULT_FEE) {
|
||||
// Nothing to sweep — adopt the fresh seed wallet directly. Its wallet.dat replaces the
|
||||
// current (empty) one; adopt still moves the legacy wallet aside to a timestamped backup.
|
||||
ImGui::TextWrapped("%s", TR("mig_no_funds"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s%s", TR("mig_to"), seed_migration_dest_.c_str());
|
||||
ImGui::Spacing();
|
||||
// Adopt swaps wallet.dat — gate it behind an explicit acknowledgement even though no
|
||||
// funds are at risk (the legacy wallet is still moved aside to a backup on adopt).
|
||||
ImGui::Checkbox(TR("mig_nofunds_confirm"), &seed_migration_nofunds_confirmed_);
|
||||
ImGui::Spacing(); ImGui::Separator();
|
||||
if (!seed_migration_nofunds_confirmed_) ImGui::BeginDisabled();
|
||||
if (ui::material::StyledButton(TR("mig_adopt_now"), ImVec2(190 * dp, 0)))
|
||||
beginAdoptSeedWallet();
|
||||
if (!seed_migration_nofunds_confirmed_) ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton(TR("refresh"), ImVec2(110 * dp, 0)))
|
||||
refreshSeedMigrationBalance();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton(TR("mig_later"), ImVec2(100 * dp, 0)))
|
||||
close();
|
||||
break;
|
||||
}
|
||||
ImGui::TextWrapped("%s", TR("mig_sweep_intro"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium()),
|
||||
"Current wallet balance: %.8f DRGX (minus a small fee)", seed_migration_balance_);
|
||||
ImGui::TextWrapped("To: %s", seed_migration_dest_.c_str());
|
||||
char balbuf[96]; snprintf(balbuf, sizeof(balbuf), TR("mig_balance"), seed_migration_balance_);
|
||||
ImGui::TextColored(kMedium, "%s", balbuf);
|
||||
ImGui::TextWrapped("%s%s", TR("mig_to"), seed_migration_dest_.c_str());
|
||||
if (state_.isLocked()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
|
||||
ImGui::TextWrapped("Unlock your wallet first to spend from it.");
|
||||
ImGui::TextWrapped("%s", TR("mig_unlock_first"));
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::Spacing();
|
||||
@@ -3187,80 +3325,79 @@ void App::renderSeedMigrationDialog()
|
||||
{
|
||||
const bool canSweep = !state_.isLocked() && seed_migration_balance_ > 0.0;
|
||||
if (!canSweep) ImGui::BeginDisabled();
|
||||
if (ui::material::StyledButton("Sweep all funds", ImVec2(170, 0)))
|
||||
if (ui::material::StyledButton(TR("mig_sweep_all"), ImVec2(170 * dp, 0)))
|
||||
beginSweepToSeedWallet();
|
||||
if (!canSweep) ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton("Refresh", ImVec2(110, 0)))
|
||||
if (ui::material::StyledButton(TR("refresh"), ImVec2(110 * dp, 0)))
|
||||
refreshSeedMigrationBalance();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton("Later", ImVec2(100, 0)))
|
||||
if (ui::material::StyledButton(TR("mig_later"), ImVec2(100 * dp, 0)))
|
||||
close(); // pending state persists; resumes here next time
|
||||
break;
|
||||
}
|
||||
|
||||
case SeedMigrationStep::Sweeping:
|
||||
ImGui::TextWrapped("Sweeping your funds into the new wallet…");
|
||||
ImGui::TextWrapped("%s", TR("mig_sweeping"));
|
||||
ImGui::Spacing();
|
||||
if (!seed_migration_status_.empty())
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium()),
|
||||
"%s", seed_migration_status_.c_str());
|
||||
ImGui::TextColored(kMedium, "%s%s", seed_migration_status_.c_str(), ui::material::LoadingDots());
|
||||
break;
|
||||
|
||||
case SeedMigrationStep::Confirming: {
|
||||
const ImVec4 medium = ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium());
|
||||
const double dust = DRAGONX_DEFAULT_FEE; // treat <= one fee as "empty"
|
||||
const bool confirmed = seed_migration_sweep_confs_ >= 1;
|
||||
const bool legacyEmpty = seed_migration_legacy_remaining_ >= 0.0 &&
|
||||
seed_migration_legacy_remaining_ <= dust;
|
||||
ImGui::TextWrapped("Waiting for the sweep to confirm on-chain before switching wallets — "
|
||||
"this guarantees your funds have actually moved.");
|
||||
ImGui::TextWrapped("%s", TR("mig_confirming"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(medium, "Sweep confirmations: %d", seed_migration_sweep_confs_);
|
||||
char cbuf[64]; snprintf(cbuf, sizeof(cbuf), TR("mig_confs"), seed_migration_sweep_confs_);
|
||||
ImGui::TextColored(kMedium, "%s", cbuf);
|
||||
if (!seed_migration_sweep_txid_.empty())
|
||||
ImGui::TextColored(medium, "txid: %s", seed_migration_sweep_txid_.c_str());
|
||||
if (seed_migration_legacy_remaining_ >= 0.0)
|
||||
ImGui::TextColored(medium, "Remaining in old wallet: %.8f DRGX",
|
||||
seed_migration_legacy_remaining_);
|
||||
ImGui::TextColored(kMedium, "%s%s", TR("mig_txid"), seed_migration_sweep_txid_.c_str());
|
||||
if (seed_migration_legacy_remaining_ >= 0.0) {
|
||||
char rbuf[96]; snprintf(rbuf, sizeof(rbuf), TR("mig_remaining"), seed_migration_legacy_remaining_);
|
||||
ImGui::TextColored(kMedium, "%s", rbuf);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
if (confirmed && legacyEmpty) {
|
||||
if (ui::material::StyledButton("Make this my wallet", ImVec2(180, 0)))
|
||||
if (ui::material::StyledButton(TR("mig_adopt"), ImVec2(180 * dp, 0)))
|
||||
beginAdoptSeedWallet();
|
||||
} else if (confirmed && !legacyEmpty) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
|
||||
ImGui::TextWrapped("Some funds are still in your old wallet (too many inputs for one "
|
||||
"transaction) — sweep the remainder before switching.");
|
||||
ImGui::TextWrapped("%s", TR("mig_remainder"));
|
||||
ImGui::PopStyleColor();
|
||||
if (ui::material::StyledButton("Sweep remaining", ImVec2(180, 0)))
|
||||
if (ui::material::StyledButton(TR("mig_sweep_remaining"), ImVec2(180 * dp, 0)))
|
||||
beginSweepToSeedWallet();
|
||||
} else {
|
||||
ImGui::TextColored(medium, "Waiting for the sweep transaction to be mined…");
|
||||
ImGui::TextColored(kMedium, "%s", TR("mig_waiting_mine"));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton("Refresh", ImVec2(110, 0)))
|
||||
if (ui::material::StyledButton(TR("refresh"), ImVec2(110 * dp, 0)))
|
||||
pollSweepStatus();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton("Later", ImVec2(100, 0)))
|
||||
if (ui::material::StyledButton(TR("mig_later"), ImVec2(100 * dp, 0)))
|
||||
close(); // pending state + txid persist; resumes here next time
|
||||
break;
|
||||
}
|
||||
|
||||
case SeedMigrationStep::Adopting:
|
||||
ImGui::TextWrapped("Installing your new wallet and rescanning…");
|
||||
ImGui::TextWrapped("%s", TR("mig_adopting"));
|
||||
ImGui::Spacing();
|
||||
if (!seed_migration_status_.empty())
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium()),
|
||||
"%s", seed_migration_status_.c_str());
|
||||
ImGui::TextColored(kMedium, "%s%s", seed_migration_status_.c_str(), ui::material::LoadingDots());
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
||||
ImGui::TextWrapped("%s", TR("mig_adopting_note"));
|
||||
ImGui::PopStyleColor();
|
||||
break;
|
||||
|
||||
case SeedMigrationStep::Done:
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.8f, 0.3f, 1.0f));
|
||||
ImGui::TextWrapped("Migration complete. Your wallet is now backed by your seed phrase.");
|
||||
ImGui::TextWrapped("%s", TR("mig_done"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("The daemon is rescanning to show your funds — this can take a few "
|
||||
"minutes. Your previous wallet was saved as a .bak in the data folder.");
|
||||
ImGui::TextWrapped("%s", TR("mig_done_detail"));
|
||||
if (!seed_migration_status_.empty()) { // a non-fatal warning (e.g. the daemon didn't restart)
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
|
||||
@@ -3269,7 +3406,7 @@ void App::renderSeedMigrationDialog()
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
if (ui::material::StyledButton("Done", ImVec2(120, 0)))
|
||||
if (ui::material::StyledButton(TR("mig_done_btn"), ImVec2(120 * dp, 0)))
|
||||
close();
|
||||
break;
|
||||
|
||||
@@ -3279,7 +3416,7 @@ void App::renderSeedMigrationDialog()
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
if (ui::material::StyledButton("Close", ImVec2(120, 0)))
|
||||
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0)))
|
||||
close();
|
||||
break;
|
||||
}
|
||||
@@ -3298,9 +3435,6 @@ void App::renderAntivirusHelpDialog()
|
||||
return;
|
||||
}
|
||||
|
||||
ui::material::Type().text(ui::material::TypeStyle::H6, "Windows Defender Blocked xmrig");
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Mining software is often flagged as potentially unwanted. "
|
||||
"Follow these steps to enable pool mining:");
|
||||
@@ -3714,8 +3848,12 @@ void App::reinstallBundledDaemon()
|
||||
ui::Notifications::instance().warning("This build has no bundled daemon to install");
|
||||
return;
|
||||
}
|
||||
if (!daemon_controller_ || !isUsingEmbeddedDaemon()) {
|
||||
ui::Notifications::instance().warning("Reinstalling the daemon requires the embedded daemon");
|
||||
// Require embedded-daemon *support*, but NOT an active daemon_controller_. When the wallet is
|
||||
// attached to an external or leftover dragonxd (controller null, so stopEmbeddedDaemon() no-ops)
|
||||
// we can still RPC-stop that node, overwrite the binaries, and bring up our own managed daemon —
|
||||
// which is exactly the state that previously blocked "Install bundled" with a cryptic warning.
|
||||
if (!supportsEmbeddedDaemon()) {
|
||||
ui::Notifications::instance().warning("This build has no embedded daemon to install");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3730,7 +3868,13 @@ void App::reinstallBundledDaemon()
|
||||
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||
AppDaemonLifecycleRuntime runtime(*this);
|
||||
daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_);
|
||||
// Stop the daemon so its binary is no longer locked (Windows) / in use (ETXTBSY on Linux).
|
||||
// Stop the running node so its binary is free. stopDaemonWithPolicy() covers a wallet-managed
|
||||
// daemon; if we're instead attached to an external/leftover dragonxd (daemon_controller_ is
|
||||
// null, so stopEmbeddedDaemon() returns immediately), send an explicit RPC stop so that node
|
||||
// exits too — otherwise it keeps the RPC port and the fresh daemon can't start.
|
||||
if (!daemon_controller_ && rpc_ && rpc_->isConnected()) {
|
||||
sendStopCommandSafely(*rpc_, "reinstall-daemon");
|
||||
}
|
||||
runtime.stopDaemonWithPolicy();
|
||||
// Wait (bounded, ~12s) for the process to exit and release the binary before overwriting.
|
||||
for (int i = 0; i < 120 && daemon::EmbeddedDaemon::isRpcPortInUse()
|
||||
|
||||
81
src/app.h
81
src/app.h
@@ -16,6 +16,7 @@
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "data/address_book.h"
|
||||
#include "data/wallet_index.h"
|
||||
#include "data/wallet_state.h"
|
||||
#include "rpc/connection.h"
|
||||
#include "services/network_refresh_service.h"
|
||||
@@ -197,6 +198,13 @@ public:
|
||||
data::AddressBook& addressBook() { return address_book_; }
|
||||
const data::AddressBook& addressBook() const { return address_book_; }
|
||||
|
||||
// Hash of the active wallet's identity (derived from its address list), used to scope
|
||||
// per-wallet data (e.g. address-book contacts). Empty until addresses are known (pre-connect).
|
||||
std::string activeWalletIdentityHash() const;
|
||||
|
||||
data::WalletIndex& walletIndex() { return wallet_index_; }
|
||||
const data::WalletIndex& walletIndex() const { return wallet_index_; }
|
||||
|
||||
// Connection state (convenience wrappers)
|
||||
bool isConnected() const { return state_.connected; }
|
||||
int getBlockHeight() const { return state_.sync.blocks; }
|
||||
@@ -343,6 +351,12 @@ public:
|
||||
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
|
||||
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
|
||||
void startScreenshotSweep();
|
||||
// Full UI sweep: like the tab sweep, but also drives every modal / dialog / multi-step flow /
|
||||
// state overlay into view (with injected demo data, offline, firing no live ops) and captures
|
||||
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
|
||||
void startFullUiSweep();
|
||||
std::string screenshotFullDir() const;
|
||||
bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
|
||||
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
|
||||
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
|
||||
void onScreenshotCaptured();
|
||||
@@ -436,6 +450,10 @@ public:
|
||||
* Shows "Restarting daemon..." in the loading overlay while the daemon cycles.
|
||||
*/
|
||||
void restartDaemon();
|
||||
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
|
||||
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
||||
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
||||
void switchToWallet(const std::string& walletFile);
|
||||
|
||||
// Wallet encryption helpers
|
||||
void encryptWalletWithPassphrase(const std::string& passphrase);
|
||||
@@ -549,6 +567,10 @@ private:
|
||||
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
|
||||
std::string transactionHistoryCacheWalletIdentity() const;
|
||||
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
|
||||
// Record the active wallet's cached metadata (balance, address count, identity, size) into the
|
||||
// wallet index (wallets.json). Cheap + throttled: only writes when a value changed. markOpened
|
||||
// stamps last-opened + syncedHere (call once per connect).
|
||||
void updateWalletIndexForActiveWallet(bool markOpened);
|
||||
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
|
||||
// Shared main-thread continuations for a wallet unlock attempt, so the passphrase
|
||||
// and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
|
||||
@@ -693,6 +715,19 @@ private:
|
||||
enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error };
|
||||
bool show_seed_migration_ = false;
|
||||
SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;
|
||||
|
||||
// Intro pre-flight: probe the current wallet before offering to create a seed wallet, so we can
|
||||
// skip a pointless migration (AlreadyMnemonic → offer backup) or explain why it can't run
|
||||
// (DaemonTooOld). Set from beginSeedMigrationPrecheck()'s async callback (main thread).
|
||||
enum class SeedMigrationPrecheck { Pending, Legacy, AlreadyMnemonic, DaemonTooOld, CheckFailed };
|
||||
SeedMigrationPrecheck seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
|
||||
bool seed_migration_precheck_started_ = false;
|
||||
bool seed_migration_balance_loaded_ = false; // Sweep step: distinguishes "0 funds" from "not loaded yet"
|
||||
bool seed_migration_nofunds_confirmed_ = false; // "replace my wallet" gate for the no-funds adopt
|
||||
|
||||
// "A newer node is bundled — update?" startup prompt (see maybeOfferDaemonUpdate).
|
||||
bool show_daemon_update_prompt_ = false;
|
||||
unsigned long long daemon_update_bundled_size_ = 0; // bundled daemon size the prompt offers
|
||||
bool seed_migration_in_flight_ = false; // main-thread guard while the bg create task runs
|
||||
std::string seed_migration_seed_; // SECRET — the revealed phrase, wiped on close
|
||||
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
|
||||
@@ -756,16 +791,50 @@ private:
|
||||
bool screenshot_sweep_active_ = false;
|
||||
bool sweep_capture_this_frame_ = false;
|
||||
int sweep_skin_idx_ = 0;
|
||||
int sweep_page_idx_ = 0;
|
||||
int sweep_settle_frames_ = 0; // frames to let a new skin/page settle before capturing
|
||||
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
|
||||
std::vector<std::string> sweep_skins_; // skin ids to cycle
|
||||
std::vector<ui::NavPage> sweep_pages_; // enabled pages to cycle
|
||||
std::string sweep_dir_; // output folder for this sweep
|
||||
std::string sweep_current_path_; // PNG path for the frame about to be captured
|
||||
std::string sweep_saved_skin_; // restore on completion
|
||||
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
|
||||
void updateScreenshotSweep(); // called at the top of render() while active
|
||||
void applySweepTarget(); // apply current (skin,page), compute path, arm settle
|
||||
void applySweepTarget(); // apply current (skin,page/surface), path, arm settle
|
||||
|
||||
// --- Full UI sweep: the capture unit is a "surface" (a tab, optionally with a modal / step /
|
||||
// state forced on top). A tab is a surface with a null setup. ---
|
||||
struct SweepTarget {
|
||||
std::string name; // fs-safe id, e.g. "modal-seed-backup" / "overview"
|
||||
ui::NavPage page = ui::NavPage::Overview; // base tab under the surface
|
||||
std::function<void(App&)> setup; // reveal the surface (null = plain tab)
|
||||
std::function<void(App&)> teardown; // clear it (null = nothing to undo)
|
||||
int settle = 4;// blur overlays override to 8
|
||||
};
|
||||
std::vector<SweepTarget> sweep_targets_;
|
||||
int sweep_target_idx_ = 0;
|
||||
bool sweep_full_ = false; // full-UI sweep (drives surfaces) vs the legacy tab-only sweep
|
||||
// capture_mode_: set only during a full sweep. CONTRACT: while true, NO live op may fire — no
|
||||
// RPC, no auto-lock, no async pump. New async paths that could run mid-sweep must guard on it.
|
||||
bool capture_mode_ = false;
|
||||
void startSweepImpl(bool full);
|
||||
void buildSweepCatalog();
|
||||
void installDemoWalletData();
|
||||
void clearDemoWalletData();
|
||||
void applyHealthyDemoState(); // reset the connection/encryption flags to the healthy demo values
|
||||
void writeSweepManifest() const;
|
||||
// Snapshot of the state_ fields the demo installer mutates, restored at sweep end. Kept as a
|
||||
// plain struct because WalletState has reference-alias members and isn't copy-assignable.
|
||||
struct SweepStateSnapshot {
|
||||
bool valid = false;
|
||||
bool connected=false, warming_up=false, daemon_initializing=false;
|
||||
bool encrypted=false, locked=false, encryption_state_known=false;
|
||||
std::string warmup_status, warmup_description;
|
||||
SyncInfo sync;
|
||||
double privateBalance=0, transparentBalance=0, totalBalance=0, unconfirmedBalance=0;
|
||||
std::vector<AddressInfo> addresses, z_addresses, t_addresses;
|
||||
std::vector<TransactionInfo> transactions;
|
||||
double market_price_usd=0;
|
||||
double market_change_24h=0;
|
||||
} sweep_state_snapshot_;
|
||||
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
||||
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
||||
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
|
||||
@@ -913,6 +982,7 @@ private:
|
||||
std::unique_ptr<util::SecureVault> vault_;
|
||||
data::TransactionHistoryCache transaction_history_cache_;
|
||||
data::AddressBook address_book_; // shared contact store; loaded once in init(), self-saves on mutation
|
||||
data::WalletIndex wallet_index_; // per-wallet metadata cache (wallets.json); populated after each load
|
||||
std::string pending_transaction_history_cache_passphrase_;
|
||||
bool transaction_history_cache_loaded_ = false;
|
||||
|
||||
@@ -979,6 +1049,9 @@ private:
|
||||
void renderBackupDialog();
|
||||
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
|
||||
void renderSeedMigrationDialog(); // "Migrate to a seed wallet" guided modal (Phase 1: create)
|
||||
void beginSeedMigrationPrecheck(); // Intro: probe whether the wallet is legacy / already-seeded
|
||||
void maybeOfferDaemonUpdate(); // at startup, flag the prompt if a newer daemon is bundled
|
||||
void renderDaemonUpdatePrompt(); // "a newer node is bundled — update the installed daemon?"
|
||||
void renderFirstRunWizard();
|
||||
void renderLockScreen();
|
||||
void renderEncryptWalletDialog();
|
||||
|
||||
@@ -499,7 +499,11 @@ void App::onConnected()
|
||||
daemon_start_error_shown_ = false;
|
||||
daemon_last_seen_crashes_ = 0; // (onConnected resets the daemon's crash count too)
|
||||
connection_status_ = TR("connected");
|
||||
|
||||
|
||||
// Stamp the active wallet as opened in the index (last-opened + size + synced-here). Balance +
|
||||
// address count fill in on the first address refresh (addresses aren't loaded yet here).
|
||||
updateWalletIndexForActiveWallet(/*markOpened=*/true);
|
||||
|
||||
// Reset crash counter on successful connection
|
||||
if (daemon_controller_) {
|
||||
daemon_controller_->resetCrashCount();
|
||||
@@ -759,109 +763,7 @@ void App::setCurrentPage(ui::NavPage page)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Debug screenshot sweep ──────────────────────────────────────────────────
|
||||
// Cycle every skin x every build-enabled tab, saving one PNG per (skin,tab). Driven from
|
||||
// App::render() (updateScreenshotSweep pins the page + settles); main.cpp reads the framebuffer
|
||||
// on the settled frame and calls onScreenshotCaptured() to advance.
|
||||
|
||||
// Filesystem-safe one-word page name (tracePageName returns "Overview tab" etc.).
|
||||
static const char* sweepPageName(ui::NavPage page)
|
||||
{
|
||||
switch (page) {
|
||||
case ui::NavPage::Overview: return "overview";
|
||||
case ui::NavPage::Send: return "send";
|
||||
case ui::NavPage::Receive: return "receive";
|
||||
case ui::NavPage::History: return "history";
|
||||
case ui::NavPage::Contacts: return "contacts";
|
||||
case ui::NavPage::Chat: return "chat";
|
||||
case ui::NavPage::Mining: return "mining";
|
||||
case ui::NavPage::Market: return "market";
|
||||
case ui::NavPage::Console: return "console";
|
||||
case ui::NavPage::Peers: return "network";
|
||||
case ui::NavPage::Explorer: return "explorer";
|
||||
case ui::NavPage::Settings: return "settings";
|
||||
default: return "page";
|
||||
}
|
||||
}
|
||||
|
||||
std::string App::screenshotDir() const
|
||||
{
|
||||
return (std::filesystem::path(util::Platform::getObsidianDragonDir()) / "screenshots").string();
|
||||
}
|
||||
|
||||
void App::startScreenshotSweep()
|
||||
{
|
||||
if (screenshot_sweep_active_) return;
|
||||
|
||||
sweep_skins_.clear();
|
||||
for (const auto& s : ui::schema::SkinManager::instance().available())
|
||||
if (s.valid) sweep_skins_.push_back(s.id);
|
||||
sweep_pages_.clear();
|
||||
for (int p = 0; p < static_cast<int>(ui::NavPage::Count_); ++p) {
|
||||
ui::NavPage pg = static_cast<ui::NavPage>(p);
|
||||
if (isPageEnabledForBuild(pg)) sweep_pages_.push_back(pg);
|
||||
}
|
||||
if (sweep_skins_.empty() || sweep_pages_.empty()) return;
|
||||
|
||||
// Fixed output folder (organized into per-tab subfolders below); subsequent sweeps overwrite the
|
||||
// existing PNGs in place rather than creating a new folder each time.
|
||||
sweep_dir_ = screenshotDir();
|
||||
std::error_code ec; std::filesystem::create_directories(sweep_dir_, ec);
|
||||
|
||||
sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId();
|
||||
sweep_saved_page_ = current_page_;
|
||||
sweep_skin_idx_ = 0; sweep_page_idx_ = 0;
|
||||
screenshot_sweep_active_ = true;
|
||||
sweep_capture_this_frame_ = false;
|
||||
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]);
|
||||
applySweepTarget();
|
||||
ui::Notifications::instance().info("Screenshot sweep running…");
|
||||
DEBUG_LOGF("[Sweep] -> %s (%d skins x %d pages)\n",
|
||||
sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_pages_.size());
|
||||
}
|
||||
|
||||
void App::applySweepTarget()
|
||||
{
|
||||
current_page_ = sweep_pages_[sweep_page_idx_];
|
||||
page_alpha_ = 1.0f; // skip the page-switch fade so the shot isn't captured mid-animation
|
||||
// Organized as <screenshots>/<tab>/<skin>.png — one subfolder per tab, one PNG per theme,
|
||||
// overwritten in place on the next sweep. (writePng in main.cpp creates the parent subfolder.)
|
||||
sweep_current_path_ = (std::filesystem::path(sweep_dir_) / sweepPageName(current_page_)
|
||||
/ (sweep_skins_[sweep_skin_idx_] + ".png")).string();
|
||||
sweep_settle_frames_ = 4; // let the new skin (TOML reload / acrylic recapture) + page settle
|
||||
sweep_capture_this_frame_ = false;
|
||||
}
|
||||
|
||||
void App::updateScreenshotSweep()
|
||||
{
|
||||
if (!screenshot_sweep_active_) return;
|
||||
current_page_ = sweep_pages_[sweep_page_idx_]; // keep the page pinned each frame
|
||||
page_alpha_ = 1.0f;
|
||||
if (sweep_settle_frames_ > 0) { sweep_settle_frames_--; sweep_capture_this_frame_ = false; }
|
||||
else sweep_capture_this_frame_ = true; // settled — main.cpp captures this frame
|
||||
}
|
||||
|
||||
void App::onScreenshotCaptured()
|
||||
{
|
||||
sweep_capture_this_frame_ = false;
|
||||
if (!screenshot_sweep_active_) return;
|
||||
if (++sweep_page_idx_ >= static_cast<int>(sweep_pages_.size())) {
|
||||
sweep_page_idx_ = 0;
|
||||
if (++sweep_skin_idx_ >= static_cast<int>(sweep_skins_.size())) {
|
||||
// Done — restore the original skin + page.
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_);
|
||||
current_page_ = sweep_saved_page_;
|
||||
page_alpha_ = 1.0f;
|
||||
screenshot_sweep_active_ = false;
|
||||
ui::Notifications::instance().success("Screenshots saved to " + sweep_dir_);
|
||||
DEBUG_LOGF("[Sweep] done -> %s\n", sweep_dir_.c_str());
|
||||
return;
|
||||
}
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[sweep_skin_idx_]);
|
||||
}
|
||||
applySweepTarget();
|
||||
}
|
||||
// Screenshot sweep (tab-only + full-UI) + its demo data live in src/app_sweep.cpp.
|
||||
|
||||
bool App::shouldRefreshTransactions() const
|
||||
{
|
||||
@@ -1061,6 +963,102 @@ std::string App::transactionHistoryCacheWalletIdentity() const
|
||||
shieldedAddresses, transparentAddresses);
|
||||
}
|
||||
|
||||
std::string App::activeWalletIdentityHash() const
|
||||
{
|
||||
const std::string identity = transactionHistoryCacheWalletIdentity();
|
||||
if (identity.empty()) return std::string();
|
||||
return data::TransactionHistoryCache::walletIdentityHash(identity);
|
||||
}
|
||||
|
||||
void App::updateWalletIndexForActiveWallet(bool markOpened)
|
||||
{
|
||||
if (!settings_) return;
|
||||
const std::string file = settings_->getActiveWalletFile();
|
||||
if (file.empty()) return;
|
||||
|
||||
// Start from the existing entry so we preserve fields we're not updating this call.
|
||||
data::WalletIndexEntry e;
|
||||
if (const auto* existing = wallet_index_.find(file)) e = *existing;
|
||||
e.fileName = file;
|
||||
if (e.displayName.empty()) e.displayName = file;
|
||||
|
||||
// Balance + address count are only meaningful once the wallet's addresses are known (identity
|
||||
// non-empty); at connect time they're still 0, so don't cache bogus zeros then.
|
||||
const std::string idHash = state_.connected ? activeWalletIdentityHash() : std::string();
|
||||
if (!idHash.empty()) {
|
||||
e.walletIdentityHash = idHash;
|
||||
e.cachedBalance = state_.totalBalance;
|
||||
e.cachedAddressCount = static_cast<long long>(state_.z_addresses.size() + state_.t_addresses.size());
|
||||
}
|
||||
|
||||
// File size is always readable from disk.
|
||||
std::error_code ec;
|
||||
const std::string path = util::Platform::getDragonXDataDir() + "/" + file;
|
||||
if (std::filesystem::exists(path, ec)) {
|
||||
auto sz = std::filesystem::file_size(path, ec);
|
||||
if (!ec) e.sizeBytesAtLastOpen = static_cast<long long>(sz);
|
||||
}
|
||||
|
||||
if (markOpened) {
|
||||
e.lastOpenedEpoch = static_cast<long long>(std::time(nullptr));
|
||||
e.syncedHere = true; // we've loaded it in this datadir -> catch-up (no full rescan) on switch
|
||||
}
|
||||
|
||||
if (wallet_index_.upsert(e)) wallet_index_.save();
|
||||
}
|
||||
|
||||
void App::switchToWallet(const std::string& walletFile)
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||
return;
|
||||
}
|
||||
if (walletFile.empty() || !settings_) return;
|
||||
if (settings_->getActiveWalletFile() == walletFile) {
|
||||
ui::Notifications::instance().info("That wallet is already open.");
|
||||
return;
|
||||
}
|
||||
if (daemon_restarting_) {
|
||||
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
|
||||
return;
|
||||
}
|
||||
if (!isUsingEmbeddedDaemon()) {
|
||||
ui::Notifications::instance().warning("Switching wallets needs the embedded daemon — stop any external dragonxd first.");
|
||||
return;
|
||||
}
|
||||
if (hasTransactionSendProgress()) {
|
||||
ui::Notifications::instance().warning("Finish or cancel the pending send before switching wallets.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Rescan only if this wallet was never synced in this datadir (freshly imported/new). One we've
|
||||
// loaded here before just catches up from its recorded block on start — fast.
|
||||
bool needRescan = true;
|
||||
if (const auto* e = wallet_index_.find(walletFile)) needRescan = !e->syncedHere;
|
||||
|
||||
settings_->setActiveWalletFile(walletFile);
|
||||
settings_->save();
|
||||
|
||||
// Same restart coordination as the seed-adopt flow: gate the main-loop reconnect, disconnect,
|
||||
// then stop + restart on a background thread. syncSettings() re-reads active_wallet_file on
|
||||
// start, so the daemon comes up on -wallet=<name>. WalletState clears on disconnect and the
|
||||
// per-wallet caches re-key off the new identity (P1), so no previous-wallet data leaks through.
|
||||
daemon_restarting_ = true;
|
||||
if (rpc_ && rpc_->isConnected()) rpc_->disconnect();
|
||||
onDisconnected("Switching wallet");
|
||||
ui::Notifications::instance().info("Switching wallet — the node will restart…");
|
||||
|
||||
async_tasks_.submit("Switch wallet", [this, needRescan](const util::AsyncTaskManager::Token&) {
|
||||
// Stop the node fully so it releases the datadir .lock + RPC port before we relaunch.
|
||||
stopEmbeddedDaemon();
|
||||
for (int i = 0; i < 60 && isEmbeddedDaemonRunning(); ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
|
||||
if (!shutting_down_) startEmbeddedDaemon();
|
||||
daemon_restarting_ = false; // re-arm reconnect once the new daemon has been launched
|
||||
});
|
||||
}
|
||||
|
||||
void App::wipePendingTransactionHistoryCachePassphrase()
|
||||
{
|
||||
if (!pending_transaction_history_cache_passphrase_.empty()) {
|
||||
@@ -1439,6 +1437,9 @@ void App::refreshAddressData()
|
||||
if (state_.transactions.empty()) loadTransactionHistoryCacheIfAvailable();
|
||||
else storeTransactionHistoryCacheIfAvailable();
|
||||
maybeFinishTransactionSendProgress();
|
||||
// Addresses (hence identity + count) are now known — refresh the wallet index's cached
|
||||
// metadata for the active wallet. Throttled: upsert() only writes on a real change.
|
||||
updateWalletIndexForActiveWallet(/*markOpened=*/false);
|
||||
};
|
||||
}, 3);
|
||||
if (!enqueued.enqueued) return;
|
||||
@@ -2914,6 +2915,7 @@ void App::exportSeedPhrase(std::function<void(bool, bool, const std::string&, co
|
||||
// learns whether a mnemonic exists). Legacy (non-mnemonic) wallets are not nagged.
|
||||
void App::maybeRemindSeedBackup()
|
||||
{
|
||||
if (capture_mode_) return; // no live ops during a UI sweep
|
||||
if (lite_wallet_) return; // lite has its own seed UX
|
||||
if (!settings_ || settings_->getSeedBackupReminded()) return;
|
||||
if (!state_.connected || !state_.encryption_state_known) return;
|
||||
@@ -2980,13 +2982,43 @@ void App::showSeedMigrationDialog()
|
||||
seed_migration_step_ = SeedMigrationStep::Confirming;
|
||||
} else {
|
||||
seed_migration_step_ = SeedMigrationStep::Sweep;
|
||||
seed_migration_balance_loaded_ = false;
|
||||
seed_migration_nofunds_confirmed_ = false;
|
||||
refreshSeedMigrationBalance();
|
||||
}
|
||||
} else {
|
||||
seed_migration_step_ = SeedMigrationStep::Intro;
|
||||
// Fresh start: the Intro step will pre-flight the wallet (legacy vs already-seeded vs old
|
||||
// daemon) before offering to create anything.
|
||||
seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
|
||||
seed_migration_precheck_started_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Intro pre-flight: probe z_exportmnemonic on the CURRENT wallet so we can branch the Intro:
|
||||
// ok → the wallet is already mnemonic-backed (offer backup, not a pointless migration)
|
||||
// "not derived" → a legacy wallet (proceed with create)
|
||||
// "Method not found" → the daemon is too old to create a seed wallet (explain + bail)
|
||||
// The probe needs an unlocked, connected wallet; the Intro gates on that before calling this.
|
||||
void App::beginSeedMigrationPrecheck()
|
||||
{
|
||||
seed_migration_precheck_started_ = true;
|
||||
seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
|
||||
exportSeedPhrase([this](bool ok, bool noMnemonic, const std::string& /*phrase*/,
|
||||
const std::string& error) {
|
||||
// exportSeedPhrase wipes the phrase after this callback — we only need the classification.
|
||||
if (ok)
|
||||
seed_migration_precheck_ = SeedMigrationPrecheck::AlreadyMnemonic;
|
||||
else if (noMnemonic)
|
||||
seed_migration_precheck_ = SeedMigrationPrecheck::Legacy;
|
||||
else if (error.find("Method not found") != std::string::npos ||
|
||||
error.find("-32601") != std::string::npos)
|
||||
seed_migration_precheck_ = SeedMigrationPrecheck::DaemonTooOld;
|
||||
else
|
||||
seed_migration_precheck_ = SeedMigrationPrecheck::CheckFailed; // transient — allow re-check
|
||||
});
|
||||
}
|
||||
|
||||
void App::refreshSeedMigrationBalance()
|
||||
{
|
||||
if (!rpc_ || !worker_ || !state_.connected) return;
|
||||
@@ -2999,7 +3031,7 @@ void App::refreshSeedMigrationBalance()
|
||||
total = std::stod(res["total"].get<std::string>());
|
||||
ok = true;
|
||||
} catch (...) {}
|
||||
return [this, total, ok]() { if (ok) seed_migration_balance_ = total; };
|
||||
return [this, total, ok]() { if (ok) { seed_migration_balance_ = total; seed_migration_balance_loaded_ = true; } };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3063,6 +3095,7 @@ void App::beginSweepToSeedWallet()
|
||||
// remainder is left behind (a sweep too big for one transaction).
|
||||
void App::pollSweepStatus()
|
||||
{
|
||||
if (capture_mode_) return; // no live ops during a UI sweep
|
||||
if (!rpc_ || !worker_ || !state_.connected) return;
|
||||
if (seed_migration_confirm_in_flight_ || seed_migration_sweep_txid_.empty()) return;
|
||||
seed_migration_confirm_in_flight_ = true;
|
||||
@@ -3122,7 +3155,12 @@ void App::beginAdoptSeedWallet()
|
||||
const std::string legacy = datadir + "/wallet.dat";
|
||||
const std::string newWallet = base + "/DRAGONX/wallet.dat";
|
||||
std::time_t t = std::time(nullptr);
|
||||
std::tm tmv{}; localtime_r(&t, &tmv); // thread-safe (the UI thread also uses localtime)
|
||||
std::tm tmv{}; // thread-safe local time (the UI thread also uses localtime)
|
||||
#ifdef _WIN32
|
||||
localtime_s(&tmv, &t);
|
||||
#else
|
||||
localtime_r(&t, &tmv);
|
||||
#endif
|
||||
char ts[32]; std::strftime(ts, sizeof(ts), "%Y%m%d-%H%M%S", &tmv);
|
||||
const std::string backup = legacy + ".legacy-migrated-" + ts + ".bak";
|
||||
if (!fs::exists(newWallet)) {
|
||||
@@ -3172,6 +3210,7 @@ void App::beginAdoptSeedWallet()
|
||||
|
||||
void App::pumpSeedMigration()
|
||||
{
|
||||
if (capture_mode_) return; // no live ops during a UI sweep (steps are set directly)
|
||||
// --- Adopt handoff (Phase 2, background daemon swap) ---
|
||||
{
|
||||
bool done = false, ok = false; std::string err;
|
||||
|
||||
@@ -593,6 +593,7 @@ void App::refreshWalletEncryptionState() {
|
||||
// ===========================================================================
|
||||
|
||||
void App::checkAutoLock() {
|
||||
if (capture_mode_) return; // don't auto-lock while a UI sweep forces the encrypted/locked state
|
||||
if (!state_.isEncrypted() || state_.isLocked()) return;
|
||||
|
||||
// Don't auto-lock while mining — mining is a long-running intentional
|
||||
@@ -823,12 +824,19 @@ void App::renderLockScreen() {
|
||||
MarkBlurOverlayDrawn(nullptr); // suppress foreground theme-effect bleed + re-capture on unlock
|
||||
}
|
||||
|
||||
// Card
|
||||
// Card. The lock screen is fully hand-drawn (dl->AddText at manual y offsets), so — unlike
|
||||
// BeginOverlayDialog cards — nothing scales automatically. Multiply every geometry literal
|
||||
// (card size, logo, input/button widths, vertical gaps) by dpiScale() so the card grows with
|
||||
// OS DPI / font scale instead of stranding a native-size card in a large window. The font
|
||||
// metrics (LegacySize / CalcTextSize) are already DPI-scaled via the atlas, so leave those.
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
const auto& S = ui::schema::UI();
|
||||
float cardW = S.drawElement("screens.lock-screen", "card").getFloat("width", 400.0f);
|
||||
float cardH = S.drawElement("screens.lock-screen", "card").height;
|
||||
if (cardW <= 0) cardW = 400.0f;
|
||||
if (cardH <= 0) cardH = 320.0f;
|
||||
cardW *= dp;
|
||||
cardH *= dp;
|
||||
|
||||
float cardX = winPos.x + (winSize.x - cardW) * 0.5f;
|
||||
float cardY = winPos.y + (winSize.y - cardH) * 0.5f;
|
||||
@@ -844,17 +852,17 @@ void App::renderLockScreen() {
|
||||
cardGlass.borderWidth = 1.0f;
|
||||
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
||||
|
||||
float cy = cardY + 24.0f;
|
||||
float cy = cardY + 24.0f * dp;
|
||||
|
||||
// Logo
|
||||
float logoSize = S.drawElement("screens.lock-screen", "logo").sizeOr(64.0f);
|
||||
float logoSize = S.drawElement("screens.lock-screen", "logo").sizeOr(64.0f) * dp;
|
||||
if (logo_tex_ != 0) {
|
||||
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
|
||||
float logoW = logoSize * aspect;
|
||||
float logoX = cardX + (cardW - logoW) * 0.5f;
|
||||
dl->AddImage(logo_tex_, ImVec2(logoX, cy), ImVec2(logoX + logoW, cy + logoSize));
|
||||
}
|
||||
cy += logoSize + 16.0f;
|
||||
cy += logoSize + 16.0f * dp;
|
||||
|
||||
// Title
|
||||
ImFont* titleFont = S.resolveFont(S.label("screens.lock-screen", "title").font);
|
||||
@@ -868,7 +876,7 @@ void App::renderLockScreen() {
|
||||
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
|
||||
dl->AddText(titleFont, titleFont->LegacySize,
|
||||
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
|
||||
cy += ts.y + 20.0f;
|
||||
cy += ts.y + 20.0f * dp;
|
||||
}
|
||||
|
||||
// Lockout timer
|
||||
@@ -881,7 +889,7 @@ void App::renderLockScreen() {
|
||||
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
|
||||
cy += captionFont->LegacySize + 12.0f;
|
||||
cy += captionFont->LegacySize + 12.0f * dp;
|
||||
}
|
||||
|
||||
// Check if PIN vault is available
|
||||
@@ -906,7 +914,7 @@ void App::renderLockScreen() {
|
||||
ImVec2(startX, cy), IM_COL32(255,255,255,120), modeIcon);
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(startX + iconSize.x, textY), IM_COL32(255,255,255,120), modeText);
|
||||
cy += std::max(iconSize.y, textSize.y) + 8.0f;
|
||||
cy += std::max(iconSize.y, textSize.y) + 8.0f * dp;
|
||||
|
||||
// Switch link
|
||||
ImVec2 sls = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, switchLabel);
|
||||
@@ -923,7 +931,7 @@ void App::renderLockScreen() {
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(switchX, cy), ui::material::Primary(), switchLabel);
|
||||
ImGui::PopStyleColor();
|
||||
cy += captionFont->LegacySize + 12.0f;
|
||||
cy += captionFont->LegacySize + 12.0f * dp;
|
||||
} else {
|
||||
// No PIN vault — don't show toggle, force passphrase mode
|
||||
lock_use_pin_ = false;
|
||||
@@ -932,6 +940,7 @@ void App::renderLockScreen() {
|
||||
// Input field
|
||||
float inputW = S.drawElement("screens.lock-screen", "input").getFloat("width", 320.0f);
|
||||
if (inputW <= 0) inputW = 320.0f;
|
||||
inputW *= dp;
|
||||
float inputX = cardX + (cardW - inputW) * 0.5f;
|
||||
|
||||
bool canSubmit = lock_lockout_timer_ <= 0.0f && !lock_unlock_in_progress_;
|
||||
@@ -941,7 +950,7 @@ void App::renderLockScreen() {
|
||||
// PIN input
|
||||
ImGui::SetCursorScreenPos(ImVec2(inputX, cy));
|
||||
ImGui::PushItemWidth(inputW);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
|
||||
ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal;
|
||||
if (canSubmit) pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
|
||||
submitted = ImGui::InputText("##lock_pin", lock_pin_buf_,
|
||||
@@ -952,7 +961,7 @@ void App::renderLockScreen() {
|
||||
// Passphrase input (original)
|
||||
ImGui::SetCursorScreenPos(ImVec2(inputX, cy));
|
||||
ImGui::PushItemWidth(inputW);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
|
||||
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_Password;
|
||||
if (canSubmit) inputFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
|
||||
submitted = ImGui::InputText("##lock_pass", lock_passphrase_buf_,
|
||||
@@ -964,9 +973,9 @@ void App::renderLockScreen() {
|
||||
// default frame is subtle on the glass card, so this shows the field is active and ready to type.
|
||||
if (ImGui::IsItemActive() || ImGui::IsItemFocused()) {
|
||||
dl->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(),
|
||||
ui::material::Primary(), 6.0f, 0, 2.0f);
|
||||
ui::material::Primary(), 6.0f * dp, 0, 2.0f * dp);
|
||||
}
|
||||
cy += 40.0f + 12.0f;
|
||||
cy += (40.0f + 12.0f) * dp;
|
||||
|
||||
// Focus the input when the lock screen first appears.
|
||||
// IsWindowAppearing() does not work here because the lock screen is
|
||||
@@ -984,13 +993,13 @@ void App::renderLockScreen() {
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(),
|
||||
lock_error_msg_.c_str());
|
||||
cy += captionFont->LegacySize + 8.0f;
|
||||
cy += captionFont->LegacySize + 8.0f * dp;
|
||||
}
|
||||
|
||||
// "Unlocking..." feedback while worker thread is running
|
||||
// Always reserve the vertical space so the button doesn't shift.
|
||||
{
|
||||
float rowH = captionFont->LegacySize + 8.0f;
|
||||
float rowH = captionFont->LegacySize + 8.0f * dp;
|
||||
if (lock_unlock_in_progress_) {
|
||||
// Animated spinner dots
|
||||
char msg[64];
|
||||
@@ -1008,13 +1017,15 @@ void App::renderLockScreen() {
|
||||
float unlockH = S.drawElement("screens.lock-screen", "unlock-button").height;
|
||||
if (unlockW <= 0) unlockW = 320.0f;
|
||||
if (unlockH <= 0) unlockH = 44.0f;
|
||||
unlockW *= dp;
|
||||
unlockH *= dp;
|
||||
float unlockX = cardX + (cardW - unlockW) * 0.5f;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(unlockX, cy));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!canSubmit);
|
||||
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
637
src/app_sweep.cpp
Normal file
637
src/app_sweep.cpp
Normal file
@@ -0,0 +1,637 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Screenshot sweep: capture the UI under every skin. Two modes share one state machine:
|
||||
// - startScreenshotSweep() — the legacy tab-only sweep (<config>/screenshots).
|
||||
// - startFullUiSweep() — ALSO drives every modal / dialog / multi-step flow / state overlay
|
||||
// into view (offline, with injected demo data, firing no live ops) and captures each under
|
||||
// every skin (<config>/screenshots-full/<surface>/<skin>.png + index.md).
|
||||
// The capture unit is a "surface" (SweepTarget): a base tab, optionally with a setup() that forces
|
||||
// a modal/step/state on top and a teardown() that clears it. main.cpp reads the framebuffer on the
|
||||
// settled frame and calls onScreenshotCaptured() to advance.
|
||||
|
||||
#include "app.h"
|
||||
|
||||
#include "config/settings.h"
|
||||
#include "ui/schema/skin_manager.h"
|
||||
#include "ui/notifications.h"
|
||||
#include "ui/sidebar.h"
|
||||
#include "ui/windows/send_tab.h"
|
||||
#include "ui/windows/wallets_dialog.h"
|
||||
#include "ui/windows/daemon_download_dialog.h"
|
||||
#include "ui/windows/xmrig_download_dialog.h"
|
||||
#include "util/platform.h"
|
||||
#include "wallet/wallet_capabilities.h"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h" // ClosePopupToLevel / OpenPopupStack — flush stuck popups after teardown
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
namespace dragonx {
|
||||
|
||||
namespace {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// The real portfolio, snapshotted while demo groups are shown during a full sweep. Settings are
|
||||
// forward-declared in app.h, so this can't live in the app.h SweepStateSnapshot struct.
|
||||
std::vector<config::Settings::PortfolioEntry> g_pfSnapshot;
|
||||
int g_pfStyleSnapshot = 0;
|
||||
bool g_pfSnapshotValid = false;
|
||||
std::vector<double> g_marketHistorySnapshot; // real price history, restored at sweep end
|
||||
|
||||
// Filesystem-safe one-word tab name.
|
||||
const char* sweepPageName(ui::NavPage page)
|
||||
{
|
||||
switch (page) {
|
||||
case ui::NavPage::Overview: return "overview";
|
||||
case ui::NavPage::Send: return "send";
|
||||
case ui::NavPage::Receive: return "receive";
|
||||
case ui::NavPage::History: return "history";
|
||||
case ui::NavPage::Contacts: return "contacts";
|
||||
case ui::NavPage::Chat: return "chat";
|
||||
case ui::NavPage::Mining: return "mining";
|
||||
case ui::NavPage::Market: return "market";
|
||||
case ui::NavPage::Console: return "console";
|
||||
case ui::NavPage::LiteConsole: return "console";
|
||||
case ui::NavPage::Peers: return "network";
|
||||
case ui::NavPage::LiteNetwork: return "network";
|
||||
case ui::NavPage::Explorer: return "explorer";
|
||||
case ui::NavPage::Settings: return "settings";
|
||||
default: return "page";
|
||||
}
|
||||
}
|
||||
|
||||
bool sweepPageEnabled(ui::NavPage page)
|
||||
{
|
||||
return wallet::isUiSurfaceAvailable(wallet::currentWalletCapabilities(), ui::NavPageSurface(page));
|
||||
}
|
||||
|
||||
// Deterministic demo secrets/addresses (NON-secret — safe to hold in memory + display).
|
||||
const char* kDemoMnemonic =
|
||||
"select milk exit banana type alcohol comic moral drama federal just green "
|
||||
"elevator render stumble lesson convince organ category caution panther misery pelican immune";
|
||||
const char* kDemoZAddr =
|
||||
"zs1demoseedwalletreceiveaddressforuisweepcaptures00000000000000000000000000";
|
||||
const char* kDemoTxid = "b647077c471fdd2877d35c9d8b70e3c785547fae618458b4068d913ee3d1dc4f";
|
||||
} // namespace
|
||||
|
||||
std::string App::screenshotDir() const
|
||||
{
|
||||
return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots").string();
|
||||
}
|
||||
|
||||
std::string App::screenshotFullDir() const
|
||||
{
|
||||
return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots-full").string();
|
||||
}
|
||||
|
||||
// ── Demo data (full sweep only): make every data-dependent screen render offline ─────────────
|
||||
void App::installDemoWalletData()
|
||||
{
|
||||
// Snapshot the state_ fields we mutate (WalletState isn't copy-assignable — reference aliases).
|
||||
auto& s = sweep_state_snapshot_;
|
||||
s.connected = state_.connected; s.warming_up = state_.warming_up;
|
||||
s.daemon_initializing = state_.daemon_initializing;
|
||||
s.encrypted = state_.encrypted; s.locked = state_.locked;
|
||||
s.encryption_state_known = state_.encryption_state_known;
|
||||
s.warmup_status = state_.warmup_status; s.warmup_description = state_.warmup_description;
|
||||
s.sync = state_.sync;
|
||||
s.privateBalance = state_.privateBalance; s.transparentBalance = state_.transparentBalance;
|
||||
s.totalBalance = state_.totalBalance; s.unconfirmedBalance = state_.unconfirmedBalance;
|
||||
s.addresses = state_.addresses; s.z_addresses = state_.z_addresses; s.t_addresses = state_.t_addresses;
|
||||
s.transactions = state_.transactions;
|
||||
s.market_price_usd = state_.market.price_usd;
|
||||
s.market_change_24h = state_.market.change_24h;
|
||||
s.valid = true;
|
||||
|
||||
applyHealthyDemoState();
|
||||
state_.sync.blocks = state_.sync.headers = 3124322;
|
||||
state_.sync.verification_progress = 1.0; state_.sync.syncing = false;
|
||||
|
||||
state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000;
|
||||
state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000;
|
||||
state_.market.price_usd = 0.01336200;
|
||||
state_.market.change_24h = 5.24000000;
|
||||
// Wavy, gently-rising price history so the row sparklines render (restored at sweep end).
|
||||
g_marketHistorySnapshot = state_.market.price_history;
|
||||
{
|
||||
std::vector<double> hist;
|
||||
for (int i = 0; i < 48; i++) {
|
||||
double t = static_cast<double>(i);
|
||||
hist.push_back(0.01250 + 0.0000180 * t
|
||||
+ 0.00060 * std::sin(t * 0.55) + 0.00025 * std::sin(t * 1.7));
|
||||
}
|
||||
state_.market.price_history = hist;
|
||||
}
|
||||
|
||||
auto zaddr = [](const char* a, double bal, const char* label) {
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i;
|
||||
};
|
||||
auto taddr = [](const char* a, double bal, const char* label) {
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "transparent"; i.label = label; return i;
|
||||
};
|
||||
state_.z_addresses = {
|
||||
zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"),
|
||||
zaddr("zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", 0.5, ""),
|
||||
};
|
||||
state_.t_addresses = {
|
||||
taddr("t1DemoTransparentAddressForUiSweep00000", 3.25, "Mining payouts"),
|
||||
taddr("t1DemoSecondTransparentAddressForUiSw00", 0.0, ""),
|
||||
};
|
||||
state_.rebuildAddressList();
|
||||
|
||||
auto tx = [](const char* id, const char* type, double amt, int64_t ts, int conf,
|
||||
const char* addr, const char* memo) {
|
||||
TransactionInfo t; t.txid = id; t.type = type; t.amount = amt; t.timestamp = ts;
|
||||
t.confirmations = conf; t.address = addr; t.memo = memo; return t;
|
||||
};
|
||||
state_.transactions = {
|
||||
tx(kDemoTxid, "receive", 15.75000000, 1751286000, 42,
|
||||
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", "welcome to DragonX"),
|
||||
tx("a1b2c3d4e5f600000000000000000000000000000000000000000000000000000000", "send", 2.50000000,
|
||||
1751200000, 120, "zs1demopeerreceivingaddressabcdef0000000000000000000000000000000", ""),
|
||||
tx("c0ffee00000000000000000000000000000000000000000000000000000000000000", "mined", 0.30000000,
|
||||
1751100000, 300, "t1DemoTransparentAddressForUiSweep00000", ""),
|
||||
tx("deadbeef000000000000000000000000000000000000000000000000000000000000", "receive", 0.50000000,
|
||||
1751290000, 0, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", "pending"),
|
||||
};
|
||||
|
||||
// Demo portfolio groups so the Market tab's row styles render with real-looking data. Snapshot
|
||||
// the user's real portfolio first; setPortfolio* only mutate memory (save() is never called
|
||||
// here), and clearDemoWalletData() restores them at sweep end.
|
||||
if (settings_) {
|
||||
g_pfSnapshot = settings_->getPortfolioEntries();
|
||||
g_pfStyleSnapshot = settings_->getPortfolioStyle();
|
||||
g_pfSnapshotValid = true;
|
||||
auto grp = [](const char* label, const char* icon, uint32_t color, const char* addr,
|
||||
bool drgx, bool value, bool ch, bool spark) {
|
||||
config::Settings::PortfolioEntry e;
|
||||
e.label = label; e.icon = icon; e.color = color; e.outlineOpacity = 30;
|
||||
e.addresses = { addr }; e.priceBasis = 0;
|
||||
e.showDrgx = drgx; e.showValue = value; e.show24h = ch; e.showSparkline = spark;
|
||||
return e;
|
||||
};
|
||||
settings_->setPortfolioEntries({
|
||||
grp("Savings", "savings", 0xFFFF9D4Fu,
|
||||
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", true, true, true, true),
|
||||
grp("Mining rewards", "pickaxe", 0xFF3DB0FFu,
|
||||
"t1DemoTransparentAddressForUiSweep00000", true, true, true, true),
|
||||
grp("Cold storage", "diamond", 0xFFFF7BB0u,
|
||||
"zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", true, true, false, false),
|
||||
});
|
||||
settings_->setPortfolioStyle(0);
|
||||
}
|
||||
|
||||
seedChatDemoData();
|
||||
}
|
||||
|
||||
void App::applyHealthyDemoState()
|
||||
{
|
||||
state_.connected = true;
|
||||
state_.warming_up = false;
|
||||
state_.daemon_initializing = false;
|
||||
state_.encryption_state_known = true;
|
||||
state_.encrypted = false;
|
||||
state_.locked = false;
|
||||
state_.warmup_status.clear();
|
||||
state_.warmup_description.clear();
|
||||
// Dismiss any first-run wizard so the base tabs/modals aren't occluded (the wizard targets set
|
||||
// wizard_phase_ themselves and restore None on teardown).
|
||||
wizard_phase_ = WizardPhase::None;
|
||||
}
|
||||
|
||||
void App::clearDemoWalletData()
|
||||
{
|
||||
auto& s = sweep_state_snapshot_;
|
||||
if (!s.valid) return;
|
||||
state_.connected = s.connected; state_.warming_up = s.warming_up;
|
||||
state_.daemon_initializing = s.daemon_initializing;
|
||||
state_.encrypted = s.encrypted; state_.locked = s.locked;
|
||||
state_.encryption_state_known = s.encryption_state_known;
|
||||
state_.warmup_status = s.warmup_status; state_.warmup_description = s.warmup_description;
|
||||
state_.sync = s.sync;
|
||||
state_.privateBalance = s.privateBalance; state_.transparentBalance = s.transparentBalance;
|
||||
state_.totalBalance = s.totalBalance; state_.unconfirmedBalance = s.unconfirmedBalance;
|
||||
state_.addresses = s.addresses; state_.z_addresses = s.z_addresses; state_.t_addresses = s.t_addresses;
|
||||
state_.transactions = s.transactions;
|
||||
state_.market.price_usd = s.market_price_usd;
|
||||
state_.market.change_24h = s.market_change_24h;
|
||||
state_.market.price_history = g_marketHistorySnapshot;
|
||||
g_marketHistorySnapshot.clear();
|
||||
s = SweepStateSnapshot{}; // invalidate
|
||||
|
||||
// Restore the user's real portfolio (demo groups were in-memory only).
|
||||
if (g_pfSnapshotValid && settings_) {
|
||||
settings_->setPortfolioEntries(g_pfSnapshot);
|
||||
settings_->setPortfolioStyle(g_pfStyleSnapshot);
|
||||
}
|
||||
g_pfSnapshot.clear();
|
||||
g_pfSnapshotValid = false;
|
||||
}
|
||||
|
||||
// ── Catalog ──────────────────────────────────────────────────────────────────────────────────
|
||||
// Lambdas defined in this member function may touch App's private members through the App& arg.
|
||||
void App::buildSweepCatalog()
|
||||
{
|
||||
sweep_targets_.clear();
|
||||
|
||||
// Tabs (both sweeps).
|
||||
for (int p = 0; p < static_cast<int>(ui::NavPage::Count_); ++p) {
|
||||
ui::NavPage pg = static_cast<ui::NavPage>(p);
|
||||
if (sweepPageEnabled(pg)) sweep_targets_.push_back({ sweepPageName(pg), pg, nullptr, nullptr, 4 });
|
||||
}
|
||||
if (!sweep_full_) return;
|
||||
|
||||
// Full sweep: modals / flows / states. Blur overlays need more settle frames.
|
||||
const int kOverlaySettle = 8;
|
||||
auto add = [&](const char* name, ui::NavPage pg, std::function<void(App&)> setup,
|
||||
std::function<void(App&)> teardown) {
|
||||
sweep_targets_.push_back({ name, pg, std::move(setup), std::move(teardown), kOverlaySettle });
|
||||
};
|
||||
|
||||
// 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; });
|
||||
add("modal-export-key", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; });
|
||||
add("modal-backup", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
||||
add("modal-about", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
|
||||
add("modal-settings", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_settings_ = true; }, [](App& a) { a.show_settings_ = false; });
|
||||
|
||||
// Seed-backup: pre-set fetch + a demo phrase so it renders the word grid without any RPC.
|
||||
add("modal-seed-backup", ui::NavPage::Overview,
|
||||
[](App& a) {
|
||||
a.show_seed_backup_ = true; a.seed_backup_fetch_started_ = true;
|
||||
a.seed_backup_loading_ = false; a.seed_backup_no_mnemonic_ = false;
|
||||
a.seed_backup_status_.clear();
|
||||
if (a.seed_backup_phrase_.empty()) a.seed_backup_phrase_ = kDemoMnemonic;
|
||||
},
|
||||
[](App& a) {
|
||||
a.show_seed_backup_ = false; a.seed_backup_fetch_started_ = false;
|
||||
if (!a.seed_backup_phrase_.empty())
|
||||
sodium_memzero(&a.seed_backup_phrase_[0], a.seed_backup_phrase_.size());
|
||||
a.seed_backup_phrase_.clear();
|
||||
});
|
||||
|
||||
// Migrate-to-seed — one surface per step (full-node only). Setting the step directly never
|
||||
// fires the async create/sweep/adopt (those are button-triggered).
|
||||
if (supportsFullNodeLifecycleActions()) {
|
||||
auto mig = [&](const char* name, SeedMigrationStep step, std::function<void(App&)> extra) {
|
||||
add(name, ui::NavPage::Settings,
|
||||
[step, extra](App& a) {
|
||||
a.show_seed_migration_ = true; a.seed_migration_step_ = step;
|
||||
a.seed_migration_dest_ = kDemoZAddr;
|
||||
if (extra) extra(a);
|
||||
},
|
||||
[](App& a) {
|
||||
a.show_seed_migration_ = false;
|
||||
if (!a.seed_migration_seed_.empty())
|
||||
sodium_memzero(&a.seed_migration_seed_[0], a.seed_migration_seed_.size());
|
||||
a.seed_migration_seed_.clear(); a.seed_migration_status_.clear();
|
||||
});
|
||||
};
|
||||
// Intro pre-flight branches: pre-set the probe result so the sweep captures each variant
|
||||
// (the probe itself is guarded off during capture_mode_).
|
||||
mig("modal-migrate-intro", SeedMigrationStep::Intro, [](App& a) {
|
||||
a.seed_migration_precheck_ = SeedMigrationPrecheck::Legacy;
|
||||
a.seed_migration_precheck_started_ = true;
|
||||
});
|
||||
mig("modal-migrate-intro-seeded", SeedMigrationStep::Intro, [](App& a) {
|
||||
a.seed_migration_precheck_ = SeedMigrationPrecheck::AlreadyMnemonic;
|
||||
a.seed_migration_precheck_started_ = true;
|
||||
});
|
||||
mig("modal-migrate-intro-oldnode", SeedMigrationStep::Intro, [](App& a) {
|
||||
a.seed_migration_precheck_ = SeedMigrationPrecheck::DaemonTooOld;
|
||||
a.seed_migration_precheck_started_ = true;
|
||||
});
|
||||
mig("modal-migrate-showseed", SeedMigrationStep::ShowSeed,
|
||||
[](App& a) { a.seed_migration_seed_ = kDemoMnemonic; a.seed_migration_backed_up_ = false; });
|
||||
mig("modal-migrate-sweep", SeedMigrationStep::Sweep,
|
||||
[](App& a) { a.seed_migration_balance_ = 15.75000000; a.seed_migration_balance_loaded_ = true; });
|
||||
mig("modal-migrate-nofunds", SeedMigrationStep::Sweep,
|
||||
[](App& a) { a.seed_migration_balance_ = 0.0; a.seed_migration_balance_loaded_ = true; });
|
||||
mig("modal-migrate-confirming", SeedMigrationStep::Confirming, [](App& a) {
|
||||
a.seed_migration_sweep_confs_ = 1; a.seed_migration_sweep_txid_ = kDemoTxid;
|
||||
a.seed_migration_legacy_remaining_ = 0.0;
|
||||
});
|
||||
mig("modal-migrate-done", SeedMigrationStep::Done, nullptr);
|
||||
mig("modal-migrate-error", SeedMigrationStep::Error,
|
||||
[](App& a) { a.seed_migration_status_ = "Example error: the daemon did not respond."; });
|
||||
|
||||
// Multi-wallet: the wallet-files list. Drop a couple of demo wallet files in the (throwaway)
|
||||
// datadir + cache their metadata so the list renders populated.
|
||||
add("modal-wallets", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
std::error_code ec;
|
||||
const std::string dd = util::Platform::getDragonXDataDir();
|
||||
fs::create_directories(dd, ec);
|
||||
if (!fs::exists(dd + "/wallet.dat", ec))
|
||||
std::ofstream(dd + "/wallet.dat", std::ios::binary) << std::string(122880, '\0');
|
||||
if (!fs::exists(dd + "/wallet-savings.dat", ec))
|
||||
std::ofstream(dd + "/wallet-savings.dat", std::ios::binary) << std::string(65536, '\0');
|
||||
data::WalletIndexEntry e1; e1.fileName = "wallet.dat"; e1.displayName = "wallet.dat";
|
||||
e1.cachedBalance = 15.7526; e1.cachedAddressCount = 4;
|
||||
e1.lastOpenedEpoch = 1720000000; e1.syncedHere = true;
|
||||
data::WalletIndexEntry e2; e2.fileName = "wallet-savings.dat"; e2.displayName = "wallet-savings.dat";
|
||||
e2.cachedBalance = 250.0; e2.cachedAddressCount = 2;
|
||||
e2.lastOpenedEpoch = 1719400000; e2.syncedHere = true;
|
||||
a.walletIndex().upsert(e1);
|
||||
a.walletIndex().upsert(e2);
|
||||
// An out-of-datadir wallet (in an extra folder) so the Import action shows too.
|
||||
const std::string extra = util::Platform::getConfigDir() + "extra-wallets";
|
||||
fs::create_directories(extra, ec);
|
||||
if (!fs::exists(extra + "/my-wallet.dat", ec))
|
||||
std::ofstream(extra + "/my-wallet.dat", std::ios::binary) << std::string(80000, '\0');
|
||||
a.walletIndex().addExtraFolder(extra);
|
||||
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
|
||||
ui::WalletsDialog::show(&a);
|
||||
},
|
||||
[](App&) { ui::WalletsDialog::hide(); });
|
||||
|
||||
// Many wallets — exercises the viewport-cap path: the card can't fit all rows, so the list
|
||||
// must scroll internally while the create/scan/footer controls stay pinned (esp. at 150%).
|
||||
// Teardown removes the extra files it dropped so the plain modal-wallets surface stays a
|
||||
// clean 3-wallet reference (both surfaces share the one throwaway datadir).
|
||||
static const char* kManyExtra[] = {"wallet-cold.dat", "wallet-trading.dat", "wallet-mining.dat",
|
||||
"wallet-donations.dat", "wallet-2023.dat", "wallet-payroll.dat"};
|
||||
add("modal-wallets-many", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
std::error_code ec;
|
||||
const std::string dd = util::Platform::getDragonXDataDir();
|
||||
fs::create_directories(dd, ec);
|
||||
const char* names[] = {"wallet.dat", "wallet-savings.dat", "wallet-cold.dat",
|
||||
"wallet-trading.dat", "wallet-mining.dat", "wallet-donations.dat",
|
||||
"wallet-2023.dat", "wallet-payroll.dat"};
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (!fs::exists(dd + "/" + names[i], ec))
|
||||
std::ofstream(dd + "/" + names[i], std::ios::binary) << std::string(64000 + i * 4096, '\0');
|
||||
data::WalletIndexEntry e; e.fileName = names[i]; e.displayName = names[i];
|
||||
e.cachedBalance = 5.0 * (i + 1); e.cachedAddressCount = 2 + i;
|
||||
e.lastOpenedEpoch = 1720000000 - i * 86400; e.syncedHere = true;
|
||||
a.walletIndex().upsert(e);
|
||||
}
|
||||
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
|
||||
ui::WalletsDialog::show(&a);
|
||||
},
|
||||
[](App& a) {
|
||||
ui::WalletsDialog::hide();
|
||||
std::error_code ec;
|
||||
const std::string dd = util::Platform::getDragonXDataDir();
|
||||
for (const char* n : kManyExtra) fs::remove(dd + "/" + n, ec);
|
||||
});
|
||||
}
|
||||
|
||||
// First-run wizard — one per meaningful phase (full-node only; blocks all other UI while shown).
|
||||
if (!isLiteBuild()) {
|
||||
auto wiz = [&](const char* name, WizardPhase phase) {
|
||||
add(name, ui::NavPage::Overview,
|
||||
[phase](App& a) { a.wizard_phase_ = phase; },
|
||||
[](App& a) { a.wizard_phase_ = WizardPhase::None; });
|
||||
};
|
||||
wiz("wizard-appearance", WizardPhase::Appearance);
|
||||
wiz("wizard-bootstrap", WizardPhase::BootstrapOffer);
|
||||
wiz("wizard-encrypt", WizardPhase::EncryptOffer);
|
||||
wiz("wizard-pin", WizardPhase::PinSetup);
|
||||
}
|
||||
|
||||
// App-state overlays. Teardown restores the healthy demo flags (full restore at sweep end).
|
||||
add("overlay-lock", ui::NavPage::Overview,
|
||||
[](App& a) { a.state_.encrypted = true; a.state_.locked = true; },
|
||||
[](App& a) { a.applyHealthyDemoState(); });
|
||||
add("overlay-warmup", ui::NavPage::Overview,
|
||||
[](App& a) {
|
||||
a.state_.warming_up = true;
|
||||
a.state_.warmup_status = "Processing blocks…";
|
||||
a.state_.warmup_description = "The node is loading the block index.";
|
||||
},
|
||||
[](App& a) { a.applyHealthyDemoState(); });
|
||||
add("overlay-not-ready", ui::NavPage::Overview,
|
||||
[](App& a) { a.state_.connected = false; a.state_.encryption_state_known = false; },
|
||||
[](App& a) { a.applyHealthyDemoState(); });
|
||||
|
||||
// Send-confirm popup (state lives in send_tab statics → driven via a debug hook).
|
||||
add("popup-send-confirm", ui::NavPage::Send,
|
||||
[](App& a) { ui::SweepShowSendConfirm(&a, true); },
|
||||
[](App& a) { ui::SweepShowSendConfirm(&a, false); });
|
||||
|
||||
// Market portfolio row styles — capture all three so the redesign is reviewable per skin.
|
||||
add("market-rows-compact", ui::NavPage::Market,
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); },
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||
add("market-rows-detailed", ui::NavPage::Market,
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(1); },
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||
add("market-rows-featured", ui::NavPage::Market,
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(2); },
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||
|
||||
// Console RPC command-reference popup (fixed-height dialog with a fill-height command list).
|
||||
add("modal-console-commands", ui::NavPage::Console,
|
||||
[](App& a) { a.console_tab_.sweepSetCommandsPopup(true); },
|
||||
[](App& a) { a.console_tab_.sweepSetCommandsPopup(false); });
|
||||
|
||||
// Daemon updater — the two-pane version picker (versions left, selected-version detail right).
|
||||
// Seeds fake releases so the offline sweep renders it without a network fetch / live updater.
|
||||
add("modal-daemon-update", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
|
||||
util::DaemonRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
|
||||
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
|
||||
util::DaemonReleaseAsset as;
|
||||
as.name = std::string("dragonx-") + tag + "-linux-amd64.zip";
|
||||
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 43000000;
|
||||
r.assets.push_back(as);
|
||||
return r;
|
||||
};
|
||||
std::vector<util::DaemonRelease> rels;
|
||||
rels.push_back(mk("v1.0.4", "DragonX v1.0.4", "2026-06-28",
|
||||
"## What is DragonX?\n\n"
|
||||
"DragonX is a privacy-focused cryptocurrency built on zero-knowledge mathematics. "
|
||||
"It enforces mandatory z2z (shielded-to-shielded) transactions after block 340,000.\n\n"
|
||||
"## Bug Fixes\n"
|
||||
"* Fix sapling pool persistence \xE2\x80\x94 pool total no longer resets to 0 on restart\n"
|
||||
"* Add `subsidy` and `fees` fields to the `getblock` RPC response\n\n"
|
||||
"## Key Features\n"
|
||||
"* **RandomX Proof-of-Work** \xE2\x80\x94 CPU-mineable, ASIC-resistant\n"
|
||||
"* **Sapling zk-SNARKs** \xE2\x80\x94 zero-knowledge proofs for private transactions\n"
|
||||
"* **Encrypted P2P** \xE2\x80\x94 all connections secured with TLS 1.3 via WolfSSL\n\n"
|
||||
"## Checksums\n| File | SHA-256 |\n"
|
||||
"|---|---|\n| dragonx-v1.0.4-linux-amd64.zip | `ab12cd34` |\n", false));
|
||||
rels.push_back(mk("v1.0.3", "DragonX v1.0.3", "2026-05-14",
|
||||
"## Notes\n- Stability improvements\n- RPC fixes\n", false));
|
||||
rels.push_back(mk("v1.0.2", "DragonX v1.0.2", "2026-04-02",
|
||||
"- First tagged mainnet build\n", false));
|
||||
rels.push_back(mk("v1.1.0-rc1", "DragonX v1.1.0-rc1", "2026-07-01",
|
||||
"Release candidate for the 1.1.0 series. Testing only.\n", true));
|
||||
ui::DaemonUpdateDialog::sweepSeed(&a, rels, util::DaemonUpdater::State::ReleaseList,
|
||||
"v1.0.3-dc45e7d90");
|
||||
},
|
||||
[](App&) { ui::DaemonUpdateDialog::sweepClose(); });
|
||||
|
||||
// Miner (xmrig) updater — same two-pane version picker, seeded with fake releases for the sweep.
|
||||
add("modal-xmrig-update", ui::NavPage::Mining,
|
||||
[](App& a) {
|
||||
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
|
||||
util::XmrigRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
|
||||
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
|
||||
util::XmrigReleaseAsset as;
|
||||
as.name = std::string("drg-xmrig-") + tag + "-linux-x64.zip";
|
||||
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 8000000;
|
||||
r.assets.push_back(as);
|
||||
return r;
|
||||
};
|
||||
std::vector<util::XmrigRelease> rels;
|
||||
rels.push_back(mk("v6.25.3", "DRG-XMRig v6.25.3", "2026-06-20",
|
||||
"## What's new\n- Rebased on upstream XMRig 6.25.3\n- **RandomX** JIT speedups on modern CPUs\n"
|
||||
"- Fix `--cpu-priority` parsing on Windows\n\n## Checksums\n| File | SHA-256 |\n"
|
||||
"|---|---|\n| drg-xmrig-v6.25.3-linux-x64.zip | `ab12cd34` |\n", false));
|
||||
rels.push_back(mk("v6.24.0", "DRG-XMRig v6.24.0", "2026-04-30",
|
||||
"## Notes\n- Pool TLS fixes\n- Lower idle CPU\n", false));
|
||||
rels.push_back(mk("v6.23.0", "DRG-XMRig v6.23.0", "2026-03-11",
|
||||
"- First DRG-XMRig build\n", false));
|
||||
rels.push_back(mk("v6.26.0-rc1", "DRG-XMRig v6.26.0-rc1", "2026-07-02",
|
||||
"Release candidate. **Testing only.**\n", true));
|
||||
ui::XmrigDownloadDialog::sweepSeed(&a, rels, util::XmrigUpdater::State::ReleaseList, "v6.24.0");
|
||||
},
|
||||
[](App&) { ui::XmrigDownloadDialog::sweepClose(); });
|
||||
|
||||
// In-app folder picker (Wallets → Scan another folder). Populate a small demo tree so the
|
||||
// list shows sub-folders + wallet files, then open the wallets dialog + the picker over it.
|
||||
add("modal-folder-picker", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
std::error_code ec;
|
||||
const std::string demo = util::Platform::getConfigDir() + "picker-demo";
|
||||
for (const char* sub : {"Documents", "Downloads", "Backups", "wallet-archive"})
|
||||
fs::create_directories(demo + "/" + sub, ec);
|
||||
for (const char* f : {"wallet-cold.dat", "wallet-2023.dat"})
|
||||
if (!fs::exists(demo + "/" + f, ec))
|
||||
std::ofstream(demo + "/" + f, std::ios::binary) << std::string(66000, '\0');
|
||||
ui::WalletsDialog::show(&a);
|
||||
ui::FolderPicker::open(demo, [](const std::string&) {});
|
||||
},
|
||||
[](App&) { ui::FolderPicker::close(); ui::WalletsDialog::hide(); });
|
||||
}
|
||||
|
||||
// ── State machine ───────────────────────────────────────────────────────────────────────────
|
||||
void App::startSweepImpl(bool full)
|
||||
{
|
||||
if (screenshot_sweep_active_) return;
|
||||
|
||||
sweep_skins_.clear();
|
||||
for (const auto& sk : ui::schema::SkinManager::instance().available())
|
||||
if (sk.valid) sweep_skins_.push_back(sk.id);
|
||||
if (sweep_skins_.empty()) return;
|
||||
|
||||
sweep_full_ = full;
|
||||
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
||||
buildSweepCatalog();
|
||||
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
|
||||
|
||||
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
|
||||
std::error_code ec; fs::create_directories(sweep_dir_, ec);
|
||||
|
||||
sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId();
|
||||
sweep_saved_page_ = current_page_;
|
||||
sweep_skin_idx_ = 0; sweep_target_idx_ = 0;
|
||||
screenshot_sweep_active_ = true;
|
||||
sweep_capture_this_frame_ = false;
|
||||
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]);
|
||||
applySweepTarget();
|
||||
ui::Notifications::instance().info(full ? "Full UI sweep running…" : "Screenshot sweep running…");
|
||||
DEBUG_LOGF("[Sweep] %s -> %s (%d skins x %d surfaces)\n", full ? "FULL" : "tabs",
|
||||
sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_targets_.size());
|
||||
}
|
||||
|
||||
void App::startScreenshotSweep() { startSweepImpl(false); }
|
||||
void App::startFullUiSweep() { startSweepImpl(true); }
|
||||
|
||||
void App::applySweepTarget()
|
||||
{
|
||||
const SweepTarget& t = sweep_targets_[sweep_target_idx_];
|
||||
current_page_ = t.page;
|
||||
page_alpha_ = 1.0f; // skip the page-switch fade so the shot isn't captured mid-animation
|
||||
if (t.setup) t.setup(*this);
|
||||
// <dir>/<surface>/<skin>.png — one subfolder per surface, one PNG per theme, overwritten in
|
||||
// place next sweep. (writePng in main.cpp creates the parent subfolder.)
|
||||
sweep_current_path_ = (fs::path(sweep_dir_) / t.name / (sweep_skins_[sweep_skin_idx_] + ".png")).string();
|
||||
sweep_settle_frames_ = t.settle;
|
||||
sweep_capture_this_frame_ = false;
|
||||
}
|
||||
|
||||
void App::updateScreenshotSweep()
|
||||
{
|
||||
if (!screenshot_sweep_active_) return;
|
||||
const SweepTarget& t = sweep_targets_[sweep_target_idx_];
|
||||
current_page_ = t.page; // keep the surface pinned each frame
|
||||
page_alpha_ = 1.0f;
|
||||
// Re-run setup every frame: idempotent for flags/enums, and required for OpenPopup-based popups
|
||||
// (must re-fire while open) + to keep the surface state fixed against any refresh.
|
||||
if (t.setup) t.setup(*this);
|
||||
if (sweep_settle_frames_ > 0) { sweep_settle_frames_--; sweep_capture_this_frame_ = false; }
|
||||
else sweep_capture_this_frame_ = true; // settled — main.cpp captures this frame
|
||||
}
|
||||
|
||||
void App::onScreenshotCaptured()
|
||||
{
|
||||
sweep_capture_this_frame_ = false;
|
||||
if (!screenshot_sweep_active_) return;
|
||||
|
||||
{ const SweepTarget& t = sweep_targets_[sweep_target_idx_]; if (t.teardown) t.teardown(*this); }
|
||||
|
||||
// Some surfaces leave an ImGui popup open (e.g. the send-confirm dialog calls OpenPopup but is
|
||||
// torn down by just clearing its flag). The headless sweep never clicks, so ImGui's normal
|
||||
// click-to-dismiss cleanup never runs and the popup lingers on the stack — which keeps
|
||||
// IsPopupOpen(AnyPopup) true forever and disables the smooth-scroll wheel capture on
|
||||
// Settings/Explorer/Console (draw_helpers::ApplySmoothScroll). Flush any lingering popup here so
|
||||
// it can't bleed into the next surface's capture or survive past the sweep.
|
||||
if (ImGuiContext* g = ImGui::GetCurrentContext(); g && g->OpenPopupStack.Size > 0)
|
||||
ImGui::ClosePopupToLevel(0, false);
|
||||
|
||||
if (++sweep_target_idx_ >= static_cast<int>(sweep_targets_.size())) {
|
||||
sweep_target_idx_ = 0;
|
||||
if (++sweep_skin_idx_ >= static_cast<int>(sweep_skins_.size())) {
|
||||
// Done — restore the original skin + page, and (full) the real state.
|
||||
const bool wasFull = sweep_full_;
|
||||
if (wasFull) writeSweepManifest();
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_);
|
||||
current_page_ = sweep_saved_page_;
|
||||
page_alpha_ = 1.0f;
|
||||
if (wasFull) { clearDemoWalletData(); capture_mode_ = false; }
|
||||
sweep_full_ = false;
|
||||
screenshot_sweep_active_ = false;
|
||||
ui::Notifications::instance().success(
|
||||
(wasFull ? std::string("Full UI screenshots saved to ") : std::string("Screenshots saved to ")) + sweep_dir_);
|
||||
DEBUG_LOGF("[Sweep] done -> %s\n", sweep_dir_.c_str());
|
||||
return;
|
||||
}
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[sweep_skin_idx_]);
|
||||
}
|
||||
applySweepTarget();
|
||||
}
|
||||
|
||||
void App::writeSweepManifest() const
|
||||
{
|
||||
std::error_code ec; fs::create_directories(sweep_dir_, ec);
|
||||
std::ofstream out((fs::path(sweep_dir_) / "index.md").string(), std::ios::trunc);
|
||||
if (!out) return;
|
||||
out << "# Full UI sweep\n\n";
|
||||
out << sweep_targets_.size() << " surfaces x " << sweep_skins_.size() << " skins\n\n";
|
||||
for (const auto& t : sweep_targets_) {
|
||||
out << "## " << t.name << " (base: " << sweepPageName(t.page) << ")\n\n";
|
||||
for (const auto& skin : sweep_skins_)
|
||||
out << "- " << skin << ": `" << t.name << "/" << skin << ".png`\n";
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dragonx
|
||||
@@ -172,6 +172,8 @@ bool Settings::load(const std::string& path)
|
||||
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
|
||||
}
|
||||
}
|
||||
loadScalar(j, "portfolio_style", portfolio_style_);
|
||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
||||
hidden_addresses_.clear();
|
||||
@@ -200,6 +202,8 @@ bool Settings::load(const std::string& path)
|
||||
}
|
||||
loadScalar(j, "wizard_completed", wizard_completed_);
|
||||
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
||||
loadScalar(j, "daemon_update_prompted_size", daemon_update_prompted_size_);
|
||||
loadScalar(j, "active_wallet_file", active_wallet_file_);
|
||||
loadScalar(j, "seed_migration_pending", seed_migration_pending_);
|
||||
loadScalar(j, "seed_migration_dest", seed_migration_dest_);
|
||||
loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
|
||||
@@ -342,6 +346,7 @@ bool Settings::load(const std::string& path)
|
||||
entry.showSparkline = e["show_sparkline"].get<bool>();
|
||||
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
|
||||
entry.sparklineInterval = e["sparkline_interval"].get<int>();
|
||||
entry.scope = e.value("scope", "");
|
||||
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
|
||||
entry.gridCol = e["grid_col"].get<int>();
|
||||
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
||||
@@ -416,6 +421,7 @@ bool Settings::save(const std::string& path)
|
||||
j["ui_opacity"] = ui_opacity_;
|
||||
j["window_opacity"] = window_opacity_;
|
||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||
j["portfolio_style"] = portfolio_style_;
|
||||
j["scanline_enabled"] = scanline_enabled_;
|
||||
j["hidden_addresses"] = json::array();
|
||||
for (const auto& addr : hidden_addresses_)
|
||||
@@ -438,6 +444,8 @@ bool Settings::save(const std::string& path)
|
||||
}
|
||||
j["wizard_completed"] = wizard_completed_;
|
||||
j["seed_backup_reminded"] = seed_backup_reminded_;
|
||||
j["daemon_update_prompted_size"] = daemon_update_prompted_size_;
|
||||
j["active_wallet_file"] = active_wallet_file_;
|
||||
j["seed_migration_pending"] = seed_migration_pending_;
|
||||
j["seed_migration_dest"] = seed_migration_dest_;
|
||||
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
|
||||
@@ -516,6 +524,7 @@ bool Settings::save(const std::string& path)
|
||||
entry["show_24h"] = e.show24h;
|
||||
entry["show_sparkline"] = e.showSparkline;
|
||||
entry["sparkline_interval"] = e.sparklineInterval;
|
||||
entry["scope"] = e.scope;
|
||||
entry["grid_col"] = e.gridCol;
|
||||
entry["grid_row"] = e.gridRow;
|
||||
entry["grid_w"] = e.gridW;
|
||||
|
||||
@@ -92,12 +92,16 @@ public:
|
||||
bool show24h = false; // show the 24h % change (live-market bases only)
|
||||
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
|
||||
int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history)
|
||||
// Dashboard grid placement on the Market portfolio. col/row in fine ~32px square cells
|
||||
// (-1 = auto-place), w/h in cells (span). Set when the user drags/resizes a card.
|
||||
// Per-wallet visibility: "" (shown in every wallet — legacy/global) or a wallet-identity
|
||||
// hash (shown only when that wallet is active). New entries are tagged with the current
|
||||
// wallet so a portfolio built for wallet A doesn't clutter wallet B.
|
||||
std::string scope;
|
||||
// Legacy dashboard-grid placement (deprecated by the row layout; kept for back-compat so an
|
||||
// older build's saved positions aren't dropped, but no longer used by the renderer).
|
||||
int gridCol = -1;
|
||||
int gridRow = -1;
|
||||
int gridW = 8; // default group width (cells); minimum is 8
|
||||
int gridH = 3; // default group height (cells); minimum is 2, card design adapts
|
||||
int gridW = 8;
|
||||
int gridH = 3;
|
||||
};
|
||||
|
||||
// Theme
|
||||
@@ -176,6 +180,11 @@ public:
|
||||
std::string getBalanceLayout() const { return balance_layout_; }
|
||||
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
|
||||
|
||||
// Market-tab portfolio row style: 0 = single-line, 1 = two-line, 2 = value-hero. Cycled with
|
||||
// Left/Right arrows on the Market tab (like the Overview layouts).
|
||||
int getPortfolioStyle() const { return portfolio_style_; }
|
||||
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
|
||||
// Console scanline effect
|
||||
bool getScanlineEnabled() const { return scanline_enabled_; }
|
||||
void setScanlineEnabled(bool v) { scanline_enabled_ = v; }
|
||||
@@ -250,6 +259,17 @@ public:
|
||||
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
|
||||
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
|
||||
|
||||
// Bundled-daemon size we last prompted to install (see App::renderDaemonUpdatePrompt). Lets the
|
||||
// "a newer node is bundled — update?" prompt fire once per wallet version, never re-nagging.
|
||||
long long getDaemonUpdatePromptedSize() const { return daemon_update_prompted_size_; }
|
||||
void setDaemonUpdatePromptedSize(long long v) { daemon_update_prompted_size_ = v; }
|
||||
|
||||
// Active wallet file the daemon loads via -wallet=<name> (multi-wallet). A plain filename in
|
||||
// the datadir; defaults to the daemon's own default. Used at launch to know which wallet we're
|
||||
// on before connect + to scope per-wallet data.
|
||||
std::string getActiveWalletFile() const { return active_wallet_file_; }
|
||||
void setActiveWalletFile(const std::string& v) { active_wallet_file_ = v; }
|
||||
|
||||
// Pending "migrate to a seed wallet" state (Phase 1 created the wallet; a later sweep/adopt
|
||||
// step consumes it). dest = the new wallet's sweep-target z-address; tempDir = its datadir.
|
||||
bool getSeedMigrationPending() const { return seed_migration_pending_; }
|
||||
@@ -456,12 +476,15 @@ private:
|
||||
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||
#endif
|
||||
std::string balance_layout_ = "classic";
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
||||
bool scanline_enabled_ = true;
|
||||
std::set<std::string> hidden_addresses_;
|
||||
std::set<std::string> favorite_addresses_;
|
||||
std::map<std::string, AddressMeta> address_meta_;
|
||||
bool wizard_completed_ = false;
|
||||
bool seed_backup_reminded_ = false;
|
||||
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
|
||||
std::string active_wallet_file_ = "wallet.dat"; // -wallet=<name> the daemon loads (multi-wallet)
|
||||
bool seed_migration_pending_ = false;
|
||||
std::string seed_migration_dest_;
|
||||
std::string seed_migration_temp_dir_;
|
||||
|
||||
@@ -23,6 +23,7 @@ void DaemonController::syncSettings(const config::Settings* settings)
|
||||
if (!settings) return;
|
||||
daemon_->setDebugCategories(settings->getDebugCategories());
|
||||
daemon_->setMaxConnections(settings->getMaxConnections());
|
||||
daemon_->setWalletFile(settings->getActiveWalletFile());
|
||||
}
|
||||
|
||||
bool DaemonController::start(const config::Settings* settings)
|
||||
|
||||
@@ -488,6 +488,14 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
||||
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
||||
}
|
||||
|
||||
// Active wallet file (multi-wallet). The daemon loads <datadir>/<name>. Only pass it for a
|
||||
// non-default name so the common case's command line is unchanged; skip during an isolated
|
||||
// start (seed migration manages its own throwaway wallet).
|
||||
if (!wallet_file_.empty() && wallet_file_ != "wallet.dat" && override_datadir_.empty()) {
|
||||
DEBUG_LOGF("[INFO] Loading wallet file: %s\n", wallet_file_.c_str());
|
||||
args.push_back("-wallet=" + wallet_file_);
|
||||
}
|
||||
|
||||
// Add wallet-repair flag if requested (one-shot). -zapwallettxes=2 wipes all wallet tx/note
|
||||
// records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan.
|
||||
if (zap_on_next_start_.exchange(false)) {
|
||||
|
||||
@@ -183,6 +183,12 @@ public:
|
||||
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
|
||||
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
|
||||
|
||||
// Active wallet file (multi-wallet). Passed to the daemon as -wallet=<name> so it loads
|
||||
// <datadir>/<name>; empty or "wallet.dat" keeps the daemon default (no arg). Must be a plain
|
||||
// filename in the datadir — the daemon rejects paths.
|
||||
void setWalletFile(const std::string& v) { wallet_file_ = v; }
|
||||
std::string walletFile() const { return wallet_file_; }
|
||||
|
||||
/**
|
||||
* @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all
|
||||
* wallet transaction/note records and rebuilds them from the chain (keys are kept); the
|
||||
@@ -251,6 +257,7 @@ private:
|
||||
std::atomic<bool> should_stop_{false};
|
||||
std::set<std::string> debug_categories_;
|
||||
int max_connections_ = 0; // 0 = daemon default
|
||||
std::string wallet_file_; // -wallet=<name> for the active wallet; empty/"wallet.dat" = default
|
||||
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
||||
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
||||
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
|
||||
|
||||
@@ -117,7 +117,19 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
|
||||
r.ok = !r.seedPhrase.empty() && !r.destAddress.empty();
|
||||
if (!r.ok) r.error = "The isolated node returned an empty seed or address.";
|
||||
} catch (const std::exception& e) {
|
||||
r.error = std::string("Seed export failed: ") + e.what();
|
||||
const std::string what = e.what();
|
||||
// "Method not found" (JSON-RPC -32601) means this dragonxd predates mnemonic support —
|
||||
// it has no z_exportmnemonic RPC (the older bundled binary). Migrate-to-seed can't work
|
||||
// until the daemon is updated, so give an actionable message, not the raw RPC error.
|
||||
if (what.find("Method not found") != std::string::npos ||
|
||||
what.find("-32601") != std::string::npos) {
|
||||
r.error = "This DragonX daemon is too old to create a seed wallet — it lacks mnemonic "
|
||||
"support (the z_exportmnemonic RPC). Update to the latest DragonX daemon "
|
||||
"(Settings -> NODE & SECURITY -> Check for updates, or Install bundled), then "
|
||||
"try again.";
|
||||
} else {
|
||||
r.error = std::string("Seed export failed: ") + what;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
|
||||
|
||||
@@ -51,7 +51,10 @@ bool AddressBook::load()
|
||||
e.label = entry.value("label", "");
|
||||
e.address = entry.value("address", "");
|
||||
e.notes = entry.value("notes", "");
|
||||
|
||||
// Legacy entries (no "scope") migrate to "global" so nothing disappears when
|
||||
// multi-wallet scoping lands — a contact you already had stays visible everywhere.
|
||||
e.scope = entry.value("scope", "global");
|
||||
|
||||
if (!e.address.empty()) {
|
||||
entries_.push_back(e);
|
||||
}
|
||||
@@ -82,6 +85,7 @@ bool AddressBook::save()
|
||||
e["label"] = entry.label;
|
||||
e["address"] = entry.address;
|
||||
e["notes"] = entry.notes;
|
||||
e["scope"] = entry.scope.empty() ? std::string("global") : entry.scope;
|
||||
j["entries"].push_back(e);
|
||||
}
|
||||
|
||||
@@ -103,11 +107,12 @@ bool AddressBook::save()
|
||||
|
||||
bool AddressBook::addEntry(const AddressBookEntry& entry)
|
||||
{
|
||||
// Check for duplicate address
|
||||
if (findByAddress(entry.address) >= 0) {
|
||||
// Reject a duplicate only within the same visible set (same wallet or global) — the same
|
||||
// address may legitimately be a contact in two different wallets.
|
||||
if (hasVisibleDuplicate(entry.address, entry.scope)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
entries_.push_back(entry);
|
||||
return save();
|
||||
}
|
||||
@@ -118,12 +123,11 @@ bool AddressBook::updateEntry(size_t index, const AddressBookEntry& entry)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate address (excluding current entry)
|
||||
int existing = findByAddress(entry.address);
|
||||
if (existing >= 0 && static_cast<size_t>(existing) != index) {
|
||||
// Check for a duplicate visible alongside this entry's scope (excluding the entry being edited)
|
||||
if (hasVisibleDuplicate(entry.address, entry.scope, static_cast<int>(index))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
entries_[index] = entry;
|
||||
return save();
|
||||
}
|
||||
@@ -148,5 +152,19 @@ int AddressBook::findByAddress(const std::string& address) const
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool AddressBook::hasVisibleDuplicate(const std::string& address, const std::string& scope,
|
||||
int excludeIndex) const
|
||||
{
|
||||
AddressBookEntry probe; probe.scope = scope; // reuse the isGlobal()/scope logic
|
||||
for (size_t i = 0; i < entries_.size(); i++) {
|
||||
if (static_cast<int>(i) == excludeIndex) continue;
|
||||
const auto& e = entries_[i];
|
||||
if (e.address != address) continue;
|
||||
// Collides if they'd ever be shown together: same wallet scope, or either is global.
|
||||
if (e.isGlobal() || probe.isGlobal() || e.scope == scope) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -17,10 +17,21 @@ struct AddressBookEntry {
|
||||
std::string label;
|
||||
std::string address;
|
||||
std::string notes;
|
||||
|
||||
// Per-wallet visibility scope: "global" (shown in every wallet) or a wallet-identity hash
|
||||
// (shown only when that wallet is the active one). Empty is treated as "global" so legacy
|
||||
// entries — written before multi-wallet scoping — keep showing everywhere.
|
||||
std::string scope = "global";
|
||||
|
||||
AddressBookEntry() = default;
|
||||
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "")
|
||||
: label(l), address(a), notes(n) {}
|
||||
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "",
|
||||
const std::string& s = "global")
|
||||
: label(l), address(a), notes(n), scope(s) {}
|
||||
|
||||
bool isGlobal() const { return scope.empty() || scope == "global"; }
|
||||
// Visible under the wallet whose identity hash is walletHash (or globally).
|
||||
bool visibleInWallet(const std::string& walletHash) const {
|
||||
return isGlobal() || scope == walletHash;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,12 +84,21 @@ public:
|
||||
bool removeEntry(size_t index);
|
||||
|
||||
/**
|
||||
* @brief Find entry by address
|
||||
* @brief Find entry by address (any scope). Used for contact-label lookups.
|
||||
* @param address Address to search for
|
||||
* @return Index or -1 if not found
|
||||
*/
|
||||
int findByAddress(const std::string& address) const;
|
||||
|
||||
/**
|
||||
* @brief Is there an existing entry with this address that would be visible ALONGSIDE a new
|
||||
* entry of the given scope? (Same wallet, or either side global.) Used to reject
|
||||
* duplicates within a wallet while still allowing the same address in different wallets.
|
||||
* @param excludeIndex Storage index to skip (for edit), or -1.
|
||||
*/
|
||||
bool hasVisibleDuplicate(const std::string& address, const std::string& scope,
|
||||
int excludeIndex = -1) const;
|
||||
|
||||
/**
|
||||
* @brief Get all entries
|
||||
*/
|
||||
|
||||
159
src/data/wallet_index.cpp
Normal file
159
src/data/wallet_index.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "wallet_index.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "../util/logger.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace dragonx {
|
||||
namespace data {
|
||||
|
||||
namespace {
|
||||
// A field-wise "did anything change" check so we only rewrite wallets.json when needed.
|
||||
bool sameEntry(const WalletIndexEntry& a, const WalletIndexEntry& b)
|
||||
{
|
||||
return a.fileName == b.fileName
|
||||
&& a.displayName == b.displayName
|
||||
&& a.walletIdentityHash == b.walletIdentityHash
|
||||
&& a.cachedBalance == b.cachedBalance
|
||||
&& a.cachedAddressCount == b.cachedAddressCount
|
||||
&& a.lastOpenedEpoch == b.lastOpenedEpoch
|
||||
&& a.sizeBytesAtLastOpen == b.sizeBytesAtLastOpen
|
||||
&& a.syncedHere == b.syncedHere;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string WalletIndex::getDefaultPath()
|
||||
{
|
||||
// Co-located with settings.json / addressbook.json in the per-variant config dir.
|
||||
const std::string dir = util::Platform::getConfigDir();
|
||||
fs::create_directories(dir);
|
||||
return (fs::path(dir) / "wallets.json").string();
|
||||
}
|
||||
|
||||
bool WalletIndex::load()
|
||||
{
|
||||
file_path_ = getDefaultPath();
|
||||
entries_.clear();
|
||||
extra_folders_.clear();
|
||||
|
||||
std::ifstream file(file_path_);
|
||||
if (!file.is_open()) return true; // no file yet is fine
|
||||
|
||||
try {
|
||||
json j;
|
||||
file >> j;
|
||||
|
||||
if (j.contains("entries") && j["entries"].is_array()) {
|
||||
for (const auto& e : j["entries"]) {
|
||||
WalletIndexEntry w;
|
||||
w.fileName = e.value("file", "");
|
||||
if (w.fileName.empty()) continue;
|
||||
w.displayName = e.value("name", w.fileName);
|
||||
w.walletIdentityHash = e.value("identity", "");
|
||||
w.cachedBalance = e.value("balance", -1.0);
|
||||
w.cachedAddressCount = e.value("addresses", (long long)-1);
|
||||
w.lastOpenedEpoch = e.value("lastOpened", (long long)0);
|
||||
w.sizeBytesAtLastOpen = e.value("size", (long long)0);
|
||||
w.syncedHere = e.value("syncedHere", false);
|
||||
entries_.push_back(std::move(w));
|
||||
}
|
||||
}
|
||||
if (j.contains("extraFolders") && j["extraFolders"].is_array()) {
|
||||
for (const auto& d : j["extraFolders"]) {
|
||||
if (d.is_string() && !d.get<std::string>().empty())
|
||||
extra_folders_.push_back(d.get<std::string>());
|
||||
}
|
||||
}
|
||||
DEBUG_LOGF("Wallet index loaded: %zu wallets, %zu extra folders\n",
|
||||
entries_.size(), extra_folders_.size());
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Error loading wallet index: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool WalletIndex::save()
|
||||
{
|
||||
if (file_path_.empty()) file_path_ = getDefaultPath();
|
||||
|
||||
try {
|
||||
json j;
|
||||
j["entries"] = json::array();
|
||||
for (const auto& w : entries_) {
|
||||
json e;
|
||||
e["file"] = w.fileName;
|
||||
e["name"] = w.displayName;
|
||||
e["identity"] = w.walletIdentityHash;
|
||||
e["balance"] = w.cachedBalance;
|
||||
e["addresses"] = w.cachedAddressCount;
|
||||
e["lastOpened"] = w.lastOpenedEpoch;
|
||||
e["size"] = w.sizeBytesAtLastOpen;
|
||||
e["syncedHere"] = w.syncedHere;
|
||||
j["entries"].push_back(std::move(e));
|
||||
}
|
||||
j["extraFolders"] = extra_folders_;
|
||||
|
||||
// No secrets here (file names + coarse metadata) but keep it owner-only for consistency.
|
||||
if (!util::Platform::writeFileAtomically(file_path_, j.dump(2), /*restrictPermissions=*/true)) {
|
||||
DEBUG_LOGF("Could not write wallet index: %s\n", file_path_.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Error saving wallet index: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool WalletIndex::upsert(const WalletIndexEntry& e)
|
||||
{
|
||||
for (auto& w : entries_) {
|
||||
if (w.fileName == e.fileName) {
|
||||
if (sameEntry(w, e)) return false; // nothing changed -> caller can skip save()
|
||||
w = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
entries_.push_back(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
const WalletIndexEntry* WalletIndex::find(const std::string& fileName) const
|
||||
{
|
||||
for (const auto& w : entries_) {
|
||||
if (w.fileName == fileName) return &w;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool WalletIndex::addExtraFolder(const std::string& dir)
|
||||
{
|
||||
if (dir.empty()) return false;
|
||||
if (std::find(extra_folders_.begin(), extra_folders_.end(), dir) != extra_folders_.end())
|
||||
return false;
|
||||
extra_folders_.push_back(dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WalletIndex::removeExtraFolder(const std::string& dir)
|
||||
{
|
||||
auto it = std::find(extra_folders_.begin(), extra_folders_.end(), dir);
|
||||
if (it == extra_folders_.end()) return false;
|
||||
extra_folders_.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
68
src/data/wallet_index.h
Normal file
68
src/data/wallet_index.h
Normal file
@@ -0,0 +1,68 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace data {
|
||||
|
||||
/**
|
||||
* @brief Cached, file-keyed metadata for one wallet file.
|
||||
*
|
||||
* A wallet.dat on disk only reveals its size + mtime; balance and address count require the daemon
|
||||
* to have loaded it. So those are cached here after each load and shown in the wallet-files list
|
||||
* (P2) even when the wallet isn't the active one. walletIdentityHash bridges file -> per-wallet data
|
||||
* (address book, tx history) so the right caches can be selected before/after a switch.
|
||||
*/
|
||||
struct WalletIndexEntry {
|
||||
std::string fileName; // plain wallet filename in the datadir (e.g. "wallet.dat")
|
||||
std::string displayName; // user-facing name (defaults to fileName)
|
||||
std::string walletIdentityHash; // address-derived identity of the last load; "" = unknown
|
||||
double cachedBalance = -1.0; // last-known total balance; < 0 = unknown (never opened)
|
||||
long long cachedAddressCount = -1;// < 0 = unknown
|
||||
long long lastOpenedEpoch = 0; // unix seconds of last open; 0 = never opened
|
||||
long long sizeBytesAtLastOpen = 0;
|
||||
bool syncedHere = false; // loaded in this datadir before -> catch-up on switch (no full rescan)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Persistent wallet-files metadata index (wallets.json, in the config dir).
|
||||
*
|
||||
* Keyed by wallet file name. Populated after each load (P1b) and read by the wallet-files list (P2).
|
||||
* Also remembers extra folders the user added to scan for wallet files. Not encrypted — it holds
|
||||
* only file names + coarse metadata (no keys/addresses).
|
||||
*/
|
||||
class WalletIndex {
|
||||
public:
|
||||
bool load();
|
||||
bool save();
|
||||
static std::string getDefaultPath();
|
||||
|
||||
const std::vector<WalletIndexEntry>& entries() const { return entries_; }
|
||||
const std::vector<std::string>& extraFolders() const { return extra_folders_; }
|
||||
|
||||
/**
|
||||
* @brief Insert or update the entry for e.fileName.
|
||||
* @return true if a new entry was added or any field changed (so the caller can skip save()).
|
||||
*/
|
||||
bool upsert(const WalletIndexEntry& e);
|
||||
|
||||
/** @brief Find the entry for a wallet file, or nullptr. */
|
||||
const WalletIndexEntry* find(const std::string& fileName) const;
|
||||
|
||||
/** @brief Add/remove an extra scan folder. Returns true if the set changed. */
|
||||
bool addExtraFolder(const std::string& dir);
|
||||
bool removeExtraFolder(const std::string& dir);
|
||||
|
||||
private:
|
||||
std::vector<WalletIndexEntry> entries_;
|
||||
std::vector<std::string> extra_folders_;
|
||||
std::string file_path_;
|
||||
};
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
212
src/main.cpp
212
src/main.cpp
@@ -71,6 +71,7 @@ extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid,
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdarg>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -235,6 +236,8 @@ static float g_borderlessDpi = 1.0f; // DPI scale, updated from SDL each f
|
||||
// Custom WndProc that removes the native title bar by extending the
|
||||
// client area to cover the entire window (WM_NCCALCSIZE) and provides
|
||||
// resize borders + caption drag zone (WM_NCHITTEST).
|
||||
static void winlog(const char* fmt, ...); // [WINLOG] tracer (defined below)
|
||||
|
||||
static LRESULT CALLBACK BorderlessWndProc(HWND hwnd, UINT msg,
|
||||
WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
@@ -314,6 +317,12 @@ static LRESULT CALLBACK BorderlessWndProc(HWND hwnd, UINT msg,
|
||||
// Scale min window size by DPI so it matches the SDL-side minimum.
|
||||
mmi->ptMinTrackSize.x = (LONG)(1024 * g_borderlessDpi);
|
||||
mmi->ptMinTrackSize.y = (LONG)(720 * g_borderlessDpi);
|
||||
static float s_lastBd = -1.0f; // [WINLOG] log only when g_borderlessDpi changes (this fires a lot)
|
||||
if (g_borderlessDpi != s_lastBd) {
|
||||
s_lastBd = g_borderlessDpi;
|
||||
winlog("WM_GETMINMAXINFO g_borderlessDpi=%.3f -> minTrack=%ldx%ld",
|
||||
g_borderlessDpi, (long)mmi->ptMinTrackSize.x, (long)mmi->ptMinTrackSize.y);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -531,6 +540,63 @@ static void drawWindowBackdrop(dragonx::App& app, ImDrawList* bgDL, ImVec2 p0, I
|
||||
}
|
||||
}
|
||||
|
||||
// ── Window-dimension diagnostic tracer ──────────────────────────────────────────────────────
|
||||
// Always-on (writes to stderr → dragonx-debug.log on Windows) so it's visible in release builds,
|
||||
// unlike DRAGONX_DEBUG-gated DEBUG_LOGF. Grep the log for "[WINLOG]" to trace how the window size
|
||||
// changes when dragging between monitors of different DPI. Low volume — only fires on window
|
||||
// resize / move / DPI-scale events. TODO: remove once the multi-monitor sizing bug is resolved.
|
||||
static void winlog(const char* fmt, ...)
|
||||
{
|
||||
char buf[1024];
|
||||
va_list a; va_start(a, fmt); vsnprintf(buf, sizeof(buf), fmt, a); va_end(a);
|
||||
fprintf(stderr, "[WINLOG] %s\n", buf);
|
||||
fflush(stderr);
|
||||
}
|
||||
// Dump the full current window state with a short tag.
|
||||
static void winlogState(SDL_Window* w, const char* tag)
|
||||
{
|
||||
int sw = 0, sh = 0, pw = 0, ph = 0, mw = 0, mh = 0;
|
||||
SDL_GetWindowSize(w, &sw, &sh);
|
||||
SDL_GetWindowSizeInPixels(w, &pw, &ph);
|
||||
SDL_GetWindowMinimumSize(w, &mw, &mh);
|
||||
float ds = SDL_GetWindowDisplayScale(w);
|
||||
float st = dragonx::ui::material::Typography::instance().getDpiScale();
|
||||
SDL_DisplayID did = SDL_GetDisplayForWindow(w);
|
||||
winlog("%-20s size=%dx%d px=%dx%d min=%dx%d dispScale=%.3f storedScale=%.3f display=%u",
|
||||
tag, sw, sh, pw, ph, mw, mh, ds, st, (unsigned)did);
|
||||
#ifdef _WIN32
|
||||
HWND hwnd = (HWND)SDL_GetPointerProperty(SDL_GetWindowProperties(w),
|
||||
SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
|
||||
if (hwnd) {
|
||||
RECT wr = {}, cr = {};
|
||||
GetWindowRect(hwnd, &wr);
|
||||
GetClientRect(hwnd, &cr);
|
||||
winlog("%-20s native win=%ldx%ld client=%ldx%ld pos=%ld,%ld",
|
||||
tag, (long)(wr.right - wr.left), (long)(wr.bottom - wr.top),
|
||||
(long)(cr.right - cr.left), (long)(cr.bottom - cr.top), (long)wr.left, (long)wr.top);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Resize the window to an EXACT client size. On the DX11 borderless native HWND, SDL_SetWindowSize
|
||||
// inflates the result by a phantom caption+border frame: SDL runs AdjustWindowRectEx to turn the
|
||||
// requested client size into a window rect (adding the WS_CAPTION/WS_THICKFRAME frame), but our
|
||||
// WM_NCCALCSIZE handler folds that entire window rect back into the client area — so the client ends
|
||||
// up ~frame bigger than asked (e.g. a 720 min becomes 759, unreachable). Resize the HWND directly:
|
||||
// for a borderless window the window rect equals the client, so SetWindowPos gives the exact size.
|
||||
static void resizeWindowExact(SDL_Window* window, int w, int h)
|
||||
{
|
||||
#if defined(_WIN32) && defined(DRAGONX_USE_DX11)
|
||||
HWND hwnd = (HWND)SDL_GetPointerProperty(SDL_GetWindowProperties(window),
|
||||
SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
|
||||
if (hwnd) {
|
||||
SetWindowPos(hwnd, nullptr, 0, 0, w, h, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
SDL_SetWindowSize(window, w, h);
|
||||
}
|
||||
|
||||
// Handle SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: save the old-scale size,
|
||||
// compute + clamp the new physical size, arm the deferred resize retry,
|
||||
// and rebuild fonts / reset ImGui style at the new DPI scale. newScale
|
||||
@@ -539,7 +605,8 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||
std::map<int,std::pair<int,int>>& savedSizeForScale,
|
||||
int& lastKnownW, int& lastKnownH,
|
||||
int& dpiTargetW, int& dpiTargetH,
|
||||
int& dpiResizeRetries, bool& dpiResizePending)
|
||||
int& dpiResizeRetries, bool& dpiResizePending,
|
||||
int& dpiSettleFrames)
|
||||
{
|
||||
auto& typo = dragonx::ui::material::Typography::instance();
|
||||
float oldScale = typo.getDpiScale();
|
||||
@@ -547,6 +614,9 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||
float ratio = newScale / oldScale;
|
||||
DEBUG_LOGF("Display scale changed: %.2f -> %.2f (ratio %.2f)\n",
|
||||
oldScale, newScale, ratio);
|
||||
winlog("======== DPI CHANGE storedScale %.3f -> newScale %.3f (ratio %.3f) ========",
|
||||
oldScale, newScale, ratio);
|
||||
winlogState(window, " entry");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
#ifdef DRAGONX_USE_DX11
|
||||
@@ -562,6 +632,7 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||
savedSizeForScale[oldPct] = {lastKnownW, lastKnownH};
|
||||
DEBUG_LOGF(" Saved %dx%d for scale %d%%\n",
|
||||
lastKnownW, lastKnownH, oldPct);
|
||||
winlog(" saved lastKnown %dx%d under old scale %d%%", lastKnownW, lastKnownH, oldPct);
|
||||
}
|
||||
|
||||
// Compute new physical size: if we've been to this
|
||||
@@ -573,14 +644,24 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||
if (it != savedSizeForScale.end()) {
|
||||
newW = it->second.first;
|
||||
newH = it->second.second;
|
||||
winlog(" restore saved %dx%d for scale %d%%", newW, newH, newPct);
|
||||
} else {
|
||||
// Use lastKnownW/H (pre-auto-resize) instead of
|
||||
// SDL_GetWindowSize() which already reflects the
|
||||
// WM_DPICHANGED auto-resize.
|
||||
newW = (int)lroundf((float)lastKnownW * newScale / oldScale);
|
||||
newH = (int)lroundf((float)lastKnownH * newScale / oldScale);
|
||||
winlog(" proportional %dx%d = lastKnown %dx%d * %.3f/%.3f",
|
||||
newW, newH, lastKnownW, lastKnownH, newScale, oldScale);
|
||||
}
|
||||
|
||||
// Never restore/scale below the new scale's minimum — guarantees the window lands exactly
|
||||
// on the min in both directions even if the per-scale map was ever seeded oddly.
|
||||
const int minW = (int)(1024 * newScale), minH = (int)(720 * newScale);
|
||||
newW = std::max(newW, minW);
|
||||
newH = std::max(newH, minH);
|
||||
winlog(" after min-clamp (min %dx%d): %dx%d", minW, minH, newW, newH);
|
||||
|
||||
// Clamp to the target display's work area so the
|
||||
// window doesn't overflow a smaller/higher-density monitor.
|
||||
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
|
||||
@@ -589,23 +670,33 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||
if (SDL_GetDisplayUsableBounds(did, &usable)) {
|
||||
newW = std::min(newW, usable.w);
|
||||
newH = std::min(newH, usable.h);
|
||||
winlog(" after display-clamp (usable %dx%d): %dx%d", usable.w, usable.h, newW, newH);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new minimum BEFORE resizing. SDL_SetWindowSize clamps the request UP to the
|
||||
// current minimum, so when moving to a LOWER scale the shrink (e.g. 1080 -> 720) issued
|
||||
// while the min is still the old 1080 would be clamped back to 1080 and stick there
|
||||
// (SDL_SetWindowMinimumSize only ever grows a window, never shrinks it). Min-then-size
|
||||
// makes the shrink land on the new min immediately, without relying on the deferred retry.
|
||||
SDL_SetWindowMinimumSize(window, minW, minH);
|
||||
dpiResizePending = true;
|
||||
SDL_SetWindowSize(window, newW, newH);
|
||||
resizeWindowExact(window, newW, newH);
|
||||
lastKnownW = newW;
|
||||
lastKnownH = newH;
|
||||
// Arm deferred retry — the SetWindowSize above may
|
||||
// not stick if the user is mid-drag (modal loop).
|
||||
// Arm deferred retry (SetWindowSize may not stick mid-drag) and suppress resize-recording
|
||||
// for the whole settle burst: a transient WINDOW_RESIZED (ours, the WM_DPICHANGED settle,
|
||||
// or a retry frame) that arrives after dpiResizePending was consumed and with scales already
|
||||
// matching would otherwise be misclassified as a user resize and corrupt savedSizeForScale.
|
||||
dpiTargetW = newW;
|
||||
dpiTargetH = newH;
|
||||
dpiResizeRetries = 30; // retry for ~30 frames
|
||||
SDL_SetWindowMinimumSize(window,
|
||||
(int)(1024 * newScale),
|
||||
(int)(720 * newScale));
|
||||
dpiSettleFrames = 40; // ignore resize-recording until the transition settles
|
||||
DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n",
|
||||
newW, newH, newScale, newPct);
|
||||
winlog(" SetWindowMinimumSize(%dx%d) then SetWindowSize(%dx%d); armed retries=30 settle=40",
|
||||
minW, minH, newW, newH);
|
||||
winlogState(window, " after set");
|
||||
|
||||
// 2) Rebuild font atlas at the new DPI scale
|
||||
typo.reload(io, newScale);
|
||||
@@ -703,6 +794,9 @@ int main(int argc, char* argv[])
|
||||
savedWinW = sw;
|
||||
savedWinH = sh;
|
||||
DEBUG_LOGF("Restored window size from settings: %dx%d\n", sw, sh);
|
||||
} else if (sw > 0 || sh > 0) {
|
||||
DEBUG_LOGF("Ignored saved window size %dx%d (below 1024x720 minimum) — using default %dx%d\n",
|
||||
sw, sh, savedWinW, savedWinH);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1062,13 +1156,21 @@ int main(int argc, char* argv[])
|
||||
// (point) coordinates and the framebuffer handles pixel density, so
|
||||
// we must NOT multiply the window size by the content scale.
|
||||
#ifndef __APPLE__
|
||||
if (currentDpiScale > 1.01f) {
|
||||
{
|
||||
int curW = 0, curH = 0;
|
||||
SDL_GetWindowSize(window, &curW, &curH);
|
||||
int newW = (int)(curW * currentDpiScale);
|
||||
int newH = (int)(curH * currentDpiScale);
|
||||
int newW = curW, newH = curH;
|
||||
if (currentDpiScale > 1.01f) {
|
||||
// On a HiDPI display the saved size is logical px — scale it up to physical so the UI
|
||||
// appears the same physical size as on a 100% display, and scale the minimum too.
|
||||
newW = (int)(curW * currentDpiScale);
|
||||
newH = (int)(curH * currentDpiScale);
|
||||
SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale));
|
||||
}
|
||||
|
||||
// Clamp to the display's work area so the window fits on screen.
|
||||
// Clamp to the current display's work area — runs on EVERY startup (this clamp used to live
|
||||
// inside the HiDPI branch, so a size saved on a larger/disconnected monitor could open the
|
||||
// window off-screen or bigger than the screen on a same-DPI cold start).
|
||||
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
|
||||
if (did) {
|
||||
SDL_Rect usable;
|
||||
@@ -1078,15 +1180,16 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
}
|
||||
|
||||
SDL_SetWindowSize(window, newW, newH);
|
||||
SDL_SetWindowMinimumSize(window,
|
||||
(int)(1024 * currentDpiScale),
|
||||
(int)(720 * currentDpiScale));
|
||||
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
DEBUG_LOGF("HiDPI startup: window resized %dx%d -> %dx%d (scale %.2f)\n",
|
||||
curW, curH, newW, newH, currentDpiScale);
|
||||
if (newW != curW || newH != curH) {
|
||||
SDL_SetWindowSize(window, newW, newH);
|
||||
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
DEBUG_LOGF("Startup: window fitted %dx%d -> %dx%d (scale %.2f)\n",
|
||||
curW, curH, newW, newH, currentDpiScale);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
winlog("STARTUP savedSize=%dx%d currentDpiScale=%.3f", savedWinW, savedWinH, currentDpiScale);
|
||||
winlogState(window, " startup");
|
||||
|
||||
// Create application instance
|
||||
dragonx::App app;
|
||||
@@ -1267,6 +1370,10 @@ int main(int argc, char* argv[])
|
||||
// overrides SetWindowPos). We retry for several frames.
|
||||
int dpiTargetW = 0, dpiTargetH = 0;
|
||||
int dpiResizeRetries = 0;
|
||||
// Frames to keep ignoring resize-recording after a DPI transition (settle window) so transient
|
||||
// WINDOW_RESIZED events during the transition can't corrupt savedSizeForScale / lastKnownW/H.
|
||||
int dpiSettleFrames = 0;
|
||||
SDL_DisplayID lastLoggedDisplay = 0; // [WINLOG] throttle: log MOVED only when the display changes
|
||||
{
|
||||
float s = dragonx::ui::material::Typography::instance().getDpiScale();
|
||||
int w = 0, h = 0;
|
||||
@@ -1286,16 +1393,23 @@ int main(int argc, char* argv[])
|
||||
SDL_GetWindowSize(window, &curW, &curH);
|
||||
if (curW != dpiTargetW || curH != dpiTargetH) {
|
||||
dpiResizePending = true;
|
||||
SDL_SetWindowSize(window, dpiTargetW, dpiTargetH);
|
||||
resizeWindowExact(window, dpiTargetW, dpiTargetH);
|
||||
DEBUG_LOGF(" Deferred resize retry %d: %dx%d -> %dx%d\n",
|
||||
dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH);
|
||||
winlog("RETRY %d: cur %dx%d != target %dx%d -> SetWindowSize(target)",
|
||||
dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH);
|
||||
} else {
|
||||
// Size matches — done
|
||||
winlog("RETRY done: reached target %dx%d", dpiTargetW, dpiTargetH);
|
||||
dpiResizeRetries = 0;
|
||||
}
|
||||
if (dpiResizeRetries == 1) winlogState(window, "RETRY-last(stuck?)");
|
||||
--dpiResizeRetries;
|
||||
needsRedraw = true;
|
||||
}
|
||||
// Burn down the post-DPI-transition settle window (see handleDisplayScaleChange). The retry
|
||||
// above forces redraws while active, so this drains promptly even if the app is otherwise idle.
|
||||
if (dpiSettleFrames > 0) --dpiSettleFrames;
|
||||
|
||||
// P6: Idle rendering — sleep if nothing needs redrawing
|
||||
if (!needsRedraw) {
|
||||
@@ -1326,8 +1440,12 @@ int main(int argc, char* argv[])
|
||||
#ifdef __APPLE__
|
||||
actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
|
||||
#endif
|
||||
bool isDpiResize = dpiResizePending ||
|
||||
bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending ||
|
||||
std::abs(actualScale - storedScale) > 0.01f;
|
||||
winlog("RESIZED(wait) evt=%dx%d actualScale=%.3f storedScale=%.3f dpiPending=%d settle=%d retries=%d -> %s",
|
||||
waitEvent.window.data1, waitEvent.window.data2, actualScale, storedScale,
|
||||
(int)dpiResizePending, dpiSettleFrames, dpiResizeRetries,
|
||||
isDpiResize ? "IGNORED(dpi)" : "RECORDED(user)");
|
||||
if (dpiResizePending) dpiResizePending = false;
|
||||
if (!isDpiResize) {
|
||||
int pct = (int)lroundf(storedScale * 100.0f);
|
||||
@@ -1349,9 +1467,10 @@ int main(int argc, char* argv[])
|
||||
#ifdef __APPLE__
|
||||
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
||||
#endif
|
||||
winlog("SCALE_CHANGED(wait) reported newScale=%.3f", newScale);
|
||||
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
||||
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
||||
dpiResizeRetries, dpiResizePending);
|
||||
dpiResizeRetries, dpiResizePending, dpiSettleFrames);
|
||||
}
|
||||
needsRedraw = true; // Got an event — redraw
|
||||
}
|
||||
@@ -1408,10 +1527,22 @@ int main(int argc, char* argv[])
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
app.beginShutdown();
|
||||
}
|
||||
if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED &&
|
||||
if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED &&
|
||||
event.window.windowID == SDL_GetWindowID(window)) {
|
||||
app.beginShutdown();
|
||||
}
|
||||
// [WINLOG] Log a window MOVE only when it crosses to a different display (avoids drag spam).
|
||||
if (event.type == SDL_EVENT_WINDOW_MOVED &&
|
||||
event.window.windowID == SDL_GetWindowID(window)) {
|
||||
SDL_DisplayID d = SDL_GetDisplayForWindow(window);
|
||||
if (d != lastLoggedDisplay) {
|
||||
lastLoggedDisplay = d;
|
||||
winlog("MOVED to display %u pos=%d,%d dispScale=%.3f",
|
||||
(unsigned)d, event.window.data1, event.window.data2,
|
||||
SDL_GetWindowDisplayScale(window));
|
||||
winlogState(window, " on-cross");
|
||||
}
|
||||
}
|
||||
#ifdef DRAGONX_USE_DX11
|
||||
// Handle window resize for DX11 swap chain
|
||||
if (event.type == SDL_EVENT_WINDOW_RESIZED &&
|
||||
@@ -1429,8 +1560,12 @@ int main(int argc, char* argv[])
|
||||
#ifdef __APPLE__
|
||||
actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
|
||||
#endif
|
||||
bool isDpiResize = dpiResizePending ||
|
||||
bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending ||
|
||||
std::abs(actualScale - storedScale) > 0.01f;
|
||||
winlog("RESIZED(poll) evt=%dx%d actualScale=%.3f storedScale=%.3f dpiPending=%d settle=%d retries=%d fsPending=%d -> %s",
|
||||
event.window.data1, event.window.data2, actualScale, storedScale,
|
||||
(int)dpiResizePending, dpiSettleFrames, dpiResizeRetries, (int)fontScaleResizePending,
|
||||
isDpiResize ? "IGNORED(dpi)" : "RECORDED(user)");
|
||||
if (dpiResizePending) dpiResizePending = false;
|
||||
bool isFSResize = fontScaleResizePending;
|
||||
if (fontScaleResizePending) fontScaleResizePending = false;
|
||||
@@ -1462,9 +1597,10 @@ int main(int argc, char* argv[])
|
||||
#ifdef __APPLE__
|
||||
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
||||
#endif
|
||||
winlog("SCALE_CHANGED(poll) reported newScale=%.3f", newScale);
|
||||
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
||||
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
||||
dpiResizeRetries, dpiResizePending);
|
||||
dpiResizeRetries, dpiResizePending, dpiSettleFrames);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1754,6 +1890,16 @@ int main(int argc, char* argv[])
|
||||
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
|
||||
}
|
||||
|
||||
// Uncap FPS during a screenshot sweep so it doesn't crawl at the compositor's (often
|
||||
// heavily throttled for an unfocused window) vsync rate. Restored when the sweep ends.
|
||||
{
|
||||
static bool sweepVsyncOff = false;
|
||||
const bool sweeping = app.isScreenshotSweeping();
|
||||
if (sweeping != sweepVsyncOff) {
|
||||
SDL_GL_SetSwapInterval(sweeping ? 0 : 1);
|
||||
sweepVsyncOff = sweeping;
|
||||
}
|
||||
}
|
||||
SDL_GL_SwapWindow(window);
|
||||
#endif
|
||||
PERF_END("Present", _perfPresent);
|
||||
@@ -1761,6 +1907,21 @@ int main(int argc, char* argv[])
|
||||
// --- PerfLog: end frame ---
|
||||
dragonx::util::PerfLog::instance().endFrame();
|
||||
|
||||
// Headless verification hook: DRAGONX_FULL_SWEEP=1 runs the full UI sweep once the app has
|
||||
// settled, then quits when it finishes. Used to run the sweep without clicking the button.
|
||||
{
|
||||
static const bool autoSweep = std::getenv("DRAGONX_FULL_SWEEP") != nullptr;
|
||||
static int autoFrames = 0;
|
||||
static bool autoStarted = false;
|
||||
if (autoSweep) {
|
||||
if (!autoStarted) {
|
||||
if (++autoFrames >= 90) { app.startFullUiSweep(); autoStarted = true; }
|
||||
} else if (!app.isScreenshotSweeping()) {
|
||||
running = false; // sweep complete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exit when shutdown is complete
|
||||
if (app.shouldQuit()) {
|
||||
running = false;
|
||||
@@ -1825,9 +1986,12 @@ int main(int argc, char* argv[])
|
||||
float s = dragonx::ui::material::Typography::instance().getDpiScale();
|
||||
int logW = (int)lroundf((float)curW / s);
|
||||
int logH = (int)lroundf((float)curH / s);
|
||||
winlog("SHUTDOWN save: size=%dx%d storedScale=%.3f -> logical=%dx%d", curW, curH, s, logW, logH);
|
||||
if (logW >= 1024 && logH >= 720) {
|
||||
app.settings()->setWindowSize(logW, logH);
|
||||
app.settings()->save();
|
||||
} else {
|
||||
DEBUG_LOGF("Not saving window size %dx%d (logical, below 1024x720 minimum)\n", logW, logH);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1283,11 +1283,23 @@ inline int SegmentedControl(ImDrawList* dl, ImVec2 origin, float totalW, float h
|
||||
// Creates a fullscreen semi-transparent overlay with a centered card dialog.
|
||||
// Similar to the shutdown screen pattern but for interactive dialogs.
|
||||
|
||||
// Per-dialog content height (keyed by the child's id) measured at the end of each frame, so the next
|
||||
// frame can size the glass card to its content instead of a fixed viewport band. g_overlayCurrentKey
|
||||
// carries the active dialog's key from BeginOverlayDialog to EndOverlayDialog (overlays don't nest).
|
||||
inline std::unordered_map<std::string, float> g_overlayCardHeights;
|
||||
// Per-dialog content state (keyed by the child's id), measured at the end of each frame so the next
|
||||
// frame can position/size the (auto-height) card to its content. An auto-resize child reports a
|
||||
// too-small height on its first render and the true height a frame later, so we keep the card hidden
|
||||
// until the height holds steady (stableCount) before revealing it centered — otherwise it visibly
|
||||
// slides for a couple frames as the height converges. g_overlayCurrentKey carries the active dialog's
|
||||
// key from BeginOverlayDialog to EndOverlayDialog (overlays don't nest).
|
||||
struct OverlayCardState {
|
||||
float height = 0.0f; // last measured content-child height
|
||||
int stableCount = 0; // consecutive frames the height held steady (within 1px)
|
||||
int appearFrames = 0; // frames since (re)appearing while still hidden — a safety cap
|
||||
bool shown = false; // revealed (centered) at least once this open; don't re-hide after
|
||||
};
|
||||
inline std::unordered_map<std::string, OverlayCardState> g_overlayCardHeights;
|
||||
inline std::string g_overlayCurrentKey;
|
||||
// Set when BeginOverlayDialog clips the card on an auto-height dialog's measuring frame (so it never
|
||||
// flashes off-center); EndOverlayDialog pops the clip. Overlays don't nest, so a single flag is fine.
|
||||
inline bool g_overlayHideFrameClip = false;
|
||||
|
||||
// The dark base color for any full-window overlay backdrop (dialog scrims + the shutdown screen),
|
||||
// so every overlay shares one backdrop tone. `opacity` is the alpha (dialogs vary it per call).
|
||||
@@ -1362,6 +1374,10 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
return false;
|
||||
}
|
||||
|
||||
// True only on the frame the dialog (re)opens — used to re-arm the auto-height settle so each
|
||||
// fresh open reveals already-centered (rather than flashing last-open's stale position).
|
||||
const bool scrimAppearing = ImGui::IsWindowAppearing();
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// Live-blur backdrop with capture-once: (re)capture the content drawn BELOW this overlay for a
|
||||
@@ -1396,10 +1412,13 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
ImGuiButtonFlags_MouseButtonRight |
|
||||
ImGuiButtonFlags_MouseButtonMiddle);
|
||||
|
||||
// Card geometry: fixed-height (large modals) is centered; auto-height reuses last frame's
|
||||
// measured content height (short dialogs get short cards), capped at the viewport ratio.
|
||||
// Card geometry: both fixed- and auto-height cards are VERTICALLY CENTERED in the window.
|
||||
// Fixed-height centers its known height; auto-height centers last frame's measured content
|
||||
// height (cached per dialog id, so a re-opened dialog is centered from frame 1 — only the very
|
||||
// first open of a given dialog this session briefly anchors near the top before it re-centers).
|
||||
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
|
||||
float cardY, cardBottomY;
|
||||
bool hideForMeasure = false; // true on an auto-height dialog's first (unmeasured) frame
|
||||
const bool fixedHeight = (spec.cardHeight > 0.0f);
|
||||
if (fixedHeight) {
|
||||
float cardH = std::min(spec.cardHeight * dp, vp_size.y - 32.0f);
|
||||
@@ -1407,25 +1426,45 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
cardBottomY = cardY + cardH;
|
||||
g_overlayCurrentKey.clear();
|
||||
} else {
|
||||
cardY = vp_pos.y + vp_size.y * 0.15f;
|
||||
g_overlayCurrentKey = childId;
|
||||
float ratioMaxY = vp_pos.y + vp_size.y * spec.cardBottomViewportRatio;
|
||||
auto it = g_overlayCardHeights.find(childId);
|
||||
cardBottomY = (it != g_overlayCardHeights.end() && it->second > 0.0f)
|
||||
? std::min(cardY + it->second, ratioMaxY) : ratioMaxY;
|
||||
OverlayCardState& cs = g_overlayCardHeights[childId];
|
||||
if (scrimAppearing) { cs.shown = false; cs.stableCount = 0; cs.appearFrames = 0; }
|
||||
if (!cs.shown) cs.appearFrames++;
|
||||
const float measuredH = cs.height;
|
||||
// Reveal once the measured height has settled (auto-resize converges in ~2 frames) or it's
|
||||
// already been shown this open (don't re-hide on a mid-dialog content change); a frame cap
|
||||
// guarantees a pathological ever-changing height can't hide the dialog forever.
|
||||
const bool ready = measuredH > 0.0f &&
|
||||
(cs.shown || cs.stableCount >= 1 || cs.appearFrames >= 8);
|
||||
if (ready) {
|
||||
cs.shown = true;
|
||||
// Center the measured content; if it's taller than the window, anchor at the top margin.
|
||||
cardY = (measuredH < vp_size.y - 32.0f)
|
||||
? vp_pos.y + (vp_size.y - measuredH) * 0.5f
|
||||
: vp_pos.y + 16.0f;
|
||||
cardBottomY = cardY + measuredH;
|
||||
} else {
|
||||
// Still settling: lay the content out (so the auto-height child gets measured) but keep
|
||||
// the card hidden (hideForMeasure below) so it never flashes off-center — it appears,
|
||||
// already centered and stable, once the height stops changing.
|
||||
hideForMeasure = true;
|
||||
cardY = vp_pos.y + vp_size.y * 0.15f;
|
||||
cardBottomY = vp_pos.y + vp_size.y * spec.cardBottomViewportRatio;
|
||||
}
|
||||
}
|
||||
ImVec2 cardMin(cardX, cardY), cardMax(cardX + cardWidth, cardBottomY);
|
||||
|
||||
// Card background — a glass panel unless the content floats directly on the backdrop.
|
||||
if (!floating) {
|
||||
// Card background — a glass panel unless the content floats directly on the backdrop. Skipped on
|
||||
// the measuring frame (its geometry is a placeholder; the whole card is hidden until centered).
|
||||
if (!floating && !hideForMeasure) {
|
||||
GlassPanelSpec cardGlass;
|
||||
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 35; cardGlass.borderAlpha = 50; cardGlass.borderWidth = 1.0f;
|
||||
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
||||
}
|
||||
|
||||
// Click outside the card dismisses — but not on the appearing frame (the opening click) or while
|
||||
// a popup opened from the dialog is showing (a click in an overhanging combo would look outside).
|
||||
if (spec.p_open && !overlayPopupOpen && !ImGui::IsWindowAppearing() &&
|
||||
// Click outside the card dismisses — but not on the measuring frame (placeholder geometry), the
|
||||
// appearing frame (the opening click), or while a popup opened from the dialog is showing.
|
||||
if (spec.p_open && !hideForMeasure && !overlayPopupOpen && !ImGui::IsWindowAppearing() &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
!ImGui::IsMouseHoveringRect(cardMin, cardMax, false)) {
|
||||
*spec.p_open = false;
|
||||
@@ -1437,9 +1476,13 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
|
||||
ImGuiChildFlags cflags = ImGuiChildFlags_AlwaysUseWindowPadding | (fixedHeight ? 0 : ImGuiChildFlags_AutoResizeY);
|
||||
// NoScrollWithMouse (not just NoScrollbar): a modal is a fixed frame — the wheel must never drift
|
||||
// the WHOLE card. If content marginally overflows a fixed card, the wheel would otherwise scroll
|
||||
// the entire dialog (title + footer and all). Inner scroll regions (lists, notes) still scroll on
|
||||
// their own; auto-height cards resize to content so they never overflow anyway.
|
||||
bool childVisible = ImGui::BeginChild(childId.c_str(),
|
||||
ImVec2(cardWidth, fixedHeight ? (cardBottomY - cardY) : 0.0f),
|
||||
cflags, ImGuiWindowFlags_NoScrollbar);
|
||||
cflags, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
// Floating (portfolio-style) cards: the padding applies to this content child only, so pop it
|
||||
// now (nested children mustn't inherit it), and center button labels. Net style-var count stays
|
||||
// at 2 (ChildRounding + ButtonTextAlign) so EndOverlayDialog's PopStyleVar(2) is unchanged.
|
||||
@@ -1448,6 +1491,14 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
// Measuring frame: clip the card to nothing. Content (title + the caller's body) still lays out,
|
||||
// so the child's auto-height is measured for next frame, but nothing is drawn — no off-center
|
||||
// flash. EndOverlayDialog pops this before EndChild. Only entered when childVisible (End runs).
|
||||
if (childVisible && hideForMeasure) {
|
||||
ImGui::PushClipRect(ImVec2(0, 0), ImVec2(0, 0), false);
|
||||
g_overlayHideFrameClip = true;
|
||||
}
|
||||
|
||||
if (childVisible && spec.title) {
|
||||
if (plainHd) {
|
||||
ImGui::PushFont(Type().h6());
|
||||
@@ -1462,10 +1513,10 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
return childVisible;
|
||||
}
|
||||
|
||||
// Positional overload — the default look for the app's dialogs: a card (auto-height, so small
|
||||
// dialogs stay small) on the live-blur backdrop. The card renders opaque via the sole-consumer
|
||||
// fallback (glass would thrash the single-slot blur cache against the radius-64 backdrop). A dialog
|
||||
// that wants the old opaque scrim (no blur) can pass a spec with blurBackdrop = false.
|
||||
// Positional overload — the default look for the app's dialogs: content floats directly on the
|
||||
// live-blur backdrop (no glass card) under a plain h6 heading, matching the Manage-Portfolio
|
||||
// editor. Auto-height keeps short dialogs short. A dialog that needs the old contained card can
|
||||
// pass an explicit OverlayDialogSpec with style = OverlayStyle::GlassCard.
|
||||
inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f,
|
||||
float scrimOpacity = 0.92f, float cardBottomViewportRatio = 0.85f,
|
||||
const char* idSuffix = nullptr)
|
||||
@@ -1473,17 +1524,26 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
|
||||
OverlayDialogSpec s;
|
||||
s.title = title; s.p_open = p_open; s.cardWidth = cardWidth;
|
||||
s.scrimOpacity = scrimOpacity; s.cardBottomViewportRatio = cardBottomViewportRatio; s.idSuffix = idSuffix;
|
||||
s.blurBackdrop = true;
|
||||
s.blurBackdrop = true;
|
||||
s.floatingContent = true; // no glass card — content floats on the blur
|
||||
s.plainHeading = true; // h6 heading instead of the boxed title bar (drops the ✕)
|
||||
return BeginOverlayDialog(s);
|
||||
}
|
||||
|
||||
inline void EndOverlayDialog()
|
||||
{
|
||||
if (g_overlayHideFrameClip) { ImGui::PopClipRect(); g_overlayHideFrameClip = false; }
|
||||
ImGui::EndChild();
|
||||
// Remember the rendered card height (the child is the last item) so the next frame's
|
||||
// BeginOverlayDialog can size the glass to the content — kills the empty band under short dialogs.
|
||||
// Remember the rendered card height (the child is the last item) + track whether it has stopped
|
||||
// changing, so next frame's BeginOverlayDialog can reveal the card only once its height (and thus
|
||||
// its centered position) is stable.
|
||||
if (!g_overlayCurrentKey.empty()) {
|
||||
g_overlayCardHeights[g_overlayCurrentKey] = ImGui::GetItemRectSize().y;
|
||||
OverlayCardState& cs = g_overlayCardHeights[g_overlayCurrentKey];
|
||||
const float h = ImGui::GetItemRectSize().y;
|
||||
const float d = (h >= cs.height) ? (h - cs.height) : (cs.height - h);
|
||||
if (h > 0.0f && d <= 1.0f) cs.stableCount++;
|
||||
else cs.stableCount = 0;
|
||||
cs.height = h;
|
||||
}
|
||||
ImGui::PopStyleColor(); // ChildBg
|
||||
ImGui::PopStyleVar(2); // ChildRounding, WindowPadding (for child)
|
||||
|
||||
154
src/ui/material/markdown.h
Normal file
154
src/ui/material/markdown.h
Normal file
@@ -0,0 +1,154 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Minimal in-app markdown renderer for release notes (the daemon / miner updater bodies are GitHub-
|
||||
// flavoured markdown). Supports the subset those notes actually use: # / ## / ### headings,
|
||||
// **bold**, `inline code`, - / * / + bullets, and blank-line paragraph breaks. It lays text out with
|
||||
// the ImGui draw list (word-wrapped, mixed fonts) and reserves the block height via a Dummy so a
|
||||
// scrollable parent scrolls correctly. Not a full CommonMark implementation — just enough to make
|
||||
// release notes readable.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include "../layout.h"
|
||||
#include "colors.h"
|
||||
#include "type.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace material {
|
||||
|
||||
// Render `md` into the current window, wrapping to `wrapW` (defaults to the content region width).
|
||||
// Advances the ImGui cursor past the rendered block.
|
||||
inline void RenderMarkdown(const std::string& md, float wrapW = 0.0f) {
|
||||
const float dp = Layout::dpiScale();
|
||||
if (wrapW <= 0.0f) wrapW = ImGui::GetContentRegionAvail().x;
|
||||
if (wrapW <= 1.0f) return;
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
ImFont* fBody = Type().body2(); // 14 Regular
|
||||
ImFont* fBold = Type().subtitle2(); // 14 Medium (bold at body size)
|
||||
ImFont* fCode = Type().mono();
|
||||
ImFont* fH1 = Type().h6(); // 20 Medium
|
||||
ImFont* fH2 = Type().subtitle2(); // heading level 2+ = medium + top spacing
|
||||
|
||||
const ImU32 cText = OnSurface();
|
||||
const ImU32 cCode = Primary();
|
||||
const ImU32 cBullet = OnSurfaceMedium();
|
||||
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const float xLeft = origin.x;
|
||||
const float maxX = xLeft + wrapW;
|
||||
float y = origin.y;
|
||||
|
||||
struct Run { std::string text; ImFont* font; ImU32 col; bool code; };
|
||||
|
||||
// Split a line into styled runs on **bold** and `code`.
|
||||
auto parseInline = [&](const std::string& line, ImFont* base, ImU32 col) {
|
||||
std::vector<Run> runs;
|
||||
std::string cur;
|
||||
auto flush = [&]() { if (!cur.empty()) { runs.push_back({cur, base, col, false}); cur.clear(); } };
|
||||
for (size_t i = 0; i < line.size();) {
|
||||
if (line[i] == '*' && i + 1 < line.size() && line[i + 1] == '*') {
|
||||
size_t e = line.find("**", i + 2);
|
||||
if (e == std::string::npos) { cur += line[i]; ++i; continue; }
|
||||
flush();
|
||||
runs.push_back({line.substr(i + 2, e - (i + 2)), fBold, col, false});
|
||||
i = e + 2;
|
||||
} else if (line[i] == '`') {
|
||||
size_t e = line.find('`', i + 1);
|
||||
if (e == std::string::npos) { cur += line[i]; ++i; continue; }
|
||||
flush();
|
||||
runs.push_back({line.substr(i + 1, e - (i + 1)), fCode, cCode, true});
|
||||
i = e + 1;
|
||||
} else {
|
||||
cur += line[i];
|
||||
++i;
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return runs;
|
||||
};
|
||||
|
||||
// Lay runs out as words, wrapping to [xStart, maxX] and hanging at xHang on wrap. Advances y.
|
||||
auto layout = [&](const std::vector<Run>& runs, float xStart, float xHang) {
|
||||
float x = xStart;
|
||||
float lineH = 0.0f;
|
||||
for (const Run& r : runs) {
|
||||
size_t i = 0;
|
||||
while (i < r.text.size()) {
|
||||
size_t ws = r.text.find(' ', i);
|
||||
std::string word = (ws == std::string::npos) ? r.text.substr(i) : r.text.substr(i, ws - i + 1);
|
||||
i = (ws == std::string::npos) ? r.text.size() : ws + 1;
|
||||
if (word.empty()) continue;
|
||||
float wW = r.font->CalcTextSizeA(r.font->LegacySize, FLT_MAX, 0, word.c_str()).x;
|
||||
if (x + wW > maxX && x > xHang + 0.5f) { // wrap
|
||||
y += (lineH > 0.0f ? lineH : r.font->LegacySize * 1.4f);
|
||||
x = xHang;
|
||||
lineH = 0.0f;
|
||||
}
|
||||
lineH = std::max(lineH, r.font->LegacySize * 1.4f);
|
||||
if (r.code) {
|
||||
std::string trimmed = word;
|
||||
while (!trimmed.empty() && trimmed.back() == ' ') trimmed.pop_back();
|
||||
float tw = r.font->CalcTextSizeA(r.font->LegacySize, FLT_MAX, 0, trimmed.c_str()).x;
|
||||
dl->AddRectFilled(ImVec2(x - 2.0f * dp, y), ImVec2(x + tw + 2.0f * dp, y + r.font->LegacySize + 3.0f * dp),
|
||||
WithAlpha(OnSurface(), 26), 3.0f * dp);
|
||||
}
|
||||
dl->AddText(r.font, r.font->LegacySize, ImVec2(x, y), r.col, word.c_str());
|
||||
x += wW;
|
||||
}
|
||||
}
|
||||
y += (lineH > 0.0f ? lineH : fBody->LegacySize * 1.4f);
|
||||
};
|
||||
|
||||
std::istringstream ss(md);
|
||||
std::string raw;
|
||||
bool prevBlank = true;
|
||||
while (std::getline(ss, raw)) {
|
||||
if (!raw.empty() && raw.back() == '\r') raw.pop_back();
|
||||
size_t a = raw.find_first_not_of(" \t");
|
||||
std::string t = (a == std::string::npos) ? std::string() : raw.substr(a);
|
||||
|
||||
if (t.empty()) { y += fBody->LegacySize * 0.55f; prevBlank = true; continue; }
|
||||
|
||||
if (t[0] == '#') { // heading
|
||||
size_t h = t.find_first_not_of('#');
|
||||
std::string txt;
|
||||
if (h != std::string::npos) {
|
||||
size_t ts = t.find_first_not_of(" \t", h);
|
||||
if (ts != std::string::npos) txt = t.substr(ts);
|
||||
}
|
||||
if (!prevBlank) y += fBody->LegacySize * 0.5f; // breathing room above a heading
|
||||
ImFont* hf = (h <= 1) ? fH1 : fH2;
|
||||
layout(parseInline(txt, hf, cText), xLeft, xLeft);
|
||||
prevBlank = false;
|
||||
continue;
|
||||
}
|
||||
if ((t[0] == '*' || t[0] == '-' || t[0] == '+') && t.size() > 1 && t[1] == ' ') { // bullet
|
||||
const float indent = fBody->LegacySize * 1.15f;
|
||||
dl->AddText(fBody, fBody->LegacySize, ImVec2(xLeft + fBody->LegacySize * 0.35f, y),
|
||||
cBullet, "\xE2\x80\xA2"); // •
|
||||
layout(parseInline(t.substr(2), fBody, cText), xLeft + indent, xLeft + indent);
|
||||
prevBlank = false;
|
||||
continue;
|
||||
}
|
||||
layout(parseInline(t, fBody, cText), xLeft, xLeft); // paragraph
|
||||
prevBlank = false;
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(origin);
|
||||
ImGui::Dummy(ImVec2(wrapW, std::max(0.0f, y - origin.y)));
|
||||
}
|
||||
|
||||
} // namespace material
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "../windows/export_transactions_dialog.h"
|
||||
#include "../windows/bootstrap_download_dialog.h"
|
||||
#include "../windows/daemon_download_dialog.h"
|
||||
#include "../windows/wallets_dialog.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "imgui.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
@@ -1340,6 +1341,11 @@ void RenderSettingsPage(App* app) {
|
||||
if (TactileButton(TR("seed_migrate_button"), ImVec2(0, 0), btnFont))
|
||||
app->showSeedMigrationDialog();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_migrate"));
|
||||
// Multi-wallet: list wallet files + switch the active one.
|
||||
ImGui::SameLine(0, scaledSp);
|
||||
if (TactileButton(TR("wallets_button"), ImVec2(0, 0), btnFont))
|
||||
ui::WalletsDialog::show(app);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wallets_button"));
|
||||
}
|
||||
ImGui::SameLine(0, scaledSp);
|
||||
if (TactileButton(r1[4], ImVec2(0, 0), btnFont))
|
||||
@@ -2128,49 +2134,68 @@ void RenderSettingsPage(App* app) {
|
||||
return std::string(buf);
|
||||
};
|
||||
|
||||
const ImVec4 dbDim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Inline single line: Installed <v> (<size> · <date>) · Bundled <v> (<size>) · <status>
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(TR("daemon_installed"));
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
// Status line: Installed <v> (<size> · <date>) · Bundled <v> (<size>) · <status>.
|
||||
// Built as color-tagged atoms and laid out with manual wrapping — as one long inline
|
||||
// row of SameLine() Text calls it overran the content width and clipped its colored
|
||||
// tail at narrow windows / higher font scale (the status was effectively hidden).
|
||||
struct DbAtom { std::string text; ImU32 col; bool sep; };
|
||||
std::vector<DbAtom> dbAtoms;
|
||||
const ImU32 dbNorm = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
const ImU32 dbDimU = OnSurfaceMedium();
|
||||
|
||||
dbAtoms.push_back({ TR("daemon_installed"), dbNorm, false });
|
||||
if (inst.exists) {
|
||||
ImGui::TextUnformatted(inst.version.empty() ? TR("unknown") : inst.version.c_str());
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
ImGui::TextColored(dbDim, "(%s · %s)",
|
||||
util::Platform::formatFileSize(inst.size).c_str(),
|
||||
fmtDate(inst.modifiedEpoch).c_str());
|
||||
dbAtoms.push_back({ inst.version.empty() ? std::string(TR("unknown")) : inst.version, dbNorm, false });
|
||||
char dbParen[96];
|
||||
std::snprintf(dbParen, sizeof(dbParen), "(%s · %s)",
|
||||
util::Platform::formatFileSize(inst.size).c_str(),
|
||||
fmtDate(inst.modifiedEpoch).c_str());
|
||||
dbAtoms.push_back({ dbParen, dbDimU, false });
|
||||
} else {
|
||||
ImGui::TextColored(dbDim, "%s", TR("daemon_not_installed"));
|
||||
dbAtoms.push_back({ TR("daemon_not_installed"), dbDimU, false });
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
ImGui::TextColored(dbDim, "·");
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(TR("daemon_bundled"));
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
dbAtoms.push_back({ "·", dbDimU, true });
|
||||
dbAtoms.push_back({ TR("daemon_bundled"), dbNorm, false });
|
||||
if (bun.available) {
|
||||
ImGui::TextUnformatted(bun.version.empty() ? TR("unknown") : bun.version.c_str());
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
ImGui::TextColored(dbDim, "(%s)", util::Platform::formatFileSize(bun.size).c_str());
|
||||
dbAtoms.push_back({ bun.version.empty() ? std::string(TR("unknown")) : bun.version, dbNorm, false });
|
||||
dbAtoms.push_back({ "(" + util::Platform::formatFileSize(bun.size) + ")", dbDimU, false });
|
||||
} else {
|
||||
ImGui::TextColored(dbDim, "%s", TR("daemon_none_bundled"));
|
||||
dbAtoms.push_back({ TR("daemon_none_bundled"), dbDimU, false });
|
||||
}
|
||||
if (bun.available) {
|
||||
const bool sameSize = inst.exists && inst.size == bun.size;
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
ImGui::TextColored(dbDim, "·");
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
dbAtoms.push_back({ "·", dbDimU, true });
|
||||
if (!inst.exists)
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_missing"));
|
||||
dbAtoms.push_back({ TR("daemon_status_missing"), ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 0.8f, 0.2f, 1.0f)), false });
|
||||
else if (sameSize)
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("daemon_status_match"));
|
||||
dbAtoms.push_back({ TR("daemon_status_match"), ImGui::ColorConvertFloat4ToU32(ImVec4(0.3f, 0.8f, 0.3f, 1.0f)), false });
|
||||
else
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ"));
|
||||
dbAtoms.push_back({ TR("daemon_status_differ"), ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 0.8f, 0.2f, 1.0f)), false });
|
||||
}
|
||||
|
||||
// Lay the atoms out left-to-right, wrapping to a new line when the next one would
|
||||
// exceed the content width. A "·" separator is dropped when it (plus the atom after
|
||||
// it) doesn't fit, so a wrapped line never starts or ends with a dangling separator.
|
||||
const float dbAvail = ImGui::GetContentRegionAvail().x;
|
||||
const float dbGap = Layout::spacingXs();
|
||||
float dbX = 0.0f;
|
||||
ImGui::AlignTextToFramePadding();
|
||||
for (std::size_t i = 0; i < dbAtoms.size(); ++i) {
|
||||
const float w = ImGui::CalcTextSize(dbAtoms[i].text.c_str()).x;
|
||||
if (dbAtoms[i].sep) {
|
||||
const float nextW = (i + 1 < dbAtoms.size())
|
||||
? ImGui::CalcTextSize(dbAtoms[i + 1].text.c_str()).x : 0.0f;
|
||||
if (dbX > 0.0f && dbX + dbGap + w + dbGap + nextW > dbAvail) continue;
|
||||
}
|
||||
const bool wrap = (dbX > 0.0f) && (dbX + dbGap + w > dbAvail);
|
||||
if (dbX > 0.0f && !wrap) { ImGui::SameLine(0, dbGap); dbX += dbGap; }
|
||||
else if (wrap) { dbX = 0.0f; }
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(dbAtoms[i].col), "%s", dbAtoms[i].text.c_str());
|
||||
dbX += w;
|
||||
}
|
||||
|
||||
// In-app node update: download + verify the latest dragonxd from the project Gitea.
|
||||
@@ -2510,6 +2535,10 @@ void RenderSettingsPage(App* app) {
|
||||
if (TactileButton(TR("screenshot_sweep"), ImVec2(0, 0), S.resolveFont("button")))
|
||||
app->startScreenshotSweep();
|
||||
ImGui::SameLine();
|
||||
// Full UI sweep: also captures every modal / dialog / flow / state (demo data, offline).
|
||||
if (TactileButton(TR("screenshot_sweep_full"), ImVec2(0, 0), S.resolveFont("button")))
|
||||
app->startFullUiSweep();
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button")))
|
||||
util::Platform::openFolder(app->screenshotDir());
|
||||
if (chat::hushChatFeatureEnabledAtBuild()) {
|
||||
|
||||
@@ -24,7 +24,16 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
auto closeBtn = S.button("dialogs.about", "close-button");
|
||||
auto versionLbl = S.label("dialogs.about", "version-label");
|
||||
auto editionLbl = S.label("dialogs.about", "edition-label");
|
||||
|
||||
|
||||
// Schema positions/widths are logical px. BeginOverlayDialog scales the card by dpiScale(), so
|
||||
// scale the value-column x, the edition inset, and the button widths to match — raw values left
|
||||
// the value column jammed against the labels and the buttons left-packed at higher DPI.
|
||||
const float dp = Layout::dpiScale();
|
||||
const float valX = versionLbl.position * dp;
|
||||
const float edX = editionLbl.position * dp;
|
||||
const float linkW = linkBtn.width * dp;
|
||||
const float closeW = closeBtn.width * dp;
|
||||
|
||||
if (!material::BeginOverlayDialog(TR("about_title"), p_open, win.width, 0.94f)) {
|
||||
return;
|
||||
}
|
||||
@@ -39,7 +48,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::PopFont();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - editionLbl.position);
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - edX);
|
||||
ImGui::TextDisabled("%s", TR("about_edition"));
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -48,24 +57,24 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
|
||||
// Version info
|
||||
ImGui::Text("%s", TR("about_version"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("%s", DRAGONX_VERSION);
|
||||
|
||||
ImGui::Text("%s", TR("about_imgui"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("%s", IMGUI_VERSION);
|
||||
|
||||
ImGui::Text("%s", TR("about_build_date"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("%s %s", __DATE__, __TIME__);
|
||||
|
||||
#ifdef DRAGONX_DEBUG
|
||||
ImGui::Text("%s", TR("about_build_type"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("about_debug"));
|
||||
#else
|
||||
ImGui::Text("%s", TR("about_build_type"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("%s", TR("about_release"));
|
||||
#endif
|
||||
|
||||
@@ -76,20 +85,20 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("%s", TR("about_daemon"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("connected"));
|
||||
|
||||
const auto& state = app->getWalletState();
|
||||
ImGui::Text("%s", TR("about_chain"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("ObsidianDragon");
|
||||
|
||||
ImGui::Text("%s", TR("about_block_height"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("%d", state.sync.blocks);
|
||||
|
||||
ImGui::Text("%s", TR("about_connections"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text(TR("about_peers_count"), state.peers.size());
|
||||
}
|
||||
|
||||
@@ -121,22 +130,22 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Links
|
||||
if (material::StyledButton(TR("about_website"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::StyledButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
util::Platform::openUrl("https://dragonx.is");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("about_github"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::StyledButton(TR("about_github"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
util::Platform::openUrl("https://explorer.dragonx.is");
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Close button
|
||||
float button_width = closeBtn.width;
|
||||
float button_width = closeW;
|
||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f);
|
||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
||||
*p_open = false;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "../../util/i18n.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/type.h"
|
||||
#include "../layout.h" // Layout::dpiScale()
|
||||
#include "imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -134,8 +135,11 @@ void RenderChatTab(App* app)
|
||||
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const float listW = std::clamp(avail.x * 0.32f, 220.0f, 360.0f);
|
||||
const float pad = 10.0f;
|
||||
const float rowH = 52.0f;
|
||||
// Row geometry is logical px — scale by dpiScale() so rows/padding grow with the (DPI-scaled)
|
||||
// fonts. Left raw, at higher DPI the row was too short for the enlarged text and the preview's
|
||||
// right margin (rowW - pad) shrank to ~zero, clipping the last glyph mid-word.
|
||||
const float pad = 10.0f * Layout::dpiScale();
|
||||
const float rowH = 52.0f * Layout::dpiScale();
|
||||
|
||||
ImFont* nameFont = material::Type().body2();
|
||||
ImFont* metaFont = material::Type().caption();
|
||||
|
||||
@@ -1391,13 +1391,15 @@ void ConsoleTab::renderCommandsPopup()
|
||||
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;
|
||||
}
|
||||
|
||||
Type().text(TypeStyle::H6, TR("console_rpc_reference"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Search filter
|
||||
static char cmdFilter[128] = {0};
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
|
||||
@@ -47,7 +47,11 @@ public:
|
||||
* Must be called outside any child window so the modal blocks all input.
|
||||
*/
|
||||
void renderCommandsPopupModal();
|
||||
|
||||
|
||||
// Debug/UI-sweep hook: force the RPC command-reference popup open/closed so the full UI sweep
|
||||
// can capture it (the popup is otherwise opened only by a toolbar button).
|
||||
void sweepSetCommandsPopup(bool open) { show_commands_popup_ = open; }
|
||||
|
||||
/**
|
||||
* @brief Add a line to the console output
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,7 @@ static int s_confirm_delete_idx = -1; // armed storage index; a 2nd Delete co
|
||||
static char s_edit_label[128] = "";
|
||||
static char s_edit_address[512] = "";
|
||||
static char s_edit_notes[512] = "";
|
||||
static bool s_edit_global = false; // add/edit dialog: "visible in every wallet" toggle
|
||||
static char s_search[128] = "";
|
||||
|
||||
static void copyEditField(char* dest, size_t destSize, const std::string& source) {
|
||||
@@ -69,16 +70,22 @@ void RenderContactsTab(App* app)
|
||||
|
||||
auto& book = app->addressBook();
|
||||
|
||||
// The active wallet's identity hash scopes which contacts show + tags newly-added ones.
|
||||
// Empty pre-connect (addresses unknown) — then we don't filter, to avoid hiding contacts.
|
||||
const std::string activeHash = app->activeWalletIdentityHash();
|
||||
|
||||
auto clearEditFields = []() {
|
||||
s_edit_label[0] = '\0';
|
||||
s_edit_address[0] = '\0';
|
||||
s_edit_notes[0] = '\0';
|
||||
s_edit_global = false; // new contacts default to the current wallet
|
||||
};
|
||||
|
||||
auto loadEditFields = [](const data::AddressBookEntry& entry) {
|
||||
copyEditField(s_edit_label, sizeof(s_edit_label), entry.label);
|
||||
copyEditField(s_edit_address, sizeof(s_edit_address), entry.address);
|
||||
copyEditField(s_edit_notes, sizeof(s_edit_notes), entry.notes);
|
||||
s_edit_global = entry.isGlobal();
|
||||
};
|
||||
|
||||
bool has_selection = s_selected_index >= 0 && s_selected_index < static_cast<int>(book.size());
|
||||
@@ -149,6 +156,10 @@ void RenderContactsTab(App* app)
|
||||
material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes",
|
||||
s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Checkbox(TR("contact_global"), &s_edit_global);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_tt"));
|
||||
|
||||
bool canSubmit = std::strlen(s_edit_label) > 0 && std::strlen(s_edit_address) > 0;
|
||||
float totalActionsW = actionW * 2.0f + actionGap;
|
||||
material::BeginOverlayDialogFooter(totalActionsW);
|
||||
@@ -164,6 +175,9 @@ void RenderContactsTab(App* app)
|
||||
return s;
|
||||
};
|
||||
data::AddressBookEntry entry(trimAB(s_edit_label), trimAB(s_edit_address), s_edit_notes);
|
||||
// Global if the user asked, or as a safe fallback when we don't yet know the wallet
|
||||
// (pre-connect) — better a visible-everywhere contact than one orphaned to no wallet.
|
||||
entry.scope = (s_edit_global || activeHash.empty()) ? std::string("global") : activeHash;
|
||||
if (isEdit) {
|
||||
if (app->addressBook().updateEntry(s_selected_index, entry)) {
|
||||
Notifications::instance().success(TR("address_book_updated"));
|
||||
@@ -242,7 +256,12 @@ void RenderContactsTab(App* app)
|
||||
std::vector<size_t> visibleRows;
|
||||
visibleRows.reserve(book.size());
|
||||
for (size_t i = 0; i < book.size(); ++i) {
|
||||
if (matchesSearch(book.entries()[i], needle)) visibleRows.push_back(i);
|
||||
const auto& e = book.entries()[i];
|
||||
if (!matchesSearch(e, needle)) continue;
|
||||
// Scope filter: global contacts + this wallet's contacts. Pre-connect (activeHash empty)
|
||||
// we don't know the wallet yet, so show everything rather than hide contacts.
|
||||
if (!activeHash.empty() && !e.visibleInWallet(activeHash)) continue;
|
||||
visibleRows.push_back(i);
|
||||
}
|
||||
|
||||
// Address list — size the table to fill the tab, leaving room for the count footer.
|
||||
@@ -328,6 +347,13 @@ void RenderContactsTab(App* app)
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s", entry.address.c_str());
|
||||
}
|
||||
// Globe badge marks a global contact (shown in every wallet).
|
||||
if (entry.isGlobal()) {
|
||||
ImGui::SameLine(0.0f, 8.0f);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()),
|
||||
"%s", ICON_MD_PUBLIC);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_badge_tt"));
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(entry.notes.c_str());
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Modal dialog to download / update the dragonxd full node from the project Gitea, driven by
|
||||
// util::DaemonUpdater. Sibling of XmrigDownloadDialog (the miner updater). States follow the
|
||||
// updater: Checking -> UpToDate/UpdateAvailable -> Downloading/Verifying/Extracting -> Done /
|
||||
// Failed. Header-only (static inline state). The new binary is installed into the daemon directory
|
||||
// without touching the running node; on Done the user is offered a daemon restart to apply it.
|
||||
// util::DaemonUpdater. Two-pane layout (mirrors the Manage-Portfolio editor): the list of node
|
||||
// versions on the LEFT, the selected version's detail + install action on the RIGHT. States follow
|
||||
// the updater: Listing -> ReleaseList -> Downloading/Verifying/Extracting -> Done / Failed. The new
|
||||
// binary is installed into the daemon directory without touching the running node; on Done the user
|
||||
// is offered a daemon restart to apply it. Header-only (static inline state).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -20,6 +24,7 @@
|
||||
#include "../../util/i18n.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/markdown.h"
|
||||
#include "../material/type.h"
|
||||
#include "../theme.h"
|
||||
#include "release_list_view.h"
|
||||
@@ -30,8 +35,11 @@ namespace ui {
|
||||
|
||||
class DaemonUpdateDialog {
|
||||
public:
|
||||
using Progress = util::DaemonUpdater::Progress;
|
||||
using St = util::DaemonUpdater::State;
|
||||
|
||||
// `installedVersion` is the version scanned from the installed dragonxd (may be empty/unknown),
|
||||
// used to tell "up to date" from "update available".
|
||||
// used to badge the installed release and tell "up to date" from "update available".
|
||||
static void show(App* app, const std::string& installedVersion) {
|
||||
if (!app) return;
|
||||
s_app = app;
|
||||
@@ -39,10 +47,12 @@ public:
|
||||
s_notified = false;
|
||||
s_installed_flag = false; // start each session clean (don't leak a prior install's flag)
|
||||
s_installed_version = installedVersion;
|
||||
s_sel = 0;
|
||||
s_sweep = false;
|
||||
s_rows.clear();
|
||||
s_releases.clear();
|
||||
s_updater = std::make_unique<util::DaemonUpdater>();
|
||||
s_updater->startCheck(installedVersion);
|
||||
s_updater->startListReleases(); // fetch every version up front for the two-pane picker
|
||||
}
|
||||
|
||||
static bool isOpen() { return s_open; }
|
||||
@@ -55,135 +65,359 @@ public:
|
||||
return v;
|
||||
}
|
||||
|
||||
// Sweep-only: seed fake releases + a forced state so the offline UI sweep can render the modal
|
||||
// without a network fetch or a live updater.
|
||||
static void sweepSeed(App* app, std::vector<util::DaemonRelease> releases, St state,
|
||||
const std::string& installedVersion) {
|
||||
s_app = app;
|
||||
s_open = true;
|
||||
s_notified = false;
|
||||
s_installed_version = installedVersion;
|
||||
s_sel = 0;
|
||||
s_sweep = true;
|
||||
s_releases = std::move(releases);
|
||||
s_rows.clear();
|
||||
s_updater.reset();
|
||||
s_sweepProgress = Progress{};
|
||||
s_sweepProgress.state = state;
|
||||
if (!s_releases.empty()) s_sweepProgress.latest_tag = s_releases.front().tag;
|
||||
s_sweepProgress.installed_tag = installedVersion;
|
||||
s_sweepProgress.percent = 62.0f;
|
||||
s_sweepProgress.status_text = "dragonx-1.0.3-linux-amd64.zip";
|
||||
}
|
||||
static void sweepClose() { s_open = false; s_sweep = false; s_releases.clear(); s_rows.clear(); }
|
||||
|
||||
static void render() {
|
||||
if (!s_open || !s_app || !s_updater) {
|
||||
if (!s_open) s_updater.reset(); // closed: drop the updater (dtor joins the worker)
|
||||
if (!s_open || !s_app) {
|
||||
if (!s_open) s_updater.reset();
|
||||
return;
|
||||
}
|
||||
using namespace material;
|
||||
const float dp = Layout::dpiScale();
|
||||
const auto p = s_updater->getProgress();
|
||||
const Progress p = s_sweep ? s_sweepProgress
|
||||
: (s_updater ? s_updater->getProgress() : Progress{});
|
||||
// No window X during an active download/verify/extract — closing would orphan/block on the
|
||||
// worker; the in-dialog Cancel is the intended way to abort.
|
||||
const bool active = (p.state == St::Downloading || p.state == St::Verifying || p.state == St::Extracting);
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("daemon_update_title"); ov.p_open = active ? nullptr : &s_open;
|
||||
ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 480.0f; ov.cardBottomViewportRatio = 0.94f;
|
||||
ov.title = TR("daemon_update_title");
|
||||
ov.p_open = active ? nullptr : &s_open;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 880.0f;
|
||||
// Tall card (a generous share of the window) so the release notes get vertical room and
|
||||
// usually don't need to scroll; the layout is GetContentRegionAvail-based, so it adapts.
|
||||
ov.cardHeight = std::min(760.0f, ImGui::GetMainViewport()->Size.y * 0.84f / dp);
|
||||
ov.idSuffix = "daemonupd";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
switch (p.state) {
|
||||
case St::Checking: renderChecking(dp, p); break;
|
||||
case St::Idle:
|
||||
case St::Checking:
|
||||
case St::UpToDate:
|
||||
case St::UpdateAvailable: renderPrompt(dp, p); break;
|
||||
case St::Unavailable: renderUnavailable(dp, p); break;
|
||||
case St::Listing: renderListing(dp, p); break;
|
||||
case St::ReleaseList: renderReleaseList(dp, p); break;
|
||||
case St::Downloading:
|
||||
case St::Verifying:
|
||||
case St::Extracting: renderProgress(dp, p); break;
|
||||
case St::Done: renderDone(dp, p); break;
|
||||
case St::Failed: renderFailed(dp, p); break;
|
||||
default: break;
|
||||
case St::UpdateAvailable:
|
||||
case St::Listing: renderCentered(TR("daemon_update_loading")); break;
|
||||
case St::Unavailable: renderUnavailable(p); break;
|
||||
case St::Failed:
|
||||
// A failed release-list fetch has no versions to show — render a plain retry rather
|
||||
// than an empty two-pane. (An install that failed keeps the list, so falls through.)
|
||||
if (s_releases.empty()) { renderListFailed(p); break; }
|
||||
renderTwoPane(dp, p, active); break;
|
||||
default: renderTwoPane(dp, p, active); break;
|
||||
}
|
||||
EndOverlayDialog();
|
||||
}
|
||||
if (!s_open) s_updater.reset();
|
||||
if (!s_open) { s_updater.reset(); s_sweep = false; }
|
||||
}
|
||||
|
||||
private:
|
||||
using Progress = util::DaemonUpdater::Progress;
|
||||
using St = util::DaemonUpdater::State;
|
||||
|
||||
static float fullW() { return ImGui::GetContentRegionAvail().x; }
|
||||
|
||||
static void installAction(const char* label) {
|
||||
static void renderCentered(const char* msg) {
|
||||
using namespace material;
|
||||
if (TactileButton(label, ImVec2(fullW(), 0)))
|
||||
s_updater->startInstall(resources::getDaemonDirectory());
|
||||
ImVec2 av = ImGui::GetContentRegionAvail();
|
||||
ImFont* f = Type().body2();
|
||||
ImVec2 ts = f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, msg);
|
||||
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + std::max(0.0f, (av.x - ts.x) * 0.5f),
|
||||
ImGui::GetCursorPosY() + av.y * 0.45f));
|
||||
Type().text(TypeStyle::Body2, msg);
|
||||
}
|
||||
|
||||
static void renderChecking(float, const Progress&) {
|
||||
// A failed release-list fetch (no versions to show) — a plain centered retry / close.
|
||||
static void renderListFailed(const Progress& p) {
|
||||
using namespace material;
|
||||
Type().text(TypeStyle::Body2, TR("daemon_update_checking"));
|
||||
}
|
||||
|
||||
static void renderUnavailable(float, const Progress& p) {
|
||||
using namespace material;
|
||||
Type().text(TypeStyle::Subtitle2, TR("daemon_update_unavailable_title"));
|
||||
const float bw = std::min(fullW(), 220.0f * Layout::dpiScale());
|
||||
Type().textColored(TypeStyle::Subtitle2, Error(), TR("daemon_update_failed"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", p.status_text.empty()
|
||||
? TR("daemon_update_unavailable_body")
|
||||
: p.status_text.c_str());
|
||||
ImGui::TextWrapped("%s", p.error.empty() ? TR("daemon_update_unknown_error") : p.error.c_str());
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||
}
|
||||
|
||||
static void renderPrompt(float, const Progress& p) {
|
||||
using namespace material;
|
||||
const std::string installed = p.installed_tag.empty() ? TR("xmrig_none") : p.installed_tag;
|
||||
if (p.state == St::UpdateAvailable) {
|
||||
Type().text(TypeStyle::Subtitle2, TR("daemon_update_available"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s %s", TR("daemon_update_latest"), p.latest_tag.c_str());
|
||||
ImGui::Text("%s %s", TR("daemon_update_installed"), installed.c_str());
|
||||
ImGui::Spacing();
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_verify_note"));
|
||||
ImGui::Spacing();
|
||||
installAction(TR("daemon_update_download_install"));
|
||||
} else {
|
||||
Type().text(TypeStyle::Subtitle2, TR("daemon_update_up_to_date"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s %s", TR("daemon_update_installed"), p.latest_tag.c_str());
|
||||
ImGui::Spacing();
|
||||
installAction(TR("daemon_update_reinstall"));
|
||||
}
|
||||
ImGui::Spacing();
|
||||
// Browse all releases (pre-releases / older versions).
|
||||
if (TactileButton(TR("daemon_update_browse"), ImVec2(fullW(), 0))) {
|
||||
if (TactileButton(TR("retry"), ImVec2(bw, 0)) && s_updater) {
|
||||
s_releases.clear();
|
||||
s_rows.clear();
|
||||
s_updater->startListReleases();
|
||||
}
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||
if (TactileButton(TR("close"), ImVec2(bw, 0))) s_open = false;
|
||||
}
|
||||
|
||||
static void renderListing(float, const Progress&) {
|
||||
static void renderUnavailable(const Progress& p) {
|
||||
using namespace material;
|
||||
Type().text(TypeStyle::Body2, TR("daemon_update_loading"));
|
||||
}
|
||||
|
||||
static void renderReleaseList(float dp, const Progress&) {
|
||||
using namespace material;
|
||||
// Snapshot the worker's list + build the rows once per listing (not every frame — the
|
||||
// release bodies are large). Caches are cleared when a new listing starts (browse / show).
|
||||
if (s_rows.empty()) {
|
||||
s_releases = s_updater->getReleases();
|
||||
const std::string token = util::currentDaemonPlatformToken();
|
||||
const std::string instCore = util::daemonVersionCore(s_installed_version);
|
||||
s_rows.reserve(s_releases.size());
|
||||
for (const auto& r : s_releases) {
|
||||
ReleaseRow row;
|
||||
row.tag = r.tag;
|
||||
row.title = r.name;
|
||||
row.date = r.publishedAt.size() >= 10 ? r.publishedAt.substr(0, 10) : r.publishedAt;
|
||||
row.prerelease = r.prerelease;
|
||||
row.hasAsset = util::selectDaemonAsset(r, token) >= 0;
|
||||
row.installed = !instCore.empty() && util::daemonVersionCore(r.tag) == instCore;
|
||||
s_rows.push_back(std::move(row));
|
||||
}
|
||||
}
|
||||
// Downgrade caution: an older node binary may not match the current chain data.
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_downgrade_note"));
|
||||
Type().text(TypeStyle::Subtitle2, TR("daemon_update_unavailable_title"));
|
||||
ImGui::Spacing();
|
||||
bool back = false;
|
||||
const int idx = RenderReleaseList(s_rows, dp, &back);
|
||||
if (back) { s_rows.clear(); s_updater->startCheck(s_installed_version); return; }
|
||||
if (idx >= 0 && idx < static_cast<int>(s_releases.size())) {
|
||||
const util::DaemonRelease rel = s_releases[idx];
|
||||
s_rows.clear();
|
||||
s_updater->startInstallRelease(resources::getDaemonDirectory(), rel);
|
||||
ImGui::TextWrapped("%s", p.status_text.empty() ? TR("daemon_update_unavailable_body")
|
||||
: p.status_text.c_str());
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("close"), ImVec2(std::min(fullW(), 200.0f * Layout::dpiScale()), 0)))
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
// Build the display rows from the fetched releases once per listing (release bodies are large).
|
||||
static void buildRows() {
|
||||
if (!s_rows.empty() || s_releases.empty()) return;
|
||||
const std::string token = util::currentDaemonPlatformToken();
|
||||
const std::string instCore = util::daemonVersionCore(s_installed_version);
|
||||
s_rows.reserve(s_releases.size());
|
||||
for (const auto& r : s_releases) {
|
||||
ReleaseRow row;
|
||||
row.tag = r.tag;
|
||||
row.title = r.name;
|
||||
row.date = r.publishedAt.size() >= 10 ? r.publishedAt.substr(0, 10) : r.publishedAt;
|
||||
row.prerelease = r.prerelease;
|
||||
row.hasAsset = util::selectDaemonAsset(r, token) >= 0;
|
||||
row.installed = !instCore.empty() && util::daemonVersionCore(r.tag) == instCore;
|
||||
s_rows.push_back(std::move(row));
|
||||
}
|
||||
}
|
||||
|
||||
// Release-notes markdown with the machine-readable checksum table / signature block cut off (the
|
||||
// body ends with a "| file | sha |" table). Keeps the markdown so RenderMarkdown can style it.
|
||||
static std::string cleanNotes(const std::string& body) {
|
||||
std::istringstream ss(body);
|
||||
std::string line, out;
|
||||
while (std::getline(ss, line)) {
|
||||
size_t a = line.find_first_not_of(" \t");
|
||||
std::string lt = (a == std::string::npos) ? std::string() : line.substr(a);
|
||||
std::string low = lt;
|
||||
std::transform(low.begin(), low.end(), low.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
if (!lt.empty() && lt[0] == '|') break; // markdown table (checksums)
|
||||
if (low.rfind("## checksum", 0) == 0 || low.rfind("#### checksum", 0) == 0 ||
|
||||
low.rfind("checksums", 0) == 0 || low.rfind("### sha", 0) == 0) break;
|
||||
out += line;
|
||||
out += '\n';
|
||||
}
|
||||
while (!out.empty() && (out.back() == '\n' || out.back() == '\r' ||
|
||||
out.back() == ' ' || out.back() == '\t')) out.pop_back();
|
||||
return out;
|
||||
}
|
||||
|
||||
static void startInstallSelected() {
|
||||
if (!s_updater || s_sel < 0 || s_sel >= (int)s_releases.size()) return;
|
||||
const util::DaemonRelease rel = s_releases[s_sel];
|
||||
s_updater->startInstallRelease(resources::getDaemonDirectory(), rel);
|
||||
}
|
||||
|
||||
// ================= Two-pane: version list (left) + detail/progress (right) =================
|
||||
static void renderTwoPane(float dp, const Progress& p, bool active) {
|
||||
using namespace material;
|
||||
// Snapshot the worker's release list once it's available (mutex-guarded getReleases()); the
|
||||
// sweep seeds s_releases directly, so don't clobber it there.
|
||||
if (!s_sweep && s_releases.empty() && s_updater) s_releases = s_updater->getReleases();
|
||||
buildRows();
|
||||
if (s_sel >= (int)s_rows.size()) s_sel = s_rows.empty() ? 0 : (int)s_rows.size() - 1;
|
||||
|
||||
ImFont* bodyF = Type().body2();
|
||||
ImFont* capF = Type().caption();
|
||||
|
||||
const bool showFooter = (p.state == St::ReleaseList);
|
||||
// Budget the footer block (a gap + the enlarged Close button, no divider) so the two-pane
|
||||
// body leaves exact room and nothing overflows the card.
|
||||
const float footerH = showFooter
|
||||
? (ImGui::GetFrameHeight() * 1.25f + ImGui::GetStyle().ItemSpacing.y * 2.0f + Layout::spacingSm())
|
||||
: 0.0f;
|
||||
const float bodyH = std::max(120.0f * dp, ImGui::GetContentRegionAvail().y - footerH);
|
||||
const float contentW = ImGui::GetContentRegionAvail().x;
|
||||
const float gap = Layout::spacingLg();
|
||||
const float masterW = std::min(std::max(contentW * 0.34f, 240.0f * dp), 340.0f * dp);
|
||||
const float detailW = contentW - masterW - gap;
|
||||
|
||||
// ---- LEFT: scrollable version list — borderless; each version is a tactile Material card ----
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::BeginChild("##daemonVersions", ImVec2(masterW, bodyH), false);
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, Layout::spacingSm()));
|
||||
// Transparent Selectable highlight — we draw our own Material card fill/hover/press.
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0, 0, 0, 0));
|
||||
ImDrawList* mdl = ImGui::GetWindowDrawList();
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const float rowH = Layout::spacingMd() * 2.0f + bodyF->LegacySize + Layout::spacingXs() + capF->LegacySize;
|
||||
const float rowW = ImGui::GetContentRegionAvail().x;
|
||||
const float round = 8.0f * dp;
|
||||
for (int i = 0; i < (int)s_rows.size(); ++i) {
|
||||
const ReleaseRow& r = s_rows[i];
|
||||
ImGui::PushID(i);
|
||||
bool sel = (s_sel == i);
|
||||
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
||||
if (ImGui::Selectable("##ver", sel, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) {
|
||||
if (!active) s_sel = i;
|
||||
}
|
||||
bool hov = ImGui::IsItemHovered();
|
||||
bool held = ImGui::IsItemActive();
|
||||
ImVec2 rmx(rmn.x + rowW, rmn.y + rowH);
|
||||
// Material card: filled surface, brighter on hover, primary-tinted + outlined when selected.
|
||||
ImU32 fill = sel ? WithAlpha(Primary(), held ? 55 : 42)
|
||||
: held ? WithAlpha(OnSurface(), 34)
|
||||
: hov ? WithAlpha(OnSurface(), 24)
|
||||
: WithAlpha(OnSurface(), 12);
|
||||
mdl->AddRectFilled(rmn, rmx, fill, round);
|
||||
if (sel) mdl->AddRect(rmn, rmx, WithAlpha(Primary(), 150), round, 0, 1.6f * dp);
|
||||
if (hov || held) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
// Status — one of active (installed), pre-release, or newest — shown as a small text
|
||||
// label next to the tag AND a matching right-aligned icon badge.
|
||||
const char* ico = nullptr; ImU32 statusC = 0; const char* tt = nullptr; const char* lbl = nullptr;
|
||||
if (r.installed) { ico = ICON_MD_CHECK_CIRCLE; statusC = Success(); tt = TR("daemon_update_active_tt"); lbl = TR("upd_installed_badge"); }
|
||||
else if (r.prerelease) { ico = ICON_MD_SCIENCE; statusC = Warning(); tt = TR("daemon_update_prerelease_tt"); lbl = TR("upd_prerelease"); }
|
||||
else if (i == 0) { ico = ICON_MD_NEW_RELEASES; statusC = Primary(); tt = TR("daemon_update_latest_tt"); lbl = TR("daemon_update_latest_badge"); }
|
||||
float rightLimit = rmx.x - Layout::spacingMd();
|
||||
if (ico) {
|
||||
float cs = bodyF->LegacySize * 1.3f;
|
||||
ImVec2 cp(rmx.x - Layout::spacingMd() - cs, (rmn.y + rmx.y) * 0.5f - cs * 0.5f);
|
||||
mdl->AddText(icoF, cs, cp, statusC, ico);
|
||||
// Tooltip only while the icon itself is hovered — not while merely selected (the
|
||||
// newest version is selected by default, which would pin its tooltip on screen).
|
||||
if (tt && hov && ImGui::IsMouseHoveringRect(cp, ImVec2(cp.x + cs, cp.y + cs)))
|
||||
Tooltip("%s", tt);
|
||||
rightLimit = cp.x - Layout::spacingSm();
|
||||
}
|
||||
float x = rmn.x + Layout::spacingMd();
|
||||
float tagY = rmn.y + Layout::spacingMd();
|
||||
mdl->AddText(bodyF, bodyF->LegacySize, ImVec2(x, tagY),
|
||||
r.hasAsset ? OnSurface() : OnSurfaceDisabled(), r.tag.c_str());
|
||||
if (lbl) {
|
||||
float bx = x + bodyF->CalcTextSizeA(bodyF->LegacySize, FLT_MAX, 0, r.tag.c_str()).x + Layout::spacingSm();
|
||||
float by = tagY + (bodyF->LegacySize - capF->LegacySize) * 0.5f;
|
||||
if (bx + capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, lbl).x <= rightLimit)
|
||||
mdl->AddText(capF, capF->LegacySize, ImVec2(bx, by), statusC, lbl);
|
||||
}
|
||||
if (!r.date.empty())
|
||||
mdl->AddText(capF, capF->LegacySize,
|
||||
ImVec2(x, rmx.y - Layout::spacingMd() - capF->LegacySize),
|
||||
OnSurfaceMedium(), r.date.c_str());
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::PopStyleVar(); // ItemSpacing
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(); // WindowPadding
|
||||
|
||||
ImGui::SameLine(0.0f, gap);
|
||||
|
||||
// ---- RIGHT: detail / progress / done / failed for the selected version ----
|
||||
ImGui::BeginChild("##daemonDetail", ImVec2(detailW, bodyH), false);
|
||||
switch (p.state) {
|
||||
case St::ReleaseList: renderDetail(dp, p); break;
|
||||
case St::Downloading:
|
||||
case St::Verifying:
|
||||
case St::Extracting: renderProgress(dp, p); break;
|
||||
case St::Done: renderDone(dp, p); break;
|
||||
case St::Failed: renderFailed(dp, p); break;
|
||||
default: break;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
// ---- Footer: Close (only while browsing; transient states carry their own actions). No
|
||||
// divider above it — a larger, right-aligned button reads as the modal-level dismiss. ----
|
||||
if (showFooter) {
|
||||
ImGui::Spacing();
|
||||
const float bw = 170.0f * dp, bh = ImGui::GetFrameHeight() * 1.25f;
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - bw));
|
||||
if (TactileButton(TR("close"), ImVec2(bw, bh))) s_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Right pane for the ReleaseList state: the selected version's detail + install action.
|
||||
static void renderDetail(float dp, const Progress&) {
|
||||
using namespace material;
|
||||
if (s_sel < 0 || s_sel >= (int)s_releases.size() || s_sel >= (int)s_rows.size()) return;
|
||||
const util::DaemonRelease& rel = s_releases[s_sel];
|
||||
const ReleaseRow& row = s_rows[s_sel];
|
||||
const std::string instCore = util::daemonVersionCore(s_installed_version);
|
||||
const bool haveInstalled = !instCore.empty();
|
||||
const bool downgrade = !row.installed && s_sel != 0 && haveInstalled;
|
||||
|
||||
const char* noteStr = !row.hasAsset ? TR("upd_no_build_platform")
|
||||
: downgrade ? TR("daemon_update_downgrade_note")
|
||||
: TR("daemon_update_verify_note");
|
||||
const char* label = row.installed ? TR("daemon_update_reinstall")
|
||||
: (s_sel == 0) ? TR("daemon_update_download_install")
|
||||
: TR("daemon_update_install_this");
|
||||
|
||||
// Reserve the bottom for the verify/downgrade note + the install button, so the info card
|
||||
// fills the space above them.
|
||||
const float btnH = ImGui::GetFrameHeight() * 1.15f;
|
||||
const float noteH = ImGui::CalcTextSize(noteStr, nullptr, false, ImGui::GetContentRegionAvail().x).y;
|
||||
const float reserve = noteH + btnH + Layout::spacingMd() * 3.0f;
|
||||
const float infoH = std::max(120.0f * dp, ImGui::GetContentRegionAvail().y - reserve);
|
||||
|
||||
// ---- Info card: ONE rounded Material surface wrapping the version header + release notes.
|
||||
// AlwaysUseWindowPadding is required or a borderless child ignores WindowPadding (which
|
||||
// let the content touch the surface edges). ----
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, WithAlpha(OnSurface(), 22));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f * dp); // slightly smaller than the notes had
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingLg()));
|
||||
ImGui::BeginChild("##daemonInfo", ImVec2(0, infoH), ImGuiChildFlags_AlwaysUseWindowPadding);
|
||||
{
|
||||
// Header (pinned): tag + status + installed/released — no divider, just a gap.
|
||||
Type().text(TypeStyle::H6, rel.tag.c_str());
|
||||
if (row.installed)
|
||||
Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_status_current"));
|
||||
else if (s_sel == 0)
|
||||
Type().textColored(TypeStyle::Subtitle2, Primary(),
|
||||
haveInstalled ? TR("daemon_update_available") : TR("daemon_update_status_latest"));
|
||||
else if (haveInstalled)
|
||||
Type().textColored(TypeStyle::Subtitle2, Warning(), TR("daemon_update_status_older"));
|
||||
else
|
||||
Type().text(TypeStyle::Subtitle2, rel.name.empty() ? rel.tag.c_str() : rel.name.c_str());
|
||||
|
||||
const std::string installed = s_installed_version.empty() ? std::string(TR("xmrig_none"))
|
||||
: s_installed_version;
|
||||
ImGui::Spacing();
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
||||
(std::string(TR("daemon_update_installed")) + " " + installed).c_str());
|
||||
if (!row.date.empty())
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
||||
(std::string(TR("daemon_update_released")) + " " + row.date).c_str());
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Notes: markdown, scrolls within the info card (header stays pinned above it). The info
|
||||
// card's WindowPadding already insets it from the surface edges.
|
||||
const std::string notes = cleanNotes(rel.body);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
|
||||
ImGui::BeginChild("##daemonNotes", ImVec2(0, std::max(0.0f, ImGui::GetContentRegionAvail().y)), false);
|
||||
if (notes.empty())
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("daemon_update_no_notes"));
|
||||
else
|
||||
RenderMarkdown(notes);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// ---- Below the info card (outside the surface): verify note + install button ----
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Caption, downgrade ? Warning() : OnSurfaceMedium(), noteStr);
|
||||
ImGui::Spacing();
|
||||
// Install button sized to its text and centered in the pane.
|
||||
const float bw = ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
|
||||
ImGui::BeginDisabled(!row.hasAsset);
|
||||
if (TactileButton(label, ImVec2(bw, btnH))) startInstallSelected();
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
static void renderProgress(float dp, const Progress& p) {
|
||||
using namespace material;
|
||||
const char* title = p.state == St::Downloading ? TR("daemon_update_downloading")
|
||||
@@ -193,19 +427,16 @@ private:
|
||||
ImGui::Spacing();
|
||||
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s", p.status_text.c_str());
|
||||
ImGui::TextWrapped("%s", p.status_text.c_str());
|
||||
ImGui::Spacing();
|
||||
// Cancel aborts the in-flight transfer promptly (the curl progress callback returns abort).
|
||||
if (TactileButton(TR("cancel"), ImVec2(fullW(), 0))) {
|
||||
s_updater->cancel();
|
||||
if (s_updater) s_updater->cancel();
|
||||
s_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void renderDone(float, const Progress& p) {
|
||||
using namespace material;
|
||||
// Tell the Settings panel to refresh its cached daemon info — once, not every frame (a
|
||||
// re-scan reads the whole daemon binary).
|
||||
if (!s_notified) { s_installed_flag = true; s_notified = true; }
|
||||
Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_installed_ok"));
|
||||
ImGui::Spacing();
|
||||
@@ -231,7 +462,7 @@ private:
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", p.error.empty() ? TR("daemon_update_unknown_error") : p.error.c_str());
|
||||
ImGui::Spacing();
|
||||
installAction(TR("retry"));
|
||||
if (TactileButton(TR("retry"), ImVec2(fullW(), 0))) startInstallSelected();
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||
}
|
||||
@@ -239,9 +470,12 @@ private:
|
||||
static inline bool s_open = false;
|
||||
static inline bool s_installed_flag = false;
|
||||
static inline bool s_notified = false;
|
||||
static inline bool s_sweep = false;
|
||||
static inline int s_sel = 0; // selected version (index into s_releases)
|
||||
static inline std::string s_installed_version;
|
||||
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
|
||||
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
|
||||
static inline std::vector<util::DaemonRelease> s_releases; // matching releases (install by index)
|
||||
static inline Progress s_sweepProgress; // forced state for the offline UI sweep
|
||||
static inline App* s_app = nullptr;
|
||||
static inline std::unique_ptr<util::DaemonUpdater> s_updater;
|
||||
};
|
||||
|
||||
281
src/ui/windows/folder_picker.h
Normal file
281
src/ui/windows/folder_picker.h
Normal file
@@ -0,0 +1,281 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// In-app, Material-styled folder picker. A modal overlay that lets the user browse the filesystem
|
||||
// and pick a directory (used by the Wallets dialog's "Scan another folder" action instead of a
|
||||
// raw path input field). Single overlay at a time — the overlay framework doesn't nest, so the
|
||||
// caller suppresses its own overlay while FolderPicker::isOpen().
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "../layout.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
class FolderPicker {
|
||||
public:
|
||||
// Open the picker starting at `startDir` (falls back to home if empty/invalid). `onPick` is
|
||||
// invoked with the chosen absolute path when the user confirms; nothing runs on cancel.
|
||||
static void open(const std::string& startDir, std::function<void(const std::string&)> onPick) {
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
if (startDir.empty()) {
|
||||
// Reopen where the user last browsed (nice for repeated scans); else start at home.
|
||||
if (s_dir.empty() || !fs::is_directory(fs::path(s_dir), ec))
|
||||
navigate(util::Platform::getHomeDir());
|
||||
else
|
||||
navigate(s_dir);
|
||||
} else {
|
||||
fs::path p(startDir);
|
||||
if (!fs::is_directory(p, ec)) p = fs::path(util::Platform::getHomeDir());
|
||||
navigate(p.string());
|
||||
}
|
||||
s_onPick = std::move(onPick);
|
||||
s_open = true;
|
||||
}
|
||||
|
||||
static bool isOpen() { return s_open; }
|
||||
static void close() { s_open = false; }
|
||||
|
||||
static void render() {
|
||||
if (!s_open) return;
|
||||
using namespace material;
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
ImFont* rowFont = Type().body1();
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
ImFont* metaFont = Type().caption();
|
||||
|
||||
const float rowH = rowFont->LegacySize + Layout::spacingMd();
|
||||
const float listPad = Layout::spacingXs();
|
||||
|
||||
// The card takes a generous share of the window (a file browser wants room) — not a fixed
|
||||
// row count. The list inside FLEXES to fill whatever height is left above the footer, so the
|
||||
// footer is always pinned + visible and nothing clips, at any window size or DPI.
|
||||
const float vpH = ImGui::GetMainViewport()->Size.y;
|
||||
const float cardH = (vpH * 0.82f) / dp; // logical; the framework re-applies dp + caps at vp-32
|
||||
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("picker_title");
|
||||
ov.p_open = &s_open;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 760.0f;
|
||||
ov.cardHeight = cardH;
|
||||
ov.idSuffix = "folderpicker";
|
||||
if (!BeginOverlayDialog(ov)) return;
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_open = false;
|
||||
|
||||
const float fullW = ImGui::GetContentRegionAvail().x;
|
||||
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
|
||||
// End-truncate, popping whole UTF-8 code points.
|
||||
auto fit = [&](std::string s, ImFont* f, float maxW){
|
||||
bool t=false;
|
||||
while (s.size()>1 && tw(f,s)>maxW){
|
||||
while (s.size()>1 && (static_cast<unsigned char>(s.back()) & 0xC0) == 0x80) s.pop_back();
|
||||
if (s.size()>1) s.pop_back();
|
||||
t=true;
|
||||
}
|
||||
if (t) s += "\xE2\x80\xA6";
|
||||
return s;
|
||||
};
|
||||
// Left-truncate a path (keep the tail — the current folder + nearest parents).
|
||||
auto fitPath = [&](std::string s, ImFont* f, float maxW){
|
||||
const std::string ell = "\xE2\x80\xA6";
|
||||
if (tw(f,s) <= maxW) return s;
|
||||
while (s.size()>1 && tw(f, ell+s) > maxW){
|
||||
s.erase(s.begin());
|
||||
while (!s.empty() && (static_cast<unsigned char>(s.front()) & 0xC0) == 0x80) s.erase(s.begin());
|
||||
}
|
||||
return ell + s;
|
||||
};
|
||||
|
||||
// ---- Path bar: Up + Home + current path strip -----------------------------------------
|
||||
std::string pendingNav;
|
||||
{
|
||||
const float bh = ImGui::GetFrameHeight();
|
||||
IconButtonStyle st; st.color = OnSurfaceMedium(); st.hoverBg = StateHover();
|
||||
st.bgRounding = 5.0f * dp;
|
||||
std::filesystem::path cur(s_dir);
|
||||
const bool hasParent = cur.has_parent_path() && cur.parent_path() != cur;
|
||||
st.tooltip = TR("picker_up");
|
||||
if (IconButton("##pickerUp", ICON_MD_ARROW_UPWARD, icoFont, ImVec2(bh, bh), st) && hasParent)
|
||||
pendingNav = cur.parent_path().string();
|
||||
ImGui::SameLine(0.0f, Layout::spacingXs());
|
||||
IconButtonStyle hst = st; hst.tooltip = TR("picker_home");
|
||||
if (IconButton("##pickerHome", ICON_MD_HOME, icoFont, ImVec2(bh, bh), hst))
|
||||
pendingNav = util::Platform::getHomeDir();
|
||||
ImGui::SameLine(0.0f, Layout::spacingSm());
|
||||
|
||||
// Framed path strip
|
||||
ImVec2 sMin = ImGui::GetCursorScreenPos();
|
||||
const float stripW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 sMax(sMin.x + stripW, sMin.y + bh);
|
||||
ImDrawList* wdl = ImGui::GetWindowDrawList();
|
||||
wdl->AddRectFilled(sMin, sMax, WithAlpha(OnSurface(), 14), 6.0f * dp);
|
||||
wdl->AddRect(sMin, sMax, WithAlpha(OnSurface(), 40), 6.0f * dp, 0, 1.0f);
|
||||
const float tpad = Layout::spacingSm();
|
||||
wdl->AddText(rowFont, rowFont->LegacySize,
|
||||
ImVec2(sMin.x + tpad, sMin.y + (bh - rowFont->LegacySize) * 0.5f),
|
||||
OnSurface(), fitPath(s_dir, rowFont, stripW - tpad * 2.0f).c_str());
|
||||
ImGui::Dummy(ImVec2(stripW, bh));
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// ---- Directory list: folders (clickable) then *.dat wallets present (informational) ----
|
||||
// Flex: the list fills the space between the path bar and the footer block. Reserving a
|
||||
// (slightly generous) footer height keeps the status line + buttons pinned and never clipped.
|
||||
const ImGuiStyle& gstyle = ImGui::GetStyle();
|
||||
const float footerBlockH = Layout::spacingXs() + metaFont->LegacySize // status line
|
||||
+ Layout::spacingSm() + 1.0f + Layout::spacingSm() // separator block
|
||||
+ ImGui::GetFrameHeight() // footer buttons
|
||||
+ 6.0f * gstyle.ItemSpacing.y; // inter-item gaps
|
||||
float listH = ImGui::GetContentRegionAvail().y - footerBlockH;
|
||||
listH = std::max(listH, rowH * 3.0f + listPad * 2.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(listPad, listPad));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, WithAlpha(OnSurface(), 20));
|
||||
ImGui::BeginChild("##pickerList", ImVec2(fullW, listH), true);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
const float rowW = ImGui::GetContentRegionAvail().x;
|
||||
const float padX = Layout::spacingSm();
|
||||
|
||||
if (s_subdirs.empty() && s_datfiles.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::Indent(padX);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("picker_empty"));
|
||||
ImGui::Unindent(padX);
|
||||
}
|
||||
|
||||
// Folder rows
|
||||
for (std::size_t i = 0; i < s_subdirs.size(); ++i) {
|
||||
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rMax(rMin.x + rowW, rMin.y + rowH);
|
||||
ImGui::PushID((int)i);
|
||||
const bool clicked = ImGui::InvisibleButton("##dir", ImVec2(rowW, rowH));
|
||||
const bool hov = ImGui::IsItemHovered();
|
||||
ImGui::PopID();
|
||||
if (hov) {
|
||||
dl->AddRectFilled(rMin, rMax, WithAlpha(OnSurface(), 22), 6.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
}
|
||||
const float midY = (rMin.y + rMax.y) * 0.5f;
|
||||
const float icoSz = rowFont->LegacySize * 1.05f;
|
||||
float x = rMin.x + padX;
|
||||
dl->AddText(icoFont, icoSz, ImVec2(x, midY - icoSz * 0.5f),
|
||||
hov ? Primary() : OnSurfaceMedium(), ICON_MD_FOLDER);
|
||||
x += icoSz + Layout::spacingSm();
|
||||
// chevron affordance on the right
|
||||
const float chSz = metaFont->LegacySize;
|
||||
const float chX = rMax.x - padX - chSz;
|
||||
if (hov)
|
||||
dl->AddText(icoFont, chSz, ImVec2(chX, midY - chSz * 0.5f), OnSurfaceMedium(), ICON_MD_CHEVRON_RIGHT);
|
||||
std::string nm = fit(s_subdirs[i], rowFont, (chX - Layout::spacingSm()) - x);
|
||||
dl->AddText(rowFont, rowFont->LegacySize, ImVec2(x, midY - rowFont->LegacySize * 0.5f),
|
||||
OnSurface(), nm.c_str());
|
||||
if (clicked) pendingNav = (std::filesystem::path(s_dir) / s_subdirs[i]).string();
|
||||
}
|
||||
|
||||
// Wallet-file rows (informational — dimmed, not clickable)
|
||||
for (std::size_t i = 0; i < s_datfiles.size(); ++i) {
|
||||
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||
const float midY = rMin.y + rowH * 0.5f;
|
||||
const float icoSz = rowFont->LegacySize * 1.05f;
|
||||
float x = rMin.x + padX;
|
||||
dl->AddText(icoFont, icoSz, ImVec2(x, midY - icoSz * 0.5f), OnSurfaceDisabled(),
|
||||
ICON_MD_ACCOUNT_BALANCE_WALLET);
|
||||
x += icoSz + Layout::spacingSm();
|
||||
std::string nm = fit(s_datfiles[i], rowFont, (rMin.x + rowW - padX) - x);
|
||||
dl->AddText(rowFont, rowFont->LegacySize, ImVec2(x, midY - rowFont->LegacySize * 0.5f),
|
||||
OnSurfaceDisabled(), nm.c_str());
|
||||
ImGui::Dummy(ImVec2(rowW, rowH));
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar(); // ItemSpacing
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor(); // ChildBg
|
||||
ImGui::PopStyleVar(); // WindowPadding
|
||||
|
||||
// ---- Status: how many wallet files are in the selected folder --------------------------
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
if (!s_datfiles.empty()) {
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), TR("picker_dat_count"), (int)s_datfiles.size());
|
||||
Type().textColored(TypeStyle::Caption, Success(), buf);
|
||||
} else {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("picker_dat_none"));
|
||||
}
|
||||
|
||||
// ---- Footer: Select this folder / Cancel ----------------------------------------------
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
if (StyledButton(TR("picker_select"), ImVec2(200.0f * dp, 0))) {
|
||||
if (s_onPick) s_onPick(s_dir);
|
||||
s_open = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (StyledButton(TR("cancel"), ImVec2(120.0f * dp, 0))) s_open = false;
|
||||
|
||||
EndOverlayDialog();
|
||||
|
||||
if (!pendingNav.empty()) navigate(pendingNav);
|
||||
}
|
||||
|
||||
private:
|
||||
static void navigate(const std::string& dir) {
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
fs::path p(dir);
|
||||
if (!fs::is_directory(p, ec)) return;
|
||||
fs::path abs = fs::absolute(p, ec);
|
||||
s_dir = ec ? dir : abs.lexically_normal().string();
|
||||
s_subdirs.clear();
|
||||
s_datfiles.clear();
|
||||
fs::directory_iterator it(p, ec), end;
|
||||
int guard = 0;
|
||||
for (; !ec && it != end && guard < 4000; it.increment(ec), ++guard) {
|
||||
std::error_code fec;
|
||||
std::string name = it->path().filename().string();
|
||||
if (name.empty()) continue;
|
||||
if (it->is_directory(fec)) {
|
||||
s_subdirs.push_back(name);
|
||||
} else if (it->is_regular_file(fec)) {
|
||||
if (name.size() > 4 && name.substr(name.size() - 4) == ".dat")
|
||||
s_datfiles.push_back(name);
|
||||
}
|
||||
}
|
||||
auto ci = [](const std::string& a, const std::string& b){
|
||||
return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(),
|
||||
[](char x, char y){ return std::tolower((unsigned char)x) < std::tolower((unsigned char)y); });
|
||||
};
|
||||
std::sort(s_subdirs.begin(), s_subdirs.end(), ci);
|
||||
std::sort(s_datfiles.begin(), s_datfiles.end(), ci);
|
||||
}
|
||||
|
||||
static inline bool s_open = false;
|
||||
static inline std::string s_dir;
|
||||
static inline std::vector<std::string> s_subdirs;
|
||||
static inline std::vector<std::string> s_datfiles;
|
||||
static inline std::function<void(const std::string&)> s_onPick;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "../material/colors.h"
|
||||
#include "../material/typography.h"
|
||||
#include "../material/project_icons.h"
|
||||
#include "../notifications.h"
|
||||
#include "../effects/imgui_acrylic.h"
|
||||
#include "../effects/scroll_fade_shader.h"
|
||||
#include "../effects/low_spec.h"
|
||||
@@ -175,18 +176,6 @@ static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f);
|
||||
}
|
||||
|
||||
// Dashboard-grid state for the portfolio cards (drag to move, corner-drag to resize).
|
||||
struct PfCell { int col, row, w, h; };
|
||||
struct PfGridDrag {
|
||||
int drag = -1; // card index being dragged (-1 = none)
|
||||
int resize = -1; // card index being resized
|
||||
bool moved = false; // drag passed the click threshold this gesture
|
||||
ImVec2 press; // mouse pos at grab
|
||||
ImVec2 grab; // mouse offset within the card at grab
|
||||
PfCell old = {0,0,1,1}; // dragged card's cell at grab (for swap-on-drop)
|
||||
PfCell target = {0,0,1,1};// live drop target cell
|
||||
};
|
||||
static PfGridDrag s_grid;
|
||||
|
||||
// Build the right-hand amount strings for a group card / preview from its price-data config.
|
||||
struct PfDisplay {
|
||||
@@ -326,14 +315,18 @@ 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.
|
||||
static void pfCommitIfNeeded(config::Settings* settings)
|
||||
// New groups default to the active wallet's scope (per-wallet visibility); existing groups keep
|
||||
// whatever scope they already had.
|
||||
static void pfCommitIfNeeded(App* app)
|
||||
{
|
||||
config::Settings* settings = app->settings();
|
||||
if (s_pfEdit.label[0] == '\0') return;
|
||||
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 (existing) entries[s_pfEdit.sel] = e;
|
||||
else { entries.push_back(e); s_pfEdit.sel = (int)entries.size() - 1; }
|
||||
pfPersist(settings, entries);
|
||||
@@ -830,6 +823,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
auto es = settings->getPortfolioEntries();
|
||||
config::Settings::PortfolioEntry ne;
|
||||
ne.label = TR("portfolio_untitled");
|
||||
ne.scope = app->activeWalletIdentityHash(); // new group defaults to this wallet
|
||||
es.push_back(ne);
|
||||
pfPersist(settings, es);
|
||||
s_pfEdit.sel = (int)es.size() - 1;
|
||||
@@ -956,7 +950,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
PortfolioBeginEdit(app, s_pfEdit.sel); // reload the working set from the stored entry
|
||||
ImGui::SameLine(0, sp);
|
||||
ImGui::BeginDisabled(s_pfEdit.label[0] == '\0');
|
||||
if (material::TactileButton(TR("portfolio_save"), ImVec2(bw, addH))) pfCommitIfNeeded(settings);
|
||||
if (material::TactileButton(TR("portfolio_save"), ImVec2(bw, addH))) pfCommitIfNeeded(app);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
@@ -967,7 +961,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
{
|
||||
float bh = 40.0f * dp, bw = 120.0f * dp;
|
||||
material::RightAlignX(bw);
|
||||
if (material::TactileButton(TR("portfolio_close"), ImVec2(bw, bh))) { pfCommitIfNeeded(settings); s_pfEdit.open = false; }
|
||||
if (material::TactileButton(TR("portfolio_close"), ImVec2(bw, bh))) { pfCommitIfNeeded(app); s_pfEdit.open = false; }
|
||||
}
|
||||
|
||||
material::EndOverlayDialog();
|
||||
@@ -975,157 +969,132 @@ static void RenderPortfolioEditor(App* app)
|
||||
|
||||
bool PortfolioEditorActive() { return s_pfEdit.open; }
|
||||
|
||||
// Resolve each portfolio group's grid cell: honor a stored placement when it fits, else auto-place
|
||||
// row-major into the first free cells. Pure + deterministic (no ImGui). One PfCell per entry.
|
||||
static std::vector<PfCell> pfComputeGridLayout(
|
||||
const std::vector<config::Settings::PortfolioEntry>& entries, int cols, int minW, int minH)
|
||||
{
|
||||
int n = (int)entries.size();
|
||||
std::vector<PfCell> layout((size_t)std::max(0, n));
|
||||
std::vector<std::vector<char>> occ;
|
||||
auto ensure = [&](int rEnd) { while ((int)occ.size() < rEnd) occ.emplace_back(cols, 0); };
|
||||
auto fits = [&](int c, int r, int w, int h) -> bool {
|
||||
if (c < 0 || r < 0 || c + w > cols) return false;
|
||||
ensure(r + h);
|
||||
for (int rr = r; rr < r + h; rr++) for (int cc = c; cc < c + w; cc++) if (occ[rr][cc]) return false;
|
||||
return true;
|
||||
};
|
||||
auto mark = [&](int c, int r, int w, int h) { ensure(r + h);
|
||||
for (int rr = r; rr < r + h; rr++) for (int cc = c; cc < c + w; cc++) occ[rr][cc] = 1; };
|
||||
std::vector<char> placed((size_t)std::max(0, n), 0);
|
||||
for (int i = 0; i < n; i++) {
|
||||
const auto& e = entries[i];
|
||||
int w = std::max(minW, std::min(e.gridW, cols)), h = std::max(minH, e.gridH);
|
||||
if (e.gridCol >= 0 && e.gridRow >= 0 && fits(e.gridCol, e.gridRow, w, h)) {
|
||||
layout[i] = {e.gridCol, e.gridRow, w, h}; mark(e.gridCol, e.gridRow, w, h); placed[i] = 1;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (placed[i]) continue;
|
||||
const auto& e = entries[i];
|
||||
int w = std::max(minW, std::min(e.gridW, cols)), h = std::max(minH, e.gridH);
|
||||
int fc = 0, fr = 0; bool found = false;
|
||||
for (int r = 0; !found && r < 4096; r++)
|
||||
for (int c = 0; c + w <= cols; c++)
|
||||
if (fits(c, r, w, h)) { fc = c; fr = r; found = true; break; }
|
||||
layout[i] = {fc, fr, w, h}; mark(fc, fr, w, h);
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
// Shared drawing context for a portfolio group card (bound once per frame in RenderMarketTab).
|
||||
struct MktCardCtx {
|
||||
ImDrawList* dl;
|
||||
const WalletState* state;
|
||||
const MarketInfo* market;
|
||||
float mktDp;
|
||||
float pfInset;
|
||||
float pfCardPad;
|
||||
ImFont* sub1;
|
||||
ImFont* capFont;
|
||||
};
|
||||
|
||||
// Draw one portfolio group card (name/icon, value + %chg, secondary DRGX, sparkline body) within
|
||||
// [gcMin,gcMax] (inset for gaps). hov = hover/active highlight. Pure drawing into cx.dl.
|
||||
static void mktDrawCard(const MktCardCtx& cx, ImVec2 gcMin, ImVec2 gcMax,
|
||||
const config::Settings::PortfolioEntry& e, bool hov)
|
||||
// ---- Portfolio ROW layouts (replace the drag/resize grid) --------------------------------------
|
||||
// Draw one group as a full-width row card in [rowMin,rowMax]; style 0=compact, 1=detailed,
|
||||
// 2=value-hero. No left accent strip — identity comes from the tinted icon + a subtle accent
|
||||
// border. Fixed right-aligned columns keep numbers aligned across rows; on-but-absent fields show a
|
||||
// muted em-dash so a row never looks broken. Pure drawing into dl.
|
||||
static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
const config::Settings::PortfolioEntry& e, const WalletState& state,
|
||||
const MarketInfo& market, int style, float dp,
|
||||
ImFont* sub1, ImFont* capFont, bool hov)
|
||||
{
|
||||
ImDrawList* dl = cx.dl;
|
||||
const auto& state = *cx.state;
|
||||
const auto& market = *cx.market;
|
||||
float mktDp = cx.mktDp, pfInset = cx.pfInset;
|
||||
ImFont* sub1 = cx.sub1, *capFont = cx.capFont;
|
||||
// The grid is gapless, so inset the card within its cell block for separation.
|
||||
gcMin.x += pfInset; gcMin.y += pfInset; gcMax.x -= pfInset; gcMax.y -= pfInset;
|
||||
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
ImU32 accent = e.color ? (ImU32)e.color : 0;
|
||||
float pad = cx.pfCardPad;
|
||||
GlassPanelSpec gcGlass; gcGlass.rounding = 10.0f;
|
||||
gcGlass.fillAlpha = hov ? 34 : 22; gcGlass.borderAlpha = 40;
|
||||
DrawGlassPanel(dl, gcMin, gcMax, gcGlass);
|
||||
// Accented groups get a colored outline around the whole card (configurable opacity);
|
||||
// others get a subtle hover ring.
|
||||
|
||||
// Card — a touch more fill than before so rows read as distinct cards, plus a subtle accent
|
||||
// border for identity (the left accent strip was removed).
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
if (accent) {
|
||||
int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f);
|
||||
dl->AddRect(gcMin, gcMax, WithAlpha(accent, oa), 10.0f, 0, hov ? 2.5f : 2.0f);
|
||||
} else if (hov) dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f);
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), 10.0f, 0, hov ? 1.8f : 1.2f);
|
||||
} else if (hov) {
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), 10.0f, 0, 1.2f);
|
||||
}
|
||||
|
||||
auto fit = [&](std::string s, ImFont* f, float maxW) {
|
||||
bool t = false;
|
||||
while (s.size() > 1 && f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x > maxW) { s.pop_back(); t = true; }
|
||||
if (t) s += "\xE2\x80\xA6";
|
||||
return s;
|
||||
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
|
||||
auto fit = [&](std::string s, ImFont* f, float maxW){ bool t=false; while (s.size()>1 && tw(f,s)>maxW){ s.pop_back(); t=true; } if (t) s += "\xE2\x80\xA6"; return s; };
|
||||
// Right-aligned text: right edge at xr, vertically centred on yc.
|
||||
auto rtext = [&](ImFont* f, float xr, float yc, ImU32 col, const std::string& s){
|
||||
dl->AddText(f, f->LegacySize, ImVec2(xr - tw(f, s), yc - f->LegacySize * 0.5f), col, s.c_str());
|
||||
};
|
||||
const std::string kDash = "\xE2\x80\x94"; // em-dash for on-but-absent fields
|
||||
|
||||
// Value + DRGX strings per basis.
|
||||
char vb[80]; std::string valueStr; bool hasValue = false;
|
||||
if (e.priceBasis == 0 && market.price_usd > 0) { snprintf(vb, sizeof(vb), "$%.2f", bal * market.price_usd); valueStr = vb; hasValue = true; }
|
||||
else if (e.priceBasis == 1 && market.price_btc > 0) { snprintf(vb, sizeof(vb), "%.8f BTC", bal * market.price_btc); valueStr = vb; hasValue = true; }
|
||||
else if (e.priceBasis == 3 && e.manualPrice > 0.0) { snprintf(vb, sizeof(vb), "%.2f %s", bal * e.manualPrice, e.manualCurrency.c_str()); valueStr = vb; hasValue = true; }
|
||||
char db[48]; snprintf(db, sizeof(db), "%.4f %s", bal, DRAGONX_TICKER);
|
||||
bool heroIsValue = e.showValue && hasValue;
|
||||
std::string heroStr = heroIsValue ? valueStr : (e.showDrgx ? std::string(db) : (hasValue ? valueStr : std::string(db)));
|
||||
std::string footStr = (heroIsValue && e.showDrgx) ? std::string(db) : std::string();
|
||||
// ---- Field values + presence ----
|
||||
char vb[80]; std::string valStr; bool hasValue=false;
|
||||
if (e.priceBasis==0 && market.price_usd>0){ snprintf(vb,sizeof(vb),"$%.2f",bal*market.price_usd); valStr=vb; hasValue=true; }
|
||||
else if (e.priceBasis==1 && market.price_btc>0){ snprintf(vb,sizeof(vb),"%.8f BTC",bal*market.price_btc); valStr=vb; hasValue=true; }
|
||||
else if (e.priceBasis==3 && e.manualPrice>0.0){ snprintf(vb,sizeof(vb),"%.2f %s",bal*e.manualPrice,e.manualCurrency.c_str()); valStr=vb; hasValue=true; }
|
||||
char db[48]; snprintf(db,sizeof(db),"%.4f %s",bal,DRAGONX_TICKER);
|
||||
std::string drgxStr = std::string(db);
|
||||
bool wantValue = e.showValue;
|
||||
bool wantChange = e.show24h && (e.priceBasis==0 || e.priceBasis==1);
|
||||
bool hasChange = wantChange && market.change_24h != 0.0;
|
||||
std::string chgStr; if (hasChange){ char cb[32]; snprintf(cb,sizeof(cb),"%+.2f%%",market.change_24h); chgStr=cb; }
|
||||
ImU32 chgCol = market.change_24h>=0 ? Success() : Error();
|
||||
bool wantSpark = e.showSparkline && (e.priceBasis==0 || e.priceBasis==1);
|
||||
std::vector<double> spark;
|
||||
if (wantSpark) spark = data::sparklineSeries(market, e.sparklineInterval);
|
||||
bool hasSpark = wantSpark && spark.size() >= 2;
|
||||
ImU32 sparkCol = (!spark.empty() && spark.back()>=spark.front()) ? Success() : Error();
|
||||
|
||||
// Compact stat card: name (+ secondary DRGX) on the left; value with % change stacked
|
||||
// below it in the top-right corner; the sparkline fills the body — so it renders even
|
||||
// on a 2-cell card. Tighter padding on short cards.
|
||||
float cardHeight = gcMax.y - gcMin.y;
|
||||
bool cardShort = cardHeight < 90.0f * mktDp;
|
||||
float hpad = cardShort ? Layout::spacingSm() : pad;
|
||||
float hleft = gcMin.x + hpad, hright = gcMax.x - hpad;
|
||||
float hAvailW = hright - hleft;
|
||||
float topY = gcMin.y + hpad;
|
||||
const float padX = Layout::spacingMd(), padY = Layout::spacingSm(), colGap = Layout::spacingMd();
|
||||
float left = rowMin.x + padX;
|
||||
float right = rowMax.x - padX;
|
||||
float midY = (rowMin.y + rowMax.y) * 0.5f;
|
||||
|
||||
// Right column: value (top-right), % change just below it.
|
||||
float rightBottom = topY;
|
||||
float valLeft = hright;
|
||||
bool showChange = e.show24h && (e.priceBasis == 0 || e.priceBasis == 1) && market.change_24h != 0.0;
|
||||
if (!heroStr.empty()) {
|
||||
ImFont* vf = sub1;
|
||||
std::string vs = fit(heroStr, vf, hAvailW * 0.66f);
|
||||
float vw = vf->CalcTextSizeA(vf->LegacySize, FLT_MAX, 0, vs.c_str()).x;
|
||||
valLeft = hright - vw;
|
||||
dl->AddText(vf, vf->LegacySize, ImVec2(valLeft, topY), OnSurface(), vs.c_str());
|
||||
rightBottom = topY + vf->LegacySize;
|
||||
if (showChange) {
|
||||
char cb[32]; snprintf(cb, sizeof(cb), "%+.2f%%", market.change_24h);
|
||||
float cw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, cb).x;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(hright - cw, rightBottom + 1.0f * mktDp),
|
||||
market.change_24h >= 0 ? Success() : Error(), cb);
|
||||
valLeft = std::min(valLeft, hright - cw);
|
||||
rightBottom += capFont->LegacySize + 1.0f * mktDp;
|
||||
if (style == 0) {
|
||||
// ---- Compact: icon + label (left); fixed right-aligned columns DRGX | value | 24h | spark.
|
||||
float iconSz = sub1->LegacySize, x = left;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, midY), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
}
|
||||
}
|
||||
const float sparkW = 92.0f*dp, chgW = 60.0f*dp, valW = 112.0f*dp, drgxW = 122.0f*dp;
|
||||
float sparkL = right - sparkW;
|
||||
float chgR = sparkL - colGap;
|
||||
float valR = chgR - chgW - colGap;
|
||||
float drgxR = valR - valW - colGap;
|
||||
float labelR = drgxR - drgxW - colGap;
|
||||
|
||||
// Left column: icon + name (larger, top), secondary DRGX amount below (always shown).
|
||||
float nameX = hleft;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(),
|
||||
ImVec2(nameX + sub1->LegacySize * 0.5f, topY + sub1->LegacySize * 0.5f),
|
||||
accent ? accent : OnSurfaceMedium(), Type().iconSmall(), sub1->LegacySize);
|
||||
nameX += sub1->LegacySize + Layout::spacingSm();
|
||||
}
|
||||
float nameMaxW = std::max(24.0f * mktDp, valLeft - Layout::spacingSm() - nameX);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(nameX, topY), OnSurfaceMedium(),
|
||||
fit(e.label, sub1, nameMaxW).c_str());
|
||||
float leftBottom = topY + sub1->LegacySize;
|
||||
if (!footStr.empty()) {
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(nameX, leftBottom + 1.0f * mktDp),
|
||||
OnSurfaceDisabled(), fit(footStr, capFont, nameMaxW).c_str());
|
||||
leftBottom += capFont->LegacySize + 1.0f * mktDp;
|
||||
}
|
||||
if (e.showDrgx) rtext(capFont, drgxR, midY, OnSurfaceMedium(), fit(drgxStr, capFont, drgxW));
|
||||
if (wantValue) rtext(sub1, valR, midY, hasValue?OnSurface():OnSurfaceDisabled(), hasValue?fit(valStr,sub1,valW):kDash);
|
||||
if (wantChange) rtext(capFont, chgR, midY, hasChange?chgCol:OnSurfaceDisabled(), hasChange?chgStr:kDash);
|
||||
if (hasSpark) pfDrawSparkline(dl, ImVec2(sparkL, rowMin.y+padY), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
|
||||
// Body: sparkline fills from below the header to the card bottom.
|
||||
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) {
|
||||
std::vector<double> hh = data::sparklineSeries(market, e.sparklineInterval);
|
||||
if (hh.size() >= 2) {
|
||||
float sparkTop = std::max(leftBottom, rightBottom) + Layout::spacingXs();
|
||||
float sparkBot = gcMax.y - hpad;
|
||||
if (sparkBot - sparkTop >= 8.0f * mktDp)
|
||||
pfDrawSparklineFilled(dl, ImVec2(hleft, sparkTop), ImVec2(hright, sparkBot),
|
||||
hh, hh.back() >= hh.front() ? Success() : Error());
|
||||
float lblMaxW = std::max(24.0f*dp, labelR - x);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(x, midY - sub1->LegacySize*0.5f), OnSurface(), fit(e.label,sub1,lblMaxW).c_str());
|
||||
} else if (style == 1) {
|
||||
// ---- Detailed: 2x2 grid — label/value on top, DRGX/24h below — plus a tall sparkline strip.
|
||||
float iconSz = sub1->LegacySize, x = left;
|
||||
const float sparkW = 150.0f*dp;
|
||||
float sparkL = right - sparkW;
|
||||
float textR = hasSpark ? (sparkL - colGap) : right;
|
||||
float topY = rowMin.y + padY;
|
||||
float botY = rowMax.y - padY - capFont->LegacySize;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
}
|
||||
// value (top-right, emphasized)
|
||||
std::string vTop = wantValue ? (hasValue?valStr:kDash) : std::string();
|
||||
if (wantValue) dl->AddText(sub1, sub1->LegacySize, ImVec2(textR - tw(sub1,vTop), topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str());
|
||||
// label (top-left, fills up to the value column)
|
||||
float labelR = wantValue ? (textR - tw(sub1,vTop) - colGap) : textR;
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(x, topY), OnSurface(), fit(e.label,sub1,std::max(24.0f*dp, labelR - x)).c_str());
|
||||
// DRGX (bottom-left, muted) + 24h (bottom-right, colored)
|
||||
if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), OnSurfaceMedium(), drgxStr.c_str());
|
||||
if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(textR - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); }
|
||||
// tall sparkline strip spanning both lines
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, topY), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
} else {
|
||||
// ---- Value-hero: label; big neutral value; 24h; DRGX + sparkline fill the right.
|
||||
float iconSz = capFont->LegacySize, x = left;
|
||||
const float sparkW = (right-left)*0.40f;
|
||||
float sparkL = right - sparkW;
|
||||
float topY = rowMin.y + padY;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
}
|
||||
float rightZoneL = hasSpark ? sparkL : right;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY), OnSurfaceMedium(), fit(e.label,capFont,std::max(24.0f*dp, rightZoneL - x - colGap)).c_str());
|
||||
// hero value — neutral bold (accent stays in the icon/border, so it can't clash with the 24h)
|
||||
float valY = topY + capFont->LegacySize + Layout::spacingXs();
|
||||
std::string hero = hasValue ? valStr : (e.showDrgx ? drgxStr : kDash);
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(left, valY), hasValue?OnSurface():OnSurfaceDisabled(), hero.c_str());
|
||||
// 24h below the value (colored / dash)
|
||||
float botY = valY + sub1->LegacySize + Layout::spacingXs();
|
||||
if (wantChange) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), hasChange?chgCol:OnSurfaceDisabled(), (hasChange?chgStr:kDash).c_str());
|
||||
// DRGX — right-aligned on the value line; fills the right when there's no sparkline.
|
||||
if (e.showDrgx && hasValue) {
|
||||
float dr = hasSpark ? (sparkL - colGap) : right;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(dr - tw(capFont,drgxStr), valY + (sub1->LegacySize - capFont->LegacySize)*0.5f), OnSurfaceMedium(), drgxStr.c_str());
|
||||
}
|
||||
// filled sparkline (right ~40%)
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, valY - 2*dp), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1237,10 +1206,6 @@ struct MktCtx {
|
||||
bool chartUp;
|
||||
double periodChangePct;
|
||||
std::string periodSuffix;
|
||||
const std::vector<config::Settings::PortfolioEntry>* pfEntriesGeo;
|
||||
const std::vector<PfCell>* pfLayout;
|
||||
int pfN, pfCols, pfMinW, pfMinH, pfMaxRow;
|
||||
float pfCellW, pfCellH, pfInset, pfCardPad, pfCardGap;
|
||||
};
|
||||
|
||||
// Combined hero card: draws the ONE glass panel spanning the price header AND the chart below it,
|
||||
@@ -1659,10 +1624,6 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
ImFont* body2 = Type().body2();
|
||||
float ratioBarH = cx.ratioBarH, pfSummaryH = cx.pfSummaryH, portfolioH = cx.portfolioH;
|
||||
const auto& pfEntriesGeo = *cx.pfEntriesGeo;
|
||||
const auto& pfLayout = *cx.pfLayout;
|
||||
int pfN = cx.pfN, pfCols = cx.pfCols, pfMinW = cx.pfMinW, pfMinH = cx.pfMinH, pfMaxRow = cx.pfMaxRow;
|
||||
float pfCellW = cx.pfCellW, pfCellH = cx.pfCellH, pfInset = cx.pfInset, pfCardPad = cx.pfCardPad, pfCardGap = cx.pfCardGap;
|
||||
char buf[128];
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
||||
@@ -1760,157 +1721,56 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
ImVec2(cx0, cy + ratioBarH + Layout::spacingXs()), OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// ---- CUSTOM GROUPS — dashboard grid: drag a card to move it, drag its corner to resize.
|
||||
// A subtle dot grid shows while rearranging. Click (no drag) opens the editor. ----
|
||||
if (pfN > 0) {
|
||||
float gy = cardMin.y + pfSummaryH;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
bool interacting = (s_grid.drag >= 0 || s_grid.resize >= 0);
|
||||
float rh = 20.0f * mktDp; // corner resize-grip size (generous so a short resize is easy to grab)
|
||||
|
||||
auto cellMin = [&](const PfCell& L) {
|
||||
return ImVec2(cx0 + L.col * (pfCellW + pfCardGap), gy + L.row * (pfCellH + pfCardGap));
|
||||
};
|
||||
auto cellSize = [&](int w, int h) {
|
||||
return ImVec2(w * pfCellW + (w - 1) * pfCardGap, h * pfCellH + (h - 1) * pfCardGap);
|
||||
};
|
||||
|
||||
// Draw one card's content into [gcMin, gcMax].
|
||||
// Stat tile: header (icon + name, 24h badge) / hero value (large, centered band) /
|
||||
// footer (DRGX amount) + a dedicated sparkline strip along the bottom.
|
||||
MktCardCtx cardCx{ dl, &state, &market, mktDp, pfInset, pfCardPad, sub1, capFont };
|
||||
|
||||
// Subtle dot grid while rearranging (fine 32px cells). Fill the whole visible grid area
|
||||
// — down to the bottom of the scroll viewport, not just the occupied rows — so a card can
|
||||
// be dropped anywhere, including the empty space below the current cards.
|
||||
if (interacting) {
|
||||
ImU32 dotc = WithAlpha(OnSurface(), 38);
|
||||
float visibleBottom = dl->GetClipRectMax().y; // bottom edge of the scroll child
|
||||
int rowsToBottom = (int)((visibleBottom - gy) / pfCellH) + 1;
|
||||
int lastRow = std::max(pfMaxRow + 2, rowsToBottom);
|
||||
for (int r = 0; r <= lastRow; r++)
|
||||
for (int c = 0; c <= pfCols; c++)
|
||||
dl->AddCircleFilled(ImVec2(cx0 + c * pfCellW, gy + r * pfCellH), 1.2f * mktDp, dotc);
|
||||
// ---- CUSTOM GROUPS — full-width rows in the selected style (single-line / two-line / value-hero),
|
||||
// inside an internal scroll region so the portfolio stays within its height budget. Left/Right
|
||||
// arrows switch styles (handled in RenderMarketTab). Click a row to edit it. ----
|
||||
{
|
||||
const auto& allEntries = app->settings()->getPortfolioEntries();
|
||||
const std::string activeHash = app->activeWalletIdentityHash();
|
||||
// Visible = this wallet's groups + legacy (empty scope). Keep storage indices for edit targets.
|
||||
std::vector<int> vis;
|
||||
for (int i = 0; i < (int)allEntries.size(); i++) {
|
||||
const auto& e = allEntries[i];
|
||||
if (e.scope.empty() || activeHash.empty() || e.scope == activeHash) vis.push_back(i);
|
||||
}
|
||||
|
||||
// Draw + hit-test each card. Skip the one being dragged/resized (drawn specially below).
|
||||
for (int i = 0; i < pfN; i++) {
|
||||
ImVec2 gcMin = cellMin(pfLayout[i]);
|
||||
ImVec2 sz = cellSize(pfLayout[i].w, pfLayout[i].h);
|
||||
ImVec2 gcMax(gcMin.x + sz.x, gcMin.y + sz.y);
|
||||
int style = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
||||
float rowH = (style == 0 ? 46.0f : style == 1 ? 64.0f : 84.0f) * mktDp;
|
||||
float rowGap = Layout::spacingSm();
|
||||
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
|
||||
|
||||
if (i == s_grid.drag && s_grid.moved) {
|
||||
dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 45), 10.0f, 0, 1.0f); // source ghost
|
||||
continue;
|
||||
}
|
||||
if (i == s_grid.resize) continue;
|
||||
|
||||
bool hov = !interacting && material::IsRectHovered(gcMin, gcMax);
|
||||
mktDrawCard(cardCx, gcMin, gcMax, pfEntriesGeo[i], hov);
|
||||
|
||||
// Resize grip (bottom-right).
|
||||
bool gripHov = material::IsRectHovered(ImVec2(gcMax.x - rh, gcMax.y - rh), gcMax);
|
||||
ImU32 gcc = WithAlpha(OnSurface(), gripHov ? 170 : 80);
|
||||
for (int k = 1; k <= 2; k++) {
|
||||
float o = k * 4.0f * mktDp;
|
||||
dl->AddLine(ImVec2(gcMax.x - o, gcMax.y - 3.0f), ImVec2(gcMax.x - 3.0f, gcMax.y - o), gcc, 1.3f * mktDp);
|
||||
}
|
||||
|
||||
// Start a gesture only when idle. One body button; the press location (corner vs body)
|
||||
// decides resize vs move — avoids overlapping-item hit-test issues.
|
||||
if (s_grid.drag < 0 && s_grid.resize < 0) {
|
||||
ImGui::PushID(3000 + i);
|
||||
ImGui::SetCursorScreenPos(gcMin);
|
||||
ImGui::InvisibleButton("##pfBody", sz);
|
||||
bool bodyAct = ImGui::IsItemActivated();
|
||||
bool bodyHov = ImGui::IsItemHovered();
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH));
|
||||
// Flush-left child (no window padding) so rows align with the summary; a scrollbar appears
|
||||
// only when the visible groups overflow the bounded height.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::BeginChild("##pfRows", ImVec2(availWidth, rowsH), false);
|
||||
ImGui::PopStyleVar();
|
||||
// Zero item-spacing INSIDE the child: each row emits an InvisibleButton + a gap Dummy, and
|
||||
// ImGui's default ItemSpacing.y between those items would inflate the content past rowsH and
|
||||
// clip the last row (and widen the gaps). The inter-row gap is controlled by the Dummy below.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
ImDrawList* rdl = ImGui::GetWindowDrawList();
|
||||
float rowW = ImGui::GetContentRegionAvail().x; // narrows automatically if a scrollbar shows
|
||||
if (vis.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||
} else {
|
||||
for (int vi = 0; vi < (int)vis.size(); vi++) {
|
||||
int i = vis[vi];
|
||||
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rMax(rMin.x + rowW, rMin.y + rowH);
|
||||
ImGui::PushID(4000 + i);
|
||||
bool clicked = ImGui::InvisibleButton("##pfRowBtn", ImVec2(rowW, rowH));
|
||||
bool hov = ImGui::IsItemHovered();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
if (bodyHov) ImGui::SetMouseCursor(gripHov ? ImGuiMouseCursor_ResizeNWSE : ImGuiMouseCursor_Hand);
|
||||
if (bodyAct) {
|
||||
if (gripHov) { s_grid.resize = i; s_grid.old = pfLayout[i]; }
|
||||
else {
|
||||
s_grid.drag = i; s_grid.moved = false; s_grid.old = pfLayout[i];
|
||||
s_grid.press = io.MousePos;
|
||||
s_grid.grab = ImVec2(io.MousePos.x - gcMin.x, io.MousePos.y - gcMin.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto pfPersist = [&](const std::vector<config::Settings::PortfolioEntry>& v) {
|
||||
app->settings()->setPortfolioEntries(v); app->settings()->save();
|
||||
};
|
||||
|
||||
// ----- Resize in progress (corner drag) -----
|
||||
if (s_grid.resize >= 0 && s_grid.resize < pfN) {
|
||||
int i = s_grid.resize;
|
||||
ImVec2 mn = cellMin(pfLayout[i]);
|
||||
int nw = std::max(pfMinW, std::min(pfCols - pfLayout[i].col,
|
||||
(int)((io.MousePos.x - mn.x + pfCardGap) / (pfCellW + pfCardGap) + 0.5f)));
|
||||
int nh = std::max(pfMinH, (int)((io.MousePos.y - mn.y + pfCardGap) / (pfCellH + pfCardGap) + 0.5f));
|
||||
ImVec2 sz = cellSize(nw, nh), mx(mn.x + sz.x, mn.y + sz.y);
|
||||
mktDrawCard(cardCx, mn, mx, pfEntriesGeo[i], true);
|
||||
dl->AddRect(mn, mx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp);
|
||||
// Live size readout (e.g. "8\xC3\x972") so the reached grid span is visible — the
|
||||
// minimum is pfMinW x pfMinH (8x2).
|
||||
{
|
||||
char szb[32]; snprintf(szb, sizeof(szb), "%d\xC3\x97%d", nw, nh);
|
||||
ImVec2 tsz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, szb);
|
||||
ImVec2 tp((mn.x + mx.x) * 0.5f - tsz.x * 0.5f, (mn.y + mx.y) * 0.5f - tsz.y * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(tp.x - 6, tp.y - 3), ImVec2(tp.x + tsz.x + 6, tp.y + tsz.y + 3),
|
||||
IM_COL32(20, 20, 30, 205), 4.0f);
|
||||
dl->AddText(sub1, sub1->LegacySize, tp, OnSurface(), szb);
|
||||
}
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNWSE);
|
||||
if (!io.MouseDown[0]) {
|
||||
auto entries = app->settings()->getPortfolioEntries();
|
||||
if (i < (int)entries.size()) {
|
||||
entries[i].gridCol = pfLayout[i].col; entries[i].gridRow = pfLayout[i].row;
|
||||
entries[i].gridW = nw; entries[i].gridH = nh;
|
||||
pfPersist(entries);
|
||||
}
|
||||
s_grid.resize = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Drag in progress -----
|
||||
if (s_grid.drag >= 0 && s_grid.drag < pfN) {
|
||||
int i = s_grid.drag;
|
||||
if (io.MouseDown[0]) {
|
||||
if (std::abs(io.MousePos.x - s_grid.press.x) > 4.0f || std::abs(io.MousePos.y - s_grid.press.y) > 4.0f)
|
||||
s_grid.moved = true;
|
||||
if (s_grid.moved) {
|
||||
int w = pfLayout[i].w, h = pfLayout[i].h;
|
||||
float tx = io.MousePos.x - s_grid.grab.x, ty = io.MousePos.y - s_grid.grab.y;
|
||||
int tc = std::max(0, std::min(pfCols - w, (int)((tx - cx0) / (pfCellW + pfCardGap) + 0.5f)));
|
||||
int tr = std::max(0, (int)((ty - gy) / (pfCellH + pfCardGap) + 0.5f));
|
||||
s_grid.target = {tc, tr, w, h};
|
||||
ImVec2 tmn(cx0 + tc * (pfCellW + pfCardGap), gy + tr * (pfCellH + pfCardGap));
|
||||
ImVec2 tsz = cellSize(w, h), tmx(tmn.x + tsz.x, tmn.y + tsz.y);
|
||||
dl->AddRectFilled(tmn, tmx, WithAlpha(Primary(), 25), 10.0f);
|
||||
dl->AddRect(tmn, tmx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp);
|
||||
ImVec2 fmn(tx, ty), fmx(fmn.x + tsz.x, fmn.y + tsz.y);
|
||||
mktDrawCard(cardCx, fmn, fmx, pfEntriesGeo[i], true); // dragged card follows the mouse
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll);
|
||||
}
|
||||
} else {
|
||||
if (!s_grid.moved) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; }
|
||||
else {
|
||||
auto entries = app->settings()->getPortfolioEntries();
|
||||
if (i < (int)entries.size()) {
|
||||
int occ = -1;
|
||||
for (int j = 0; j < pfN; j++)
|
||||
if (j != i && pfLayout[j].col == s_grid.target.col && pfLayout[j].row == s_grid.target.row) { occ = j; break; }
|
||||
entries[i].gridCol = s_grid.target.col; entries[i].gridRow = s_grid.target.row;
|
||||
if (occ >= 0 && occ < (int)entries.size()) {
|
||||
entries[occ].gridCol = s_grid.old.col; entries[occ].gridRow = s_grid.old.row;
|
||||
}
|
||||
pfPersist(entries);
|
||||
}
|
||||
}
|
||||
s_grid.drag = -1; s_grid.moved = false;
|
||||
pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, capFont, hov);
|
||||
if (clicked) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; }
|
||||
if (vi + 1 < (int)vis.size()) ImGui::Dummy(ImVec2(rowW, rowGap));
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleVar(); // ItemSpacing
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + portfolioH));
|
||||
@@ -1949,6 +1809,22 @@ void RenderMarketTab(App* app)
|
||||
const auto& currentExchange = registry[s_mkt.exchangeIdx];
|
||||
if (s_mkt.pairIdx >= (int)currentExchange.pairs.size()) s_mkt.pairIdx = 0;
|
||||
|
||||
// Left/Right arrows: cycle portfolio row styles (compact / detailed / featured), mirroring the
|
||||
// Overview tab's layout switch. Skip while typing, when Ctrl is held (theme cycle), or while the
|
||||
// portfolio editor modal is open.
|
||||
if (app->settings() && !s_pfEdit.open && !ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
||||
bool prev = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
|
||||
bool next = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
|
||||
if (prev || next) {
|
||||
int st = app->settings()->getPortfolioStyle();
|
||||
st = next ? (st + 1) % 3 : (st + 2) % 3;
|
||||
app->settings()->setPortfolioStyle(st);
|
||||
const char* names[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
|
||||
TR("portfolio_style_featured") };
|
||||
Notifications::instance().info(std::string(TR("portfolio_style_label")) + ": " + names[st]);
|
||||
}
|
||||
}
|
||||
|
||||
// (Panel theme-effects are suppressed frame-wide in App::render() while the modal is open, so
|
||||
// their foreground borders don't bleed over the overlay — see PortfolioEditorActive().)
|
||||
ImVec2 marketAvail = ImGui::GetContentRegionAvail();
|
||||
@@ -1991,23 +1867,24 @@ void RenderMarketTab(App* app)
|
||||
+ body2->LegacySize + Layout::spacingSm() // DRGX balance row
|
||||
+ ratioBarH + Layout::spacingXs() + capFont->LegacySize // ratio bar + % label
|
||||
+ Layout::spacingMd(); // gap before the group cards
|
||||
int pfN = (int)pfEntriesGeo.size();
|
||||
float pfCardPad = Layout::spacingMd(); // inner padding of each group card
|
||||
// Fine SQUARE grid: cells are ~32px minimum and scale up to evenly fill the width. Cards snap to
|
||||
// this grid and span many cells; they're drawn with an inner inset so adjacent cards don't touch.
|
||||
float pfCardGap = 0.0f; // gapless cell grid
|
||||
float pfInset = 3.0f * mktDp; // per-card inset for visual separation
|
||||
const int pfMinW = 8, pfMinH = 2; // minimum card size in cells (~256x64)
|
||||
int pfCols = std::max(pfMinW, (int)(availWidth / (32.0f * mktDp)));
|
||||
float pfCellW = availWidth / (float)pfCols; // square cell edge (>= ~32px)
|
||||
float pfCellH = pfCellW;
|
||||
// Resolve each group's grid cell: honor a stored placement when it fits, else auto-place
|
||||
// row-major into the first free cells. Deterministic each frame.
|
||||
std::vector<PfCell> pfLayout = pfComputeGridLayout(pfEntriesGeo, pfCols, pfMinW, pfMinH);
|
||||
int pfMaxRow = 0; for (const auto& L : pfLayout) pfMaxRow = std::max(pfMaxRow, L.row + L.h);
|
||||
// Content height = the grid plus a couple of empty rows so cards can be dragged below the
|
||||
// current bottom (the tab scrolls when the total exceeds the window).
|
||||
float pfGroupsH = (pfN > 0) ? ((pfMaxRow + 2) * pfCellH) : 0.0f;
|
||||
// Content height = full-width rows in the selected style, confined to a bounded area (up to
|
||||
// pfMaxVisibleRows) with an internal scroll when the visible groups overflow. Only groups
|
||||
// visible in the active wallet (own scope or legacy/global) count toward the height.
|
||||
const std::string pfActiveHash = app->activeWalletIdentityHash();
|
||||
int pfVisN = 0;
|
||||
for (const auto& e : pfEntriesGeo)
|
||||
if (e.scope.empty() || pfActiveHash.empty() || e.scope == pfActiveHash) pfVisN++;
|
||||
int pfStyle = app->settings()->getPortfolioStyle();
|
||||
float pfRowH = (pfStyle == 0 ? 46.0f : pfStyle == 1 ? 64.0f : 84.0f) * mktDp;
|
||||
float pfRowGap = Layout::spacingSm();
|
||||
const int pfMaxVisibleRows = 4; // rows shown before the list scrolls
|
||||
// Height for up to pfMaxVisibleRows rows. ItemSpacing is zeroed inside the row child, so the
|
||||
// content is exactly rows + inter-row gaps; one extra gap of bottom breathing room keeps the
|
||||
// last visible row off the clip edge. More groups than this scroll internally.
|
||||
int pfVisRows = std::min(pfVisN, pfMaxVisibleRows);
|
||||
float pfGroupsH = (pfVisRows > 0)
|
||||
? (pfVisRows * pfRowH + (pfVisRows - 1) * pfRowGap + pfRowGap)
|
||||
: 0.0f;
|
||||
float portfolioH = pfSummaryH + pfGroupsH;
|
||||
|
||||
// Chart series for the selected interval (timestamped), shared by the hero change badge and
|
||||
@@ -2040,9 +1917,7 @@ void RenderMarketTab(App* app)
|
||||
MktCtx mktc{
|
||||
app, ¤tExchange,
|
||||
chartH, heroHeaderH, pfSummaryH, portfolioH, ratioBarH,
|
||||
&chartTimes, nowSec, chartUp, periodChangePct, periodSuffix,
|
||||
&pfEntriesGeo, &pfLayout, pfN, pfCols, pfMinW, pfMinH, pfMaxRow,
|
||||
pfCellW, pfCellH, pfInset, pfCardPad, pfCardGap
|
||||
&chartTimes, nowSec, chartUp, periodChangePct, periodSuffix
|
||||
};
|
||||
mktDrawPriceHero(mktc);
|
||||
|
||||
|
||||
@@ -277,6 +277,23 @@ static void RenderMiningTabContent(App* app)
|
||||
RenderMiningModeToggle(app, state, mining, dl, capFont, ovFont, dp, hs, gap, availWidth,
|
||||
s_pool_mode, s_pool_url, s_pool_worker, s_pool_settings_dirty);
|
||||
|
||||
// Payout safety (multi-wallet): in pool mode, warn if the worker looks like a DragonX address
|
||||
// that isn't in the CURRENT wallet — e.g. left over from a wallet you switched away from, so
|
||||
// rewards would land in the wrong wallet. Non-destructive: we only warn, never overwrite (it
|
||||
// could be a deliberate external payout).
|
||||
if (s_pool_mode) {
|
||||
std::string w(s_pool_worker);
|
||||
while (!w.empty() && (w.front()==' '||w.front()=='\t'||w.front()=='\n'||w.front()=='\r')) w.erase(w.begin());
|
||||
while (!w.empty() && (w.back()==' '||w.back()=='\t'||w.back()=='\n'||w.back()=='\r')) w.pop_back();
|
||||
const bool looksLikeAddr = w.size() > 10 && (w.rfind("zs", 0) == 0 || w[0] == 'R');
|
||||
bool inWallet = false;
|
||||
for (const auto& a : state.addresses) if (a.address == w) { inWallet = true; break; }
|
||||
if (looksLikeAddr && !inWallet && !state.addresses.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
Type().textColored(TypeStyle::Caption, Warning(), TR("mining_payout_foreign"));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// CONTROLS — Glass card with CPU core grid (no heading)
|
||||
// ================================================================
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "../layout.h" // Layout::dpiScale()
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
@@ -31,14 +32,26 @@ inline std::vector<std::string> SplitSeedWords(const std::string& phrase)
|
||||
}
|
||||
|
||||
// Render the words as a numbered 4-column grid (matches the lite welcome wizard layout).
|
||||
// The column stride is sized to the widest "NN. word" cell and scaled by dpiScale(): a raw px
|
||||
// stride collided with the next column's index once the font grew at higher DPI / font scale
|
||||
// (e.g. at 150% "13. elevator" ran straight into "14."). Sizing to content keeps a gap at any
|
||||
// scale and for any word length.
|
||||
inline void RenderSeedWordGrid(const std::vector<std::string>& words, float colSpacingPx = 130.0f)
|
||||
{
|
||||
const float dp = Layout::dpiScale();
|
||||
float colStride = colSpacingPx * dp;
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
char cell[96];
|
||||
std::snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
|
||||
const float need = ImGui::CalcTextSize(cell).x + 20.0f * dp; // cell width + inter-column gap
|
||||
if (need > colStride) colStride = need;
|
||||
}
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
char cell[96];
|
||||
std::snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
|
||||
ImGui::TextUnformatted(cell);
|
||||
if ((i % 4) != 3 && i + 1 < words.size())
|
||||
ImGui::SameLine(((i % 4) + 1) * colSpacingPx);
|
||||
ImGui::SameLine(((i % 4) + 1) * colStride);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -659,6 +659,23 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
// ============================================================================
|
||||
// Confirmation dialog
|
||||
// ============================================================================
|
||||
void SweepShowSendConfirm(App* app, bool show) {
|
||||
(void)app;
|
||||
if (show) {
|
||||
std::snprintf(s_from_address, sizeof(s_from_address), "%s",
|
||||
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000");
|
||||
std::snprintf(s_to_address, sizeof(s_to_address), "%s",
|
||||
"zs1demopeerreceivingaddressabcdef0000000000000000000000000000000");
|
||||
s_amount = 2.50000000; s_fee = DRAGONX_DEFAULT_FEE;
|
||||
std::snprintf(s_memo, sizeof(s_memo), "%s", "thanks!");
|
||||
s_show_confirm = true; // RenderSendConfirmPopup re-issues OpenPopup while this is set
|
||||
} else {
|
||||
s_show_confirm = false;
|
||||
s_from_address[0] = s_to_address[0] = s_memo[0] = '\0';
|
||||
s_amount = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderSendConfirmPopup(App* app) {
|
||||
if (!s_show_confirm) return;
|
||||
|
||||
@@ -688,9 +705,6 @@ void RenderSendConfirmPopup(App* app) {
|
||||
float popW = ImGui::GetContentRegionAvail().x;
|
||||
ImDrawList* popDl = ImGui::GetWindowDrawList();
|
||||
|
||||
Type().text(TypeStyle::H6, TR("confirm_transaction"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
float addrCardH = std::max(schema::UI().drawElement("tabs.send", "confirm-addr-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-addr-card-height").size * popVs);
|
||||
float popGlassRound = Layout::glassRounding() * S.drawElement("tabs.send", "confirm-glass-rounding-ratio").size;
|
||||
|
||||
@@ -804,7 +818,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
if (s_sending) {
|
||||
Type().text(TypeStyle::Body2, TR("sending"));
|
||||
} else {
|
||||
if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) {
|
||||
if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width * Layout::dpiScale(), std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) {
|
||||
// Re-validate against LIVE state — the confirm dialog persists across frames, so the
|
||||
// balance could have dropped or sync (re)started (or the fee bumped total over available)
|
||||
// since Review. Don't broadcast a now-invalid transaction.
|
||||
@@ -857,7 +871,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(TR("cancel"), ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
|
||||
if (TactileButton(TR("cancel"), ImVec2(S.button("tabs.send", "cancel-button").width * Layout::dpiScale(), std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
|
||||
s_show_confirm = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,13 @@ void RenderSendTab(App* app);
|
||||
*/
|
||||
void RenderSendConfirmPopup(App* app);
|
||||
|
||||
/**
|
||||
* @brief Debug/screenshot-sweep hook: force the send-confirm popup open with demo values (show=true)
|
||||
* or close it (show=false). The popup's state lives in send_tab file statics, so this hook is
|
||||
* the only way an external driver (the full UI sweep) can reveal it. Fires no send.
|
||||
*/
|
||||
void SweepShowSendConfirm(App* app, bool show);
|
||||
|
||||
/**
|
||||
* @brief Pre-fill the "from" address in the send tab
|
||||
* @param address The address to send from
|
||||
|
||||
366
src/ui/windows/wallets_dialog.h
Normal file
366
src/ui/windows/wallets_dialog.h
Normal file
@@ -0,0 +1,366 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Wallet-files list + switcher (multi-wallet P2). Lists wallet files in the datadir (and any
|
||||
// user-added folders) with cached metadata (size from disk; balance / address count / last-opened
|
||||
// from the wallet index, since those can't be read off a wallet.dat without loading it). "Open"
|
||||
// switches the active wallet via App::switchToWallet (daemon restart on -wallet=<name>).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include "../../app.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../util/text_format.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "../notifications.h"
|
||||
#include "../layout.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
#include "folder_picker.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
class WalletsDialog {
|
||||
public:
|
||||
static void show(App* app) {
|
||||
s_open = true;
|
||||
s_app = app;
|
||||
s_needScan = true;
|
||||
}
|
||||
|
||||
static void hide() { s_open = false; }
|
||||
|
||||
static void render() {
|
||||
if (!s_open || !s_app) return;
|
||||
// The folder picker (opened by "Scan another folder") takes over the modal surface while
|
||||
// it's up — the overlay framework doesn't nest, so only one overlay renders at a time.
|
||||
if (FolderPicker::isOpen()) { FolderPicker::render(); return; }
|
||||
using namespace material;
|
||||
App* app = s_app;
|
||||
const float dp = Layout::dpiScale();
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
|
||||
if (s_needScan) { scan(); s_needScan = false; }
|
||||
|
||||
// ---- Content-sized card: a stack of wallet cards, sized to the wallet count (up to
|
||||
// kMaxVisibleRows, then the list scrolls). Computed from the KNOWN row count so the
|
||||
// dialog stays fixed-height (no auto-height feedback that slid content off on a move). --
|
||||
const int kMaxVisibleRows = 7;
|
||||
ImFont* nameFont = Type().subtitle1();
|
||||
ImFont* metaFont = Type().caption();
|
||||
const int visRows = std::min(std::max(1, (int)s_rows.size()), kMaxVisibleRows);
|
||||
const float cardPadY = Layout::spacingMd(); // roomier cards (was spacingSm)
|
||||
const float walRowH = cardPadY * 2.0f + nameFont->LegacySize + Layout::spacingSm() + metaFont->LegacySize;
|
||||
const float cardGap = Layout::spacingMd(); // more breathing room between wallet cards
|
||||
// ItemSpacing is zeroed inside the list child, so the content is exactly cards + inter-card
|
||||
// gaps; a small bottom pad keeps the last card off the clip edge (no spurious scrollbar).
|
||||
const float listH = visRows * walRowH + (float)std::max(0, visRows - 1) * cardGap + Layout::spacingSm();
|
||||
|
||||
const float ctrlRow = ImGui::GetFrameHeightWithSpacing();
|
||||
const float capRow = Type().caption()->LegacySize + style.ItemSpacing.y;
|
||||
const float belowH = 4.0f * Layout::spacingSm()
|
||||
+ (capRow + ctrlRow) // Create label + input row
|
||||
+ ctrlRow // full-width "scan folder" button
|
||||
+ (style.ItemSpacing.y + 1.0f) // separator
|
||||
+ ctrlRow // footer buttons
|
||||
+ 6.0f * style.ItemSpacing.y; // uncounted inter-item gaps
|
||||
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
|
||||
+ Type().caption()->LegacySize + Layout::spacingSm(); // intro + gap
|
||||
const float padV = 48.0f; // content-child padding (top+bottom) + margin
|
||||
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
|
||||
// screen the content-sized card could exceed the window (the framework would clip a too-tall
|
||||
// card, footer and all). When capped, the wallet list flexes + scrolls and the controls
|
||||
// below it stay pinned. In the common case (few wallets) nothing changes.
|
||||
const float desiredH = headH + listH + belowH + padV; // physical
|
||||
const float vpH = ImGui::GetMainViewport()->Size.y;
|
||||
const bool capped = desiredH > vpH * 0.86f;
|
||||
const float cardH = std::min(desiredH, vpH * 0.86f) / dp;
|
||||
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("wallets_title");
|
||||
ov.p_open = &s_open;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 780.0f;
|
||||
ov.cardHeight = cardH;
|
||||
ov.idSuffix = "wallets";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_intro"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
const std::string active = app->settings() ? app->settings()->getActiveWalletFile() : "wallet.dat";
|
||||
|
||||
// ---- Wallet cards (Material-style rows; no table) -------------------------------------
|
||||
const float listW = ImGui::GetContentRegionAvail().x;
|
||||
// Flex only when the card was viewport-capped: give the list the space left above the
|
||||
// create/scan/footer controls so it scrolls internally and those stay visible. Uncapped,
|
||||
// it equals the content height exactly (no spurious scrollbar — the common case).
|
||||
float listHFit = listH;
|
||||
if (capped) listHFit = std::max(walRowH, ImGui::GetContentRegionAvail().y - belowH);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::BeginChild("##walletList", ImVec2(listW, listHFit), false);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); // inter-card gaps are explicit
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
const float rowW = ImGui::GetContentRegionAvail().x;
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* dot = " \xC2\xB7 ";
|
||||
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
|
||||
auto fit = [&](std::string s, ImFont* f, float maxW){
|
||||
bool t=false;
|
||||
while (s.size()>1 && tw(f,s)>maxW){
|
||||
// Pop a whole UTF-8 code point (never split a multibyte char — external .dat names
|
||||
// and some locale metadata are non-ASCII).
|
||||
while (s.size()>1 && (static_cast<unsigned char>(s.back()) & 0xC0) == 0x80) s.pop_back();
|
||||
if (s.size()>1) s.pop_back();
|
||||
t=true;
|
||||
}
|
||||
if (t) s += "\xE2\x80\xA6";
|
||||
return s;
|
||||
};
|
||||
|
||||
for (std::size_t i = 0; i < s_rows.size(); ++i) {
|
||||
const WalletRow& r = s_rows[i];
|
||||
const data::WalletIndexEntry* meta = app->walletIndex().find(r.fileName);
|
||||
const bool isCurrent = r.inDatadir && r.fileName == active;
|
||||
|
||||
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rMax(rMin.x + rowW, rMin.y + walRowH);
|
||||
bool hov = ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(rMin, rMax);
|
||||
|
||||
// Card background + state
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 40 : 26; g.borderAlpha = 45;
|
||||
DrawGlassPanel(dl, rMin, rMax, g);
|
||||
if (isCurrent) {
|
||||
dl->AddRectFilled(rMin, rMax, WithAlpha(Success(), 16), 10.0f);
|
||||
dl->AddRect(rMin, rMax, WithAlpha(Success(), 130), 10.0f, 0, 1.6f * dp);
|
||||
} else if (hov) {
|
||||
dl->AddRect(rMin, rMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f);
|
||||
}
|
||||
|
||||
const float padX = Layout::spacingMd();
|
||||
const float iconSz = nameFont->LegacySize * 1.15f;
|
||||
const float midY = (rMin.y + rMax.y) * 0.5f;
|
||||
float x = rMin.x + padX;
|
||||
dl->AddText(icoFont, iconSz, ImVec2(x, midY - iconSz * 0.5f),
|
||||
isCurrent ? Success() : OnSurfaceMedium(), ICON_MD_ACCOUNT_BALANCE_WALLET);
|
||||
x += iconSz + Layout::spacingMd();
|
||||
|
||||
// Action-button slot on the right — reserve room so text truncates before it.
|
||||
const float btnW = 96.0f * dp, btnH = ImGui::GetFrameHeight();
|
||||
const float btnX = rMax.x - padX - btnW;
|
||||
const float textR = btnX - Layout::spacingMd();
|
||||
|
||||
// Name (+ info icon for external files)
|
||||
const float topY = rMin.y + cardPadY;
|
||||
float extra = (!r.inDatadir) ? (metaFont->LegacySize + Layout::spacingSm()) : 0.0f;
|
||||
std::string nm = fit(r.fileName, nameFont, std::max(24.0f * dp, textR - x - extra));
|
||||
dl->AddText(nameFont, nameFont->LegacySize, ImVec2(x, topY), OnSurface(), nm.c_str());
|
||||
if (!r.inDatadir) {
|
||||
ImVec2 ip(x + tw(nameFont, nm) + Layout::spacingSm(), topY + (nameFont->LegacySize - metaFont->LegacySize));
|
||||
dl->AddText(icoFont, metaFont->LegacySize, ip, OnSurfaceMedium(), ICON_MD_INFO);
|
||||
if (ImGui::IsMouseHoveringRect(ip, ImVec2(ip.x + metaFont->LegacySize, ip.y + metaFont->LegacySize)))
|
||||
Tooltip("%s\n%s", TR("wallets_external_tt"), r.dir.c_str());
|
||||
}
|
||||
|
||||
// Metadata line (size · N addresses · balance DRGX · last opened)
|
||||
std::string ms = util::Platform::formatFileSize((uint64_t)r.sizeBytes);
|
||||
char b[64];
|
||||
if (meta && meta->cachedAddressCount >= 0) { snprintf(b, sizeof(b), "%s%lld %s", dot, (long long)meta->cachedAddressCount, TR("wallets_col_addresses")); ms += b; }
|
||||
if (meta && meta->cachedBalance >= 0.0) { snprintf(b, sizeof(b), "%s%.4f %s", dot, meta->cachedBalance, DRAGONX_TICKER); ms += b; }
|
||||
ms += dot;
|
||||
ms += (meta && meta->lastOpenedEpoch > 0) ? util::formatTimeAgo(meta->lastOpenedEpoch) : std::string(TR("wallets_never"));
|
||||
ms = fit(ms, metaFont, std::max(24.0f * dp, textR - x));
|
||||
dl->AddText(metaFont, metaFont->LegacySize, ImVec2(x, topY + nameFont->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceMedium(), ms.c_str());
|
||||
|
||||
// Action: Active chip (current) / Open / Import
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
if (isCurrent) {
|
||||
const char* al = TR("wallets_active");
|
||||
float aw = tw(metaFont, al), cw = aw + 16.0f * dp, ch = metaFont->LegacySize + 6.0f * dp;
|
||||
ImVec2 cp(rMax.x - padX - cw, midY - ch * 0.5f);
|
||||
dl->AddRectFilled(cp, ImVec2(cp.x + cw, cp.y + ch), WithAlpha(Success(), 34), 5.0f * dp);
|
||||
dl->AddText(metaFont, metaFont->LegacySize, ImVec2(cp.x + 8.0f * dp, cp.y + 3.0f * dp), Success(), al);
|
||||
} else if (r.inDatadir) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(btnX, midY - btnH * 0.5f));
|
||||
if (StyledButton(TR("wallets_open"), ImVec2(btnW, btnH))) {
|
||||
app->switchToWallet(r.fileName);
|
||||
s_open = false;
|
||||
}
|
||||
} else {
|
||||
ImGui::SetCursorScreenPos(ImVec2(btnX, midY - btnH * 0.5f));
|
||||
if (StyledButton(TR("wallets_import"), ImVec2(btnW, btnH)))
|
||||
importAndOpen(app, r);
|
||||
if (ImGui::IsItemHovered()) Tooltip("%s", TR("wallets_import_tt"));
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
// Register the card's footprint with a Dummy (not an interactive item, so nothing
|
||||
// overlaps the action button's ID) and add an explicit gap between cards (ItemSpacing
|
||||
// is 0, so no double spacing; the last card has no trailing gap).
|
||||
ImGui::SetCursorScreenPos(rMin);
|
||||
ImGui::Dummy(ImVec2(rowW, walRowH));
|
||||
if (i + 1 < s_rows.size()) ImGui::Dummy(ImVec2(rowW, cardGap));
|
||||
}
|
||||
ImGui::PopStyleVar(); // ItemSpacing
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// ---- Create a new wallet (primary) — the input + button span the full list width -----
|
||||
const float createBtnW = 140.0f * dp;
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_new_label"));
|
||||
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))) {
|
||||
std::string name = normalizeWalletName(s_newName);
|
||||
std::error_code ec;
|
||||
if (name.empty()) {
|
||||
Notifications::instance().warning(TR("wallets_name_invalid"));
|
||||
} else if (std::filesystem::exists(util::Platform::getDragonXDataDir() + "/" + name, ec)) {
|
||||
Notifications::instance().warning(TR("wallets_exists"));
|
||||
} else {
|
||||
s_newName[0] = '\0';
|
||||
Notifications::instance().info(TR("wallets_creating"));
|
||||
app->switchToWallet(name);
|
||||
s_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// ---- 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))) {
|
||||
FolderPicker::open(util::Platform::getDragonXDataDir(), [app](const std::string& dir) {
|
||||
std::error_code ec;
|
||||
if (!dir.empty() && std::filesystem::is_directory(dir, ec)) {
|
||||
if (app->walletIndex().addExtraFolder(dir)) app->walletIndex().save();
|
||||
s_needScan = true;
|
||||
} else {
|
||||
Notifications::instance().warning(TR("wallets_folder_invalid"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::Separator();
|
||||
// ---- Refresh: icon-only, de-emphasized next to the text actions -----------------------
|
||||
{
|
||||
float bh = ImGui::GetFrameHeight();
|
||||
IconButtonStyle rst;
|
||||
rst.color = OnSurfaceMedium();
|
||||
rst.hoverBg = StateHover();
|
||||
rst.bgRounding = 4.0f * dp;
|
||||
rst.tooltip = TR("refresh");
|
||||
if (IconButton("##walletsRefresh", ICON_MD_REFRESH, Type().iconSmall(), ImVec2(bh, bh), rst))
|
||||
s_needScan = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (StyledButton(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;
|
||||
|
||||
EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct WalletRow {
|
||||
std::string fileName;
|
||||
std::string dir;
|
||||
bool inDatadir = false;
|
||||
long long sizeBytes = 0;
|
||||
};
|
||||
|
||||
// Force a "wallet-...dat" filename (plain, no path) so a new/imported wallet shows in the
|
||||
// datadir scan and can't collide with the daemon's internal .dat files. Returns "" if the
|
||||
// input has no usable characters.
|
||||
static std::string normalizeWalletName(const std::string& raw) {
|
||||
std::string s = raw;
|
||||
while (!s.empty() && static_cast<unsigned char>(s.front()) <= ' ') s.erase(s.begin());
|
||||
while (!s.empty() && static_cast<unsigned char>(s.back()) <= ' ') s.pop_back();
|
||||
auto slash = s.find_last_of("/\\");
|
||||
if (slash != std::string::npos) s = s.substr(slash + 1);
|
||||
if (s.size() > 4 && s.substr(s.size() - 4) == ".dat") s = s.substr(0, s.size() - 4);
|
||||
std::string clean;
|
||||
for (char c : s)
|
||||
if (std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_') clean += c;
|
||||
if (clean.empty()) return "";
|
||||
if (clean.rfind("wallet", 0) != 0) clean = "wallet-" + clean;
|
||||
return clean + ".dat";
|
||||
}
|
||||
|
||||
// Copy an out-of-datadir wallet file into the datadir (the daemon only loads plain filenames
|
||||
// from there) under a wallet-*.dat name, then switch to it. Never overwrites an existing file.
|
||||
static void importAndOpen(App* app, const WalletRow& r) {
|
||||
namespace fs = std::filesystem;
|
||||
std::string dest = normalizeWalletName(r.fileName);
|
||||
if (dest.empty()) { Notifications::instance().warning(TR("wallets_name_invalid")); return; }
|
||||
std::error_code ec;
|
||||
const std::string datadir = util::Platform::getDragonXDataDir();
|
||||
fs::create_directories(datadir, ec);
|
||||
const std::string destPath = datadir + "/" + dest;
|
||||
if (fs::exists(destPath, ec)) { Notifications::instance().warning(TR("wallets_exists")); return; }
|
||||
fs::copy_file(r.dir + "/" + r.fileName, destPath, ec);
|
||||
if (ec) { Notifications::instance().error(TR("wallets_import_failed")); return; }
|
||||
Notifications::instance().success(TR("wallets_imported"));
|
||||
app->switchToWallet(dest);
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
// Scan the datadir (wallet-prefixed *.dat only, to skip peers.dat / asmap.dat / fee_estimates.dat)
|
||||
// and each user-added folder (any *.dat). Exception-safe: iterates with error_codes.
|
||||
static void scan() {
|
||||
s_rows.clear();
|
||||
namespace fs = std::filesystem;
|
||||
auto addFrom = [](const std::string& dir, bool inDatadir) {
|
||||
std::error_code ec;
|
||||
if (dir.empty() || !fs::is_directory(dir, ec)) return;
|
||||
fs::directory_iterator it(dir, ec), end;
|
||||
for (; !ec && it != end; it.increment(ec)) {
|
||||
std::error_code fec;
|
||||
if (!it->is_regular_file(fec)) continue;
|
||||
std::string name = it->path().filename().string();
|
||||
if (name.size() <= 4 || name.substr(name.size() - 4) != ".dat") continue;
|
||||
if (inDatadir && name.rfind("wallet", 0) != 0) continue;
|
||||
WalletRow r;
|
||||
r.fileName = name;
|
||||
r.dir = dir;
|
||||
r.inDatadir = inDatadir;
|
||||
r.sizeBytes = static_cast<long long>(fs::file_size(it->path(), fec));
|
||||
s_rows.push_back(std::move(r));
|
||||
}
|
||||
};
|
||||
addFrom(util::Platform::getDragonXDataDir(), /*inDatadir=*/true);
|
||||
if (s_app)
|
||||
for (const auto& f : s_app->walletIndex().extraFolders())
|
||||
addFrom(f, /*inDatadir=*/false);
|
||||
}
|
||||
|
||||
static inline bool s_open = false;
|
||||
static inline App* s_app = nullptr;
|
||||
static inline bool s_needScan = false;
|
||||
static inline char s_newName[128] = "";
|
||||
static inline std::vector<WalletRow> s_rows;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -3,13 +3,18 @@
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Modal dialog to download / update the DRG-XMRig miner from the project Gitea, driven by
|
||||
// util::XmrigUpdater. States follow the updater: Checking -> UpToDate/UpdateAvailable ->
|
||||
// Downloading/Verifying/Extracting -> Done / Failed. Header-only (static inline state), mirroring
|
||||
// BootstrapDownloadDialog. The installed release tag is persisted to settings on success.
|
||||
// util::XmrigUpdater. Two-pane layout (mirrors the node updater / Manage-Portfolio editor): the list
|
||||
// of miner versions on the LEFT, the selected version's detail + install action on the RIGHT.
|
||||
// States: Listing -> ReleaseList -> Downloading/Verifying/Extracting -> Done / Failed. Header-only.
|
||||
// The installed release tag is persisted to settings on success. Installs are refused while the
|
||||
// miner is running (you can't replace a running binary).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -18,9 +23,10 @@
|
||||
#include "../../resources/embedded_resources.h" // resources::getDaemonDirectory()
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/xmrig_updater.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/markdown.h"
|
||||
#include "../material/type.h"
|
||||
#include "../theme.h"
|
||||
#include "release_list_view.h"
|
||||
#include "imgui.h"
|
||||
@@ -30,151 +36,345 @@ namespace ui {
|
||||
|
||||
class XmrigDownloadDialog {
|
||||
public:
|
||||
using Progress = util::XmrigUpdater::Progress;
|
||||
using St = util::XmrigUpdater::State;
|
||||
|
||||
static void show(App* app) {
|
||||
if (!app) return;
|
||||
s_app = app;
|
||||
s_open = true;
|
||||
s_persisted = false;
|
||||
s_sel = 0;
|
||||
s_sweep = false;
|
||||
s_installed_tag = app->settings() ? app->settings()->getXmrigVersion() : std::string();
|
||||
s_rows.clear();
|
||||
s_releases.clear();
|
||||
s_updater = std::make_unique<util::XmrigUpdater>();
|
||||
s_updater->startCheck(s_installed_tag);
|
||||
s_updater->startListReleases(); // fetch every version up front for the two-pane picker
|
||||
}
|
||||
|
||||
static bool isOpen() { return s_open; }
|
||||
|
||||
// Sweep-only: seed fake releases + a forced state so the offline UI sweep can render the modal.
|
||||
static void sweepSeed(App* app, std::vector<util::XmrigRelease> releases, St state,
|
||||
const std::string& installedTag) {
|
||||
s_app = app;
|
||||
s_open = true;
|
||||
s_persisted = false;
|
||||
s_installed_tag = installedTag;
|
||||
s_sel = 0;
|
||||
s_sweep = true;
|
||||
s_releases = std::move(releases);
|
||||
s_rows.clear();
|
||||
s_updater.reset();
|
||||
s_sweepProgress = Progress{};
|
||||
s_sweepProgress.state = state;
|
||||
if (!s_releases.empty()) s_sweepProgress.latest_tag = s_releases.front().tag;
|
||||
s_sweepProgress.installed_tag = installedTag;
|
||||
s_sweepProgress.percent = 62.0f;
|
||||
s_sweepProgress.status_text = "drg-xmrig-linux-x64.zip";
|
||||
}
|
||||
static void sweepClose() { s_open = false; s_sweep = false; s_releases.clear(); s_rows.clear(); }
|
||||
|
||||
static void render() {
|
||||
if (!s_open || !s_app || !s_updater) {
|
||||
if (!s_open) s_updater.reset(); // closed: drop the updater (dtor joins the worker)
|
||||
if (!s_open || !s_app) {
|
||||
if (!s_open) s_updater.reset();
|
||||
return;
|
||||
}
|
||||
using namespace material;
|
||||
const float dp = Layout::dpiScale();
|
||||
const auto p = s_updater->getProgress();
|
||||
// No window X during an active download/verify/extract — closing would orphan/block on the
|
||||
// worker; the in-dialog Cancel is the intended way to abort.
|
||||
const Progress p = s_sweep ? s_sweepProgress
|
||||
: (s_updater ? s_updater->getProgress() : Progress{});
|
||||
const bool active = (p.state == St::Downloading || p.state == St::Verifying || p.state == St::Extracting);
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("xmrig_update_title"); ov.p_open = active ? nullptr : &s_open;
|
||||
ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 480.0f; ov.cardBottomViewportRatio = 0.94f;
|
||||
ov.title = TR("xmrig_update_title");
|
||||
ov.p_open = active ? nullptr : &s_open;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 880.0f;
|
||||
ov.cardHeight = std::min(760.0f, ImGui::GetMainViewport()->Size.y * 0.84f / dp);
|
||||
ov.idSuffix = "xmrigupd";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
switch (p.state) {
|
||||
case St::Checking: renderChecking(dp, p); break;
|
||||
case St::Idle:
|
||||
case St::Checking:
|
||||
case St::UpToDate:
|
||||
case St::UpdateAvailable: renderPrompt(dp, p); break;
|
||||
case St::Unavailable: renderUnavailable(dp, p); break;
|
||||
case St::Listing: renderListing(dp, p); break;
|
||||
case St::ReleaseList: renderReleaseList(dp, p); break;
|
||||
case St::Downloading:
|
||||
case St::Verifying:
|
||||
case St::Extracting: renderProgress(dp, p); break;
|
||||
case St::Done: renderDone(dp, p); break;
|
||||
case St::Failed: renderFailed(dp, p); break;
|
||||
default: break;
|
||||
case St::UpdateAvailable:
|
||||
case St::Listing: renderCentered(TR("xmrig_loading_releases")); break;
|
||||
case St::Unavailable: renderUnavailable(p); break;
|
||||
case St::Failed:
|
||||
if (s_releases.empty()) { renderListFailed(p); break; }
|
||||
renderTwoPane(dp, p, active); break;
|
||||
default: renderTwoPane(dp, p, active); break;
|
||||
}
|
||||
EndOverlayDialog();
|
||||
}
|
||||
if (!s_open) s_updater.reset();
|
||||
if (!s_open) { s_updater.reset(); s_sweep = false; }
|
||||
}
|
||||
|
||||
private:
|
||||
using Progress = util::XmrigUpdater::Progress;
|
||||
using St = util::XmrigUpdater::State;
|
||||
|
||||
static float fullW() { return ImGui::GetContentRegionAvail().x; }
|
||||
|
||||
// Install button that refuses (with a note) while the miner is running, re-checked live so a
|
||||
// dialog opened before mining started can't replace a now-running binary (TOCTOU guard).
|
||||
static void installAction(const char* label) {
|
||||
static void renderCentered(const char* msg) {
|
||||
using namespace material;
|
||||
if (s_app->isPoolMinerRunning()) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
||||
TR("xmrig_stop_mining_first"));
|
||||
return;
|
||||
}
|
||||
if (TactileButton(label, ImVec2(fullW(), 0)))
|
||||
s_updater->startInstall(resources::getDaemonDirectory());
|
||||
ImVec2 av = ImGui::GetContentRegionAvail();
|
||||
ImFont* f = Type().body2();
|
||||
ImVec2 ts = f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, msg);
|
||||
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + std::max(0.0f, (av.x - ts.x) * 0.5f),
|
||||
ImGui::GetCursorPosY() + av.y * 0.45f));
|
||||
Type().text(TypeStyle::Body2, msg);
|
||||
}
|
||||
|
||||
static void renderChecking(float, const Progress&) {
|
||||
using namespace material;
|
||||
Type().text(TypeStyle::Body2, TR("xmrig_checking"));
|
||||
}
|
||||
|
||||
static void renderUnavailable(float, const Progress& p) {
|
||||
static void renderUnavailable(const Progress& p) {
|
||||
using namespace material;
|
||||
Type().text(TypeStyle::Subtitle2, TR("xmrig_unavailable_title"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", p.status_text.empty()
|
||||
? TR("xmrig_unavailable_body")
|
||||
: p.status_text.c_str());
|
||||
ImGui::TextWrapped("%s", p.status_text.empty() ? TR("xmrig_unavailable_body")
|
||||
: p.status_text.c_str());
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||
if (TactileButton(TR("close"), ImVec2(std::min(fullW(), 200.0f * Layout::dpiScale()), 0)))
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
static void renderPrompt(float, const Progress& p) {
|
||||
static void renderListFailed(const Progress& p) {
|
||||
using namespace material;
|
||||
const std::string installed = p.installed_tag.empty() ? "none" : p.installed_tag;
|
||||
if (p.state == St::UpdateAvailable) {
|
||||
Type().text(TypeStyle::Subtitle2, TR("xmrig_update_available"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s %s", TR("xmrig_latest"), p.latest_tag.c_str());
|
||||
ImGui::Text("%s %s", TR("xmrig_installed"), installed.c_str());
|
||||
ImGui::Spacing();
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("xmrig_verify_note"));
|
||||
ImGui::Spacing();
|
||||
installAction(TR("xmrig_download_install"));
|
||||
} else {
|
||||
Type().text(TypeStyle::Subtitle2, TR("xmrig_up_to_date"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s %s", TR("xmrig_installed"), p.latest_tag.c_str());
|
||||
ImGui::Spacing();
|
||||
installAction(TR("xmrig_reinstall"));
|
||||
}
|
||||
const float bw = std::min(fullW(), 220.0f * Layout::dpiScale());
|
||||
Type().textColored(TypeStyle::Subtitle2, Error(), TR("xmrig_update_failed"));
|
||||
ImGui::Spacing();
|
||||
// Browse all releases (pre-releases / older versions).
|
||||
if (TactileButton(TR("xmrig_browse_releases"), ImVec2(fullW(), 0))) {
|
||||
ImGui::TextWrapped("%s", p.error.empty() ? TR("xmrig_unknown_error") : p.error.c_str());
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("retry"), ImVec2(bw, 0)) && s_updater) {
|
||||
s_releases.clear();
|
||||
s_rows.clear();
|
||||
s_updater->startListReleases();
|
||||
}
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||
if (TactileButton(TR("close"), ImVec2(bw, 0))) s_open = false;
|
||||
}
|
||||
|
||||
static void renderListing(float, const Progress&) {
|
||||
using namespace material;
|
||||
Type().text(TypeStyle::Body2, TR("xmrig_loading_releases"));
|
||||
static void buildRows() {
|
||||
if (!s_rows.empty() || s_releases.empty()) return;
|
||||
const std::string token = util::currentXmrigPlatformToken();
|
||||
s_rows.reserve(s_releases.size());
|
||||
for (const auto& r : s_releases) {
|
||||
ReleaseRow row;
|
||||
row.tag = r.tag;
|
||||
row.title = r.name;
|
||||
row.date = r.publishedAt.size() >= 10 ? r.publishedAt.substr(0, 10) : r.publishedAt;
|
||||
row.prerelease = r.prerelease;
|
||||
row.hasAsset = util::selectXmrigAsset(r, token) >= 0;
|
||||
row.installed = !s_installed_tag.empty() && r.tag == s_installed_tag;
|
||||
s_rows.push_back(std::move(row));
|
||||
}
|
||||
}
|
||||
|
||||
static void renderReleaseList(float dp, const Progress&) {
|
||||
static std::string cleanNotes(const std::string& body) {
|
||||
std::istringstream ss(body);
|
||||
std::string line, out;
|
||||
while (std::getline(ss, line)) {
|
||||
size_t a = line.find_first_not_of(" \t");
|
||||
std::string lt = (a == std::string::npos) ? std::string() : line.substr(a);
|
||||
std::string low = lt;
|
||||
std::transform(low.begin(), low.end(), low.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
if (!lt.empty() && lt[0] == '|') break;
|
||||
if (low.rfind("## checksum", 0) == 0 || low.rfind("#### checksum", 0) == 0 ||
|
||||
low.rfind("checksums", 0) == 0 || low.rfind("### sha", 0) == 0) break;
|
||||
out += line;
|
||||
out += '\n';
|
||||
}
|
||||
while (!out.empty() && (out.back() == '\n' || out.back() == '\r' ||
|
||||
out.back() == ' ' || out.back() == '\t')) out.pop_back();
|
||||
return out;
|
||||
}
|
||||
|
||||
static void startInstallSelected() {
|
||||
if (!s_updater || s_app->isPoolMinerRunning()) return; // can't replace a running miner
|
||||
if (s_sel < 0 || s_sel >= (int)s_releases.size()) return;
|
||||
const util::XmrigRelease rel = s_releases[s_sel];
|
||||
s_updater->startInstallRelease(resources::getDaemonDirectory(), rel);
|
||||
}
|
||||
|
||||
// ================= Two-pane: version list (left) + detail/progress (right) =================
|
||||
static void renderTwoPane(float dp, const Progress& p, bool active) {
|
||||
using namespace material;
|
||||
// Snapshot + build rows once per listing (not every frame). Cleared on browse / show.
|
||||
if (s_rows.empty()) {
|
||||
s_releases = s_updater->getReleases();
|
||||
const std::string token = util::currentXmrigPlatformToken();
|
||||
s_rows.reserve(s_releases.size());
|
||||
for (const auto& r : s_releases) {
|
||||
ReleaseRow row;
|
||||
row.tag = r.tag;
|
||||
row.title = r.name;
|
||||
row.date = r.publishedAt.size() >= 10 ? r.publishedAt.substr(0, 10) : r.publishedAt;
|
||||
row.prerelease = r.prerelease;
|
||||
row.hasAsset = util::selectXmrigAsset(r, token) >= 0;
|
||||
row.installed = !s_installed_tag.empty() && r.tag == s_installed_tag;
|
||||
s_rows.push_back(std::move(row));
|
||||
if (!s_sweep && s_releases.empty() && s_updater) s_releases = s_updater->getReleases();
|
||||
buildRows();
|
||||
if (s_sel >= (int)s_rows.size()) s_sel = s_rows.empty() ? 0 : (int)s_rows.size() - 1;
|
||||
|
||||
ImFont* bodyF = Type().body2();
|
||||
ImFont* capF = Type().caption();
|
||||
|
||||
const bool showFooter = (p.state == St::ReleaseList);
|
||||
const float footerH = showFooter
|
||||
? (ImGui::GetFrameHeight() * 1.25f + ImGui::GetStyle().ItemSpacing.y * 2.0f + Layout::spacingSm())
|
||||
: 0.0f;
|
||||
const float bodyH = std::max(120.0f * dp, ImGui::GetContentRegionAvail().y - footerH);
|
||||
const float contentW = ImGui::GetContentRegionAvail().x;
|
||||
const float gap = Layout::spacingLg();
|
||||
const float masterW = std::min(std::max(contentW * 0.34f, 240.0f * dp), 340.0f * dp);
|
||||
const float detailW = contentW - masterW - gap;
|
||||
|
||||
// ---- LEFT: borderless list of tactile version cards ----
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::BeginChild("##xmrigVersions", ImVec2(masterW, bodyH), false);
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0, 0, 0, 0));
|
||||
ImDrawList* mdl = ImGui::GetWindowDrawList();
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const float rowH = Layout::spacingMd() * 2.0f + bodyF->LegacySize + Layout::spacingXs() + capF->LegacySize;
|
||||
const float rowW = ImGui::GetContentRegionAvail().x;
|
||||
const float round = 8.0f * dp;
|
||||
for (int i = 0; i < (int)s_rows.size(); ++i) {
|
||||
const ReleaseRow& r = s_rows[i];
|
||||
ImGui::PushID(i);
|
||||
bool sel = (s_sel == i);
|
||||
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
||||
if (ImGui::Selectable("##ver", sel, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) {
|
||||
if (!active) s_sel = i;
|
||||
}
|
||||
bool hov = ImGui::IsItemHovered();
|
||||
bool held = ImGui::IsItemActive();
|
||||
ImVec2 rmx(rmn.x + rowW, rmn.y + rowH);
|
||||
ImU32 fill = sel ? WithAlpha(Primary(), held ? 55 : 42)
|
||||
: held ? WithAlpha(OnSurface(), 34)
|
||||
: hov ? WithAlpha(OnSurface(), 24)
|
||||
: WithAlpha(OnSurface(), 12);
|
||||
mdl->AddRectFilled(rmn, rmx, fill, round);
|
||||
if (sel) mdl->AddRect(rmn, rmx, WithAlpha(Primary(), 150), round, 0, 1.6f * dp);
|
||||
if (hov || held) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
const char* ico = nullptr; ImU32 statusC = 0; const char* tt = nullptr; const char* lbl = nullptr;
|
||||
if (r.installed) { ico = ICON_MD_CHECK_CIRCLE; statusC = Success(); tt = TR("xmrig_active_tt"); lbl = TR("upd_installed_badge"); }
|
||||
else if (r.prerelease) { ico = ICON_MD_SCIENCE; statusC = Warning(); tt = TR("xmrig_prerelease_tt"); lbl = TR("upd_prerelease"); }
|
||||
else if (i == 0) { ico = ICON_MD_NEW_RELEASES; statusC = Primary(); tt = TR("xmrig_latest_tt"); lbl = TR("xmrig_latest_badge"); }
|
||||
float rightLimit = rmx.x - Layout::spacingMd();
|
||||
if (ico) {
|
||||
float cs = bodyF->LegacySize * 1.3f;
|
||||
ImVec2 cp(rmx.x - Layout::spacingMd() - cs, (rmn.y + rmx.y) * 0.5f - cs * 0.5f);
|
||||
mdl->AddText(icoF, cs, cp, statusC, ico);
|
||||
if (tt && hov && ImGui::IsMouseHoveringRect(cp, ImVec2(cp.x + cs, cp.y + cs)))
|
||||
Tooltip("%s", tt);
|
||||
rightLimit = cp.x - Layout::spacingSm();
|
||||
}
|
||||
float x = rmn.x + Layout::spacingMd();
|
||||
float tagY = rmn.y + Layout::spacingMd();
|
||||
mdl->AddText(bodyF, bodyF->LegacySize, ImVec2(x, tagY),
|
||||
r.hasAsset ? OnSurface() : OnSurfaceDisabled(), r.tag.c_str());
|
||||
if (lbl) {
|
||||
float bx = x + bodyF->CalcTextSizeA(bodyF->LegacySize, FLT_MAX, 0, r.tag.c_str()).x + Layout::spacingSm();
|
||||
float by = tagY + (bodyF->LegacySize - capF->LegacySize) * 0.5f;
|
||||
if (bx + capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, lbl).x <= rightLimit)
|
||||
mdl->AddText(capF, capF->LegacySize, ImVec2(bx, by), statusC, lbl);
|
||||
}
|
||||
if (!r.date.empty())
|
||||
mdl->AddText(capF, capF->LegacySize,
|
||||
ImVec2(x, rmx.y - Layout::spacingMd() - capF->LegacySize),
|
||||
OnSurfaceMedium(), r.date.c_str());
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
// Can't replace a running miner binary — disable installs while mining (TOCTOU guard).
|
||||
const char* disabledNote = s_app->isPoolMinerRunning() ? TR("xmrig_stop_mining_first") : nullptr;
|
||||
bool back = false;
|
||||
const int idx = RenderReleaseList(s_rows, dp, &back, disabledNote);
|
||||
if (back) { s_rows.clear(); s_updater->startCheck(s_installed_tag); return; }
|
||||
if (idx >= 0 && !s_app->isPoolMinerRunning() && idx < static_cast<int>(s_releases.size())) {
|
||||
const util::XmrigRelease rel = s_releases[idx];
|
||||
s_rows.clear();
|
||||
s_updater->startInstallRelease(resources::getDaemonDirectory(), rel);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::SameLine(0.0f, gap);
|
||||
|
||||
// ---- RIGHT: detail / progress / done / failed for the selected version ----
|
||||
ImGui::BeginChild("##xmrigDetail", ImVec2(detailW, bodyH), false);
|
||||
switch (p.state) {
|
||||
case St::ReleaseList: renderDetail(dp, p); break;
|
||||
case St::Downloading:
|
||||
case St::Verifying:
|
||||
case St::Extracting: renderProgress(dp, p); break;
|
||||
case St::Done: renderDone(dp, p); break;
|
||||
case St::Failed: renderFailed(dp, p); break;
|
||||
default: break;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
if (showFooter) {
|
||||
ImGui::Spacing();
|
||||
const float bw = 170.0f * dp, bh = ImGui::GetFrameHeight() * 1.25f;
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - bw));
|
||||
if (TactileButton(TR("close"), ImVec2(bw, bh))) s_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void renderDetail(float dp, const Progress&) {
|
||||
using namespace material;
|
||||
if (s_sel < 0 || s_sel >= (int)s_releases.size() || s_sel >= (int)s_rows.size()) return;
|
||||
const util::XmrigRelease& rel = s_releases[s_sel];
|
||||
const ReleaseRow& row = s_rows[s_sel];
|
||||
const bool haveInstalled = !s_installed_tag.empty();
|
||||
const bool downgrade = !row.installed && s_sel != 0 && haveInstalled;
|
||||
const bool mining = s_app->isPoolMinerRunning();
|
||||
|
||||
const char* noteStr = mining ? TR("xmrig_stop_mining_first")
|
||||
: !row.hasAsset ? TR("upd_no_build_platform")
|
||||
: downgrade ? TR("xmrig_downgrade_note")
|
||||
: TR("xmrig_verify_note");
|
||||
const char* label = row.installed ? TR("xmrig_reinstall")
|
||||
: (s_sel == 0) ? TR("xmrig_download_install")
|
||||
: TR("xmrig_install_this");
|
||||
|
||||
const float btnH = ImGui::GetFrameHeight() * 1.15f;
|
||||
const float noteH = ImGui::CalcTextSize(noteStr, nullptr, false, ImGui::GetContentRegionAvail().x).y;
|
||||
const float reserve = noteH + btnH + Layout::spacingMd() * 3.0f;
|
||||
const float infoH = std::max(120.0f * dp, ImGui::GetContentRegionAvail().y - reserve);
|
||||
|
||||
// ---- Info card: ONE rounded Material surface (version header + release notes markdown) ----
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, WithAlpha(OnSurface(), 22));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingLg()));
|
||||
ImGui::BeginChild("##xmrigInfo", ImVec2(0, infoH), ImGuiChildFlags_AlwaysUseWindowPadding);
|
||||
{
|
||||
Type().text(TypeStyle::H6, rel.tag.c_str());
|
||||
if (row.installed)
|
||||
Type().textColored(TypeStyle::Subtitle2, Success(), TR("xmrig_status_current"));
|
||||
else if (s_sel == 0)
|
||||
Type().textColored(TypeStyle::Subtitle2, Primary(),
|
||||
haveInstalled ? TR("xmrig_update_available") : TR("xmrig_status_latest"));
|
||||
else if (haveInstalled)
|
||||
Type().textColored(TypeStyle::Subtitle2, Warning(), TR("xmrig_status_older"));
|
||||
else
|
||||
Type().text(TypeStyle::Subtitle2, rel.name.empty() ? rel.tag.c_str() : rel.name.c_str());
|
||||
|
||||
const std::string installed = s_installed_tag.empty() ? std::string(TR("xmrig_none"))
|
||||
: s_installed_tag;
|
||||
ImGui::Spacing();
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
||||
(std::string(TR("xmrig_installed")) + " " + installed).c_str());
|
||||
if (!row.date.empty())
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
||||
(std::string(TR("xmrig_released")) + " " + row.date).c_str());
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
const std::string notes = cleanNotes(rel.body);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
|
||||
ImGui::BeginChild("##xmrigNotes", ImVec2(0, std::max(0.0f, ImGui::GetContentRegionAvail().y)), false);
|
||||
if (notes.empty())
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("xmrig_no_notes"));
|
||||
else
|
||||
RenderMarkdown(notes);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// ---- Below the info card: verify / stop-mining note + install button (centered, text-fit) ----
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Caption, (mining || downgrade) ? Warning() : OnSurfaceMedium(), noteStr);
|
||||
ImGui::Spacing();
|
||||
const float bw = ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
|
||||
ImGui::BeginDisabled(!row.hasAsset || mining);
|
||||
if (TactileButton(label, ImVec2(bw, btnH))) startInstallSelected();
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
static void renderProgress(float dp, const Progress& p) {
|
||||
@@ -186,11 +386,10 @@ private:
|
||||
ImGui::Spacing();
|
||||
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s", p.status_text.c_str());
|
||||
ImGui::TextWrapped("%s", p.status_text.c_str());
|
||||
ImGui::Spacing();
|
||||
// Cancel aborts the in-flight transfer promptly (the curl progress callback returns abort).
|
||||
if (TactileButton(TR("cancel"), ImVec2(fullW(), 0))) {
|
||||
s_updater->cancel();
|
||||
if (s_updater) s_updater->cancel();
|
||||
s_open = false;
|
||||
}
|
||||
}
|
||||
@@ -217,16 +416,21 @@ private:
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", p.error.empty() ? TR("xmrig_unknown_error") : p.error.c_str());
|
||||
ImGui::Spacing();
|
||||
installAction(TR("retry"));
|
||||
ImGui::BeginDisabled(s_app->isPoolMinerRunning());
|
||||
if (TactileButton(TR("retry"), ImVec2(fullW(), 0))) startInstallSelected();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||
}
|
||||
|
||||
static inline bool s_open = false;
|
||||
static inline bool s_persisted = false;
|
||||
static inline bool s_sweep = false;
|
||||
static inline int s_sel = 0;
|
||||
static inline std::string s_installed_tag;
|
||||
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
|
||||
static inline std::vector<util::XmrigRelease> s_releases; // matching releases (install by index)
|
||||
static inline Progress s_sweepProgress;
|
||||
static inline App* s_app = nullptr;
|
||||
static inline std::unique_ptr<util::XmrigUpdater> s_updater;
|
||||
};
|
||||
|
||||
@@ -250,7 +250,91 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["seed_backup_close"] = "Close";
|
||||
strings_["seed_backup_reminder"] = "Your wallet has a 24-word recovery seed phrase. Back it up now in Settings → Node & Security.";
|
||||
strings_["seed_migrate_button"] = "Migrate to seed…";
|
||||
// Multi-wallet: wallet-files list + switcher
|
||||
strings_["wallets_button"] = "Wallets…";
|
||||
strings_["tt_wallets_button"] = "List your wallet files and switch between them";
|
||||
strings_["wallets_title"] = "Wallets";
|
||||
strings_["wallets_intro"] = "Wallet files in your data directory (and any folders you add). Open one to switch — the node restarts to load it, and each wallet keeps its own data.";
|
||||
strings_["wallets_col_name"] = "Wallet";
|
||||
strings_["wallets_col_size"] = "Size";
|
||||
strings_["wallets_col_addresses"] = "Addresses";
|
||||
strings_["wallets_col_balance"] = "Balance";
|
||||
strings_["wallets_col_opened"] = "Last opened";
|
||||
strings_["wallets_current"] = "current";
|
||||
strings_["wallets_active"] = "Active";
|
||||
strings_["wallets_open"] = "Open";
|
||||
strings_["wallets_never"] = "Never";
|
||||
strings_["wallets_import_hint"] = "Import to open";
|
||||
strings_["wallets_import_tt"] = "This wallet is outside the data directory. Opening it will copy it in first (coming soon).";
|
||||
strings_["wallets_external_tt"] = "Outside your data directory \xE2\x80\x94 Import to copy it in.";
|
||||
strings_["wallets_scan_folder"] = "Scan another folder for wallets\xE2\x80\xA6";
|
||||
strings_["wallets_folder_invalid"] = "That folder doesn't exist.";
|
||||
strings_["wallets_import"] = "Import";
|
||||
strings_["wallets_new_label"] = "Create a new wallet:";
|
||||
strings_["wallets_new_hint"] = "Name (e.g. savings)";
|
||||
strings_["wallets_create"] = "Create wallet";
|
||||
strings_["wallets_name_invalid"] = "Please enter a valid wallet name.";
|
||||
strings_["wallets_exists"] = "A wallet with that name already exists.";
|
||||
strings_["wallets_imported"] = "Wallet imported — switching…";
|
||||
strings_["wallets_import_failed"] = "Could not import that wallet file.";
|
||||
strings_["wallets_creating"] = "Creating wallet — the node will restart…";
|
||||
strings_["wallets_reveal"] = "Reveal folder";
|
||||
// In-app folder picker (Scan another folder)
|
||||
strings_["picker_title"] = "Select a folder to scan";
|
||||
strings_["picker_up"] = "Up one level";
|
||||
strings_["picker_home"] = "Home folder";
|
||||
strings_["picker_empty"] = "This folder has no sub-folders or wallet files.";
|
||||
strings_["picker_dat_count"] = "%d wallet file(s) in this folder";
|
||||
strings_["picker_dat_none"] = "No wallet files directly in this folder";
|
||||
strings_["picker_select"] = "Scan this folder";
|
||||
strings_["tt_seed_migrate"] = "Create a new seed-phrase wallet and move your funds into it";
|
||||
// Migrate-to-seed modal
|
||||
strings_["mig_title"] = "Migrate to a seed wallet";
|
||||
strings_["mig_intro"] = "This creates a brand-new wallet backed by a 24-word seed phrase, in an isolated node, so you can move your funds into it. Your current wallet is NOT touched and NO funds move in this step — you'll back up the new seed first, then sweep your funds into it as a separate, confirmed step.";
|
||||
strings_["mig_create"] = "Create seed wallet";
|
||||
// Intro pre-flight (checking the current wallet before offering to migrate)
|
||||
strings_["mig_precheck_checking"] = "Checking your wallet";
|
||||
strings_["mig_already_mnemonic"] = "Your wallet already has a 24-word seed phrase — there's nothing to migrate. Just back it up and keep the words somewhere safe.";
|
||||
strings_["mig_backup_instead"] = "Back up seed phrase";
|
||||
strings_["mig_daemon_too_old"] = "Your DragonX node is too old to create a seed wallet — it lacks the mnemonic support this needs. Update the node (Settings → NODE & SECURITY → Install bundled, or Check for updates), then try again.";
|
||||
strings_["mig_not_connected"] = "Connect to your node first, then reopen this to migrate.";
|
||||
strings_["mig_unlock_to_migrate"] = "Unlock your wallet first, then reopen this to migrate.";
|
||||
strings_["mig_check_failed"] = "Couldn't check your wallet. Make sure it's unlocked and connected, then re-check.";
|
||||
strings_["mig_recheck"] = "Re-check";
|
||||
// Sweep step: balance still loading / no funds to move
|
||||
strings_["mig_checking_balance"] = "Checking your balance";
|
||||
strings_["mig_no_funds"] = "Your wallet has no funds to migrate. You can adopt the new seed wallet directly — it replaces your current (empty) wallet with the seed-backed one. Your old wallet is still moved aside to a timestamped backup, just in case.";
|
||||
strings_["mig_adopt_now"] = "Adopt seed wallet";
|
||||
strings_["mig_nofunds_confirm"] = "I understand this replaces my current wallet with the new seed wallet";
|
||||
// Adopting step note (shown under the spinner while the node restarts + rescans)
|
||||
strings_["mig_adopting_note"] = "The node is restarting on your new wallet and will rescan the chain — this can take several minutes. Leave the wallet running; it reconnects automatically.";
|
||||
strings_["mig_working"] = "Creating your new seed wallet in an isolated node — this can take a minute. Your main wallet keeps running.";
|
||||
strings_["mig_seed_warning"] = "Write these 24 words down in order and store them offline. They are the only backup of your new wallet — if you lose them, the funds you migrate are gone forever.";
|
||||
strings_["mig_receive_addr"] = "New wallet receive address:";
|
||||
strings_["mig_copy_seed"] = "Copy seed";
|
||||
strings_["mig_backed_up"] = "I've written down my seed phrase";
|
||||
strings_["mig_step1_done"] = "Step 1 of 2 complete — your funds have NOT moved yet. Sweeping them into this new wallet is the next step.";
|
||||
strings_["mig_continue_sweep"] = "Continue to sweep";
|
||||
strings_["mig_discard"] = "Discard migration";
|
||||
strings_["mig_sweep_intro"] = "Now sweep all funds from your current wallet into the new seed wallet. This sends a single transaction to the new wallet's address; your seed phrase (already backed up) controls the funds from here on.";
|
||||
strings_["mig_balance"] = "Current wallet balance: %.8f DRGX (minus a small fee)";
|
||||
strings_["mig_to"] = "To: ";
|
||||
strings_["mig_unlock_first"] = "Unlock your wallet first to spend from it.";
|
||||
strings_["mig_sweep_all"] = "Sweep all funds";
|
||||
strings_["mig_later"] = "Later";
|
||||
strings_["mig_sweeping"] = "Sweeping your funds into the new wallet…";
|
||||
strings_["mig_confirming"] = "Waiting for the sweep to confirm on-chain before switching wallets — this guarantees your funds have actually moved.";
|
||||
strings_["mig_confs"] = "Sweep confirmations: %d";
|
||||
strings_["mig_txid"] = "txid: ";
|
||||
strings_["mig_remaining"] = "Remaining in old wallet: %.8f DRGX";
|
||||
strings_["mig_adopt"] = "Make this my wallet";
|
||||
strings_["mig_remainder"] = "Some funds are still in your old wallet (too many inputs for one transaction) — sweep the remainder before switching.";
|
||||
strings_["mig_sweep_remaining"] = "Sweep remaining";
|
||||
strings_["mig_waiting_mine"] = "Waiting for the sweep transaction to be mined…";
|
||||
strings_["mig_adopting"] = "Installing your new wallet and rescanning…";
|
||||
strings_["mig_done"] = "Migration complete. Your wallet is now backed by your seed phrase.";
|
||||
strings_["mig_done_detail"] = "The daemon is rescanning to show your funds — this can take a few minutes. Your previous wallet was saved as a .bak in the data folder.";
|
||||
strings_["mig_done_btn"] = "Done";
|
||||
strings_["contacts_search_placeholder"] = "Search contacts...";
|
||||
strings_["contacts_search_no_match"] = "No matching contacts";
|
||||
strings_["address_book_confirm_delete"] = "Confirm delete?";
|
||||
@@ -298,6 +382,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["stop_external"] = "Stop external daemon";
|
||||
strings_["verbose_logging"] = "Verbose logging";
|
||||
strings_["screenshot_sweep"] = "Run screenshot sweep";
|
||||
strings_["screenshot_sweep_full"] = "Full UI sweep";
|
||||
strings_["screenshot_open_dir"] = "Open location";
|
||||
strings_["screenshot_sweep_desc"] = "Cycles every theme across every tab and saves a screenshot of each into per-tab subfolders under the config directory's screenshots folder (overwriting the previous sweep). Runs for a few seconds.";
|
||||
strings_["mine_when_idle"] = "Mine when idle";
|
||||
@@ -489,6 +574,11 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["confirm_reinstall_daemon_title"] = "Install Bundled Daemon";
|
||||
strings_["confirm_reinstall_daemon_msg"] = "This stops the daemon, overwrites the installed dragonxd (and dragonx-cli/dragonx-tx) with the versions bundled in this wallet build, then restarts the node. Use this to recover or update the node binary.";
|
||||
strings_["confirm_reinstall_daemon_safe"] = "Your wallet, keys and blockchain data are not touched — only the daemon program files are replaced.";
|
||||
strings_["daemon_update_title"] = "Update the node daemon?";
|
||||
strings_["daemon_update_body"] = "This wallet build bundles a newer DragonX node than the one currently installed. Updating replaces the installed dragonxd (and dragonx-cli/dragonx-tx), then stops and restarts the node so the new version takes effect. Recommended — a newer node can add features (e.g. seed-phrase support) the old one lacks.";
|
||||
strings_["daemon_update_safe"] = "Your wallet, keys and blockchain data are not touched — only the daemon program files are replaced. If you deliberately run a custom node, choose Keep current.";
|
||||
strings_["daemon_update_now"] = "Update now";
|
||||
strings_["daemon_update_keep"] = "Keep current";
|
||||
strings_["confirm_restart_daemon_title"] = "Restart Daemon";
|
||||
strings_["confirm_restart_daemon_msg"] = "This stops and restarts the daemon to apply the changed options. The wallet will briefly disconnect and reconnect.";
|
||||
strings_["lite_maintenance"] = "Maintenance";
|
||||
@@ -1007,6 +1097,9 @@ void I18n::loadBuiltinEnglish()
|
||||
// --- Address Book Dialog ---
|
||||
strings_["address_book_add"] = "Add Address";
|
||||
strings_["address_book_add_new"] = "Add New";
|
||||
strings_["contact_global"] = "Show in every wallet (global contact)";
|
||||
strings_["contact_global_tt"] = "On: this contact stays visible no matter which wallet you load. Off: it belongs to the current wallet only.";
|
||||
strings_["contact_global_badge_tt"] = "Global contact — visible in every wallet";
|
||||
strings_["address_book_added"] = "Address added to book";
|
||||
strings_["address_book_count"] = "%zu addresses saved";
|
||||
strings_["address_book_deleted"] = "Entry deleted";
|
||||
@@ -1215,6 +1308,10 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["portfolio_all_transparent"] = "All transparent";
|
||||
strings_["portfolio_clear_sel"] = "Clear";
|
||||
strings_["portfolio_no_entries"] = "No custom entries yet. Add one to track a group of addresses.";
|
||||
strings_["portfolio_style_label"] = "Portfolio style";
|
||||
strings_["portfolio_style_compact"] = "Compact rows";
|
||||
strings_["portfolio_style_detailed"] = "Detailed rows";
|
||||
strings_["portfolio_style_featured"] = "Featured rows";
|
||||
strings_["portfolio_edit"] = "Edit";
|
||||
strings_["portfolio_delete"] = "Delete";
|
||||
strings_["portfolio_save"] = "Save";
|
||||
@@ -1275,6 +1372,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["mining_payout_tooltip"] = "Address to receive mining rewards";
|
||||
strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address";
|
||||
strings_["mining_pool"] = "Pool";
|
||||
strings_["mining_payout_foreign"] = "⚠ This payout address isn't in your current wallet — mined rewards would go to a different wallet. Update it if you switched wallets.";
|
||||
strings_["mining_pool_hashrate"] = "Pool Hashrate";
|
||||
strings_["mining_pool_url"] = "Pool URL";
|
||||
// Pool selection mode + auto-balance (mining tab).
|
||||
@@ -1346,6 +1444,17 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["xmrig_unknown_error"] = "Unknown error.";
|
||||
strings_["xmrig_browse_releases"] = "Browse all releases…";
|
||||
strings_["xmrig_loading_releases"] = "Loading releases…";
|
||||
strings_["xmrig_latest_badge"] = "latest";
|
||||
strings_["xmrig_status_current"] = "This version is installed";
|
||||
strings_["xmrig_status_latest"] = "Latest version";
|
||||
strings_["xmrig_status_older"] = "Older than your installed version";
|
||||
strings_["xmrig_released"] = "Released:";
|
||||
strings_["xmrig_no_notes"] = "No release notes for this version.";
|
||||
strings_["xmrig_install_this"] = "Install this version";
|
||||
strings_["xmrig_downgrade_note"] = "Installing a different miner version replaces the current one; stop mining first, then it takes effect on the next start.";
|
||||
strings_["xmrig_active_tt"] = "The currently installed (active) miner";
|
||||
strings_["xmrig_latest_tt"] = "Newest available version";
|
||||
strings_["xmrig_prerelease_tt"] = "Pre-release / testing build";
|
||||
|
||||
// --- Shared release picker ("Browse all releases") ---
|
||||
strings_["upd_select_version"] = "Select a version to install";
|
||||
@@ -1383,6 +1492,16 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["daemon_update_browse"] = "Browse all releases…";
|
||||
strings_["daemon_update_loading"] = "Loading releases…";
|
||||
strings_["daemon_update_downgrade_note"] = "Older versions may be incompatible with your current chain data. Installing a different version takes effect after a daemon restart.";
|
||||
strings_["daemon_update_latest_badge"] = "latest";
|
||||
strings_["daemon_update_status_current"] = "This version is installed";
|
||||
strings_["daemon_update_status_latest"] = "Latest version";
|
||||
strings_["daemon_update_status_older"] = "Older than your installed version";
|
||||
strings_["daemon_update_released"] = "Released:";
|
||||
strings_["daemon_update_no_notes"] = "No release notes for this version.";
|
||||
strings_["daemon_update_install_this"] = "Install this version";
|
||||
strings_["daemon_update_active_tt"] = "The currently installed (active) node";
|
||||
strings_["daemon_update_latest_tt"] = "Newest available version";
|
||||
strings_["daemon_update_prerelease_tt"] = "Pre-release / testing build";
|
||||
|
||||
// --- Lite Network tab (server browser) ---
|
||||
strings_["lite_net_title"] = "Lite Servers";
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "chat/chat_database.h"
|
||||
#include "daemon/daemon_controller.h"
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "data/address_book.h"
|
||||
#include "data/wallet_index.h"
|
||||
#include "daemon/lifecycle_adapters.h"
|
||||
#include "data/wallet_state.h"
|
||||
#include "rpc/connection.h"
|
||||
@@ -51,6 +53,7 @@
|
||||
#include <cmath>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -6029,6 +6032,140 @@ void testHushChatShuffledReceive()
|
||||
|
||||
} // namespace
|
||||
|
||||
// Per-wallet address-book scoping (multi-wallet P1): global vs wallet-scoped contacts, the
|
||||
// legacy-entry migration, and scope-aware de-duplication.
|
||||
void testAddressBookScope()
|
||||
{
|
||||
using dragonx::data::AddressBook;
|
||||
using dragonx::data::AddressBookEntry;
|
||||
|
||||
// --- Pure visibility logic (drives the contacts-tab filter) ---
|
||||
AddressBookEntry g("Alice", "zs1alice", "", "global");
|
||||
AddressBookEntry wA("Bob", "zs1bob", "", "hashA");
|
||||
AddressBookEntry legacy; legacy.scope = ""; // legacy entry (no scope) => global
|
||||
EXPECT_TRUE(g.isGlobal());
|
||||
EXPECT_TRUE(!wA.isGlobal());
|
||||
EXPECT_TRUE(legacy.isGlobal());
|
||||
EXPECT_TRUE(g.visibleInWallet("hashA")); // global shows everywhere
|
||||
EXPECT_TRUE(g.visibleInWallet("hashB"));
|
||||
EXPECT_TRUE(wA.visibleInWallet("hashA")); // wallet-scoped shows only in its wallet
|
||||
EXPECT_TRUE(!wA.visibleInWallet("hashB"));
|
||||
EXPECT_TRUE(legacy.visibleInWallet("anything"));
|
||||
|
||||
// --- Round-trip + migration + scope-aware dedup, isolated under a temp $HOME ---
|
||||
const char* oldHome = std::getenv("HOME");
|
||||
std::string savedHome = oldHome ? oldHome : "";
|
||||
fs::path tmp = fs::temp_directory_path() /
|
||||
("obsidian_ab_test_" + std::to_string((long long)::time(nullptr)));
|
||||
fs::create_directories(tmp);
|
||||
setenv("HOME", tmp.string().c_str(), 1);
|
||||
|
||||
// A pre-scoping addressbook.json (no "scope" field) migrates to global on load.
|
||||
fs::path cfg = tmp / ".config" / "ObsidianDragon";
|
||||
fs::create_directories(cfg);
|
||||
std::ofstream(cfg / "addressbook.json")
|
||||
<< R"({"entries":[{"label":"Legacy","address":"zs1legacy","notes":""}]})";
|
||||
{
|
||||
AddressBook book;
|
||||
EXPECT_TRUE(book.load());
|
||||
EXPECT_EQ(book.size(), (size_t)1);
|
||||
EXPECT_TRUE(book.entries()[0].isGlobal()); // migrated -> global
|
||||
}
|
||||
|
||||
// Same address may be a contact in two DIFFERENT wallets, but not twice in one; and a global
|
||||
// entry collides with any wallet-scoped one for the same address (they'd show together).
|
||||
{
|
||||
AddressBook book;
|
||||
book.load(); // has the migrated global "Legacy"
|
||||
EXPECT_TRUE(book.addEntry(AddressBookEntry("BobA", "zs1bob", "", "walletA")));
|
||||
EXPECT_TRUE(book.addEntry(AddressBookEntry("BobB", "zs1bob", "", "walletB"))); // diff wallet: ok
|
||||
EXPECT_TRUE(!book.addEntry(AddressBookEntry("BobA2", "zs1bob", "", "walletA"))); // same wallet: dup
|
||||
EXPECT_TRUE(!book.addEntry(AddressBookEntry("BobG", "zs1bob", "", "global"))); // global collides
|
||||
EXPECT_TRUE(!book.hasVisibleDuplicate("zs1novel", "walletA")); // novel address: ok
|
||||
}
|
||||
|
||||
// Scope survives save + reload.
|
||||
{
|
||||
AddressBook reopened;
|
||||
reopened.load();
|
||||
int ia = -1, ib = -1;
|
||||
for (size_t i = 0; i < reopened.size(); ++i) {
|
||||
const auto& e = reopened.entries()[i];
|
||||
if (e.address == "zs1bob" && e.scope == "walletA") ia = (int)i;
|
||||
if (e.address == "zs1bob" && e.scope == "walletB") ib = (int)i;
|
||||
}
|
||||
EXPECT_TRUE(ia >= 0); // both wallet-scoped Bobs round-tripped
|
||||
EXPECT_TRUE(ib >= 0);
|
||||
}
|
||||
|
||||
if (!savedHome.empty()) setenv("HOME", savedHome.c_str(), 1); else unsetenv("HOME");
|
||||
std::error_code ec; fs::remove_all(tmp, ec);
|
||||
}
|
||||
|
||||
// Wallet-files metadata index (multi-wallet P1b): upsert change-detection, find, extra folders,
|
||||
// and save/reload round-trip.
|
||||
void testWalletIndex()
|
||||
{
|
||||
using dragonx::data::WalletIndex;
|
||||
using dragonx::data::WalletIndexEntry;
|
||||
|
||||
// upsert change-detection + find + folders (in-memory)
|
||||
{
|
||||
WalletIndex idx;
|
||||
WalletIndexEntry a; a.fileName = "wallet.dat"; a.cachedBalance = 1.5; a.cachedAddressCount = 3;
|
||||
EXPECT_TRUE(idx.upsert(a)); // new -> changed
|
||||
EXPECT_TRUE(!idx.upsert(a)); // identical -> no change (skips save)
|
||||
a.cachedBalance = 2.0;
|
||||
EXPECT_TRUE(idx.upsert(a)); // value changed -> changed
|
||||
EXPECT_TRUE(idx.find("wallet.dat") != nullptr);
|
||||
EXPECT_NEAR(idx.find("wallet.dat")->cachedBalance, 2.0, 1e-9);
|
||||
EXPECT_TRUE(idx.find("missing.dat") == nullptr);
|
||||
WalletIndexEntry b; b.fileName = "wallet-2.dat";
|
||||
idx.upsert(b);
|
||||
EXPECT_EQ(idx.entries().size(), (size_t)2);
|
||||
EXPECT_TRUE(idx.addExtraFolder("/some/dir"));
|
||||
EXPECT_TRUE(!idx.addExtraFolder("/some/dir")); // duplicate rejected
|
||||
EXPECT_TRUE(idx.removeExtraFolder("/some/dir"));
|
||||
EXPECT_TRUE(!idx.removeExtraFolder("/some/dir")); // already gone
|
||||
}
|
||||
|
||||
// save + reload round-trip under a temp $HOME
|
||||
{
|
||||
const char* oldHome = std::getenv("HOME");
|
||||
std::string savedHome = oldHome ? oldHome : "";
|
||||
fs::path tmp = fs::temp_directory_path() /
|
||||
("obsidian_wi_test_" + std::to_string((long long)::time(nullptr)));
|
||||
fs::create_directories(tmp);
|
||||
setenv("HOME", tmp.string().c_str(), 1);
|
||||
|
||||
{
|
||||
WalletIndex idx;
|
||||
idx.load();
|
||||
WalletIndexEntry a; a.fileName = "wallet.dat"; a.displayName = "Main";
|
||||
a.cachedBalance = 12.34; a.cachedAddressCount = 7; a.walletIdentityHash = "hashX";
|
||||
a.lastOpenedEpoch = 1720000000; a.sizeBytesAtLastOpen = 98304; a.syncedHere = true;
|
||||
idx.upsert(a);
|
||||
idx.addExtraFolder("/other/wallets");
|
||||
EXPECT_TRUE(idx.save());
|
||||
}
|
||||
{
|
||||
WalletIndex idx;
|
||||
EXPECT_TRUE(idx.load());
|
||||
const auto* w = idx.find("wallet.dat");
|
||||
EXPECT_TRUE(w != nullptr);
|
||||
EXPECT_EQ(w->displayName, std::string("Main"));
|
||||
EXPECT_EQ(w->cachedAddressCount, (long long)7);
|
||||
EXPECT_EQ(w->walletIdentityHash, std::string("hashX"));
|
||||
EXPECT_TRUE(w->syncedHere);
|
||||
EXPECT_NEAR(w->cachedBalance, 12.34, 1e-9);
|
||||
EXPECT_EQ(idx.extraFolders().size(), (size_t)1);
|
||||
}
|
||||
|
||||
if (!savedHome.empty()) setenv("HOME", savedHome.c_str(), 1); else unsetenv("HOME");
|
||||
std::error_code ec2; fs::remove_all(tmp, ec2);
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
testConnectionConfig();
|
||||
@@ -6063,6 +6200,8 @@ int main()
|
||||
testExplorerBlockCache();
|
||||
testTransactionHistoryCache();
|
||||
testTransactionHistoryCachePrunesOldWallets();
|
||||
testAddressBookScope();
|
||||
testWalletIndex();
|
||||
testTransactionHistoryCacheRefreshApply();
|
||||
testLiteBridgeOwnedStringCopiesBeforeFreeOnSuccess();
|
||||
testLiteBridgeOwnedStringClassifiesNullWithoutFree();
|
||||
|
||||
Reference in New Issue
Block a user