feat(wallets): open external wallets in place via datadir link (no copy)
Replace the "Import" action — which copied an out-of-datadir wallet into the datadir and cluttered the list with hard-to-tell-apart duplicates — with an in-place "Open" that links the real file into the datadir under a stable per-target name (wallet-ip-<FNV8>.dat) and switches to it. The daemon only loads a bare filename from its own datadir, so a link is the minimal bridge: hard link first (no privileges, same volume — covers non-admin Windows), symlink fallback (cross-volume), then an error. Never a copy (that would fork the wallet) and never a delete of a real file. Also: - show each external wallet's originating sub-directory (…/Backups/2021) - add a per-row "open folder location" button - widen the modal (780 → 860) for the extra button - guard the daemon against a dangling link (external file moved / USB unplugged) by falling back to wallet.dat rather than creating an empty one Per-target link names (not one shared name) make switching between two external wallets a real -wallet switch, so the rescan/cache index tracks each correctly. Drops the now-dead wallets_import* i18n keys and back-fills the new open/folder strings across all 8 languages + rebuilds the CJK subset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -1344,20 +1344,18 @@
|
||||
"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_external_tt": "Außerhalb deines Datenverzeichnisses — Öffnen verlinkt sie an ihrem Ort (keine Kopie).",
|
||||
"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 geöffnet",
|
||||
"wallets_new_hint": "Name (z. B. Ersparnisse)",
|
||||
"wallets_new_label": "Eine neue Wallet erstellen:",
|
||||
"wallets_open": "Öffnen",
|
||||
"wallets_open_failed": "Diese Wallet konnte nicht an ihrem Ort geöffnet werden. Sie liegt vermutlich auf einem anderen Laufwerk als dein Datenverzeichnis — verschiebe sie auf dasselbe Laufwerk (unter Windows erlaubt auch der aktivierte Entwicklermodus laufwerkübergreifende Verknüpfungen).",
|
||||
"wallets_open_folder": "Ordnerpfad öffnen",
|
||||
"wallets_open_inplace_tt": "Öffnet diese Wallet an ihrem Ort — im Datenverzeichnis verlinkt (keine Kopie)",
|
||||
"wallets_reveal": "Ordner anzeigen",
|
||||
"wallets_sort_addresses": "Adressen",
|
||||
"wallets_sort_asc": "Aufsteigend (ältestes / wenigste / kleinstes zuerst)",
|
||||
|
||||
@@ -1344,20 +1344,18 @@
|
||||
"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_external_tt": "Fuera de tu directorio de datos — Abrir lo enlaza en su lugar (sin copiar).",
|
||||
"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 abierta",
|
||||
"wallets_new_hint": "Nombre (p. ej. ahorros)",
|
||||
"wallets_new_label": "Crear una nueva cartera:",
|
||||
"wallets_open": "Abrir",
|
||||
"wallets_open_failed": "No se pudo abrir esta cartera en su ubicación. Probablemente está en una unidad distinta a tu directorio de datos: muévela a la misma unidad (en Windows, activar el Modo de desarrollador también permite enlazar entre unidades).",
|
||||
"wallets_open_folder": "Abrir ubicación de la carpeta",
|
||||
"wallets_open_inplace_tt": "Abre esta cartera donde está, enlazándola al directorio de datos (sin copiar)",
|
||||
"wallets_reveal": "Mostrar carpeta",
|
||||
"wallets_sort_addresses": "Direcciones",
|
||||
"wallets_sort_asc": "Ascendente (más antiguo / menos / más pequeño primero)",
|
||||
|
||||
@@ -1344,20 +1344,18 @@
|
||||
"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_external_tt": "Hors de votre répertoire de données — Ouvrir le lie sur place (sans copie).",
|
||||
"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 ouvert",
|
||||
"wallets_new_hint": "Nom (par ex. épargne)",
|
||||
"wallets_new_label": "Créer un nouveau portefeuille :",
|
||||
"wallets_open": "Ouvrir",
|
||||
"wallets_open_failed": "Impossible d'ouvrir ce portefeuille à son emplacement. Il se trouve probablement sur un lecteur différent de votre répertoire de données — déplacez-le sur le même lecteur (sous Windows, activer le mode développeur permet aussi de créer des liens entre lecteurs).",
|
||||
"wallets_open_folder": "Ouvrir l'emplacement du dossier",
|
||||
"wallets_open_inplace_tt": "Ouvre ce portefeuille à son emplacement — lié au répertoire de données (sans copie)",
|
||||
"wallets_reveal": "Afficher le dossier",
|
||||
"wallets_sort_addresses": "Adresses",
|
||||
"wallets_sort_asc": "Croissant (plus ancien / moins / plus petit d'abord)",
|
||||
|
||||
@@ -1344,20 +1344,18 @@
|
||||
"wallets_creating": "ウォレットを作成中 — ノードが再起動します…",
|
||||
"wallets_current": "現在",
|
||||
"wallets_exists": "その名前のウォレットは既に存在します。",
|
||||
"wallets_external_tt": "データディレクトリの外にあります — インポートでコピーします。",
|
||||
"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_open_failed": "このウォレットをその場で開けませんでした。データディレクトリとは別のドライブにある可能性があります — 同じドライブに移動してください(Windows では開発者モードを有効にするとドライブ間のリンクも可能になります)。",
|
||||
"wallets_open_folder": "フォルダーの場所を開く",
|
||||
"wallets_open_inplace_tt": "このウォレットをその場で開きます — データディレクトリにリンク(コピーなし)",
|
||||
"wallets_reveal": "フォルダを開く",
|
||||
"wallets_sort_addresses": "アドレス",
|
||||
"wallets_sort_asc": "昇順(古い/少ない/小さい順)",
|
||||
|
||||
@@ -1344,20 +1344,18 @@
|
||||
"wallets_creating": "지갑을 만드는 중 — 노드가 재시작됩니다…",
|
||||
"wallets_current": "현재",
|
||||
"wallets_exists": "같은 이름의 지갑이 이미 존재합니다.",
|
||||
"wallets_external_tt": "데이터 디렉터리 외부에 있음 — 가져오기로 복사합니다.",
|
||||
"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_open_failed": "이 지갑을 제자리에서 열 수 없습니다. 데이터 디렉터리와 다른 드라이브에 있을 가능성이 높습니다 — 같은 드라이브로 옮기세요 (Windows에서는 개발자 모드를 켜면 드라이브 간 링크도 가능합니다).",
|
||||
"wallets_open_folder": "폴더 위치 열기",
|
||||
"wallets_open_inplace_tt": "이 지갑을 있는 자리에서 엽니다 — 데이터 디렉터리에 링크 (복사 없음)",
|
||||
"wallets_reveal": "폴더 열기",
|
||||
"wallets_sort_addresses": "주소",
|
||||
"wallets_sort_asc": "오름차순 (오래된/적은/작은 순)",
|
||||
|
||||
@@ -1344,20 +1344,18 @@
|
||||
"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_external_tt": "Fora do seu diretório de dados — Abrir o vincula no lugar (sem cópia).",
|
||||
"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 aberta",
|
||||
"wallets_new_hint": "Nome (ex.: poupança)",
|
||||
"wallets_new_label": "Criar uma nova carteira:",
|
||||
"wallets_open": "Abrir",
|
||||
"wallets_open_failed": "Não foi possível abrir esta carteira no lugar. Provavelmente está em uma unidade diferente do seu diretório de dados — mova-a para a mesma unidade (no Windows, ativar o Modo de Desenvolvedor também permite vincular entre unidades).",
|
||||
"wallets_open_folder": "Abrir local da pasta",
|
||||
"wallets_open_inplace_tt": "Abre esta carteira onde está — vinculada ao diretório de dados (sem cópia)",
|
||||
"wallets_reveal": "Mostrar pasta",
|
||||
"wallets_sort_addresses": "Endereços",
|
||||
"wallets_sort_asc": "Crescente (mais antigo / menos / menor primeiro)",
|
||||
|
||||
@@ -1344,20 +1344,18 @@
|
||||
"wallets_creating": "Создание кошелька — узел будет перезапущен…",
|
||||
"wallets_current": "текущий",
|
||||
"wallets_exists": "Кошелёк с таким именем уже существует.",
|
||||
"wallets_external_tt": "Вне вашего каталога данных — «Импорт» скопирует его.",
|
||||
"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_open_failed": "Не удалось открыть этот кошелёк на месте. Вероятно, он на другом диске, чем каталог данных — переместите его на тот же диск (в Windows включённый режим разработчика также позволяет создавать ссылки между дисками).",
|
||||
"wallets_open_folder": "Открыть расположение папки",
|
||||
"wallets_open_inplace_tt": "Открывает этот кошелёк на месте — по ссылке в каталоге данных (без копирования)",
|
||||
"wallets_reveal": "Открыть папку",
|
||||
"wallets_sort_addresses": "Адреса",
|
||||
"wallets_sort_asc": "По возрастанию (сначала старые / меньше / меньший)",
|
||||
|
||||
@@ -1344,20 +1344,18 @@
|
||||
"wallets_creating": "正在创建钱包——节点将重启…",
|
||||
"wallets_current": "当前",
|
||||
"wallets_exists": "已存在同名钱包。",
|
||||
"wallets_external_tt": "位于数据目录之外 — 导入以将其复制进来。",
|
||||
"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_open_failed": "无法在原位置打开此钱包。它可能与数据目录位于不同的驱动器上 — 请将其移动到同一驱动器(在 Windows 上,启用开发者模式也可跨驱动器链接)。",
|
||||
"wallets_open_folder": "打开文件夹位置",
|
||||
"wallets_open_inplace_tt": "在原位置打开此钱包 — 链接到数据目录(不复制)",
|
||||
"wallets_reveal": "打开文件夹",
|
||||
"wallets_sort_addresses": "地址",
|
||||
"wallets_sort_asc": "升序(最早/最少/最小优先)",
|
||||
|
||||
@@ -2196,6 +2196,42 @@ TRANSLATIONS = {
|
||||
"ru": "По убыванию (сначала новые / больше / больший)",
|
||||
"zh": "降序(最新/最多/最大优先)", "ja": "降順(新しい/多い/大きい順)", "ko": "내림차순 (최신/많은/큰 순)"
|
||||
},
|
||||
"wallets_open_folder": {
|
||||
"es": "Abrir ubicación de la carpeta", "de": "Ordnerpfad öffnen",
|
||||
"fr": "Ouvrir l'emplacement du dossier", "pt": "Abrir local da pasta",
|
||||
"ru": "Открыть расположение папки", "zh": "打开文件夹位置",
|
||||
"ja": "フォルダーの場所を開く", "ko": "폴더 위치 열기"
|
||||
},
|
||||
"wallets_open_inplace_tt": {
|
||||
"es": "Abre esta cartera donde está, enlazándola al directorio de datos (sin copiar)",
|
||||
"de": "Öffnet diese Wallet an ihrem Ort — im Datenverzeichnis verlinkt (keine Kopie)",
|
||||
"fr": "Ouvre ce portefeuille à son emplacement — lié au répertoire de données (sans copie)",
|
||||
"pt": "Abre esta carteira onde está — vinculada ao diretório de dados (sem cópia)",
|
||||
"ru": "Открывает этот кошелёк на месте — по ссылке в каталоге данных (без копирования)",
|
||||
"zh": "在原位置打开此钱包 — 链接到数据目录(不复制)",
|
||||
"ja": "このウォレットをその場で開きます — データディレクトリにリンク(コピーなし)",
|
||||
"ko": "이 지갑을 있는 자리에서 엽니다 — 데이터 디렉터리에 링크 (복사 없음)"
|
||||
},
|
||||
"wallets_open_failed": {
|
||||
"es": "No se pudo abrir esta cartera en su ubicación. Probablemente está en una unidad distinta a tu directorio de datos: muévela a la misma unidad (en Windows, activar el Modo de desarrollador también permite enlazar entre unidades).",
|
||||
"de": "Diese Wallet konnte nicht an ihrem Ort geöffnet werden. Sie liegt vermutlich auf einem anderen Laufwerk als dein Datenverzeichnis — verschiebe sie auf dasselbe Laufwerk (unter Windows erlaubt auch der aktivierte Entwicklermodus laufwerkübergreifende Verknüpfungen).",
|
||||
"fr": "Impossible d'ouvrir ce portefeuille à son emplacement. Il se trouve probablement sur un lecteur différent de votre répertoire de données — déplacez-le sur le même lecteur (sous Windows, activer le mode développeur permet aussi de créer des liens entre lecteurs).",
|
||||
"pt": "Não foi possível abrir esta carteira no lugar. Provavelmente está em uma unidade diferente do seu diretório de dados — mova-a para a mesma unidade (no Windows, ativar o Modo de Desenvolvedor também permite vincular entre unidades).",
|
||||
"ru": "Не удалось открыть этот кошелёк на месте. Вероятно, он на другом диске, чем каталог данных — переместите его на тот же диск (в Windows включённый режим разработчика также позволяет создавать ссылки между дисками).",
|
||||
"zh": "无法在原位置打开此钱包。它可能与数据目录位于不同的驱动器上 — 请将其移动到同一驱动器(在 Windows 上,启用开发者模式也可跨驱动器链接)。",
|
||||
"ja": "このウォレットをその場で開けませんでした。データディレクトリとは別のドライブにある可能性があります — 同じドライブに移動してください(Windows では開発者モードを有効にするとドライブ間のリンクも可能になります)。",
|
||||
"ko": "이 지갑을 제자리에서 열 수 없습니다. 데이터 디렉터리와 다른 드라이브에 있을 가능성이 높습니다 — 같은 드라이브로 옮기세요 (Windows에서는 개발자 모드를 켜면 드라이브 간 링크도 가능합니다)."
|
||||
},
|
||||
"wallets_external_tt": {
|
||||
"es": "Fuera de tu directorio de datos — Abrir lo enlaza en su lugar (sin copiar).",
|
||||
"de": "Außerhalb deines Datenverzeichnisses — Öffnen verlinkt sie an ihrem Ort (keine Kopie).",
|
||||
"fr": "Hors de votre répertoire de données — Ouvrir le lie sur place (sans copie).",
|
||||
"pt": "Fora do seu diretório de dados — Abrir o vincula no lugar (sem cópia).",
|
||||
"ru": "Вне каталога данных — «Открыть» создаёт ссылку на месте (без копирования).",
|
||||
"zh": "在数据目录之外 —「打开」会就地链接(不复制)。",
|
||||
"ja": "データディレクトリの外 —「開く」はその場でリンクします(コピーなし)。",
|
||||
"ko": "데이터 디렉터리 밖 — '열기'는 제자리에 링크합니다 (복사 없음)."
|
||||
},
|
||||
}
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "daemon_controller.h"
|
||||
#include "../config/settings.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
@@ -23,7 +26,18 @@ void DaemonController::syncSettings(const config::Settings* settings)
|
||||
if (!settings) return;
|
||||
daemon_->setDebugCategories(settings->getDebugCategories());
|
||||
daemon_->setMaxConnections(settings->getMaxConnections());
|
||||
daemon_->setWalletFile(settings->getActiveWalletFile());
|
||||
|
||||
std::string walletFile = settings->getActiveWalletFile();
|
||||
// The Wallets dialog opens an out-of-datadir wallet by linking it into the datadir under a
|
||||
// "wallet-ip-<hash>.dat" name. If that link went dangling (the external file was moved / a USB was
|
||||
// unplugged between sessions), don't let the daemon create a fresh EMPTY wallet at that name — fall
|
||||
// back to the default this launch. fs::exists follows the link, so it's false for a dangling one.
|
||||
if (walletFile.rfind("wallet-ip-", 0) == 0) {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(util::Platform::getDragonXDataDir() + "/" + walletFile, ec))
|
||||
walletFile = "wallet.dat";
|
||||
}
|
||||
daemon_->setWalletFile(walletFile);
|
||||
}
|
||||
|
||||
bool DaemonController::start(const config::Settings* settings)
|
||||
|
||||
@@ -103,7 +103,7 @@ public:
|
||||
ov.title = TR("wallets_title");
|
||||
ov.p_open = &s_open;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 780.0f;
|
||||
ov.cardWidth = 860.0f; // roomier — makes space for the per-row "open folder" button
|
||||
ov.cardHeight = cardH;
|
||||
ov.idSuffix = "wallets";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
@@ -111,6 +111,15 @@ public:
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
const std::string active = app->settings() ? app->settings()->getActiveWalletFile() : "wallet.dat";
|
||||
// When an external wallet is open in place, the active file is its per-target link name; that row
|
||||
// is the external wallet whose path hashes to it. Precompute per row so the sort stays cheap.
|
||||
const bool linkMode = isLinkName(active);
|
||||
auto rowIsActive = [&](const WalletRow& row) {
|
||||
return linkMode ? (!row.inDatadir && linkNameFor(row.dir + "/" + row.fileName) == active)
|
||||
: (row.inDatadir && row.fileName == active);
|
||||
};
|
||||
std::vector<char> rowActive(s_rows.size(), 0);
|
||||
for (std::size_t j = 0; j < s_rows.size(); ++j) rowActive[j] = rowIsActive(s_rows[j]) ? 1 : 0;
|
||||
|
||||
// ---- Frame-consistent snapshot of the async probe results (badges / counts / sort all read
|
||||
// the same view; one lock instead of one-per-row). ----------------------------------
|
||||
@@ -170,9 +179,8 @@ public:
|
||||
default: sortVal[k] = pr.createdEpoch; break; // 0 = date created
|
||||
}
|
||||
}
|
||||
auto isActive = [&](std::size_t i) { return s_rows[i].inDatadir && s_rows[i].fileName == active; };
|
||||
std::stable_sort(s_order.begin(), s_order.end(), [&](std::size_t a, std::size_t b) {
|
||||
const bool aa = isActive(a), ba = isActive(b);
|
||||
const bool aa = rowActive[a] != 0, ba = rowActive[b] != 0;
|
||||
if (aa != ba) return aa; // the active wallet is always pinned to the top
|
||||
return s_sortDesc ? sortVal[a] > sortVal[b] : sortVal[a] < sortVal[b];
|
||||
});
|
||||
@@ -230,7 +238,7 @@ public:
|
||||
// subdir scan) must NOT borrow a datadir wallet's cached balance/addresses. Only look up
|
||||
// metadata for datadir rows; external rows show size + "never opened" (accurate).
|
||||
const data::WalletIndexEntry* meta = r.inDatadir ? app->walletIndex().find(r.fileName) : nullptr;
|
||||
const bool isCurrent = r.inDatadir && r.fileName == active;
|
||||
const bool isCurrent = rowActive[i] != 0;
|
||||
|
||||
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rMax(rMin.x + rowW, rMin.y + walRowH);
|
||||
@@ -254,10 +262,12 @@ public:
|
||||
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.
|
||||
// Action-button + folder-button slots on the right — reserve room so text truncates first.
|
||||
const float btnW = 96.0f * dp, btnH = ImGui::GetFrameHeight();
|
||||
const float btnX = rMax.x - padX - btnW;
|
||||
const float textR = btnX - Layout::spacingMd();
|
||||
const float btnX = rMax.x - padX - btnW; // Open button / Active chip zone
|
||||
const float folderW = btnH; // circular "open folder" button
|
||||
const float folderX = btnX - folderW - Layout::spacingSm();
|
||||
const float textR = folderX - Layout::spacingMd();
|
||||
|
||||
// Name (+ info icon for external files). Status badges render as a right-aligned vertical
|
||||
// stack of "label icon" rows (e.g. "Seed phrase 🌱" over "Encrypted 🔒") to the left of the
|
||||
@@ -332,25 +342,32 @@ public:
|
||||
dl->AddText(metaFont, metaFont->LegacySize, ImVec2(x, topY + nameFont->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceMedium(), ms.c_str());
|
||||
|
||||
// Action: Active chip (current) / Open / Import
|
||||
// Action: circular "open folder" button, then the Active chip (current) or an Open button.
|
||||
// External wallets Open IN PLACE (link, no copy); datadir wallets switch directly.
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
{
|
||||
ImGui::SetCursorScreenPos(ImVec2(folderX, midY - folderW * 0.5f));
|
||||
IconButtonStyle fs;
|
||||
fs.restBg = WithAlpha(OnSurface(), 20);
|
||||
fs.hoverBg = WithAlpha(OnSurface(), 44);
|
||||
fs.color = OnSurfaceMedium();
|
||||
fs.tooltip = TR("wallets_open_folder");
|
||||
if (IconButton("##walletFolder", ICON_MD_FOLDER_OPEN, Type().iconSmall(), ImVec2(folderW, folderW), fs))
|
||||
util::Platform::openFolder(r.dir);
|
||||
}
|
||||
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"));
|
||||
if (StyledButton(TR("wallets_open"), ImVec2(btnW, btnH))) {
|
||||
if (r.inDatadir) { app->switchToWallet(r.fileName); s_open = false; }
|
||||
else { openInPlace(app, r); }
|
||||
}
|
||||
if (!r.inDatadir && ImGui::IsItemHovered()) Tooltip("%s", TR("wallets_open_inplace_tt"));
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
@@ -375,7 +392,7 @@ public:
|
||||
if (StyledButton(TR("wallets_create"), ImVec2(createBtnW, 0))) {
|
||||
std::string name = normalizeWalletName(s_newName);
|
||||
std::error_code ec;
|
||||
if (name.empty()) {
|
||||
if (name.empty() || isLinkName(name)) { // reserve the in-place-link prefix
|
||||
Notifications::instance().warning(TR("wallets_name_invalid"));
|
||||
} else if (std::filesystem::exists(util::Platform::getDragonXDataDir() + "/" + name, ec)) {
|
||||
Notifications::instance().warning(TR("wallets_exists"));
|
||||
@@ -473,30 +490,53 @@ private:
|
||||
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) {
|
||||
// A reserved bare-filename PREFIX for the datadir links that let out-of-datadir wallets open in place.
|
||||
// Each external wallet gets its own STABLE link name derived from its path — so switching between two
|
||||
// of them is a real -wallet=<different name> switch (not a no-op on one shared name), and the wallet
|
||||
// index tracks each separately (correct per-wallet rescan + cached data). Hidden from the list.
|
||||
static constexpr const char* kLinkPrefix = "wallet-ip-";
|
||||
static bool isLinkName(const std::string& n) { return n.rfind(kLinkPrefix, 0) == 0; }
|
||||
|
||||
// Stable per-target bare link name, e.g. "wallet-ip-1a2b3c4d.dat" (FNV-1a of the absolute path — a
|
||||
// deterministic, cross-platform, cross-run hash, unlike std::hash).
|
||||
static std::string linkNameFor(const std::string& absPath) {
|
||||
std::uint64_t h = 1469598103934665603ULL;
|
||||
for (unsigned char c : absPath) { h ^= c; h *= 1099511628211ULL; }
|
||||
char buf[40];
|
||||
std::snprintf(buf, sizeof(buf), "%s%08x.dat", kLinkPrefix,
|
||||
static_cast<unsigned>(h ^ (h >> 32)));
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Open an out-of-datadir wallet IN PLACE. The daemon only loads a bare filename from its datadir, so
|
||||
// link the real file in under its per-target name and switch to that. Prefer a hard link (no
|
||||
// privileges, same volume — covers most users incl. non-admin Windows), fall back to a symlink
|
||||
// (cross-volume). We NEVER copy (that would fork the wallet), and NEVER delete a real file.
|
||||
static void openInPlace(App* app, const WalletRow& r) {
|
||||
namespace fs = std::filesystem;
|
||||
const std::string base = normalizeWalletName(r.fileName);
|
||||
if (base.empty()) { Notifications::instance().warning(TR("wallets_name_invalid")); return; }
|
||||
std::error_code ec;
|
||||
const std::string datadir = util::Platform::getDragonXDataDir();
|
||||
const std::string datadir = util::Platform::getDragonXDataDir();
|
||||
fs::create_directories(datadir, ec);
|
||||
// The recursive subdir scan can surface several distinct wallets sharing one basename (e.g. a
|
||||
// "wallet.dat" in two backup folders). Auto-suffix the datadir destination to the next free name
|
||||
// instead of hard-failing, so the second one is importable too. Never overwrites an existing file.
|
||||
const std::string stem = base.substr(0, base.size() - 4); // strip ".dat"
|
||||
std::string dest = base;
|
||||
std::string destPath = datadir + "/" + dest;
|
||||
for (int n = 2; n < 1000 && fs::exists(destPath, ec); ++n) {
|
||||
dest = stem + "-" + std::to_string(n) + ".dat";
|
||||
destPath = datadir + "/" + dest;
|
||||
const std::string src = r.dir + "/" + r.fileName;
|
||||
const std::string linkName = linkNameFor(src);
|
||||
const std::string linkPath = datadir + "/" + linkName;
|
||||
|
||||
// Clear only a prior LINK for this same wallet — never a standalone regular file (belt-and-braces;
|
||||
// the reserved prefix already keeps users from creating a real wallet with this name).
|
||||
if (fs::exists(linkPath, ec)) {
|
||||
if (fs::is_symlink(linkPath, ec) || fs::hard_link_count(linkPath, ec) > 1) {
|
||||
fs::remove(linkPath, ec);
|
||||
} else {
|
||||
app->switchToWallet(linkName); s_open = false; return; // already a real file at our name — reuse, don't touch
|
||||
}
|
||||
}
|
||||
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);
|
||||
const std::string target = [&]{ auto c = fs::weakly_canonical(src, ec); return c.empty() ? src : c.string(); }(); // resolve a symlinked source
|
||||
std::error_code lec;
|
||||
fs::create_hard_link(target, linkPath, lec); // 1) hard link — no privileges, same volume
|
||||
if (lec) { lec.clear(); fs::create_symlink(target, linkPath, lec); } // 2) symlink — cross-volume (needs privilege on Windows)
|
||||
if (lec) { Notifications::instance().error(TR("wallets_open_failed")); return; } // no copy fallback (would fork the wallet)
|
||||
|
||||
app->switchToWallet(linkName); // distinct per wallet → the index tracks rescan/cache correctly
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
@@ -531,7 +571,7 @@ private:
|
||||
// neither .dat nor wallets, so reject them before paying for a stat().
|
||||
const std::string name = p.filename().string();
|
||||
if (name.size() <= 4 || name.substr(name.size() - 4) != ".dat") return;
|
||||
if (inDatadir) { if (name.rfind("wallet", 0) != 0) return; } // datadir: wallet-prefixed only
|
||||
if (inDatadir) { if (name.rfind("wallet", 0) != 0 || isLinkName(name)) return; } // datadir: wallet-prefixed, hide in-place links
|
||||
else { if (isNodeArtifact(name)) return; } // external: reject node junk
|
||||
std::error_code fec;
|
||||
if (!fs::is_regular_file(p, fec)) return;
|
||||
|
||||
@@ -281,20 +281,18 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["wallets_badge_legacy_short"] = "Legacy";
|
||||
strings_["wallets_badge_unknown_short"] = "Unknown";
|
||||
strings_["wallets_open"] = "Open";
|
||||
strings_["wallets_open_folder"] = "Open folder location";
|
||||
strings_["wallets_open_inplace_tt"] = "Open this wallet where it is — linked into the data directory (no copy)";
|
||||
strings_["wallets_open_failed"] = "Couldn't open this wallet in place. It's likely on a different drive than your data directory — move it onto the same drive (on Windows, enabling Developer Mode also lets it link across drives).";
|
||||
strings_["wallets_never"] = "Never opened";
|
||||
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_external_tt"] = "Outside your data directory \xE2\x80\x94 Open links it in place (no copy).";
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user