From b7da6335d17d4c8a01a8a228ed32269d0e788430 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 11 Jul 2026 21:21:30 -0500 Subject: [PATCH] feat(wallets): open external wallets in place via datadir link (no copy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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-.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) --- res/fonts/NotoSansCJK-Subset.ttf | Bin 606516 -> 607164 bytes res/lang/de.json | 10 +-- res/lang/es.json | 10 +-- res/lang/fr.json | 10 +-- res/lang/ja.json | 10 +-- res/lang/ko.json | 10 +-- res/lang/pt.json | 10 +-- res/lang/ru.json | 10 +-- res/lang/zh.json | 10 +-- scripts/add_missing_translations.py | 36 +++++++++ src/daemon/daemon_controller.cpp | 16 +++- src/ui/windows/wallets_dialog.h | 118 +++++++++++++++++++--------- src/util/i18n.cpp | 10 +-- 13 files changed, 166 insertions(+), 94 deletions(-) diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index 600abc43fa4309d49f1a360e051b45d65db31c2f..03f827770be48156bc7662a914887fbe4a795434 100644 GIT binary patch delta 13148 zcmbuF2Urx>yZ_(WWlkBI1r)Ih_6i7yjRh$J0t$$Ty_X<@B5I;;?8e^WMuR4?B{3$F zSWsh&EmkbCV2K5TC18m$npoifom~*~yZ8S*_qi9I4?BD2J#Rl}W{2H2t^S_b^%qw$ z->5)DH^RHd##RPS2#>{GIayr*EvSAdS|@%%!Gm0euZv$pQ7t01)~OqO&xrGPe14O zf01Imj}daXp1sglKe#(#Q@>?(6o39TQCHHYZ0)P;a#QO=s^L}_cl!$V?nKf-5NV3T z)+SBrtIGYYF)baXa(`J&%i3EfwmD_2IOXQ;8<|%|oOL*Be^#EgyV7=RVtb>ayuU2D zLrW!g>)Z~CK^e7mexR?SR9<$_w9edOLZk7I$Jflpi zGhiDwg!B`!>ympCVJYT~5C@rfCU>4_r}hb2ZOHcoWzzNh=T?mh`$C2UAoo3J7w zBOy7VM?&|6=!D3GkoZ61^WrbZ&y1fEpBX$JjNolVT^v4vg&_8yxE&>l*7E%VUnjY>HVF zV~(jC;~v9f>|&JYm(drZ&qtq$J{El>dVloZ=snRtMt>FEJvuJBU39bPy3w_wouVDP zJnT}~<<~Alx(tlE7PUTVb<~om#Zl(@QS+kaM$L(u6qOm}8C5sRHOf9pM5&RFA`2t$ zMBa+r7r7^LSEOI0Ph_jeIuYMRd>JtzB0XYQM89yC@Jiv%;XK?ftT?PN?0ndFVVlFg z4qF?xChU{2iD3i7!oxztg2Vhm4}|Uu-5S~^)GM@cs4-LtDGvTK_(|}?;Jd-=f#GJ^&N^$zM86cf}XC?qH-sAEvmpz1*tgA~&X({ROv_D6Omj`MOdpzBn3|ayo6Pl0wN2FmO9G1mj|cu7m=pMYU~phyVEe#U z9d~!!*72*3vpUY`IJM*Z9n(52X&>CaQTuA`UE5b^x3}HycAMI*Z`Z9|hqfhckF`D6 zc1PO@ZDm_Sz^i~K0T%<#2OJHU5imJmWI$|yTYzgor8cwM*tJpoi~Y~|@Au#CztMlK zf0qA3|1PQi9sIrhJ^gO`o%ZYDd&>8??@`}bzSDgt`wndVXX^{C&$T|%dVlNnt=F`+ zv~JVd)n}>CJfGe^-Fp-rc?9y}NqX^=4j$ zUe~=Qdo|IP$}8A7uFwfmmVF;u%OD=#}a4RYC-|upEubN!Wl zuA4Bhg;q;!3PwKE$MsiC>|$Vk%eQ}TU>R&Y+s1aV-E1#A%zkE9*fqIATEt)Cxp*Nz+1etukDGZlsl!i^IwGIckvBK6@3UF$~bxNnq$`YoLQt~r_2Er>3AGO6E> zA@zq0Qnzjkg@X%NWI`m>c#zLQZH{J^$I+@ zij-g9PU?+?q~1gdZ~K#)e~{EWQ2yJ`q!#3odN+^Mdj+IEs6*=SP*~WB)S{)NJ~RS2 z^eCCs$B6zhvh~M%q&`Kto@J5x9LFyZ%-{7%{Rf6$jwSWgQ&QiUtCCtAMrug_G0G!m z2n2XFP9|no1r!rg@t(~l#>=j-GqH*|t`tnH z@Stw*fcSz^sE5^HgoSj%0+TKz!G8wz}$5c7o*zstnh)F&3uf>_&7V(r!tYrlY4 zhl9idv3df3A!dS;K{LU9V!;{U8JSr~F0ruRh=qSgtTQG=d_^qsDzPrU#G+Rbi$xZ? z<`C-^O)O3YP!#_=u>?e&s1WPXh*;8EVm+S`>$RO&Z_Mor=ljEn{?I!B1_%B_Z14zT zLlzMmdW+bwd&Gvf1hdWflTU0!2>6EB$Ov$W*n1to5&&f>NLfl2C?Pg#3V;Kt9|AZ& z8qtpaoLE{EfU$JUOP>WGI|lQ|ydgGr62NOlSFj8`B{pshI7uwitl>{Q_?Fmsq-lII z_yi!r32A+h%%`~C@HA0z;jVA4eJfY{`LU=y(^2Jjv@joQWm%>Ph; z_W^WHT}f=37x;kK^y)Z2Jsk(@iOoPNXUqZn0SwJ-44`~w0kK&gU>rD1Y_=zu51td7 zGZ>)$AB6)rJ+}gY@wr!s&8q}PgOkMOcLFfH0D&w(<`!V?!m(fr>c0q+76k*OZP9gN zi{ahk@!)S_OOU!HaAxTsfHEx$14#FB4&dB!G~LIM;1IDD(6Qn+F$+p$fdij}fOW)H zRsnE+CHiI65MrNtfS-tEG0+?J&q7qIn*uc7YD`>H7t95y{bwk_XS2a2Tp8NvR|kMb z`@#g)5?gBr;OyGph^(Sloj}Y5{a~txAZLA0S0W|r>7sS5o4KV*pGot(o zQGV?RqQPh2F|kb}z-EBP`35WDn-p-I*k%~se3aO?4gf~J#n^YA;3HyN>;d{`%TDl$ zSauYEj%*x%kMZwM5c{D4SWav!0hHUk^%Svf@NOH@xhl}1?07^h zr#Zm#*@?9690`sQ+l5l?T1{-XJNTH`kIlehVtWh#(e3RBRuS8WIs4G@`_L=n)?;8GqnL;&zitKV&_84_=6OmM>n2-N$i)I z#4dypy9g&PB9cpJo=b4nyN^5`yx(X+*W)r)H@?1mat`7r9^{<~1yD^{G z&01gwv0IS4g$&;A4{j67N91=@fDHX+jwM!L1Y3yRO(S*>4RvofvHLJ`|4U-H(69$k z_&b#SevnupoGV0qi&}wg#2zBWk5IBlnE$v9D8W4r9C?C>|3E+eah}*y*m(Mc*q{Bt zWn#~Q!FFPQVV?P~?}^Xtvi_1#cVy zUW*Z3aTc)>XRw?&`GOV16(dLkkBJ+Gfdb;jX~gXsf%C-G3~+*J%rKh}R7U&{b~$FmEC5VGlk4`NTc_!3I!7 zyna0RmUsh1(_j;ET#|XiYs4F+6K{+;jejEEB$jwnAMliTGw5oL!(+yz@BX5p4jRidYPOCLRf=B0Ga= z0JV zdV_Vu<2V=sJ_Aq|_ndfqA21JW0}qKOpotTZ!G!hTcjDasd2N(geVjeIR2Y(SCh_nve0rq$mq@T#X5zwIKfa0^(mxAinl9;_GsWuSWxI*iC#Rj=zL6U;RUTlOOSKY7^hw zj`+7|v@KAWJ(l?QD~SKFpZGSUdwVGH9DCwBeFU0qo z+u~0GNCimg{>S)G+Y+G82k`#D9pXQA1c>S&vT|@XC?pS9{|5Blgsz)# z_I6{CM?4?dxr5BznMwRNWaYOFsDA;LK|wz8yK9KwLw)WKApT$n@!xL{FT|!&^eOR& zFNr@MMEnVqKY{EY=z*uJi2u2f_%lTNR|kNIpZ`ew#Z%({BoTjkocOD)#9!|r{szX2 zFA^_VO@cB>D6u3A;UtXFW)gM^_>qJf4{niQ=Sc7qB*ZupvJ(ldE(!Yp5)RWyI6~gZ z2mC?8`3Q*$Yrs7c6@3AoNLR84Cjrh^{)mK2O^^=ulW^@vqKW``U*#H!swpJg%p8A? zlBl+ig!>#4xTuI4P*MY9HKRz>as;DE)W&P=Y!Y>FzODui;bBR45+1d|UJ{;5KrxB> z>q#`gyoOBxf@%b(ajg)IKOxa18T?J6=@}BunuB8`%+29>^PMDGU{XsJbOFanw32!B-%TJbtF3A zIeUk*BswAk9U&Vyg@nlg{6JB?Zfkj{siD+atx;a3dV>XkB9ZaHY7Kv`xNyOt^0s>EX z9YP|pDL_4XAkrkHv1c`a)FnevFVK4>i9UTv^o3ksIMZ(viT>kB4DbXnH1PFw&xx}= zCn~@IjKD6(bE29uG)SfTCE3PYV+0w=o|;fB4I!%R>f@_=FvfivG-z0r@w%=aRjO3Q z`vwhN>-y@i4I29S`Fr^K_!%v!FO-_xgIfnIrmJcdv*kPGBU8oZHJ`b+7#blfDpfbu z>F?`UQwI1Hzj6<3z-u~$^soKtE|(mosQ5@#rJ-VQs$JEwZq>@28Ym93K__RY3f#%f zzOX{5Q!R%P#VKALL#8R-qi4;sh+(I?8h+ijExbX5N8QM)!rM2b!^oOiJtxnOzP`2m z-5l$Q#-q&bLjsEXbc}WrndvT$_6mL~)OWHo7@1?&ZY>=>`Zh0qZm;w#-cdoZD6NZ6 z%?;Y)X{zq)LMk;Z`4+iohg@jLg^32elZ(1~D*ivYu%^OK@vrDp(Z8Z4{Hp4#D0*V* znU{X8EHG7RT}v^x>D!4{wpZ*odJOe-s4W896hCusTgR@NHacqlHm9m@)_Jw>{E^b6 z-|*oxlYH*vu)sc+GK^^?uo4YtPb?E3fJlH+pjiZB-SiGyUvP|(V&Yd5;%`>=$ zUlnCx(BP_KZmIyEr*3kempO;7h=oRFfF&RTC$5TX;+FVRJd?h1iCij|%a1Lae-#ZY=ZgYyPgaqBa+&^g zQGOyn&AAIr2DP9hOWd=#-xB?*7nB?p_e$^piyTXe$O$Yf$raX7%ko>IF=X>0d+?U< z#UW%ZuWyNJLokXB(-D8WVM;n&8UyB1Em~1hgz33Znoo9+*bSw+LRf?uW;HVybB2J_ zlHG4*RT>NpLqMrEQDAxhwwMa5dE(xRe9;_7P;2qd7aqNA7K+d?AHmKiaI66ObnW@D zmygk+(ovfy21IlIPTmTuO!N z+!3KR*DNFNh@Pds9lIlZ&1KOGN`e1W=i`FZIKT&=CmfcBsd;pmsDWoUtDSyk^cy)pFy6wzpFV2IqZxj+Od z1(rtz;!`AE@8yMe#fZ`xyuK@{b+Wkvr*soAXftLjjJuOT@?Cg!hO*+sH`k{T-k7WHyuRJT!!lptWM}F z4fh+RQ5R7j3||3P%N(&BxNmK9eZ@V!FFIj$LOJrIC*d$O9)>o(VRVyS9|+gdkVifc zHTAZ$>el6QbvfNC6_x1uwgss#k6fJ1r)->gs|m+OoOzBjx~$Eb?!Q%oJ^~-!U0kKQ zaYnCvk!>Teu-~oz=qsoW4$%R&rqy*@cNX0bYZutciM(LMrdRh$_c<3n>+xknNyhJ@ zk6M75-?Lo$U3mFI)@n`9g{@SZt+M~=9Y*PqRX5I9+82uIHEpiw#%~~nH^D9N6y_A{ zG8u)|ZBEbW+CtF{U1w9N?-Y76^_@bmh*l(ms^KJ-r|zkKTF;Wc_2`xc7YVP@daNoE zzbGp$sSia%^GdlA>DB*nZzKn5i<$7%(q&57$w90eEu563f25CcV=agsGbB^MWI3`YrQzBQK73akTaY^LKEIavy zT$khaNK{qKo8&h{@>}^W8RQnZg^cnCd7SLzNqLrP$n)|h)sqGCF13>PEX!!ugL@mLr^`H;5zFJ>=U&2o8L5cPvar;VkYXkLkVk8$MhcS}J`>=YI~;_SMQzj4C}7t$v>&k7>=(X109k!a%Yov?Y)Twx9?E^$nR2l`Y z45bB`1Gy|M#M()=gzIjXIb+L%oDm5S$u&n)-LZyCI zkNL3Hxip?K1$q_rgyxq@=-qE~NuO`q3A{>OdN*P{pmMn^ef=-hSgG>=tf(!0`U)wl zX!%rILcOfZuDl#&a<<+tf2SY=xEn6bm~{ofkz6t>xO`}DPn7Lf#&)48n_MQd z(OQ7=-7?V{ZN6<^!>scC^KI)|P3oGA1%f^F&QGUe{-X7uy=3_S2Cjog+I4h#B~!l%Ssq_xIDnF@Uo>Kle_Oa9M@ zZ5O5T4a@nR`j%aSY@3#D?(OQAr?Fe)!ioxc2GFY3n;(tbEo)xjY1JOv9S`nraCeh) z;)O69&4Y4U+AqUrJx z?ucgNj_3s~kgwz$T8#TNg_hy&NYKZcra90m%}H~j)mjCu0`U!*1T{#^*wH<+RzTIt=55Zv>+{*e#EU+2<_Fv zw9d3oi_{|NC#?%^unyt|E0KrkYr8 zy%9kT>&W_YtK21j#Led4nyXe<^V7O&y|q52!hc(ZD_S}ii~7~K%e|Vj)=>-8OxDx* zJc(C>yd!_J%qbSloqv^@^w276RkfO0ZOtR+aIr8NF_^hmHQ55NBzZUcC zWIKE$Tx;=EWJCLP)&Ua+)>#tu0h>gqRbq}JLmS&9qIC(YY>a*iFmlo@$ocqP1ZU=3 z@)g-mnQy6OkhP=nv>$i-`dvQqOXQ5l%TMi|5C0xUQpBH@;i|0JveX)e z^oLz|(1o4goeUxsdPA&|pOV2?L78dEwU-@po~kn5V0LA0%#$@_O<4=(#agotEQp1$ za2Cm8ST~lydaz{Hj}2sl*>LtA8_mYDO!hvT%syiC*aEhaZDgC+w`>dB%5vB)wuc>H zhuBeejGbi{*i}}@9<%F)Ug`{Gr=gi$@3+qBl~I(Lie1h@Ar~3URaiAvpEY95nEq8j zU)GTYmwq8Imc_B|EXn%Cz(Llp299Fs)~^SCz^1UdY(87aRjjs&uw0>pq zC+n96kF#^^BD=i8%rza#Zu$zpN3DRHLqx_^CR*ovi zlvA?1a$dQhT$YK-E#)`aLwT(HBa;k912YJNv!SBF)lk*oZm40XC3{-BIAC%wOA3>X zTx4&sr`ZX9VGW1MK5WSnW7EeBctX0of0L+t9?HIhRusgAO$GR!i|QFf4M%hZq6m14K0m6NP$PE!x6ht;DpU5-(6)wAk(^@1F$UQ=(# z4E2p1#~fG%R*9j7nhzW4Jyg^czoj76|6tSgIWiL58tv^ytK{$Rfvdh6G3b!OS}%E_%P$0@_={STIu{$1Ph1@$4DhJ~~ZOGsZp z$Jt4iizW2XwtTG5%Da{=9x^D)>1X}!!LrF!R!7iBU1d+DzokwU*+WjYJ8E~r?yMYP z`LK%Yj(A=E6|p}v{g;UQBI03hBcAw9#JjBVo-T{GELyuhZ{uxURbP4kmw5jjZBA*d cZ?_7|Q8!s%`8ub_P4;qAd~-}*@|pAh1N|lU1ONa4 delta 12521 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 diff --git a/res/lang/de.json b/res/lang/de.json index c04b261..95ca511 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -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)", diff --git a/res/lang/es.json b/res/lang/es.json index 5bca138..2350c49 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -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)", diff --git a/res/lang/fr.json b/res/lang/fr.json index f3b9d13..c088f6e 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -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)", diff --git a/res/lang/ja.json b/res/lang/ja.json index 1b70563..6ffc0ee 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -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": "昇順(古い/少ない/小さい順)", diff --git a/res/lang/ko.json b/res/lang/ko.json index 0f7848b..5f87a6d 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -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": "오름차순 (오래된/적은/작은 순)", diff --git a/res/lang/pt.json b/res/lang/pt.json index 7d2ad74..49ce80d 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -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)", diff --git a/res/lang/ru.json b/res/lang/ru.json index 2c1e5a3..1948d8d 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -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": "По возрастанию (сначала старые / меньше / меньший)", diff --git a/res/lang/zh.json b/res/lang/zh.json index 8c40624..0899d5a 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -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": "升序(最早/最少/最小优先)", diff --git a/scripts/add_missing_translations.py b/scripts/add_missing_translations.py index ea15147..d2990e6 100644 --- a/scripts/add_missing_translations.py +++ b/scripts/add_missing_translations.py @@ -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(): diff --git a/src/daemon/daemon_controller.cpp b/src/daemon/daemon_controller.cpp index 1b82d36..1ebd8de 100644 --- a/src/daemon/daemon_controller.cpp +++ b/src/daemon/daemon_controller.cpp @@ -1,7 +1,10 @@ #include "daemon_controller.h" #include "../config/settings.h" +#include "../util/platform.h" #include +#include +#include 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-.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) diff --git a/src/ui/windows/wallets_dialog.h b/src/ui/windows/wallets_dialog.h index ab55999..d943543 100644 --- a/src/ui/windows/wallets_dialog.h +++ b/src/ui/windows/wallets_dialog.h @@ -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 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(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= 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(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; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index e79a50d..ac1bbf6 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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)