GajuDesk/src/gd_v_netman.erl
Craig Everett f92c5fbde0 First-run wallet creatorator (v0.7.0) (#24)
Adds a first-run wallet and account creator option for noobs.
If they select the "create a default wallet" option, they are jumped to the name + password screen for wallet creation, and a new account is generated for them named "Account 1".

This leapfrogs the problem of users having to know what is going on with the blockchain and wallet at all before getting started.
#18
#19

Reviewed-on: #24
Reviewed-by: Ulf Wiger <ulfwiger@qpq.swiss>
Co-authored-by: Craig Everett <zxq9@zxq9.com>
Co-committed-by: Craig Everett <zxq9@zxq9.com>
2025-08-07 15:47:05 +09:00

332 lines
9.9 KiB
Erlang

-module(gd_v_netman).
-vsn("0.7.0").
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later").
-behavior(wx_object).
%-behavior(gd_v).
-include_lib("wx/include/wx.hrl").
-export([to_front/1]).
-export([set_manifest/1]).
-export([start_link/1]).
-export([init/1, terminate/2, code_change/3,
handle_call/3, handle_cast/2, handle_info/2, handle_event/2]).
-include("$zx_include/zx_logger.hrl").
-include("gd.hrl").
-record(w,
{name = none :: atom(),
id = 0 :: integer(),
wx = none :: none | wx:wx_object()}).
-record(s,
{wx = none :: none | wx:wx_object(),
frame = none :: none | wx:wx_object(),
lang = en :: en | jp,
j = none :: none | fun(),
prefs = #{} :: map(),
buttons = [] :: [#w{}],
nets = [] :: [#net{}],
book = #w{} :: #w{}}).
%%% Interface
-spec to_front(Win) -> ok
when Win :: wx:wx_object().
to_front(Win) ->
wx_object:cast(Win, to_front).
-spec set_manifest(Entries) -> ok
when Entries :: [ael:conf_meta()].
set_manifest(Entries) ->
case is_pid(whereis(?MODULE)) of
true -> wx_object:cast(?MODULE, {set_manifest, Entries});
false -> ok
end.
%%% Startup Functions
start_link(Args) ->
wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []).
init({Prefs, Manifest}) ->
Lang = maps:get(lang, Prefs, en_us),
Trans = gd_jt:read_translations(?MODULE),
J = gd_jt:j(Lang, Trans),
Wx = wx:new(),
Frame = wxFrame:new(Wx, ?wxID_ANY, J("Networks")),
MainSz = wxBoxSizer:new(?wxVERTICAL),
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
ButtonTemplates =
[{node, J("Add Node")},
{drop, J("Delete")}],
MakeButton =
fun({Name, Label}) ->
B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]),
#w{name = Name, id = wxButton:getId(B), wx = B}
end,
Buttons = lists:map(MakeButton, ButtonTemplates),
% FIXME
Disable = fun(#w{wx = B}) -> wxButton:disable(B) end,
ok = lists:foreach(Disable, Buttons),
AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end,
Notebook = wxNotebook:new(Frame, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]),
ok = add_pages(Notebook, J, Manifest),
ok = lists:foreach(AddButton, Buttons),
_ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)),
_ = wxSizer:add(MainSz, Notebook, zxw:flags(wide)),
_ = wxFrame:setSizer(Frame, MainSz),
_ = wxSizer:layout(MainSz),
ok = gd_v:safe_size(Frame, Prefs),
ok = wxFrame:connect(Frame, close_window),
ok = wxFrame:connect(Frame, command_button_clicked),
ok = wxFrame:center(Frame),
true = wxFrame:show(Frame),
State =
#s{wx = Wx, frame = Frame,
j = J, prefs = Prefs,
buttons = Buttons, nets = Manifest, book = Notebook},
{Frame, State}.
add_pages(Notebook, J, [#net{id = ID, chains = Chains} | Rest]) ->
Page = wxScrolledWindow:new(Notebook),
PageSz = wxBoxSizer:new(?wxVERTICAL),
ChainPanels = draw_chain_panels(Page, J, Chains),
AddPanel = fun(Panel) -> wxSizer:add(PageSz, Panel, zxw:flags(wide)) end,
ok = lists:foreach(AddPanel, ChainPanels),
ok = wxWindow:setSizer(Page, PageSz),
ok = wxSizer:layout(PageSz),
true = wxNotebook:addPage(Notebook, Page, ID, []),
add_pages(Notebook, J, Rest);
add_pages(_, _, []) ->
ok.
draw_chain_panels(Page, J, [#chain{id = ID, coins = Coins, nodes = Nodes} | Rest]) ->
Sizer = wxStaticBoxSizer:new(?wxVERTICAL, Page, [{label, ID}]),
CurrencySizer = wxBoxSizer:new(?wxHORIZONTAL),
CurrencyLabel = wxStaticText:new(Page, ?wxID_ANY, J("Currencies: ")),
Currencies = wxStaticText:new(Page, ?wxID_ANY, string:join(Coins, " ")),
_ = wxSizer:add(CurrencySizer, CurrencyLabel, zxw:flags(base)),
_ = wxSizer:add(CurrencySizer, Currencies, zxw:flags(base)),
List = wxListCtrl:new(Page, [{style, ?wxLC_REPORT bor ?wxLC_SINGLE_SEL}]),
Labels =
[J("Address"),
J("External"),
J("Internal"),
J("Rosetta"),
J("Channel"),
J("Middleware")],
Columns = indexify(Labels),
AddColumn = fun({I, L}) -> wxListCtrl:insertColumn(List, I, L, []) end,
ok = lists:foreach(AddColumn, Columns),
IndexedNodes = indexify(Nodes),
AddNodes =
fun({Index,
#node{ip = IP,
external = E, internal = I, rosetta = R, channel = C, mdw = M}}) ->
Address =
case is_list(IP) of
false -> inet:ntoa(IP);
true -> IP
end,
Elements = indexify([Address | stringify_ports([E, I, R, C, M])]),
_ = wxListCtrl:insertItem(List, Index, ""),
AddText = fun({K, L}) -> wxListCtrl:setItem(List, Index, K, L) end,
lists:foreach(AddText, Elements)
end,
ok = lists:foreach(AddNodes, IndexedNodes),
_ = wxListCtrl:setColumnWidth(List, 0, ?wxLIST_AUTOSIZE),
_ = wxSizer:add(Sizer, CurrencySizer, zxw:flags(base)),
_ = wxSizer:add(Sizer, List, zxw:flags(wide)),
[Sizer | draw_chain_panels(Page, J, Rest)];
draw_chain_panels(_, _, []) ->
[].
indexify(List) ->
lists:zip(lists:seq(0, length(List) -1), List).
stringify_ports([none | T]) -> ["" | stringify_ports(T)];
stringify_ports([Port | T]) -> [integer_to_list(Port) | stringify_ports(T)];
stringify_ports([]) -> [].
%%% OTP callbacks
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
{noreply, State}.
handle_cast(to_front, State = #s{frame = Frame}) ->
ok = wxFrame:raise(Frame),
{noreply, State};
handle_cast(Unexpected, State) ->
ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]),
{noreply, State}.
handle_info(Unexpected, State) ->
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]),
{noreply, State}.
handle_event(E = #wx{event = #wxCommand{type = command_button_clicked},
id = ID},
State = #s{buttons = Buttons}) ->
NewState =
case lists:keyfind(ID, #w.id, Buttons) of
#w{name = node} -> add_node(State);
#w{name = drop} -> drop_network(State);
false ->
tell("Received message: ~w", [E]),
State
end,
{noreply, NewState};
handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame, prefs = Prefs}) ->
Geometry =
case wxTopLevelWindow:isMaximized(Frame) of
true ->
max;
false ->
{X, Y} = wxWindow:getPosition(Frame),
{W, H} = wxWindow:getSize(Frame),
{X, Y, W, H}
end,
NewPrefs = maps:put(geometry, Geometry, Prefs),
ok = gd_con:save(?MODULE, NewPrefs),
ok = wxWindow:destroy(Frame),
{noreply, State};
handle_event(Event, State) ->
ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]),
{noreply, State}.
code_change(_, State, _) ->
{ok, State}.
terminate(Reason, State) ->
ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]),
wx:destroy().
%%% Doers
add_node(State = #s{frame = Frame, j = J}) ->
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Add Node")),
Sizer = wxBoxSizer:new(?wxVERTICAL),
AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address")}]),
AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
_ = wxSizer:add(AddressSz, AddressTx, zxw:flags(wide)),
PortSz = wxBoxSizer:new(?wxHORIZONTAL),
Labels =
[J("External"),
J("Internal"),
J("Rosetta"),
J("Channel"),
J("Middleware")],
MakePortCtrl =
fun(L) ->
Sz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, L}]),
Tx = wxTextCtrl:new(Dialog, ?wxID_ANY),
_ = wxSizer:add(Sz, Tx, zxw:flags(wide)),
_ = wxSizer:add(PortSz, Sz, zxw:flags(wide)),
Tx
end,
PortCtrls = lists:map(MakePortCtrl, Labels),
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
Affirm = wxButton:new(Dialog, ?wxID_OK),
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
_ = wxSizer:add(Sizer, AddressSz, zxw:flags(base)),
_ = wxSizer:add(Sizer, PortSz, zxw:flags(base)),
_ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxFrame:setSize(Dialog, {500, 200}),
ok = wxFrame:center(Dialog),
ok = wxStyledTextCtrl:setFocus(AddressTx),
ok =
case wxDialog:showModal(Dialog) of
?wxID_OK ->
case wxTextCtrl:getValue(AddressTx) of
"" -> ok;
Address -> add_node2(Address, PortCtrls)
end;
?wxID_CANCEL ->
ok
end,
ok = wxDialog:destroy(Dialog),
State.
add_node2(Address, PortCtrls) ->
IP =
case inet:parse_address(Address) of
{ok, N} -> N;
{error, einval} -> Address
end,
[E, I, R, C, M] = lists:map(fun numerify_port/1, PortCtrls),
New = #node{ip = IP, external = E, internal = I, rosetta = R, channel = C, mdw = M},
gd_con:add_node(New).
numerify_port(Ctrl) ->
case wxTextCtrl:getValue(Ctrl) of
"" -> none;
String ->
case s_to_i(String) of
{ok, PortNum} -> PortNum;
error -> none
end
end.
s_to_i(S) ->
try
I = list_to_integer(S),
{ok, I}
catch error:badarg ->
error
end.
drop_network(State = #s{nets = Nets, book = Book}) ->
ok =
case wxNotebook:getSelection(Book) of
?wxNOT_FOUND ->
ok;
Index ->
#net{id = ID} = lists:nth(Index + 1, Nets),
gd_con:drop_network(ID)
end,
State.