Files
LlamaCasty/ViewModels/MainViewModel.Logs.cs
T
gramps a80108dd96 feat(ui): left-panel alert stack — licence indicator pinned on top, events LIFO below
Creator design (2026-11-05). A stack of red rows at the top of the left panel, above both mode grids
so it shows while editing and while live, pushing the panel's content down.

- Row 1 is the **Steam licence indicator** and nothing displaces it. It reads as status, not a
  warning: "Entitlement 30 fps · Maximum 60 fps", both numbers derived from QualityOptions and
  HasProOutput. While higher tiers are unavailable it shows a **pin** (standing indicator, no close
  control); once ownership is verified the pin becomes a **close control** and the dismissal is
  persisted (`Steam.LicenceIndicatorDismissed`), permanent while ownership holds. Losing ownership
  restores it as pinned — a change of state, not a preference. Click → About.
- **Event rows stack beneath, newest first**, one row per episode (an episode advancing its state
  updates its own row rather than adding another). Each is dismissible. Click → the Logs tab opens
  with that event's sequence range painted red (`HighlightedLogFrom/To` + `LogInRangeConverter`; two
  numbers rather than a collection because the log list is virtualised and realised rows do not
  re-evaluate on membership changes).
- Replaces the old red panel under the quality picker, which is removed.
- The Steam diagnostic text loses "Contact the creator or support" for a neutral
  "Steam licensing is not configured in this build."

Tests: `AttentionAlertStackTests` — pinned first with the status text; owned → closable and the close
survives a reload but ownership loss restores it pinned; pinned cannot be dismissed; event rows LIFO
with in-place episode updates; clicking an event opens Logs and sets its range. AboutHub + Entitlement
tests updated for the removed panel and the new wording. FAST gate: 532 passed, 0 failed, 0 warnings.
2026-11-05 11:41:59 -08:00

158 lines
5.2 KiB
C#

using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Threading;
using ytLive.Helpers;
namespace ytLive.ViewModels;
public partial class MainViewModel
{
private const int VisibleLogEntryLimit = 500;
private readonly ObservableCollection<AppLogEntry> _currentLaunchLogEntries = new();
private readonly ObservableCollection<AppLogEntry> _pinnedLogEntries = new();
private DispatcherTimer? _currentLaunchLogTimer;
private long _lastCurrentLaunchLogSequence;
private bool _isLogsTabSelected;
public ObservableCollection<AppLogEntry> CurrentLaunchLogEntries => _currentLaunchLogEntries;
public ObservableCollection<AppLogEntry> PinnedLogEntries => _pinnedLogEntries;
// The event range a clicked alert points at. A range rather than a collection: the log list is
// virtualised, and a membership change would not re-evaluate realised rows — two numbers do.
private long _highlightedLogFrom;
private long _highlightedLogTo;
public long HighlightedLogFrom => _highlightedLogFrom;
public long HighlightedLogTo => _highlightedLogTo;
private void HighlightLogRange(long from, long to)
{
SetProperty(ref _highlightedLogFrom, from, nameof(HighlightedLogFrom));
SetProperty(ref _highlightedLogTo, to, nameof(HighlightedLogTo));
}
public bool HasPinnedLogEntries => _pinnedLogEntries.Count > 0;
public RelayCommand<AppLogEntry> AcknowledgeLogEntryCommand => new(AcknowledgeLogEntry);
public bool IsLiveChatTabSelected
{
get => !_isLogsTabSelected;
set
{
if (value)
SelectLogsTab(false);
else if (!_isLogsTabSelected)
OnPropertyChanged(nameof(IsLiveChatTabSelected));
}
}
public bool IsLogsTabSelected
{
get => _isLogsTabSelected;
set
{
if (value)
SelectLogsTab(true);
else if (_isLogsTabSelected)
OnPropertyChanged(nameof(IsLogsTabSelected));
}
}
private void InitializeCurrentLaunchLogs()
{
var entries = AppLog.SnapshotCurrentLaunch()
.OrderByDescending(entry => entry.Sequence)
.Take(VisibleLogEntryLimit)
.ToArray();
foreach (var entry in entries)
{
if (entry.ErrorClass.HasValue)
InsertPinnedLogEntry(entry);
else
_currentLaunchLogEntries.Add(entry);
}
_lastCurrentLaunchLogSequence = entries.FirstOrDefault()?.Sequence ?? 0;
_currentLaunchLogTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
_currentLaunchLogTimer.Tick += (_, _) => RefreshCurrentLaunchLogs();
}
private void SelectLogsTab(bool selected)
{
if (_isLogsTabSelected == selected)
{
OnPropertyChanged(nameof(IsLiveChatTabSelected));
OnPropertyChanged(nameof(IsLogsTabSelected));
return;
}
_isLogsTabSelected = selected;
OnPropertyChanged(nameof(IsLiveChatTabSelected));
OnPropertyChanged(nameof(IsLogsTabSelected));
if (selected)
{
RefreshCurrentLaunchLogs();
_currentLaunchLogTimer?.Start();
}
else
{
_currentLaunchLogTimer?.Stop();
}
}
internal void RefreshCurrentLaunchLogs()
{
var newEntries = AppLog.SnapshotCurrentLaunch()
.Where(entry => entry.Sequence > _lastCurrentLaunchLogSequence)
.OrderBy(entry => entry.Sequence)
.ToArray();
foreach (var entry in newEntries)
{
if (entry.ErrorClass.HasValue)
InsertPinnedLogEntry(entry);
else
InsertChronologicalLogEntry(entry);
_lastCurrentLaunchLogSequence = entry.Sequence;
}
}
private void InsertPinnedLogEntry(AppLogEntry entry)
{
var index = 0;
while (index < _pinnedLogEntries.Count)
{
var current = _pinnedLogEntries[index];
var currentClass = (int)current.ErrorClass!.Value;
var entryClass = (int)entry.ErrorClass!.Value;
if (currentClass < entryClass
|| (currentClass == entryClass && current.Sequence > entry.Sequence))
index++;
else
break;
}
_pinnedLogEntries.Insert(index, entry);
OnPropertyChanged(nameof(HasPinnedLogEntries));
}
private void AcknowledgeLogEntry(AppLogEntry? entry)
{
if (entry?.ErrorClass == null || !_pinnedLogEntries.Remove(entry)) return;
InsertChronologicalLogEntry(entry);
OnPropertyChanged(nameof(HasPinnedLogEntries));
}
private void InsertChronologicalLogEntry(AppLogEntry entry)
{
var index = 0;
while (index < _currentLaunchLogEntries.Count
&& _currentLaunchLogEntries[index].Sequence > entry.Sequence)
index++;
_currentLaunchLogEntries.Insert(index, entry);
if (_currentLaunchLogEntries.Count > VisibleLogEntryLimit)
_currentLaunchLogEntries.RemoveAt(_currentLaunchLogEntries.Count - 1);
}
private void StopCurrentLaunchLogFeed()
=> _currentLaunchLogTimer?.Stop();
}