From d351ed86bec49c16a2d03cb76bd7e4ee40166d1a Mon Sep 17 00:00:00 2001 From: Moonbase Date: Sun, 7 Dec 2025 17:20:35 -0800 Subject: [PATCH] Add project files. --- QtCNETAPI/Dtos/Room/RoomDto.cs | 8 + QtCNETAPI/Dtos/User/UserDto.cs | 10 + QtCNETAPI/Dtos/User/UserInformationDto.cs | 17 + QtCNETAPI/Dtos/User/UserLoginDto.cs | 9 + QtCNETAPI/Dtos/User/UserPasswordResetDto.cs | 14 + QtCNETAPI/Dtos/User/UserRefreshLoginDto.cs | 8 + QtCNETAPI/Dtos/User/UserStatusDto.cs | 8 + .../Dtos/User/UserStockActionResultDto.cs | 8 + .../Dtos/User/UserUpdateInformationDto.cs | 11 + QtCNETAPI/Enums/GameStatus.cs | 13 + QtCNETAPI/Enums/NumberGuessResult.cs | 16 + QtCNETAPI/Enums/StoreItemType.cs | 8 + QtCNETAPI/Enums/TicTacToeSymbol.cs | 9 + QtCNETAPI/Events/ClientFunctionEventArgs.cs | 13 + QtCNETAPI/Events/DirectMessageEventArgs.cs | 16 + QtCNETAPI/Events/GuestUserJoinEventArgs.cs | 13 + QtCNETAPI/Events/RoomListEventArgs.cs | 14 + QtCNETAPI/Events/ServerConfigEventArgs.cs | 14 + .../Events/ServerConnectionClosedEventArgs.cs | 13 + .../ServerConnectionReconnectingEventArgs.cs | 13 + QtCNETAPI/Events/ServerMessageEventArgs.cs | 13 + QtCNETAPI/Models/Contact.cs | 16 + QtCNETAPI/Models/Message.cs | 10 + QtCNETAPI/Models/OwnedStoreItem.cs | 14 + QtCNETAPI/Models/RefreshToken.cs | 12 + QtCNETAPI/Models/Room.cs | 11 + QtCNETAPI/Models/ServerConfig.cs | 11 + QtCNETAPI/Models/ServiceResponse.cs | 9 + QtCNETAPI/Models/User.cs | 28 + QtCNETAPI/QtCNETAPI.csproj | 17 + QtCNETAPI/Schema/GameRoom.cs | 16 + QtCNETAPI/Schema/StoreItem.cs | 23 + QtCNETAPI/Schema/TicTacToeBoard.cs | 17 + QtCNETAPI/Schema/TicTacToeMove.cs | 10 + QtCNETAPI/Services/ApiService/ApiService.cs | 956 ++++++++++++++++++ QtCNETAPI/Services/ApiService/IApiService.cs | 58 ++ QtCNETAPI/Services/CredentialService.cs | 42 + .../Services/GatewayService/GatewayService.cs | 196 ++++ .../GatewayService/IGatewayService.cs | 161 +++ QtCNETAPI/Services/LoggingService.cs | 93 ++ qtcnet-client.slnx | 4 + .../Controls/BrandingControl.Designer.cs | 62 ++ qtcnet-client/Controls/BrandingControl.cs | 18 + qtcnet-client/Controls/BrandingControl.resx | 120 +++ .../Controls/ContactControl.Designer.cs | 96 ++ qtcnet-client/Controls/ContactControl.cs | 94 ++ qtcnet-client/Controls/ContactControl.resx | 120 +++ .../CurrentProfileControl.Designer.cs | 133 +++ .../Controls/CurrentProfileControl.cs | 33 + .../Controls/CurrentProfileControl.resx | 120 +++ .../Controls/LoginControl.Designer.cs | 158 +++ qtcnet-client/Controls/LoginControl.cs | 77 ++ qtcnet-client/Controls/LoginControl.resx | 120 +++ .../Controls/MainTabControl.Designer.cs | 284 ++++++ qtcnet-client/Controls/MainTabControl.cs | 68 ++ qtcnet-client/Controls/MainTabControl.resx | 515 ++++++++++ .../Controls/MessageControl.Designer.cs | 92 ++ qtcnet-client/Controls/MessageControl.cs | 37 + qtcnet-client/Controls/MessageControl.resx | 120 +++ .../Controls/RegisterControl.Designer.cs | 245 +++++ qtcnet-client/Controls/RegisterControl.cs | 74 ++ qtcnet-client/Controls/RegisterControl.resx | 120 +++ .../Controls/RoomControl.Designer.cs | 97 ++ qtcnet-client/Controls/RoomControl.cs | 61 ++ qtcnet-client/Controls/RoomControl.resx | 120 +++ qtcnet-client/Forms/ChatRoomForm.Designer.cs | 149 +++ qtcnet-client/Forms/ChatRoomForm.cs | 105 ++ qtcnet-client/Forms/ChatRoomForm.resx | 216 ++++ .../Forms/JackpotSpinForm.Designer.cs | 68 ++ qtcnet-client/Forms/JackpotSpinForm.cs | 83 ++ qtcnet-client/Forms/JackpotSpinForm.resx | 120 +++ qtcnet-client/Forms/MainForm.Designer.cs | 52 + qtcnet-client/Forms/MainForm.cs | 586 +++++++++++ qtcnet-client/Forms/MainForm.resx | 120 +++ qtcnet-client/Forms/ProfileForm.Designer.cs | 166 +++ qtcnet-client/Forms/ProfileForm.cs | 52 + qtcnet-client/Forms/ProfileForm.resx | 120 +++ qtcnet-client/Model/ClientConfig.cs | 19 + qtcnet-client/Program.cs | 98 ++ .../Properties/Resources.Designer.cs | 113 +++ qtcnet-client/Properties/Resources.resx | 136 +++ qtcnet-client/Resources/CurrencyIcon.png | Bin 0 -> 649 bytes qtcnet-client/Resources/DefaultPfp.jpg | Bin 0 -> 13151 bytes qtcnet-client/Resources/QtCNETIcon.png | Bin 0 -> 21541 bytes qtcnet-client/Resources/RoomsChatIcon.png | Bin 0 -> 1589 bytes qtcnet-client/Resources/SendIcon.png | Bin 0 -> 1936 bytes qtcnet-client/qtcnet-client.csproj | 37 + 87 files changed, 7176 insertions(+) create mode 100644 QtCNETAPI/Dtos/Room/RoomDto.cs create mode 100644 QtCNETAPI/Dtos/User/UserDto.cs create mode 100644 QtCNETAPI/Dtos/User/UserInformationDto.cs create mode 100644 QtCNETAPI/Dtos/User/UserLoginDto.cs create mode 100644 QtCNETAPI/Dtos/User/UserPasswordResetDto.cs create mode 100644 QtCNETAPI/Dtos/User/UserRefreshLoginDto.cs create mode 100644 QtCNETAPI/Dtos/User/UserStatusDto.cs create mode 100644 QtCNETAPI/Dtos/User/UserStockActionResultDto.cs create mode 100644 QtCNETAPI/Dtos/User/UserUpdateInformationDto.cs create mode 100644 QtCNETAPI/Enums/GameStatus.cs create mode 100644 QtCNETAPI/Enums/NumberGuessResult.cs create mode 100644 QtCNETAPI/Enums/StoreItemType.cs create mode 100644 QtCNETAPI/Enums/TicTacToeSymbol.cs create mode 100644 QtCNETAPI/Events/ClientFunctionEventArgs.cs create mode 100644 QtCNETAPI/Events/DirectMessageEventArgs.cs create mode 100644 QtCNETAPI/Events/GuestUserJoinEventArgs.cs create mode 100644 QtCNETAPI/Events/RoomListEventArgs.cs create mode 100644 QtCNETAPI/Events/ServerConfigEventArgs.cs create mode 100644 QtCNETAPI/Events/ServerConnectionClosedEventArgs.cs create mode 100644 QtCNETAPI/Events/ServerConnectionReconnectingEventArgs.cs create mode 100644 QtCNETAPI/Events/ServerMessageEventArgs.cs create mode 100644 QtCNETAPI/Models/Contact.cs create mode 100644 QtCNETAPI/Models/Message.cs create mode 100644 QtCNETAPI/Models/OwnedStoreItem.cs create mode 100644 QtCNETAPI/Models/RefreshToken.cs create mode 100644 QtCNETAPI/Models/Room.cs create mode 100644 QtCNETAPI/Models/ServerConfig.cs create mode 100644 QtCNETAPI/Models/ServiceResponse.cs create mode 100644 QtCNETAPI/Models/User.cs create mode 100644 QtCNETAPI/QtCNETAPI.csproj create mode 100644 QtCNETAPI/Schema/GameRoom.cs create mode 100644 QtCNETAPI/Schema/StoreItem.cs create mode 100644 QtCNETAPI/Schema/TicTacToeBoard.cs create mode 100644 QtCNETAPI/Schema/TicTacToeMove.cs create mode 100644 QtCNETAPI/Services/ApiService/ApiService.cs create mode 100644 QtCNETAPI/Services/ApiService/IApiService.cs create mode 100644 QtCNETAPI/Services/CredentialService.cs create mode 100644 QtCNETAPI/Services/GatewayService/GatewayService.cs create mode 100644 QtCNETAPI/Services/GatewayService/IGatewayService.cs create mode 100644 QtCNETAPI/Services/LoggingService.cs create mode 100644 qtcnet-client.slnx create mode 100644 qtcnet-client/Controls/BrandingControl.Designer.cs create mode 100644 qtcnet-client/Controls/BrandingControl.cs create mode 100644 qtcnet-client/Controls/BrandingControl.resx create mode 100644 qtcnet-client/Controls/ContactControl.Designer.cs create mode 100644 qtcnet-client/Controls/ContactControl.cs create mode 100644 qtcnet-client/Controls/ContactControl.resx create mode 100644 qtcnet-client/Controls/CurrentProfileControl.Designer.cs create mode 100644 qtcnet-client/Controls/CurrentProfileControl.cs create mode 100644 qtcnet-client/Controls/CurrentProfileControl.resx create mode 100644 qtcnet-client/Controls/LoginControl.Designer.cs create mode 100644 qtcnet-client/Controls/LoginControl.cs create mode 100644 qtcnet-client/Controls/LoginControl.resx create mode 100644 qtcnet-client/Controls/MainTabControl.Designer.cs create mode 100644 qtcnet-client/Controls/MainTabControl.cs create mode 100644 qtcnet-client/Controls/MainTabControl.resx create mode 100644 qtcnet-client/Controls/MessageControl.Designer.cs create mode 100644 qtcnet-client/Controls/MessageControl.cs create mode 100644 qtcnet-client/Controls/MessageControl.resx create mode 100644 qtcnet-client/Controls/RegisterControl.Designer.cs create mode 100644 qtcnet-client/Controls/RegisterControl.cs create mode 100644 qtcnet-client/Controls/RegisterControl.resx create mode 100644 qtcnet-client/Controls/RoomControl.Designer.cs create mode 100644 qtcnet-client/Controls/RoomControl.cs create mode 100644 qtcnet-client/Controls/RoomControl.resx create mode 100644 qtcnet-client/Forms/ChatRoomForm.Designer.cs create mode 100644 qtcnet-client/Forms/ChatRoomForm.cs create mode 100644 qtcnet-client/Forms/ChatRoomForm.resx create mode 100644 qtcnet-client/Forms/JackpotSpinForm.Designer.cs create mode 100644 qtcnet-client/Forms/JackpotSpinForm.cs create mode 100644 qtcnet-client/Forms/JackpotSpinForm.resx create mode 100644 qtcnet-client/Forms/MainForm.Designer.cs create mode 100644 qtcnet-client/Forms/MainForm.cs create mode 100644 qtcnet-client/Forms/MainForm.resx create mode 100644 qtcnet-client/Forms/ProfileForm.Designer.cs create mode 100644 qtcnet-client/Forms/ProfileForm.cs create mode 100644 qtcnet-client/Forms/ProfileForm.resx create mode 100644 qtcnet-client/Model/ClientConfig.cs create mode 100644 qtcnet-client/Program.cs create mode 100644 qtcnet-client/Properties/Resources.Designer.cs create mode 100644 qtcnet-client/Properties/Resources.resx create mode 100644 qtcnet-client/Resources/CurrencyIcon.png create mode 100644 qtcnet-client/Resources/DefaultPfp.jpg create mode 100644 qtcnet-client/Resources/QtCNETIcon.png create mode 100644 qtcnet-client/Resources/RoomsChatIcon.png create mode 100644 qtcnet-client/Resources/SendIcon.png create mode 100644 qtcnet-client/qtcnet-client.csproj diff --git a/QtCNETAPI/Dtos/Room/RoomDto.cs b/QtCNETAPI/Dtos/Room/RoomDto.cs new file mode 100644 index 0000000..2d5ced6 --- /dev/null +++ b/QtCNETAPI/Dtos/Room/RoomDto.cs @@ -0,0 +1,8 @@ +namespace QtCNETAPI.Dtos.Room +{ + public class RoomDto + { + public string Name { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = new DateTime(); + } +} diff --git a/QtCNETAPI/Dtos/User/UserDto.cs b/QtCNETAPI/Dtos/User/UserDto.cs new file mode 100644 index 0000000..0d915e6 --- /dev/null +++ b/QtCNETAPI/Dtos/User/UserDto.cs @@ -0,0 +1,10 @@ +namespace QtCNETAPI.Dtos.User +{ + public class UserDto + { + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public DateTime DateOfBirth { get; set; } = new DateTime(); + } +} diff --git a/QtCNETAPI/Dtos/User/UserInformationDto.cs b/QtCNETAPI/Dtos/User/UserInformationDto.cs new file mode 100644 index 0000000..d6c93a6 --- /dev/null +++ b/QtCNETAPI/Dtos/User/UserInformationDto.cs @@ -0,0 +1,17 @@ +namespace QtCNETAPI.Dtos.User +{ + public class UserInformationDto + { + public string Id { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string ProfilePicture { get; set; } = string.Empty; + public string Bio { get; set; } = string.Empty; + public string Role { get; set; } = string.Empty; + public DateTime DateOfBirth { get; set; } = new DateTime(); + public DateTime CreatedAt { get; set; } = new DateTime(); + public DateTime LastLogin { get; set; } = new DateTime(); + public int Status { get; set; } = 0; + public int CurrencyAmount { get; set; } = 0; + public int ProfileCosmeticId { get; set; } = 0; + } +} diff --git a/QtCNETAPI/Dtos/User/UserLoginDto.cs b/QtCNETAPI/Dtos/User/UserLoginDto.cs new file mode 100644 index 0000000..7a21de7 --- /dev/null +++ b/QtCNETAPI/Dtos/User/UserLoginDto.cs @@ -0,0 +1,9 @@ +namespace QtCNETAPI.Dtos.User +{ + public class UserLoginDto + { + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public bool RememberMe { get; set; } = false; + } +} diff --git a/QtCNETAPI/Dtos/User/UserPasswordResetDto.cs b/QtCNETAPI/Dtos/User/UserPasswordResetDto.cs new file mode 100644 index 0000000..3436077 --- /dev/null +++ b/QtCNETAPI/Dtos/User/UserPasswordResetDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Dtos.User +{ + public class UserPasswordResetDto + { + public string Token { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + } +} diff --git a/QtCNETAPI/Dtos/User/UserRefreshLoginDto.cs b/QtCNETAPI/Dtos/User/UserRefreshLoginDto.cs new file mode 100644 index 0000000..7933b1f --- /dev/null +++ b/QtCNETAPI/Dtos/User/UserRefreshLoginDto.cs @@ -0,0 +1,8 @@ +namespace QtCNETAPI.Dtos.User +{ + public class UserRefreshLoginDto + { + public string Email { get; set; } = string.Empty; + public string RefreshToken { get; set; } = string.Empty; + } +} diff --git a/QtCNETAPI/Dtos/User/UserStatusDto.cs b/QtCNETAPI/Dtos/User/UserStatusDto.cs new file mode 100644 index 0000000..3e628be --- /dev/null +++ b/QtCNETAPI/Dtos/User/UserStatusDto.cs @@ -0,0 +1,8 @@ +namespace QtCNETAPI.Dtos.User +{ + public class UserStatusDto + { + public string Id { get; set; } = string.Empty; + public int Status { get; set; } = 0; + } +} diff --git a/QtCNETAPI/Dtos/User/UserStockActionResultDto.cs b/QtCNETAPI/Dtos/User/UserStockActionResultDto.cs new file mode 100644 index 0000000..0645227 --- /dev/null +++ b/QtCNETAPI/Dtos/User/UserStockActionResultDto.cs @@ -0,0 +1,8 @@ +namespace QtCNETAPI.Dtos.User +{ + public class UserStockActionResultDto + { + public int StockAmount { get; set; } + public int CurrencyAmount { get; set; } + } +} diff --git a/QtCNETAPI/Dtos/User/UserUpdateInformationDto.cs b/QtCNETAPI/Dtos/User/UserUpdateInformationDto.cs new file mode 100644 index 0000000..ac0ab53 --- /dev/null +++ b/QtCNETAPI/Dtos/User/UserUpdateInformationDto.cs @@ -0,0 +1,11 @@ +namespace QtCNETAPI.Dtos.User +{ + public class UserUpdateInformationDto + { + public string Id { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string Bio { get; set; } = string.Empty; + public DateTime DateOfBirth { get; set; } = new DateTime(); + public int ProfileCosmeticId { get; set; } = 0; + } +} diff --git a/QtCNETAPI/Enums/GameStatus.cs b/QtCNETAPI/Enums/GameStatus.cs new file mode 100644 index 0000000..75ffddf --- /dev/null +++ b/QtCNETAPI/Enums/GameStatus.cs @@ -0,0 +1,13 @@ +namespace QtCNETAPI.Enums +{ + public enum GameStatus + { + WaitingForPlayer, + SelectingSymbol, + Ongoing, + P1Win, + P2Win, + NoWin, + PlayerDisconnected + } +} diff --git a/QtCNETAPI/Enums/NumberGuessResult.cs b/QtCNETAPI/Enums/NumberGuessResult.cs new file mode 100644 index 0000000..6fa3d20 --- /dev/null +++ b/QtCNETAPI/Enums/NumberGuessResult.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Enums +{ + public enum NumberGuessResult + { + Higher, + Lower, + Correct, + Incorrect + } +} diff --git a/QtCNETAPI/Enums/StoreItemType.cs b/QtCNETAPI/Enums/StoreItemType.cs new file mode 100644 index 0000000..f5ee7b7 --- /dev/null +++ b/QtCNETAPI/Enums/StoreItemType.cs @@ -0,0 +1,8 @@ +namespace QtCNETAPI.Enums +{ + public enum StoreItemType + { + ProfileCosmetic = 1, + ClientCosmetic = 2 + } +} diff --git a/QtCNETAPI/Enums/TicTacToeSymbol.cs b/QtCNETAPI/Enums/TicTacToeSymbol.cs new file mode 100644 index 0000000..b3d2b2c --- /dev/null +++ b/QtCNETAPI/Enums/TicTacToeSymbol.cs @@ -0,0 +1,9 @@ +namespace QtCNETAPI.Enums +{ + public enum TicTacToeSymbol + { + X, + O, + Blank + } +} diff --git a/QtCNETAPI/Events/ClientFunctionEventArgs.cs b/QtCNETAPI/Events/ClientFunctionEventArgs.cs new file mode 100644 index 0000000..53847ff --- /dev/null +++ b/QtCNETAPI/Events/ClientFunctionEventArgs.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Events +{ + public class ClientFunctionEventArgs : EventArgs + { + public required string Function { get; set; } + } +} diff --git a/QtCNETAPI/Events/DirectMessageEventArgs.cs b/QtCNETAPI/Events/DirectMessageEventArgs.cs new file mode 100644 index 0000000..581aef7 --- /dev/null +++ b/QtCNETAPI/Events/DirectMessageEventArgs.cs @@ -0,0 +1,16 @@ +using QtCNETAPI.Dtos.User; +using QtCNETAPI.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Events +{ + public class DirectMessageEventArgs : EventArgs + { + public required Message Message { get; set; } + public required UserInformationDto User { get; set; } + } +} diff --git a/QtCNETAPI/Events/GuestUserJoinEventArgs.cs b/QtCNETAPI/Events/GuestUserJoinEventArgs.cs new file mode 100644 index 0000000..3318a50 --- /dev/null +++ b/QtCNETAPI/Events/GuestUserJoinEventArgs.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Events +{ + public class GuestUserJoinEventArgs : EventArgs + { + public required string Username { get; set; } + } +} diff --git a/QtCNETAPI/Events/RoomListEventArgs.cs b/QtCNETAPI/Events/RoomListEventArgs.cs new file mode 100644 index 0000000..b259331 --- /dev/null +++ b/QtCNETAPI/Events/RoomListEventArgs.cs @@ -0,0 +1,14 @@ +using QtCNETAPI.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Events +{ + public class RoomListEventArgs : EventArgs + { + public required List UserList { get; set; } + } +} diff --git a/QtCNETAPI/Events/ServerConfigEventArgs.cs b/QtCNETAPI/Events/ServerConfigEventArgs.cs new file mode 100644 index 0000000..862d443 --- /dev/null +++ b/QtCNETAPI/Events/ServerConfigEventArgs.cs @@ -0,0 +1,14 @@ +using QtCNETAPI.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Events +{ + public class ServerConfigEventArgs : EventArgs + { + public required ServerConfig ServerConfig { get; set; } + } +} diff --git a/QtCNETAPI/Events/ServerConnectionClosedEventArgs.cs b/QtCNETAPI/Events/ServerConnectionClosedEventArgs.cs new file mode 100644 index 0000000..0c25439 --- /dev/null +++ b/QtCNETAPI/Events/ServerConnectionClosedEventArgs.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Events +{ + public class ServerConnectionClosedEventArgs : EventArgs + { + public Exception? Error { get; set; } + } +} diff --git a/QtCNETAPI/Events/ServerConnectionReconnectingEventArgs.cs b/QtCNETAPI/Events/ServerConnectionReconnectingEventArgs.cs new file mode 100644 index 0000000..dd43f65 --- /dev/null +++ b/QtCNETAPI/Events/ServerConnectionReconnectingEventArgs.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Events +{ + public class ServerConnectionReconnectingEventArgs : EventArgs + { + public Exception? Error { get; set; } + } +} diff --git a/QtCNETAPI/Events/ServerMessageEventArgs.cs b/QtCNETAPI/Events/ServerMessageEventArgs.cs new file mode 100644 index 0000000..de24b5e --- /dev/null +++ b/QtCNETAPI/Events/ServerMessageEventArgs.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Events +{ + public class ServerMessageEventArgs : EventArgs + { + public required string Message { get; set; } + } +} diff --git a/QtCNETAPI/Models/Contact.cs b/QtCNETAPI/Models/Contact.cs new file mode 100644 index 0000000..ebdcdde --- /dev/null +++ b/QtCNETAPI/Models/Contact.cs @@ -0,0 +1,16 @@ +namespace QtCNETAPI.Models +{ + public class Contact + { + public string Id { get; set; } = string.Empty; + public string OwnerId { get; set; } = string.Empty; + public string UserId { get; set; } = string.Empty; + public ContactStatus OwnerStatus { get; set; } = ContactStatus.AwaitingApprovalFromOther; + public ContactStatus UserStatus { get; set; } = ContactStatus.AwaitingApprovalFromSelf; + + public virtual User? Owner { get; } + public virtual User? User { get; } + + public enum ContactStatus { AwaitingApprovalFromOther = 0, AwaitingApprovalFromSelf = 1, Accepted = 2 } + } +} diff --git a/QtCNETAPI/Models/Message.cs b/QtCNETAPI/Models/Message.cs new file mode 100644 index 0000000..c6623aa --- /dev/null +++ b/QtCNETAPI/Models/Message.cs @@ -0,0 +1,10 @@ +namespace QtCNETAPI.Models +{ + public class Message + { + public string Id { get; set; } = string.Empty; + public string AuthorId { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = new DateTime(); + } +} diff --git a/QtCNETAPI/Models/OwnedStoreItem.cs b/QtCNETAPI/Models/OwnedStoreItem.cs new file mode 100644 index 0000000..f328563 --- /dev/null +++ b/QtCNETAPI/Models/OwnedStoreItem.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace QtCNETAPI.Models +{ + public class OwnedStoreItem + { + [Key] + public int Id { get; set; } + public string UserId { get; set; } = string.Empty; + public int StoreItemId { get; set; } + + public virtual User? User { get; } + } +} diff --git a/QtCNETAPI/Models/RefreshToken.cs b/QtCNETAPI/Models/RefreshToken.cs new file mode 100644 index 0000000..a0c247f --- /dev/null +++ b/QtCNETAPI/Models/RefreshToken.cs @@ -0,0 +1,12 @@ +namespace QtCNETAPI.Models +{ + public class RefreshToken + { + public string ID { get; set; } = string.Empty; + public string UserID { get; set; } = string.Empty; + public string Token { get; set; } = string.Empty; + public DateTime Expires { get; set; } + + public virtual User User { get; } = null!; + } +} diff --git a/QtCNETAPI/Models/Room.cs b/QtCNETAPI/Models/Room.cs new file mode 100644 index 0000000..6902eb6 --- /dev/null +++ b/QtCNETAPI/Models/Room.cs @@ -0,0 +1,11 @@ +namespace QtCNETAPI.Models +{ + public class Room + { + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string CreatorId { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = new DateTime(); + public int UserCount { get; set; } = 0; + } +} diff --git a/QtCNETAPI/Models/ServerConfig.cs b/QtCNETAPI/Models/ServerConfig.cs new file mode 100644 index 0000000..5d52143 --- /dev/null +++ b/QtCNETAPI/Models/ServerConfig.cs @@ -0,0 +1,11 @@ +namespace QtCNETAPI.Models +{ + public class ServerConfig + { + public string? Name { get; set; } + public string? Description { get; set; } + public string? AdminUserId { get; set; } + public bool IsDown { get; set; } + public string? IsDownMessage { get; set; } + } +} diff --git a/QtCNETAPI/Models/ServiceResponse.cs b/QtCNETAPI/Models/ServiceResponse.cs new file mode 100644 index 0000000..f1f8e4f --- /dev/null +++ b/QtCNETAPI/Models/ServiceResponse.cs @@ -0,0 +1,9 @@ +namespace QtCNETAPI.Models +{ + public class ServiceResponse + { + public T? Data { get; set; } + public bool Success { get; set; } = false; + public string Message { get; set; } = string.Empty; + } +} diff --git a/QtCNETAPI/Models/User.cs b/QtCNETAPI/Models/User.cs new file mode 100644 index 0000000..21cd0cc --- /dev/null +++ b/QtCNETAPI/Models/User.cs @@ -0,0 +1,28 @@ +namespace QtCNETAPI.Models +{ + public class User + { + public string Id { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string ProfilePicture { get; set; } = string.Empty; + public string Bio { get; set; } = string.Empty; + public string Role { get; set; } = string.Empty; + public string PasswordHash { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public bool IsEmailVerified { get; set; } = false; + public DateTime DateOfBirth { get; set; } + public DateTime CreatedAt { get; set; } + public int Status { get; set; } = 0; + public int CurrencyAmount { get; set; } = 0; + public int StockAmount { get; set; } = 0; + public DateTime LastCurrencySpin { get; set; } + public int ActiveProfileCosmetic { get; set; } = 0; + public string CurrentRoomId { get; set; } = string.Empty; + public DateTime LastLogin { get; set; } + + public virtual IEnumerable? RefreshTokens { get; } + public virtual IEnumerable? ContactsMade { get; } + public virtual IEnumerable? ContactsList { get; } + public virtual IEnumerable? OwnedStoreItems { get; } + } +} diff --git a/QtCNETAPI/QtCNETAPI.csproj b/QtCNETAPI/QtCNETAPI.csproj new file mode 100644 index 0000000..9ee6acb --- /dev/null +++ b/QtCNETAPI/QtCNETAPI.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/QtCNETAPI/Schema/GameRoom.cs b/QtCNETAPI/Schema/GameRoom.cs new file mode 100644 index 0000000..95f06de --- /dev/null +++ b/QtCNETAPI/Schema/GameRoom.cs @@ -0,0 +1,16 @@ +using QtCNETAPI.Enums; +using QtCNETAPI.Models; + +namespace QtCNETAPI.Schema +{ + public class GameRoom + { + public string Id { get; set; } = string.Empty; + public GameStatus Status { get; set; } + public TicTacToeBoard Board { get; set; } = new(); + public User? Player1 { get; set; } + public TicTacToeSymbol P1Symbol { get; set; } = TicTacToeSymbol.Blank; + public User? Player2 { get; set; } + public TicTacToeSymbol P2Symbol { get; set; } = TicTacToeSymbol.Blank; + } +} diff --git a/QtCNETAPI/Schema/StoreItem.cs b/QtCNETAPI/Schema/StoreItem.cs new file mode 100644 index 0000000..e6d837e --- /dev/null +++ b/QtCNETAPI/Schema/StoreItem.cs @@ -0,0 +1,23 @@ +using QtCNETAPI.Enums; +using System.Text.Json.Serialization; + +namespace QtCNETAPI.Schema +{ + public class StoreItem + { + [JsonPropertyName("Id")] + public int Id { get; set; } + [JsonPropertyName("Type")] + public StoreItemType Type { get; set; } + [JsonPropertyName("Price")] + public int Price { get; set; } + [JsonPropertyName("Name")] + public string Name { get; set; } = string.Empty; + [JsonPropertyName("Description")] + public string Description { get; set; } = string.Empty; + [JsonPropertyName("AssetUrl")] + public string AssetUrl { get; set; } = string.Empty; + [JsonPropertyName("ThumbnailUrl")] + public string ThumbnailUrl { get; set; } = string.Empty; + } +} diff --git a/QtCNETAPI/Schema/TicTacToeBoard.cs b/QtCNETAPI/Schema/TicTacToeBoard.cs new file mode 100644 index 0000000..fb7a5d9 --- /dev/null +++ b/QtCNETAPI/Schema/TicTacToeBoard.cs @@ -0,0 +1,17 @@ +using QtCNETAPI.Enums; + +namespace QtCNETAPI.Schema +{ + public class TicTacToeBoard + { + public TicTacToeSymbol Square1 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square2 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square3 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square4 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square5 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square6 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square7 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square8 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square9 { get; set; } = TicTacToeSymbol.Blank; + } +} diff --git a/QtCNETAPI/Schema/TicTacToeMove.cs b/QtCNETAPI/Schema/TicTacToeMove.cs new file mode 100644 index 0000000..3b62a91 --- /dev/null +++ b/QtCNETAPI/Schema/TicTacToeMove.cs @@ -0,0 +1,10 @@ +using QtCNETAPI.Models; + +namespace QtCNETAPI.Schema +{ + public class TicTacToeMove + { + public User User { get; set; } = new(); + public int Point { get; set; } + } +} diff --git a/QtCNETAPI/Services/ApiService/ApiService.cs b/QtCNETAPI/Services/ApiService/ApiService.cs new file mode 100644 index 0000000..aae4448 --- /dev/null +++ b/QtCNETAPI/Services/ApiService/ApiService.cs @@ -0,0 +1,956 @@ +using QtCNETAPI.Dtos.Room; +using QtCNETAPI.Dtos.User; +using QtCNETAPI.Enums; +using QtCNETAPI.Models; +using QtCNETAPI.Schema; +using RestSharp; +using System.IdentityModel.Tokens.Jwt; +using System.Text.Json; + +namespace QtCNETAPI.Services.ApiService +{ + public class ApiService : IApiService + { + private User? user; + private readonly RestClient _client; + + internal string? sessionToken; + + public event EventHandler? OnCurrentUserUpdate; + + public string? SessionToken + { + get { return sessionToken; } + set { sessionToken = value; } + } + + public string? ApiUri { get; private set; } + + public User? CurrentUser + { + get { return user; } + private set { user = value; } + } + + private readonly LoggingService _loggingService; + private readonly CredentialService _credentialService; + public ApiService(string apiUri, LoggingService loggingService, CredentialService credentialService) + { + ApiUri = apiUri; + + _client = new RestClient(ApiUri); + _loggingService = loggingService; + _credentialService = credentialService; + } + + public async Task> PingServerAsync() + { + var serviceResponse = new ServiceResponse(); + + try + { + var request = new RestRequest("general/ping"); + var response = await _client.GetAsync(request); + + if (response != null) + { + if (response != "Pong!") serviceResponse.Success = false; + else serviceResponse.Success = true; + } + else serviceResponse.Success = false; + } catch (HttpRequestException ex) + { + serviceResponse.Success = false; + serviceResponse.Message = ex.Message; + } + + return serviceResponse; + } + + public async Task>> GetAllUsersAsync() + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse>(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var request = new RestRequest("users/all") + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.GetAsync>>(request); + + if (response != null || response!.Data != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + else + { + serviceResponse.Success = false; + serviceResponse.Message = "API didn't respond with users."; + } + + return serviceResponse; + } + + public async Task>> GetOnlineUsersAsync() + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse>(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var request = new RestRequest("users/users-online") + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.GetAsync>>(request); + + if (response != null || response!.Data != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } else + { + serviceResponse.Success = false; + serviceResponse.Message = "API didn't respond with online users."; + } + + return serviceResponse; + } + + public async Task> GetUserInformationAsync(string id) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var request = new RestRequest($"users/user-info") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("id", id); + var response = await _client.GetAsync>(request); + + if(response != null || response!.Data != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } else + { + serviceResponse.Success = false; + serviceResponse.Message = "API did not return user information."; + } + + return serviceResponse; + } + + public async Task> UpdateUserInformationAsync(UserUpdateInformationDto request) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + var restRequest = new RestRequest("users/update") + .AddJsonBody(request) + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.PutAsync>(restRequest); + + if(response != null || response!.Data != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + + // anything that changes the user should tell the api service to set it again + await SetCurrentUser(); + OnCurrentUserUpdate?.Invoke(this, EventArgs.Empty); + } else + { + serviceResponse.Success = false; + serviceResponse.Message = "API never responded."; + } + + return serviceResponse; + } + + public async Task> UpdateUserProfilePic(string filePath) + { + var serviceResponse = new ServiceResponse(); + + try + { + var restRequest = new RestRequest($"users/upload-profile-pic") + .AddQueryParameter("userId", CurrentUser!.Id) + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddFile("file", filePath); + var response = await _client.PostAsync>(restRequest); + + if (response != null && response.Success != false) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + + // anything that changes the user should tell the api service to set it again + await SetCurrentUser(); + OnCurrentUserUpdate?.Invoke(this, EventArgs.Empty); + } + else + { + serviceResponse.Success = false; + serviceResponse.Data = "Upload Failed."; + } + } catch (JsonException) + { + serviceResponse.Success = false; + serviceResponse.Message = "Profile Pictures Can Only Be Less Then 3 MB In Size."; + } catch (HttpRequestException ex) + { + serviceResponse.Success = false; + serviceResponse.Data = ex.Message; + } + + return serviceResponse; + } + + public async Task> GetUserProfilePic(string userId) + { + var serviceResponse = new ServiceResponse(); + try + { + var restRequest = new RestRequest($"users/profile-pic/{userId}") + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.GetAsync(restRequest); + + if (response != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.RawBytes; + } + else + { + serviceResponse.Success = false; + serviceResponse.Message = "No Profile Picture Received."; + } + + return serviceResponse; + } catch (HttpRequestException ex) + { + serviceResponse.Success = false; + serviceResponse.Message = ex.Message; + return serviceResponse; + } + } + + public async Task> LoginAsync(UserLoginDto userLoginDto) + { + var serviceResponse = new ServiceResponse(); + + try + { + if (string.IsNullOrWhiteSpace(userLoginDto.Email) || string.IsNullOrWhiteSpace(userLoginDto.Password)) + { + serviceResponse.Success = false; + serviceResponse.Message = "Email or Password cannot be null."; + } + + var request = new RestRequest("auth/login") + .AddJsonBody(userLoginDto); + var response = await _client.PostAsync>(request); + + if (response != null) + { + if (response.Data != null && response.Success) + { + SessionToken = response.Data; + + var user = await SetCurrentUser(); + + serviceResponse.Success = true; + if (response.Message != null) serviceResponse.Message = response.Message; + serviceResponse.Data = response.Message; + } + else + { + serviceResponse.Success = false; + serviceResponse.Message = response.Message; + } + } + } catch (Exception ex) + { + serviceResponse.Success = false; + serviceResponse.Message = ex.Message; + } + + return serviceResponse; + } + + public async Task> ResendVerificationEmail(string email) + { + var serviceResponse = new ServiceResponse(); + + var restRequest = new RestRequest($"auth/resend-verification-email") + .AddQueryParameter("email", email); + var response = await _client.PostAsync>(restRequest); + + if (response != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + else + { + serviceResponse.Success = false; + serviceResponse.Message = "API never responded."; + } + + return serviceResponse; + } + + public async Task> SendPasswordResetEmail(string email) + { + var serviceResponse = new ServiceResponse(); + + var restRequest = new RestRequest($"auth/request-password-reset") + .AddQueryParameter("email", email); + var response = await _client.PostAsync>(restRequest); + + if (response != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + else + { + serviceResponse.Success = false; + serviceResponse.Message = "API never responded."; + } + + return serviceResponse; + } + + public async Task> ResetPassword(UserPasswordResetDto request) + { + var serviceResponse = new ServiceResponse(); + + var restRequest = new RestRequest($"auth/reset-password") + .AddJsonBody(request); + var response = await _client.PostAsync>(restRequest); + + if (response != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + else + { + serviceResponse.Success = false; + serviceResponse.Message = "API never responded."; + } + + return serviceResponse; + } + + public async Task SetCurrentUser() + { + var userRequest = new RestRequest("users/user-authorized") + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var userResponse = await _client.GetAsync>(userRequest); + + if (userResponse != null && userResponse.Success && userResponse.Data != null) + { + user = userResponse.Data; + + _loggingService.LogString($"Current User's Status Is {userResponse.Data.Status}"); + + return userResponse.Data; + } else + { + throw new NullReferenceException("Current User could not be set."); + } + } + + public async Task> RefreshLogin(string refreshToken) + { + var serviceResponse = new ServiceResponse(); + + try + { + if (string.IsNullOrEmpty(refreshToken)) + { + serviceResponse.Success = false; + serviceResponse.Message = "No Refresh Token Specified."; + } + + var request = new RestRequest("auth/refresh") + .AddQueryParameter("token", refreshToken); + var response = await _client.PostAsync>(request); + + if (response != null && response.Success != false) + { + SessionToken = response.Data; + + if (user == null) + { + var user = await SetCurrentUser(); + + serviceResponse.Success = true; + serviceResponse.Data = user; + } + + serviceResponse.Success = true; + serviceResponse.Data = CurrentUser; + } + else + { + serviceResponse.Success = false; + serviceResponse.Message = "API didn't respond with a session token."; + } + } catch (Exception ex) + { + serviceResponse.Success = false; + serviceResponse.Message = ex.Message; + } + + return serviceResponse; + } + + public async Task> RefreshSessionIfInvalid() + { + var tokenHandler = new JwtSecurityTokenHandler(); + var refToken = _credentialService.GetAccessToken(); // fuck CA1416, if this is being ran on linux it should just crash (theoretically) + + if (refToken == null) + { + // treat as session expired + return new ServiceResponse { Success = false, Message = "Refresh Token Not Found. Session Expired." }; + } + + JwtSecurityToken token = tokenHandler.ReadJwtToken(SessionToken); + + if(DateTime.Compare(DateTime.UtcNow, token.ValidTo) > 0) + { + var result = await RefreshLogin(refToken); + + if (result == null || result.Success == false) + { + return new ServiceResponse { Success = false, Message = "Session Expired." }; // logging in again should overwrite old token + } else return new ServiceResponse { Success = true, Data = refToken }; + } else return new ServiceResponse { Success = true, Data = refToken }; + } + + public async Task> RegisterAsync(UserDto userDto) + { + var serviceResponse = new ServiceResponse(); + + if (string.IsNullOrEmpty(userDto.Username) || string.IsNullOrEmpty(userDto.Password) || string.IsNullOrEmpty(userDto.Email)) + { + serviceResponse.Success = false; + serviceResponse.Message = "Incomplete UserDto."; + } + + var request = new RestRequest("auth/register") + .AddJsonBody(userDto); + var response = await _client.PostAsync>(request); + + if (response != null || response!.Success == true) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } else + { + serviceResponse.Success = false; + serviceResponse.Message = "API never responded with created user."; + } + + return serviceResponse; + } + + public async Task> CreateRoomAsync(RoomDto request) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest($"rooms/create-room?userId={user!.Id}") + .AddJsonBody(request) + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.PostAsync>(restRequest); + + if(response != null || response!.Data != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } else + { + serviceResponse.Success = false; + serviceResponse.Message = "API never responded with created room."; + } + + return serviceResponse; + } + + public async Task>> GetAllRoomsAsync() + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse>(); + + if(SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var request = new RestRequest("rooms/get-all-rooms") + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.GetAsync>>(request); + + if (response != null || response!.Data != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + else + { + serviceResponse.Success = false; + serviceResponse.Message = "API never responded with rooms."; + } + + return serviceResponse; + } + + public async Task> DeleteRoomAsync(string roomId) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if(SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var request = new RestRequest($"rooms/delete-room?roomId={roomId}") + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.DeleteAsync>(request); + + if (response != null || response!.Data != null) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } else + { + serviceResponse.Success = false; + serviceResponse.Message = "API never responded."; + } + + return serviceResponse; + } + + public async Task>> GetCurrentUserContacts() + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse>(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var request = new RestRequest("contacts/get-user-contacts") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddJsonBody(CurrentUser!); + var response = await _client.GetAsync>>(request); + + if (response == null) + { + serviceResponse.Success = false; + serviceResponse.Message = "API didn't respond."; + + return serviceResponse; + } + + if (response.Message == "User Has No Contacts.") + { + serviceResponse.Success = true; + serviceResponse.Message = "No Contacts Found."; + } + + if (response.Success == true) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task> AddContactToCurrentUser(string userId) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("contacts/add-contact") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("ownerId", CurrentUser!.Id) + .AddQueryParameter("userId", userId); + var response = await _client.PostAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task> AcceptContactRequest(string userId) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("contacts/approve-contact") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("ownerId", CurrentUser!.Id) + .AddQueryParameter("userId", userId); + var response = await _client.PostAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task> RemoveContactFromCurrentUser(string userId) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("contacts/remove-contact") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("userId", userId); + var response = await _client.DeleteAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task> AddCurrencyToCurrentUser(int amount, bool isSpinClaim) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("users/update-user-currency") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("amount", amount) + .AddQueryParameter("isSpinClaim", isSpinClaim); + var response = await _client.PostAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + + // anything that changes the user should tell the api service to set it again + await SetCurrentUser(); + OnCurrentUserUpdate?.Invoke(this, EventArgs.Empty); + } + + return serviceResponse; + } + + public async Task> GetCurrentStockPrice() + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("games/stock-market/current-price") + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.GetAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task> BuyStock(int amount) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("games/stock-market/buy-stock") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("amount", amount); + var response = await _client.PostAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + + // anything that changes the user should tell the api service to set it again + await SetCurrentUser(); + OnCurrentUserUpdate?.Invoke(this, EventArgs.Empty); + } + + return serviceResponse; + } + + public async Task> SellStock(int amount) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("games/stock-market/sell-stock") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("amount", amount); + var response = await _client.PostAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + + // anything that changes the user should tell the api service to set it again + await SetCurrentUser(); + OnCurrentUserUpdate?.Invoke(this, EventArgs.Empty); + } + + return serviceResponse; + } + + public async Task> GetRandomNumber() + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("games/number-guess/get-number") + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.GetAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task> GuessRandomNumber(int original, int guess) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("games/number-guess/guess-number") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("original", original) + .AddQueryParameter("guess", guess); + var response = await _client.GetAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task>> GetStoreItems() + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse>(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("store/all-items") + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.GetAsync>>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task> GetStoreItem(int id) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("store/item") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("id", id); + var response = await _client.GetAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task> BuyStoreItem(int id) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("store/buy-item") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("id", id); + var response = await _client.PostAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + + // anything that changes the user should tell the api service to set it again + await SetCurrentUser(); + OnCurrentUserUpdate?.Invoke(this, EventArgs.Empty); + } + + return serviceResponse; + } + + public async Task>> GetOwnedStoreItems() + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse>(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("store/bought-items") + .AddHeader("Authorization", $"Bearer {SessionToken}"); + var response = await _client.GetAsync>>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task> GetOwnedStoreItem(int id) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("store/bought-item") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("id", id); + var response = await _client.GetAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + + public async Task> DeleteUserById(string id) + { + await RefreshSessionIfInvalid(); + + var serviceResponse = new ServiceResponse(); + + if (SessionToken == null) throw new NullReferenceException("Function Was Called Before A Session Was Made."); + + var restRequest = new RestRequest("users/delete-user") + .AddHeader("Authorization", $"Bearer {SessionToken}") + .AddQueryParameter("id", id); + var response = await _client.DeleteAsync>(restRequest); + + if (response == null) { serviceResponse.Success = false; serviceResponse.Message = "API did not respond."; return serviceResponse; } + + if (response.Success) + { + serviceResponse.Success = true; + serviceResponse.Data = response.Data; + } + + return serviceResponse; + } + } +} diff --git a/QtCNETAPI/Services/ApiService/IApiService.cs b/QtCNETAPI/Services/ApiService/IApiService.cs new file mode 100644 index 0000000..f88c57f --- /dev/null +++ b/QtCNETAPI/Services/ApiService/IApiService.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Mvc; +using QtCNETAPI.Dtos.User; +using QtCNETAPI.Dtos.Room; +using QtCNETAPI.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using QtCNETAPI.Enums; +using QtCNETAPI.Schema; + +namespace QtCNETAPI.Services.ApiService +{ + public interface IApiService + { + public string? SessionToken { get; set; } + public string? ApiUri { get; } + public User? CurrentUser { get; } + + public event EventHandler? OnCurrentUserUpdate; + + public Task> PingServerAsync(); + public Task>> GetOnlineUsersAsync(); + public Task>> GetAllUsersAsync(); + public Task> DeleteUserById(string id); + public Task> LoginAsync(UserLoginDto userLoginDto); + public Task> ResendVerificationEmail(string email); + public Task> SendPasswordResetEmail(string email); + public Task> ResetPassword(UserPasswordResetDto request); + public Task> RefreshLogin(string refreshToken); + public Task> RefreshSessionIfInvalid(); + public Task SetCurrentUser(); + public Task> RegisterAsync(UserDto userDto); + public Task> GetUserInformationAsync(string id); + public Task> UpdateUserInformationAsync(UserUpdateInformationDto request); + public Task> UpdateUserProfilePic(string filePath); + public Task> GetUserProfilePic(string userId); + public Task> CreateRoomAsync(RoomDto request); + public Task> DeleteRoomAsync(string roomId); + public Task>> GetAllRoomsAsync(); + public Task>> GetCurrentUserContacts(); + public Task> AddContactToCurrentUser(string userId); + public Task> AcceptContactRequest(string userId); + public Task> RemoveContactFromCurrentUser(string userId); + public Task> AddCurrencyToCurrentUser(int amount, bool isSpinClaim); + public Task> GetCurrentStockPrice(); + public Task> BuyStock(int amount); + public Task> SellStock(int amount); + public Task> GetRandomNumber(); + public Task> GuessRandomNumber(int original, int guess); + public Task>> GetStoreItems(); + public Task> GetStoreItem(int id); + public Task> BuyStoreItem(int id); + public Task>> GetOwnedStoreItems(); + public Task> GetOwnedStoreItem(int id); + } +} diff --git a/QtCNETAPI/Services/CredentialService.cs b/QtCNETAPI/Services/CredentialService.cs new file mode 100644 index 0000000..22c8282 --- /dev/null +++ b/QtCNETAPI/Services/CredentialService.cs @@ -0,0 +1,42 @@ +using Meziantou.Framework.Win32; + +namespace QtCNETAPI.Services +{ + public class CredentialService() + { + /* + + * NOTE * + This does not work on other platforms such as Linux or macOS. + I will probably recode the legacy way of doing this for those other platforms. + + */ + + public void SaveAccessToken(string username, string accessToken) + { + string applicationName = "QtC.NET"; + if (System.Diagnostics.Debugger.IsAttached) applicationName = "QtC.NET.Development"; + + CredentialManager.WriteCredential(applicationName, username, accessToken, $"Access Token For User {username} On QtC.NET", CredentialPersistence.LocalMachine); + } + + public void DeleteAccessToken() + { + string applicationName = "QtC.NET"; + if (System.Diagnostics.Debugger.IsAttached) applicationName = "QtC.NET.Development"; + + CredentialManager.DeleteCredential(applicationName); + } + + public string? GetAccessToken() + { + string applicationName = "QtC.NET"; + if (System.Diagnostics.Debugger.IsAttached) applicationName = "QtC.NET.Development"; + + var credential = CredentialManager.ReadCredential(applicationName); + if (credential == null) return null; + + return credential.Password; + } + } +} diff --git a/QtCNETAPI/Services/GatewayService/GatewayService.cs b/QtCNETAPI/Services/GatewayService/GatewayService.cs new file mode 100644 index 0000000..b58aea7 --- /dev/null +++ b/QtCNETAPI/Services/GatewayService/GatewayService.cs @@ -0,0 +1,196 @@ +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; +using QtCNETAPI.Dtos.User; +using QtCNETAPI.Events; +using QtCNETAPI.Models; +using QtCNETAPI.Services.ApiService; + +namespace QtCNETAPI.Services.GatewayService +{ + public class GatewayService : IGatewayService, IAsyncDisposable + { + public Room? CurrentRoom { get; private set; } + public HubConnection? HubConnection { get; private set; } + public string? GWUri { get; private set; } + + + public event EventHandler? OnRoomMessageReceived; + public event EventHandler? OnRoomUserListReceived; + public event EventHandler? OnGuestUserJoin; + public event EventHandler? OnRefreshUserListsReceived; + public event EventHandler? OnRefreshRoomListReceived; + public event EventHandler? OnRoomDeleted; + public event EventHandler? OnRefreshContactsListReceived; + public event EventHandler? OnClientFunctionReceived; + public event EventHandler? OnDirectMessageReceived; + public event EventHandler? OnServerConfigReceived; + public event EventHandler? OnServerDisconnect; + public event EventHandler? OnServerReconnecting; + public event EventHandler? OnServerReconnected; + public event EventHandler? OnUserForceLogout; + + private IApiService _apiService; + private LoggingService _loggingService; + public GatewayService(string gwUri, IApiService apiService, LoggingService loggingService) + { + GWUri = gwUri; + _apiService = apiService; + _loggingService = loggingService; + } + + public async Task StartAsync() + { + // build connection + var gwConBuilder = new HubConnectionBuilder() + .WithAutomaticReconnect() + .ConfigureLogging((builder) => + { + builder.AddProvider(new LoggingServiceProvider(_loggingService)); + + if (System.Diagnostics.Debugger.IsAttached) builder.SetMinimumLevel(LogLevel.Debug); + else builder.SetMinimumLevel(LogLevel.Error); + }) + .WithUrl(GWUri!, options => + { + options.AccessTokenProvider = async () => + { + // this should hopefully refresh the session every time the gateway connection is used to prevent connection aborts + await _apiService.RefreshSessionIfInvalid(); + return _apiService.SessionToken; + }; + }) + .WithStatefulReconnect(); + HubConnection = gwConBuilder.Build(); + + // register events + HubConnection.On("RoomMessage", (serverMessage) => OnRoomMessageReceived?.Invoke(this, new ServerMessageEventArgs { Message = serverMessage })); + HubConnection.On("cf", (function) => OnClientFunctionReceived?.Invoke(this, new ClientFunctionEventArgs { Function = function })); + HubConnection.On("ReceiveDirectMessage", (message, user) => OnDirectMessageReceived?.Invoke(this, new DirectMessageEventArgs { Message = message, User = user })); + HubConnection.On("RefreshUserLists", () => OnRefreshUserListsReceived?.Invoke(this, EventArgs.Empty)); + HubConnection.On("RefreshRoomList", () => OnRefreshRoomListReceived?.Invoke(this, EventArgs.Empty)); + HubConnection.On("RefreshContactsList", () => OnRefreshContactsListReceived?.Invoke(this, EventArgs.Empty)); + HubConnection.On("ReceiveServerConfig", (serverConfig) => OnServerConfigReceived?.Invoke(this, new ServerConfigEventArgs { ServerConfig = serverConfig })); + HubConnection.On>("RoomUserList", (userList) => OnRoomUserListReceived?.Invoke(this, new RoomListEventArgs { UserList = userList })); + HubConnection.On("GuestJoin", (username) => OnGuestUserJoin?.Invoke(this, new GuestUserJoinEventArgs { Username = username })); + HubConnection.On("RoomDeleted", () => OnRoomDeleted?.Invoke(this, EventArgs.Empty)); + HubConnection.On("ForceSignOut", () => OnUserForceLogout?.Invoke(this, EventArgs.Empty)); + + HubConnection.Closed += HubConnection_Closed; + HubConnection.Reconnecting += HubConnection_Reconnecting; + HubConnection.Reconnected += HubConnection_Reconnected; + + // start connection + try + { + await HubConnection.StartAsync(); + } + catch (HttpRequestException ex) + { + _loggingService.LogString($"Unable To Connect To SignalR.\n{ex.Message}\n{ex.StackTrace}"); + return; + } + + // ensure current user is up to date (particularly status) + await _apiService.SetCurrentUser(); + } + + public async Task StopAsync() + { + if (HubConnection != null && HubConnection.State == HubConnectionState.Connected) + { + await HubConnection.StopAsync(); + } + } + + async ValueTask IAsyncDisposable.DisposeAsync() + { + if (HubConnection != null && HubConnection.State == HubConnectionState.Disconnected) + { + await HubConnection.DisposeAsync(); + + HubConnection = null; + CurrentRoom = null; + } + } + + public async Task DisposeAsync() + { + if (HubConnection != null && HubConnection.State == HubConnectionState.Disconnected) + { + await HubConnection.DisposeAsync(); + + HubConnection = null; + CurrentRoom = null; + } + } + + public async Task JoinRoomAsync(Room? room = null) + { + if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); + + // assume user is trying to join lobby if room is null (does not have db entry) + room ??= new Room + { + Id = "LOBBY", + Name = "Lobby" + }; + + if (CurrentRoom != null) await HubConnection.SendAsync("LeaveRoom", _apiService.CurrentUser, CurrentRoom); + await HubConnection.SendAsync("JoinRoom", _apiService.CurrentUser, room); + CurrentRoom = room; + } + + public async Task LeaveRoomAsync() + { + if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); + + if (CurrentRoom != null) await HubConnection.SendAsync("LeaveRoom", _apiService.CurrentUser, CurrentRoom); + CurrentRoom = null; + } + + public async Task PostMessageAsync(Message message) + { + if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); + + await HubConnection.SendAsync("SendMessage", _apiService.CurrentUser, message, CurrentRoom); + } + + public async Task SendDirectMessageAsync(UserInformationDto user, Message message) + { + await _apiService.RefreshSessionIfInvalid(); + + if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); + + await HubConnection.SendAsync("SendDirectMessage", _apiService.CurrentUser, user, message); + } + + public async Task UpdateStatus(int status) + { + if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); + + await HubConnection.SendAsync("UpdateStatus", _apiService.CurrentUser, status); + + // anything that changes the user should tell the api service to set it again + await _apiService.SetCurrentUser(); + } + + + private Task HubConnection_Closed(Exception? arg) + { + OnServerDisconnect?.Invoke(this, new ServerConnectionClosedEventArgs { Error = arg }); + return Task.CompletedTask; + } + + private Task HubConnection_Reconnecting(Exception? arg) + { + OnServerReconnecting?.Invoke(this, new ServerConnectionReconnectingEventArgs { Error = arg }); + return Task.CompletedTask; + } + + private Task HubConnection_Reconnected(string? arg) + { + OnServerReconnected?.Invoke(this, EventArgs.Empty); + return Task.CompletedTask; + } + } +} diff --git a/QtCNETAPI/Services/GatewayService/IGatewayService.cs b/QtCNETAPI/Services/GatewayService/IGatewayService.cs new file mode 100644 index 0000000..233ec21 --- /dev/null +++ b/QtCNETAPI/Services/GatewayService/IGatewayService.cs @@ -0,0 +1,161 @@ +using Microsoft.AspNetCore.SignalR.Client; +using QtCNETAPI.Dtos.User; +using QtCNETAPI.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace QtCNETAPI.Services.GatewayService +{ + public interface IGatewayService + { + // VARIABLES + + /// + /// The Current Room The Current User Is In + /// + public Room? CurrentRoom { get; } + + /// + /// The Current Connection To The Gateway + /// + public HubConnection? HubConnection { get; } + /// + /// The URI in which the client will try to use for SignalR requests. + /// + public string? GWUri { get; } + + // FUNCTIONS + + /// + /// The Function Used To Connect To The Gateway Server Asynchronously + /// + /// + public Task StartAsync(); + + /// + /// Stops The Connection To The Gateway Server + /// + /// + public Task StopAsync(); + + /// + /// Disposes Of The Gateway Connection And Clears Other Variables + /// + /// + public Task DisposeAsync(); + + /// + /// Joins The Current User To A Room On The Server + /// + /// Room To Join + /// + /// Thrown if the function is called before the connection is established. + public Task JoinRoomAsync(Room? room = null); + + /// + /// Leaves The Current Room The Current User Is In + /// + /// + public Task LeaveRoomAsync(); + + /// + /// Posts A Message To Whatever Room The User Is In + /// + /// Message To Post + /// + /// Thrown if the function is called before the connection is established. + public Task PostMessageAsync(Message message); + + /// + /// Sends A Direct Message To The Specified User + /// + /// The User You Wish To DM + /// Yourself + /// + /// + public Task SendDirectMessageAsync(UserInformationDto user, Message message); + + /// + /// Updates The Status For The Current User + /// + /// The Status You Want To Set On The Current User + /// + public Task UpdateStatus(int status); + + // EVENTS + + /// + /// When A Room Message Is Received, This Event Fires + /// + public event EventHandler OnRoomMessageReceived; + + /// + /// Fires When The User List For A Room Is Received + /// + public event EventHandler OnRoomUserListReceived; + + /// + /// Fires When The Room The User Is In Gets Deleted By An Admin + /// + public event EventHandler OnRoomDeleted; + + /// + /// Fires When A Guest User Joins Your Room + /// + public event EventHandler OnGuestUserJoin; + + /// + /// When A Client Function/Event Is Received, This Event Fires + /// + public event EventHandler OnClientFunctionReceived; + + /// + /// When The Client Received A DM, This Event Fires + /// + public event EventHandler OnDirectMessageReceived; + + /// + /// Fires When The Client Receives The Request To Refresh Its User List + /// + public event EventHandler OnRefreshUserListsReceived; + + /// + /// Fires When The Client Receives The Request To Refresh Its Room List + /// + public event EventHandler OnRefreshRoomListReceived; + + /// + /// Fires When The Client Receives The Request To Refresh Its Contacts List + /// + public event EventHandler OnRefreshContactsListReceived; + + /// + /// When The Server Config Is Received, This Event Fires + /// + public event EventHandler OnServerConfigReceived; + + /// + /// When The Connection To The Gateway Is Lost, This Event Fires + /// + public event EventHandler OnServerDisconnect; + + /// + /// When The Connection Attempts To Reconnect, This Event Fires + /// + public event EventHandler OnServerReconnecting; + + /// + /// When the Connection Reconnects, This Event Fires + /// + public event EventHandler OnServerReconnected; + + /// + /// Fires When The Current User Is Signed Out By The Server + /// + public event EventHandler OnUserForceLogout; + } +} diff --git a/QtCNETAPI/Services/LoggingService.cs b/QtCNETAPI/Services/LoggingService.cs new file mode 100644 index 0000000..4ae7c88 --- /dev/null +++ b/QtCNETAPI/Services/LoggingService.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Abstractions; + +namespace QtCNETAPI.Services +{ + public class LoggingService : IDisposable, ILogger + { + private DateTime LogDate { get; set; } + private string LogFilePath { get; set; } + private StreamWriter LogFile { get; set; } + public LoggingService() + { + LogDate = DateTime.Now; + LogFilePath = $"./Logs/QtCClientLog_{LogDate:ddMMyyy-hhmm}.log"; + + // create log file + + if (!Directory.Exists("./Logs")) Directory.CreateDirectory("./Logs"); + LogFile = new StreamWriter(File.Create(LogFilePath)); + + Debug.WriteLine($"Log File Created At {LogFilePath}"); + } + + public void LogString(string message) + { + try + { + Debug.WriteLine($"({DateTime.Now.ToLocalTime():hh:mm}) {message}"); + LogFile.WriteLine($"({DateTime.Now.ToLocalTime():hh:mm}) {message}"); + } catch (ObjectDisposedException) + { + } + } + + public void LogModel(T model) + { + try + { + // serialize the model as json + string modelSerialized = JsonSerializer.Serialize(model, options: new JsonSerializerOptions { WriteIndented = true }); + + // log it + Debug.WriteLine($"({DateTime.Now.ToLocalTime():hh:mm}) {modelSerialized}"); + LogFile.WriteLine($"({DateTime.Now.ToLocalTime():hh:mm}) {modelSerialized}"); + } catch (ObjectDisposedException) + { + } + } + + public void Dispose() + { + LogFile.WriteLine("--- END OF LOG ---"); + LogFile.Close(); + LogFile.Dispose(); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + try + { + // format message + string message = $"({DateTime.Now.ToLocalTime():hh:mm}) [{logLevel}] {formatter(state, exception)}"; + + // log it + Debug.WriteLine(message); + LogFile.WriteLine(message); + } catch (ObjectDisposedException) + { + } + } + + public bool IsEnabled(LogLevel logLevel) => true; + public IDisposable? BeginScope(TState state) where TState : notnull => default!; + } + + public class LoggingServiceProvider(LoggingService? loggingService = null) : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) + { + if (loggingService != null) return loggingService; + else return new LoggingService(); + } + + public void Dispose() { } + } +} diff --git a/qtcnet-client.slnx b/qtcnet-client.slnx new file mode 100644 index 0000000..f633b39 --- /dev/null +++ b/qtcnet-client.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/qtcnet-client/Controls/BrandingControl.Designer.cs b/qtcnet-client/Controls/BrandingControl.Designer.cs new file mode 100644 index 0000000..e5b472a --- /dev/null +++ b/qtcnet-client/Controls/BrandingControl.Designer.cs @@ -0,0 +1,62 @@ +namespace qtcnet_client.Controls +{ + partial class BrandingControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + pictureBox1 = new PictureBox(); + ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); + SuspendLayout(); + // + // pictureBox1 + // + pictureBox1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + pictureBox1.Image = Properties.Resources.QtCNETIcon; + pictureBox1.Location = new Point(0, 0); + pictureBox1.Name = "pictureBox1"; + pictureBox1.Size = new Size(122, 110); + pictureBox1.SizeMode = PictureBoxSizeMode.Zoom; + pictureBox1.TabIndex = 0; + pictureBox1.TabStop = false; + // + // BrandingControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.Transparent; + Controls.Add(pictureBox1); + Name = "BrandingControl"; + Size = new Size(122, 110); + ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit(); + ResumeLayout(false); + } + + #endregion + + private PictureBox pictureBox1; + } +} diff --git a/qtcnet-client/Controls/BrandingControl.cs b/qtcnet-client/Controls/BrandingControl.cs new file mode 100644 index 0000000..b4b8c19 --- /dev/null +++ b/qtcnet-client/Controls/BrandingControl.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Text; +using System.Windows.Forms; + +namespace qtcnet_client.Controls +{ + public partial class BrandingControl : UserControl + { + public BrandingControl() + { + InitializeComponent(); + } + } +} diff --git a/qtcnet-client/Controls/BrandingControl.resx b/qtcnet-client/Controls/BrandingControl.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/qtcnet-client/Controls/BrandingControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/qtcnet-client/Controls/ContactControl.Designer.cs b/qtcnet-client/Controls/ContactControl.Designer.cs new file mode 100644 index 0000000..028263f --- /dev/null +++ b/qtcnet-client/Controls/ContactControl.Designer.cs @@ -0,0 +1,96 @@ +namespace qtcnet_client.Controls +{ + partial class ContactControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + pbProfilePic = new PictureBox(); + lblUsername = new Label(); + lblStatus = new Label(); + ((System.ComponentModel.ISupportInitialize)pbProfilePic).BeginInit(); + SuspendLayout(); + // + // pbProfilePic + // + pbProfilePic.Image = Properties.Resources.DefaultPfp; + pbProfilePic.Location = new Point(3, 3); + pbProfilePic.Name = "pbProfilePic"; + pbProfilePic.Size = new Size(37, 35); + pbProfilePic.SizeMode = PictureBoxSizeMode.Zoom; + pbProfilePic.TabIndex = 0; + pbProfilePic.TabStop = false; + // + // lblUsername + // + lblUsername.AutoSize = true; + lblUsername.Font = new Font("Segoe UI", 8F, FontStyle.Bold, GraphicsUnit.Point, 0); + lblUsername.ForeColor = Color.Black; + lblUsername.Location = new Point(44, 8); + lblUsername.Name = "lblUsername"; + lblUsername.Size = new Size(59, 13); + lblUsername.TabIndex = 1; + lblUsername.Text = "Username"; + lblUsername.DoubleClick += lblUsername_DoubleClick; + lblUsername.MouseLeave += lblUsername_MouseLeave; + lblUsername.MouseHover += lblUsername_MouseHover; + // + // lblStatus + // + lblStatus.AutoSize = true; + lblStatus.Font = new Font("Segoe UI Semibold", 7F, FontStyle.Bold | FontStyle.Italic, GraphicsUnit.Point, 0); + lblStatus.ForeColor = SystemColors.AppWorkspace; + lblStatus.Location = new Point(46, 21); + lblStatus.Name = "lblStatus"; + lblStatus.Size = new Size(40, 12); + lblStatus.TabIndex = 2; + lblStatus.Text = "\"Status\""; + // + // ContactControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.Transparent; + Controls.Add(lblUsername); + Controls.Add(lblStatus); + Controls.Add(pbProfilePic); + DoubleBuffered = true; + Name = "ContactControl"; + Size = new Size(150, 43); + Load += ContactControl_Load; + Paint += ContactControl_Paint; + ((System.ComponentModel.ISupportInitialize)pbProfilePic).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private PictureBox pbProfilePic; + private Label lblUsername; + private Label lblStatus; + } +} diff --git a/qtcnet-client/Controls/ContactControl.cs b/qtcnet-client/Controls/ContactControl.cs new file mode 100644 index 0000000..cef184d --- /dev/null +++ b/qtcnet-client/Controls/ContactControl.cs @@ -0,0 +1,94 @@ +using qtcnet_client.Properties; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Text; +using System.Windows.Forms; + +namespace qtcnet_client.Controls +{ + public partial class ContactControl : UserControl + { + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public string UserId { get; set; } = string.Empty; // only for distinction + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string Username { get; set; } = "Username"; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string TextStatus { get; set; } = "Status"; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public int Status { get; set; } = 0; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public Image ProfilePic { get; set; } = Resources.DefaultPfp; + + public event EventHandler? OnContactDoubleClicked; + + bool _isHovering = false; + public ContactControl() + { + InitializeComponent(); + } + + private void ContactControl_Paint(object sender, PaintEventArgs e) + { + Graphics g = e.Graphics; + Rectangle bgBounds = ClientRectangle; + + Color statusColor = Color.Transparent; + switch (Status) + { + case 0: + statusColor = Color.LightGray; + break; + case 1: + statusColor = Color.LawnGreen; + break; + case 2: + statusColor = Color.Gold; + break; + case 3: + statusColor = Color.Red; + break; + } + + // draw background based on contacts status + using LinearGradientBrush lgb = new(bgBounds, statusColor, BackColor, LinearGradientMode.Horizontal); + g.FillRectangle(lgb, bgBounds); + } + + private void ContactControl_Load(object sender, EventArgs e) + { + lblUsername.Text = Username; + lblStatus.Text = $"'{TextStatus}'"; + pbProfilePic.Image = ProfilePic; + + if (Status == 0) + { + lblStatus.Visible = false; + lblUsername.Location = new(44, 15); + } + } + + private void lblUsername_MouseHover(object sender, EventArgs e) + { + if (!_isHovering) + { + lblUsername.ForeColor = Color.White; + _isHovering = true; + } + } + + private void lblUsername_MouseLeave(object sender, EventArgs e) + { + if (_isHovering) + { + lblUsername.ForeColor = Color.Black; + _isHovering = false; + } + } + + private void lblUsername_DoubleClick(object sender, EventArgs e) => OnContactDoubleClicked?.Invoke(this, EventArgs.Empty); + } +} diff --git a/qtcnet-client/Controls/ContactControl.resx b/qtcnet-client/Controls/ContactControl.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/qtcnet-client/Controls/ContactControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/qtcnet-client/Controls/CurrentProfileControl.Designer.cs b/qtcnet-client/Controls/CurrentProfileControl.Designer.cs new file mode 100644 index 0000000..a521661 --- /dev/null +++ b/qtcnet-client/Controls/CurrentProfileControl.Designer.cs @@ -0,0 +1,133 @@ +namespace qtcnet_client.Controls +{ + partial class CurrentProfileControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + pbCurrentProfilePic = new PictureBox(); + llblSignOut = new LinkLabel(); + linkLabel1 = new LinkLabel(); + pictureBox1 = new PictureBox(); + lblUsername = new Label(); + lblCurrencyAmount = new Label(); + ((System.ComponentModel.ISupportInitialize)pbCurrentProfilePic).BeginInit(); + ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); + SuspendLayout(); + // + // pbCurrentProfilePic + // + pbCurrentProfilePic.Image = Properties.Resources.DefaultPfp; + pbCurrentProfilePic.Location = new Point(3, 3); + pbCurrentProfilePic.Name = "pbCurrentProfilePic"; + pbCurrentProfilePic.Size = new Size(69, 67); + pbCurrentProfilePic.SizeMode = PictureBoxSizeMode.Zoom; + pbCurrentProfilePic.TabIndex = 0; + pbCurrentProfilePic.TabStop = false; + // + // llblSignOut + // + llblSignOut.AutoSize = true; + llblSignOut.Location = new Point(75, 24); + llblSignOut.Name = "llblSignOut"; + llblSignOut.Size = new Size(55, 15); + llblSignOut.TabIndex = 2; + llblSignOut.TabStop = true; + llblSignOut.Text = "Not You?"; + // + // linkLabel1 + // + linkLabel1.AutoSize = true; + linkLabel1.Location = new Point(130, 24); + linkLabel1.Name = "linkLabel1"; + linkLabel1.Size = new Size(64, 15); + linkLabel1.TabIndex = 3; + linkLabel1.TabStop = true; + linkLabel1.Text = "Edit Profile"; + // + // pictureBox1 + // + pictureBox1.Image = Properties.Resources.CurrencyIcon; + pictureBox1.Location = new Point(79, 49); + pictureBox1.Name = "pictureBox1"; + pictureBox1.Size = new Size(14, 14); + pictureBox1.SizeMode = PictureBoxSizeMode.AutoSize; + pictureBox1.TabIndex = 4; + pictureBox1.TabStop = false; + // + // lblUsername + // + lblUsername.AutoSize = true; + lblUsername.Font = new Font("Segoe UI", 9F, FontStyle.Bold | FontStyle.Italic); + lblUsername.ForeColor = Color.Black; + lblUsername.Location = new Point(76, 9); + lblUsername.Name = "lblUsername"; + lblUsername.Size = new Size(94, 15); + lblUsername.TabIndex = 1; + lblUsername.Text = "Welcome, User!"; + // + // lblCurrencyAmount + // + lblCurrencyAmount.AutoSize = true; + lblCurrencyAmount.Font = new Font("Segoe UI", 9F, FontStyle.Bold); + lblCurrencyAmount.ForeColor = Color.Black; + lblCurrencyAmount.Location = new Point(94, 49); + lblCurrencyAmount.Name = "lblCurrencyAmount"; + lblCurrencyAmount.Size = new Size(35, 15); + lblCurrencyAmount.TabIndex = 5; + lblCurrencyAmount.Text = "9999"; + // + // CurrentProfileControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + AutoSize = true; + BackColor = Color.Transparent; + Controls.Add(lblCurrencyAmount); + Controls.Add(pictureBox1); + Controls.Add(linkLabel1); + Controls.Add(llblSignOut); + Controls.Add(lblUsername); + Controls.Add(pbCurrentProfilePic); + Name = "CurrentProfileControl"; + Size = new Size(197, 73); + Load += CurrentProfileControl_Load; + ((System.ComponentModel.ISupportInitialize)pbCurrentProfilePic).EndInit(); + ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private PictureBox pbCurrentProfilePic; + private LinkLabel llblSignOut; + private LinkLabel linkLabel1; + private PictureBox pictureBox1; + private Label lblUsername; + private Label lblCurrencyAmount; + } +} diff --git a/qtcnet-client/Controls/CurrentProfileControl.cs b/qtcnet-client/Controls/CurrentProfileControl.cs new file mode 100644 index 0000000..2e57a7b --- /dev/null +++ b/qtcnet-client/Controls/CurrentProfileControl.cs @@ -0,0 +1,33 @@ +using qtcnet_client.Properties; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Text; +using System.Windows.Forms; + +namespace qtcnet_client.Controls +{ + public partial class CurrentProfileControl : UserControl + { + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string Username { get; set; } = "Username"; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public int CurrencyCount { get; set; } = 0; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public Image ProfileImage { get; set; } = Resources.DefaultPfp; + + public CurrentProfileControl() + { + InitializeComponent(); + } + + private void CurrentProfileControl_Load(object sender, EventArgs e) + { + lblUsername.Text = $"Welcome, {Username}!"; + lblCurrencyAmount.Text = CurrencyCount.ToString(); + pbCurrentProfilePic.Image = ProfileImage; + } + } +} diff --git a/qtcnet-client/Controls/CurrentProfileControl.resx b/qtcnet-client/Controls/CurrentProfileControl.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/qtcnet-client/Controls/CurrentProfileControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/qtcnet-client/Controls/LoginControl.Designer.cs b/qtcnet-client/Controls/LoginControl.Designer.cs new file mode 100644 index 0000000..97cd76e --- /dev/null +++ b/qtcnet-client/Controls/LoginControl.Designer.cs @@ -0,0 +1,158 @@ +namespace qtcnet_client.Controls +{ + partial class LoginControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + pbDefaultPfp = new Krypton.Toolkit.KryptonPictureBox(); + txtEmail = new Krypton.Toolkit.KryptonTextBox(); + lblEmail = new Label(); + lblPassword = new Label(); + cbRememberMe = new Krypton.Toolkit.KryptonCheckBox(); + llblForgotPassword = new Krypton.Toolkit.KryptonLinkLabel(); + llblRegister = new Krypton.Toolkit.KryptonLinkLabel(); + btnLogin = new Krypton.Toolkit.KryptonButton(); + txtPassword = new Krypton.Toolkit.KryptonTextBox(); + ((System.ComponentModel.ISupportInitialize)pbDefaultPfp).BeginInit(); + SuspendLayout(); + // + // pbDefaultPfp + // + pbDefaultPfp.Image = Properties.Resources.DefaultPfp; + pbDefaultPfp.Location = new Point(11, 7); + pbDefaultPfp.Name = "pbDefaultPfp"; + pbDefaultPfp.Size = new Size(128, 128); + pbDefaultPfp.SizeMode = PictureBoxSizeMode.AutoSize; + pbDefaultPfp.TabIndex = 0; + pbDefaultPfp.TabStop = false; + // + // txtEmail + // + txtEmail.Location = new Point(187, 23); + txtEmail.Name = "txtEmail"; + txtEmail.Size = new Size(375, 23); + txtEmail.TabIndex = 1; + // + // lblEmail + // + lblEmail.AutoSize = true; + lblEmail.ForeColor = Color.White; + lblEmail.Location = new Point(145, 27); + lblEmail.Name = "lblEmail"; + lblEmail.Size = new Size(36, 15); + lblEmail.TabIndex = 2; + lblEmail.Text = "Email"; + // + // lblPassword + // + lblPassword.AutoSize = true; + lblPassword.ForeColor = Color.White; + lblPassword.Location = new Point(145, 57); + lblPassword.Name = "lblPassword"; + lblPassword.Size = new Size(57, 15); + lblPassword.TabIndex = 4; + lblPassword.Text = "Password"; + // + // cbRememberMe + // + cbRememberMe.Location = new Point(208, 81); + cbRememberMe.Name = "cbRememberMe"; + cbRememberMe.PaletteMode = Krypton.Toolkit.PaletteMode.Office2007Silver; + cbRememberMe.Size = new Size(171, 20); + cbRememberMe.TabIndex = 3; + cbRememberMe.Values.Text = "Remember Me For 30 Days"; + // + // llblForgotPassword + // + llblForgotPassword.Location = new Point(450, 81); + llblForgotPassword.Name = "llblForgotPassword"; + llblForgotPassword.Size = new Size(112, 20); + llblForgotPassword.TabIndex = 4; + llblForgotPassword.Values.Text = "Forgot Passsword?"; + // + // llblRegister + // + llblRegister.Location = new Point(448, 97); + llblRegister.Name = "llblRegister"; + llblRegister.Size = new Size(118, 20); + llblRegister.TabIndex = 5; + llblRegister.Values.Text = "New Here? Register"; + llblRegister.LinkClicked += llblRegister_LinkClicked; + // + // btnLogin + // + btnLogin.Location = new Point(484, 119); + btnLogin.Name = "btnLogin"; + btnLogin.Size = new Size(90, 25); + btnLogin.TabIndex = 6; + btnLogin.Values.DropDownArrowColor = Color.Empty; + btnLogin.Values.Text = "Login"; + btnLogin.Click += btnLogin_Click; + // + // txtPassword + // + txtPassword.Location = new Point(208, 52); + txtPassword.Name = "txtPassword"; + txtPassword.PasswordChar = '●'; + txtPassword.Size = new Size(354, 23); + txtPassword.TabIndex = 2; + txtPassword.UseSystemPasswordChar = true; + // + // LoginControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.Transparent; + Controls.Add(txtPassword); + Controls.Add(btnLogin); + Controls.Add(llblRegister); + Controls.Add(llblForgotPassword); + Controls.Add(cbRememberMe); + Controls.Add(lblPassword); + Controls.Add(lblEmail); + Controls.Add(txtEmail); + Controls.Add(pbDefaultPfp); + Name = "LoginControl"; + Size = new Size(575, 146); + ((System.ComponentModel.ISupportInitialize)pbDefaultPfp).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Krypton.Toolkit.KryptonPictureBox pbDefaultPfp; + private Krypton.Toolkit.KryptonTextBox txtEmail; + private Label lblEmail; + private Label lblPassword; + private Krypton.Toolkit.KryptonCheckBox cbRememberMe; + private Krypton.Toolkit.KryptonLinkLabel llblForgotPassword; + private Krypton.Toolkit.KryptonLinkLabel llblRegister; + private Krypton.Toolkit.KryptonButton btnLogin; + private Krypton.Toolkit.KryptonTextBox txtPassword; + } +} diff --git a/qtcnet-client/Controls/LoginControl.cs b/qtcnet-client/Controls/LoginControl.cs new file mode 100644 index 0000000..ecb607f --- /dev/null +++ b/qtcnet-client/Controls/LoginControl.cs @@ -0,0 +1,77 @@ +using Krypton.Toolkit; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Reflection.Metadata.Ecma335; +using System.Text; +using System.Windows.Forms; + +namespace qtcnet_client.Controls +{ + public partial class LoginControl : UserControl + { + public string Email { get; private set; } = string.Empty; + public string Password { get; private set; } = string.Empty; + public bool RememberMe { get; private set; } = false; + + public event EventHandler? OnSuccessfulLogin; + public event EventHandler? OnRegisterPressed; + public LoginControl() + { + InitializeComponent(); + } + + private void btnLogin_Click(object sender, EventArgs e) + { + ToggleControls(false); + + bool formComplete = !string.IsNullOrWhiteSpace(txtEmail.Text) || !string.IsNullOrWhiteSpace(txtPassword.Text); + if (formComplete) + { + Email = txtEmail.Text; + Password = txtPassword.Text; + RememberMe = cbRememberMe.Checked; + + OnSuccessfulLogin?.Invoke(this, EventArgs.Empty); + } + else + { + KryptonMessageBox.Show("A Required Field Is Missing. Please Complete The Form.", "Oops."); + ToggleControls(true); + } + } + + private void llblRegister_LinkClicked(object sender, EventArgs e) + { + ToggleControls(false); + OnRegisterPressed?.Invoke(this, EventArgs.Empty); + } + + public void ToggleControls(bool toggle) + { + SuspendLayout(); + + txtEmail.Enabled = toggle; + txtPassword.Enabled = toggle; + cbRememberMe.Enabled = toggle; + llblForgotPassword.Enabled = toggle; + llblRegister.Enabled = toggle; + btnLogin.Enabled = toggle; + + ResumeLayout(); + } + + public void ClearControls() + { + SuspendLayout(); + + txtEmail.Clear(); + txtPassword.Clear(); + cbRememberMe.Checked = false; + + ResumeLayout(); + } + } +} diff --git a/qtcnet-client/Controls/LoginControl.resx b/qtcnet-client/Controls/LoginControl.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/qtcnet-client/Controls/LoginControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/qtcnet-client/Controls/MainTabControl.Designer.cs b/qtcnet-client/Controls/MainTabControl.Designer.cs new file mode 100644 index 0000000..f34d176 --- /dev/null +++ b/qtcnet-client/Controls/MainTabControl.Designer.cs @@ -0,0 +1,284 @@ +namespace qtcnet_client.Controls +{ + partial class MainTabControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainTabControl)); + ListViewItem listViewItem1 = new ListViewItem("Stock Market", "StockMarket"); + ListViewItem listViewItem2 = new ListViewItem("Guess The Number", "NumberGuessGame"); + ListViewItem listViewItem3 = new ListViewItem("Tic-Tac-Toe (Multiplayer)", "TicTacToe"); + tcMain = new TabControl(); + tpContacts = new TabPage(); + tlpContactsList = new TableLayoutPanel(); + tpRooms = new TabPage(); + tlpRoomsList = new TableLayoutPanel(); + tpUsers = new TabPage(); + lvUserList = new ListView(); + ilStatusIcons = new ImageList(components); + tpGames = new TabPage(); + lvGameList = new ListView(); + ilGameIcons = new ImageList(components); + tpStore = new TabPage(); + listView1 = new ListView(); + ilTabIcons = new ImageList(components); + ilStoreIcons = new ImageList(components); + tcMain.SuspendLayout(); + tpContacts.SuspendLayout(); + tpRooms.SuspendLayout(); + tpUsers.SuspendLayout(); + tpGames.SuspendLayout(); + tpStore.SuspendLayout(); + SuspendLayout(); + // + // tcMain + // + tcMain.Controls.Add(tpContacts); + tcMain.Controls.Add(tpRooms); + tcMain.Controls.Add(tpUsers); + tcMain.Controls.Add(tpGames); + tcMain.Controls.Add(tpStore); + tcMain.Font = new Font("Segoe UI", 9F, FontStyle.Bold, GraphicsUnit.Point, 0); + tcMain.ImageList = ilTabIcons; + tcMain.Location = new Point(0, 0); + tcMain.Multiline = true; + tcMain.Name = "tcMain"; + tcMain.SelectedIndex = 0; + tcMain.Size = new Size(380, 577); + tcMain.TabIndex = 0; + // + // tpContacts + // + tpContacts.BorderStyle = BorderStyle.Fixed3D; + tpContacts.Controls.Add(tlpContactsList); + tpContacts.Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point, 0); + tpContacts.ImageKey = "Contacts"; + tpContacts.Location = new Point(4, 24); + tpContacts.Name = "tpContacts"; + tpContacts.Padding = new Padding(3); + tpContacts.RightToLeft = RightToLeft.No; + tpContacts.Size = new Size(372, 549); + tpContacts.TabIndex = 1; + tpContacts.Text = "Contacts"; + tpContacts.UseVisualStyleBackColor = true; + // + // tlpContactsList + // + tlpContactsList.ColumnCount = 1; + tlpContactsList.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tlpContactsList.Dock = DockStyle.Fill; + tlpContactsList.Location = new Point(3, 3); + tlpContactsList.Name = "tlpContactsList"; + tlpContactsList.Padding = new Padding(0, 5, 0, 5); + tlpContactsList.RowCount = 1; + tlpContactsList.RowStyles.Add(new RowStyle()); + tlpContactsList.Size = new Size(362, 539); + tlpContactsList.TabIndex = 0; + // + // tpRooms + // + tpRooms.BorderStyle = BorderStyle.Fixed3D; + tpRooms.Controls.Add(tlpRoomsList); + tpRooms.Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point, 0); + tpRooms.ImageKey = "Rooms"; + tpRooms.Location = new Point(4, 24); + tpRooms.Name = "tpRooms"; + tpRooms.Padding = new Padding(3); + tpRooms.RightToLeft = RightToLeft.No; + tpRooms.Size = new Size(372, 549); + tpRooms.TabIndex = 7; + tpRooms.Text = "Rooms"; + tpRooms.UseVisualStyleBackColor = true; + // + // tlpRoomsList + // + tlpRoomsList.ColumnCount = 1; + tlpRoomsList.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tlpRoomsList.Dock = DockStyle.Fill; + tlpRoomsList.Location = new Point(3, 3); + tlpRoomsList.Name = "tlpRoomsList"; + tlpRoomsList.Padding = new Padding(0, 3, 0, 3); + tlpRoomsList.RowCount = 1; + tlpRoomsList.RowStyles.Add(new RowStyle()); + tlpRoomsList.Size = new Size(362, 539); + tlpRoomsList.TabIndex = 1; + // + // tpUsers + // + tpUsers.BorderStyle = BorderStyle.Fixed3D; + tpUsers.Controls.Add(lvUserList); + tpUsers.Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point, 0); + tpUsers.ImageKey = "Users"; + tpUsers.Location = new Point(4, 24); + tpUsers.Name = "tpUsers"; + tpUsers.Size = new Size(372, 549); + tpUsers.TabIndex = 3; + tpUsers.Text = "Users"; + tpUsers.UseVisualStyleBackColor = true; + // + // lvUserList + // + lvUserList.BorderStyle = BorderStyle.FixedSingle; + lvUserList.Location = new Point(3, 3); + lvUserList.MultiSelect = false; + lvUserList.Name = "lvUserList"; + lvUserList.Size = new Size(364, 541); + lvUserList.SmallImageList = ilStatusIcons; + lvUserList.TabIndex = 0; + lvUserList.UseCompatibleStateImageBehavior = false; + lvUserList.View = View.List; + // + // ilStatusIcons + // + ilStatusIcons.ColorDepth = ColorDepth.Depth32Bit; + ilStatusIcons.ImageStream = (ImageListStreamer)resources.GetObject("ilStatusIcons.ImageStream"); + ilStatusIcons.TransparentColor = Color.Transparent; + ilStatusIcons.Images.SetKeyName(0, "Offline"); + ilStatusIcons.Images.SetKeyName(1, "Online"); + ilStatusIcons.Images.SetKeyName(2, "Away"); + ilStatusIcons.Images.SetKeyName(3, "DoNotDisturb"); + // + // tpGames + // + tpGames.BorderStyle = BorderStyle.Fixed3D; + tpGames.Controls.Add(lvGameList); + tpGames.Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point, 0); + tpGames.ImageKey = "Games"; + tpGames.Location = new Point(4, 24); + tpGames.Name = "tpGames"; + tpGames.Padding = new Padding(3); + tpGames.Size = new Size(372, 549); + tpGames.TabIndex = 5; + tpGames.Text = "Games"; + tpGames.UseVisualStyleBackColor = true; + // + // lvGameList + // + lvGameList.Dock = DockStyle.Fill; + lvGameList.Font = new Font("Segoe UI", 9F, FontStyle.Bold, GraphicsUnit.Point, 0); + listViewItem1.ToolTipText = "Stock Market"; + listViewItem2.ToolTipText = "Guess The Number"; + listViewItem3.ToolTipText = "Tic-Tac-Toe (Multiplayer)"; + lvGameList.Items.AddRange(new ListViewItem[] { listViewItem1, listViewItem2, listViewItem3 }); + lvGameList.LargeImageList = ilGameIcons; + lvGameList.Location = new Point(3, 3); + lvGameList.MultiSelect = false; + lvGameList.Name = "lvGameList"; + lvGameList.Size = new Size(362, 539); + lvGameList.SmallImageList = ilGameIcons; + lvGameList.TabIndex = 0; + lvGameList.UseCompatibleStateImageBehavior = false; + // + // ilGameIcons + // + ilGameIcons.ColorDepth = ColorDepth.Depth32Bit; + ilGameIcons.ImageStream = (ImageListStreamer)resources.GetObject("ilGameIcons.ImageStream"); + ilGameIcons.TransparentColor = Color.Transparent; + ilGameIcons.Images.SetKeyName(0, "StockMarket"); + ilGameIcons.Images.SetKeyName(1, "NumberGuessGame"); + ilGameIcons.Images.SetKeyName(2, "TicTacToe"); + // + // tpStore + // + tpStore.BorderStyle = BorderStyle.Fixed3D; + tpStore.Controls.Add(listView1); + tpStore.Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point, 0); + tpStore.ImageKey = "Store"; + tpStore.Location = new Point(4, 24); + tpStore.Name = "tpStore"; + tpStore.Padding = new Padding(3); + tpStore.Size = new Size(372, 549); + tpStore.TabIndex = 8; + tpStore.Text = "Store"; + tpStore.UseVisualStyleBackColor = true; + // + // listView1 + // + listView1.Dock = DockStyle.Fill; + listView1.Location = new Point(3, 3); + listView1.MultiSelect = false; + listView1.Name = "listView1"; + listView1.Size = new Size(362, 539); + listView1.TabIndex = 0; + listView1.UseCompatibleStateImageBehavior = false; + // + // ilTabIcons + // + ilTabIcons.ColorDepth = ColorDepth.Depth32Bit; + ilTabIcons.ImageStream = (ImageListStreamer)resources.GetObject("ilTabIcons.ImageStream"); + ilTabIcons.TransparentColor = Color.Transparent; + ilTabIcons.Images.SetKeyName(0, "Contacts"); + ilTabIcons.Images.SetKeyName(1, "Rooms"); + ilTabIcons.Images.SetKeyName(2, "Users"); + ilTabIcons.Images.SetKeyName(3, "Games"); + ilTabIcons.Images.SetKeyName(4, "Store"); + // + // ilStoreIcons + // + ilStoreIcons.ColorDepth = ColorDepth.Depth32Bit; + ilStoreIcons.ImageSize = new Size(32, 32); + ilStoreIcons.TransparentColor = Color.Transparent; + // + // MainTabControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + AutoSize = true; + BackColor = Color.Transparent; + Controls.Add(tcMain); + DoubleBuffered = true; + Name = "MainTabControl"; + Size = new Size(383, 580); + tcMain.ResumeLayout(false); + tpContacts.ResumeLayout(false); + tpRooms.ResumeLayout(false); + tpUsers.ResumeLayout(false); + tpGames.ResumeLayout(false); + tpStore.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + + private TabControl tcMain; + private TabPage tpContacts; + private TabPage tpUsers; + private ListView lvUserList; + private TabPage tpGames; + private ListView lvGameList; + private ImageList ilTabIcons; + private ImageList ilStatusIcons; + private TableLayoutPanel tlpContactsList; + private TabPage tpStore; + private ListView listView1; + private TabPage tpRooms; + private TableLayoutPanel tlpRoomsList; + private ImageList ilGameIcons; + private ImageList ilStoreIcons; + } +} diff --git a/qtcnet-client/Controls/MainTabControl.cs b/qtcnet-client/Controls/MainTabControl.cs new file mode 100644 index 0000000..ba694e4 --- /dev/null +++ b/qtcnet-client/Controls/MainTabControl.cs @@ -0,0 +1,68 @@ +using Krypton.Toolkit; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Text; +using System.Windows.Forms; + +namespace qtcnet_client.Controls +{ + public partial class MainTabControl : UserControl + { + public event EventHandler? OnContactControlDoubleClicked; + public event EventHandler? OnRoomControlDoubleClicked; + + public MainTabControl() + { + InitializeComponent(); + } + + public void AddContacts(List contactControls) + { + tlpContactsList.SuspendLayout(); + + foreach (ContactControl contactControl in contactControls) + { + contactControl.Width = tlpContactsList.ClientSize.Width; + contactControl.OnContactDoubleClicked += ContactControl_OnContactDoubleClicked; + } + + tlpContactsList.Controls.Clear(); + tlpContactsList.Controls.AddRange([.. contactControls.DistinctBy(ctrl => ctrl.UserId)]); + + tlpContactsList.ResumeLayout(true); + } + + private void ContactControl_OnContactDoubleClicked(object? sender, EventArgs e) => OnContactControlDoubleClicked?.Invoke(sender, e); + + public void AddRooms(List roomControls) + { + tlpRoomsList.SuspendLayout(); + + foreach (RoomControl roomControl in roomControls) + { + roomControl.Width = tlpRoomsList.ClientSize.Width; + roomControl.OnRoomDoubleClicked += RoomControl_OnRoomDoubleClicked; + } + + tlpRoomsList.Controls.Clear(); + tlpRoomsList.Controls.AddRange([.. roomControls.DistinctBy(ctrl => ctrl.RoomId)]); + + tlpRoomsList.ResumeLayout(true); + } + + private void RoomControl_OnRoomDoubleClicked(object? sender, EventArgs e) => OnRoomControlDoubleClicked?.Invoke(sender, e); + + public void AddUsers(List users) + { + lvUserList.SuspendLayout(); + + lvUserList.Items.Clear(); + lvUserList.Items.AddRange([.. users.DistinctBy(lvi => lvi.Tag)]); // Tag = UserId here + + lvUserList.ResumeLayout(true); + } + } +} diff --git a/qtcnet-client/Controls/MainTabControl.resx b/qtcnet-client/Controls/MainTabControl.resx new file mode 100644 index 0000000..231c789 --- /dev/null +++ b/qtcnet-client/Controls/MainTabControl.resx @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 121, 15 + + + + AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs + LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu + SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAAhQAAAJNU0Z0AUkBTAIBAQQB + AAGoAQABqAEAARABAAEQAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABQAMAASADAAEBAQABIAYAASD/ + AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AC4AAwYBBwM0AVQDUQGiA14B0gNaAekDYAHoA10B + 0QNQAZ8DMQFNAwUBBhgAAwYBBwM0AVQDUQGiA14B0gNaAekDYAHoA10B0QNQAZ8DMQFNAwUBBhgAAwYB + BwM0AVQDUQGiA14B0gNaAekDYAHoA10B0QNQAZ8DMQFNAwUBBhgAAwYBBwM0AVQDUQGiA14B0gNaAekD + YAHoA10B0QNQAZ8DMQFNAwUBBhQAAyABLQNUAasDWwHkA08B9QMkAfsDOQH+AzkB/gMkAfsDUwH0A2IB + 4QNRAaEDHgEqEAADIAEtA1QBqwNbAeQBSAFaAUgB9QEhAVUBIQH7ARUBVQEVAf4BFQFVARUB/gEhAVUB + IQH7A1MB9ANiAeEDUQGhAx4BKhAAAyABLQNUAasDWwHkAUgCWgH1ASECVQH7ARUCVQH+ARUCVQH+ASEC + VQH7A1MB9ANiAeEDUQGhAx4BKhAAAyABLQNUAasDWwHkAkgBWgH1AiEBVQH7AhUBVQH+AhUBVQH+AiEB + VQH7A1MB9ANiAeEDUQGhAx4BKgwAAxsBJQNYAb0DWgHyAz0B/gMwAf8DOQH/AzwB/wM2Af8DKgH/AyQB + /wNAAf0DWwHwA1YBsgMaASMIAAMbASUDWAG9A1oB8gEVAV0BFQH+AQABVwEAAf8BAAFnAQAB/wEAAWwB + AAH/AQABYQEAAf8BAAFMAQAB/wEAAUABAAH/ASoBQAEqAf0BVwFeAVcB8ANWAbIBGQEaARkBIwgAAxsB + JQNYAb0DWgHyARUCXQH+AQACVwH/AQACZwH/AQACbAH/AQACYQH/AQACTAH/AQACQAH/ASoCQAH9AVcC + XgHwA1YBsgEZAhoBIwgAAxsBJQNYAb0DWgHyAhUBXQH+AgABVwH/AgABZwH/AgABbAH/AgABYQH/AgAB + TAH/AgABQAH/AioBQAH9AlcBXgHwA1YBsgIZARoBIwQAAwMBBANSAaUDYAHzA0kB/wNVAf8DZQH/A3EB + /wN1Af8DcQH/A2QB/wNMAf8DMQH/AzkB/gNfAe4DUAGaAwMBBAMDAQQBUgFTAVIBpQFTAW8BUwHzAQAB + ggEAAf8BAAGZAQAB/wEAAbYBAAH/AQABzAEAAf8BAAHTAQAB/wEAAcsBAAH/AQABswEAAf8BAAGIAQAB + /wEAAVcBAAH/ARUBVQEVAf4BXAFiAVwB7gNQAZoDAwEEAwMBBAFSAlMBpQFTAm8B8wEAAoIB/wEAApkB + /wEAArYB/wEAAswB/wEAAtMB/wEAAssB/wEAArMB/wEAAogB/wEAAlcB/wEVAlUB/gFcAmIB7gNQAZoD + AwEEAwMBBAJSAVMBpQJTAW8B8wIAAYIB/wIAAZkB/wIAAbYB/wIAAcwB/wIAAdMB/wIAAcsB/wIAAbMB + /wIAAYgB/wIAAVcB/wIVAVUB/gJcAWIB7gNQAZoDAwEEAy0BRANgAegDeAH+A24B/wN7Af8DhQH/A4oB + /wOMAf8DigH/A4UB/wN2Af8DVwH/AzIB/wNAAf0DXgHdAyoBPwMtAUQBYAFpAWAB6AEVAYoBFQH+AQAB + xgEAAf8BAAHcAQAB/wEAAe4BAAH/AQAB+AEAAf8BAAH7AQAB/wEAAfkBAAH/AQAB7wEAAf8BAAHUAQAB + /wEAAZwBAAH/AQABWgEAAf8BKgFAASoB/QNeAd0DKgE/Ay0BRAFgAmkB6AEVAooB/gEAAsYB/wEAAtwB + /wEAAu4B/wEAAvgB/wEAAvsB/wEAAvkB/wEAAu8B/wEAAtQB/wEAApwB/wEAAloB/wEqAkAB/QNeAd0D + KgE/Ay0BRAJgAWkB6AIVAYoB/gIAAcYB/wIAAdwB/wIAAe4B/wIAAfgB/wIAAfsB/wIAAfkB/wIAAe8B + /wIAAdQB/wIAAZwB/wIAAVoB/wIqAUAB/QNeAd0DKgE/A04BlQN3AfgDfwH/A4UB/wOKAf8DjQH/A44B + /wOOAf8DjgH/A40B/wOJAf8DdwH/A00B/wMlAf8DWgHyA0oBiwNOAZUBPwGKAT8B+AEAAeUBAAH/AQAB + 7wEAAf8BAAH4AQAB/wEAAf0BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/gEAAf8BAAH2AQAB + /wEAAdUBAAH/AQABiwEAAf8BAAFBAQAB/wNaAfIDSgGLA04BlQE/AooB+AEAAuUB/wEAAu8B/wEAAvgB + /wEAAv0B/wEAA/8BAAP/AQAD/wEAAv4B/wEAAvYB/wEAAtUB/wEAAosB/wEAAkEB/wNaAfIDSgGLA04B + lQI/AYoB+AIAAeUB/wIAAe8B/wIAAfgB/wIAAf0B/wIAAv8CAAL/AgAC/wIAAf4B/wIAAfYB/wIAAdUB + /wIAAYsB/wIAAUEB/wNaAfIDSgGLA18B0wN+AfwDkwH/A44B/wONAf8DjgH/A44B/wOOAf8DjgH/A44B + /wONAf8DhQH/A2cB/wM0Af8DQQH5A1oBxAFbAV8BWwHTASsBtgErAfwBDgH7AQ4B/wEDAf0BAwH/AQAB + /gEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/QEAAf8BAAHvAQAB + /wEAAbkBAAH/AQABXQEAAf8BPgFBAT4B+QNaAcQBWwJfAdMBKwK2AfwBDgL7Af8BAwL9Af8BAAL+Af8B + AAP/AQAD/wEAA/8BAAP/AQAD/wEAAv0B/wEAAu8B/wEAArkB/wEAAl0B/wE+AkEB+QNaAcQCWwFfAdMC + KwG2AfwCDgH7Af8CAwH9Af8CAAH+Af8CAAL/AgAC/wIAAv8CAAL/AgAC/wIAAf0B/wIAAe8B/wIAAbkB + /wIAAV0B/wI+AUEB+QNaAcQDbgH1A4AB/gOfAf8DkwH/A48B/wOOAf8DjgH/A44B/wOOAf8DjgH/A44B + /wOLAf8DdwH/A0gB/wNAAf0DYgHhAU0BfwFNAfUBOQHVATkB/gEnAf8BJwH/AQsB/wELAf8BAQH/AQEB + /wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAfkBAAH/AQAB + 1gEAAf8BAAGBAQAB/wEqAUABKgH9A2IB4QFNAn8B9QE5AtUB/gEnA/8BCwP/AQED/wEAA/8BAAP/AQAD + /wEAA/8BAAP/AQAD/wEAAvkB/wEAAtYB/wEAAoEB/wEqAkAB/QNiAeECTQF/AfUCOQHVAf4CJwL/AgsC + /wIBAv8CAAL/AgAC/wIAAv8CAAL/AgAC/wIAAv8CAAH5Af8CAAHWAf8CAAGBAf8CKgFAAf0DYgHhA3IB + 9gOEAf4DqwH/A5kB/wOQAf8DjgH/A44B/wOOAf8DjgH/A44B/wOOAf8DjQH/A38B/wNVAf8DQAH9A14B + 4gFIAX8BSAH2AVwB1QFcAf4BQgH/AUIB/wEZAf8BGQH/AQQB/wEEAf8BAAH/AQAB/wEAAf8BAAH/AQAB + /wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH9AQAB/wEAAeQBAAH/AQABmAEAAf8BKgFAASoB + /QNeAeIBSAJ/AfYBXALVAf4BQgP/ARkD/wEEA/8BAAP/AQAD/wEAA/8BAAP/AQAD/wEAA/8BAAL9Af8B + AALkAf8BAAKYAf8BKgJAAf0DXgHiAkgBfwH2AlwB1QH+AkIC/wIZAv8CBAL/AgAC/wIAAv8CAAL/AgAC + /wIAAv8CAAL/AgAB/QH/AgAB5AH/AgABmAH/AioBQAH9A14B4gNhAdYDgwH8A7gB/wOjAf8DkwH/A44B + /wOOAf8DjgH/A44B/wOOAf8DjgH/A40B/wOCAf8DXAH/A00B+gNaAccBXAFhAVwB1gFkAboBZAH8AV8B + /wFfAf8BLwH/AS8B/wEMAf8BDAH/AQEB/wEBAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB + /wEAAf8BAAH/AQAB/gEAAf8BAAHqAQAB/wEAAaUBAAH/ASkBTQEpAfoDWgHHAVwCYQHWAWQCugH8AV8D + /wEvA/8BDAP/AQED/wEAA/8BAAP/AQAD/wEAA/8BAAP/AQAC/gH/AQAC6gH/AQACpQH/ASkCTQH6A1oB + xwJcAWEB1gJkAboB/AJfAv8CLwL/AgwC/wIBAv8CAAL/AgAC/wIAAv8CAAL/AgAC/wIAAf4B/wIAAeoB + /wIAAaUB/wIpAU0B+gNaAccDUAGaA4sB+QPFAf8DsgH/A5wB/wORAf8DjgH/A44B/wOOAf8DjgH/A48B + /wOOAf8DgwH/A2AB/wNaAfIDTAGQA1ABmgFqAZkBagH5AXwB/wF8Af8BUQH/AVEB/wEfAf8BHwH/AQcB + /wEHAf8BAQH/AQEB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wECAf8BAgH/AQIB/gECAf8BAAHrAQAB + /wEAAa0BAAH/AVoBawFaAfIDTAGQA1ABmgFqApkB+QF8A/8BUQP/AR8D/wEHA/8BAQP/AQAD/wEAA/8B + AAP/AQID/wECAv4B/wEAAusB/wEAAq0B/wFaAmsB8gNMAZADUAGaAmoBmQH5AnwC/wJRAv8CHwL/AgcC + /wIBAv8CAAL/AgAC/wIAAv8CAgL/AgIB/gH/AgAB6wH/AgABrQH/AloBawHyA0wBkAMvAUkDbAHrA6YB + /gPGAf8DrgH/A5wB/wOTAf8DkAH/A48B/wOQAf8DkwH/A5MB/wOFAf8DUQH9A2AB4AMtAUUDLwFJA2wB + 6wGAAdUBgAH+AX8B/wF/Af8BSQH/AUkB/wEfAf8BHwH/AQwB/wEMAf8BBQH/AQUB/wEDAf8BAwH/AQUB + /wEFAf8BCgH/AQoB/wEKAf4BCgH/AQEB7QEBAf8BKgG2ASoB/QFgAWYBYAHgAy0BRQMvAUkDbAHrAYAC + 1QH+AX8D/wFJA/8BHwP/AQwD/wEFA/8BAwP/AQUD/wEKA/8BCgL+Af8BAQLtAf8BKgK2Af0BYAJmAeAD + LQFFAy8BSQNsAesCgAHVAf4CfwL/AkkC/wIfAv8CDAL/AgUC/wIDAv8CBQL/AgoC/wIKAf4B/wIBAe0B + /wIqAbYB/QJgAWYB4AMtAUUDAwEEA1YBrgN2AfUD2QH/A8sB/wO3Af8DpwH/A50B/wOaAf8DnAH/A58B + /wObAf8DiQH/A2gB8ANSAaMDAwEEAwMBBANWAa4BbgF/AW4B9QGoAf8BqAH/AYkB/wGJAf8BXAH/AVwB + /wE3Af8BNwH/ASIB/wEiAf8BGwH/ARsB/wEfAf8BHwH/ASYB/wEmAf8BHQH/AR0B/wEFAfMBBQH/AVcB + aQFXAfADUgGjAwMBBAMDAQQDVgGuAW4CfwH1AagD/wGJA/8BXAP/ATcD/wEiA/8BGwP/AR8D/wEmA/8B + HQP/AQUC8wH/AVcCaQHwA1IBowMDAQQDAwEEA1YBrgJuAX8B9QKoAv8CiQL/AlwC/wI3Av8CIgL/AhsC + /wIfAv8CJgL/Ah0C/wIFAfMB/wJXAWkB8ANSAaMDAwEEBAADHAEnA10BxwN6AfYDtQH+A9cB/wPMAf8D + wgH/A7sB/wO3Af8DsQH/A4AB/gNoAfQDWQG8AxsBJggAAxwBJwNdAccBdQF/AXUB9gGNAdUBjQH+AaUB + /wGlAf8BiwH/AYsB/wF0Af8BdAH/AWYB/wFmAf8BXAH/AVwB/wFOAf8BTgH/AUUB1QFFAf4BUwF6AVMB + 9AFXAVkBVwG8AxsBJggAAxwBJwNdAccBdQJ/AfYBjQLVAf4BpQP/AYsD/wF0A/8BZgP/AVwD/wFOA/8B + RQLVAf4BUwJ6AfQBVwJZAbwDGwEmCAADHAEnA10BxwJ1AX8B9gKNAdUB/gKlAv8CiwL/AnQC/wJmAv8C + XAL/Ak4C/wJFAdUB/gJTAXoB9AJXAVkBvAMbASYMAAMhATADWQG2A2oB7gObAfoDvgH9A9QB/wPMAf8D + vgH9A4cB+QNsAesDVQGsAx8BLBAAAyEBMANZAbYBaAFsAWgB7gGBAaUBgQH6Aa4BwAGuAf0BnwH/AZ8B + /wGMAf8BjAH/AWEBwAFhAf0BaAGZAWgB+QFhAWwBYQHrA1UBrAMfASwQAAMhATADWQG2AWgCbAHuAYEC + pQH6Aa4CwAH9AZ8D/wGMA/8BYQLAAf0BaAKZAfkBYQJsAesDVQGsAx8BLBAAAyEBMANZAbYCaAFsAe4C + gQGlAfoCrgHAAf0CnwL/AowC/wJhAcAB/QJoAZkB+QJhAWwB6wNVAawDHwEsFAADBgEHAzYBWANVAawD + ZgHlA6kB/AOWAfsDZQHiA1MBpwMzAVEDBgEHGAADBgEHAzYBWANVAawDZgHlAX4BugF+AfwBcwGqAXMB + +wNlAeIDUwGnAzMBUQMGAQcYAAMGAQcDNgFYA1UBrANmAeUBfgK6AfwBcwKqAfsDZQHiA1MBpwMzAVED + BgEHGAADBgEHAzYBWANVAawDZgHlAn4BugH8AnMBqgH7A2UB4gNTAacDMwFRAwYBBwwAAUIBTQE+BwAB + PgMAASgDAAFAAwABIAMAAQEBAAEBBgABARYAA/+BAAHgAQcB4AEHAeABBwHgAQcBwAEDAcABAwHAAQMB + wAEDAYABAQGAAQEBgAEBAYABAVAAAYABAQGAAQEBgAEBAYABAQHAAQMBwAEDAcABAwHAAQMB4AEHAeAB + BwHgAQcB4AEHCw== + + + + 241, 15 + + + + AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs + LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu + SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAiicAAAJNU0Z0AUkBTAIBAQMB + AAEYAQABGAEAASABAAEgAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABgAMAASADAAEBAQABIAYAAUB6 + AAIxASwB5wEIAQoBAAH/AyoBQAwAAyoBQAM2AVcCQAE/AWwCQAE/AWwCQAE/AWwCQAE/AWwCQAE/AWwC + QAE/AWwCQAE/AWwCQAE/AWwCQAE/AWwCQAE/AWwCQAE/AWwCQAE/AWwCQAE/AWwCQAE/AWwCQAE/AWwC + QAE/AWwCOwE6AWIDMwFRAxgBIVQAA1UBogNhAcAgAANXAaYDXwG88AADIQEwAkYBRQGAFAABFQEfAQQB + /wENARUBAAH/AQgBCgEAAf8BCAEKAQAB/wwAAUYBRQFDAXoBYQFWAU0BpwGMAWUBRAHPAYwBZQFEAc8B + jAFlAUQBzwGMAWUBRAHPAYwBZQFEAc8BjAFlAUQBzwGMAWUBRAHPAYwBZQFEAc8BjAFlAUQBzwGMAWUB + RAHPAYwBZQFEAc8BjAFlAUQBzwGMAWUBRAHPAYwBZQFEAc8BjAFlAUQBzwGMAWcBRQHOAXIBXAFKAbsB + WQFSAUwBmwMqAUBUAANoAdADdQHuIAADagHUA3MB6eQAAkwBSgG/AQgBCgEAAf8BCAEKAQAB/wEIAQoB + AAH/AQgBCgEAAf8BCAEKAQAB/wEIAQoBAAH/AQgBCgEAAf8CTAFKAb8CTAFKAb8BQwF0AQcB/wFHAXcB + DAH/ASUBRwEAAf8BDgEZAQAB/wwAAU8BSwFJAYoBdAFfAUwBvQG+AW8BKAHqAb4BbwEoAeoBvgFvASgB + 6gG+AW8BKAHqAb4BbwEoAeoBvgFvASgB6gG+AW8BKAHqAb4BbwEoAeoBvgFvASgB6gG+AW8BKAHqAb4B + bwEoAeoBvgFvASgB6gG+AW8BKAHqAb4BbwEoAeoBvgFvASgB6gG7AW8BKAHpAZIBZwFAAdMBZwFZAU0B + rwEvAi4BSFQAA2gB0AN1Ae4gAANqAdQDcwHp4AABCAEKAQAB/wEQAR4BAAH/ASEBQQEAAf8BJwFLAQAB + /wEmAUoBAAH/ASEBQQEAAf8BEAEeAQAB/wEIAQoBAAH/AQgBCgEAAf8BCAEKAQAB/wEpAU4BAAH/AVoB + lgEQAf8BWgGcARAB/wFzAbYBJgH/ARgBLgEAAf8MAAFMAUoBRwGGAW8BXQFNAbcBrwFtATIB4wGvAW0B + MgHjAa8BbQEyAeMBrwFtATIB4wG2AW8BLgHmAckBcQEiAe0B2gF1ARcB9AHwAXcBBgH8Af8BeAEAAv8B + eAEAAf8B3gFxARQB9QG2AW8BLgHmAa8BbQEyAeMBrwFtATIB4wGvAW0BMgHjAa8BawEzAeIBiQFkAUQB + zQFjAVcBTQGqAi4BLQFGVAADaAHQA3UB7iAAA2oB1ANzAencAAEIAQoBAAH/AT0BcQECAf8BTAGGAQcB + /wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEQAf8BTAF/AQ8B/wE7AWcBCAH/ASEB + NAEIAf8BUwGOAQsB/wFaAZwBEAH/AXIBswEnAf8BUwGBARsB/xAAAysBQQE3AjYBWQFBAUABPwFuAUEB + QAE/AW4BQQFAAT8BbgFBAUABPwFuAUcBRQFEAXsBWwFTAUsBnwF9AWEBSQHFAcoBbwEhAe4B/wF4AQAC + /wF4AQAB/wGDAWQBSAHJAUcBRgFEAXwBQQFAAT8BbgFBAUABPwFuAUEBQAE/AW4BQQFAAT8BbQE8AjsB + YwMzAVIDGQEiVAADaAHQA3UB7iAAA2oB1ANzAenYAAEVASkBAAH/AUoBhAEEAf8BUgGMAQgB/wFSAYwB + CAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEQAf8BUgGMARAB/wFSAYwBEAH/AVoBlAEQAf8B + VgGQAQwB/wFaAZoBEAH/AVoBnAEQAf8BcgGpATAB/wEIAQoBAAH/AQgBCgEAAf8MAAMMARADEQEWAxUB + HAMVARwDFQEcAxUBHAMhATABPwE+AT0BaQFeAVUBTAGlAbUBbgEwAeUB/wF4AQAC/wF4AQAB/wFjAVcB + TQGqAyIBMQMVARwDFQEcAxUBHAMUARsDEwEZAxABFQMHAQlUAANoAdADdQHuIAADagHUA3MB6dQAARYB + JwECAf8BSQGAAQcB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQsB + /wFSAYwBEAH/AVIBjAEQAf8BWAGSARAB/wFaAZQBEAH/AVoBmwEQAf8BWgGcARAB/wFcAZ4BEgH/ATsB + bQEDAf8BCAEKAQAB/wEIAQoBAAH/AQgBCgEAAf8IAAMFBAYECAEKAwgBCgMIAQoDCAEKAxcBHwI5ATgB + XQFZAVIBTAGeAa8BbQEyAeMB/wF4AQAC/wF4AQAB/wFeAVUBTAGjAxgBIQMIAQoDCAEKAwgBCgMIAQoD + BwEJAwYBBwMCAQNUAANoAdADdQHuIAADagHUA3MB6dAAAT0BQgE0Ad8BRwGAAQMB/wFSAYwBCAH/AVIB + jAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBEAH/AVIBjAEQAf8BUgGMARAB + /wFaAZQBEAH/AVoBlAEQAf8BWgGcARAB/wFaAZwBEAH/AVoBnAEQAf8BWgGcARAB/wFaAZgBEAH/ARYB + KgEAAf8BCAEKAQAB/wEIAQoBAAH/HAADEgEXATYCNQFWAVgBUgFMAZoBqwFtATQB4QH/AXgBAAL/AXgB + AAH/AVsBUwFLAZ8DEgEYcAADaAHQA3UB7iAAA2oB1ANzAenQAAEsAVABAAH/AUoBjAEAAf8BUgGMAQgB + /wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEOAf8BVQGSARAB/wFaAZwBEAH/AVoB + nAEQAf8BWgGUARAB/wFaAZwBEAH/AVoBnAEQAf8BWgGcARAB/wFaAZwBEAH/AVoBnAEQAf8BWgGUARgB + /wFYAZIBFgH/ARABHgEAAf8BCAEKAQAB/xwAAxIBFwE2AjUBVgFYAVIBTAGaAasBbQE0AeEB/wF4AQAC + /wF4AQAB/wFbAVMBSwGfAxIBGHAAA2gB0AN1Ae4gAANqAdQDcwHp1AACSgFJAY8BMwFeAQAB/wFSAYwB + CAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBEAH/AX8BwAEzAf8BXwGeARcB/wEgAT8BAAH/AQgBCgEAAf8B + UgGMARAB/wFaAZwBEAH/AVoBnAEQAf8BWgGcARAB/wFaAZwBEAH/AVoBlQEXAf8BWgGUARgB/wFaAZQB + GAH/AVoBlAEYAf8BCAEKAQAB/wEIAQoBAAH/GAADEgEXATYCNQFWAVgBUgFMAZoBqwFtATQB4QH/AXgB + AAL/AXgBAAH/AVsBUwFLAZ8DEgEYFAAEAQMDAQQDBgQHAQkDBwEJAwQBBQQCBAEUAAMRARYDIQEvAyEB + LwMhAS8DIQEvAyEBLwMhAS8DIQEvAyEBLwMhAS8DagHZA3YB8QMhAS8DIQEvAyEBLwMhAS8DIQEvAyEB + LwMhAS8DIQEvA20B3AN1Ae0DIQEvAyEBLwMhAS8DIQEvAyEBLwMhAS8DIQEvAyEBLwMhAS8DDQERuAAB + QgF7AQAB/wFSAYwBCAH/AXsBxgEpAf8DMwFQBAADDAEQATkBawEAAf8BWgGcARAB/wFaAZwBEAH/AV0B + lwETAf8BVgGUAQwB/wFaAZwBEAH/AVoBlAEYAf8BWgGUARgB/wFjAZwBGAH/AWMBpQEXAf8BKQFQAQAB + /wEIAQoBAAH/GAADEgEXATYCNQFWAVgBUgFMAZoBqwFtATQB4QH/AXgBAAL/AXgBAAH/AVsBUwFLAZ8D + EgEYFAAEAgMJAQwDDwEUAxMBGgMSARgDCgENAwQBBQQBFAADcwHoA38B/wN/Af8DfwH/A38B/wN/Af8D + fwH/A38B/wN/Af8DfwH/A38B/wN/Af8DfwH/A38B/wN/Af8DfwH/A38B/wN/Af8DfwH/A38B/wN/Af8D + fwH/A38B/wN/Af8DfwH/A38B/wN/Af8DfwH/A38B/wN/Af8DfwH/A2gBz7wAAQgBCgEAAf8MAAETASMB + AAH/AVoBlAEQAf8BWgGcARAB/wGUAdYBSgH/AQ0BEQECAf8BMQFaAQAB/wFaAZQBEgH/AVoBlAEYAf8B + WgGUARgB/wFjAZwBGAH/AWMBpQEXAf8BawGlASEB/wEQAR4BAAH/GAADEgEXATYCNQFWAVgBUgFMAZoB + qwFtATQB4QH/AXgBAAL/AXgBAAH/AVsBUwFLAZ8DEgEYFAADBQEGAx0BKQMwAUoCOwE9AWUCOwE8AWQD + KAE8AhUBFgEdAwYBCBQAAzwBZANNAY8DTQGPA00BjwNNAY8DTQGPA00BjwNNAY8DTQGPA00BjwNyAeoD + fAH4A00BjwNNAY8DTQGPA00BjwNNAY8DTQGPA00BjwNNAY8DdQHsA3YB9QNNAY8DTQGPA00BjwNNAY8D + TQGPA00BjwNNAY8DTQGPA00BjwM1AVaoAAI4ATQB3wEIAQoBAAH/AQgBCgEAAf8BCAEKAQAB/wEIAQoB + AAH/AQgBCgEAAf8BCAEKAQAB/wMzAVADOgFgAUsBhQEHAf8BWgGUARAB/wFaAZwBEAH/ATkBawEAAf8D + CQEMARABHgEAAf8BVAGOARIB/wFaAZQBGAH/AWMBnAEYAf8BYwGcARgB/wFjAaUBFwH/AXMBtAEpAf8B + EAEeAQAB/xgAAxIBFwE2AjUBVgFYAVIBTAGaAasBbQE0AeEB/wF4AQAC/wF4AQAB/wFbAVMBSwGfAxIB + GBQAAwkBDAMzAVACSgFSAZECSAGDAckCRgGFAcoCQwFGAXoDKQE9AwwBEDwAA2gB0AN1Ae4gAANqAdQD + cwHpzAABCAEKAQAB/wEIAQoBAAH/AQgBCgEAAf8BCAEKAQAB/wEIAQoBAAH/AQgBCgEAAf8BCAEKAQAB + /wEIAQoBAAH/AQgBCgEAAf8BMwFfAQIB/wFaAZQBEAH/AVoBnAEQAf8BrQHnAWMB/wEQAR4BAAH/AQgB + CgEAAf8BFAEmAQAB/wFUAY4BEgH/AWIBmwEYAf8BYwGcARgB/wFjAaUBFwH/AWMBpQEXAf8BYwGcASEB + /wEYAS4BAAH/GAADEgEXATYCNQFWAVgBUgFMAZoBqwFtATQB4QH/AXgBAAL/AXgBAAH/AVsBUwFLAZ8D + EgEYFAADCQELAy4DRwFLAYQCSQF8AcMCRQGGAcsCRwFMAYUDLgFHAw8BEzwAA2gB0AN1Ae4gAANqAdQD + cwHpyAABCAEKAQAB/wErAVIBAAH/AUEBcgEFAf8BSgGEAQYB/wFKAYQBBgH/AUoBhAEGAf8BQgFzAQYB + /wEpAVABAAH/ARABHgEAAf8BHwE6AQAB/wFVAY8BCwH/AVoBlQEQAf8BWgGcARAB/wHWAf8BjAH/ARAB + HgEAAf8BGAEuAQAB/wFCAXgBAwH/AVoBlAEYAf8BYgGbARgB/wFjAZwBGAH/AWMBpQEXAf8BYwGlARcB + /wFzAbQBKQH/ARABHgEAAf8YAAMSARcBNgI1AVYBWAFSAUwBmgGrAW0BNAHhAf8BeAEAAv8BeAEAAf8B + WwFTAUsBnwMSARgUAAMGAQgDJAE0AjwBPQFmAk0BYQGnAksBcAG4AkYBTAGDAzEBTQMPARQ8AANoAdAD + dQHuIAADagHUA3MB6cQAASgBTQEAAf8BTgGIAQcB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwB + CAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBEAH/AVIBjAEQAf8BUgGMARAB/wFaAZQBEAH/AVoBnAEQAf8B + YwGlARcB/wFCAXsBAAH/AUoBhAEGAf8BUgGMAQgB/wFaAZgBFAH/AVoBlAEYAf8BYwGcARgB/wFjAaUB + FwH/AWMBpQEXAf8BYwGlARcB/wGUAckBTQH/BAADEAEVAxoBJAMhAS8DEQEWBAIEAQMSARcBNgI1AVYB + WAFSAUwBmgGrAW0BNAHhAf8BeAEAAv8BeAEAAf8BWwFTAUsBnwMSARgUAAMDAQQDEwEZAyUBNgI7ATwB + ZAJBAUMBcgMzAVEDIQEvAwkBDDwAA2gB0AN1Ae4gAANqAdQDcwHpwAABKQFOAQIB/wFSAYwBCAH/AVIB + jAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEQAf8BUgGMARAB + /wFSAYwBEAH/AVoBlAEQAf8BWgGVARAB/wFaAZwBEAH/AVoBnAEQAf8BWgGcARAB/wFaAZwBEAH/AVoB + lAEYAf8BWgGUARgB/wFjAZwBGAH/AWMBnAEYAf8BYwGlARcB/wFjAaUBFwH/AWMBpQEXAf8BZQGgASEB + /wQAAx4BKwIxATABTAE8AjsBYwMiATEDBwEJAwQBBQMSARgDNgFXAVgBUgFMAZoBqwFtATQB4QH/AXgB + AAL/AXgBAAH/AVsBUwFLAZ8DEgEYHAADBwEJAxwBJwMjATICGQEaASMDDwEUAwQBBTwAA2gB0AN1Ae4g + AANqAdQDcwHpvAACRgFFAYABTgGIAQcB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIB + jAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEQAf8BUgGMARAB/wFSAYwBEAH/AVoBlAEQAf8BWgGXARAB + /wFaAZwBEAH/AVoBnAEQAf8BWgGcARAB/wFaAZwBEAH/AVoBlAEYAf8BWgGUARgB/wFjAZwBGAH/AWMB + oQEYAf8BYwGlARcB/wFjAaUBFwH/AXgBugEsAf8BCAEKAQAB/wQAAyQBNAFHAUYBRAF8AW8BXQFNAbcB + VQFQAUoBlAFBAUABPwFtAygBOwMfASwDOQFeAVgBUgFMAZoBqwFtATQB4QH/AXgBAAL/AXgBAAH/AVsB + UwFLAZ8DEgEYHAADEgEXAjwBPQFmAkkBTgGJAkABQQFvAy4BRwMOARI8AANoAdADdQHuIAADagHUA3MB + 6bwAATUBXgEHAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8B + UgGMAQgB/wFSAYwBCAH/AVIBjAEQAf8BUgGMARAB/wFaAZQBEAH/AVoBlAEQAf8BWgGbARAB/wFaAZwB + EAH/AVoBnAEQAf8BWgGcARAB/wFaAZYBFgH/AVoBlAEYAf8BXQGXARgB/wFjAZwBGAH/AWMBpQEXAf8B + YwGlARcB/wFwAbMBIAH/ASsBUAEEAf8IAAMnAToBXQFUAUwBogHtAXIBCQH7AboBbgEsAecBfgFiAUgB + xAFEAUMBQgF1AzEBTQFBAUABPwFuAVsBUwFLAZ8BrwFtATIB4wH/AXgBAAL/AXgBAAH/AVsBUwFLAZ8D + EgEYHAADGQEiAksBVQGVAkQBiQHNAkwBagGyAkMBRgF6Ax0BKQMGAQgEAgQBMAADaAHQA3UB7iAAA2oB + 1ANzAem8AAFKAYQBBgH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFnAakB + FwH/AVIBjAEIAf8BSgGEAQYB/wFKAYQBBgH/AVIBjAEQAf8BWgGUARAB/wFaAZcBEAH/AVoBnAEQAf8B + WgGcARAB/wFaAZwBEAH/AVoBnAEQAf8BWgGUARgB/wFaAZQBGAH/AV8BmAEYAf8BYwGeARgB/wFjAaUB + FwH/AWsBqQEdAf8BKQFQAQAB/wwAAyIBMQFWAVABSwGYAdcBcgEaAfMBzQFwAR0B8AG2AW8BLgHmAXoB + XwFJAcIBYwFXAU0BqgFhAVYBTQGnAXIBXgFKAbwBwgFyASkB6wH/AXgBAAL/AXgBAAH/AVsBUwFLAZ8D + EgEYHAADFAEbAkMBRQF3AkwBbQG2AkQBiQHNAk0BawGzAj8BQAFsAyYBOAMMAQ8DAgEDCAADAgEDAw0B + EQMNAREDDQERAw0BEQMNAREDDQERAw0BEQMNAREDDQERA2cB0wN1Ae8DDQERAw0BEQMNAREDDQERAw0B + EQMNAREDDQERAw0BEQNqAdcDcwHqAw0BEQMNAREDDQERAw0BEQMNAREDDQERAw0BEQMNAREDDQERAwQB + BZQAAUoBhAEGAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFpAaQBHgH/ASwBUAEDAf8B + CAEKAQAB/wE3AWEBBwH/AVIBjAEOAf8BUgGMARAB/wF/AcEBMgH/AU8BXgFBAdMBWAFhAUoBvwEvAVkB + AAH/AUQBegEFAf8BUAGKAQgB/wFaAZwBDgH/AVoBnAEQAf8BWgGcARAB/wFPAYYBEAH/AUQBdAEMAf8B + TwFSAUoBvxAAAhoBGQEjAUcBRQFEAXsBhQFkAUYBygGeAWkBOwHaAbsBcAEqAegBygFxASAB7wHFAXEB + JwHsAZgBaQE9AdYBmAFoAT0B2AHaAXUBFwH0Af8BeAEAAv8BeAEAAf8BWwFTAUsBnwMSARgcAAMPARMD + NAFTAkoBUwGSAkQBjAHPAkEBkwHUAk0BYgGoAkEBQwFyAyYBOAMUARsDBgEIAwIBAwNhAcIDfwH/A38B + /wN/Af8DfwH/A38B/wN/Af8DfwH/A38B/wN/Af8DfwH/A38B/wN/Af8DfwH/A38B/wN/Af8DfwH/A38B + /wN/Af8DfwH/A38B/wN/Af8DfwH/A38B/wN/Af8DfwH/A38B/wN/Af8DfwH/A38B/wN/Af8DbgHclAAB + SgGMAQAB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AWcBnQEnAf8BCAEKAQAB/wEKAQ8B + AAH/AUIBdwEDAf8BUgGMARAB/wFaAZwBEAH/ASUBQQEFAf8EAAM6AWABCAEKAQAB/wEIAQoBAAH/BAAC + RgFFAYACRgFFAYACRgFFAYAcAAMKAQ0DIgExAzQBVAFGAUQBQwF4AVsBUwFLAZ8BjAFlAUQBzwHJAXIB + IgHtAcUBcQEnAewBzQFwAR0B8AHtAXIBCQH7Af8BeAEAAv8BeAEAAf8BWwFTAUsBnwMSARgcAAMGAQgD + GAEhAzEBTQJKAVIBkQJMAXQBvQJCAY8B0QJJAXwBwwJMAV0BogI9AT4BZwIZARoBIwMMAQ8DQAFvA1oB + rQNaAa0DWgGtA1oBrQNaAa0DWgGtA1oBrQNaAa0DWgGtA3YB8AN9AfoDWgGtA1oBrQNaAa0DWgGtA1oB + rQNaAa0DWgGtA1oBrQN2AfEDfAH4A1oBrQNaAa0DWgGtA1oBrQNaAa0DWgGtA1oBrQNaAa0DWgGtA0cB + gZQAATsBbQECAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFTAY4BEAH/AQ8BGwEAAf8B + IwFDAQAB/wFSAYwBEAH/AVIBjAEQAf8BewG9ATAB/wEIAQoBAAH/AQgBCgEAAf8BCAEKAQAB/wEIAQoB + AAH/AQgBCgEAAf8BCAEKAQAB/wEIAQoBAAH/KAADAwEEAwkBCwMiATEDOAFbAVYBUAFLAZcBhAFkAUcB + yAG1AW4BMAHlAeQBcgEUAfcB9gF1AQYB/QH/AXgBAAL/AXgBAAH/AVsBUwFLAZ8DEgEYHAAEAQMCAQMD + FQEcAjUBNgFWAkkBUQGPAkcBhAHIAjQBpgHfAjIBrwHjAkwBXgGlAzQBUwMcAScoAANoAdADdQHuIAAD + agHUA3MB6bwAAQgBCgEAAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AU4B + ggEPAf8BSgGEAQYB/wFSAYwBEAH/AWMBpQEXAf8BCAEKAQAB/wEIAQoBAAH/ARMBIwEAAf8BQgFzAQYB + /wEgAT8BAAH/AQgBCgEAAf8BCAEKAQAB/wEIAQoBAAH/AQgBCgEAAf8BCAEKAQAB/yAABAEDAwEEAwwB + EAMYASADKQE9AT8CPgFqAW0BWwFLArUBbgEwAeUB5AFyARQB9wH/AXgBAAL/AXgBAAH/AVsBUwFLAZ8D + EgEYIAAEAQMHAQkDFAEbAyoBQAJCAUQBdgJNAWcBrwIoAbsB6QJCAY4B0AJLAVsBnwMzAVEoAANoAdAD + dQHuIAADagHUA3MB6cAAAUIBcwEGAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwB + CAH/AVIBjAEQAf8BUgGMARAB/wFKAYQBBgH/ARABHgEAAf8BMQFaAQAB/wFNAYQBDQH/AVoBnAEQAf8B + WgGcARAB/wFSAYwBCAH/ARABHgEAAf8BCAEKAQAB/wEIAQoBAAH/AkYBRQGALAADAgEDAwsBDgMgAS4B + RAFDAUIBdgFqAVoBTgGxAaoBbQE1AeAB3wF1ARQB9gHnAXIBDwH5AVkBUwFMAZwDEgEXDAAEAQMMARAD + FwEfAx0BKAMdASgDDAEPCAADDwEUAiYBJwE5AkYBTAGDAjQBqwHhAiwBugHnAkABkAHSAj8BQQFuKAAD + aAHQA3UB7iAAA2oB1ANzAenEAAFJAYkBAAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEIAf8B + UgGMARAB/wFSAYwBEAH/AVIBjAEQAf8BUgGMARAB/wFaAZQBEAH/AVoBnAEQAf8BWgGcARAB/wFaAZwB + EAH/AVoBnAEQAf8BWgGUARgB/wFCAXMBBgH/AQoBDgEAAf8wAAQBAwMBBAMMAQ8DHgEqATsCOgFhAXEB + XQFLAbkBsAFrAS8B5AHKAW8BIQHuAVYBUAFLAZcDEgEXDAADAgEDAyEBLwI2ATcBWQJCAUQBdQJBAUMB + cwMeASsIAAMEAQUDDAEPAjgBOQFcAkUBjAHOAicBxQHsAiABygHvAkUBSQF/KAADaAHQA3UB7iAAA2oB + 1ANzAenIAAE6AWsBAAH/AVIBjAEIAf8BUgGMAQgB/wFSAYwBCAH/AVIBjAEQAf8BUgGMARAB/wFaAZQB + EAH/AVoBlAEQAf8BWgGcARAB/wFaAZwBEAH/AVoBnAEQAf8BWgGcARAB/wFaAZQBGAH/AVoBlAEYAf8B + YwGlARcB/wEZASwBAwH/OAADAgEDAwgBCgMgAS0BQgFBAUABcQFSAU0BSgGRAVgBUgFMAZoCOwE6AWID + DAEPDAADAwEEAysBQQJDAUYBegJNAWEBpwJNAWUBrQM3AVoDGAEgAwkBCwMGAQcDDQERAjkBOgFfAkIB + jwHRAiABygHvAhoB1wHzAkUBSQGBKAADaAHQA3UB7iAAA2oB1ANzAenMAAEsAVUBAAH/AVIBjAEIAf8B + UgGMAQgB/wFSAYwBEAH/AVIBjAEQAf8BWgGUARAB/wFaAZQBEAH/AVoBnAEQAf8BWgGcARAB/wFaAZwB + EAH/AVoBnAEQAf8BWgGUARgB/wFaAZQBGAH/ASEBQQEAAf9AAAQCAwcBCQMQBBUBHAIWARUBHQMPARMD + AgEDDAADAwEEAzABSwJJAVEBjwJGAYUBygI4AaUB3gJKAVUBlAM0AVMDFQEcAw0BEQMeASsCQwFFAXcC + NAGmAd8CHQHUAfECKAG7AekCRAFHAXsoAANoAdADdQHuIAADagHUA3MB6cgAARIBIgEAAf8BTAGGAQYB + /wFSAYwBCAH/AVoBnAEQAf8BUAGKAQgB/wFSAYwBEAH/AVoBlAEQAf8BWgGcARAB/wFaAZwBEAH/AVoB + nAEQAf8BWgGcARAB/wFaAZwBEQH/AWkBqwEfAf8BCAEKAQAB/2wAAwIBAwMhAS8COgE7AWECTQFlAa0C + OwGeAdoCSgF3Ab8CSwFbAZ8CQwFGAXoCQQFDAXICRwFMAYUCTAFqAbICIgHJAe0COgGgAdsCTgFqAbED + OAFbKAADaAHQA3UB7iAAA2oB1ANzAenIAAEQAR4BAAH/AUgBfQEHAf8BWQGTARMB/wFVAYgBGAH/CAAB + IQFBAQAB/wFCAXsBAAH/AVIBjAEIAf8BVQGRAQgB/wE5AWYBBAH/ASoBMwEgAe94AAMGAQgDFwEfAkQB + RwF7AkcBfwHGAi8BsAHkAhcB2gH0AiEBygHuAiEBygHuAh0B1AHxAhQB3wH2AgYB8AH8AksBdgG+Aj4B + PwFrAyQBNCgAA2gB0AN1Ae4gAANqAdQDcwHp0AABHgE6AQAB/5wABAIDCQEMAyoDPwFBAW4CSgFVAZYC + TQFrAbMCSQF5AcECRgGAAccCRgGAAccCSwFxAbkCTQFeAaQCQAFCAXEDJQE2AxMBGSgAA2gB0AN1Ae4g + AANqAdQDcwHp/wB5AAQBAwsBDgMmATgDOQFeAkUBSAF9AkkBTwGLAkkBTgGJAj4BPwFrAyoBPwMXAR8D + AgEDLAADVQGiA2EBvyAAA1cBpgNfAbuoAAFCAU0BPgcAAT4DAAEoAwABgAMAASADAAEBAQABAQYAAQIW + AAP/AQAD/wHHAgABBwL/Ac8B8wH/BAAC/wHPAYcCAAEHAv8BzwHzAf8EAAH/Af4BAAEHAgABBwL/Ac8B + 8wH/BAAB/wH8AQABBwIAAQcC/wHPAfMB/wQAAf8B+AEAAQ8CAAEHAv8BzwHzAf8EAAH/AfABAAEHAgAB + BwL/Ac8B8wH/BAAB/wHgAQABAwIAAQcC/wHPAfMB/wQAAf8BwAEAAQEB/AEDA/8BzwHzAf8EAAH/AcAB + AAEBAfwBAwP/Ac8B8wH/BAAB/wHgAgAB/AEDAeABHwgAAf8B/AEgAQAB/AEDAeABHwgAAf8B/gHgAQAB + /AEDAeABHwgAAf8BwAIAAfwBAwHgAR8B/wHPAfMB/wQAAf8BgAIAAfwBAwHgAR8B/wHPAfMB/wQAAf8D + AAH8AQMB4AEfAf8BzwHzAf8EAAH+AgABAQEAAQMB4AEfAf8BzwHzAf8EAAH8AgABAQEAAQMB+AEfAf8B + zwHzAf8EAAH4AgABAQEAAQMB+AEfAf8BzwHzAf8EAAH4AgABAwEAAQMB+AEDAf8BzwHzAf8EAAH4AgAB + BwEAAQMB+AEDCAAB+AIAAQ8BAAEDAfgJAAH4AQABRAF/AQABAwH4CQAB+AEAAQEB/wGAAQMB+AEAAf8B + zwHzAf8EAAH4AgABfwGAAQMB/AEAAf8BzwHzAf8EAAH8AgABfwHwAQMBgQGAAf8BzwHzAf8EAAH+AgAB + /wHwAQMBgQGAAf8BzwHzAf8EAAH/AgAB/wH8AQMBgAEAAf8BzwHzAf8EAAH/AYABAQH/Af4BAwGAAQAB + /wHPAfMB/wQAAf8BAAEDA/8BgAEAAf8BzwHzAf8EAAH/AQwBDwP/AcABAAH/Ac8B8wH/BAAB/wHfBP8B + wAEAAf8BzwHzAf8EAAb/AfABAQH/Ac8B8wH/BAAL + + + + 17, 17 + + + + AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs + LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu + SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAHBkAAAJNU0Z0AUkBTAIBAQUB + AAGwAQABsAEAARABAAEQAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABQAMAASADAAEBAQABIAYAASAa + AAM3AVoDWAG4A2MB3wJjAV0B3wFiAl0B3wNdAd8DXQHfAWECXQHfA2MB3wNjAd8DVQGsAzABS8wAAzsB + YgNdAcUBcwJoAfQBnwFlATEB/wGXAVMBFwH/AZYBSwEJAf8BkwFGAQEB/wGMAUMBAwH/AX0BQAELAf8B + awFAARoB/wFuAVABNgH/A2gB8ANaAbcDNAFUxAADNQFVA10BxwFxAW4BWgH1AbgBZQEbAf8BuQFYAQIB + /wHJAV8BAAH/AdgBZQEAAf8B3AFnAQAB/wHWAWQBAAH/AcMBXAEAAf8BogFMAQAB/wF8ATsBAwH/AW4B + RgEjAf8DaAHwA1oBtwMwAUrAAANcAcQBkAF8AVwB+AHUAXEBGAH/AdcBZQEAAf8B5QFsAQAB/wHyAXIB + AAH/AfoBdQEAAf8B/AF2AQAB/wH6AXYBAAH/AfMBcgEAAf8B4gFrAQAB/wG9AVkBAAH/AYcBQAEAAf8B + cQFIASMB/wNoAfADVgGrwAAB/gHdAcEB/wHtAYABIAH/Ae0BcQECAf8B8wFzAQAB/wH6AXYBAAH/Af4B + eAEAAv8BewEIAv8BiAEnAv8BogFTAf8B/gGBARcB/wH8AXgBBAH/AewBbwEAAf8BwQFbAQAB/wGGAUEB + AwH/AXgBVgE2Af8DYwHfwAAB/wGyAW8B/wH9AYABEQH/AfwBdwEBAf8B/QF3AQAC/wF4AQAC/wF/AQ8C + /wGSATsC/wGzAYMC/wHqAeAC/wGQAT0C/wF7AQoB/wH8AXcBAAH/AeUBbAEAAf8BsQFUAQAB/wGEAUsB + GgH/A2MB38AAAf8BmgFCAv8BgwEVAf8B/gF6AQQC/wF4AQAC/wF4AQAC/wGVAT8C/wHKAa4C/wHaAcYC + /wHtAeUC/wGWAUkC/wF8AQ0B/wH+AXgBAAH/AfQBcwEAAf8B0AFiAQAB/wGaAU4BCgH/AWMCXQHfwAAB + /wGVATcC/wGJAR8C/wF9AQgC/wF4AQAC/wF4AQAC/wGoAVwC/wHgAc0C/wGhAWYC/wHYAcUC/wG5AZQC + /wGHASMC/wF4AQAB/wH7AXYBAAH/AeIBagEAAf8BrwFUAQMB/wFjAl0B38AAAf8BoAFJAv8BkgExAv8B + gQERAv8BeQEDAv8BeAEAAv8BqAFcAv8B4AHNAv8BoQFmAv8B2AHFAv8BwAGfAv8BiQEnAv8BeAEAAf8B + /gF4AQAB/wHsAW8BAAH/Ab8BWgECAf8BYwJdAd/AAAH/AbMBbwL/AZ4BSAL/AYgBHgL/AXwBBwL/AXgB + AAL/AZgBQwL/Ac4BtAL/AdcBwAL/AeoB4AL/AZ0BVwL/AX4BEQL/AXgBAAL/AXgBAAH/AfABcQEAAf8B + ywFkAQkB/wFjAWEBXQHfwAAB/wHMAZ8C/wGsAWMC/wGTATMC/wGBAREC/wF5AQIC/wGCARYC/wGaAUsC + /wGuAXgC/wGlAVcC/wGBARcC/wF5AQQC/wF4AQAC/wF4AQAB/wHwAXEBAAH/AdUBcQEXAf8CYwFdAd/A + AAH/AekB1QL/AbwBgQL/AaQBVAL/AY4BKgL/AX8BDQL/AXkBAgL/AXsBCQL/AYIBHQL/AXgBAAL/AXgB + AAL/AXgBAQL/AXoBBAL/AXoBAwH/AfMBdAEDAf8B4gGEATIB/wNjAd/AAANeAdIBlQGMAYMB+QH/AbsB + fQL/AaMBUgL/AZABLAL/AYIBEwL/AX0BCAL/AXoBBAL/AXkBAgL/AXoBBAL/AX0BCQL/AYABDwL/AX8B + DQH/AfgBhgEgAf8BeAFtAWgB9ANYAbjAAAM8AWYDYwHVAZUBhwF8AfgB/wG+AYUC/wGqAV8C/wGZAT4C + /wGNAScC/wGGARoC/wGDARUC/wGFARkC/wGKASIC/wGNASgC/wGTATMB/wF8AW4BaAH1A10BxQM2AVnE + AANCAXIDYwHVAZUBjgGIAfkB/wHJAZkC/wG8AX8C/wGuAWYC/wGkAVMC/wGfAUoC/wGfAUsC/wGjAVEC + /wGnAVgB/wGVAX4BfAH4A10BxwM7AWLMAAM8AWUDXgHSAf8B7wHgAv8B3AG8Av8BzQGfAv8BwQGKAv8B + uwF/Av8BvwGGAv8BzQGhAv8B6QHWAf8DXAHEAzUBVcgAA2cB7wJnAVkB7wFnAV0BWQHvAWcBWwFZAe8B + ZwFbAVkB7wFnAlkB7wFnAWQBWQHvA2cB7wNnAe8DZwHvA2cB7wNnAe8DZwHvA2cB7wNnAe8DZwHvOAAD + MwFRA3kB9QMHAQkDKgE/A0UBfANZAbsDYwHfA2gB9AOAAf4DgQH/A4EB/wOBAf8DgAH+A2gB9ANjAd8D + WgG6A0QBegMnAToIAAM3AVoDWAG4A2MB3wJjAV0B3wFiAl0B3wNdAd8DXQHfAWECXQHfA2MB3wNjAd8D + VQGsAzABSwgAA/gB/wG5AZUBPAH/AYMBfQFuAf8BhAF9AWwB/wGqAYQBJwH/AawBewEAAf8BzAG8AZQB + /wN+Af8DfgH/A34B/wN+Af8DfgH/A34B/wN+Af8DfgH/A44B/zgAAxIBGAM/AW0DQwF1A10BzAN8AfgD + gQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wN8AfgDVAGoBAADOwFiA10B + xQFzAmgB9AGfAWUBMQH/AZcBUwEXAf8BlgFLAQkB/wGTAUYBAQH/AYwBQwEDAf8BfQFAAQsB/wFrAUAB + GgH/AW4BUAE2Af8DaAHwA1oBtwM0AVQEAAT/AZcBiwFtAf8CgQGAAf8BggGBAYAB/wGYAYgBYAH/AcoB + kAEAAf8B3QHMAZ8B/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A5MB/zQAA18B0wM9AWcE + AANqAe0DfQH6A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB + /wNVAa8DNQFVA10BxwFxAW4BWgH1AbgBZQEbAf8BuQFYAQIB/wHJAV8BAAH/AdgBZQEAAf8B3AFnAQAB + /wHWAWQBAAH/AcMBXAEAAf8BogFMAQAB/wF8ATsBAwH/AW4BRgEjAf8DaAHwA1oBtwMwAUoE/wGGAYQB + fQH/A4EB/wOBAf8BhwGDAXoB/wHPAZQBAAH/Ad4BzAGfAf8D4AH/A+AB/wPgAf8D4AH/A+AB/wPgAf8D + 4AH/A+AB/wO8Af80AANaAcIDNAFTBAADYwHfA24B9QOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8D + gQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DVQGvA1wBxAGQAXwBXAH4AdQBcQEYAf8B1wFlAQAB/wHlAWwB + AAH/AfIBcgEAAf8B+gF1AQAB/wH8AXYBAAH/AfoBdgEAAf8B8wFyAQAB/wHiAWsBAAH/Ab0BWQEAAf8B + hwFAAQAB/wFxAUgBIwH/A2gB8ANWAasE/wGLAYYBegH/A4EB/wOBAf8BjgGGAXEB/wHPAZQBAAH/Ad4B + zAGfIf8DygH/EAADDQERAz8BbANTAacBXAJZAb4BWAJWAbMBSAJHAYMDIQEwBAADcwHzAzoBYAgAAzYB + WANbAcADbgH1A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DaAH0A1IB + pAH+Ad0BwQH/Ae0BgAEgAf8B7QFxAQIB/wHzAXMBAAH/AfoBdgEAAf8B/gF4AQAC/wF7AQgC/wGIAScC + /wGiAVMB/wH+AYEBFwH/AfwBeAEEAf8B7AFvAQAB/wHBAVsBAAH/AYYBQQEDAf8BeAFWATYB/wNjAd8E + /wGsAZYBYAH/AYMBggF/Af8BhQGCAX0B/wGzAZMBRAH/Ac8BlAEAAf8B3gHMAZ8B/wOwAf8DsAH/A7AB + /wOwAf8DsAH/A7AB/wOwAf8DsAH/A6gB/wgAAxoBJANSAaABZwFjAUgB9gGiAXMBAAH/Aa4BfAEAAf8B + sAF9AQAB/wGoAXgBAAH/AZUBagEAAf8BgAFqARYB/gFcAlkBxgNXAbUDFgEeCAADAgEDAxoBIwM4AVwD + VAGoA2IB1wNwAfEDgAH+A4EB/wOBAf8DgQH/A4EB/QNoAfADYQHUA1MBpQM2AVkDGAEgAf8BsgFvAf8B + /QGAAREB/wH8AXcBAQH/Af0BdwEAAv8BeAEAAv8BfwEPAv8BkgE7Av8BswGDAv8B6gHgAv8BkAE9Av8B + ewEKAf8B/AF3AQAB/wHlAWwBAAH/AbEBVAEAAf8BhAFLARoB/wNjAd8E/wHZAaoBNwH/Ab4BmAE4Af8B + wAGYATYB/wHcAaIBFAH/Ac8BlAEAAf8B3gHMAZ8B/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8D + gQH/A5MB/wQAAyABLQJjAVoB6QG/AYgBAAH/Ac0BlQEKAf8BsAGIAScB/wFzAWQBPwH/AU0BSwFHAf8B + TgFLAUIB/wFmAVcBMQH/AZoBdAEXAf8BpAF2AQMB/wFwAU8BAAH/A0MBdgQBRAAB/wGaAUIC/wGDARUB + /wH+AXoBBAL/AXgBAAL/AXgBAAL/AZUBPwL/AcoBrgL/AdoBxgL/Ae0B5QL/AZYBSQL/AXwBDQH/Af4B + eAEAAf8B9AFzAQAB/wHQAWIBAAH/AZoBTgEKAf8BYwJdAd8E/wHhAa4BMQH/Ab0BlwE7Af8BwAGYATUB + /wHjAaUBCgH/Ac8BlAEAAf8B3gHMAZ8B/wPAAf8DwAH/A8AB/wPAAf8DwAH/A8AB/wPAAf8DwAH/A68B + /wQAAmMBWgHpAdkBmgEAAf8B2gGjARwB/wKOAYwB/wOKAf8DlwH/A5sB/wORAf8DdAH/A0gB/wFDAUIB + PwH/AbUBgwEHAf8BegFXAQAB/wM2AVgMAAMCAQMDCAEKAyEBLwMxAU4DPQFoA0MBdgNEAXoDQwF1Az0B + ZwMxAU0DIAEuAwcBCQQCBAAB/wGVATcC/wGJAR8C/wF9AQgC/wF4AQAC/wF4AQAC/wGoAVwC/wHgAc0C + /wGhAWYC/wHYAcUC/wG5AZQC/wGHASMC/wF4AQAB/wH7AXYBAAH/AeIBagEAAf8BrwFUAQMB/wFjAl0B + 3wT/AbsBnQFTAf8BiAGEAXkB/wGMAYUBdAH/AcQBmQEwAf8BzwGUAQAB/wHeAcwBnyH/A8oB/wNDAXYB + 6QGnAQIB/wHpAasBEgH/AdABygG7Af8DrAH/A10B/wNMAf8DSwH/A0QB/wMPAf8DswH/A2YB/wFUAUwB + OgH/Aa4BfQEEAf8DXQHMCAADEwEaAzkBXQNZAbwDZAHbA2oB7QNjAfYDXwH7A4EB/QNfAfsDYwH2A2UB + 7ANjAdoDWgG6AzgBXAMTARoB/wGgAUkC/wGSATEC/wGBAREC/wF5AQMC/wF4AQAC/wGoAVwC/wHgAc0C + /wGhAWYC/wHYAcUC/wHAAZ8C/wGJAScC/wF4AQAB/wH+AXgBAAH/AewBbwEAAf8BvwFaAQIB/wFjAl0B + 3wT/AZQBigFzAf8DgQH/A4EB/wGaAYoBYwH/Ac8BlAEAAf8B3gHMAZ8B/wPQAf8D0AH/A9AB/wPQAf8D + 0AH/A9AB/wPQAf8D0AH/A7UB/wJqAWEB5gHtAa0BEAH/AfQB0AF2Af8D+gH/A/oB/wN9Af8DfwH/A4AB + /wOAAf8DfgH/A4QB/wO7Af8DagH/AagBgAEcAf8BaQFjAUgB9gQAAxoBJANWAa4DaAH0A4EB/wOBAf8D + gQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DawHyA1IBoQH/AbMBbwL/AZ4BSAL/AYgB + HgL/AXwBBwL/AXgBAAL/AZgBQwL/Ac4BtAL/AdcBwAL/AeoB4AL/AZ0BVwL/AX4BEQL/AXgBAAL/AXgB + AAH/AfABcQEAAf8BywFkAQkB/wFjAWEBXQHfBP8BhgGDAX4B/wOBAf8DgQH/AYYBgwF7Af8BzwGUAQAB + /wHeAcwBnwH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DkwH/AWoBZwFiAe4B7wG0ASEB + /wH3AdwBlwn/A5EB/wOIAf8DhwH/A4cB/wOBAf8DVwH/A+YB/wOjAf8BtgGRATYB/wGFAWoBQAH5BAAD + VwGyA2UB5wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8D + VQGvAf8BzAGfAv8BrAFjAv8BkwEzAv8BgQERAv8BeQECAv8BggEWAv8BmgFLAv8BrgF4Av8BpQFXAv8B + gQEXAv8BeQEEAv8BeAEAAv8BeAEAAf8B8AFxAQAB/wHVAXEBFwH/AmMBXQHfBP8BiwGGAXkB/wOBAf8D + gQH/AY4BhgFxAf8BzwGUAQAB/wHeAcwBnwH/A6EB/wOhAf8DoQH/A6EB/wOhAf8DoQH/A6EB/wOhAf8D + oQH/A0sBjQHwAb4BPwH/AfQBzQFsIf8D+wH/A9AB/wHXAacBMQH/AmEBXQHRBAADbwHzA18B+wOBAf8D + gQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DVQGvAf8B6QHVAv8B + vAGBAv8BpAFUAv8BjgEqAv8BfwENAv8BeQECAv8BewEJAv8BggEdAv8BeAEAAv8BeAEAAv8BeAEBAv8B + egEEAv8BegEDAf8B8wF0AQMB/wHiAYQBMgH/A2MB3wT/AacBlAFnAf8BgwGCAX8B/wGFAYMBfgH/AbAB + lAFOAf8B0wGXAQIB/wHgAc4BnwH/A+cB/wPnAf8D5wH/A+cB/wPnAf8D5wH/A+cB/wPnAf8DwAH/AwcB + CQFqAWgBYgHuAfEBvAE7Af8B+gHqAcIB/wPcAf8DdwH/A2gB/wNoAf8DaAH/AzIJ/wHyAd0BqQH/AeoB + qQEIAf8DPgFqBAADZAHbA2gB9AOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8D + gQH/A4EB/wOBAf8DVQGvA14B0gGVAYwBgwH5Af8BuwF9Av8BowFSAv8BkAEsAv8BggETAv8BfQEIAv8B + egEEAv8BeQECAv8BegEEAv8BfQEJAv8BgAEPAv8BfwENAf8B+AGGASAB/wF4AW0BaAH0A1gBuAT/AdcB + sQFSAf8BmgGPAXQB/wGgAZIBbQH/AeABrwE3Af8B5wGpARAB/wHrAdUBoAH/A4EB/wOBAf8DgQH/A4EB + /wOBAf8DgQH/A4EB/wOBAf8DkwH/BAADMwFRAWoCaAHwAfMBxgFYAf8B+gHnAbgW/wH+AfsB/wH5AeIB + qgH/Ae8BuAEtAf8DTgGWBAIEAAM8AWQDXwHJA30B+gOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8D + gQH/A4EB/wOBAf8DgQH/A30B+gNVAaoDPAFmA2MB1QGVAYcBfAH4Af8BvgGFAv8BqgFfAv8BmQE+Av8B + jQEnAv8BhgEaAv8BgwEVAv8BhQEZAv8BigEiAv8BjQEoAv8BkwEzAf8BfAFuAWgB9QNdAcUDNgFZBP8B + 9AHNAWwB/wH0AcsBZgH/AfQBywFlAf8B9AHLAWUB/wHxAcEBSQH/AfkB4wGsAf8DiQH/A4kB/wOJAf8D + iQH/A4kB/wOJAf8DiQH/A4kB/wOaAf8IAAMiATEDXwHJAaABigFnAfoB8wHKAWUB/wH5AeEBpgH/AfsB + 7QHMAf8B+wHsAcgB/wH4Ad0BmwH/AcYBmAFnAf4CZQFeAeIDPQFoBAEIAAMGAQgDMQFMA1ABmwNlAewD + fQH6A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wN9AfoDagHtA1ABmwMvAUkEAANCAXIDYwHVAZUB + jgGIAfkB/wHJAZkC/wG8AX8C/wGuAWYC/wGkAVMC/wGfAUoC/wGfAUsC/wGjAVEC/wGnAVgB/wGVAX4B + fAH4A10BxwM7AWIEAED/EAADDwETA0cBggNkAdsBtAGeAW8B/ANnAeoDVAGoAygBOxwAAwUBBgMSARcD + OgFgA1EBnwNfAdMDZwHvA2MB9gNiAe4DXgHSA1EBngM5AV8DEQEWAwUBBgwAAzwBZQNeAdIB/wHvAeAC + /wHcAbwC/wHNAZ8C/wHBAYoC/wG7AX8C/wG/AYYC/wHNAaEC/wHpAdYB/wNcAcQDNQFVCAABQgFNAT4H + AAE+AwABKAMAAUADAAEgAwABAQEAAQEGAAEBFgAD/wEAAcABAwYAAYABAWYAAYABAQYAAcABAwgAAf8B + /AIAAcABAwIAAf8B/AIAAYABAQIAAf8B+QYAAf8B+QYAAfABEwYAAcABAwYAAYABAQL/BAABgAEBAcAB + AQUAAQEBgAYAAQEHAAEBBwABAQcAAQEGAAGAAQEGAAHAAQMCAAGAAQECAAHwAR8BwAEBAcABAws= + + + + 360, 15 + + \ No newline at end of file diff --git a/qtcnet-client/Controls/MessageControl.Designer.cs b/qtcnet-client/Controls/MessageControl.Designer.cs new file mode 100644 index 0000000..b73e088 --- /dev/null +++ b/qtcnet-client/Controls/MessageControl.Designer.cs @@ -0,0 +1,92 @@ +namespace qtcnet_client.Controls +{ + partial class MessageControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + pbProfileImage = new PictureBox(); + lblUsername = new Label(); + rtxtMessage = new RichTextBox(); + ((System.ComponentModel.ISupportInitialize)pbProfileImage).BeginInit(); + SuspendLayout(); + // + // pbProfileImage + // + pbProfileImage.Image = Properties.Resources.DefaultPfp; + pbProfileImage.Location = new Point(4, 5); + pbProfileImage.Name = "pbProfileImage"; + pbProfileImage.Size = new Size(40, 38); + pbProfileImage.SizeMode = PictureBoxSizeMode.Zoom; + pbProfileImage.TabIndex = 0; + pbProfileImage.TabStop = false; + // + // lblUsername + // + lblUsername.AutoSize = true; + lblUsername.Font = new Font("Segoe UI", 9F, FontStyle.Bold); + lblUsername.Location = new Point(46, 5); + lblUsername.Name = "lblUsername"; + lblUsername.Size = new Size(64, 15); + lblUsername.TabIndex = 1; + lblUsername.Text = "Username"; + // + // rtxtMessage + // + rtxtMessage.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + rtxtMessage.BackColor = Color.White; + rtxtMessage.BorderStyle = BorderStyle.None; + rtxtMessage.Location = new Point(50, 23); + rtxtMessage.Name = "rtxtMessage"; + rtxtMessage.ReadOnly = true; + rtxtMessage.Size = new Size(98, 22); + rtxtMessage.TabIndex = 2; + rtxtMessage.Text = ""; + rtxtMessage.ContentsResized += rtxtMessage_ContentsResized; + // + // MessageControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.Transparent; + Controls.Add(rtxtMessage); + Controls.Add(lblUsername); + Controls.Add(pbProfileImage); + Name = "MessageControl"; + Size = new Size(154, 48); + Load += MessageControl_Load; + ((System.ComponentModel.ISupportInitialize)pbProfileImage).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private PictureBox pbProfileImage; + private Label lblUsername; + private RichTextBox rtxtMessage; + } +} diff --git a/qtcnet-client/Controls/MessageControl.cs b/qtcnet-client/Controls/MessageControl.cs new file mode 100644 index 0000000..18ae5e6 --- /dev/null +++ b/qtcnet-client/Controls/MessageControl.cs @@ -0,0 +1,37 @@ +using qtcnet_client.Properties; +using System.ComponentModel; + +namespace qtcnet_client.Controls +{ + public partial class MessageControl : UserControl + { + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string Username { get; set; } = "Username"; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string Message { get; set; } = "Message"; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public Image ProfileImage { get; set; } = Resources.DefaultPfp; + public MessageControl() + { + InitializeComponent(); + } + + private void MessageControl_Load(object sender, EventArgs e) + { + lblUsername.Text = Username; + rtxtMessage.Text = Message; + pbProfileImage.Image = ProfileImage; + } + + private void rtxtMessage_ContentsResized(object sender, ContentsResizedEventArgs e) + { + rtxtMessage.Height = e.NewRectangle.Height + 4; + + int bottom = + rtxtMessage.Bottom + + Padding.Bottom; + + Height = Math.Max(bottom, pbProfileImage.Bottom + 4); + } + } +} diff --git a/qtcnet-client/Controls/MessageControl.resx b/qtcnet-client/Controls/MessageControl.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/qtcnet-client/Controls/MessageControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/qtcnet-client/Controls/RegisterControl.Designer.cs b/qtcnet-client/Controls/RegisterControl.Designer.cs new file mode 100644 index 0000000..9e8517e --- /dev/null +++ b/qtcnet-client/Controls/RegisterControl.Designer.cs @@ -0,0 +1,245 @@ +namespace qtcnet_client.Controls +{ + partial class RegisterControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + pbDefaultPfp = new Krypton.Toolkit.KryptonPictureBox(); + txtPassword = new Krypton.Toolkit.KryptonTextBox(); + lblPassword = new Label(); + lblEmail = new Label(); + txtEmail = new Krypton.Toolkit.KryptonTextBox(); + txtConfirmPassword = new Krypton.Toolkit.KryptonTextBox(); + lblConfirmPassword = new Label(); + lblConfirmEmail = new Label(); + txtConfirmEmail = new Krypton.Toolkit.KryptonTextBox(); + cbTOSAgree = new Krypton.Toolkit.KryptonCheckBox(); + btnRegister = new Krypton.Toolkit.KryptonButton(); + llblReshowLogin = new LinkLabel(); + label3 = new Label(); + txtUsername = new Krypton.Toolkit.KryptonTextBox(); + dtpDateOfBirth = new Krypton.Toolkit.KryptonDateTimePicker(); + lblDateOfBirth = new Label(); + ((System.ComponentModel.ISupportInitialize)pbDefaultPfp).BeginInit(); + SuspendLayout(); + // + // pbDefaultPfp + // + pbDefaultPfp.Image = Properties.Resources.DefaultPfp; + pbDefaultPfp.Location = new Point(11, 15); + pbDefaultPfp.Name = "pbDefaultPfp"; + pbDefaultPfp.Size = new Size(128, 128); + pbDefaultPfp.SizeMode = PictureBoxSizeMode.AutoSize; + pbDefaultPfp.TabIndex = 1; + pbDefaultPfp.TabStop = false; + // + // txtPassword + // + txtPassword.Location = new Point(208, 76); + txtPassword.Name = "txtPassword"; + txtPassword.PasswordChar = '●'; + txtPassword.Size = new Size(354, 23); + txtPassword.TabIndex = 3; + txtPassword.UseSystemPasswordChar = true; + // + // lblPassword + // + lblPassword.AutoSize = true; + lblPassword.ForeColor = Color.White; + lblPassword.Location = new Point(145, 81); + lblPassword.Name = "lblPassword"; + lblPassword.Size = new Size(57, 15); + lblPassword.TabIndex = 8; + lblPassword.Text = "Password"; + // + // lblEmail + // + lblEmail.AutoSize = true; + lblEmail.ForeColor = Color.White; + lblEmail.Location = new Point(145, 51); + lblEmail.Name = "lblEmail"; + lblEmail.Size = new Size(36, 15); + lblEmail.TabIndex = 7; + lblEmail.Text = "Email"; + // + // txtEmail + // + txtEmail.Location = new Point(187, 47); + txtEmail.Name = "txtEmail"; + txtEmail.Size = new Size(375, 23); + txtEmail.TabIndex = 2; + // + // txtConfirmPassword + // + txtConfirmPassword.Location = new Point(255, 134); + txtConfirmPassword.Name = "txtConfirmPassword"; + txtConfirmPassword.PasswordChar = '●'; + txtConfirmPassword.Size = new Size(307, 23); + txtConfirmPassword.TabIndex = 5; + txtConfirmPassword.UseSystemPasswordChar = true; + // + // lblConfirmPassword + // + lblConfirmPassword.AutoSize = true; + lblConfirmPassword.ForeColor = Color.White; + lblConfirmPassword.Location = new Point(145, 138); + lblConfirmPassword.Name = "lblConfirmPassword"; + lblConfirmPassword.Size = new Size(104, 15); + lblConfirmPassword.TabIndex = 12; + lblConfirmPassword.Text = "Confirm Password"; + // + // lblConfirmEmail + // + lblConfirmEmail.AutoSize = true; + lblConfirmEmail.ForeColor = Color.White; + lblConfirmEmail.Location = new Point(145, 109); + lblConfirmEmail.Name = "lblConfirmEmail"; + lblConfirmEmail.Size = new Size(83, 15); + lblConfirmEmail.TabIndex = 11; + lblConfirmEmail.Text = "Confirm Email"; + // + // txtConfirmEmail + // + txtConfirmEmail.Location = new Point(234, 105); + txtConfirmEmail.Name = "txtConfirmEmail"; + txtConfirmEmail.Size = new Size(328, 23); + txtConfirmEmail.TabIndex = 4; + // + // cbTOSAgree + // + cbTOSAgree.Location = new Point(255, 190); + cbTOSAgree.Name = "cbTOSAgree"; + cbTOSAgree.PaletteMode = Krypton.Toolkit.PaletteMode.Office2007Silver; + cbTOSAgree.Size = new Size(354, 20); + cbTOSAgree.TabIndex = 7; + cbTOSAgree.Values.Text = "By Checking Me, You Agree To This Servers Terms Of Service"; + // + // btnRegister + // + btnRegister.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + btnRegister.Location = new Point(502, 231); + btnRegister.Name = "btnRegister"; + btnRegister.Size = new Size(90, 25); + btnRegister.TabIndex = 9; + btnRegister.Values.DropDownArrowColor = Color.Empty; + btnRegister.Values.Text = "Register"; + btnRegister.Click += btnRegister_Click; + // + // llblReshowLogin + // + llblReshowLogin.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + llblReshowLogin.AutoSize = true; + llblReshowLogin.Location = new Point(499, 210); + llblReshowLogin.Name = "llblReshowLogin"; + llblReshowLogin.Size = new Size(95, 15); + llblReshowLogin.TabIndex = 8; + llblReshowLogin.TabStop = true; + llblReshowLogin.Text = "Meant To Login?"; + llblReshowLogin.LinkClicked += llblReshowLogin_LinkClicked; + // + // label3 + // + label3.AutoSize = true; + label3.ForeColor = Color.White; + label3.Location = new Point(145, 22); + label3.Name = "label3"; + label3.Size = new Size(60, 15); + label3.TabIndex = 17; + label3.Text = "Username"; + // + // txtUsername + // + txtUsername.Location = new Point(211, 18); + txtUsername.Name = "txtUsername"; + txtUsername.Size = new Size(351, 23); + txtUsername.TabIndex = 1; + // + // dtpDateOfBirth + // + dtpDateOfBirth.Location = new Point(226, 163); + dtpDateOfBirth.Name = "dtpDateOfBirth"; + dtpDateOfBirth.Size = new Size(336, 21); + dtpDateOfBirth.TabIndex = 6; + // + // lblDateOfBirth + // + lblDateOfBirth.AutoSize = true; + lblDateOfBirth.ForeColor = Color.White; + lblDateOfBirth.Location = new Point(145, 165); + lblDateOfBirth.Name = "lblDateOfBirth"; + lblDateOfBirth.Size = new Size(75, 15); + lblDateOfBirth.TabIndex = 19; + lblDateOfBirth.Text = "Date Of Birth"; + // + // RegisterControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.Transparent; + Controls.Add(lblDateOfBirth); + Controls.Add(dtpDateOfBirth); + Controls.Add(label3); + Controls.Add(txtUsername); + Controls.Add(llblReshowLogin); + Controls.Add(btnRegister); + Controls.Add(cbTOSAgree); + Controls.Add(txtConfirmPassword); + Controls.Add(lblConfirmPassword); + Controls.Add(lblConfirmEmail); + Controls.Add(txtConfirmEmail); + Controls.Add(txtPassword); + Controls.Add(lblPassword); + Controls.Add(lblEmail); + Controls.Add(txtEmail); + Controls.Add(pbDefaultPfp); + Name = "RegisterControl"; + Size = new Size(597, 260); + ((System.ComponentModel.ISupportInitialize)pbDefaultPfp).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Krypton.Toolkit.KryptonPictureBox pbDefaultPfp; + private Krypton.Toolkit.KryptonTextBox txtPassword; + private Label lblPassword; + private Label lblEmail; + private Krypton.Toolkit.KryptonTextBox txtEmail; + private Krypton.Toolkit.KryptonTextBox txtConfirmPassword; + private Label lblConfirmPassword; + private Label lblConfirmEmail; + private Krypton.Toolkit.KryptonTextBox txtConfirmEmail; + private Krypton.Toolkit.KryptonCheckBox cbTOSAgree; + private Krypton.Toolkit.KryptonButton btnRegister; + private LinkLabel llblReshowLogin; + private Label label3; + private Krypton.Toolkit.KryptonTextBox txtUsername; + private Krypton.Toolkit.KryptonDateTimePicker dtpDateOfBirth; + private Label lblDateOfBirth; + } +} diff --git a/qtcnet-client/Controls/RegisterControl.cs b/qtcnet-client/Controls/RegisterControl.cs new file mode 100644 index 0000000..f631456 --- /dev/null +++ b/qtcnet-client/Controls/RegisterControl.cs @@ -0,0 +1,74 @@ +using Krypton.Toolkit; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Formats.Cbor; +using System.Text; +using System.Windows.Forms; + +namespace qtcnet_client.Controls +{ + public partial class RegisterControl : UserControl + { + public string Username { get; private set; } = string.Empty; + public string Email { get; private set; } = string.Empty; + public string Password { get; private set; } = string.Empty; + public DateTime DateOfBirth { get; private set; } = DateTime.MinValue; + + public event EventHandler? OnReshowLoginPressed; + public event EventHandler? OnRegister; + public RegisterControl() + { + InitializeComponent(); + } + + private void llblReshowLogin_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + ToggleControls(false); + OnReshowLoginPressed?.Invoke(this, EventArgs.Empty); + } + + private void btnRegister_Click(object sender, EventArgs e) + { + if(ValidateForm()) + { + ToggleControls(false); + + Username = txtUsername.Text; + Email = txtEmail.Text; + Password = txtPassword.Text; + DateOfBirth = dtpDateOfBirth.Value; + OnRegister?.Invoke(this, EventArgs.Empty); + } + else + { + KryptonMessageBox.Show("A Required Field Is Missing. Please Complete The Form.", "Oops."); + ToggleControls(true); + } + } + + public void ToggleControls(bool toggle) + { + txtUsername.Enabled = toggle; + txtEmail.Enabled = toggle; + txtPassword.Enabled = toggle; + txtConfirmEmail.Enabled = toggle; + txtConfirmPassword.Enabled = toggle; + cbTOSAgree.Enabled = toggle; + dtpDateOfBirth.Enabled = toggle; + llblReshowLogin.Enabled = toggle; + btnRegister.Enabled = toggle; + } + + private bool ValidateForm() + { + return !string.IsNullOrEmpty(txtEmail.Text) && + !string.IsNullOrEmpty(txtPassword.Text) && + (txtConfirmEmail.Text == txtEmail.Text) && + (txtConfirmPassword.Text == txtPassword.Text) && + cbTOSAgree.Checked; + } + } +} diff --git a/qtcnet-client/Controls/RegisterControl.resx b/qtcnet-client/Controls/RegisterControl.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/qtcnet-client/Controls/RegisterControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/qtcnet-client/Controls/RoomControl.Designer.cs b/qtcnet-client/Controls/RoomControl.Designer.cs new file mode 100644 index 0000000..2a2a35b --- /dev/null +++ b/qtcnet-client/Controls/RoomControl.Designer.cs @@ -0,0 +1,97 @@ +namespace qtcnet_client.Controls +{ + partial class RoomControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + pbIcon = new PictureBox(); + lblRoomName = new Label(); + lblRoomUserCount = new Label(); + ((System.ComponentModel.ISupportInitialize)pbIcon).BeginInit(); + SuspendLayout(); + // + // pbIcon + // + pbIcon.Image = Properties.Resources.RoomsChatIcon; + pbIcon.Location = new Point(3, 3); + pbIcon.Name = "pbIcon"; + pbIcon.Size = new Size(40, 35); + pbIcon.SizeMode = PictureBoxSizeMode.Zoom; + pbIcon.TabIndex = 0; + pbIcon.TabStop = false; + // + // lblRoomName + // + lblRoomName.AutoSize = true; + lblRoomName.Font = new Font("Segoe UI", 8F, FontStyle.Bold, GraphicsUnit.Point, 0); + lblRoomName.Location = new Point(49, 14); + lblRoomName.Name = "lblRoomName"; + lblRoomName.Size = new Size(38, 13); + lblRoomName.TabIndex = 1; + lblRoomName.Text = "Room"; + lblRoomName.DoubleClick += lblRoomName_DoubleClick; + lblRoomName.MouseLeave += lblRoomName_MouseLeave; + lblRoomName.MouseHover += lblRoomName_MouseHover; + // + // lblRoomUserCount + // + lblRoomUserCount.AutoSize = true; + lblRoomUserCount.Dock = DockStyle.Right; + lblRoomUserCount.Font = new Font("Segoe UI Semibold", 9F, FontStyle.Bold | FontStyle.Italic, GraphicsUnit.Point, 0); + lblRoomUserCount.ForeColor = Color.Silver; + lblRoomUserCount.Location = new Point(154, 0); + lblRoomUserCount.Name = "lblRoomUserCount"; + lblRoomUserCount.Padding = new Padding(0, 15, 0, 0); + lblRoomUserCount.Size = new Size(14, 30); + lblRoomUserCount.TabIndex = 2; + lblRoomUserCount.Text = "0"; + lblRoomUserCount.TextAlign = ContentAlignment.MiddleCenter; + // + // RoomControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.Transparent; + Controls.Add(lblRoomUserCount); + Controls.Add(lblRoomName); + Controls.Add(pbIcon); + DoubleBuffered = true; + Name = "RoomControl"; + Size = new Size(168, 42); + Load += RoomControl_Load; + ((System.ComponentModel.ISupportInitialize)pbIcon).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private PictureBox pbIcon; + private Label lblRoomName; + private Label lblRoomUserCount; + } +} diff --git a/qtcnet-client/Controls/RoomControl.cs b/qtcnet-client/Controls/RoomControl.cs new file mode 100644 index 0000000..5560735 --- /dev/null +++ b/qtcnet-client/Controls/RoomControl.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Drawing; +using System.Text; +using System.Windows.Forms; + +namespace qtcnet_client.Controls +{ + public partial class RoomControl : UserControl + { + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public string RoomId { get; set; } = string.Empty; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string RoomName { get; set; } = "Room"; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int UserCount { get; set; } = 0; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public bool IsUserCountVisible { get; set; } = true; + + public event EventHandler? OnRoomDoubleClicked; + + bool _isHovering = false; + public RoomControl() + { + InitializeComponent(); + + AutoSize = false; + Height = 59; + } + + private void RoomControl_Load(object sender, EventArgs e) + { + lblRoomName.Text = RoomName; + lblRoomUserCount.Text = UserCount.ToString(); + lblRoomUserCount.Visible = IsUserCountVisible; + } + + private void lblRoomName_MouseHover(object sender, EventArgs e) + { + if (!_isHovering) + { + lblRoomName.ForeColor = Color.White; + _isHovering = true; + } + } + + private void lblRoomName_MouseLeave(object sender, EventArgs e) + { + if (_isHovering) + { + lblRoomName.ForeColor = Color.Black; + _isHovering = false; + } + } + + private void lblRoomName_DoubleClick(object sender, EventArgs e) => OnRoomDoubleClicked?.Invoke(this, EventArgs.Empty); + } +} diff --git a/qtcnet-client/Controls/RoomControl.resx b/qtcnet-client/Controls/RoomControl.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/qtcnet-client/Controls/RoomControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/qtcnet-client/Forms/ChatRoomForm.Designer.cs b/qtcnet-client/Forms/ChatRoomForm.Designer.cs new file mode 100644 index 0000000..c3ef501 --- /dev/null +++ b/qtcnet-client/Forms/ChatRoomForm.Designer.cs @@ -0,0 +1,149 @@ +namespace qtcnet_client.Forms +{ + partial class ChatRoomForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ChatRoomForm)); + lblUsersOnline = new Label(); + rtxtChatbox = new RichTextBox(); + btnSend = new Button(); + lblRoomName = new Label(); + lvUsers = new ListView(); + flpMessages = new FlowLayoutPanel(); + ilStatusIcons = new ImageList(components); + SuspendLayout(); + // + // lblUsersOnline + // + lblUsersOnline.AutoSize = true; + lblUsersOnline.Font = new Font("Segoe UI", 10F, FontStyle.Bold); + lblUsersOnline.ForeColor = Color.White; + lblUsersOnline.Location = new Point(12, 47); + lblUsersOnline.Name = "lblUsersOnline"; + lblUsersOnline.Size = new Size(92, 19); + lblUsersOnline.TabIndex = 1; + lblUsersOnline.Text = "Users Online"; + // + // rtxtChatbox + // + rtxtChatbox.Location = new Point(169, 318); + rtxtChatbox.Name = "rtxtChatbox"; + rtxtChatbox.Size = new Size(508, 66); + rtxtChatbox.TabIndex = 3; + rtxtChatbox.Text = ""; + rtxtChatbox.KeyDown += rtxtChatbox_KeyDown; + // + // btnSend + // + btnSend.BackgroundImage = Properties.Resources.SendIcon; + btnSend.BackgroundImageLayout = ImageLayout.Zoom; + btnSend.FlatAppearance.BorderSize = 0; + btnSend.FlatStyle = FlatStyle.Flat; + btnSend.Location = new Point(683, 329); + btnSend.Name = "btnSend"; + btnSend.Size = new Size(75, 44); + btnSend.TabIndex = 4; + btnSend.UseVisualStyleBackColor = true; + btnSend.Click += btnSend_Click; + // + // lblRoomName + // + lblRoomName.AutoSize = true; + lblRoomName.Font = new Font("Segoe UI", 20F, FontStyle.Bold | FontStyle.Italic); + lblRoomName.ForeColor = Color.White; + lblRoomName.Location = new Point(6, 4); + lblRoomName.Name = "lblRoomName"; + lblRoomName.Size = new Size(91, 37); + lblRoomName.TabIndex = 5; + lblRoomName.Text = "Room"; + // + // lvUsers + // + lvUsers.Location = new Point(12, 69); + lvUsers.Name = "lvUsers"; + lvUsers.Size = new Size(151, 315); + lvUsers.SmallImageList = ilStatusIcons; + lvUsers.TabIndex = 6; + lvUsers.UseCompatibleStateImageBehavior = false; + lvUsers.View = View.List; + // + // flpMessages + // + flpMessages.AutoScroll = true; + flpMessages.BackColor = Color.White; + flpMessages.FlowDirection = FlowDirection.TopDown; + flpMessages.Location = new Point(169, 69); + flpMessages.Name = "flpMessages"; + flpMessages.Size = new Size(589, 243); + flpMessages.TabIndex = 7; + flpMessages.WrapContents = false; + // + // ilStatusIcons + // + ilStatusIcons.ColorDepth = ColorDepth.Depth32Bit; + ilStatusIcons.ImageStream = (ImageListStreamer)resources.GetObject("ilStatusIcons.ImageStream"); + ilStatusIcons.TransparentColor = Color.Transparent; + ilStatusIcons.Images.SetKeyName(0, "Offline"); + ilStatusIcons.Images.SetKeyName(1, "Online"); + ilStatusIcons.Images.SetKeyName(2, "Away"); + ilStatusIcons.Images.SetKeyName(3, "DoNotDisturb"); + // + // ChatRoomForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.DodgerBlue; + ClientSize = new Size(770, 396); + Controls.Add(flpMessages); + Controls.Add(lvUsers); + Controls.Add(lblRoomName); + Controls.Add(btnSend); + Controls.Add(rtxtChatbox); + Controls.Add(lblUsersOnline); + FormBorderStyle = FormBorderStyle.FixedSingle; + MaximizeBox = false; + Name = "ChatRoomForm"; + StartPosition = FormStartPosition.CenterScreen; + Text = "QtC.NET Chat Room"; + FormClosed += ChatRoomForm_FormClosed; + Load += ChatRoomForm_Load; + ResumeLayout(false); + PerformLayout(); + } + + #endregion + private Label lblUsersOnline; + private RichTextBox rtxtChatbox; + private Button btnSend; + private Label lblRoomName; + private ListView lvUsers; + private FlowLayoutPanel flpMessages; + private ImageList ilStatusIcons; + } +} \ No newline at end of file diff --git a/qtcnet-client/Forms/ChatRoomForm.cs b/qtcnet-client/Forms/ChatRoomForm.cs new file mode 100644 index 0000000..339b1af --- /dev/null +++ b/qtcnet-client/Forms/ChatRoomForm.cs @@ -0,0 +1,105 @@ +using qtcnet_client.Controls; +using QtCNETAPI.Dtos.User; +using System.ComponentModel; +namespace qtcnet_client.Forms +{ + public partial class ChatRoomForm : Form + { + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public string RoomId { get; set; } = string.Empty; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string RoomName { get; set; } = "Room"; + public string SentMessage { get; private set; } = string.Empty; + + public event EventHandler? OnSend; + public event EventHandler? OnClose; + + public ChatRoomForm() + { + InitializeComponent(); + } + + private void ChatRoomForm_Load(object sender, EventArgs e) + { + Text = $"QtC.NET Chat Room - {RoomName}"; + lblRoomName.Text = RoomName; + } + + private void rtxtChatbox_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Enter) + { + if (ValidateChatBox()) + { + SentMessage = rtxtChatbox.Text; + OnSend?.Invoke(this, EventArgs.Empty); + rtxtChatbox.Clear(); + } + } + } + + private void btnSend_Click(object sender, EventArgs e) + { + if (ValidateChatBox()) + { + SentMessage = rtxtChatbox.Text; + OnSend?.Invoke(this, EventArgs.Empty); + rtxtChatbox.Clear(); + } + } + + private void ChatRoomForm_FormClosed(object sender, FormClosedEventArgs e) + { + OnClose?.Invoke(this, EventArgs.Empty); + Close(); + } + + public void AddUsersToRoomList(List users) + { + lvUsers.SuspendLayout(); + + lvUsers.Items.Clear(); + + List lvis = []; + foreach (UserInformationDto user in users) + { + lvis.Add(new ListViewItem + { + Tag = user.Id, + Text = user.Username, + ImageIndex = user.Status + }); + } + + lvUsers.Items.AddRange([.. lvis.DistinctBy(u => u.Tag)]); + + lvUsers.ResumeLayout(true); + } + + public void AddUserToRoomList(UserInformationDto user) + { + ListViewItem lvi = new() + { + Tag = user.Id, + Text = user.Username, + ImageIndex = user.Status + }; + + lvUsers.Items.Add(lvi); + } + + public void AddMessage(MessageControl messageCtrl) + { + messageCtrl.Width = flpMessages.ClientSize.Width - 10; + messageCtrl.Margin = new Padding(0, 0, 0, 6); + + flpMessages.Controls.Add(messageCtrl); + flpMessages.ScrollControlIntoView(messageCtrl); + } + + private bool ValidateChatBox() + { + return !string.IsNullOrWhiteSpace(rtxtChatbox.Text) || !string.IsNullOrEmpty(rtxtChatbox.Text); + } + } +} diff --git a/qtcnet-client/Forms/ChatRoomForm.resx b/qtcnet-client/Forms/ChatRoomForm.resx new file mode 100644 index 0000000..6d83f4c --- /dev/null +++ b/qtcnet-client/Forms/ChatRoomForm.resx @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 16, 13 + + + + AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs + LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu + SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAGhQAAAJNU0Z0AUkBTAIBAQQB + AAGYAQABmAEAARABAAEQAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABQAMAASADAAEBAQABIAYAASD/ + AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AC4AAwYBBwM0AVQDUQGiA14B0gNaAekDYAHoA10B + 0QNQAZ8DMQFNAwUBBhgAAwYBBwM0AVQDUQGiA14B0gNaAekDYAHoA10B0QNQAZ8DMQFNAwUBBhgAAwYB + BwM0AVQDUQGiA14B0gNaAekDYAHoA10B0QNQAZ8DMQFNAwUBBhgAAwYBBwM0AVQDUQGiA14B0gNaAekD + YAHoA10B0QNQAZ8DMQFNAwUBBhQAAyABLQNUAasDWwHkA0wB9QMkAfsDNwH+AzcB/gMkAfsDUwH0A2IB + 4QNRAaEDHgEqEAADIAEtA1QBqwNbAeQBRgFYAUYB9QEhAVMBIQH7ARMBUwETAf4BEwFTARMB/gEhAVMB + IQH7AU8BUwFPAfQDYgHhA1EBoQMeASoQAAMgAS0DVAGrA1sB5AFGAlgB9QEhAlMB+wETAlMB/gETAlMB + /gEhAlMB+wFPAlMB9ANiAeEDUQGhAx4BKhAAAyABLQNUAasDWwHkAkYBWAH1AiEBUwH7AhMBUwH+AhMB + UwH+AiEBUwH7Ak8BUwH0A2IB4QNRAaEDHgEqDAADGwElA1gBvQNaAfIDOwH+AzAB/wM5Af8DPAH/AzYB + /wMqAf8DJAH/A0AB/QNZAfADVgGyAxoBIwgAAxsBJQNYAb0BVwFaAVcB8gETAVsBEwH+AQABVwEAAf8B + AAFnAQAB/wEAAWwBAAH/AQABYQEAAf8BAAFMAQAB/wEAAUABAAH/ASYBQAEmAf0BVAFeAVQB8ANWAbIB + GQEaARkBIwgAAxsBJQNYAb0BVwJaAfIBEwJbAf4BAAJXAf8BAAJnAf8BAAJsAf8BAAJhAf8BAAJMAf8B + AAJAAf8BJgJAAf0BVAJeAfADVgGyARkCGgEjCAADGwElA1gBvQJXAVoB8gITAVsB/gIAAVcB/wIAAWcB + /wIAAWwB/wIAAWEB/wIAAUwB/wIAAUAB/wImAUAB/QJUAV4B8ANWAbICGQEaASMEAAMDAQQDUgGlA18B + 8wNJAf8DVQH/A2UB/wNxAf8DdQH/A3EB/wNkAf8DTAH/AzEB/wM3Af4DXQHuA1ABmgMDAQQDAwEEAVIB + UwFSAaUBUQFvAVEB8wEAAYIBAAH/AQABmQEAAf8BAAG2AQAB/wEAAcwBAAH/AQAB0wEAAf8BAAHLAQAB + /wEAAbMBAAH/AQABiAEAAf8BAAFXAQAB/wETAVMBEwH+AVoBYQFaAe4DUAGaAwMBBAMDAQQBUgJTAaUB + UQJvAfMBAAKCAf8BAAKZAf8BAAK2Af8BAALMAf8BAALTAf8BAALLAf8BAAKzAf8BAAKIAf8BAAJXAf8B + EwJTAf4BWgJhAe4DUAGaAwMBBAMDAQQCUgFTAaUCUQFvAfMCAAGCAf8CAAGZAf8CAAG2Af8CAAHMAf8C + AAHTAf8CAAHLAf8CAAGzAf8CAAGIAf8CAAFXAf8CEwFTAf4CWgFhAe4DUAGaAwMBBAMtAUQDYAHoA3YB + /gNuAf8DewH/A4UB/wOKAf8DjAH/A4oB/wOFAf8DdgH/A1cB/wMyAf8DQAH9A14B3QMqAT8DLQFEAWAB + aQFgAegBEwGOARMB/gEAAcYBAAH/AQAB3AEAAf8BAAHuAQAB/wEAAfgBAAH/AQAB+wEAAf8BAAH5AQAB + /wEAAe8BAAH/AQAB1AEAAf8BAAGcAQAB/wEAAVoBAAH/ASYBQAEmAf0DXgHdAyoBPwMtAUQBYAJpAegB + EwKOAf4BAALGAf8BAALcAf8BAALuAf8BAAL4Af8BAAL7Af8BAAL5Af8BAALvAf8BAALUAf8BAAKcAf8B + AAJaAf8BJgJAAf0DXgHdAyoBPwMtAUQCYAFpAegCEwGOAf4CAAHGAf8CAAHcAf8CAAHuAf8CAAH4Af8C + AAH7Af8CAAH5Af8CAAHvAf8CAAHUAf8CAAGcAf8CAAFaAf8CJgFAAf0DXgHdAyoBPwNOAZUDdwH4A38B + /wOFAf8DigH/A40B/wOOAf8DjgH/A44B/wONAf8DiQH/A3cB/wNNAf8DJQH/A1oB8gNKAYsDTgGVAT0B + kAE9AfgBAAHlAQAB/wEAAe8BAAH/AQAB+AEAAf8BAAH9AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB + /wEAAf4BAAH/AQAB9gEAAf8BAAHVAQAB/wEAAYsBAAH/AQABQQEAAf8BVwFaAVcB8gNKAYsDTgGVAT0C + kAH4AQAC5QH/AQAC7wH/AQAC+AH/AQAC/QH/AQAD/wEAA/8BAAP/AQAC/gH/AQAC9gH/AQAC1QH/AQAC + iwH/AQACQQH/AVcCWgHyA0oBiwNOAZUCPQGQAfgCAAHlAf8CAAHvAf8CAAH4Af8CAAH9Af8CAAL/AgAC + /wIAAv8CAAH+Af8CAAH2Af8CAAHVAf8CAAGLAf8CAAFBAf8CVwFaAfIDSgGLA18B0wN+AfwDkwH/A44B + /wONAf8DjgH/A44B/wOOAf8DjgH/A44B/wONAf8DhQH/A2cB/wM0Af8DQQH5A1oBxAFbAV8BWwHTASsB + ugErAfwBDgH7AQ4B/wEDAf0BAwH/AQAB/gEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB + /wEAAf8BAAH/AQAB/QEAAf8BAAHvAQAB/wEAAbkBAAH/AQABXQEAAf8BOAFBATgB+QNaAcQBWwJfAdMB + KwK6AfwBDgL7Af8BAwL9Af8BAAL+Af8BAAP/AQAD/wEAA/8BAAP/AQAD/wEAAv0B/wEAAu8B/wEAArkB + /wEAAl0B/wE4AkEB+QNaAcQCWwFfAdMCKwG6AfwCDgH7Af8CAwH9Af8CAAH+Af8CAAL/AgAC/wIAAv8C + AAL/AgAC/wIAAf0B/wIAAe8B/wIAAbkB/wIAAV0B/wI4AUEB+QNaAcQDbgH1A4AB/gOfAf8DkwH/A48B + /wOOAf8DjgH/A44B/wOOAf8DjgH/A44B/wOLAf8DdwH/A0gB/wNAAf0DYgHhAUsBgwFLAfUBNwHZATcB + /gEnAf8BJwH/AQsB/wELAf8BAQH/AQEB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB + /wEAAf8BAAH/AQAB/wEAAfkBAAH/AQAB1gEAAf8BAAGBAQAB/wEmAUABJgH9A2IB4QFLAoMB9QE3AtkB + /gEnA/8BCwP/AQED/wEAA/8BAAP/AQAD/wEAA/8BAAP/AQAD/wEAAvkB/wEAAtYB/wEAAoEB/wEmAkAB + /QNiAeECSwGDAfUCNwHZAf4CJwL/AgsC/wIBAv8CAAL/AgAC/wIAAv8CAAL/AgAC/wIAAv8CAAH5Af8C + AAHWAf8CAAGBAf8CJgFAAf0DYgHhA3QB9gOIAf4DqwH/A5kB/wOQAf8DjgH/A44B/wOOAf8DjgH/A44B + /wOOAf8DjQH/A38B/wNVAf8DQAH9A14B4gFIAYcBSAH2AVoB2QFaAf4BQgH/AUIB/wEZAf8BGQH/AQQB + /wEEAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH9AQAB + /wEAAeQBAAH/AQABmAEAAf8BJgFAASYB/QNeAeIBSAKHAfYBWgLZAf4BQgP/ARkD/wEEA/8BAAP/AQAD + /wEAA/8BAAP/AQAD/wEAA/8BAAL9Af8BAALkAf8BAAKYAf8BJgJAAf0DXgHiAkgBhwH2AloB2QH+AkIC + /wIZAv8CBAL/AgAC/wIAAv8CAAL/AgAC/wIAAv8CAAL/AgAB/QH/AgAB5AH/AgABmAH/AiYBQAH9A14B + 4gNhAdYDiQH8A7gB/wOjAf8DkwH/A44B/wOOAf8DjgH/A44B/wOOAf8DjgH/A40B/wOCAf8DXAH/A00B + +gNaAccBXAFhAVwB1gFkAb4BZAH8AV8B/wFfAf8BLwH/AS8B/wEMAf8BDAH/AQEB/wEBAf8BAAH/AQAB + /wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB/gEAAf8BAAHqAQAB/wEAAaUBAAH/AScB + TQEnAfoDWgHHAVwCYQHWAWQCvgH8AV8D/wEvA/8BDAP/AQED/wEAA/8BAAP/AQAD/wEAA/8BAAP/AQAC + /gH/AQAC6gH/AQACpQH/AScCTQH6A1oBxwJcAWEB1gJkAb4B/AJfAv8CLwL/AgwC/wIBAv8CAAL/AgAC + /wIAAv8CAAL/AgAC/wIAAf4B/wIAAeoB/wIAAaUB/wInAU0B+gNaAccDUAGaA40B+QPFAf8DsgH/A5wB + /wORAf8DjgH/A44B/wOOAf8DjgH/A48B/wOOAf8DgwH/A2AB/wNaAfIDTAGQA1ABmgFqAaEBagH5AXwB + /wF8Af8BUQH/AVEB/wEfAf8BHwH/AQcB/wEHAf8BAQH/AQEB/wEAAf8BAAH/AQAB/wEAAf8BAAH/AQAB + /wECAf8BAgH/AQIB/gECAf8BAAHrAQAB/wEAAa0BAAH/AVcBawFXAfIDTAGQA1ABmgFqAqEB+QF8A/8B + UQP/AR8D/wEHA/8BAQP/AQAD/wEAA/8BAAP/AQID/wECAv4B/wEAAusB/wEAAq0B/wFXAmsB8gNMAZAD + UAGaAmoBoQH5AnwC/wJRAv8CHwL/AgcC/wIBAv8CAAL/AgAC/wIAAv8CAgL/AgIB/gH/AgAB6wH/AgAB + rQH/AlcBawHyA0wBkAMvAUkDbAHrA6oB/gPGAf8DrgH/A5wB/wOTAf8DkAH/A48B/wOQAf8DkwH/A5MB + /wOFAf8DUwH9A2AB4AMtAUUDLwFJA2wB6wGAAdkBgAH+AX8B/wF/Af8BSQH/AUkB/wEfAf8BHwH/AQwB + /wEMAf8BBQH/AQUB/wEDAf8BAwH/AQUB/wEFAf8BCgH/AQoB/wEKAf4BCgH/AQEB7QEBAf8BJgG2ASYB + /QFgAWYBYAHgAy0BRQMvAUkDbAHrAYAC2QH+AX8D/wFJA/8BHwP/AQwD/wEFA/8BAwP/AQUD/wEKA/8B + CgL+Af8BAQLtAf8BJgK2Af0BYAJmAeADLQFFAy8BSQNsAesCgAHZAf4CfwL/AkkC/wIfAv8CDAL/AgUC + /wIDAv8CBQL/AgoC/wIKAf4B/wIBAe0B/wImAbYB/QJgAWYB4AMtAUUDAwEEA1YBrgN8AfUD2QH/A8sB + /wO3Af8DpwH/A50B/wOaAf8DnAH/A58B/wObAf8DiQH/A2gB8ANSAaMDAwEEAwMBBANWAa4BbwGDAW8B + 9QGoAf8BqAH/AYkB/wGJAf8BXAH/AVwB/wE3Af8BNwH/ASIB/wEiAf8BGwH/ARsB/wEfAf8BHwH/ASYB + /wEmAf8BHQH/AR0B/wEFAfMBBQH/AVQBawFUAfADUgGjAwMBBAMDAQQDVgGuAW8CgwH1AagD/wGJA/8B + XAP/ATcD/wEiA/8BGwP/AR8D/wEmA/8BHQP/AQUC8wH/AVQCawHwA1IBowMDAQQDAwEEA1YBrgJvAYMB + 9QKoAv8CiQL/AlwC/wI3Av8CIgL/AhsC/wIfAv8CJgL/Ah0C/wIFAfMB/wJUAWsB8ANSAaMDAwEEBAAD + HAEnA10BxwN9AfYDuQH+A9cB/wPMAf8DwgH/A7sB/wO3Af8DsQH/A4AB/gNpAfQDWQG8AxsBJggAAxwB + JwNdAccBdwGHAXcB9gGRAdkBkQH+AaUB/wGlAf8BiwH/AYsB/wF0Af8BdAH/AWYB/wFmAf8BXAH/AVwB + /wFOAf8BTgH/AUMB2QFDAf4BUgF9AVIB9AFXAVkBVwG8AxsBJggAAxwBJwNdAccBdwKHAfYBkQLZAf4B + pQP/AYsD/wF0A/8BZgP/AVwD/wFOA/8BQwLZAf4BUgJ9AfQBVwJZAbwDGwEmCAADHAEnA10BxwJ3AYcB + 9gKRAdkB/gKlAv8CiwL/AnQC/wJmAv8CXAL/Ak4C/wJDAdkB/gJSAX0B9AJXAVkBvAMbASYMAAMhATAD + WQG2A2wB7gOfAfoDvgH9A9QB/wPMAf8DvgH9A4kB+QNsAesDVQGsAx8BLBAAAyEBMANZAbYBagFuAWoB + 7gGHAakBhwH6Aa4BxgGuAf0BnwH/AZ8B/wGMAf8BjAH/AWMBxgFjAf0BaAGhAWgB+QFhAWwBYQHrA1UB + rAMfASwQAAMhATADWQG2AWoCbgHuAYcCqQH6Aa4CxgH9AZ8D/wGMA/8BYwLGAf0BaAKhAfkBYQJsAesD + VQGsAx8BLBAAAyEBMANZAbYCagFuAe4ChwGpAfoCrgHGAf0CnwL/AowC/wJjAcYB/QJoAaEB+QJhAWwB + 6wNVAawDHwEsFAADBgEHAzYBWANVAawDZgHlA60B/AOYAfsDZQHiA1MBpwMzAVEDBgEHGAADBgEHAzYB + WANVAawDZgHlAX4BvgF+AfwBdwGyAXcB+wNlAeIDUwGnAzMBUQMGAQcYAAMGAQcDNgFYA1UBrANmAeUB + fgK+AfwBdwKyAfsDZQHiA1MBpwMzAVEDBgEHGAADBgEHAzYBWANVAawDZgHlAn4BvgH8AncBsgH7A2UB + 4gNTAacDMwFRAwYBBwwAAUIBTQE+BwABPgMAASgDAAFAAwABIAMAAQEBAAEBBgABARYAA/+BAAHgAQcB + 4AEHAeABBwHgAQcBwAEDAcABAwHAAQMBwAEDAYABAQGAAQEBgAEBAYABAVAAAYABAQGAAQEBgAEBAYAB + AQHAAQMBwAEDAcABAwHAAQMB4AEHAeABBwHgAQcB4AEHCw== + + + \ No newline at end of file diff --git a/qtcnet-client/Forms/JackpotSpinForm.Designer.cs b/qtcnet-client/Forms/JackpotSpinForm.Designer.cs new file mode 100644 index 0000000..4ddd83f --- /dev/null +++ b/qtcnet-client/Forms/JackpotSpinForm.Designer.cs @@ -0,0 +1,68 @@ +namespace qtcnet_client.Forms +{ + partial class JackpotSpinForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + lblSpinAnim = new Label(); + SuspendLayout(); + // + // lblSpinAnim + // + lblSpinAnim.Dock = DockStyle.Fill; + lblSpinAnim.Font = new Font("Segoe UI", 15F, FontStyle.Bold); + lblSpinAnim.ForeColor = Color.White; + lblSpinAnim.Location = new Point(0, 0); + lblSpinAnim.Name = "lblSpinAnim"; + lblSpinAnim.Size = new Size(150, 52); + lblSpinAnim.TabIndex = 0; + lblSpinAnim.Text = "9999 Q's Won"; + lblSpinAnim.TextAlign = ContentAlignment.MiddleCenter; + // + // JackpotSpinForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.DodgerBlue; + ClientSize = new Size(150, 52); + Controls.Add(lblSpinAnim); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + Name = "JackpotSpinForm"; + StartPosition = FormStartPosition.CenterScreen; + Text = "Jackpot Spin"; + FormClosed += JackpotSpinForm_FormClosed; + Load += JackpotSpinForm_Load; + ResumeLayout(false); + } + + #endregion + + private Label lblSpinAnim; + } +} \ No newline at end of file diff --git a/qtcnet-client/Forms/JackpotSpinForm.cs b/qtcnet-client/Forms/JackpotSpinForm.cs new file mode 100644 index 0000000..30a7d0c --- /dev/null +++ b/qtcnet-client/Forms/JackpotSpinForm.cs @@ -0,0 +1,83 @@ +using System; +using System.ComponentModel; +using System.Windows.Forms; + +namespace qtcnet_client.Forms +{ + public partial class JackpotSpinForm : Form + { + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public int CurrencyWon { get; set; } = 0; + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public double AnimationLengthMilliseconds { get; set; } = 1500; + + private readonly System.Windows.Forms.Timer _animationTimer; + private readonly System.Windows.Forms.Timer _stopTimer; + private readonly Random _rndInt = new(); + private bool _allowClose = true; + + public JackpotSpinForm() + { + InitializeComponent(); + FormClosing += JackpotSpinForm_FormClosing; + + _animationTimer = new() + { + Interval = 20 + }; + _animationTimer.Tick += AnimationTimer_Tick; + + _stopTimer = new(); + _stopTimer.Tick += StopTimer_Tick; + } + + private void JackpotSpinForm_Load(object sender, EventArgs e) + { + ExecuteAnimation(); + } + + private void JackpotSpinForm_FormClosing(object? sender, FormClosingEventArgs e) + { + if (!_allowClose) + e.Cancel = true; + } + + private void JackpotSpinForm_FormClosed(object sender, FormClosedEventArgs e) + { + DialogResult = DialogResult.OK; + } + + private void ExecuteAnimation() + { + DoubleBuffered = true; + _allowClose = false; + + // Start spinning numbers + _animationTimer.Start(); + + // Schedule stop + _stopTimer.Interval = (int)AnimationLengthMilliseconds; + _stopTimer.Start(); + } + + private void AnimationTimer_Tick(object? sender, EventArgs e) + { + if (!IsHandleCreated && IsDisposed) + return; + + lblSpinAnim.Text = $"{_rndInt.Next(999)} Q's Won"; + } + + private void StopTimer_Tick(object? sender, EventArgs e) + { + _stopTimer.Stop(); + _animationTimer.Stop(); + + // Show final result + lblSpinAnim.Text = $"{CurrencyWon} Q's Won"; + + _allowClose = true; + } + } +} \ No newline at end of file diff --git a/qtcnet-client/Forms/JackpotSpinForm.resx b/qtcnet-client/Forms/JackpotSpinForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/qtcnet-client/Forms/JackpotSpinForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/qtcnet-client/Forms/MainForm.Designer.cs b/qtcnet-client/Forms/MainForm.Designer.cs new file mode 100644 index 0000000..e5a603f --- /dev/null +++ b/qtcnet-client/Forms/MainForm.Designer.cs @@ -0,0 +1,52 @@ +namespace qtcnet_client +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + SuspendLayout(); + // + // MainForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(403, 672); + DoubleBuffered = true; + FormBorderStyle = FormBorderStyle.FixedSingle; + MaximizeBox = false; + Name = "MainForm"; + StartPosition = FormStartPosition.CenterScreen; + Text = "QtC.NET"; + Load += MainForm_Load; + Paint += MainForm_Paint; + Resize += MainForm_Resize; + ResumeLayout(false); + } + + #endregion + } +} diff --git a/qtcnet-client/Forms/MainForm.cs b/qtcnet-client/Forms/MainForm.cs new file mode 100644 index 0000000..f26f206 --- /dev/null +++ b/qtcnet-client/Forms/MainForm.cs @@ -0,0 +1,586 @@ +using Krypton.Toolkit; +using qtcnet_client.Controls; +using qtcnet_client.Forms; +using qtcnet_client.Model; +using QtCNETAPI.Dtos.User; +using QtCNETAPI.Models; +using QtCNETAPI.Services; +using QtCNETAPI.Services.ApiService; +using QtCNETAPI.Services.GatewayService; +using System.Diagnostics; +using System.Drawing.Drawing2D; + +namespace qtcnet_client +{ + public partial class MainForm : Form + { + private BrandingControl? BrandingControl; + private LoginControl? LoginControl; + private RegisterControl? RegisterControl; + private CurrentProfileControl? CurrentProfileControl; + private MainTabControl? MainTabControl; + + private readonly List OpenChatRoomForms = []; + private readonly List OpenProfileForms = []; + + private Size LoggedOutSize = new(615, 702); + private Size LoggedInSize = new(419, 715); + + private readonly IApiService _apiService; + private readonly IGatewayService _gatewayService; + private readonly LoggingService _loggingService; + private readonly CredentialService _credentialService; + private readonly ClientConfig _config; + + public MainForm(IApiService apiService, IGatewayService gatewayService, LoggingService loggingService, CredentialService credentialService, ClientConfig config) + { + _apiService = apiService; + _gatewayService = gatewayService; + _loggingService = loggingService; + _credentialService = credentialService; + _config = config; + + InitializeComponent(); + } + + private void MainForm_Paint(object sender, PaintEventArgs e) + { + Graphics g = e.Graphics; + Rectangle _formRect = ClientRectangle; + + if (_formRect.IsEmpty) + _formRect = e.ClipRectangle; + + // draw gradient background + using LinearGradientBrush b = new(_formRect, Color.White, Color.DodgerBlue, LinearGradientMode.Vertical); + g.FillRectangle(b, _formRect); + } + + private void MainForm_Resize(object sender, EventArgs e) => Invalidate(); + + private async void MainForm_Load(object sender, EventArgs e) + { + SuspendLayout(); + Size = LoggedOutSize; + + // add branding control + BrandingControl = new() + { + Location = new(-2, -17), + Anchor = AnchorStyles.Top | AnchorStyles.Left + }; + + // add login control + LoginControl = new() + { + Location = new(12, 233) + }; + LoginControl.OnSuccessfulLogin += LoginControl_OnSuccessfulLogin; + LoginControl.OnRegisterPressed += LoginControl_OnRegisterPressed; + + Controls.Add(BrandingControl); + Controls.Add(LoginControl); + + ResumeLayout(true); + + // ensure api is reachable before letting the user login + LoginControl.ToggleControls(false); + var _pingRes = await Task.Run(PingAPI); + if (_pingRes) + { + // check for existing login in credential service + var _token = _credentialService.GetAccessToken(); + if (_token != null) + { + // try it + var _refreshRes = await _apiService.RefreshLogin(_token); + if (_refreshRes.Success) + OnSuccessfulLoginRefresh(); + } + else + LoginControl.ToggleControls(true); + } + else + { + KryptonMessageBox.Show("The Server Is Unreachable. Ensure Your Config Is Correct. The Client Will Now Close", "Oops"); + Application.Exit(); + } + } + + private void LoginControl_OnRegisterPressed(object? sender, EventArgs e) + { + if (sender is LoginControl _) + { + // pause ui + SuspendLayout(); + + // remove and dispose login control + Controls.Remove(LoginControl); + LoginControl?.Dispose(); + LoginControl = null; + + // create register control + RegisterControl = new() + { + Location = new(1, 233) + }; + RegisterControl.OnReshowLoginPressed += RegisterControl_OnReshowLoginPressed; + RegisterControl.OnRegister += RegisterControl_OnRegister; + + Controls.Add(RegisterControl); + + ResumeLayout(true); + } + } + + private void RegisterControl_OnReshowLoginPressed(object? sender, EventArgs e) + { + if (sender is RegisterControl _) + { + // pause ui + SuspendLayout(); + + // remove and dispose register control + Controls.Remove(RegisterControl); + RegisterControl?.Dispose(); + RegisterControl = null; + + // add login control + LoginControl = new() + { + Location = new(12, 233) + }; + LoginControl.OnSuccessfulLogin += LoginControl_OnSuccessfulLogin; + LoginControl.OnRegisterPressed += LoginControl_OnRegisterPressed; + + Controls.Add(LoginControl); + + ResumeLayout(true); + } + } + + private async void RegisterControl_OnRegister(object? sender, EventArgs e) + { + if (sender is RegisterControl _senderCtrl) + { + // attempt registration + var res = await _apiService.RegisterAsync(new() + { + Username = _senderCtrl.Username, + Email = _senderCtrl.Email, + Password = _senderCtrl.Password, + DateOfBirth = _senderCtrl.DateOfBirth, + }); + + + if (res.Success && res.Data != null) + { + KryptonMessageBox.Show("Registration Success!\n\nNote: Some Servers Require Email Verification, Check Your Email!\n\nYou May Now Login.", "Success!"); + + // pause ui + SuspendLayout(); + + // remove and dispose register control + Controls.Remove(RegisterControl); + RegisterControl?.Dispose(); + RegisterControl = null; + + // add login control + LoginControl = new() + { + Location = new(12, 233) + }; + LoginControl.OnSuccessfulLogin += LoginControl_OnSuccessfulLogin; + LoginControl.OnRegisterPressed += LoginControl_OnRegisterPressed; + + Controls.Add(LoginControl); + + ResumeLayout(true); + } + else + { + KryptonMessageBox.Show($"Sorry, Something Went Wrong Registering You. Please Try Again Later.\n\nServer Response = {res.Message}", "Uh Oh.", + KryptonMessageBoxButtons.OK, + KryptonMessageBoxIcon.Error); + _senderCtrl.ToggleControls(true); + } + } + } + + private async void LoginControl_OnSuccessfulLogin(object? sender, EventArgs e) + { + if (sender is LoginControl _senderCtrl) + { + // login using function + var _loginRes = await OnSuccessfulLogin(new() { Email = _senderCtrl.Email, Password = _senderCtrl.Password, RememberMe = _senderCtrl.RememberMe }); + if(_loginRes) + { + // pause ui + SuspendLayout(); + + // remove and dispose login and branding controls + Controls.Remove(LoginControl); + Controls.Remove(BrandingControl); + + LoginControl?.Dispose(); + LoginControl = null; + BrandingControl?.Dispose(); + BrandingControl = null; + + // set size to logged in size + Size = LoggedInSize; + + // start gateway connection + var _gwRes = await SetupGatewayConnection(); + + if(_gwRes) + { + // setup current profile control based on current user + CurrentProfileControl = new() + { + Username = _apiService.CurrentUser!.Username, + CurrencyCount = _apiService.CurrentUser.CurrencyAmount, + Location = new(12, 12), + }; + + // get profile image for the current user + var _currentProfileImageRes = await _apiService.GetUserProfilePic(_apiService.CurrentUser.Id); + if (_currentProfileImageRes.Success && _currentProfileImageRes.Data != null) + { + using MemoryStream ms = new(_currentProfileImageRes.Data); + Image img = Image.FromStream(ms); + CurrentProfileControl.ProfileImage = img; + } + + // setup main tab control + MainTabControl = new() + { + Location = new(12, 91) + }; + + // add controls to current form + Controls.Add(CurrentProfileControl); + Controls.Add(MainTabControl); + + // get and set contacts + var _currentUserContacts = await _apiService.GetCurrentUserContacts(); + if (_currentUserContacts.Success && _currentUserContacts.Data != null) + await SetupContactsUI(_currentUserContacts.Data); + + // get and set user directory + var _currentUserDirectory = await _apiService.GetAllUsersAsync(); + if (_currentUserDirectory.Success && _currentUserDirectory.Data != null) + await SetupDirectoryUI(_currentUserDirectory.Data); + + ResumeLayout(true); + } + else + { + ResumeLayout(true); + KryptonMessageBox.Show("An Error Occured Trying To Connect To The Gateway. Please Try Again Later.", "Uh Oh.", KryptonMessageBoxButtons.OK, KryptonMessageBoxIcon.Error); + Application.Exit(); + } + } + } + } + + private void MainTabControl_OnRoomControlDoubleClicked(object? sender, EventArgs e) + { + if (sender is RoomControl _) + { + + } + } + + private void Chat_OnSend(object? sender, EventArgs e) + { + if (sender is ChatRoomForm _) + { + + } + } + + private void Chat_OnClose(object? sender, EventArgs e) + { + if (sender is ChatRoomForm chatRoom) + { + OpenChatRoomForms.Remove(chatRoom); + chatRoom.Dispose(); + } + } + + private void MainTabControl_OnContactControlDoubleClicked(object? sender, EventArgs e) + { + if (sender is ContactControl _) + { + + } + } + + private void ProfileForm_OnClose(object? sender, EventArgs e) + { + if (sender is ProfileForm profile) + { + OpenProfileForms.Remove(profile); + profile.Dispose(); + } + } + + private void _apiService_OnCurrentUserUpdate(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + private async Task PingAPI() + { + // ping it + var res = await _apiService.PingServerAsync(); + + // return result + return res.Success; + } + + private async Task SetupGatewayConnection() + { + // subscribe to gateway events + //_gatewayService.OnServerReconnecting += _gatewayService_OnServerReconnecting; + //_gatewayService.OnServerReconnected += _gatewayService_OnServerReconnected; + //_gatewayService.OnServerDisconnect += _gatewayService_OnServerDisconnect; + //_gatewayService.OnDirectMessageReceived += _gatewayService_OnDirectMessageReceived; + //_gatewayService.OnRefreshUserListsReceived += _gatewayService_OnRefreshUserListReceived; + //_gatewayService.OnRefreshRoomListReceived += _gatewayService_OnRefreshRoomListReceived; + //_gatewayService.OnRefreshContactsListReceived += _gatewayService_OnRefreshContactsListReceived; + //_gatewayService.OnServerConfigReceived += _gatewayService_OnServerConfigReceived; + //_gatewayService.OnUserForceLogout += _gatewayService_OnUserForceLogout; + + // start connection + await _gatewayService.StartAsync(); + return _gatewayService.HubConnection != null && _gatewayService.HubConnection.State == Microsoft.AspNetCore.SignalR.Client.HubConnectionState.Connected; + } + + private void _gatewayService_OnUserForceLogout(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + private void _gatewayService_OnServerConfigReceived(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + private void _gatewayService_OnRefreshContactsListReceived(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + private void _gatewayService_OnRefreshRoomListReceived(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + private void _gatewayService_OnRefreshUserListReceived(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + private void _gatewayService_OnServerDisconnect(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + private void _gatewayService_OnServerReconnected(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + private void _gatewayService_OnServerReconnecting(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + private void _gatewayService_OnDirectMessageReceived(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + private async Task SetupContactsUI(List data) + { + // build ctrl list + List _contactCtrls = []; + foreach (var contact in data) + { + var ctrl = await BuildContactControl(contact); + if (ctrl != null) + _contactCtrls.Add(ctrl); + } + + // add to control + MainTabControl?.AddContacts(_contactCtrls); + } + + private async Task SetupDirectoryUI(List data) + { + // build listviewitem list + List _userViews = []; + foreach(var user in data) + { + ListViewItem lvi = new() + { + Tag = user.Id, + Text = user.Username, + ImageIndex = user.Status, + }; + _userViews.Add(lvi); + } + + // add to control + MainTabControl?.AddUsers(_userViews); + } + + private async Task BuildContactControl(Contact contact) + { + if(_apiService.CurrentUser != null) + { + ServiceResponse user = null!; + if (contact.OwnerId == _apiService.CurrentUser.Id) + user = await _apiService.GetUserInformationAsync(contact.UserId); + else if (contact.UserId == _apiService.CurrentUser.Id) + user = await _apiService.GetUserInformationAsync(contact.OwnerId); + + if (user.Data != null) + { + var ctrl = new ContactControl + { + Username = user.Data.Username, + TextStatus = "NOT IMPLEMENTED", + Status = user.Data.Status, + }; + + if (contact.OwnerId == _apiService.CurrentUser.Id) + { + switch (contact.OwnerStatus) + { + case Contact.ContactStatus.AwaitingApprovalFromOther: + ctrl.Username = $"{user.Data.Username} [Request Sent]"; + //await AddProfilePicToList(user.Data.Id); + //ctrl.Avatar = ilProfilePics.Images[user.Data.Id] ?? ilProfilePics.Images[0]; + break; + case Contact.ContactStatus.Accepted: + ctrl.Username = user.Data.Username; + //await AddProfilePicToList(user.Data.Id); + //ctrl.Avatar = ilProfilePics.Images[user.Data.Id] ?? ilProfilePics.Images[0]; + break; + } + } + else if (contact.UserId == _apiService.CurrentUser.Id) + { + switch (contact.UserStatus) + { + case Contact.ContactStatus.AwaitingApprovalFromSelf: + ctrl.Username = $"{user.Data.Username} [Contact Request]"; + //await AddProfilePicToList(user.Data.Id); + //ctrl.Avatar = ilProfilePics.Images[user.Data.Id] ?? ilProfilePics.Images[0]; + //AudioService.PlaySoundEffect("sndContactRequest"); + break; + case Contact.ContactStatus.Accepted: + ctrl.Username = user.Data.Username; + //await AddProfilePicToList(user.Data.Id); + //ctrl.Avatar = ilProfilePics.Images[user.Data.Id] ?? ilProfilePics.Images[0]; + break; + } + } + + // return the control + return ctrl; + } + } + + return default; + } + + private async Task OnSuccessfulLogin(UserLoginDto dto) + { + // attempt login + var res = await _apiService.LoginAsync(dto); + + if (res.Success && res.Data != null && _apiService.CurrentUser != null) // "CurrentUser" should not be null on successful login but checking anyways + { + // store refresh token in credential store + _credentialService.SaveAccessToken(_apiService.CurrentUser.Username, res.Message); + + // sub to currentuser updates + _apiService.OnCurrentUserUpdate += _apiService_OnCurrentUserUpdate; + + return true; + } + else + { + KryptonMessageBox.Show(res.Message, "Login Error", KryptonMessageBoxButtons.OK, KryptonMessageBoxIcon.Error); + return false; + } + } + + private async void OnSuccessfulLoginRefresh() + { + if (_apiService.CurrentUser != null) + { + // remove and dispose login and branding controls + Controls.Remove(LoginControl); + Controls.Remove(BrandingControl); + + LoginControl?.Dispose(); + LoginControl = null; + BrandingControl?.Dispose(); + BrandingControl = null; + + // set size to logged in size + Size = LoggedInSize; + + // start gateway connection + var _gwRes = await SetupGatewayConnection(); + + if (_gwRes) + { + // setup current profile control based on current user + CurrentProfileControl = new() + { + Username = _apiService.CurrentUser!.Username, + CurrencyCount = _apiService.CurrentUser.CurrencyAmount, + Location = new(12, 12), + }; + + // get profile image for the current user + var _currentProfileImageRes = await _apiService.GetUserProfilePic(_apiService.CurrentUser.Id); + if (_currentProfileImageRes.Success && _currentProfileImageRes.Data != null) + { + using MemoryStream ms = new(_currentProfileImageRes.Data); + Image img = Image.FromStream(ms); + CurrentProfileControl.ProfileImage = img; + } + + // setup main tab control + MainTabControl = new() + { + Location = new(12, 91) + }; + + // add controls to current form + Controls.Add(CurrentProfileControl); + Controls.Add(MainTabControl); + + // get and set contacts + var _currentUserContacts = await _apiService.GetCurrentUserContacts(); + if (_currentUserContacts.Success && _currentUserContacts.Data != null) + await SetupContactsUI(_currentUserContacts.Data); + + // get and set user directory + var _currentUserDirectory = await _apiService.GetAllUsersAsync(); + if (_currentUserDirectory.Success && _currentUserDirectory.Data != null) + await SetupDirectoryUI(_currentUserDirectory.Data); + + ResumeLayout(true); + } + } + } + } +} diff --git a/qtcnet-client/Forms/MainForm.resx b/qtcnet-client/Forms/MainForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/qtcnet-client/Forms/MainForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/qtcnet-client/Forms/ProfileForm.Designer.cs b/qtcnet-client/Forms/ProfileForm.Designer.cs new file mode 100644 index 0000000..7d7ba8d --- /dev/null +++ b/qtcnet-client/Forms/ProfileForm.Designer.cs @@ -0,0 +1,166 @@ +namespace qtcnet_client.Forms +{ + partial class ProfileForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + pbProfileImage = new PictureBox(); + lblStatus = new Label(); + rtxtBio = new RichTextBox(); + tlpActionButtons = new TableLayoutPanel(); + tlpUsernameTags = new TableLayoutPanel(); + lblUsername = new Label(); + tlpTagIcons = new TableLayoutPanel(); + ((System.ComponentModel.ISupportInitialize)pbProfileImage).BeginInit(); + tlpUsernameTags.SuspendLayout(); + SuspendLayout(); + // + // pbProfileImage + // + pbProfileImage.Image = Properties.Resources.DefaultPfp; + pbProfileImage.Location = new Point(12, 12); + pbProfileImage.Name = "pbProfileImage"; + pbProfileImage.Size = new Size(97, 99); + pbProfileImage.SizeMode = PictureBoxSizeMode.Zoom; + pbProfileImage.TabIndex = 0; + pbProfileImage.TabStop = false; + // + // lblStatus + // + lblStatus.AutoEllipsis = true; + lblStatus.Font = new Font("Segoe UI Semibold", 9F, FontStyle.Bold, GraphicsUnit.Point, 0); + lblStatus.ForeColor = Color.White; + lblStatus.Location = new Point(12, 114); + lblStatus.Name = "lblStatus"; + lblStatus.Padding = new Padding(0, 0, 0, 15); + lblStatus.Size = new Size(97, 54); + lblStatus.TabIndex = 2; + lblStatus.Text = "Status"; + lblStatus.TextAlign = ContentAlignment.MiddleCenter; + // + // rtxtBio + // + rtxtBio.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + rtxtBio.Location = new Point(115, 69); + rtxtBio.Name = "rtxtBio"; + rtxtBio.ReadOnly = true; + rtxtBio.Size = new Size(302, 340); + rtxtBio.TabIndex = 4; + rtxtBio.Text = ""; + // + // tlpActionButtons + // + tlpActionButtons.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + tlpActionButtons.ColumnCount = 2; + tlpActionButtons.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); + tlpActionButtons.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); + tlpActionButtons.Location = new Point(296, 415); + tlpActionButtons.Name = "tlpActionButtons"; + tlpActionButtons.RowCount = 1; + tlpActionButtons.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); + tlpActionButtons.Size = new Size(121, 42); + tlpActionButtons.TabIndex = 5; + // + // tlpUsernameTags + // + tlpUsernameTags.AutoSize = true; + tlpUsernameTags.ColumnCount = 2; + tlpUsernameTags.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); + tlpUsernameTags.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); + tlpUsernameTags.Controls.Add(lblUsername, 0, 0); + tlpUsernameTags.Controls.Add(tlpTagIcons, 1, 0); + tlpUsernameTags.Location = new Point(115, 11); + tlpUsernameTags.Name = "tlpUsernameTags"; + tlpUsernameTags.RowCount = 1; + tlpUsernameTags.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); + tlpUsernameTags.Size = new Size(304, 55); + tlpUsernameTags.TabIndex = 6; + // + // lblUsername + // + lblUsername.AutoSize = true; + lblUsername.Dock = DockStyle.Left; + lblUsername.Font = new Font("Segoe UI", 20F, FontStyle.Bold); + lblUsername.ForeColor = Color.White; + lblUsername.Location = new Point(3, 0); + lblUsername.Name = "lblUsername"; + lblUsername.Size = new Size(146, 55); + lblUsername.TabIndex = 0; + lblUsername.Text = "Username"; + lblUsername.TextAlign = ContentAlignment.MiddleLeft; + // + // tlpTagIcons + // + tlpTagIcons.AutoSize = true; + tlpTagIcons.AutoSizeMode = AutoSizeMode.GrowAndShrink; + tlpTagIcons.ColumnCount = 1; + tlpTagIcons.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); + tlpTagIcons.GrowStyle = TableLayoutPanelGrowStyle.AddColumns; + tlpTagIcons.Location = new Point(155, 3); + tlpTagIcons.MinimumSize = new Size(145, 49); + tlpTagIcons.Name = "tlpTagIcons"; + tlpTagIcons.RowCount = 1; + tlpTagIcons.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); + tlpTagIcons.Size = new Size(145, 49); + tlpTagIcons.TabIndex = 1; + // + // ProfileForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + AutoSize = true; + BackColor = Color.DodgerBlue; + ClientSize = new Size(429, 463); + Controls.Add(tlpUsernameTags); + Controls.Add(tlpActionButtons); + Controls.Add(rtxtBio); + Controls.Add(lblStatus); + Controls.Add(pbProfileImage); + MaximizeBox = false; + Name = "ProfileForm"; + StartPosition = FormStartPosition.CenterScreen; + Text = "QtC.NET User Profile"; + FormClosed += ProfileForm_FormClosed; + Load += ProfileForm_Load; + ((System.ComponentModel.ISupportInitialize)pbProfileImage).EndInit(); + tlpUsernameTags.ResumeLayout(false); + tlpUsernameTags.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private PictureBox pbProfileImage; + private Label lblStatus; + private RichTextBox rtxtBio; + private TableLayoutPanel tlpActionButtons; + private TableLayoutPanel tlpUsernameTags; + private Label lblUsername; + private TableLayoutPanel tlpTagIcons; + } +} \ No newline at end of file diff --git a/qtcnet-client/Forms/ProfileForm.cs b/qtcnet-client/Forms/ProfileForm.cs new file mode 100644 index 0000000..f0766b5 --- /dev/null +++ b/qtcnet-client/Forms/ProfileForm.cs @@ -0,0 +1,52 @@ +using qtcnet_client.Properties; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Text; +using System.Windows.Forms; + +namespace qtcnet_client.Forms +{ + public partial class ProfileForm : Form + { + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string Username { get; set; } = "Username"; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string Bio { get; set; } = string.Empty; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public int Status { get; set; } = 0; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string TextStatus { get; set; } = string.Empty; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public string[] Tags { get; set; } = []; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public int ContactStatus { get; set; } = 0; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + public Image ProfileImage { get; set; } = Resources.DefaultPfp; + + public event EventHandler? OnClose; + public ProfileForm() + { + InitializeComponent(); + } + + private void ProfileForm_Load(object sender, EventArgs e) + { + Text = $"QtC.NET User Profile - {Username}"; + lblUsername.Text = Username; + lblStatus.Text = TextStatus; + rtxtBio.Text = Bio; + pbProfileImage.Image = ProfileImage; + + if (Status == 0) lblStatus.Visible = false; + } + + private void ProfileForm_FormClosed(object sender, FormClosedEventArgs e) + { + OnClose?.Invoke(this, EventArgs.Empty); + Close(); + } + } +} diff --git a/qtcnet-client/Forms/ProfileForm.resx b/qtcnet-client/Forms/ProfileForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/qtcnet-client/Forms/ProfileForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/qtcnet-client/Model/ClientConfig.cs b/qtcnet-client/Model/ClientConfig.cs new file mode 100644 index 0000000..6d938dc --- /dev/null +++ b/qtcnet-client/Model/ClientConfig.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace qtcnet_client.Model +{ + public class ClientConfig + { + [JsonPropertyName("serverUri")] + public string ServerBaseUri { get; set; } = "https://api.qtchat.net"; + [JsonPropertyName("startMinimized")] + public bool StartMinimized { get; set; } = false; + [JsonPropertyName("minimizeToTray")] + public bool MinimizeToTray { get; set; } = false; + [JsonPropertyName("enableDebugLogs")] + public bool EnableDebugLogs { get; set; } = false; + } +} diff --git a/qtcnet-client/Program.cs b/qtcnet-client/Program.cs new file mode 100644 index 0000000..ad00838 --- /dev/null +++ b/qtcnet-client/Program.cs @@ -0,0 +1,98 @@ +using Krypton.Toolkit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using qtcnet_client.Model; +using QtCNETAPI.Services; +using QtCNETAPI.Services.ApiService; +using QtCNETAPI.Services.GatewayService; +using System.Text.Json; + +namespace qtcnet_client +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + + var host = CreateHostBuilder().Build(); + ServiceProvider = host.Services; + + Application.Run(ServiceProvider.GetRequiredService()); + } + + private static JsonSerializerOptions JsonSerializerOptions { get; } = new() { WriteIndented = true }; + public static IServiceProvider? ServiceProvider { get; private set; } + static IHostBuilder CreateHostBuilder() + { + return Host.CreateDefaultBuilder() + .ConfigureServices((context, service) => + { + service.AddTransient(); + service.AddSingleton(); + service.AddSingleton(); + service.AddSingleton(provider => GetOrCreateClientConfig()); + service.AddSingleton(provider => new ApiService(provider.GetService()!.ServerBaseUri + "/api", provider.GetService()!, provider.GetService()!)); + service.AddSingleton(provider => new GatewayService(provider.GetService()!.ServerBaseUri + "/chat", provider.GetService()!, provider.GetService()!)); + }); + } + + static ClientConfig GetOrCreateClientConfig() + { + var _configPath = $"{Application.StartupPath}/config.json"; + try + { + if (!File.Exists(_configPath)) + { + var _defaultConfig = new ClientConfig(); + + // create the config using the default config model + string serializedModel = JsonSerializer.Serialize(_defaultConfig, JsonSerializerOptions); + + // write it to a new file + File.WriteAllText(_configPath, serializedModel); + + // return it + return _defaultConfig; + } + else + { + // deserialize the contents of config.json and return it + ClientConfig? _deserializedConfig = JsonSerializer.Deserialize(File.ReadAllText(_configPath)); + if (_deserializedConfig != null) + return _deserializedConfig; + } + } + catch (JsonException) + { + } + + // if the functions gets here, inform the user their config is incorrect and create a new one if allowed + var _diagResult = KryptonMessageBox.Show($"Your Config Is Incorrect. Would You Like To Create A New One?", "Uh Oh.", KryptonMessageBoxButtons.YesNo); + if (_diagResult == DialogResult.Yes) + { + var _defaultConfig = new ClientConfig(); + + // create the config using the default config model + string serializedModel = JsonSerializer.Serialize(_defaultConfig, JsonSerializerOptions); + + // write it to a new file + File.WriteAllText(_configPath, serializedModel); + + // return it + return _defaultConfig; + } + else + { + Application.Exit(); + return default!; // application should exit before it can return this lol + } + } + } +} \ No newline at end of file diff --git a/qtcnet-client/Properties/Resources.Designer.cs b/qtcnet-client/Properties/Resources.Designer.cs new file mode 100644 index 0000000..90df4f7 --- /dev/null +++ b/qtcnet-client/Properties/Resources.Designer.cs @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace qtcnet_client.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("qtcnet_client.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap CurrencyIcon { + get { + object obj = ResourceManager.GetObject("CurrencyIcon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap DefaultPfp { + get { + object obj = ResourceManager.GetObject("DefaultPfp", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap QtCNETIcon { + get { + object obj = ResourceManager.GetObject("QtCNETIcon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap RoomsChatIcon { + get { + object obj = ResourceManager.GetObject("RoomsChatIcon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap SendIcon { + get { + object obj = ResourceManager.GetObject("SendIcon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/qtcnet-client/Properties/Resources.resx b/qtcnet-client/Properties/Resources.resx new file mode 100644 index 0000000..58b64b5 --- /dev/null +++ b/qtcnet-client/Properties/Resources.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\DefaultPfp.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\QtCNETIcon.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\CurrencyIcon.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\RoomsChatIcon.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\SendIcon.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/qtcnet-client/Resources/CurrencyIcon.png b/qtcnet-client/Resources/CurrencyIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..50fda3ef3991111ee3e1a500d9cf0f8abfb8999e GIT binary patch literal 649 zcmV;40(Sk0P)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ;4@pEpRCwBKl21q!VHn1L^Es}%`RA}C-U7>rs6)r>4-}zUr|?qOsEc-rNTr}o z*%m<(2}RkViw6meI^?AS+n__CP@$`?nyzcR>g?>SJ2TGg+hH^p>Vfa@@xIUdJkR@m znuxH8UfZL4JO$G$uxNtiK&1}5Bhbz{$rwYMK#M2>5D-; z1E1$%b`4w~wiAHTph5@7gKB4y`0WPN#{?u+VQeWNIbJ|IS-(vz3fLYLMKG;A58o!x zKg|V;SjYxS0MAO2PFblt93D znV6VDr7g&;gX8mGWD6x1&w;`UA#V*;9fGV4CGW{#40>8&PXj#pTB7Efke#J5qM@31 zLC*G}=m$mM(g^gv1IsQ^^Igc!!tXK89&SQgHhpQjwSc*yL#P(Y!=R~ua$(Fse3;ry zjQEF_sQMZe9k>uiOOHW%9L!(fT0!&1I#?-4et{o@Xo(R9?p>okQWyRE<%UKR9T)Gs zBDbMKcqiyp!GKMBHLAdi2VJzc9q$J^5s|GEO&fXX=^K&*XRb(j<0;v(@3b6hyDT^F jKbQ21AtDmp>i;tU$JY;Qbu)|F00000NkvXXu0mjfWCj^{ literal 0 HcmV?d00001 diff --git a/qtcnet-client/Resources/DefaultPfp.jpg b/qtcnet-client/Resources/DefaultPfp.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b124732c3a34e161a88afacf2624d9d45536b0f8 GIT binary patch literal 13151 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 z zOhM|W{~FZ!#F8hm5;Ot(`&KFoeKB@s<=2h;Ze0DBjDbtI%SkmL)992J)BV?72ibm@ z?fGH;*yfZIPRf61xq0r}2#bPr6Pn*0Gq}Ml^RXW7ryi)^nM{SdNdGI;xePbj(~OHm z^y-1DF+QTVU7xrLs$2Na=vV429Vi1(O86X8BtN_~228SB_BQqcJDC4VR7B>+)XHZg z{Y|H+_b7m={qqiPr z-A8Z!b583f1kcdGDE3mwC+`BP)9}g#&{*Iy-nhK<{g7^473AT->5ws zpjQvUQ=*0^nIa|x|M(QdgtXP*LWK1A#rAm+aB@>lpt++88bW=+$3IL6>&r@CB<0E< z1P0qGS+%GOwCaUTzI7S<&zi>dzn+G&RjRC~%>SOAPW=;7Dldu=75nnxtyY!aoTalW zKvl_=^c`eTP{P5Y8e9*f4Tydp`+kKVz81w@`5C@>U3bv};&{=B#KHmkN`$?%8k>)b zCL%`_KRwMD7a;Y<&FZ>ZIXLJd4R!z7@qD1p1bQ2y1bj%mq8aNlM#o?B?@s(?uUt8! z>YaJ=GmlTDI_~l@h{WT&_iuT}eb1g$cQ#2_34J;$fH zKzeV<0*$??w~Y6x0(e7%I6e_*)^Pw9k+*e%O!bL5GIFGT`}bPEL`d#0^t(Y0wLfmw z=aK*V_uxh$IaRplt#ZDXt;~HbOHfYYCgxr}nxo@g2y~Y=Q&&Q}($ z(J(5v1?MoAd7L2RqAv+*_1Yy|!lu4O%4$4ylO&L>0otark;UE*#sSk$T^pD1cN(1; zKl2OGiL@&HC}&lb!>3j%k9yWnohh@+Uuwpl(*6x`${vU4qZYC~NmT_A>7ep0^$y`2}g#^xABDd%j@E=|e@`pe=HM9v{qtwu>PuEejl z*!yJej1~G}()SF@d*2VNYKFO$w?&z38QJ-)?0EcIasK7NDieMnO5lYj!K*A&A(ebl z#SgqvHs)IQj8-hRt(^`&YV7QP;$Qe7L{7)!F~IWF!~3wiIsQ?Mh;I!`4WH7i75!Ix zJBN!k`0Yn$Mbp;J{xUnmdnp?_ZCK?Dg#0NFyEU;y4>yPq60K`oP@)ZMI6IQi?|MOb z(2>a^!>ej~a95LPE-U;0MvMByEu!qy(0Mu8XDrI#<-bP;ZeKNcG_6E%g)>MBNB>ZH z;&dw{AEh~mJh`mX(ARgfB5`Xn$XhY9!R?MPwsG^B3yLRcCJk%*czr*7`{@|NeVp<) zRHupj6w)~!8Vbx>tgR}+1KfZR>iagiyF3Gr1HHz}UxL>e$q#qLQeYDZ>-E!Z9Tb_V zusaHMu%pecF_i-b3&Sldl~1sJo^MY#ErXIjymU7Y9j-{&vb&`1B~D^F{l1zu_!>0$ zwcBJ#!B56j-c|t=(XF||ot~xd%2Eo-(Zid}kw65!qEiGc4!_!63vqZ$Nru;ve9|k} zA2l>_kMcS{;-g9xb%Z%;5SF>hP+zEOg4W|x^&+8ajhmpHgn@F>k(XwYo!TTDU6gAP zHsNpyOL?KNjfeliAUSu`td^N|;N>FN zk4qntJ+6_TVvScaT9skb(thpJp;Jr3DhDY$@YQQkrASr6Kjg!RrB^~zKcGs*Xy^VW zEsK8IQEMle$@t(B@rObUZD=EAWA?*J#zQsng=;Yn9Y=^Pr~p8!Y_5MhnYPfPO1PI| zTs#}*?#nS4#0930(5H=@v)={;db&uhgNcaWJJkWd6wg&rpB-B0gs-Z|RXfg9xfu3e zJDeP2Pgy{=HA^|`BD+JKk|&={ZvGym$H$2z#&b*dvq$+O7!%?9p7jcF@ck#OSfyFO1k=Ibv0FioN6S+ySe4+gVlyd`a965dx;54M z&!swH$d}8JUsy$D?x3O;`3TU0p-kd@%gfqN$NE`y-yd&k4Xpk6&ZE?;xFY**%>_`! z4`RNc*7{coQuYm2ihM`EEV}RJD9E+pSKi&_bbt@S=Oyg1s}AJ;s}0%4gA+dzroLwz z<}@l3S8aw?2S!#C_OQm1Z6QvQZ6)t6@PvhRZ-dU;Sh6+DNwU>hyREC{qY}%=lI>77 zQ)xwoAF7aOC9lV`8|x9k6Lm95pPmP`;r!V@DN>-^m5B~7G&q(fyHWG0bja@`lBu*P zY0AmNI4yGb1zRQU*v`|5`30(0lURY zj`d>sMOB@xe_XCd1zXq+Q@b80YY+x!w(lUZPnE?8&YmMUQM(qi$g2doxE~8i2J~fU zn6Qd!tVRCADVjU!)n6tj9Ce(}bqqdgGp4VAHo*{4p7HT|zsk;vx@*E(dfnzT?^g1! zjUd%@nJACG=0GRoHsm3#P~e!61{X(yDqCeb2)$}iChWy+hqP!AR^avkWkq`%l{x&R zEe=;Q`|4RyT)}s+vL417lJdpcRsad$DarLxUxoa#9k7l_Q}P*VzWU~0_*|ov4}Cfv z@B7@7RU_NAaO@#p1<>JMX^|u|z&3;85&OGZvCJc?FCZz8h1MX1Fw(I1b2qxw+|h!e zYHfapqjT`XJjb!~ABCWESEd{iG)YGc6{imQ>J-<@|r=f&&IJr7S z%Y89}Q3P}RbN(Qpm%}LU08BqlJw80!&gxbTJ=f%NcPU?eCv^YB{)!1)6K%TTY-B+2 zvmr)qa$Y<(I)KW0=TTD)OcwA*I|W++)hw0o%MqvQ7J=+F2Q&SDlJiqxjn@gB5~mz- zmj_uxz;q`o_-@lJ(jz?kPY&C!gIXb&KiHt*J}9%a$IQyk)FAiL!CBYQJYCcFIlZCYJxCbQLi`!pG04O zY@YCZK6aIw^s>;==XV~|0Ld)W*nD_|$tS)DSp|7KrphT#J(a!6oa6GV(_?Xne~8Inlu{;fkLL z0zn_L;N|^gyZ~#|huCbgEDrJ2c}=IVRJH=)ADw&zQz@CEh-UIzmo=?Kgn-?G?cD^! z3hKvTN1&e%YaMFQcFYpU+C7RZyrU*~HClUXcV6${CpII*&`!#-u=dfygz{W#1rD#Y zmI7Uv_hYa01$Mdh*N!JDHa6jwRizKaMmm1;rBb5DYdt5XUY!mq3LCv1s6i)RzVH=#gF7RirkU7cim}RWy z4<&;z`weUkr4~+3cWc{F7bZ~Qn%)j^<U~!Vj6cHg2FkA6M~o|g5V%Ao*icVN2WdIKC2J3f89zg zXsNmyt+4%hb5mkUg$JfmygOj~!_v-+>+Z?WacVMHFDl`q^OA!3g;8LIMsZlj^w+H% zgDOKvSP;)pgHHuKu%~{jE5vMMr}4{L(}>d96e_FrucsicWxP&;IIU24QNN;OZh_*~ z3`Hycsh#;6)6uH(`6ro{sxYQhWQ{+NS|9FyU;t>K_sRavoqgX~YKF3gn)!a(oc{iT zFPkC&mg~ynFADt9bkHk0l!(^I043E`^ve|*&Caf}qtBSW1QiNdDu1&M9x*r=JmM0R zmYAHyBRtlWA~6xn1#)z51v*`7;G7|=b@+|1-X|Hu5_P`(Q~3$x(Be@VqI8 zTP?5CXAvGydWG+7HgvVZwMQ4Bhg*60EVnuQqcN$%=%i!eNK1qe^}WD$Djb_d@$3p{ z%R;-n^UeLzr{Bus9~JSJ)ZXc~1sDpfuGRI60W!0RXM&Q$&{>v+tO~->c32nPR}&#O zyRUXmJq!gQ{aKVW-~C=@OB=32@)w}9pd9gV+$#t;D)m_m4$nCIbcOyLL-IIY8S`s# zanXR@oT0SdMzj-77BUvS8DMyS+a`OOcC4vg%8QJz$KAi<@}xxuX?G%QD9vRN#tT+A zL!dhWQ+&hu?Z4ME>iv>~7Cz_XzyluB4xR<%+on=Nr=lT>KSR;s-O?Nhgrap~MXUXr z#B4`QZrHvBz{XOk5aLNYlI)EFlibhMw#FVn4$!mvtNXWj283~ihY7FRWC@3Ho?jzw z5HRC)z9f%7Wm^t7-o7hNK_q%}bF&w5P#EX~P_`wf2&<)%zQ#cuwVR2y zU7Ebhv*W}(-rxmn@fKhC=nME2Vsa`9ne9^(1)ag7c$UM$UlPoqW*wD3fMWYiD2^f0V$WzlfxlF zG>BKxdUpG}tp#XvAvvLjvtU)_==*d0RJ39eoE{6EP(nYj^p>H; zn~_Pm8<#1`C}6GK{0F+nSEW$p515%2AB{}Nk+Tv__y1~YzW~P%V4??ZJ0K27$8DTi zG4vgg92m*`%96VTxzi8F`~0Efhb$~uT0IAXilg9lO5V&>k$?jX6~1hv8aeB-5Sw_h z1}w;JfjEN&o3rh^ySr|MEw`u6+vJOzi$4?Q0((%pYyO#&P~UEId9ik7G*RvKTTysq zj*FQNY4tbKv%9xuSB+9e7(J=WF}@mw&KkpXGUOZbw1|}q-&9J7CgmtJCOBL_Cdo=- zyxgHcCFF1?Ou$b113sM+6PKNXFi1D=x;7zCPb411>q%=4MLz*4BF*;|I5sMR#<$RRC9)tJk2y7F{!JUYA}W?@0@dF9=WZOo~P zW)P^;k8+UpuX+>92txqr$3}m9JVdQ816i3>y4(aGI0Mn>Ob&7|C}Mo2NfbL}j>w6} zuCRlFSd&v{x1KrL6R*sb)==*m|= z8TN8kq)2iQME1Ah2qQ(t$GTv=u9iaI0lqqdS@Ne4zYMdF!LDlNPO~rLr1OAqXu%KN zYV2P^jFosM()uuww zOPiu{e{p(0+SAHPUOhU1Iv`Kn@`_M!<$a?|^B`PqQ7WjiH(N$ix}koEt^3ZPqd)m0 z88GyuDjFL#CPY44r%X4t#rcHBIq}No=vUvkdYI>IhJ$M2**iTaFoj#whJoxvTcGVi zw2C4~X|7#)S<-5!rvYTopxfYyk)jNN2fcr#K-SUbl|DU%_JbN1@}o2V{K9r9;NO=H z?>2kWG^T*N!uggBeTnqjXH;Vje$ZBsjp!ILq7$GbX!^*H&br)Pv*Wffkuf z@qOmf^gF};2d^RKT&oD#HkOGs43x)$XUV7JALOIM;E;e0Nj0YynK_O6vMe>Po#W!e z&-ry`*F6sTP_rek>8UjfFR34(cSRaMo=SD*$7?tk1bm-9$aD^AkS4GRJRRT@GE9Fv z@Cd{|gox}sF{mprv#Xp=qeKDTQ@?#wpadZ^?)PMgDX;z;(Snn6u>K~zbXb10x(jz< zDRGcht)L|&Tpxfb1o3x5Q93}0DLF4~@Tq@#dcZYC4w!r^B>o7w6WzAk2F>y9h})>~ zcGN`APrKS#`2k86+DkCeMOTG6fse>y_HnTi_O}sPN&p5HRhU2Nvl8SXq<+utHL14& z1M5#YbCItNPA}p1>AxaM{3|MF{(MPV8f9wOy<2Ipd~2MX;==Ge+{*+eDbmk6n~GGE zY~B$`B+?ScuYIDzk~X~W(HiFy=Wg~vt@4?8!Y?Qr6lJ8pGyk%Wr(`nu>O)o_k2RFy=uuY|iuJVFV_R60ggCS<*u#(;`7qG!deJ#|gyZ2+AipqhN?2l*E zFqn#%tE%Y0C3Zkv<4F0-lZ%@I)vXZ5btZAq5C(aGMUC=c_|AShT=MHz^#`M1JKGDL zKka8O3on@(H39f*JEhqN`7mE;5xyF?pK;C1`bA&kE;$(QZ@2_#u^d zQXy4Q56t+$%!O9M9vtP=u0KY0{GdpV&r3m3cY7z4>s4otlwFI*WSeQZ&tn6DB@)ZT zcq*N|-~no?B;Wgc(`ec3-vm*me>jb{#E(bm4YwVHeBKbrii-Z3Aj&S8yhN~tfO$`Y z?y1e`kR9V@aY8SAMNiZ;z@mF7jjcGU{6d(^NMeP-;6=KUPb$}jXw%Ml@J=D(#BV;7Lb-aG1qOUi(#-TKa zj{P}=`D|!oeiZ&tU*ml{`S)*HBh?_fj7P{K!6h@1?r%W zlHtktxdfF$$mY#WH2QUCgiLt`&^ECHdQSrN-&!1wwLlF@w2}?hdpfwDSDTKA+f?(L zgXkD2p%E@vBK9uDSC3vZlpjEMWUIucaFJ!;<&|8-%WOD9tMTT!2^7j)JVn1=RYpHn z3wSAtXc^(bn^Vi76<^1{aVrPM@0tD$d>hHWr&e7uyN<8(b(~-x{fsF*xxUmR|CL)b z#BF-=r%zU@>Fv0c;M>4N5HoWnlwcsMTzy;;Q8eCsyZ%;L(i-W0m%GqE6)r)DM@l6&aJfJ0=>=hmtJ6T%!)aU$ z=)7Vz%Umar=HkJIYi+|{6x2a~tP*-6>afc+r3<3l$x}RADHdb~u}Pudntsdh=X(^3 zalN3KmQMtHnzUSdRrQem2Vqm~H*C}Bnp^jzzOVH`r%6-NOLzx?D7(DE9@Zzmz(W50 zdrw6KD})e5m+I{e>Zqx4Z@a5#SNCM?KIxb1(D};(5wbPjn54!9~b_2-F z2{UdF^UxGrXk>So9OBWZx-4&HD*}j((znXJ=v&N)Pw&`POU=hL9A&S>U)gG=Jy~qh z&)2Y1{b5m9TB zbxywB-m&W(Y^R&nOJ+;Fgr02#?Qs6?!i0Pl3=j{hb>;9o`VKcXsDkTE9AyVl1*8QP zo`F3%_F(VJ%w8W~&0Gw$-yopeDMmZc7ip_8(3?Hrto1crlz6G-+(>rhYnKTO?7b0q zyEOn&^X+y0uL>PY$KS#k$z+RcMnV4FJ&5QxWX6()|%hJ@J#vmDj zm+7f$u!Z+XK0HhjQDvnw5qT@)2nxLOcLokj$vpY3eINQdwn&vefj|qi_}b_3?FYz9 zz27e!8|^MPtU~A$Y{pkXY-jg0_c8k{N*SD-=3=5#Pu+%U`8}HOGp1`a3n%{=fg+63 z6^-}8l?tTIv!%XqLs1{s&zHVgebANM#C~k_#?+gD!p-}3->unAi&yqZ*Y>yk%y~lV z6}+4K^sMq537UYU_#K0`UVZQ;iVnI{5-v?>%bz@6(ku)%0Z`cEIChuWhy7>Q?I1Q~ zDxx7jHS@%*`75LLuScx{&@iY<>y-(lJ79s^G28YvqN_BEmVA7#zI_*YXSVEv>=Rq& ziNijqkm9r7A2e+Rq)s>Wu{Ujf^BwZctjD!Qlty7=x=F(n{nLUJr*VW zOx35w_J>Pp4co=#XWBZbQHJg$BQ&G!Es`E)nGEDpM*!pWKB3c^LTWwi(sW@HYtVmvQC2kcC7 z=X!W%T8)74s)|T0oI?Cf&aM(Uqn>dGPDabRj_x#KjZ6Bv+8$9E2f_rV;ow_JLJMs7 zyO@uS=I+`7Fz8NP`G>M*KwW9>pp}hn$6X6UDG(K2N=^QusF@}!QPn$V%L7F@0qMaW zyHpT+<889Ko0eTw(Sn-=cLxIH^Vp*D`d_hZ*ebm`TJOgG33=`yBLRLh_rw zu!x2X+kTTiQz$$pJa^Flyvf;e>NUj*ZlCR-~ zmJUzSPiNf?n|cF=4iW@H(^GEM#_=|(0Z_s8nqYHhm&)OhAi`j>RrC&@U=AfeacLvv zxu}`xH0E8zexwax;p#H2!4F@6jwuz;1Wy06EQ)Yu=r_Ln^dpBk8BrAhNQqb#8ywQ` zn60>Je#AqM{i0Ido_yoeh$j&bp6;Is=ltv-J~HHM-kTpx)N+JKXUkf?9XN zt9*M~EnB{3C;m`%`1!Om@@p%5EIr#cDvk5q=jHn=O+qT^T+e7C_qjS4JN_5RX4wpU zt#-9Y=WnE7&&+umEXU{}=edo48eDpXY;hXcc)W$lSv(_8^>48`J)^NUdb6u}R#;+K z-Vw0^FBVTf0?`2ed`6F=eT^Vb6F<1UJiDj0x2v*hs!Yug9}WY}zzOvI={DmM^7a85 z;U9F+>%eysjEqQjyH4$`vz^0~5zh5RWe$4z3{%_ti)ZJVf$&yx&JT>OnY9iL!Y5rd zgp_52OkALj10EpUaQtSI3TSA5MC#60%cBk-Rp%#=XF!t#{d2UEQlvhYe`D7$i-+qi zKy{-+q*>@Fc%d|O<8?1k?S!HBOIcKKE%Q6#YG^h{mxJi^dG_~@ul8|G@nbJeQnfT@ z;#t@!tZjT&v{se6{0^uBWu2s3qwPoL<6zr>EPsn?Ji0J+9COlrR3X_WjfHD>^oo2; z$Jb2#=sIrPAKA7moCx6sBhUCr>D%aS^p%|rSn|HN!rTf>w5GysRb@^qFyx$lZNF~!()(c;HTh8}EkUr5Xb?-nfi;5{L(#EG1C6iPWEJ_%1N z2YH@{n7wW%f5*CXR2bQs%w`Rg_GOI4oQB=W`~<^3R17H6&OZ~<06csay`Ck#j<&qA z)}N6S7^3)F=fcm+3W3>5^hvl8@tN}pb%gr`lDE4eFcali#W5C?a4zsc=Qv5=FyQ)C zb@yH0rpqc`NKx?tYQCSNsi*U+o_ABzF&2?<3LfV~ z@HIJzetA^}K#6KLY3kT3b2jU=h-$Cb=qd{WK)Eo)Vek`TlN4^>9~Bw*;bim5!F3C^ zjyCR7AD<&nUVdC-{Brj&PF9UvT=%vNp*Yrcvx<$F3}I%yv&>hS^_!=@r6hh`&~2kQ zSdMhqkle~eVZ2gI^*_}TP$n50+<%<6bCQ@sJQbsVW&bLs@6-FCUwFu_2s7FvzwjAm zXWwN%c(~%uv!)-}4rS^&NpRTn^x~5RyIe+gLjzm>;3wR%20s4N)#1Bj2Xq;2y6c9y zG{i-cty?SHzvgEZMn$K6PUrqTIdKJ$_m2xG{UPPRM9hmeKpmm=x?n}qKl=rn4SeN~ z^Y T}89cNzIQxka^{3Q3DK?$fNf6B{P5*(UghXQw5Ui?6!2*p~RHiTS2St4yCR zi#zbCH13EgI*Pv#w(7TsF=D@h#QF`l43Pn^n`i|lEQz&L1sQ25*rNlf9~jw`A?h$d z%5)FPyUdn3NLoZSbR zC8es#BRUQefb5_uBF#_|g*}v8b+LDxQ(P2X#X60YD;ry+>pM?fuK9UpxiwJoViO@H zPNyB)Vx&XpMryiA&>+I=6d=Yb0*y}sy4V4F{q(=#DjY`nnXlDM^gc_Y0bgOlE6wzg zzNjczA*|8EpA~AvkihEHPn(*8@H!zFizyPmvtY6ULt1E|QH-156&DnhfdNNHe(+Ni zXvVY-zdsn0h@^_B`WL`mPa)#*>ENbb zTJ$s^4c&coVTtudL{~Q62i~2)nHxe7j#bV-p^Y{r%(x+1Sa*418fqZao#h&E;WT)G zqvn&g8083t(&H+27ucEN%sd|F3y$e2!9i(RHn%=$2(f?I5L8EnyI=;Q18?73sil#! z?L}VX2S%+ zE?;`;(8!y3?$Dx}-4kM+vrGcoR%|Ei-#z?ow*iVNlib@=Iv;NfhO>Ity78n^-xdG> zJgid_El{u;8psBiVk_SzrauHs?P*TTng6jcXZ{z9Ulo6E@vGuyv-g=FDlobEgNxpK z;X^NaKNLR(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ>yGcYrRCwCFmRo3LR~g5DYwvxoIk`-l89Qmbv|;)%v`S^Plsaf_M5&gRK9u5v zAl{)yK|zKhlo`~AfKweSh+_jbI0HTv)Jii>8=S)6L!7D{lbFew=CYHs&&fF_XW!1g zthIbN&9N~fGaxe`EZA%o-*5lFZ~fP|g*aa)YY6%?@SeQET?KW(S24LJ5u_kAk{zl^P_|H{|Oy^rNP~O z2ID1@(Sl(VP4N{CKlD>Q+=vy8SkrF_9vZQ@Z_wgM$>2cN0H6^omOaH=t&}+@VLeos zLXaxm>?lrnss82CV?$~m^ugH{_x78N7K~!i6u;?6{ls>xu|vf`M(~aC91kDNAZ}1H z4P2@voL=g%*z290>M6pSslMeMKYFXNf;_BolV1xaLbfekjAK`-@mN zqWxE-8czHz;N060i3Tsx7dlFRRz7`4yt~I5$_Pv$PPP+0VTavvee#gSk3Lug;OyD6 zWHK3Qwc2$d<#L(n>1nj?HrWy|^`RnCN`A4D5Tt5?j(-a7_+C%xm;d4(v4kjZ$I4pv z6gdN4e`J8X*_9~7%}L#b0z^^7rc>iHf6cJiO2BWXq{+Dc=7(1SjE>~%%|uxnp<**s ze04nE1MVifnbj+Ano_IPsBLZXm{h(IdZ8K6`*oA`Ee6rWmR;=&wQjMExVx<`^82b`UPEI1F#P|K{f^6Gnc6OF1if~;Q z$8lI&TccgA!}~u1aR^@Po*uNBD1sEiR3kNPhKli$xhLtFGiPo}rSJPVj>GEeDwRrw zYk`K*2>c!JTfLrHjaXMA$Q%d!wc>^oRmYmy|v_kC>JW@%}O za=A>UQeow;V-U2Vy$$}(%737do=vnqDm9=rQ~z-JXs3>hBMie}W@d(bK9B3Vc%HWh zSg+TapPy%Sb(PDPFSEF~$o%{~3kwT$l9a0tJqXPzG`AphFZ7^6C;B;Q{48K%S}b}N z&o3K%LBCCsBuq_B-BfAk=H^&fSfEm=0C4r#Gx#a6wF0fG#4um%`IKKRL+DMRM zkSjvAz$0w%x%!)=afA?pTrNj0m%}u>{Z5jEAP8tSn^db+Ha0d0!w_wl{Oi~=*aybp z+TUUApAfn~))!~K(^HC45Oh@FwUaqhey9~0{9uz3}nRS4bRQ|NzMUwnD* zh0n`2gl%X!(A+X`8{Z<0Phw_Db1<2&Uc*jmkLJ~m~fZu|hIy5%B%WZh5 z^m$4{r}c$j?;C9vmeox85Oi$CQo+sFOmgd$MG0ekcvS8)FvOpMJpiH}_@AzFG zhHVJEEo3rX{o_pT(1F441H-^``{pUWjaDmeL`g&(ey|%XIzx%zxEeywTUbu4QN?0zJf41i!y?X%Pt}t#R>m<+wPTo$XyEgKy n+eLI7=ZtOJleJpyr~e)RJUZ}K2(yOy00000NkvXXu0mjfd!YS= literal 0 HcmV?d00001 diff --git a/qtcnet-client/Resources/SendIcon.png b/qtcnet-client/Resources/SendIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..d74e96d070635471e0cf87d89c0efd8916a005e4 GIT binary patch literal 1936 zcmV;B2XFX^P)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ@7D+@wRCwC#nO$rY*A>Tqch)w>#$a6Hn8@I@Nu8KrHOPV>T0jY;sG&+rKvARe zP^2_g(@?3UeL`(iwKNrKklLmpQbTx1L?mbfss*Gn1RSvJ8f-rNK{5V-UHr9P@7n9x z-PzflxqaBN+1P7|&>7bcJ<^rt-aB{Bo&TKkKj+*LQcCO;A;h$?Ut9~f#ZCEC!d_Lj zNH`%m?$t*#Wa!h)RL&7XfDmPI6Ustiq?VYRQ;9LM zv={*Qc?nd4s}fw+4fAHnHMB5{`NQdeu*;DJAW)Q2-GmjRDl!sJ0Yb-LRFu{&q?9yw zbh4&x$?O0T=+`o4cmexdLvfYF5)$>)1>Op`5F!RZ9D{pHU&NiG;PH6ycsz7>cjNJR zP*s(|>_=eb?pXjr1)nFH;Q%L01+`2wB-;WwPS zlcnO07|c!4#v!VX6N{LR0`b*72Qm?&SdGiM2}CRcT6heQj)u=io_(p9mZ!lS{d79ni+4Mi!{O?!~YRfka&g;*^1q7QC|SCpDa_ur$D$h zn<7ST*pH$p=(_%J4|ud>st@5Gk705V{w8XpN0-59$P1C#g^YMQ}-jlz%(EImeudLVsAofFf4z#=uR|nSb zSZ$RH?SpWj9?n%mbtNoXv+A2$SHT;{lk)GN7p^CstSo1lfx1HKuW9~c3Gz_^nfVNL-h1}K1F-^fqC zS6;B|Xgj=m!MuY|+_m5Gvu)W3pLLfm3iyV2`BWjNN9Ns6TBUjCO4)$>X#ISM^U3P3 zh)S<_h%=sI-h5u;L~{ZVm-ChQ*ZcbVXditVjM2t4{E+&wq|>#=gc2i#l(b2f&R)Fx zkKcv2HpeK<#tqQsQMr5fE*{mSsjZLldC8x}zjF6-WMw0dWt#5LP$#{<2xKkLUD_}W z*FE$^>Al+81)*-p%>B;JC2b53595vJKvr&tWsT`t#M+Mr#1x04#suOaO7%3ULPy=L7ge|a)Nh}uI}3s6R`@VkXaOT(i+yg@;iv7mgP8ao7>wXS=t{%a}y9&6npxsN|Q@P7VRK+H8!{trP?{xbmH W)76oDrSO9Q0000 + + + WinExe + net10.0-windows + qtcnet_client + enable + true + enable + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + \ No newline at end of file