Merge pull request 'Rework UI Updates' (#7) from ui-updating-rework into master

Reviewed-on: #7
This commit is contained in:
Alan Moon 2025-11-27 13:09:49 -08:00
commit cadd32d9b1
5 changed files with 247 additions and 154 deletions

View File

@ -113,7 +113,6 @@
tbcMain.SelectedIndex = 0; tbcMain.SelectedIndex = 0;
tbcMain.Size = new Size(352, 499); tbcMain.Size = new Size(352, 499);
tbcMain.TabIndex = 0; tbcMain.TabIndex = 0;
tbcMain.SelectedIndexChanged += tbcMain_SelectedIndexChanged;
// //
// tbpContacts // tbpContacts
// //

View File

@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.VisualBasic.ApplicationServices; using Microsoft.VisualBasic.ApplicationServices;
using qtc_net_client_2.ClientModel; using qtc_net_client_2.ClientModel;
using qtc_net_client_2.Controls; using qtc_net_client_2.Controls;
using qtc_net_client_2.Forms; using qtc_net_client_2.Forms;
using qtc_net_client_2.Properties;
using qtc_net_client_2.Services; using qtc_net_client_2.Services;
using QtCNETAPI.Dtos.User; using QtCNETAPI.Dtos.User;
using QtCNETAPI.Events; using QtCNETAPI.Events;
@ -20,7 +22,7 @@ namespace qtc_net_client_2
private IGatewayService _gatewayService; private IGatewayService _gatewayService;
private Config _config; private Config _config;
private ServerConfig _serverConfig; private ServerConfig? _serverConfig;
private AudioService AudioService = new(); private AudioService AudioService = new();
private ImageFactory _imgFactory = new(); private ImageFactory _imgFactory = new();
private CredentialService _credService = new(); private CredentialService _credService = new();
@ -365,27 +367,67 @@ namespace qtc_net_client_2
donationWindow.Show(); donationWindow.Show();
} }
private async void tbcMain_SelectedIndexChanged(object sender, EventArgs e) private async Task RefreshStore()
{ {
if (tbcMain.SelectedIndex == 4) // get store items
var storeItems = await _apiService.GetStoreItems();
if (storeItems != null && storeItems.Success && storeItems.Data != null)
{ {
// get store items if (lvStoreItems.Items.Count == storeItems.Data.Count) return;
var storeItems = await _apiService.GetStoreItems();
if (storeItems != null && storeItems.Success && storeItems.Data != null)
{
if (lvStoreItems.Items.Count == storeItems.Data.Count) return;
ilStoreThumbnails.Images.Clear(); if (!IsHandleCreated || IsDisposed)
foreach (var item in storeItems.Data) return;
{
await GetAndAddStoreThumbnail(item); if (InvokeRequired)
var lvitem = lvStoreItems.Items.Add(new ListViewItem { Text = item.Name, Name = item.Id.ToString() }); {
lvitem.ImageKey = item.Id.ToString(); Invoke(() => ApplyStoreItems(storeItems.Data));
} }
else
{
ApplyStoreItems(storeItems.Data);
} }
} }
} }
private async void ApplyStoreItems(IEnumerable<StoreItem> storeItems)
{
ilStoreThumbnails.Images.Clear();
lvStoreItems.Items.Clear();
var items = await BuildListViewItemsAsync(storeItems);
lvStoreItems.BeginUpdate();
try
{
lvStoreItems.Items.AddRange([.. items]);
}
finally
{
lvStoreItems.EndUpdate();
}
}
private async Task<List<ListViewItem>> BuildListViewItemsAsync(IEnumerable<StoreItem> storeItems)
{
var items = new List<ListViewItem>();
foreach (var item in storeItems)
{
await GetAndAddStoreThumbnail(item);
var lvItem = new ListViewItem
{
Text = item.Name,
Name = item.Id.ToString(),
ImageKey = item.Id.ToString()
};
items.Add(lvItem);
}
return items;
}
private async void lvStoreItems_DoubleClick(object sender, EventArgs e) private async void lvStoreItems_DoubleClick(object sender, EventArgs e)
{ {
if (lvStoreItems.SelectedItems.Count > 0) if (lvStoreItems.SelectedItems.Count > 0)
@ -621,6 +663,8 @@ namespace qtc_net_client_2
llblEditProfile.Visible = true; llblEditProfile.Visible = true;
tbcMain.Enabled = true; tbcMain.Enabled = true;
await RefreshStore();
var pfpRes = await _apiService.GetUserProfilePic(_apiService.CurrentUser.Id); var pfpRes = await _apiService.GetUserProfilePic(_apiService.CurrentUser.Id);
var cosmeticRes = await GetCosmeticImage(_apiService.CurrentUser.ActiveProfileCosmetic); var cosmeticRes = await GetCosmeticImage(_apiService.CurrentUser.ActiveProfileCosmetic);
@ -647,12 +691,6 @@ namespace qtc_net_client_2
pbUserPfp.Image = _imgFactory.CreateProfileImage(null, null, cosmetic); pbUserPfp.Image = _imgFactory.CreateProfileImage(null, null, cosmetic);
} }
if (lvUserDirectory.Items.Count <= 0)
await RefreshUsers(); // prevent edge case where the refresh event never gets sent to connecting client
await RefreshContactsList();
await RefreshRoomsList();
// set status context menu checked // set status context menu checked
// TODO - figure out more efficient way to do this // TODO - figure out more efficient way to do this
UserStatus cuStatus = (UserStatus)_apiService.CurrentUser.Status; UserStatus cuStatus = (UserStatus)_apiService.CurrentUser.Status;
@ -733,153 +771,200 @@ namespace qtc_net_client_2
} }
} }
private readonly SemaphoreSlim _roomsRefreshLock = new(1, 1);
private async Task RefreshRoomsList() private async Task RefreshRoomsList()
{ {
LoggingService.LogString("Refreshing Rooms List..."); LoggingService.LogString("Refreshing Rooms List...");
if (IsHandleCreated && !IsDisposed) if (IsHandleCreated && !IsDisposed)
{ {
await Invoke(async delegate () if (!await _roomsRefreshLock.WaitAsync(0))
return;
try
{ {
flpRooms.Controls.Clear();
// always add lobby room to rooms list
var lobbyCtrl = new RoomEntryControl
{
RoomName = "Lobby",
HideUserCount = true,
BackColor = flpRooms.BackColor
};
lobbyCtrl.Margin = new Padding(0, 4, 0, 4);
lobbyCtrl.Width = flpRooms.ClientSize.Width - flpRooms.Padding.Horizontal;
lobbyCtrl.OnRoomDoubleClicked += OnRoomDoubleClicked;
flpRooms.Controls.Add(lobbyCtrl);
var roomsRes = await _apiService.GetAllRoomsAsync(); var roomsRes = await _apiService.GetAllRoomsAsync();
if (roomsRes.Success && roomsRes.Data != null)
if (!roomsRes.Success || roomsRes.Data == null)
return;
roomsRes.Data.Add(new Room
{ {
foreach (var room in roomsRes.Data) Id = "LOBBY",
{ Name = "Lobby"
// create room entry });
var ctrl = new RoomEntryControl
{
RoomName = room.Name,
RoomUserCount = room.UserCount, // placeholder
BackColor = flpRooms.BackColor
};
ctrl.Margin = new Padding(0, 4, 0, 4); var uniqueRooms = roomsRes.Data
ctrl.Width = flpRooms.ClientSize.Width - flpRooms.Padding.Horizontal; .GroupBy(r => r.Id)
ctrl.OnRoomDoubleClicked += OnRoomDoubleClicked; .Select(g => g.First())
.ToList();
flpRooms.Controls.Add(ctrl); if (InvokeRequired)
} {
RoomList = roomsRes.Data; Invoke(() => ApplyRooms(uniqueRooms));
if (System.Diagnostics.Debugger.IsAttached || _config.EnableDebugLogs)
LoggingService.LogModel(roomsRes.Data);
} }
}); else
{
ApplyRooms(uniqueRooms);
}
}
finally
{
_roomsRefreshLock.Release();
}
} }
} }
private void ApplyRooms(List<Room> newRooms)
{
RoomList.Clear();
RoomList.AddRange(newRooms.DistinctBy(r => r.Id));
var roomsSnap = RoomList.ToList();
flpRooms.SuspendLayout();
flpRooms.Controls.Clear();
foreach (var room in roomsSnap)
{
var ctrl = new RoomEntryControl
{
RoomName = room.Name,
RoomUserCount = room.UserCount,
};
if(room.Id == "LOBBY")
ctrl.HideUserCount = true; // TODO - probably make this work with the lobby room (might need backend work)
ctrl.Margin = new Padding(0, 4, 0, 4);
ctrl.Width = flpRooms.ClientSize.Width - flpRooms.Padding.Horizontal;
ctrl.OnRoomDoubleClicked += OnRoomDoubleClicked;
flpRooms.Controls.Add(ctrl);
}
flpRooms.ResumeLayout(true);
}
private readonly SemaphoreSlim _contactsRefreshLock = new(1, 1);
private async Task RefreshContactsList() private async Task RefreshContactsList()
{ {
LoggingService.LogString("Refreshing Contacts List..."); if (!await _contactsRefreshLock.WaitAsync(0))
return; // already refreshing, skip
if (IsHandleCreated && !IsDisposed) try
{ {
await Invoke(async delegate () var contactsRes = await _apiService.GetCurrentUserContacts();
{ if (!contactsRes.Success || contactsRes.Data == null)
Contacts.Clear(); return;
flpContacts.Controls.Clear();
lblRequestNotif.Visible = false;
var contactsRes = await _apiService.GetCurrentUserContacts();
if (contactsRes.Success && contactsRes.Data != null)
{
if (contactsRes.Data.Where(e => e.UserId == _apiService.CurrentUser!.Id && e.UserStatus == Contact.ContactStatus.AwaitingApprovalFromSelf).Count() >= 1)
lblRequestNotif.Visible = true;
else
lblRequestNotif.Visible = false;
foreach (var contact in contactsRes.Data) if (InvokeRequired)
{ Invoke(() => ApplyContacts(contactsRes.Data));
ServiceResponse<UserInformationDto> user = null!; else
if (contact.OwnerId == _apiService.CurrentUser!.Id) ApplyContacts(contactsRes.Data);
user = await _apiService.GetUserInformationAsync(contact.UserId);
else if (contact.UserId == _apiService.CurrentUser!.Id)
user = await _apiService.GetUserInformationAsync(contact.OwnerId);
var ctrl = new ContactEntryControl();
if (user.Data != null)
{
Contacts.Add(contact);
switch(user.Data.Status)
{
case 0:
ctrl.StatusColor = Color.Gray;
break;
case 1:
ctrl.StatusColor = Color.LightGreen;
break;
case 2:
ctrl.StatusColor = Color.Gold;
break;
case 3:
ctrl.StatusColor = Color.Red;
break;
}
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;
}
}
// add the control to the flow panel
ctrl.Margin = new Padding(0, 4, 0, 4);
ctrl.Width = flpContacts.ClientSize.Width - flpContacts.Padding.Horizontal;
ctrl.ContactDoubleClicked += Ctrl_ContactDoubleClicked;
flpContacts.Controls.Add(ctrl);
}
}
if (System.Diagnostics.Debugger.IsAttached || _config.EnableDebugLogs)
LoggingService.LogModel(contactsRes.Data);
}
});
} }
finally
{
_contactsRefreshLock.Release();
}
}
private async void ApplyContacts(List<Contact> newContacts)
{
lblRequestNotif.Visible =
newContacts.Any(e =>
e.UserId == _apiService.CurrentUser!.Id &&
e.UserStatus == Contact.ContactStatus.AwaitingApprovalFromSelf);
Contacts.Clear();
Contacts.AddRange(newContacts.DistinctBy(c => c.Id));
flpContacts.SuspendLayout();
flpContacts.Controls.Clear();
var contactsSnap = Contacts.ToList();
foreach (var contact in contactsSnap)
{
var ctrl = await BuildContactControl(contact);
if (ctrl != null)
flpContacts.Controls.Add(ctrl);
}
flpContacts.ResumeLayout(true);
}
private async Task<ContactEntryControl?> BuildContactControl(Contact contact)
{
ServiceResponse<UserInformationDto> 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);
var ctrl = new ContactEntryControl();
if (user.Data != null)
{
switch (user.Data.Status)
{
case 0:
ctrl.StatusColor = Color.Gray;
break;
case 1:
ctrl.StatusColor = Color.LightGreen;
break;
case 2:
ctrl.StatusColor = Color.Gold;
break;
case 3:
ctrl.StatusColor = Color.Red;
break;
}
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;
}
}
// add the control to the flow panel
ctrl.Margin = new Padding(0, 4, 0, 4);
ctrl.Width = flpContacts.ClientSize.Width - flpContacts.Padding.Horizontal;
ctrl.ContactDoubleClicked += Ctrl_ContactDoubleClicked;
// return the control
return ctrl;
}
else return default;
} }
private async void Ctrl_ContactDoubleClicked(object? sender, EventArgs e) private async void Ctrl_ContactDoubleClicked(object? sender, EventArgs e)

