From fc9bbb1d6b20b2312fe44b3a80c86b8a2930e92f Mon Sep 17 00:00:00 2001 From: GabrielTofvesson Date: Mon, 14 May 2018 22:43:03 +0200 Subject: [PATCH] * Partially reworked key event system * Reworked padding rendering (now handled natively by View) * Fixed how ConsoleController renders dirty views * Explicitly added padding to the LayoutMeta dimensions computation * Added support for updating passwords in SessionContext * Completed account display system * Added many more resources * Simplified internationalization * Added clientside representations for accounts and transations * MOAR COMMENTS! * Optimized account serialization * Corrected issue where copying a user simply copied references to the user accounts; not actually copying accounts (which caused jank) * Fixed timestamp for TimeStampWriter * Probably some other minor things --- Client/Account.cs | 52 +++ Client/BankNetInteractor.cs | 24 +- Client/Client.csproj | 3 + Client/ConsoleForms/ConsoleController.cs | 5 +- Client/ConsoleForms/Context.cs | 6 + Client/ConsoleForms/ContextManager.cs | 2 +- Client/ConsoleForms/Graphics/ButtonView.cs | 1 + Client/ConsoleForms/Graphics/DialogView.cs | 19 +- Client/ConsoleForms/Graphics/InputView.cs | 368 +++++++++++---------- Client/ConsoleForms/Graphics/ListView.cs | 69 +++- Client/ConsoleForms/Graphics/TextView.cs | 30 +- Client/ConsoleForms/Graphics/View.cs | 59 +++- Client/ConsoleForms/LangManager.cs | 2 +- Client/ConsoleForms/LayoutMeta.cs | 4 +- Client/Context/NetContext.cs | 9 +- Client/Context/SessionContext.cs | 227 +++++++++++-- Client/ParseException.cs | 28 ++ Client/Program.cs | 2 +- Client/Promise.cs | 2 + Client/Resources/Layout/Common.xml | 2 +- Client/Resources/Layout/Session.xml | 51 ++- Client/Resources/Strings/en_GB/strings.xml | 15 +- Client/Resources/Strings/en_US/strings.xml | 13 +- Client/Resources/Strings/sv_SE/strings.xml | 13 +- Client/Transaction.cs | 43 +++ Common/Common.csproj | 1 + Common/FixedQueue.cs | 70 ++++ Common/NetServer.cs | 7 +- Common/Streams.cs | 3 +- Server/Database.cs | 4 +- Server/Program.cs | 52 ++- 31 files changed, 871 insertions(+), 315 deletions(-) create mode 100644 Client/Account.cs create mode 100644 Client/ParseException.cs create mode 100644 Client/Transaction.cs create mode 100644 Common/FixedQueue.cs diff --git a/Client/Account.cs b/Client/Account.cs new file mode 100644 index 0000000..892d9b9 --- /dev/null +++ b/Client/Account.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Client +{ + public class Account + { + public decimal balance; + string owner; + public List History { get; } + public Account(decimal balance) + { + History = new List(); + this.balance = balance; + } + public Account(Account copy) : this(copy.balance) + => History.AddRange(copy.History); + public Account AddTransaction(Transaction tx) + { + History.Add(tx); + return this; + } + + public static Account Parse(string s) + { + var data = s.Split('{'); + if(!decimal.TryParse(data[0], out var balance)) + throw new ParseException("String did not represent a valid account"); + Account a = new Account(balance); + for (int i = 1; i < data.Length; ++i) + a.AddTransaction(Transaction.Parse(data[i])); + return a; + } + + public static bool TryParse(string s, out Account account) + { + try + { + account = Account.Parse(s); + return true; + } + catch + { + account = null; + return false; + } + } + } +} diff --git a/Client/BankNetInteractor.cs b/Client/BankNetInteractor.cs index 912d8c3..a8ce72d 100644 --- a/Client/BankNetInteractor.cs +++ b/Client/BankNetInteractor.cs @@ -26,7 +26,7 @@ namespace Client { get { - if (loginTimeout >= DateTime.Now.Ticks) loginTimeout = -1; + if (loginTimeout <= DateTime.Now.Ticks) loginTimeout = -1; return loginTimeout != -1; } } @@ -131,7 +131,7 @@ namespace Client bool b = !p.Value.StartsWith("ERROR"); if (b) // Set proper state before notifying listener { - loginTimeout = 280 * TimeSpan.TicksPerSecond; + RefreshTimeout(); sessionID = p.Value; } PostPromise(p.handler, b); @@ -147,10 +147,7 @@ namespace Client { bool noerror = !p.Value.StartsWith("ERROR"); if (noerror) // Set proper state before notifying listener - { - loginTimeout = 280 * TimeSpan.TicksPerSecond; - sessionID = p.Value; - } + RefreshTimeout(); PostPromise(p.handler, noerror); return false; }); @@ -203,7 +200,12 @@ namespace Client { await StatusCheck(true); client.Send(CreateCommandMessage("Account_Create", DataSet(sessionID, accountName), out long PID)); - return RegisterEventPromise(PID, RefreshSession); + return RegisterEventPromise(PID, p => + { + RefreshSession(p); + PostPromise(p.handler, !p.Value.StartsWith("ERROR")); + return false; + }); } public async virtual Task CheckIdentity(RSA check, ushort nonce) @@ -249,7 +251,7 @@ namespace Client bool b = !p.Value.StartsWith("ERROR"); if (b) // Set proper state before notifying listener { - loginTimeout = 280 * TimeSpan.TicksPerSecond; + RefreshTimeout(); sessionID = p.Value; } PostPromise(p.handler, b); @@ -350,6 +352,12 @@ namespace Client protected void RefreshTimeout() => loginTimeout = 280 * TimeSpan.TicksPerSecond + DateTime.Now.Ticks; protected string CreateCommandMessage(string command, string message, out long promiseID) => command + ":" + (promiseID = GetNewPromiseUID()) + ":" + message; protected static string DataSet(params dynamic[] data) + { + string[] data1 = new string[data.Length]; + for (int i = 0; i < data.Length; ++i) data1[i] = data[i] == null ? "null" : data[i].ToString(); + return DataSet(data1); + } + protected static string DataSet(params string[] data) { StringBuilder builder = new StringBuilder(); foreach (var datum in data) diff --git a/Client/Client.csproj b/Client/Client.csproj index 8bd11db..b37da51 100644 --- a/Client/Client.csproj +++ b/Client/Client.csproj @@ -43,6 +43,7 @@ + @@ -68,6 +69,7 @@ + @@ -78,6 +80,7 @@ + diff --git a/Client/ConsoleForms/ConsoleController.cs b/Client/ConsoleForms/ConsoleController.cs index 1d43bb7..1455054 100644 --- a/Client/ConsoleForms/ConsoleController.cs +++ b/Client/ConsoleForms/ConsoleController.cs @@ -43,6 +43,7 @@ namespace Client.ConsoleForms return false; } } + public bool ShouldExit { get; set; } private ConsoleController(bool resizeListener = true) { @@ -143,7 +144,7 @@ namespace Client.ConsoleForms public KeyEvent ReadKey(bool redrawOnDirty = true) { KeyEvent keyInfo = new KeyEvent(Console.ReadKey(true)); - int lowestDirty = -1; + int lowestDirty = renderQueue.Count; int count = renderQueue.Count - 1; for (int i = count; i >= 0; --i) if (renderQueue[i].Item1.HandleKeyEvent(keyInfo, i == count)) @@ -216,7 +217,7 @@ namespace Client.ConsoleForms { Console.BackgroundColor = clearColor; Console.ForegroundColor = ConsoleColor.White; - for (int i = rect.Top; i <= rect.Bottom; ++i) + for (int i = rect.Top; i < rect.Bottom; ++i) { Console.SetCursorPosition(rect.Left, i); for (int j = rect.Right - rect.Left; j > 0; --j) diff --git a/Client/ConsoleForms/Context.cs b/Client/ConsoleForms/Context.cs index f738bc4..4f8f544 100644 --- a/Client/ConsoleForms/Context.cs +++ b/Client/ConsoleForms/Context.cs @@ -65,5 +65,11 @@ namespace Client.ConsoleForms Hide(viewEntry.Item2); } public string GetIntlString(string i18n) => manager.GetIntlString(i18n); + protected void RegisterAutoHide(params string[] viewIDs) + { + void HideEvent(View v) => Hide(v); + foreach (var viewID in viewIDs) + GetView(viewID).OnBackEvent = HideEvent; + } } } diff --git a/Client/ConsoleForms/ContextManager.cs b/Client/ConsoleForms/ContextManager.cs index 57b74e7..596c36c 100644 --- a/Client/ConsoleForms/ContextManager.cs +++ b/Client/ConsoleForms/ContextManager.cs @@ -27,6 +27,6 @@ namespace Client.ConsoleForms public bool Update(ConsoleController.KeyEvent keypress, bool hasKeypress = true) => Current?.Update(keypress, hasKeypress) == true; - public string GetIntlString(string i18n) => I18n.MapIfExists(i18n); + public string GetIntlString(string i18n) => I18n.MapIfExists((i18n.StartsWith(LangManager.MAPPING_PREFIX) ? "" : LangManager.MAPPING_PREFIX) + i18n); } } diff --git a/Client/ConsoleForms/Graphics/ButtonView.cs b/Client/ConsoleForms/Graphics/ButtonView.cs index bc9afcf..823c61a 100644 --- a/Client/ConsoleForms/Graphics/ButtonView.cs +++ b/Client/ConsoleForms/Graphics/ButtonView.cs @@ -19,6 +19,7 @@ namespace Client.ConsoleForms.Graphics public override bool HandleKeyEvent(ConsoleController.KeyEvent info, bool inFocus) { bool b = inFocus && info.ValidEvent && info.Event.Key == ConsoleKey.Enter; + base.HandleKeyEvent(info, inFocus); if (b) evt?.Invoke(this); return b; } diff --git a/Client/ConsoleForms/Graphics/DialogView.cs b/Client/ConsoleForms/Graphics/DialogView.cs index 28f6956..0020505 100644 --- a/Client/ConsoleForms/Graphics/DialogView.cs +++ b/Client/ConsoleForms/Graphics/DialogView.cs @@ -21,8 +21,16 @@ namespace Client.ConsoleForms.Graphics get => select; set => select = value < 0 ? 0 : value >= options.Length ? options.Length - 1 : value; } - public override Region Occlusion => new Region(new Rectangle(-1, -1, ContentWidth + 4, ContentHeight + 2)); - + /* + public override Region Occlusion => new Region( + new Rectangle( + -padding.Left() - (DrawBorder ? 2 : 0), // Left bound + -padding.Top() - (DrawBorder ? 1 : 0), // Top bound + ContentWidth + padding.Right() + (DrawBorder ? 2 : 0), // Right bound + ContentHeight + padding.Bottom() + (DrawBorder ? 1 : 0) // Bottom bound + ) + ); + */ public ConsoleColor SelectColor { get; set; } public ConsoleColor NotSelectColor { get; set; } public string[] Options { get => options.Transform(d => d.InnerText); } @@ -47,11 +55,11 @@ namespace Client.ConsoleForms.Graphics protected override void _Draw(int left, ref int top) { - DrawEmptyPadding(left, ref top, padding.Top()); + //DrawEmptyPadding(left, ref top, padding.Top()); base.DrawContent(left, ref top); DrawEmptyPadding(left, ref top, 1); DrawOptions(left, ref top); - DrawEmptyPadding(left, ref top, padding.Bottom()); + //DrawEmptyPadding(left, ref top, padding.Bottom()); } protected virtual void DrawOptions(int left, ref int top) @@ -59,7 +67,7 @@ namespace Client.ConsoleForms.Graphics int pl = padding.Left(), pr = padding.Right(); Console.SetCursorPosition(left, top++); - int pad = MaxWidth - options.CollectiveLength() - options.Length + pl + pr; + int pad = MaxWidth - options.CollectiveLength() - options.Length;// + pl + pr; int lpad = (int)(pad / 2f); Console.BackgroundColor = BackgroundColor; Console.Write(Filler(' ', lpad)); @@ -78,6 +86,7 @@ namespace Client.ConsoleForms.Graphics bool changed = base.HandleKeyEvent(evt, inFocus); ConsoleKeyInfo info = evt.Event; if (!evt.ValidEvent || !inFocus) return changed; + evt.ValidEvent = false; // Invalidate event switch (info.Key) { case ConsoleKey.LeftArrow: diff --git a/Client/ConsoleForms/Graphics/InputView.cs b/Client/ConsoleForms/Graphics/InputView.cs index dfc015a..fd68237 100644 --- a/Client/ConsoleForms/Graphics/InputView.cs +++ b/Client/ConsoleForms/Graphics/InputView.cs @@ -11,6 +11,180 @@ namespace Client.ConsoleForms.Graphics { public class InputView : TextView { + public delegate void SubmissionListener(InputView view); + public delegate bool TextEnteredListener(InputView view, InputField change, ConsoleKeyInfo info); + + public SubmissionListener SubmissionsListener { protected get; set; } + public TextEnteredListener InputListener { protected get; set; } + public ConsoleColor DefaultBackgroundColor { get; set; } + public ConsoleColor DefaultTextColor { get; set; } + public ConsoleColor DefaultSelectBackgroundColor { get; set; } + public ConsoleColor DefaultSelectTextColor { get; set; } + public InputField[] Inputs { get; private set; } + private int selectedField; + public int SelectedField + { + get => selectedField; + set + { + selectedField = value; + Dirty = true; + } + } + private string[][] splitInputs; + + + public InputView(ViewData parameters, LangManager lang) : base(parameters, lang) + { + int + sBC = parameters.AttribueAsInt("textfield_select_color", (int)ConsoleColor.Gray), + sTC = parameters.AttribueAsInt("text_select_color", (int)ConsoleColor.Black), + BC = parameters.AttribueAsInt("field_noselect_color", (int)ConsoleColor.DarkGray), + TC = parameters.AttribueAsInt("text_noselect_color", (int)ConsoleColor.Black); + + DefaultBackgroundColor = (ConsoleColor)BC; + DefaultTextColor = (ConsoleColor)TC; + DefaultSelectBackgroundColor = (ConsoleColor)sBC; + DefaultSelectTextColor = (ConsoleColor)sTC; + + List fields = new List(); + foreach (var data in parameters.nestedData.GetFirst(d => d.Name.Equals("Fields")).nestedData) + if (!data.Name.Equals("Field")) continue; + else fields.Add(new InputField(data.InnerText, data.AttribueAsInt("max_length", -1)) + { + ShowText = !data.AttribueAsBool("hide", false), + Text = data.GetAttribute("default"), + InputTypeString = data.GetAttribute("input_type"), + TextColor = (ConsoleColor)data.AttribueAsInt("color_text", TC), + BackgroundColor = (ConsoleColor)data.AttribueAsInt("color_background", BC), + SelectTextColor = (ConsoleColor)data.AttribueAsInt("color_text_select", sTC), + SelectBackgroundColor = (ConsoleColor)data.AttribueAsInt("color_background_select", sBC) + }); + + Inputs = fields.ToArray(); + + int computedSize = 0; + splitInputs = new string[Inputs.Length][]; + for (int i = 0; i < Inputs.Length; ++i) + { + splitInputs[i] = ComputeTextDimensions(Inputs[i].Label.Split(' ')); + computedSize += splitInputs[i].Length; + } + ContentHeight += computedSize + Inputs.Length * 2; + } + + protected override void _Draw(int left, ref int top) + { + DrawContent(left, ref top); + DrawInputFields(left, ref top, 1); + } + + protected void DrawInputFields(int left, ref int top, int spaceHeight) + { + + for (int j = 0; j < Inputs.Length; ++j) + { + DrawEmptyPadding(left, ref top, spaceHeight); + + for (int i = 0; i < splitInputs[j].Length; ++i) + { + Console.SetCursorPosition(left, top++); + Console.BackgroundColor = BackgroundColor; + Console.Write(splitInputs[j][i] + Filler(' ', MaxWidth - splitInputs[j][i].Length)); + } + Console.SetCursorPosition(left, top++); + + + // Draw field + Console.BackgroundColor = j == selectedField ? Inputs[j].SelectBackgroundColor : Inputs[j].BackgroundColor; + Console.ForegroundColor = j == selectedField ? Inputs[j].SelectTextColor : Inputs[j].TextColor; + Console.Write(Inputs[j].ShowText ? Inputs[j].Text.Substring(Inputs[j].RenderStart, Inputs[j].SelectIndex - Inputs[j].RenderStart) : Filler('*', Inputs[j].SelectIndex - Inputs[j].RenderStart)); + if (j == selectedField) Console.BackgroundColor = ConsoleColor.DarkGray; + Console.Write(Inputs[j].SelectIndex < Inputs[j].Text.Length ? Inputs[j].ShowText ? Inputs[j].Text[Inputs[j].SelectIndex] : '*' : ' '); + if (j == selectedField) Console.BackgroundColor = Inputs[j].SelectBackgroundColor; + int drawn = 0; + if (Inputs[j].SelectIndex < Inputs[j].Text.Length) + Console.Write( + Inputs[j].ShowText ? + Inputs[j].Text.Substring(Inputs[j].SelectIndex + 1, drawn = Math.Min(maxWidth + Inputs[j].SelectIndex - Inputs[j].RenderStart - 1, Inputs[j].Text.Length - Inputs[j].SelectIndex - 1)) : + Filler('*', drawn = Math.Min(maxWidth + Inputs[j].SelectIndex - Inputs[j].RenderStart - 1, Inputs[j].Text.Length - Inputs[j].SelectIndex - 1)) + ); + Console.Write(Filler(' ', maxWidth - 1 - drawn - Inputs[j].SelectIndex + Inputs[j].RenderStart)); + Console.ForegroundColor = ConsoleColor.Black; + } + } + + public override bool HandleKeyEvent(ConsoleController.KeyEvent evt, bool inFocus) + { + bool changed = base.HandleKeyEvent(evt, inFocus); + ConsoleKeyInfo info = evt.Event; + if (!evt.ValidEvent || !inFocus || Inputs.Length == 0) return changed; + evt.ValidEvent = false; + switch (info.Key) + { + case ConsoleKey.LeftArrow: + if (Inputs[selectedField].SelectIndex > 0) + { + if (Inputs[selectedField].RenderStart == Inputs[selectedField].SelectIndex--) --Inputs[selectedField].RenderStart; + } + else return changed; + break; + case ConsoleKey.RightArrow: + if (Inputs[selectedField].SelectIndex < Inputs[selectedField].Text.Length) + { + if (++Inputs[selectedField].SelectIndex - Inputs[selectedField].RenderStart == maxWidth) ++Inputs[selectedField].RenderStart; + } + else return changed; + break; + case ConsoleKey.Tab: + case ConsoleKey.DownArrow: + if (selectedField < Inputs.Length - 1) ++selectedField; + else return changed; + break; + case ConsoleKey.UpArrow: + if (selectedField > 0) --selectedField; + else return changed; + break; + case ConsoleKey.Backspace: + if (Inputs[selectedField].SelectIndex > 0) + { + if (InputListener?.Invoke(this, Inputs[selectedField], info) == false) break; + string text = Inputs[selectedField].Text; + Inputs[selectedField].Text = text.Substring(0, Inputs[selectedField].SelectIndex - 1); + if (Inputs[selectedField].SelectIndex < text.Length) Inputs[selectedField].Text += text.Substring(Inputs[selectedField].SelectIndex); + if (Inputs[selectedField].RenderStart == Inputs[selectedField].SelectIndex--) --Inputs[selectedField].RenderStart; + } + else return changed; + break; + case ConsoleKey.Delete: + if (Inputs[selectedField].SelectIndex < Inputs[selectedField].Text.Length) + { + if (InputListener?.Invoke(this, Inputs[selectedField], info) == false) break; + string text = Inputs[selectedField].Text; + Inputs[selectedField].Text = text.Substring(0, Inputs[selectedField].SelectIndex); + if (Inputs[selectedField].SelectIndex + 1 < text.Length) Inputs[selectedField].Text += text.Substring(Inputs[selectedField].SelectIndex + 1); + } + else return changed; + break; + case ConsoleKey.Enter: + SubmissionsListener?.Invoke(this); + return changed; + case ConsoleKey.Escape: + return changed; + default: + if (info.KeyChar != 0 && info.KeyChar != '\b' && info.KeyChar != '\r' && (Inputs[selectedField].Text.Length < Inputs[selectedField].MaxLength || Inputs[selectedField].MaxLength < 0) && Inputs[selectedField].IsValidChar(info.KeyChar)) + { + if (InputListener?.Invoke(this, Inputs[selectedField], info) == false) break; + Inputs[selectedField].Text = Inputs[selectedField].Text.Substring(0, Inputs[selectedField].SelectIndex) + info.KeyChar + Inputs[selectedField].Text.Substring(Inputs[selectedField].SelectIndex); + if (++Inputs[selectedField].SelectIndex - Inputs[selectedField].RenderStart == maxWidth) ++Inputs[selectedField].RenderStart; + } + else return changed; + break; + } + return true; + } + + public enum InputType { Any, @@ -26,7 +200,16 @@ namespace Client.ConsoleForms.Graphics public string Label { get; private set; } public int MaxLength { get; private set; } public bool ShowText { get; set; } - public string Text { get; set; } + private string text; + public string Text + { + get => text; + + internal set + { + text = value; + } + } public int SelectIndex { get; set; } public InputType Input { get; set; } public ConsoleColor TextColor { get; set; } @@ -95,188 +278,13 @@ namespace Client.ConsoleForms.Graphics (Input == InputType.Alphabet && c.IsAlphabetical()) || (Input == InputType.Integer && c.IsNumber()) || (Input == InputType.Decimal && c.IsDecimal()); - } - public delegate void SubmissionListener(InputView view); - public delegate bool TextEnteredListener(InputView view, InputField change, ConsoleKeyInfo info); - - public ConsoleColor DefaultBackgroundColor { get; set; } - public ConsoleColor DefaultTextColor { get; set; } - public ConsoleColor DefaultSelectBackgroundColor { get; set; } - public ConsoleColor DefaultSelectTextColor { get; set; } - public InputField[] Inputs { get; private set; } - private int selectedField; - public int SelectedField - { - get => selectedField; - set + public void ClearText() { - selectedField = value; - Dirty = true; + Text = ""; + SelectIndex = 0; + RenderStart = 0; } } - private string[][] splitInputs; - - public SubmissionListener SubmissionsListener { protected get; set; } - public TextEnteredListener InputListener { protected get; set; } - - public InputView(ViewData parameters, LangManager lang) : base(parameters, lang) - { - int - sBC = parameters.AttribueAsInt("textfield_select_color", (int)ConsoleColor.Gray), - sTC = parameters.AttribueAsInt("text_select_color", (int)ConsoleColor.Black), - BC = parameters.AttribueAsInt("field_noselect_color", (int)ConsoleColor.DarkGray), - TC = parameters.AttribueAsInt("text_noselect_color", (int)ConsoleColor.Black); - - DefaultBackgroundColor = (ConsoleColor)BC; - DefaultTextColor = (ConsoleColor)TC; - DefaultSelectBackgroundColor = (ConsoleColor)sBC; - DefaultSelectTextColor = (ConsoleColor)sTC; - - List fields = new List(); - foreach (var data in parameters.nestedData.GetFirst(d => d.Name.Equals("Fields")).nestedData) - if (!data.Name.Equals("Field")) continue; - else fields.Add(new InputField(data.InnerText, data.AttribueAsInt("max_length", -1)) - { - ShowText = !data.AttribueAsBool("hide", false), - Text = data.GetAttribute("default"), - InputTypeString = data.GetAttribute("input_type"), - TextColor = (ConsoleColor)data.AttribueAsInt("color_text", TC), - BackgroundColor = (ConsoleColor)data.AttribueAsInt("color_background", BC), - SelectTextColor = (ConsoleColor)data.AttribueAsInt("color_text_select", sTC), - SelectBackgroundColor = (ConsoleColor)data.AttribueAsInt("color_background_select", sBC) - }); - - Inputs = fields.ToArray(); - - int computedSize = 0; - splitInputs = new string[Inputs.Length][]; - for (int i = 0; i < Inputs.Length; ++i) - { - splitInputs[i] = ComputeTextDimensions(Inputs[i].Label.Split(' ')); - computedSize += splitInputs[i].Length; - } - ContentHeight += computedSize + Inputs.Length * 2; - //++ContentWidth; // Idk, it works, though... - } - - protected override void _Draw(int left, ref int top) - { - DrawEmptyPadding(left, ref top, padding.Top()); - DrawContent(left, ref top); - DrawInputFields(left, ref top, 1); - DrawEmptyPadding(left, ref top, padding.Bottom()); - } - - protected void DrawInputFields(int left, ref int top, int spaceHeight) - { - int pl = padding.Left(), pr = padding.Right(); - - for (int j = 0; j < Inputs.Length; ++j) - { - DrawEmptyPadding(left, ref top, spaceHeight); - - for (int i = 0; i < splitInputs[j].Length; ++i) - { - Console.SetCursorPosition(left, top++); - Console.BackgroundColor = BackgroundColor; - Console.Write(Filler(' ', pl) + splitInputs[j][i] + Filler(' ', MaxWidth - splitInputs[j][i].Length) + Filler(' ', pr)); - } - Console.SetCursorPosition(left, top++); - - // Draw padding - Console.BackgroundColor = BackgroundColor; - Console.Write(Filler(' ', pl)); - - // Draw field - Console.BackgroundColor = j == selectedField ? Inputs[j].SelectBackgroundColor : Inputs[j].BackgroundColor; - Console.ForegroundColor = j == selectedField ? Inputs[j].SelectTextColor : Inputs[j].TextColor; - Console.Write(Inputs[j].ShowText ? Inputs[j].Text.Substring(Inputs[j].RenderStart, Inputs[j].SelectIndex - Inputs[j].RenderStart) : Filler('*', Inputs[j].SelectIndex - Inputs[j].RenderStart)); - if (j == selectedField) Console.BackgroundColor = ConsoleColor.DarkGray; - Console.Write(Inputs[j].SelectIndex < Inputs[j].Text.Length ? Inputs[j].ShowText ? Inputs[j].Text[Inputs[j].SelectIndex] : '*' : ' '); - if (j == selectedField) Console.BackgroundColor = Inputs[j].SelectBackgroundColor; - int drawn = 0; - if (Inputs[j].SelectIndex < Inputs[j].Text.Length) - Console.Write( - Inputs[j].ShowText ? - Inputs[j].Text.Substring(Inputs[j].SelectIndex + 1, drawn = Math.Min(maxWidth + Inputs[j].SelectIndex - Inputs[j].RenderStart - 1, Inputs[j].Text.Length - Inputs[j].SelectIndex - 1)) : - Filler('*', drawn = Math.Min(maxWidth + Inputs[j].SelectIndex - Inputs[j].RenderStart - 1, Inputs[j].Text.Length - Inputs[j].SelectIndex - 1)) - ); - Console.Write(Filler(' ', maxWidth - 1 - drawn - Inputs[j].SelectIndex + Inputs[j].RenderStart)); - Console.ForegroundColor = ConsoleColor.Black; - - // Draw padding - Console.BackgroundColor = BackgroundColor; - Console.Write(Filler(' ', pr)); - - } - } - - public override bool HandleKeyEvent(ConsoleController.KeyEvent evt, bool inFocus) - { - bool changed = base.HandleKeyEvent(evt, inFocus); - ConsoleKeyInfo info = evt.Event; - if (!evt.ValidEvent || !inFocus || Inputs.Length == 0) return changed; - switch (info.Key) - { - case ConsoleKey.LeftArrow: - if (Inputs[selectedField].SelectIndex > 0) - { - if (Inputs[selectedField].RenderStart == Inputs[selectedField].SelectIndex--) --Inputs[selectedField].RenderStart; - } - else return changed; - break; - case ConsoleKey.RightArrow: - if (Inputs[selectedField].SelectIndex < Inputs[selectedField].Text.Length) - { - if (++Inputs[selectedField].SelectIndex - Inputs[selectedField].RenderStart == maxWidth) ++Inputs[selectedField].RenderStart; - } - else return changed; - break; - case ConsoleKey.Tab: - case ConsoleKey.DownArrow: - if (selectedField < Inputs.Length - 1) ++selectedField; - else return changed; - break; - case ConsoleKey.UpArrow: - if (selectedField > 0) --selectedField; - else return changed; - break; - case ConsoleKey.Backspace: - if (Inputs[selectedField].SelectIndex > 0) - { - if (InputListener?.Invoke(this, Inputs[selectedField], info) == false) break; - string text = Inputs[selectedField].Text; - Inputs[selectedField].Text = text.Substring(0, Inputs[selectedField].SelectIndex - 1); - if (Inputs[selectedField].SelectIndex < text.Length) Inputs[selectedField].Text += text.Substring(Inputs[selectedField].SelectIndex); - if (Inputs[selectedField].RenderStart == Inputs[selectedField].SelectIndex--) --Inputs[selectedField].RenderStart; - } - else return changed; - break; - case ConsoleKey.Delete: - if (Inputs[selectedField].SelectIndex < Inputs[selectedField].Text.Length) - { - if (InputListener?.Invoke(this, Inputs[selectedField], info) == false) break; - string text = Inputs[selectedField].Text; - Inputs[selectedField].Text = text.Substring(0, Inputs[selectedField].SelectIndex); - if (Inputs[selectedField].SelectIndex + 1 < text.Length) Inputs[selectedField].Text += text.Substring(Inputs[selectedField].SelectIndex + 1); - } - else return changed; - break; - case ConsoleKey.Enter: - SubmissionsListener?.Invoke(this); - return changed; - default: - if (info.KeyChar != 0 && info.KeyChar != '\b' && info.KeyChar != '\r' && (Inputs[selectedField].Text.Length < Inputs[selectedField].MaxLength || Inputs[selectedField].MaxLength < 0) && Inputs[selectedField].IsValidChar(info.KeyChar)) - { - if (InputListener?.Invoke(this, Inputs[selectedField], info) == false) break; - Inputs[selectedField].Text = Inputs[selectedField].Text.Substring(0, Inputs[selectedField].SelectIndex) + info.KeyChar + Inputs[selectedField].Text.Substring(Inputs[selectedField].SelectIndex); - if (++Inputs[selectedField].SelectIndex - Inputs[selectedField].RenderStart == maxWidth) ++Inputs[selectedField].RenderStart; - } - else return changed; - break; - } - return true; - } } } diff --git a/Client/ConsoleForms/Graphics/ListView.cs b/Client/ConsoleForms/Graphics/ListView.cs index 62229ed..e31aeb6 100644 --- a/Client/ConsoleForms/Graphics/ListView.cs +++ b/Client/ConsoleForms/Graphics/ListView.cs @@ -16,7 +16,14 @@ namespace Client.ConsoleForms.Graphics private readonly bool limited; - public override Region Occlusion => new Region(new Rectangle(-padding.Left(), -padding.Top(), ContentWidth + padding.Right(), ContentHeight + padding.Bottom())); + public override Region Occlusion => new Region( + new Rectangle( + -padding.Left() - (DrawBorder ? 2 : 0), // Left bound + -padding.Top() - (DrawBorder ? 1 : 0), // Top bound + ContentWidth + padding.Right() + (DrawBorder ? 2 : 0) + 1, // Right bound + ContentHeight + padding.Bottom() + (DrawBorder ? 1 : 0) // Bottom bound + ) + ); public ListView(ViewData parameters, LangManager lang) : base(parameters, lang) { @@ -42,28 +49,59 @@ namespace Client.ConsoleForms.Graphics // Optimized to add multiple view before recomputing size - public void AddViews(params Tuple[] data) + public void AddViews(params Tuple[] data) => AddViews(0, data); + public void AddViews(int insert, params Tuple[] data) { + int inIdx = insert; foreach (var datum in data) { datum.Item2.DrawBorder = false; - _AddView(datum.Item2, datum.Item1); + _AddView(datum.Item2, datum.Item1, inIdx++); } ComputeSize(); } // Add single view - public void AddView(View v, string viewID) + public void AddView(View v, string viewID, int insert = 0) { - _AddView(v, viewID); + _AddView(v, viewID, insert); ComputeSize(); } // Add view without recomputing layout size - private void _AddView(View v, string viewID) + private void _AddView(View v, string viewID, int insert) { foreach (var data in innerViews) if (data.Item1 != null && data.Item1.Equals(viewID)) throw new SystemException("Cannot load view with same id"); // TODO: Replace with custom exception - innerViews.Add(new Tuple(viewID, v)); + innerViews.Insert(Math.Min(insert, innerViews.Count), new Tuple(viewID, v)); + } + + public bool RemoveView(string name) + { + for (int i = innerViews.Count - 1; i >= 0; --i) + if (innerViews[i].Item1.Equals(name)) + { + innerViews.RemoveAt(i); + return true; + } + return false; + } + + public bool RemoveView(View view) + { + for (int i = innerViews.Count - 1; i >= 0; --i) + if (innerViews[i].Item2.Equals(view)) + { + innerViews.RemoveAt(i); + return true; + } + return false; + } + + public void RemoveIf(Predicate> p) + { + for(int i = innerViews.Count - 1; i>=0; --i) + if (p(innerViews[i])) + innerViews.RemoveAt(i); } protected void ComputeSize() @@ -88,9 +126,10 @@ namespace Client.ConsoleForms.Graphics protected override void _Draw(int left, ref int top) { + ++left; foreach(var view in innerViews) { - DrawBlankLine(left, ref top); + DrawBlankLine(left - 1, ref top); ConsoleColor bgHold = view.Item2.BackgroundColor, fgHold = view.Item2.TextColor; @@ -102,7 +141,7 @@ namespace Client.ConsoleForms.Graphics } Region sub = new Region(new Rectangle(0, 0, ContentWidth, view.Item2.ContentHeight)).Subtract(view.Item2.Occlusion); - sub.Offset(left, top); + sub.Offset(left - 1, top); ConsoleController.ClearRegion(sub, view.Item2.BackgroundColor); @@ -114,7 +153,7 @@ namespace Client.ConsoleForms.Graphics view.Item2.TextColor = fgHold; } } - DrawBlankLine(left, ref top); + DrawBlankLine(left - 1, ref top); } protected virtual void DrawView(int left, ref int top, View v) => v.Draw(left, ref top); @@ -128,17 +167,16 @@ namespace Client.ConsoleForms.Graphics public override bool HandleKeyEvent(ConsoleController.KeyEvent info, bool inFocus) { - if (!inFocus) return false; - if (innerViews[SelectedView].Item2.HandleKeyEvent(info, inFocus)) return true; - else if (!info.ValidEvent) return false; + if (!inFocus || !info.ValidEvent) return false; + bool changed = base.HandleKeyEvent(info, inFocus) || innerViews[SelectedView].Item2.HandleKeyEvent(info, inFocus); + info.ValidEvent = false; // Handle navigation switch (info.Event.Key) { case ConsoleKey.UpArrow: if (SelectedView > 0) { - info.ValidEvent = false; --SelectedView; return true; } @@ -146,14 +184,13 @@ namespace Client.ConsoleForms.Graphics case ConsoleKey.DownArrow: if(SelectedView < innerViews.Count - 1) { - info.ValidEvent = false; ++SelectedView; return true; } break; } - return base.HandleKeyEvent(info, inFocus); + return changed; } } } diff --git a/Client/ConsoleForms/Graphics/TextView.cs b/Client/ConsoleForms/Graphics/TextView.cs index a4974c4..8affbdb 100644 --- a/Client/ConsoleForms/Graphics/TextView.cs +++ b/Client/ConsoleForms/Graphics/TextView.cs @@ -14,6 +14,8 @@ namespace Client.ConsoleForms.Graphics protected string[] text_render; protected int maxWidth, maxHeight; + public string Text { get; } + public int MaxWidth { get => maxWidth; @@ -36,7 +38,14 @@ namespace Client.ConsoleForms.Graphics Dirty = true; } } - public override Region Occlusion => new Region(new Rectangle(DrawBorder ? -1 : 0, DrawBorder ? -1 : 0, ContentWidth + (DrawBorder ? 3 : 0), ContentHeight)); + public override Region Occlusion => new Region( + new Rectangle( + -padding.Left() - (DrawBorder ? 2 : 0), // Left bound + -padding.Top() - (DrawBorder ? 1 : 0), // Top bound + ContentWidth + padding.Right() + padding.Left() + (DrawBorder ? 2 : 0), // Right bound + ContentHeight + padding.Bottom() + padding.Top() + (DrawBorder ? 1 : 0) // Bottom bound + ) + ); //public char Border { get; set; } //public ConsoleColor BorderColor { get; set; } @@ -46,7 +55,7 @@ namespace Client.ConsoleForms.Graphics //BorderColor = (ConsoleColor) parameters.AttribueAsInt("border", (int)ConsoleColor.Blue); Border = ' '; - this.text = parameters.NestedText("Text").Split(' '); + this.text = (Text = parameters.NestedText("Text")).Split(' '); int widest = 0; foreach (var t in parameters.NestedText("Text").Split('\n')) if (t.Length > widest) @@ -58,8 +67,8 @@ namespace Client.ConsoleForms.Graphics text_render = ComputeTextDimensions(this.text); int actualWidth = 0; foreach (var t in text_render) if (actualWidth < t.Length) actualWidth = t.Length; - ContentWidth = maxWidth + padding.Left() + padding.Right(); - ContentHeight = text_render.Length + padding.Top() + padding.Bottom(); + ContentWidth = maxWidth;// + padding.Left() + padding.Right(); + ContentHeight = text_render.Length;// + padding.Top() + padding.Bottom(); } protected virtual string[] ComputeTextDimensions(string[] text) @@ -149,9 +158,9 @@ namespace Client.ConsoleForms.Graphics protected override void _Draw(int left, ref int top) { - DrawEmptyPadding(left, ref top, padding.Top()); + //DrawEmptyPadding(left, ref top, padding.Top()); DrawContent(left, ref top); - DrawEmptyPadding(left, ref top, padding.Bottom()); + //DrawEmptyPadding(left, ref top, padding.Bottom()); } protected void DrawContent(int left, ref int top) @@ -162,21 +171,20 @@ namespace Client.ConsoleForms.Graphics for (int i = 0; i < text_render.Length; ++i) { Console.SetCursorPosition(left, top++); - Console.Write(Filler(' ', pl) + text_render[i] + Filler(' ', MaxWidth - text_render[i].Length) + Filler(' ', pr)); + Console.Write(/*Filler(' ', pl) + */text_render[i] + Filler(' ', MaxWidth - text_render[i].Length)/* + Filler(' ', pr)*/); } } + protected void DrawEmptyPadding(int left, ref int top, int padHeight) { - int pl = padding.Left(), pr = padding.Right(); + //int pl = padding.Left(), pr = padding.Right(); for (int i = padHeight; i > 0; --i) { Console.SetCursorPosition(left, top++); Console.BackgroundColor = BackgroundColor; - Console.Write(Filler(' ', maxWidth + pl + pr)); + Console.Write(Filler(' ', maxWidth/* + pl + pr*/)); } } - - public override bool HandleKeyEvent(ConsoleController.KeyEvent info, bool inFocus) => base.HandleKeyEvent(info, inFocus); } } diff --git a/Client/ConsoleForms/Graphics/View.cs b/Client/ConsoleForms/Graphics/View.cs index 54108ff..8718949 100644 --- a/Client/ConsoleForms/Graphics/View.cs +++ b/Client/ConsoleForms/Graphics/View.cs @@ -12,10 +12,11 @@ namespace Client.ConsoleForms.Graphics public abstract class View { protected delegate void EventAction(); + public delegate void ViewEvent(View v); protected static readonly Padding DEFAULT_PADDING = new AbsolutePadding(0, 0, 0, 0); - protected readonly Padding padding; + protected internal readonly Padding padding; protected readonly Gravity gravity; protected readonly bool vCenter, hCenter; protected readonly string back_data; @@ -30,6 +31,7 @@ namespace Client.ConsoleForms.Graphics public abstract Region Occlusion { get; } public bool Dirty { get; set; } public LangManager I18n { get; private set; } + public ViewEvent OnBackEvent { get; set; } public View(ViewData parameters, LangManager lang) { @@ -62,32 +64,63 @@ namespace Client.ConsoleForms.Graphics public void Draw(int left, ref int top) { Dirty = false; - if (DrawBorder) _DrawBorder(left, top); - _Draw(left + 1, ref top); + if (DrawBorder) + _DrawBorder(left, top); + DrawPadding(ref left, ref top); + _Draw(left, ref top); } public virtual void _DrawBorder(int left, int top) { Console.BackgroundColor = BorderColor; - Console.SetCursorPosition(left - 1, top - 1); - Console.Write(Filler(Border, ContentWidth + 2)); - for (int i = -1; i < ContentHeight; ++i) + Console.SetCursorPosition(left - 1 - padding.Left(), top - 1 - padding.Top()); + Console.Write(Filler(Border, ContentWidth + padding.Left() + padding.Right() + 4)); + for (int i = 0; i < ContentHeight + padding.Top() + padding.Bottom(); ++i) { - Console.SetCursorPosition(left-1, top + i); + Console.SetCursorPosition(left - padding.Left() - 1, top - padding.Top() + i); Console.Write(Filler(Border, 2)); - Console.SetCursorPosition(left + ContentWidth + 1, top + i); + Console.SetCursorPosition(left + ContentWidth + padding.Left() + padding.Right() - 1, top - padding.Top() + i); Console.Write(Filler(Border, 2)); } - Console.SetCursorPosition(left-1, top + ContentHeight); - Console.Write(Filler(Border, ContentWidth + 4)); + Console.SetCursorPosition(left - padding.Left() - 1, top + ContentHeight + padding.Bottom()); + Console.Write(Filler(Border, ContentWidth + padding.Left() + padding.Right() + 4)); Console.BackgroundColor = ConsoleColor.Black; } + public virtual void DrawPadding(ref int left, ref int top) + { + Console.BackgroundColor = BackgroundColor; + // Top padding + for(int i = 0; i new LayoutMeta( (w, h) => new Tuple( - SpaceMaths.CenterPad(Console.WindowWidth, view.ContentWidth).Item1, - SpaceMaths.CenterPad(Console.WindowHeight, view.ContentHeight + 1).Item1 + SpaceMaths.CenterPad(Console.WindowWidth, view.ContentWidth + view.padding.Left() + view.padding.Right()).Item1, + SpaceMaths.CenterPad(Console.WindowHeight, view.ContentHeight + view.padding.Top() + view.padding.Bottom() + 1).Item1 ) ); } diff --git a/Client/Context/NetContext.cs b/Client/Context/NetContext.cs index 9527ad1..cb4600a 100644 --- a/Client/Context/NetContext.cs +++ b/Client/Context/NetContext.cs @@ -58,12 +58,13 @@ namespace Client void load() => manager.LoadContext(new WelcomeContext(manager, ita)); // Add condition check for remote peer verification - if (bool.Parse(p.Value)) controller.Popup(GetIntlString("@string/NC_verified"), 1000, ConsoleColor.Green, load); - else controller.Popup(GetIntlString("@string/verror"), 5000, ConsoleColor.Red, load); + if (bool.Parse(p.Value)) controller.Popup(GetIntlString("NC_verified"), 1000, ConsoleColor.Green, load); + else controller.Popup(GetIntlString("verror"), 5000, ConsoleColor.Red, load); }; DialogView identityNotify = GetView("IdentityVerify"); identityNotify.RegisterSelectListener( (vw, ix, nm) => { + Hide(identityNotify); connecting = false; verify.Subscribe = null; // Clear subscription #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed @@ -73,10 +74,12 @@ namespace Client }); Show(identityNotify); } - else if (i.Inputs[0].Text.Length == 0 || i.Inputs[1].Text.Length == 0) controller.AddView(views.GetNamed("EmptyFieldError")); + else if (i.Inputs[0].Text.Length == 0 || i.Inputs[1].Text.Length == 0) Show("EmptyFieldError"); else if (!ip) Show("IPError"); else Show("PortError"); }; + + GetView("NetConnect").OnBackEvent = v => controller.ShouldExit = true; } public override void OnCreate() => Show("NetConnect"); diff --git a/Client/Context/SessionContext.cs b/Client/Context/SessionContext.cs index ef5881a..3ddbb32 100644 --- a/Client/Context/SessionContext.cs +++ b/Client/Context/SessionContext.cs @@ -23,6 +23,8 @@ namespace Client private List accounts = null; private string username; private bool isAdministrator = false; + private readonly FixedQueue> accountDataCache = new FixedQueue>(64); + private bool accountChange = false; public SessionContext(ContextManager manager, BankNetInteractor interactor) : base(manager, "Session", "Common") @@ -30,83 +32,242 @@ namespace Client this.interactor = interactor; scheduleDestroy = !interactor.IsLoggedIn; - GetView("Success").RegisterSelectListener((v, i, s) => - { - interactor.Logout(); - manager.LoadContext(new NetContext(manager)); - }); + RegisterAutoHide("account_create", "account_info", "password_update", "exit_prompt", "account_show"); + + GetView("Success").RegisterSelectListener((v, i, s) => HandleLogout()); // Menu option setup ListView options = GetView("menu_options"); - options.GetView("exit").SetEvent(v => - { - interactor.Logout(); - manager.LoadContext(new NetContext(manager)); - }); + options.GetView("exit").SetEvent(v => HandleLogout()); options.GetView("view").SetEvent(v => { + if (accountChange) RefreshAccountList(); if (!accountsGetter.HasValue) Show("data_fetch"); accountsGetter.Subscribe = p => { + accountsGetter.Unsubscribe(); Hide("data_fetch"); void SubmitListener(View listener) { ButtonView view = listener as ButtonView; + void ShowAccountData(string name, decimal balance) + { + // Build dialog view manually + var show = new DialogView( + new ViewData("DialogView") + + // Layout parameters + .SetAttribute("padding_left", 2) + .SetAttribute("padding_right", 2) + .SetAttribute("padding_top", 1) + .SetAttribute("padding_bottom", 1) + + // Option buttons + .AddNested(new ViewData("Options").AddNestedSimple("Option", GetIntlString("GENERIC_dismiss"))) + + // Message + .AddNestedSimple("Text", GetIntlString("SE_info").Replace("$0", name).Replace("$1", balance.ToString())), + + // No translation (it's already handled) + LangManager.NO_LANG); + + show.RegisterSelectListener((_, s, l) => Hide(show)); + Show(show); + } + + // TODO: Show account info + var account = AccountLookup(view.Text); + if (account == null) + { + // TODO: Get account data from server + cache data + Show("data_fetch"); + Promise info_promise = Promise.AwaitPromise(interactor.AccountInfo(view.Text)); + info_promise.Subscribe = evt => + { + Hide("data_fetch"); + if (evt.Value.StartsWith("ERROR") || !Account.TryParse(evt.Value, out var act)) + controller.Popup(GetIntlString("GENERIC_error"), 3000, ConsoleColor.Red); + else + { + accountDataCache.Enqueue(new Tuple(view.Text, act.balance)); // Cache result + ShowAccountData(view.Text, act.balance); + } + + }; + } + else ShowAccountData(account.Item1, account.Item2); } var list = GetView("account_show"); + list.RemoveIf(t => !t.Item1.Equals("close")); var data = p.Value.Split('&'); bool b = data.Length == 1 && data[0].Length == 0; Tuple[] listData = new Tuple[data.Length - (b?1:0)]; if(!b) for(int i = 0; i(data[i].FromBase64String(), t); + listData[i] = new Tuple(t.Text, t); } - string dismiss = GetIntlString("@string/GENERIC_dismiss"); + string dismiss = GetIntlString("GENERIC_dismiss"); ButtonView exit = list.GetView("close"); exit.SetEvent(_ => Hide(list)); - list.AddViews(listData); + list.AddViews(0, listData); // Insert generated buttons before predefined "close" button Show(list); }; }); - // Update password - options.GetView("password_update").SetEvent(v => + GetView("password_update").SubmissionsListener = v => { + bool hasError = v.Inputs[0].Text.Length == 0; + if (hasError) + { + // Notify user, as well as mark the errant input field + v.Inputs[0].SelectBackgroundColor = ConsoleColor.Red; + v.Inputs[0].BackgroundColor = ConsoleColor.DarkRed; + controller.Popup(GetIntlString("ERR_empty"), 3000, ConsoleColor.Red); + } + if(v.Inputs[1].Text.Length == 0) + { + v.Inputs[1].SelectBackgroundColor = ConsoleColor.Red; + v.Inputs[1].BackgroundColor = ConsoleColor.DarkRed; + if(!hasError) controller.Popup(GetIntlString("ERR_empty"), 3000, ConsoleColor.Red); + return; // No need to continue, we have notified the user. There is no valid information to operate on past this point + } + if (!v.Inputs[0].Text.Equals(v.Inputs[1].Text)) + { + controller.Popup(GetIntlString("SU_mismatch"), 3000, ConsoleColor.Red); + return; + } + Show("update_stall"); + Task t = interactor.UpdatePassword(v.Inputs[0].Text); + Promise.AwaitPromise(t).Subscribe = p => + { + Hide("update_stall"); + Hide("password_update"); + v.Inputs[0].ClearText(); + v.Inputs[1].ClearText(); + v.SelectedField = 0; + }; + }; + // Actual "create account" input box thingy + var input = GetView("account_create"); + input.SubmissionsListener = __ => + { + if (input.Inputs[0].Text.Length == 0) + { + input.Inputs[0].SelectBackgroundColor = ConsoleColor.Red; + input.Inputs[0].BackgroundColor = ConsoleColor.DarkRed; + controller.Popup(GetIntlString("ERR_empty"), 3000, ConsoleColor.Red); + } + else + { + void AlreadyExists() + => controller.Popup(GetIntlString("SE_account_exists").Replace("$0", input.Inputs[0].Text), 2500, ConsoleColor.Red, () => Hide(input)); + + var act = AccountLookup(input.Inputs[0].Text); + if (act != null) AlreadyExists(); + else + { + Show("account_stall"); + Promise accountPromise = Promise.AwaitPromise(interactor.CreateAccount(input.Inputs[0].Text)); + accountPromise.Subscribe = p => + { + if (bool.Parse(p.Value)) + { + controller.Popup(GetIntlString("SE_account_success"), 750, ConsoleColor.Green, () => Hide(input)); + accountChange = true; + } + else AlreadyExists(); + Hide("account_stall"); + }; + } + } + }; + input.InputListener = (v, c, i) => + { + c.BackgroundColor = v.DefaultBackgroundColor; + c.SelectBackgroundColor = v.DefaultSelectBackgroundColor; + return true; + }; + + options.GetView("add").SetEvent(_ => Show(input)); + + // Set up a listener to reset color scheme + GetView("password_update").InputListener = (v, c, i) => + { + c.BackgroundColor = v.DefaultBackgroundColor; + c.SelectBackgroundColor = v.DefaultSelectBackgroundColor; + return true; + }; + + // Update password + options.GetView("update").SetEvent(v => Show("password_update")); + + options.OnBackEvent = v => + { + Show("exit_prompt"); + }; + + GetView("exit_prompt").RegisterSelectListener((v, i, s) => + { + if (i == 0) Hide("exit_prompt"); + else + { + interactor.Logout(); + controller.ShouldExit = true; + } }); if (!scheduleDestroy) { // We have a valid context! - userDataGetter = Promise.AwaitPromise(interactor.UserInfo()); // Get basic user info - accountsGetter = Promise.AwaitPromise(interactor.ListUserAccounts()); // Get accounts associated with this user - - userDataGetter.Subscribe = p => - { - var data = p.Value.Split('&'); - username = data[0].FromBase64String(); - isAdministrator = bool.Parse(data[1]); - }; - - accountsGetter.Subscribe = p => - { - var data = p.Value.Split('&'); - accounts = new List(); - accounts.AddRange(data); - }; + RefreshUserInfo(); // Get user info + RefreshAccountList(); // Get account list for user } } - private void HandleLogout() + private void RefreshAccountList() { + accountsGetter = Promise.AwaitPromise(interactor.ListUserAccounts()); // Get accounts associated with this user + accountsGetter.Subscribe = p => + { + var data = p.Value.Split('&'); + accounts = new List(); + accounts.AddRange(data); + }; + } + + private void RefreshUserInfo() + { + userDataGetter = Promise.AwaitPromise(interactor.UserInfo()); // Get basic user info + + userDataGetter.Subscribe = p => + { + var data = p.Value.Split('&'); + username = data[0].FromBase64String(); + isAdministrator = bool.Parse(data[1]); + }; + } + + private void HandleLogout(bool automatic = false) + { + interactor.Logout(); + controller.Popup(GetIntlString($"SE_{(automatic ? "auto" : "")}lo"), 2500, ConsoleColor.DarkMagenta, () => manager.LoadContext(new NetContext(manager))); + } + + private Tuple AccountLookup(string name) + { + foreach (var cacheEntry in accountDataCache) + if (cacheEntry.Item1.Equals(name)) + return cacheEntry; + return null; } public override void OnCreate() diff --git a/Client/ParseException.cs b/Client/ParseException.cs new file mode 100644 index 0000000..b6ac5e6 --- /dev/null +++ b/Client/ParseException.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Client +{ + public class ParseException : SystemException + { + public ParseException() + { + } + + public ParseException(string message) : base(message) + { + } + + public ParseException(string message, Exception innerException) : base(message, innerException) + { + } + + protected ParseException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/Client/Program.cs b/Client/Program.cs index f51ca5d..e04d12b 100644 --- a/Client/Program.cs +++ b/Client/Program.cs @@ -55,7 +55,7 @@ namespace ConsoleForms b = manager.Update(info, haskey); controller.Draw(); } - } while (!info.ValidEvent || info.Event.Key != ConsoleKey.Escape); + } while ((!info.ValidEvent || info.Event.Key != ConsoleKey.Escape) && !controller.ShouldExit); } // Detects if a key has been hit without blocking diff --git a/Client/Promise.cs b/Client/Promise.cs index 94b61db..8bef727 100644 --- a/Client/Promise.cs +++ b/Client/Promise.cs @@ -31,5 +31,7 @@ namespace Client p.Wait(); return p.Result; } + + public void Unsubscribe() => evt = null; } } diff --git a/Client/Resources/Layout/Common.xml b/Client/Resources/Layout/Common.xml index 5cd6b67..a5369eb 100644 --- a/Client/Resources/Layout/Common.xml +++ b/Client/Resources/Layout/Common.xml @@ -28,6 +28,6 @@ padding_right="2" padding_top="1" padding_bottom="1"> - @string/GENERiC_fetch + @string/GENERIC_fetch \ No newline at end of file diff --git a/Client/Resources/Layout/Session.xml b/Client/Resources/Layout/Session.xml index 92ff22f..e98497b 100644 --- a/Client/Resources/Layout/Session.xml +++ b/Client/Resources/Layout/Session.xml @@ -19,6 +19,15 @@ @string/SE_bal + + @string/SE_account_stall + + + + padding_right="2"> @string/SE_open @@ -58,8 +65,21 @@ + + + @string/SE_account_name + + @string/SE_account_create + + - + @string/GENERIC_dismiss @@ -73,8 +93,29 @@ padding_top="1" padding_bottom="1"> - + @string/SE_info + + + @string/SE_updatestall + + + + + + + + @string/SE_exit_prompt + + \ No newline at end of file diff --git a/Client/Resources/Strings/en_GB/strings.xml b/Client/Resources/Strings/en_GB/strings.xml index c087ff9..2130d33 100644 --- a/Client/Resources/Strings/en_GB/strings.xml +++ b/Client/Resources/Strings/en_GB/strings.xml @@ -20,7 +20,7 @@ To go back, press [ESCAPE] Register Account Registering... An account with this username already exists! - The entered passwords don't match! + The entered passwords don't match! The password you have supplied has been deemed to be weak. Are you sure you want to continue? Log in Authenticating... @@ -44,17 +44,24 @@ To go back, press [ESCAPE] Open an account Close an account Show accounts - $0 -Balance: $1 -Date of creation: $2 + Name: $0 +Balance: $1 You were automatically logged out due to inactivity Logged out + Updating password... + Account name: + Create a new account + Creating account... + Account "$0" already exists! + Account successfully created! + Are you sure you would like log out and exit? Fetching data... Close Ok Yes No + An unknown error occurred! One of more required field was empty! \ No newline at end of file diff --git a/Client/Resources/Strings/en_US/strings.xml b/Client/Resources/Strings/en_US/strings.xml index 5c01379..2bf4b77 100644 --- a/Client/Resources/Strings/en_US/strings.xml +++ b/Client/Resources/Strings/en_US/strings.xml @@ -44,17 +44,24 @@ To go back, press [ESCAPE] Open an account Close an account Show accounts - $0 -Balance: $1 -Date of creation: $2 + Name: $0 +Balance: $1 You were automatically logged out due to inactivity Logged out + Updating password... + Account name: + Create a new account + Creating account... + Account "$0" already exists! + Account successfully created! + Are you sure you would like log out and exit? Fetching data... Close Ok Yes No + An unknown error occurred! One of more required field was empty! \ No newline at end of file diff --git a/Client/Resources/Strings/sv_SE/strings.xml b/Client/Resources/Strings/sv_SE/strings.xml index 13d3952..17128dd 100644 --- a/Client/Resources/Strings/sv_SE/strings.xml +++ b/Client/Resources/Strings/sv_SE/strings.xml @@ -44,17 +44,24 @@ För att backa, tryck [ESCAPE] Öppna ett konto Stäng ett konto Visa konton - "$0" -Kontobalans: $1 -Begynnelsedatum: $2 + Namn: $0 +Kontobalans: $1 Du har automatiskt loggats ut p.g.a. inaktivitet Utloggad + Uppdaterar lösenord... + Kontonamn: + Skapa nytt konto + Skapar konto... + Kontot "$0" finns redan! + Konto skapat! + Är du säker på att du vill logga ut och stänga? Hämtar data... Stäng Ok Ja Nej + Ett oväntat fel uppstod! Ett eller flera obligatoriska inputfält är tomma! \ No newline at end of file diff --git a/Client/Transaction.cs b/Client/Transaction.cs new file mode 100644 index 0000000..637bdb3 --- /dev/null +++ b/Client/Transaction.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tofvesson.Crypto; + +namespace Client +{ + public class Transaction + { + public string fromAccount; + public string toAccount; + public string from; + public string to; + public decimal amount; + public string meta; + + public Transaction(string from, string to, decimal amount, string meta, string fromAccount, string toAccount) + { + this.fromAccount = fromAccount; + this.toAccount = toAccount; + this.from = from; + this.to = to; + this.amount = amount; + this.meta = meta; + } + + public static Transaction Parse(string txData) + { + var data = txData.Split('&'); + if (data.Length < 5 || !decimal.TryParse(data[4], out var amount)) throw new ParseException("String did not represent a transaction!"); + return new Transaction( + data[2].FromBase64String(), + data[1].FromBase64String(), + amount, + data.Length == 6 ? data[5].FromBase64String() : null, + data[3].FromBase64String(), + data[1].FromBase64String() + ); + } + } +} diff --git a/Common/Common.csproj b/Common/Common.csproj index 4b5cc6a..f0eddd7 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -74,6 +74,7 @@ + diff --git a/Common/FixedQueue.cs b/Common/FixedQueue.cs new file mode 100644 index 0000000..bba7e81 --- /dev/null +++ b/Common/FixedQueue.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tofvesson.Common +{ + // A custom queue implementation with a fixed size + // Almost directly copied from https://gist.github.com/GabrielTofvesson/1cfbb659e7b2f7cfb6549c799b0864f3 + public class FixedQueue : IEnumerable + { + protected readonly T[] queue; + protected int queueCount = 0; + protected int queueStart; + + public int Count { get => queueCount; } + + public FixedQueue(int maxSize) + { + queue = new T[maxSize]; + queueStart = 0; + } + + // Add an item to the queue + public bool Enqueue(T t) + { + queue[(queueStart + queueCount) % queue.Length] = t; + if (++queueCount > queue.Length) + { + --queueCount; + return true; + } + return false; + } + + // Remove an item from the queue + public T Dequeue() + { + if (--queueCount == -1) throw new IndexOutOfRangeException("Cannot dequeue empty queue!"); + T res = queue[queueStart]; + queue[queueStart] = default(T); // Remove reference to item + queueStart = (queueStart + 1) % queue.Length; + return res; + } + + // Indexing for the queue + public T ElementAt(int index) => queue[(queueStart + index) % queue.Length]; + + // Enumeration + public virtual IEnumerator GetEnumerator() => new QueueEnumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + // Enumerator for this queue + public sealed class QueueEnumerator : IEnumerator + { + private int offset = -1; + private readonly FixedQueue queue; + + internal QueueEnumerator(FixedQueue queue) => this.queue = queue; + + object IEnumerator.Current => this.Current; + public T Current => offset == -1 ? default(T) : queue.ElementAt(offset); // Get current item or (null) if MoveNext() hasn't been called + public void Dispose() { } // NOP + public bool MoveNext() => offset < queue.Count && ++offset < queue.Count; // Increment index tracker (offset) + public void Reset() => offset = -1; + } + } +} diff --git a/Common/NetServer.cs b/Common/NetServer.cs index 9a91f10..47b2766 100644 --- a/Common/NetServer.cs +++ b/Common/NetServer.cs @@ -156,7 +156,12 @@ namespace Tofvesson.Net public bool Update() { - bool stop = client.SyncListener(ref hasCrypto, ref expectedSize, out bool read, buffer, buf); + bool stop = true; + try + { + stop = client.SyncListener(ref hasCrypto, ref expectedSize, out bool read, buffer, buf); + } + catch { } return stop; } public bool IsConnected() => client.IsConnected; diff --git a/Common/Streams.cs b/Common/Streams.cs index 1c102fe..9025fab 100644 --- a/Common/Streams.cs +++ b/Common/Streams.cs @@ -10,7 +10,6 @@ namespace Tofvesson.Common { public sealed class TimeStampWriter : TextWriter { - private readonly DateTime time = DateTime.Now; private readonly string dateFormat; private readonly TextWriter underlying; private bool triggered; @@ -36,7 +35,7 @@ namespace Tofvesson.Common if (triggered) { StringBuilder s = new StringBuilder(); - s.Append('[').Append(time.ToString(dateFormat)).Append("] "); + s.Append('[').Append(DateTime.Now.ToString(dateFormat)).Append("] "); foreach (var c in s.ToString()) underlying.Write(c); } underlying.Write(value); diff --git a/Server/Database.cs b/Server/Database.cs index a1f3ebf..8417843 100644 --- a/Server/Database.cs +++ b/Server/Database.cs @@ -506,7 +506,7 @@ namespace Server .Append('&') .Append(tx.amount.ToString()); if (tx.meta != null) builder.Append('&').Append(tx.meta.ToBase64String()); - builder.Append('}'); + //builder.Append('}'); } return builder.ToString(); } @@ -531,7 +531,7 @@ namespace Server this.IsAdministrator = copy.IsAdministrator; this.PasswordHash = copy.PasswordHash; this.Salt = copy.Salt; - accounts.AddRange(copy.accounts); + foreach (var acc in copy.accounts) accounts.Add(new Account(acc)); } public User(string name, string passHash, string salt, bool generatePass = false, bool admin = false) diff --git a/Server/Program.cs b/Server/Program.cs index 4c4e32e..0911895 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -15,10 +15,13 @@ namespace Server { class Program { + // Message-of-the-day (for all days) private const string CONSOLE_MOTD = @"Tofvesson Enterprises Banking Server By Gabriel Tofvesson Use command 'help' to get a list of available commands"; + + // Specific error reference localization prefix private const string VERBOSE_RESPONSE = "@string/REMOTE_"; public static int verbosity = 2; public static void Main(string[] args) @@ -136,7 +139,10 @@ Use command 'help' to get a list of available commands"; { string[] cmd = ParseCommand(r, out long id); - // Perform a signature verification by signing a nonce + // Handle corrupt or badly formatted messages from client + if (cmd == null) return ErrorResponse(-1, "corrupt"); + + // Server endpoints switch (cmd[0]) { case "Auth": // Log in to a user account (get a session id) @@ -168,8 +174,11 @@ Use command 'help' to get a list of available commands"; Output.Info($"Performing availability check on name \"{name}\""); return GenerateResponse(id, !db.ContainsUser(name)); } - case "Refresh": - return GenerateResponse(id, manager.Refresh(cmd[1])); + case "Refresh": // Refresh a session + { + if (verbosity > 0) Output.Info($"Refreshing session \"{cmd[1]}\""); + return GenerateResponse(id, manager.Refresh(cmd[1])); + } case "List": // List all available users for transactions { if (!GetUser(cmd[1], out Database.User user)) @@ -202,7 +211,7 @@ Use command 'help' to get a list of available commands"; GetAccount(name, user, out var account)) { // Don't print input data to output in case sensitive information was included - Output.Error($"Failed to create account \"{name}\" for user \"{manager.GetUser(session)}\" (sessionID={session})"); + Output.Error($"Failed to create account \"{name}\" for user \"{manager.GetUser(session).Name}\" (sessionID={session})"); return ErrorResponse(id); } manager.Refresh(session); @@ -299,7 +308,7 @@ Use command 'help' to get a list of available commands"; return ErrorResponse(id, (user == null ? "badsession" : account == null ? "badacc" : "hasbal")); } manager.Refresh(session); - // Response example: "123.45{Sm9obiBEb2U=&Sm9obnMgQWNjb3VudA==&SmFuZSBEb2U=&SmFuZXMgQWNjb3VudA==&123.45&SGV5IHRoZXJlIQ==}" + // Response example: "123.45{Sm9obiBEb2U=&Sm9obnMgQWNjb3VudA==&SmFuZSBEb2U=&SmFuZXMgQWNjb3VudA==&123.45&SGV5IHRoZXJlIQ==" // Exmaple data: balance=123.45, Transaction{to="John Doe", toAccount="Johns Account", from="Jane Doe", fromAccount="Janes Account", amount=123.45, meta="Hey there!"} return GenerateResponse(id, account.ToString()); } @@ -352,7 +361,7 @@ Use command 'help' to get a list of available commands"; associations["session"] = sess; return GenerateResponse(id, sess); } - case "PassUPD": + case "PassUPD": // Update password for a certain user { if(!ParseDataPair(cmd[1], out var session, out var pass)) { @@ -367,6 +376,7 @@ Use command 'help' to get a list of available commands"; manager.Refresh(session); user.Salt = Convert.ToBase64String(random.GetBytes(Math.Abs(random.NextShort() % 60) + 20)); user.PasswordHash = user.ComputePass(pass); + db.UpdateUser(user); return GenerateResponse(id, true); } case "Verify": // Verifies server identity @@ -377,12 +387,16 @@ Use command 'help' to get a list of available commands"; while (!t.IsCompleted && !t.IsFaulted) System.Threading.Thread.Sleep(75); if (t.IsFaulted) { + Output.Fatal("Encountered error when getting RSA keyset:\n"+t.Exception.ToString()); return ErrorResponse(id, "server_err"); } byte[] ser; using (BitWriter collector = new BitWriter()) { + // Serialize public component of RSA keyset collector.PushArray(t.Result.Serialize()); + + // Prove server identity by signing a nonce with RSA collector.PushArray(t.Result.Encrypt(((BigInteger)bd.ReadUShort()).ToByteArray(), null, true)); ser = collector.Finalize(); } @@ -398,7 +412,7 @@ Use command 'help' to get a list of available commands"; return ErrorResponse(id, "unwn"); // Unknown request } - return null; + return null; // Don't respond to client }, (c, b) => // Called every time a client connects or disconnects (conn + dc with every command/request) { @@ -416,10 +430,11 @@ Use command 'help' to get a list of available commands"; CommandHandler commands = null; commands = new CommandHandler(4, " ", "", "- ") - .Append(new Command("help").SetAction(() => Output.Raw("Available commands:\n" + commands.GetString())), "Show this help menu") - .Append(new Command("stop").SetAction(() => running = false), "Stop server") - .Append(new Command("clear").SetAction(() => Output.Clear()), "Clear screen") - .Append(new Command("verb").WithParameter("level", 'l', Parameter.ParamType.STRING, true).SetAction((c, l) => + .Append(new Command("help") + .SetAction(() => Output.Raw("Available commands:\n" + commands.GetString())), "Show this help menu") // Show help menu + .Append(new Command("stop").SetAction(() => running = false), "Stop server") // Stop server + .Append(new Command("clear").SetAction(() => Output.Clear()), "Clear screen") // Clear screen + .Append(new Command("verb").WithParameter("level", 'l', Parameter.ParamType.STRING, true).SetAction((c, l) => // Set output verbosity { if (l.Count == 1) { @@ -431,7 +446,7 @@ Use command 'help' to get a list of available commands"; } Output.Raw($"Current verbosity level: {(verbosity<1?"FATAL":verbosity==1?"INFO":"DEBUG")}"); }), "Get or set verbosity level: DEBUG, INFO, FATAL (alternatively enter 0, 1 or 2 respectively)") - .Append(new Command("sess").WithParameter("sessionID", 'r', Parameter.ParamType.STRING, true).SetAction( + .Append(new Command("sess").WithParameter("sessionID", 'r', Parameter.ParamType.STRING, true).SetAction( // Display active sessions (c, l) => { StringBuilder builder = new StringBuilder(); manager.Update(); // Ensure that we don't show expired sessions (artifacts exist until it is necessary to remove them) @@ -447,7 +462,7 @@ Use command 'help' to get a list of available commands"; else --builder.Insert(0, "Active sessions:\n").Length; Output.Raw(builder); }), "List or refresh active client sessions") - .Append(new Command("list").WithParameter(Parameter.Flag('a')).SetAction( + .Append(new Command("list").WithParameter(Parameter.Flag('a')).SetAction( // Display users (c, l) => { bool filter = l.HasFlag('a'); StringBuilder builder = new StringBuilder(); @@ -459,9 +474,9 @@ Use command 'help' to get a list of available commands"; Output.Raw(builder); } }), "Show registered users. Add \"-a\" to only list admins") - .Append(new Command("admin") - .WithParameter("username", 'u', Parameter.ParamType.STRING) // Guaranteed to appear in the list passed in the action - .WithParameter("true/false", 's', Parameter.ParamType.BOOLEAN, true) // Might show up + .Append(new Command("admin") // Give or revoke administrator privileges for a certain user + .WithParameter("username", 'u', Parameter.ParamType.STRING) // Guaranteed to appear in the list passed in the action + .WithParameter("true/false", 's', Parameter.ParamType.BOOLEAN, true) // Might show up .SetAction( (c, l) => { @@ -497,12 +512,13 @@ Use command 'help' to get a list of available commands"; Output.Error("Unknown command. Enter 'help' for a list of supported commands.", true, false); } + // Stop the server (obviously) server.StopRunning(); } - // Handles unexpected console close events + // Handles unexpected console close events (kernel event hook for window close event) private delegate bool EventHandler(int eventType); - [DllImport("kernel32.dll", SetLastError = true)] + [DllImport("kernel32.dll")] private static extern bool SetConsoleCtrlHandler(EventHandler callback, bool add); } }