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 0000000..50fda3e Binary files /dev/null and b/qtcnet-client/Resources/CurrencyIcon.png differ diff --git a/qtcnet-client/Resources/DefaultPfp.jpg b/qtcnet-client/Resources/DefaultPfp.jpg new file mode 100644 index 0000000..b124732 Binary files /dev/null and b/qtcnet-client/Resources/DefaultPfp.jpg differ diff --git a/qtcnet-client/Resources/QtCNETIcon.png b/qtcnet-client/Resources/QtCNETIcon.png new file mode 100644 index 0000000..9a03466 Binary files /dev/null and b/qtcnet-client/Resources/QtCNETIcon.png differ diff --git a/qtcnet-client/Resources/RoomsChatIcon.png b/qtcnet-client/Resources/RoomsChatIcon.png new file mode 100644 index 0000000..b20ae52 Binary files /dev/null and b/qtcnet-client/Resources/RoomsChatIcon.png differ diff --git a/qtcnet-client/Resources/SendIcon.png b/qtcnet-client/Resources/SendIcon.png new file mode 100644 index 0000000..d74e96d Binary files /dev/null and b/qtcnet-client/Resources/SendIcon.png differ diff --git a/qtcnet-client/qtcnet-client.csproj b/qtcnet-client/qtcnet-client.csproj new file mode 100644 index 0000000..72b0a83 --- /dev/null +++ b/qtcnet-client/qtcnet-client.csproj @@ -0,0 +1,37 @@ + + + + WinExe + net10.0-windows + qtcnet_client + enable + true + enable + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + \ No newline at end of file