added notification system for file downloads and pair requests
This commit is contained in:
469
LightlessSync/UI/LightlessNotificationUI.cs
Normal file
469
LightlessSync/UI/LightlessNotificationUI.cs
Normal file
@@ -0,0 +1,469 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
private readonly List<LightlessNotification> _notifications = new();
|
||||
private readonly object _notificationLock = new();
|
||||
|
||||
private const float NotificationWidth = 350f;
|
||||
private const float NotificationMinHeight = 60f;
|
||||
private const float NotificationSpacing = 8f;
|
||||
private const float AnimationSpeed = 10f;
|
||||
private const float EdgeXMargin = 0;
|
||||
private const float EdgeYMargin = 30f;
|
||||
private const float SlideDistance = 100f;
|
||||
|
||||
public LightlessNotificationUI(ILogger<LightlessNotificationUI> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector)
|
||||
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
|
||||
{
|
||||
Flags = ImGuiWindowFlags.NoDecoration |
|
||||
ImGuiWindowFlags.NoMove |
|
||||
ImGuiWindowFlags.NoResize |
|
||||
ImGuiWindowFlags.NoSavedSettings |
|
||||
ImGuiWindowFlags.NoFocusOnAppearing |
|
||||
ImGuiWindowFlags.NoNav |
|
||||
ImGuiWindowFlags.NoBackground |
|
||||
ImGuiWindowFlags.NoCollapse |
|
||||
ImGuiWindowFlags.AlwaysAutoResize;
|
||||
|
||||
var viewport = ImGui.GetMainViewport();
|
||||
if (viewport.WorkSize.X > 0)
|
||||
{
|
||||
Position = new Vector2(viewport.WorkPos.X + viewport.WorkSize.X - NotificationWidth - EdgeXMargin,
|
||||
viewport.WorkPos.Y + EdgeYMargin);
|
||||
PositionCondition = ImGuiCond.Always;
|
||||
}
|
||||
|
||||
Size = new Vector2(NotificationWidth, 100);
|
||||
SizeCondition = ImGuiCond.FirstUseEver;
|
||||
IsOpen = false;
|
||||
RespectCloseHotkey = false;
|
||||
DisableWindowSounds = true;
|
||||
|
||||
Mediator.Subscribe<LightlessNotificationMessage>(this, HandleNotificationMessage);
|
||||
Mediator.Subscribe<LightlessNotificationDismissMessage>(this, HandleNotificationDismissMessage);
|
||||
}
|
||||
private void HandleNotificationMessage(LightlessNotificationMessage message)
|
||||
{
|
||||
AddNotification(message.Notification);
|
||||
}
|
||||
|
||||
private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message)
|
||||
{
|
||||
RemoveNotification(message.NotificationId);
|
||||
}
|
||||
|
||||
public void AddNotification(LightlessNotification notification)
|
||||
{
|
||||
lock (_notificationLock)
|
||||
{
|
||||
var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id);
|
||||
if (existingNotification != null)
|
||||
{
|
||||
// Update existing notification without restarting animation
|
||||
existingNotification.Message = notification.Message;
|
||||
existingNotification.Progress = notification.Progress;
|
||||
existingNotification.ShowProgress = notification.ShowProgress;
|
||||
existingNotification.Title = notification.Title;
|
||||
_logger.LogDebug("Updated existing notification: {Title}", notification.Title);
|
||||
}
|
||||
else
|
||||
{
|
||||
_notifications.Add(notification);
|
||||
_logger.LogDebug("Added new notification: {Title}", notification.Title);
|
||||
}
|
||||
|
||||
if (!IsOpen)
|
||||
{
|
||||
IsOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveNotification(string id)
|
||||
{
|
||||
lock (_notificationLock)
|
||||
{
|
||||
var notification = _notifications.FirstOrDefault(n => n.Id == id);
|
||||
if (notification != null)
|
||||
{
|
||||
notification.IsAnimatingOut = true;
|
||||
notification.IsAnimatingIn = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
lock (_notificationLock)
|
||||
{
|
||||
UpdateNotifications();
|
||||
|
||||
if (_notifications.Count == 0)
|
||||
{
|
||||
IsOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var viewport = ImGui.GetMainViewport();
|
||||
var windowPos = new Vector2(
|
||||
viewport.WorkPos.X + viewport.WorkSize.X - NotificationWidth - EdgeXMargin,
|
||||
viewport.WorkPos.Y + EdgeYMargin
|
||||
);
|
||||
ImGui.SetWindowPos(windowPos);
|
||||
|
||||
for (int i = 0; i < _notifications.Count; i++)
|
||||
{
|
||||
DrawNotification(_notifications[i], i);
|
||||
|
||||
if (i < _notifications.Count - 1)
|
||||
{
|
||||
ImGui.Dummy(new Vector2(0, NotificationSpacing));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNotifications()
|
||||
{
|
||||
var deltaTime = ImGui.GetIO().DeltaTime;
|
||||
|
||||
for (int i = _notifications.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var notification = _notifications[i];
|
||||
|
||||
if (notification.IsAnimatingIn && notification.AnimationProgress < 1f)
|
||||
{
|
||||
notification.AnimationProgress = Math.Min(1f, notification.AnimationProgress + deltaTime * AnimationSpeed);
|
||||
}
|
||||
else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f)
|
||||
{
|
||||
notification.AnimationProgress = Math.Max(0f, notification.AnimationProgress - deltaTime * (AnimationSpeed * 0.7f));
|
||||
}
|
||||
else if (!notification.IsAnimatingOut && !notification.IsDismissed)
|
||||
{
|
||||
notification.IsAnimatingIn = false;
|
||||
|
||||
if (notification.IsExpired && !notification.IsAnimatingOut)
|
||||
{
|
||||
notification.IsAnimatingOut = true;
|
||||
notification.IsAnimatingIn = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f)
|
||||
{
|
||||
_notifications.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNotification(LightlessNotification notification, int index)
|
||||
{
|
||||
var alpha = notification.AnimationProgress;
|
||||
if (alpha <= 0f) return;
|
||||
|
||||
var slideOffset = (1f - alpha) * SlideDistance;
|
||||
var originalCursorPos = ImGui.GetCursorPos();
|
||||
ImGui.SetCursorPosX(originalCursorPos.X + slideOffset);
|
||||
|
||||
var notificationHeight = CalculateNotificationHeight(notification);
|
||||
|
||||
using var child = ImRaii.Child($"notification_{notification.Id}",
|
||||
new Vector2(NotificationWidth - slideOffset, notificationHeight),
|
||||
false, ImGuiWindowFlags.NoScrollbar);
|
||||
|
||||
if (child.Success)
|
||||
{
|
||||
DrawNotificationContent(notification, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNotificationContent(LightlessNotification notification, float alpha)
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var windowPos = ImGui.GetWindowPos();
|
||||
var windowSize = ImGui.GetWindowSize();
|
||||
|
||||
var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, 0.95f * alpha);
|
||||
var accentColor = GetNotificationAccentColor(notification.Type);
|
||||
var progressBarColor = UIColors.Get("LightlessBlue");
|
||||
accentColor.W *= alpha;
|
||||
|
||||
var shadowOffset = new Vector2(1f, 1f);
|
||||
var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha);
|
||||
drawList.AddRectFilled(
|
||||
windowPos + shadowOffset,
|
||||
windowPos + windowSize + shadowOffset,
|
||||
ImGui.ColorConvertFloat4ToU32(shadowColor),
|
||||
3f
|
||||
);
|
||||
|
||||
var isHovered = ImGui.IsWindowHovered();
|
||||
if (isHovered)
|
||||
{
|
||||
bgColor = bgColor * 1.1f;
|
||||
bgColor.W = Math.Min(bgColor.W, 0.98f);
|
||||
}
|
||||
|
||||
drawList.AddRectFilled(
|
||||
windowPos,
|
||||
windowPos + windowSize,
|
||||
ImGui.ColorConvertFloat4ToU32(bgColor),
|
||||
3f
|
||||
);
|
||||
|
||||
var accentWidth = 3f;
|
||||
drawList.AddRectFilled(
|
||||
windowPos,
|
||||
windowPos + new Vector2(accentWidth, windowSize.Y),
|
||||
ImGui.ColorConvertFloat4ToU32(accentColor),
|
||||
3f
|
||||
);
|
||||
|
||||
DrawDurationProgressBar(notification, alpha, windowPos, windowSize, progressBarColor, drawList);
|
||||
|
||||
DrawNotificationText(notification, alpha);
|
||||
}
|
||||
|
||||
private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, Vector4 progressBarColor, ImDrawListPtr drawList)
|
||||
{
|
||||
var elapsed = DateTime.UtcNow - notification.CreatedAt;
|
||||
var progress = Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds));
|
||||
|
||||
var progressHeight = 2f;
|
||||
var progressY = windowPos.Y + windowSize.Y - progressHeight;
|
||||
var progressWidth = windowSize.X * progress;
|
||||
|
||||
var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha);
|
||||
drawList.AddRectFilled(
|
||||
new Vector2(windowPos.X, progressY),
|
||||
new Vector2(windowPos.X + windowSize.X, progressY + progressHeight),
|
||||
ImGui.ColorConvertFloat4ToU32(bgProgressColor),
|
||||
0f
|
||||
);
|
||||
|
||||
if (progress > 0)
|
||||
{
|
||||
var progressColor = progressBarColor;
|
||||
progressColor.W *= alpha;
|
||||
drawList.AddRectFilled(
|
||||
new Vector2(windowPos.X, progressY),
|
||||
new Vector2(windowPos.X + progressWidth, progressY + progressHeight),
|
||||
ImGui.ColorConvertFloat4ToU32(progressColor),
|
||||
0f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNotificationText(LightlessNotification notification, float alpha)
|
||||
{
|
||||
var padding = new Vector2(10f, 6f);
|
||||
var contentPos = new Vector2(6f, 0f) + padding;
|
||||
var windowSize = ImGui.GetWindowSize();
|
||||
var contentSize = windowSize - padding * 2 - new Vector2(6f, 0f);
|
||||
|
||||
ImGui.SetCursorPos(contentPos);
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
|
||||
{
|
||||
ImGui.Text(notification.Title);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(notification.Message))
|
||||
{
|
||||
ImGui.SetCursorPos(contentPos + new Vector2(0f, 18f));
|
||||
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentSize.X);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha)))
|
||||
{
|
||||
ImGui.TextWrapped(notification.Message);
|
||||
}
|
||||
ImGui.PopTextWrapPos();
|
||||
}
|
||||
|
||||
if (notification.ShowProgress)
|
||||
{
|
||||
ImGui.Spacing();
|
||||
var progressColor = GetNotificationAccentColor(notification.Type);
|
||||
progressColor.W *= alpha;
|
||||
using (ImRaii.PushColor(ImGuiCol.PlotHistogram, progressColor))
|
||||
{
|
||||
ImGui.ProgressBar(notification.Progress, new Vector2(contentSize.X, 2f), "");
|
||||
}
|
||||
}
|
||||
|
||||
if (notification.Actions.Count > 0)
|
||||
{
|
||||
ImGui.Spacing();
|
||||
ImGui.SetCursorPosX(contentPos.X);
|
||||
DrawNotificationActions(notification, contentSize.X, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha)
|
||||
{
|
||||
var buttonSpacing = 8f;
|
||||
var rightPadding = 10f;
|
||||
var usableWidth = availableWidth - rightPadding;
|
||||
var totalSpacing = (notification.Actions.Count - 1) * buttonSpacing;
|
||||
var buttonWidth = (usableWidth - totalSpacing) / notification.Actions.Count;
|
||||
|
||||
_logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
|
||||
notification.Actions.Count, buttonWidth, availableWidth);
|
||||
|
||||
var startCursorPos = ImGui.GetCursorPos();
|
||||
|
||||
for (int i = 0; i < notification.Actions.Count; i++)
|
||||
{
|
||||
var action = notification.Actions[i];
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
var currentX = startCursorPos.X + i * (buttonWidth + buttonSpacing);
|
||||
ImGui.SetCursorPosX(currentX);
|
||||
}
|
||||
DrawActionButton(action, notification, alpha, buttonWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth)
|
||||
{
|
||||
_logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth);
|
||||
|
||||
var buttonColor = action.Color;
|
||||
buttonColor.W *= alpha;
|
||||
|
||||
var hoveredColor = buttonColor * 1.1f;
|
||||
hoveredColor.W = buttonColor.W;
|
||||
|
||||
var activeColor = buttonColor * 0.9f;
|
||||
activeColor.W = buttonColor.W;
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Button, buttonColor))
|
||||
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, hoveredColor))
|
||||
using (ImRaii.PushColor(ImGuiCol.ButtonActive, activeColor))
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
|
||||
{
|
||||
var buttonPressed = false;
|
||||
|
||||
if (action.Icon != FontAwesomeIcon.None)
|
||||
{
|
||||
buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha);
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonPressed = ImGui.Button(action.Label, new Vector2(buttonWidth, 0));
|
||||
}
|
||||
|
||||
_logger.LogDebug("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed);
|
||||
|
||||
if (buttonPressed)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Executing action: {ActionId}", action.Id);
|
||||
action.OnClick(notification);
|
||||
_logger.LogDebug("Action executed successfully: {ActionId}", action.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error executing notification action: {ActionId}", action.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha)
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var cursorPos = ImGui.GetCursorScreenPos();
|
||||
var frameHeight = ImGui.GetFrameHeight();
|
||||
|
||||
Vector2 iconSize;
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
iconSize = ImGui.CalcTextSize(icon.ToIconString());
|
||||
}
|
||||
|
||||
var textSize = ImGui.CalcTextSize(text);
|
||||
var spacing = 3f * ImGuiHelpers.GlobalScale;
|
||||
var totalTextWidth = iconSize.X + spacing + textSize.X;
|
||||
|
||||
var buttonPressed = ImGui.InvisibleButton($"btn_{icon}_{text}", new Vector2(width, frameHeight));
|
||||
|
||||
var buttonMin = ImGui.GetItemRectMin();
|
||||
var buttonMax = ImGui.GetItemRectMax();
|
||||
var buttonSize = buttonMax - buttonMin;
|
||||
|
||||
var buttonColor = ImGui.GetColorU32(ImGuiCol.Button);
|
||||
if (ImGui.IsItemHovered())
|
||||
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonHovered);
|
||||
if (ImGui.IsItemActive())
|
||||
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonActive);
|
||||
|
||||
drawList.AddRectFilled(buttonMin, buttonMax, buttonColor, 3f);
|
||||
|
||||
var iconPos = buttonMin + new Vector2((buttonSize.X - totalTextWidth) / 2f, (buttonSize.Y - iconSize.Y) / 2f);
|
||||
var textPos = iconPos + new Vector2(iconSize.X + spacing, (iconSize.Y - textSize.Y) / 2f);
|
||||
|
||||
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
|
||||
|
||||
// Draw icon
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
drawList.AddText(iconPos, textColor, icon.ToIconString());
|
||||
}
|
||||
|
||||
// Draw text
|
||||
drawList.AddText(textPos, textColor, text);
|
||||
|
||||
return buttonPressed;
|
||||
}
|
||||
|
||||
private float CalculateNotificationHeight(LightlessNotification notification)
|
||||
{
|
||||
var height = 40f;
|
||||
if (!string.IsNullOrEmpty(notification.Message))
|
||||
{
|
||||
var textSize = ImGui.CalcTextSize(notification.Message, true, NotificationWidth - 35f);
|
||||
height += textSize.Y + 4f;
|
||||
}
|
||||
|
||||
if (notification.ShowProgress)
|
||||
{
|
||||
height += 12f;
|
||||
}
|
||||
|
||||
if (notification.Actions.Count > 0)
|
||||
{
|
||||
height += 28f;
|
||||
}
|
||||
|
||||
return Math.Max(height, NotificationMinHeight);
|
||||
}
|
||||
|
||||
private Vector4 GetNotificationAccentColor(NotificationType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
NotificationType.Info => UIColors.Get("LightlessPurple"),
|
||||
NotificationType.Warning => UIColors.Get("LightlessYellow"),
|
||||
NotificationType.Error => UIColors.Get("DimRed"),
|
||||
_ => UIColors.Get("LightlessPurple")
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user