notifcation refactor for better readability
This commit is contained in:
@@ -92,23 +92,17 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public int NotificationOffsetX { get; set; } = 0;
|
public int NotificationOffsetX { get; set; } = 0;
|
||||||
public float NotificationWidth { get; set; } = 350f;
|
public float NotificationWidth { get; set; } = 350f;
|
||||||
public float NotificationSpacing { get; set; } = 8f;
|
public float NotificationSpacing { get; set; } = 8f;
|
||||||
public bool NotificationStackUpwards { get; set; } = false;
|
|
||||||
|
|
||||||
// Animation & Effects
|
// Animation & Effects
|
||||||
public float NotificationAnimationSpeed { get; set; } = 10f;
|
public float NotificationAnimationSpeed { get; set; } = 10f;
|
||||||
public float NotificationAccentBarWidth { get; set; } = 3f;
|
public float NotificationAccentBarWidth { get; set; } = 3f;
|
||||||
|
|
||||||
// Typography
|
|
||||||
public float NotificationFontScale { get; set; } = 1.0f;
|
|
||||||
|
|
||||||
// Duration per Type
|
// Duration per Type
|
||||||
public int InfoNotificationDurationSeconds { get; set; } = 10;
|
public int InfoNotificationDurationSeconds { get; set; } = 10;
|
||||||
public int WarningNotificationDurationSeconds { get; set; } = 15;
|
public int WarningNotificationDurationSeconds { get; set; } = 15;
|
||||||
public int ErrorNotificationDurationSeconds { get; set; } = 20;
|
public int ErrorNotificationDurationSeconds { get; set; } = 20;
|
||||||
public int PairRequestDurationSeconds { get; set; } = 180;
|
public int PairRequestDurationSeconds { get; set; } = 180;
|
||||||
public int DownloadNotificationDurationSeconds { get; set; } = 300;
|
public int DownloadNotificationDurationSeconds { get; set; } = 300;
|
||||||
|
|
||||||
// Sound Settings
|
|
||||||
public uint CustomInfoSoundId { get; set; } = 2; // Se2
|
public uint CustomInfoSoundId { get; set; } = 2; // Se2
|
||||||
public uint CustomWarningSoundId { get; set; } = 16; // Se15
|
public uint CustomWarningSoundId { get; set; } = 16; // Se15
|
||||||
public uint CustomErrorSoundId { get; set; } = 16; // Se15
|
public uint CustomErrorSoundId { get; set; } = 16; // Se15
|
||||||
@@ -118,7 +112,7 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool DisableWarningSound { get; set; } = false;
|
public bool DisableWarningSound { get; set; } = false;
|
||||||
public bool DisableErrorSound { get; set; } = false;
|
public bool DisableErrorSound { get; set; } = false;
|
||||||
public bool DisablePairRequestSound { get; set; } = false;
|
public bool DisablePairRequestSound { get; set; } = false;
|
||||||
public bool DisableDownloadSound { get; set; } = true; // Disabled by default
|
public bool DisableDownloadSound { get; set; } = true; // Disabled by default (annoying)
|
||||||
public bool UseFocusTarget { get; set; } = false;
|
public bool UseFocusTarget { get; set; } = false;
|
||||||
public bool overrideFriendColor { get; set; } = false;
|
public bool overrideFriendColor { get; set; } = false;
|
||||||
public bool overridePartyColor { get; set; } = false;
|
public bool overridePartyColor { get; set; } = false;
|
||||||
|
|||||||
@@ -39,14 +39,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
|
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
|
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
|
||||||
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
|
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
|
||||||
{
|
{
|
||||||
var notification = new LightlessNotification
|
var notification = CreateNotification(title, message, type, duration, actions, soundEffectId);
|
||||||
|
|
||||||
|
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
|
||||||
|
{
|
||||||
|
WrapActionsWithAutoDismiss(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.SoundEffectId.HasValue)
|
||||||
|
{
|
||||||
|
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LightlessNotification CreateNotification(string title, string message, NotificationType type,
|
||||||
|
TimeSpan? duration, List<LightlessNotificationAction>? actions, uint? soundEffectId)
|
||||||
|
{
|
||||||
|
return new LightlessNotification
|
||||||
{
|
{
|
||||||
Title = title,
|
Title = title,
|
||||||
Message = message,
|
Message = message,
|
||||||
@@ -57,30 +72,28 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
ShowProgress = _configService.Current.ShowNotificationProgress,
|
ShowProgress = _configService.Current.ShowNotificationProgress,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
}
|
||||||
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
|
|
||||||
|
private void WrapActionsWithAutoDismiss(LightlessNotification notification)
|
||||||
|
{
|
||||||
|
foreach (var action in notification.Actions)
|
||||||
{
|
{
|
||||||
foreach (var action in notification.Actions)
|
var originalOnClick = action.OnClick;
|
||||||
|
action.OnClick = (n) =>
|
||||||
{
|
{
|
||||||
var originalOnClick = action.OnClick;
|
originalOnClick(n);
|
||||||
action.OnClick = (n) =>
|
if (_configService.Current.AutoDismissOnAction)
|
||||||
{
|
{
|
||||||
originalOnClick(n);
|
DismissNotification(n);
|
||||||
if (_configService.Current.AutoDismissOnAction)
|
}
|
||||||
{
|
};
|
||||||
n.IsDismissed = true;
|
|
||||||
n.IsAnimatingOut = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (notification.SoundEffectId.HasValue)
|
|
||||||
{
|
private void DismissNotification(LightlessNotification notification)
|
||||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
{
|
||||||
}
|
notification.IsDismissed = true;
|
||||||
|
notification.IsAnimatingOut = true;
|
||||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
|
||||||
}
|
}
|
||||||
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
|
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
|
||||||
{
|
{
|
||||||
@@ -90,40 +103,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
Message = $"{senderName} wants to directly pair with you.",
|
Message = $"{senderName} wants to directly pair with you.",
|
||||||
Type = NotificationType.PairRequest,
|
Type = NotificationType.PairRequest,
|
||||||
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
||||||
SoundEffectId = !_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null,
|
SoundEffectId = GetPairRequestSoundId(),
|
||||||
Actions = new List<LightlessNotificationAction>
|
Actions = CreatePairRequestActions(onAccept, onDecline)
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Id = "accept",
|
|
||||||
Label = "Accept",
|
|
||||||
Icon = FontAwesomeIcon.Check,
|
|
||||||
Color = UIColors.Get("LightlessGreen"),
|
|
||||||
IsPrimary = true,
|
|
||||||
OnClick = (n) =>
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Pair request accepted");
|
|
||||||
onAccept();
|
|
||||||
n.IsDismissed = true;
|
|
||||||
n.IsAnimatingOut = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Id = "decline",
|
|
||||||
Label = "Decline",
|
|
||||||
Icon = FontAwesomeIcon.Times,
|
|
||||||
Color = UIColors.Get("DimRed"),
|
|
||||||
IsDestructive = true,
|
|
||||||
OnClick = (n) =>
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Pair request declined");
|
|
||||||
onDecline();
|
|
||||||
n.IsDismissed = true;
|
|
||||||
n.IsAnimatingOut = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (notification.SoundEffectId.HasValue)
|
if (notification.SoundEffectId.HasValue)
|
||||||
@@ -133,7 +114,70 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
|
|
||||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private uint? GetPairRequestSoundId() =>
|
||||||
|
!_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null;
|
||||||
|
|
||||||
|
private List<LightlessNotificationAction> CreatePairRequestActions(Action onAccept, Action onDecline)
|
||||||
|
{
|
||||||
|
return new List<LightlessNotificationAction>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "accept",
|
||||||
|
Label = "Accept",
|
||||||
|
Icon = FontAwesomeIcon.Check,
|
||||||
|
Color = UIColors.Get("LightlessGreen"),
|
||||||
|
IsPrimary = true,
|
||||||
|
OnClick = (n) =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Pair request accepted");
|
||||||
|
onAccept();
|
||||||
|
DismissNotification(n);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "decline",
|
||||||
|
Label = "Decline",
|
||||||
|
Icon = FontAwesomeIcon.Times,
|
||||||
|
Color = UIColors.Get("DimRed"),
|
||||||
|
IsDestructive = true,
|
||||||
|
OnClick = (n) =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Pair request declined");
|
||||||
|
onDecline();
|
||||||
|
DismissNotification(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
|
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
|
||||||
|
{
|
||||||
|
var notification = new LightlessNotification
|
||||||
|
{
|
||||||
|
Title = "Download Complete",
|
||||||
|
Message = FormatDownloadCompleteMessage(fileName, fileCount),
|
||||||
|
Type = NotificationType.Info,
|
||||||
|
Duration = TimeSpan.FromSeconds(8),
|
||||||
|
Actions = CreateDownloadCompleteActions(onOpenFolder),
|
||||||
|
SoundEffectId = NotificationSounds.DownloadComplete
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.SoundEffectId.HasValue)
|
||||||
|
{
|
||||||
|
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatDownloadCompleteMessage(string fileName, int fileCount) =>
|
||||||
|
fileCount > 1
|
||||||
|
? $"Downloaded {fileCount} files successfully."
|
||||||
|
: $"Downloaded {fileName} successfully.";
|
||||||
|
|
||||||
|
private List<LightlessNotificationAction> CreateDownloadCompleteActions(Action? onOpenFolder)
|
||||||
{
|
{
|
||||||
var actions = new List<LightlessNotificationAction>();
|
var actions = new List<LightlessNotificationAction>();
|
||||||
|
|
||||||
@@ -148,21 +192,23 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
OnClick = (n) =>
|
OnClick = (n) =>
|
||||||
{
|
{
|
||||||
onOpenFolder();
|
onOpenFolder();
|
||||||
n.IsDismissed = true;
|
DismissNotification(n);
|
||||||
n.IsAnimatingOut = true;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null, Action? onViewLog = null)
|
||||||
|
{
|
||||||
var notification = new LightlessNotification
|
var notification = new LightlessNotification
|
||||||
{
|
{
|
||||||
Title = "Download Complete",
|
Title = title,
|
||||||
Message = fileCount > 1 ?
|
Message = FormatErrorMessage(message, exception),
|
||||||
$"Downloaded {fileCount} files successfully." :
|
Type = NotificationType.Error,
|
||||||
$"Downloaded {fileName} successfully.",
|
Duration = TimeSpan.FromSeconds(15),
|
||||||
Type = NotificationType.Info,
|
Actions = CreateErrorActions(onRetry, onViewLog),
|
||||||
Duration = TimeSpan.FromSeconds(8),
|
SoundEffectId = NotificationSounds.Error
|
||||||
Actions = actions,
|
|
||||||
SoundEffectId = NotificationSounds.DownloadComplete
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (notification.SoundEffectId.HasValue)
|
if (notification.SoundEffectId.HasValue)
|
||||||
@@ -172,9 +218,14 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
|
|
||||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
}
|
}
|
||||||
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null, Action? onViewLog = null)
|
|
||||||
|
private string FormatErrorMessage(string message, Exception? exception) =>
|
||||||
|
exception != null ? $"{message}\n\nError: {exception.Message}" : message;
|
||||||
|
|
||||||
|
private List<LightlessNotificationAction> CreateErrorActions(Action? onRetry, Action? onViewLog)
|
||||||
{
|
{
|
||||||
var actions = new List<LightlessNotificationAction>();
|
var actions = new List<LightlessNotificationAction>();
|
||||||
|
|
||||||
if (onRetry != null)
|
if (onRetry != null)
|
||||||
{
|
{
|
||||||
actions.Add(new LightlessNotificationAction
|
actions.Add(new LightlessNotificationAction
|
||||||
@@ -186,11 +237,11 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
OnClick = (n) =>
|
OnClick = (n) =>
|
||||||
{
|
{
|
||||||
onRetry();
|
onRetry();
|
||||||
n.IsDismissed = true;
|
DismissNotification(n);
|
||||||
n.IsAnimatingOut = true;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onViewLog != null)
|
if (onViewLog != null)
|
||||||
{
|
{
|
||||||
actions.Add(new LightlessNotificationAction
|
actions.Add(new LightlessNotificationAction
|
||||||
@@ -202,65 +253,14 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
OnClick = (n) => onViewLog()
|
OnClick = (n) => onViewLog()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var notification = new LightlessNotification
|
|
||||||
{
|
return actions;
|
||||||
Title = title,
|
|
||||||
Message = exception != null ? $"{message}\n\nError: {exception.Message}" : message,
|
|
||||||
Type = NotificationType.Error,
|
|
||||||
Duration = TimeSpan.FromSeconds(15),
|
|
||||||
Actions = actions,
|
|
||||||
SoundEffectId = NotificationSounds.Error
|
|
||||||
};
|
|
||||||
|
|
||||||
if (notification.SoundEffectId.HasValue)
|
|
||||||
{
|
|
||||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
|
||||||
}
|
}
|
||||||
public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus, int queueWaiting = 0)
|
public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus, int queueWaiting = 0)
|
||||||
{
|
{
|
||||||
var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList();
|
var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList();
|
||||||
|
|
||||||
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f;
|
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f;
|
||||||
var completedCount = userDownloads.Count(x => x.progress >= 1.0f);
|
var message = BuildPairDownloadMessage(userDownloads, queueWaiting);
|
||||||
var totalCount = userDownloads.Count;
|
|
||||||
|
|
||||||
var message = "";
|
|
||||||
|
|
||||||
if (queueWaiting > 0)
|
|
||||||
{
|
|
||||||
message = $"Queue: {queueWaiting} waiting";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalCount > 0)
|
|
||||||
{
|
|
||||||
var progressMessage = $"Progress: {completedCount}/{totalCount} completed";
|
|
||||||
message = string.IsNullOrEmpty(message) ? progressMessage : $"{message}\n{progressMessage}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userDownloads.Any(x => x.progress < 1.0f))
|
|
||||||
{
|
|
||||||
var maxNamesToShow = _configService.Current.MaxConcurrentPairApplications;
|
|
||||||
var activeDownloads = userDownloads.Where(x => x.progress < 1.0f).Take(maxNamesToShow);
|
|
||||||
var downloadLines = string.Join("\n", activeDownloads.Select(x =>
|
|
||||||
{
|
|
||||||
var statusText = x.status switch
|
|
||||||
{
|
|
||||||
"downloading" => $"{x.progress:P0}",
|
|
||||||
"decompressing" => "decompressing",
|
|
||||||
"queued" => "queued",
|
|
||||||
"waiting" => "waiting for slot",
|
|
||||||
_ => x.status
|
|
||||||
};
|
|
||||||
return $"• {x.playerName}: {statusText}";
|
|
||||||
}));
|
|
||||||
|
|
||||||
message += string.IsNullOrEmpty(message) ? downloadLines : $"\n{downloadLines}";
|
|
||||||
}
|
|
||||||
|
|
||||||
var allDownloadsCompleted = userDownloads.All(x => x.progress >= 1.0f) && userDownloads.Any();
|
|
||||||
|
|
||||||
var notification = new LightlessNotification
|
var notification = new LightlessNotification
|
||||||
{
|
{
|
||||||
@@ -272,59 +272,99 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
ShowProgress = true,
|
ShowProgress = true,
|
||||||
Progress = totalProgress
|
Progress = totalProgress
|
||||||
};
|
};
|
||||||
|
|
||||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
if (allDownloadsCompleted)
|
|
||||||
|
if (AreAllDownloadsCompleted(userDownloads))
|
||||||
{
|
{
|
||||||
DismissPairDownloadNotification();
|
DismissPairDownloadNotification();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DismissPairDownloadNotification()
|
private string BuildPairDownloadMessage(List<(string playerName, float progress, string status)> userDownloads, int queueWaiting)
|
||||||
{
|
{
|
||||||
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
var messageParts = new List<string>();
|
||||||
}
|
|
||||||
|
if (queueWaiting > 0)
|
||||||
private TimeSpan GetDefaultDurationForType(NotificationType type)
|
|
||||||
{
|
|
||||||
return type switch
|
|
||||||
{
|
{
|
||||||
NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds),
|
messageParts.Add($"Queue: {queueWaiting} waiting");
|
||||||
NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
|
}
|
||||||
NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds),
|
|
||||||
NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
if (userDownloads.Count > 0)
|
||||||
NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
|
{
|
||||||
_ => TimeSpan.FromSeconds(10) // Fallback for any unknown types
|
var completedCount = userDownloads.Count(x => x.progress >= 1.0f);
|
||||||
};
|
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeDownloadLines = BuildActiveDownloadLines(userDownloads);
|
||||||
|
if (!string.IsNullOrEmpty(activeDownloadLines))
|
||||||
|
{
|
||||||
|
messageParts.Add(activeDownloadLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("\n", messageParts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string BuildActiveDownloadLines(List<(string playerName, float progress, string status)> userDownloads)
|
||||||
|
{
|
||||||
|
var activeDownloads = userDownloads
|
||||||
|
.Where(x => x.progress < 1.0f)
|
||||||
|
.Take(_configService.Current.MaxConcurrentPairApplications);
|
||||||
|
|
||||||
|
if (!activeDownloads.Any()) return string.Empty;
|
||||||
|
|
||||||
|
return string.Join("\n", activeDownloads.Select(x => $"• {x.playerName}: {FormatDownloadStatus(x)}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatDownloadStatus((string playerName, float progress, string status) download) => download.status switch
|
||||||
|
{
|
||||||
|
"downloading" => $"{download.progress:P0}",
|
||||||
|
"decompressing" => "decompressing",
|
||||||
|
"queued" => "queued",
|
||||||
|
"waiting" => "waiting for slot",
|
||||||
|
_ => download.status
|
||||||
|
};
|
||||||
|
|
||||||
|
private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) =>
|
||||||
|
userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f);
|
||||||
|
|
||||||
|
public void DismissPairDownloadNotification() =>
|
||||||
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||||
|
|
||||||
|
private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds),
|
||||||
|
NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
|
||||||
|
NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds),
|
||||||
|
NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
||||||
|
NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
|
||||||
|
_ => TimeSpan.FromSeconds(10)
|
||||||
|
};
|
||||||
|
|
||||||
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
|
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
|
||||||
{
|
{
|
||||||
if (overrideSoundId.HasValue)
|
if (overrideSoundId.HasValue) return overrideSoundId;
|
||||||
return overrideSoundId;
|
if (IsSoundDisabledForType(type)) return null;
|
||||||
|
return GetConfiguredSoundForType(type);
|
||||||
// Check if this specific notification type is disabled
|
|
||||||
bool isDisabled = type switch
|
|
||||||
{
|
|
||||||
NotificationType.Info => _configService.Current.DisableInfoSound,
|
|
||||||
NotificationType.Warning => _configService.Current.DisableWarningSound,
|
|
||||||
NotificationType.Error => _configService.Current.DisableErrorSound,
|
|
||||||
NotificationType.Download => _configService.Current.DisableDownloadSound,
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isDisabled)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// Return the configured sound for this type
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
NotificationType.Info => _configService.Current.CustomInfoSoundId,
|
|
||||||
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
|
|
||||||
NotificationType.Error => _configService.Current.CustomErrorSoundId,
|
|
||||||
NotificationType.Download => _configService.Current.DownloadSoundId,
|
|
||||||
_ => NotificationSounds.GetDefaultSound(type)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsSoundDisabledForType(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => _configService.Current.DisableInfoSound,
|
||||||
|
NotificationType.Warning => _configService.Current.DisableWarningSound,
|
||||||
|
NotificationType.Error => _configService.Current.DisableErrorSound,
|
||||||
|
NotificationType.Download => _configService.Current.DisableDownloadSound,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
|
||||||
|
private uint GetConfiguredSoundForType(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => _configService.Current.CustomInfoSoundId,
|
||||||
|
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
|
||||||
|
NotificationType.Error => _configService.Current.CustomErrorSoundId,
|
||||||
|
NotificationType.Download => _configService.Current.DownloadSoundId,
|
||||||
|
_ => NotificationSounds.GetDefaultSound(type)
|
||||||
|
};
|
||||||
|
|
||||||
private void PlayNotificationSound(uint soundEffectId)
|
private void PlayNotificationSound(uint soundEffectId)
|
||||||
{
|
{
|
||||||
@@ -342,32 +382,36 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
private void HandleNotificationMessage(NotificationMessage msg)
|
private void HandleNotificationMessage(NotificationMessage msg)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("{msg}", msg.ToString());
|
_logger.LogInformation("{msg}", msg.ToString());
|
||||||
|
|
||||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||||
|
|
||||||
// Get notification location based on type and system preference
|
var location = GetNotificationLocation(msg.Type);
|
||||||
var location = _configService.Current.UseLightlessNotifications
|
|
||||||
? msg.Type switch
|
|
||||||
{
|
|
||||||
NotificationType.Info => _configService.Current.LightlessInfoNotification,
|
|
||||||
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
|
|
||||||
NotificationType.Error => _configService.Current.LightlessErrorNotification,
|
|
||||||
NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification,
|
|
||||||
NotificationType.Download => _configService.Current.LightlessDownloadNotification,
|
|
||||||
_ => NotificationLocation.LightlessUi
|
|
||||||
}
|
|
||||||
: msg.Type switch
|
|
||||||
{
|
|
||||||
NotificationType.Info => _configService.Current.InfoNotification,
|
|
||||||
NotificationType.Warning => _configService.Current.WarningNotification,
|
|
||||||
NotificationType.Error => _configService.Current.ErrorNotification,
|
|
||||||
NotificationType.PairRequest => NotificationLocation.Toast,
|
|
||||||
NotificationType.Download => NotificationLocation.Toast,
|
|
||||||
_ => NotificationLocation.Nowhere
|
|
||||||
};
|
|
||||||
|
|
||||||
ShowNotificationLocationBased(msg, location);
|
ShowNotificationLocationBased(msg, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private NotificationLocation GetNotificationLocation(NotificationType type) =>
|
||||||
|
_configService.Current.UseLightlessNotifications
|
||||||
|
? GetLightlessNotificationLocation(type)
|
||||||
|
: GetClassicNotificationLocation(type);
|
||||||
|
|
||||||
|
private NotificationLocation GetLightlessNotificationLocation(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => _configService.Current.LightlessInfoNotification,
|
||||||
|
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
|
||||||
|
NotificationType.Error => _configService.Current.LightlessErrorNotification,
|
||||||
|
NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification,
|
||||||
|
NotificationType.Download => _configService.Current.LightlessDownloadNotification,
|
||||||
|
_ => NotificationLocation.LightlessUi
|
||||||
|
};
|
||||||
|
|
||||||
|
private NotificationLocation GetClassicNotificationLocation(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => _configService.Current.InfoNotification,
|
||||||
|
NotificationType.Warning => _configService.Current.WarningNotification,
|
||||||
|
NotificationType.Error => _configService.Current.ErrorNotification,
|
||||||
|
NotificationType.PairRequest => NotificationLocation.Toast,
|
||||||
|
NotificationType.Download => NotificationLocation.Toast,
|
||||||
|
_ => NotificationLocation.Nowhere
|
||||||
|
};
|
||||||
|
|
||||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
||||||
{
|
{
|
||||||
@@ -403,18 +447,12 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
private void ShowLightlessNotification(NotificationMessage msg)
|
private void ShowLightlessNotification(NotificationMessage msg)
|
||||||
{
|
{
|
||||||
var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type);
|
var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type);
|
||||||
// GetSoundEffectId will handle checking if the sound is disabled
|
|
||||||
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null);
|
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowToast(NotificationMessage msg)
|
private void ShowToast(NotificationMessage msg)
|
||||||
{
|
{
|
||||||
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch
|
var dalamudType = ConvertToDalamudNotificationType(msg.Type);
|
||||||
{
|
|
||||||
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
|
|
||||||
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
|
||||||
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
|
|
||||||
};
|
|
||||||
|
|
||||||
_notificationManager.AddNotification(new Notification()
|
_notificationManager.AddNotification(new Notification()
|
||||||
{
|
{
|
||||||
@@ -425,6 +463,13 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Dalamud.Interface.ImGuiNotification.NotificationType ConvertToDalamudNotificationType(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
|
||||||
|
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||||
|
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
|
||||||
|
};
|
||||||
|
|
||||||
private void ShowChat(NotificationMessage msg)
|
private void ShowChat(NotificationMessage msg)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,12 +17,15 @@ namespace LightlessSync.UI;
|
|||||||
|
|
||||||
public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
|
private const float NotificationMinHeight = 60f;
|
||||||
|
private const float NotificationMaxHeight = 250f;
|
||||||
|
private const float WindowPaddingOffset = 6f;
|
||||||
|
private const float SlideAnimationDistance = 100f;
|
||||||
|
private const float OutAnimationSpeedMultiplier = 0.7f;
|
||||||
|
|
||||||
private readonly List<LightlessNotification> _notifications = new();
|
private readonly List<LightlessNotification> _notifications = new();
|
||||||
private readonly object _notificationLock = new();
|
private readonly object _notificationLock = new();
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
|
|
||||||
private const float NotificationMinHeight = 60f;
|
|
||||||
private const float NotificationMaxHeight = 250f;
|
|
||||||
|
|
||||||
public LightlessNotificationUI(ILogger<LightlessNotificationUI> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
|
public LightlessNotificationUI(ILogger<LightlessNotificationUI> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
|
||||||
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
|
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
|
||||||
@@ -49,15 +52,11 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<LightlessNotificationMessage>(this, HandleNotificationMessage);
|
Mediator.Subscribe<LightlessNotificationMessage>(this, HandleNotificationMessage);
|
||||||
Mediator.Subscribe<LightlessNotificationDismissMessage>(this, HandleNotificationDismissMessage);
|
Mediator.Subscribe<LightlessNotificationDismissMessage>(this, HandleNotificationDismissMessage);
|
||||||
}
|
}
|
||||||
private void HandleNotificationMessage(LightlessNotificationMessage message)
|
private void HandleNotificationMessage(LightlessNotificationMessage message) =>
|
||||||
{
|
|
||||||
AddNotification(message.Notification);
|
AddNotification(message.Notification);
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message)
|
private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) =>
|
||||||
{
|
|
||||||
RemoveNotification(message.NotificationId);
|
RemoveNotification(message.NotificationId);
|
||||||
}
|
|
||||||
|
|
||||||
public void AddNotification(LightlessNotification notification)
|
public void AddNotification(LightlessNotification notification)
|
||||||
{
|
{
|
||||||
@@ -66,12 +65,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
|||||||
var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id);
|
var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id);
|
||||||
if (existingNotification != null)
|
if (existingNotification != null)
|
||||||
{
|
{
|
||||||
// Update existing notification without restarting animation
|
UpdateExistingNotification(existingNotification, notification);
|
||||||
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
|
else
|
||||||
{
|
{
|
||||||
@@ -79,12 +73,18 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
|||||||
_logger.LogDebug("Added new notification: {Title}", notification.Title);
|
_logger.LogDebug("Added new notification: {Title}", notification.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsOpen)
|
if (!IsOpen) IsOpen = true;
|
||||||
{
|
|
||||||
IsOpen = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateExistingNotification(LightlessNotification existing, LightlessNotification updated)
|
||||||
|
{
|
||||||
|
existing.Message = updated.Message;
|
||||||
|
existing.Progress = updated.Progress;
|
||||||
|
existing.ShowProgress = updated.ShowProgress;
|
||||||
|
existing.Title = updated.Title;
|
||||||
|
_logger.LogDebug("Updated existing notification: {Title}", updated.Title);
|
||||||
|
}
|
||||||
|
|
||||||
public void RemoveNotification(string id)
|
public void RemoveNotification(string id)
|
||||||
{
|
{
|
||||||
@@ -93,11 +93,16 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
|||||||
var notification = _notifications.FirstOrDefault(n => n.Id == id);
|
var notification = _notifications.FirstOrDefault(n => n.Id == id);
|
||||||
if (notification != null)
|
if (notification != null)
|
||||||
{
|
{
|
||||||
notification.IsAnimatingOut = true;
|
StartOutAnimation(notification);
|
||||||
notification.IsAnimatingIn = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void StartOutAnimation(LightlessNotification notification)
|
||||||
|
{
|
||||||
|
notification.IsAnimatingOut = true;
|
||||||
|
notification.IsAnimatingIn = false;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
{
|
{
|
||||||
@@ -115,35 +120,45 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var viewport = ImGui.GetMainViewport();
|
var viewport = ImGui.GetMainViewport();
|
||||||
|
Position = CalculateWindowPosition(viewport);
|
||||||
// Always position at top (choco doesnt know how to handle top positions how fitting)
|
DrawAllNotifications();
|
||||||
var baseX = viewport.WorkPos.X + viewport.WorkSize.X - _configService.Current.NotificationWidth - _configService.Current.NotificationOffsetX - 6f ;
|
|
||||||
var baseY = viewport.WorkPos.Y;
|
|
||||||
|
|
||||||
// Apply Y offset
|
|
||||||
var finalY = baseY + _configService.Current.NotificationOffsetY;
|
|
||||||
|
|
||||||
// Update position
|
|
||||||
Position = new Vector2(baseX, finalY);
|
|
||||||
|
|
||||||
for (int i = 0; i < _notifications.Count; i++)
|
|
||||||
{
|
|
||||||
DrawNotification(_notifications[i], i);
|
|
||||||
|
|
||||||
if (i < _notifications.Count - 1)
|
|
||||||
{
|
|
||||||
ImGui.Dummy(new Vector2(0, _configService.Current.NotificationSpacing));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PopStyleVar();
|
ImGui.PopStyleVar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Vector2 CalculateWindowPosition(ImGuiViewportPtr viewport)
|
||||||
|
{
|
||||||
|
var x = viewport.WorkPos.X + viewport.WorkSize.X -
|
||||||
|
_configService.Current.NotificationWidth -
|
||||||
|
_configService.Current.NotificationOffsetX -
|
||||||
|
WindowPaddingOffset;
|
||||||
|
var y = viewport.WorkPos.Y + _configService.Current.NotificationOffsetY;
|
||||||
|
return new Vector2(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawAllNotifications()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _notifications.Count; i++)
|
||||||
|
{
|
||||||
|
DrawNotification(_notifications[i], i);
|
||||||
|
|
||||||
|
if (i < _notifications.Count - 1)
|
||||||
|
{
|
||||||
|
ImGui.Dummy(new Vector2(0, _configService.Current.NotificationSpacing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateNotifications()
|
private void UpdateNotifications()
|
||||||
{
|
{
|
||||||
var deltaTime = ImGui.GetIO().DeltaTime;
|
var deltaTime = ImGui.GetIO().DeltaTime;
|
||||||
|
EnforceMaxNotificationLimit();
|
||||||
|
UpdateAnimationsAndRemoveExpired(deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnforceMaxNotificationLimit()
|
||||||
|
{
|
||||||
var maxNotifications = _configService.Current.MaxSimultaneousNotifications;
|
var maxNotifications = _configService.Current.MaxSimultaneousNotifications;
|
||||||
while (_notifications.Count(n => !n.IsAnimatingOut) > maxNotifications)
|
while (_notifications.Count(n => !n.IsAnimatingOut) > maxNotifications)
|
||||||
{
|
{
|
||||||
@@ -151,60 +166,70 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
|||||||
.Where(n => !n.IsAnimatingOut)
|
.Where(n => !n.IsAnimatingOut)
|
||||||
.OrderBy(n => n.CreatedAt)
|
.OrderBy(n => n.CreatedAt)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (oldestNotification != null)
|
if (oldestNotification != null)
|
||||||
{
|
{
|
||||||
oldestNotification.IsAnimatingOut = true;
|
StartOutAnimation(oldestNotification);
|
||||||
oldestNotification.IsAnimatingIn = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAnimationsAndRemoveExpired(float deltaTime)
|
||||||
|
{
|
||||||
for (int i = _notifications.Count - 1; i >= 0; i--)
|
for (int i = _notifications.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var notification = _notifications[i];
|
var notification = _notifications[i];
|
||||||
|
UpdateNotificationAnimation(notification, deltaTime);
|
||||||
|
|
||||||
if (notification.IsAnimatingIn && notification.AnimationProgress < 1f)
|
if (ShouldRemoveNotification(notification))
|
||||||
{
|
|
||||||
notification.AnimationProgress = Math.Min(1f, notification.AnimationProgress + deltaTime * _configService.Current.NotificationAnimationSpeed);
|
|
||||||
}
|
|
||||||
else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f)
|
|
||||||
{
|
|
||||||
notification.AnimationProgress = Math.Max(0f, notification.AnimationProgress - deltaTime * (_configService.Current.NotificationAnimationSpeed * 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);
|
_notifications.RemoveAt(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateNotificationAnimation(LightlessNotification notification, float deltaTime)
|
||||||
|
{
|
||||||
|
if (notification.IsAnimatingIn && notification.AnimationProgress < 1f)
|
||||||
|
{
|
||||||
|
notification.AnimationProgress = Math.Min(1f,
|
||||||
|
notification.AnimationProgress + deltaTime * _configService.Current.NotificationAnimationSpeed);
|
||||||
|
}
|
||||||
|
else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f)
|
||||||
|
{
|
||||||
|
notification.AnimationProgress = Math.Max(0f,
|
||||||
|
notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * OutAnimationSpeedMultiplier);
|
||||||
|
}
|
||||||
|
else if (!notification.IsAnimatingOut && !notification.IsDismissed)
|
||||||
|
{
|
||||||
|
notification.IsAnimatingIn = false;
|
||||||
|
|
||||||
|
if (notification.IsExpired)
|
||||||
|
{
|
||||||
|
StartOutAnimation(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldRemoveNotification(LightlessNotification notification) =>
|
||||||
|
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f;
|
||||||
|
|
||||||
private void DrawNotification(LightlessNotification notification, int index)
|
private void DrawNotification(LightlessNotification notification, int index)
|
||||||
{
|
{
|
||||||
var alpha = notification.AnimationProgress;
|
var alpha = notification.AnimationProgress;
|
||||||
|
if (alpha <= 0f) return;
|
||||||
if (alpha <= 0f)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var slideOffset = (1f - alpha) * 100f; // Fixed slide distance
|
|
||||||
|
|
||||||
|
var slideOffset = (1f - alpha) * SlideAnimationDistance;
|
||||||
var originalCursorPos = ImGui.GetCursorPos();
|
var originalCursorPos = ImGui.GetCursorPos();
|
||||||
ImGui.SetCursorPosX(originalCursorPos.X + slideOffset);
|
ImGui.SetCursorPosX(originalCursorPos.X + slideOffset);
|
||||||
|
|
||||||
var notificationHeight = CalculateNotificationHeight(notification);
|
var notificationHeight = CalculateNotificationHeight(notification);
|
||||||
|
var notificationWidth = _configService.Current.NotificationWidth - slideOffset;
|
||||||
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
|
||||||
|
|
||||||
using var child = ImRaii.Child($"notification_{notification.Id}",
|
using var child = ImRaii.Child($"notification_{notification.Id}",
|
||||||
new Vector2(_configService.Current.NotificationWidth - slideOffset, notificationHeight),
|
new Vector2(notificationWidth, notificationHeight),
|
||||||
false, ImGuiWindowFlags.NoScrollbar);
|
false, ImGuiWindowFlags.NoScrollbar);
|
||||||
|
|
||||||
if (child.Success)
|
if (child.Success)
|
||||||
@@ -221,50 +246,69 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
|||||||
var windowPos = ImGui.GetWindowPos();
|
var windowPos = ImGui.GetWindowPos();
|
||||||
var windowSize = ImGui.GetWindowSize();
|
var windowSize = ImGui.GetWindowSize();
|
||||||
|
|
||||||
|
var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered());
|
||||||
|
var accentColor = GetNotificationAccentColor(notification.Type);
|
||||||
|
accentColor.W *= alpha;
|
||||||
|
|
||||||
|
DrawShadow(drawList, windowPos, windowSize, alpha);
|
||||||
|
HandleClickToDismiss(notification);
|
||||||
|
DrawBackground(drawList, windowPos, windowSize, bgColor);
|
||||||
|
DrawAccentBar(drawList, windowPos, windowSize, accentColor);
|
||||||
|
DrawDurationProgressBar(notification, alpha, windowPos, windowSize, drawList);
|
||||||
|
DrawNotificationText(notification, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector4 CalculateBackgroundColor(float alpha, bool isHovered)
|
||||||
|
{
|
||||||
var baseOpacity = _configService.Current.NotificationOpacity;
|
var baseOpacity = _configService.Current.NotificationOpacity;
|
||||||
var finalOpacity = baseOpacity * alpha;
|
var finalOpacity = baseOpacity * alpha;
|
||||||
var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity);
|
var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity);
|
||||||
var accentColor = GetNotificationAccentColor(notification.Type);
|
|
||||||
var progressBarColor = UIColors.Get("LightlessBlue");
|
|
||||||
|
|
||||||
accentColor.W *= alpha;
|
if (isHovered)
|
||||||
|
{
|
||||||
|
bgColor *= 1.1f;
|
||||||
|
bgColor.W = Math.Min(bgColor.W, 0.98f);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw shadow with fixed intensity
|
return bgColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha)
|
||||||
|
{
|
||||||
var shadowOffset = new Vector2(1f, 1f);
|
var shadowOffset = new Vector2(1f, 1f);
|
||||||
var shadowAlpha = 0.4f * alpha;
|
var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha);
|
||||||
var shadowColor = new Vector4(0f, 0f, 0f, shadowAlpha);
|
|
||||||
drawList.AddRectFilled(
|
drawList.AddRectFilled(
|
||||||
windowPos + shadowOffset,
|
windowPos + shadowOffset,
|
||||||
windowPos + windowSize + shadowOffset,
|
windowPos + windowSize + shadowOffset,
|
||||||
ImGui.ColorConvertFloat4ToU32(shadowColor),
|
ImGui.ColorConvertFloat4ToU32(shadowColor),
|
||||||
3f
|
3f
|
||||||
);
|
);
|
||||||
|
}
|
||||||
var isHovered = ImGui.IsWindowHovered();
|
|
||||||
|
private void HandleClickToDismiss(LightlessNotification notification)
|
||||||
if (isHovered)
|
{
|
||||||
|
if (ImGui.IsWindowHovered() &&
|
||||||
|
_configService.Current.DismissNotificationOnClick &&
|
||||||
|
!notification.Actions.Any() &&
|
||||||
|
ImGui.IsMouseClicked(ImGuiMouseButton.Left))
|
||||||
{
|
{
|
||||||
bgColor = bgColor * 1.1f;
|
notification.IsDismissed = true;
|
||||||
bgColor.W = Math.Min(bgColor.W, 0.98f);
|
StartOutAnimation(notification);
|
||||||
|
|
||||||
// Handle click-to-dismiss for notifications without actions
|
|
||||||
if (_configService.Current.DismissNotificationOnClick &&
|
|
||||||
!notification.Actions.Any() &&
|
|
||||||
ImGui.IsMouseClicked(ImGuiMouseButton.Left))
|
|
||||||
{
|
|
||||||
notification.IsDismissed = true;
|
|
||||||
notification.IsAnimatingOut = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor)
|
||||||
|
{
|
||||||
drawList.AddRectFilled(
|
drawList.AddRectFilled(
|
||||||
windowPos,
|
windowPos,
|
||||||
windowPos + windowSize,
|
windowPos + windowSize,
|
||||||
ImGui.ColorConvertFloat4ToU32(bgColor),
|
ImGui.ColorConvertFloat4ToU32(bgColor),
|
||||||
3f
|
3f
|
||||||
);
|
);
|
||||||
|
}
|
||||||
// Draw accent bar on left side of the notif (only if width > 0)
|
|
||||||
|
private void DrawAccentBar(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 accentColor)
|
||||||
|
{
|
||||||
var accentWidth = _configService.Current.NotificationAccentBarWidth;
|
var accentWidth = _configService.Current.NotificationAccentBarWidth;
|
||||||
if (accentWidth > 0f)
|
if (accentWidth > 0f)
|
||||||
{
|
{
|
||||||
@@ -275,30 +319,37 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
|||||||
3f
|
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)
|
private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
|
||||||
{
|
{
|
||||||
// For download notifications, use download progress instead of duration
|
var progress = CalculateProgress(notification);
|
||||||
float progress;
|
var progressBarColor = UIColors.Get("LightlessBlue");
|
||||||
if (notification.Type == NotificationType.Download && notification.ShowProgress)
|
|
||||||
{
|
|
||||||
progress = Math.Clamp(notification.Progress, 0f, 1f);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var elapsed = DateTime.UtcNow - notification.CreatedAt;
|
|
||||||
progress = Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds));
|
|
||||||
}
|
|
||||||
|
|
||||||
var progressHeight = 2f;
|
var progressHeight = 2f;
|
||||||
var progressY = windowPos.Y + windowSize.Y - progressHeight;
|
var progressY = windowPos.Y + windowSize.Y - progressHeight;
|
||||||
var progressWidth = windowSize.X * progress;
|
var progressWidth = windowSize.X * progress;
|
||||||
|
|
||||||
|
DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha);
|
||||||
|
|
||||||
|
if (progress > 0)
|
||||||
|
{
|
||||||
|
DrawProgressForeground(drawList, windowPos, progressY, progressHeight, progressWidth, progressBarColor, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateProgress(LightlessNotification notification)
|
||||||
|
{
|
||||||
|
if (notification.Type == NotificationType.Download && notification.ShowProgress)
|
||||||
|
{
|
||||||
|
return Math.Clamp(notification.Progress, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsed = DateTime.UtcNow - notification.CreatedAt;
|
||||||
|
return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha)
|
||||||
|
{
|
||||||
var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha);
|
var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha);
|
||||||
drawList.AddRectFilled(
|
drawList.AddRectFilled(
|
||||||
new Vector2(windowPos.X, progressY),
|
new Vector2(windowPos.X, progressY),
|
||||||
@@ -306,71 +357,70 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.ColorConvertFloat4ToU32(bgProgressColor),
|
ImGui.ColorConvertFloat4ToU32(bgProgressColor),
|
||||||
0f
|
0f
|
||||||
);
|
);
|
||||||
|
}
|
||||||
if (progress > 0)
|
|
||||||
{
|
private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha)
|
||||||
var progressColor = progressBarColor;
|
{
|
||||||
progressColor.W *= alpha;
|
var progressColor = progressBarColor;
|
||||||
drawList.AddRectFilled(
|
progressColor.W *= alpha;
|
||||||
new Vector2(windowPos.X, progressY),
|
drawList.AddRectFilled(
|
||||||
new Vector2(windowPos.X + progressWidth, progressY + progressHeight),
|
new Vector2(windowPos.X, progressY),
|
||||||
ImGui.ColorConvertFloat4ToU32(progressColor),
|
new Vector2(windowPos.X + progressWidth, progressY + progressHeight),
|
||||||
0f
|
ImGui.ColorConvertFloat4ToU32(progressColor),
|
||||||
);
|
0f
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawNotificationText(LightlessNotification notification, float alpha)
|
private void DrawNotificationText(LightlessNotification notification, float alpha)
|
||||||
{
|
{
|
||||||
var padding = new Vector2(10f, 6f);
|
var padding = new Vector2(10f, 6f);
|
||||||
|
|
||||||
var contentPos = new Vector2(padding.X, padding.Y);
|
var contentPos = new Vector2(padding.X, padding.Y);
|
||||||
var windowSize = ImGui.GetWindowSize();
|
var windowSize = ImGui.GetWindowSize();
|
||||||
var contentSize = new Vector2(windowSize.X - padding.X, windowSize.Y - padding.Y * 2);
|
var contentSize = new Vector2(windowSize.X - padding.X, windowSize.Y - padding.Y * 2);
|
||||||
|
|
||||||
ImGui.SetCursorPos(contentPos);
|
ImGui.SetCursorPos(contentPos);
|
||||||
|
|
||||||
float titleHeight = 0f;
|
var titleHeight = DrawTitle(notification, contentSize.X, alpha);
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
|
DrawMessage(notification, contentPos, contentSize.X, titleHeight, alpha);
|
||||||
{
|
|
||||||
// Set text wrap position to prevent title overflow
|
|
||||||
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentSize.X);
|
|
||||||
|
|
||||||
var titleStartY = ImGui.GetCursorPosY();
|
|
||||||
|
|
||||||
if (_configService.Current.ShowNotificationTimestamp)
|
|
||||||
{
|
|
||||||
var timestamp = notification.CreatedAt.ToLocalTime().ToString("HH:mm:ss");
|
|
||||||
ImGui.TextWrapped($"[{timestamp}] {notification.Title}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.TextWrapped(notification.Title);
|
|
||||||
}
|
|
||||||
|
|
||||||
titleHeight = ImGui.GetCursorPosY() - titleStartY;
|
|
||||||
ImGui.PopTextWrapPos();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(notification.Message))
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPos(contentPos + new Vector2(0f, titleHeight + 4f));
|
|
||||||
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.Actions.Count > 0)
|
if (notification.Actions.Count > 0)
|
||||||
{
|
{
|
||||||
var spacingHeight = ImGui.GetStyle().ItemSpacing.Y;
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().ItemSpacing.Y);
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + spacingHeight);
|
|
||||||
ImGui.SetCursorPosX(contentPos.X);
|
ImGui.SetCursorPosX(contentPos.X);
|
||||||
DrawNotificationActions(notification, contentSize.X, alpha);
|
DrawNotificationActions(notification, contentSize.X, alpha);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha)
|
||||||
|
{
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
|
||||||
|
{
|
||||||
|
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth);
|
||||||
|
var titleStartY = ImGui.GetCursorPosY();
|
||||||
|
|
||||||
|
var titleText = _configService.Current.ShowNotificationTimestamp
|
||||||
|
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
|
||||||
|
: notification.Title;
|
||||||
|
|
||||||
|
ImGui.TextWrapped(titleText);
|
||||||
|
var titleHeight = ImGui.GetCursorPosY() - titleStartY;
|
||||||
|
ImGui.PopTextWrapPos();
|
||||||
|
return titleHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(notification.Message)) return;
|
||||||
|
|
||||||
|
ImGui.SetCursorPos(contentPos + new Vector2(0f, titleHeight + 4f));
|
||||||
|
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth);
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha)))
|
||||||
|
{
|
||||||
|
ImGui.TextWrapped(notification.Message);
|
||||||
|
}
|
||||||
|
ImGui.PopTextWrapPos();
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha)
|
private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha)
|
||||||
{
|
{
|
||||||
@@ -495,44 +545,43 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private float CalculateNotificationHeight(LightlessNotification notification)
|
private float CalculateNotificationHeight(LightlessNotification notification)
|
||||||
{
|
{
|
||||||
var contentWidth = _configService.Current.NotificationWidth - 35f; // Account for padding and accent bar
|
var contentWidth = _configService.Current.NotificationWidth - 35f;
|
||||||
var height = 12f; // Base height for padding (top + bottom)
|
var height = 12f;
|
||||||
|
|
||||||
var titleText = notification.Title;
|
height += CalculateTitleHeight(notification, contentWidth);
|
||||||
if (_configService.Current.ShowNotificationTimestamp)
|
height += CalculateMessageHeight(notification, contentWidth);
|
||||||
{
|
|
||||||
var timestamp = notification.CreatedAt.ToLocalTime().ToString("HH:mm:ss");
|
|
||||||
titleText = $"[{timestamp}] {titleText}";
|
|
||||||
}
|
|
||||||
|
|
||||||
var titleSize = ImGui.CalcTextSize(titleText, true, contentWidth);
|
|
||||||
height += titleSize.Y; // Title height
|
|
||||||
|
|
||||||
// Calculate message height
|
|
||||||
if (!string.IsNullOrEmpty(notification.Message))
|
|
||||||
{
|
|
||||||
height += 4f; // Spacing between title and message
|
|
||||||
var messageSize = ImGui.CalcTextSize(notification.Message, true, contentWidth); // This is cringe
|
|
||||||
height += messageSize.Y; // Message height
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add height for progress bar
|
|
||||||
if (notification.ShowProgress)
|
if (notification.ShowProgress)
|
||||||
{
|
{
|
||||||
height += 12f;
|
height += 12f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add height for action buttons
|
|
||||||
if (notification.Actions.Count > 0)
|
if (notification.Actions.Count > 0)
|
||||||
{
|
{
|
||||||
height += ImGui.GetStyle().ItemSpacing.Y; // Spacing before buttons
|
height += ImGui.GetStyle().ItemSpacing.Y;
|
||||||
height += ImGui.GetFrameHeight(); // Button height
|
height += ImGui.GetFrameHeight();
|
||||||
height += 12f; // Bottom padding for buttons
|
height += 12f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow notifications to grow taller but cap at maximum height
|
|
||||||
return Math.Clamp(height, NotificationMinHeight, NotificationMaxHeight);
|
return Math.Clamp(height, NotificationMinHeight, NotificationMaxHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float CalculateTitleHeight(LightlessNotification notification, float contentWidth)
|
||||||
|
{
|
||||||
|
var titleText = _configService.Current.ShowNotificationTimestamp
|
||||||
|
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
|
||||||
|
: notification.Title;
|
||||||
|
|
||||||
|
return ImGui.CalcTextSize(titleText, true, contentWidth).Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateMessageHeight(LightlessNotification notification, float contentWidth)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(notification.Message)) return 0f;
|
||||||
|
|
||||||
|
var messageHeight = ImGui.CalcTextSize(notification.Message, true, contentWidth).Y;
|
||||||
|
return 4f + messageHeight;
|
||||||
|
}
|
||||||
|
|
||||||
private Vector4 GetNotificationAccentColor(NotificationType type)
|
private Vector4 GetNotificationAccentColor(NotificationType type)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,12 +15,9 @@ public class LightlessNotification
|
|||||||
public List<LightlessNotificationAction> Actions { get; set; } = new();
|
public List<LightlessNotificationAction> Actions { get; set; } = new();
|
||||||
public bool ShowProgress { get; set; } = false;
|
public bool ShowProgress { get; set; } = false;
|
||||||
public float Progress { get; set; } = 0f;
|
public float Progress { get; set; } = 0f;
|
||||||
public bool IsMinimized { get; set; } = false;
|
|
||||||
public float AnimationProgress { get; set; } = 0f;
|
public float AnimationProgress { get; set; } = 0f;
|
||||||
public bool IsAnimatingIn { get; set; } = true;
|
public bool IsAnimatingIn { get; set; } = true;
|
||||||
public bool IsAnimatingOut { get; set; } = false;
|
public bool IsAnimatingOut { get; set; } = false;
|
||||||
|
|
||||||
// Sound properties
|
|
||||||
public uint? SoundEffectId { get; set; } = null;
|
public uint? SoundEffectId { get; set; } = null;
|
||||||
}
|
}
|
||||||
public class LightlessNotificationAction
|
public class LightlessNotificationAction
|
||||||
|
|||||||
Reference in New Issue
Block a user