View File

@ -81,7 +81,7 @@ namespace qtc_net_client_2.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to 6.5.5. /// Looks up a localized string similar to 6.5.6.
/// </summary> /// </summary>
internal static string AssemblyVersion { internal static string AssemblyVersion {
get { get {

View File

@ -173,7 +173,7 @@
<value>..\Icons\MessageIcon.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Icons\MessageIcon.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data> </data>
<data name="AssemblyVersion" xml:space="preserve"> <data name="AssemblyVersion" xml:space="preserve">
<value>6.5.5</value> <value>6.5.6</value>
</data> </data>
<data name="cobalt_sittingatputer" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="cobalt_sittingatputer" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\cobalt_sittingatputer.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\cobalt_sittingatputer.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>

View File

@ -19,15 +19,24 @@ namespace qtc_net_client_2.Services
if (Debugger.IsAttached) return; if (Debugger.IsAttached) return;
// get client update info // get client update info
HttpClient client = new(); HttpClient client = new()
client.BaseAddress = new Uri("https://qtcclient.alanmoon.net"); {
BaseAddress = new Uri("https://qtcclient.alanmoon.net")
};
try try
{ {
ClientUpdateInfo? updateInfo = await client.GetFromJsonAsync<ClientUpdateInfo>("clientinfo.json"); ClientUpdateInfo? updateInfo = await client.GetFromJsonAsync<ClientUpdateInfo>("clientinfo.json");
if (updateInfo != null) if (updateInfo != null)
{ {
if (updateInfo.Version != Resources.AssemblyVersion) var serverVersion = Version.Parse(updateInfo.Version);
var clientVersion = Version.Parse(Resources.AssemblyVersion);
int relativeComparison = clientVersion.CompareTo(serverVersion);
if (relativeComparison > 0) return; // probably a version being developed or tested without a debugger, so don't do anything
if (relativeComparison < 0)
{ {
// inform the user an update is available // inform the user an update is available
if (!updateInfo.IsUpdateMandatory) if (!updateInfo.IsUpdateMandatory)