work on inventory

This commit is contained in:
Boki 2026-02-13 09:52:06 -05:00
parent 50d32abd49
commit 32781b1462
11 changed files with 240 additions and 146 deletions

View file

@ -181,14 +181,17 @@ public class BotOrchestrator : IAsyncDisposable
await ocrWarmup;
Emit("info", "Checking inventory for leftover items...");
await Inventory.ClearToStash();
Emit("info", "Inventory cleared");
// Wire executor events
TradeExecutor.StateChanged += _ => UpdateExecutorState();
TradeQueue.TradeCompleted += () => { _tradesCompleted++; StatusUpdated?.Invoke(); };
TradeQueue.TradeFailed += () => { _tradesFailed++; StatusUpdated?.Invoke(); };
Inventory.Updated += () => StatusUpdated?.Invoke();
_started = true;
Emit("info", "Checking inventory for leftover items...");
await Inventory.ClearToStash();
Emit("info", "Inventory cleared");
// Load links
var allUrls = new HashSet<string>(cliUrls);
@ -207,7 +210,6 @@ public class BotOrchestrator : IAsyncDisposable
// Wire trade monitor events
TradeMonitor.NewListings += OnNewListings;
_started = true;
Emit("info", $"Loaded {allUrls.Count} trade link(s)");
Log.Information("Bot started");
}

View file

@ -29,6 +29,7 @@ public class SavedSettings
public double? WindowY { get; set; }
public double? WindowWidth { get; set; }
public double? WindowHeight { get; set; }
public bool Headless { get; set; } = true;
}
public class ConfigStore

View file

@ -4,7 +4,9 @@ namespace Poe2Trade.Inventory;
public interface IInventoryManager
{
event Action? Updated;
InventoryTracker Tracker { get; }
byte[]? LastScreenshot { get; }
bool IsAtOwnHideout { get; }
string SellerAccount { get; }
void SetLocation(bool atHome, string? seller = null);

View file

@ -10,6 +10,7 @@ public class InventoryManager : IInventoryManager
{
private static readonly string SalvageTemplate = Path.Combine("assets", "salvage.png");
public event Action? Updated;
public InventoryTracker Tracker { get; } = new();
private bool _atOwnHideout = true;
@ -19,6 +20,7 @@ public class InventoryManager : IInventoryManager
private readonly IClientLogWatcher _logWatcher;
private readonly SavedSettings _config;
public byte[]? LastScreenshot { get; private set; }
public bool IsAtOwnHideout => _atOwnHideout;
public string SellerAccount => _sellerAccount;
@ -44,6 +46,7 @@ public class InventoryManager : IInventoryManager
await _game.OpenInventory();
var result = await _screen.Grid.Scan("inventory");
LastScreenshot = await _screen.CaptureRegion(GridLayouts.Inventory.Region);
var cells = new bool[5, 12];
foreach (var cell in result.Occupied)
@ -52,6 +55,7 @@ public class InventoryManager : IInventoryManager
cells[cell.Row, cell.Col] = true;
}
Tracker.InitFromScan(cells, result.Items, defaultAction);
Updated?.Invoke();
await _game.PressEscape();
await Helpers.Sleep(Delays.PostFocus);

View file

@ -52,7 +52,7 @@ public class TradeMonitor : ITradeMonitor
_config.BrowserUserDataDir,
new BrowserTypeLaunchPersistentContextOptions
{
Headless = false,
Headless = _config.Headless,
ViewportSize = null,
Args = [
"--disable-blink-features=AutomationControlled",

View file

@ -13,5 +13,6 @@
<conv:StatusDotBrushConverter x:Key="StatusDotBrush" />
<conv:ActiveOpacityConverter x:Key="ActiveOpacity" />
<conv:CellBorderConverter x:Key="CellBorderConverter" />
<conv:BoolToOverlayBrushConverter x:Key="OccupiedOverlayBrush" />
</Application.Resources>
</Application>

View file

@ -45,6 +45,7 @@ public class LinkModeToColorConverter : IValueConverter
{
LinkMode.Live => new SolidColorBrush(Color.Parse("#1f6feb")),
LinkMode.Scrap => new SolidColorBrush(Color.Parse("#9e6a03")),
_ => new SolidColorBrush(Color.Parse("#30363d")),
};
}
@ -79,6 +80,20 @@ public class ActiveOpacityConverter : IValueConverter
=> throw new NotSupportedException();
}
public class BoolToOverlayBrushConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
var occupied = value is true;
return occupied
? new SolidColorBrush(Color.FromArgb(64, 56, 168, 50))
: Brushes.Transparent;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public class CellBorderConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)

View file

@ -1,4 +1,6 @@
using System.Collections.ObjectModel;
using System.IO;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Poe2Trade.Bot;
@ -39,6 +41,8 @@ public partial class MainWindowViewModel : ObservableObject
[NotifyCanExecuteChangedFor(nameof(PauseCommand))]
private bool _isStarted;
[ObservableProperty] private Bitmap? _inventoryImage;
[ObservableProperty] private string _newUrl = "";
[ObservableProperty] private string _newLinkName = "";
[ObservableProperty] private LinkMode _newLinkMode = LinkMode.Live;
@ -178,6 +182,20 @@ public partial class MainWindowViewModel : ObservableObject
}
}
var bytes = _bot.Inventory.LastScreenshot;
if (bytes != null)
{
var old = InventoryImage;
InventoryImage = new Bitmap(new MemoryStream(bytes));
old?.Dispose();
}
else
{
var old = InventoryImage;
InventoryImage = null;
old?.Dispose();
}
OnPropertyChanged(nameof(InventoryFreeCells));
}
}

View file

@ -14,6 +14,7 @@ public partial class SettingsViewModel : ObservableObject
[ObservableProperty] private decimal? _stashScanTimeoutMs = 10000;
[ObservableProperty] private decimal? _waitForMoreItemsMs = 20000;
[ObservableProperty] private decimal? _betweenTradesDelayMs = 5000;
[ObservableProperty] private bool _headless = true;
[ObservableProperty] private bool _isSaved;
public SettingsViewModel(BotOrchestrator bot)
@ -31,6 +32,7 @@ public partial class SettingsViewModel : ObservableObject
StashScanTimeoutMs = s.StashScanTimeoutMs;
WaitForMoreItemsMs = s.WaitForMoreItemsMs;
BetweenTradesDelayMs = s.BetweenTradesDelayMs;
Headless = s.Headless;
}
[RelayCommand]
@ -44,6 +46,7 @@ public partial class SettingsViewModel : ObservableObject
s.StashScanTimeoutMs = (int)(StashScanTimeoutMs ?? 10000);
s.WaitForMoreItemsMs = (int)(WaitForMoreItemsMs ?? 20000);
s.BetweenTradesDelayMs = (int)(BetweenTradesDelayMs ?? 5000);
s.Headless = Headless;
});
IsSaved = true;
@ -55,4 +58,5 @@ public partial class SettingsViewModel : ObservableObject
partial void OnStashScanTimeoutMsChanged(decimal? value) => IsSaved = false;
partial void OnWaitForMoreItemsMsChanged(decimal? value) => IsSaved = false;
partial void OnBetweenTradesDelayMsChanged(decimal? value) => IsSaved = false;
partial void OnHeadlessChanged(bool value) => IsSaved = false;
}

View file

@ -3,14 +3,14 @@
xmlns:vm="using:Poe2Trade.Ui.ViewModels"
x:Class="Poe2Trade.Ui.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Title="POE2 Trade Bot"
Width="960" Height="720"
Title="Automata"
Width="960" Height="640"
Background="#0d1117">
<DockPanel Margin="12">
<DockPanel Margin="8">
<!-- STATUS HEADER -->
<Border DockPanel.Dock="Top" Padding="12" Margin="0,0,0,8"
<Border DockPanel.Dock="Top" Padding="8" Margin="0,0,0,6"
Background="#161b22" BorderBrush="#30363d"
BorderThickness="1" CornerRadius="8">
<Grid ColumnDefinitions="Auto,*,Auto">
@ -24,30 +24,30 @@
</StackPanel>
<!-- Stats cards -->
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="16"
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10"
HorizontalAlignment="Center">
<Border Background="#21262d" CornerRadius="6" Padding="16,8">
<Border Background="#21262d" CornerRadius="6" Padding="10,4">
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding ActiveLinksCount}"
FontSize="20" FontWeight="Bold" Foreground="#58a6ff"
FontSize="16" FontWeight="Bold" Foreground="#58a6ff"
HorizontalAlignment="Center" />
<TextBlock Text="ACTIVE LINKS" FontSize="10" Foreground="#8b949e"
HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Background="#21262d" CornerRadius="6" Padding="16,8">
<Border Background="#21262d" CornerRadius="6" Padding="10,4">
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding TradesCompleted}"
FontSize="20" FontWeight="Bold" Foreground="#3fb950"
FontSize="16" FontWeight="Bold" Foreground="#3fb950"
HorizontalAlignment="Center" />
<TextBlock Text="TRADES DONE" FontSize="10" Foreground="#8b949e"
HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Background="#21262d" CornerRadius="6" Padding="16,8">
<Border Background="#21262d" CornerRadius="6" Padding="10,4">
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding TradesFailed}"
FontSize="20" FontWeight="Bold" Foreground="#f85149"
FontSize="16" FontWeight="Bold" Foreground="#f85149"
HorizontalAlignment="Center" />
<TextBlock Text="FAILED" FontSize="10" Foreground="#8b949e"
HorizontalAlignment="Center" />
@ -66,20 +66,22 @@
<!-- TABBED CONTENT -->
<TabControl>
<!-- ========== MAIN TAB ========== -->
<TabItem Header="Main">
<Grid RowDefinitions="Auto,*" Margin="0,8,0,0">
<!-- ========== STATE TAB ========== -->
<TabItem Header="State">
<Grid RowDefinitions="Auto,*" Margin="0,6,0,0">
<!-- Inventory Grid (12x5) -->
<Border Grid.Row="0" Background="#161b22" BorderBrush="#30363d"
BorderThickness="1" CornerRadius="8" Padding="10" Margin="0,0,0,8">
BorderThickness="1" CornerRadius="8" Padding="8" Margin="0,0,0,6">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,6">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="INVENTORY" FontSize="11" FontWeight="SemiBold"
Foreground="#8b949e" />
<TextBlock Text="{Binding InventoryFreeCells, StringFormat='{}{0}/60 free'}"
FontSize="11" Foreground="#8b949e" Margin="12,0,0,0" />
</StackPanel>
<Grid MaxHeight="170">
<Image Source="{Binding InventoryImage}" Stretch="Uniform" />
<ItemsControl ItemsSource="{Binding InventoryCells}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@ -88,28 +90,45 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:CellState">
<Border Margin="1" CornerRadius="2" Height="22"
Background="{Binding IsOccupied, Converter={StaticResource OccupiedBrush}}"
BorderBrush="#3fb950" />
<Border Margin="1" CornerRadius="2"
Background="{Binding IsOccupied, Converter={StaticResource OccupiedOverlayBrush}}"
BorderBrush="#3fb950"
BorderThickness="{Binding Converter={StaticResource CellBorderConverter}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DockPanel>
</Border>
<!-- Links + Logs split -->
<Grid Grid.Row="1" ColumnDefinitions="350,*">
<!-- Logs -->
<Border Grid.Row="1" Background="#0d1117" BorderBrush="#30363d"
BorderThickness="1" CornerRadius="8" Padding="8">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="ACTIVITY LOG"
FontSize="11" FontWeight="SemiBold" Foreground="#8b949e"
Margin="0,0,0,4" />
<ScrollViewer x:Name="LogScroll">
<SelectableTextBlock x:Name="LogBlock"
FontSize="11" FontFamily="Consolas"
TextWrapping="Wrap" />
</ScrollViewer>
</DockPanel>
</Border>
</Grid>
</TabItem>
<!-- Left: Trade Links -->
<Border Grid.Column="0" Background="#161b22" BorderBrush="#30363d"
BorderThickness="1" CornerRadius="8" Padding="10" Margin="0,0,8,0">
<!-- ========== TRADE TAB ========== -->
<TabItem Header="Trade">
<Border Background="#161b22" BorderBrush="#30363d"
BorderThickness="1" CornerRadius="8" Padding="8" Margin="0,6,0,0">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="TRADE LINKS"
FontSize="11" FontWeight="SemiBold" Foreground="#8b949e"
Margin="0,0,0,8" />
Margin="0,0,0,4" />
<!-- Add link form -->
<StackPanel DockPanel.Dock="Top" Spacing="6" Margin="0,0,0,8">
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0,0,0,6">
<TextBox Text="{Binding NewLinkName}" Watermark="Name (optional)" />
<TextBox Text="{Binding NewUrl}" Watermark="Paste trade URL..." />
<StackPanel Orientation="Horizontal" Spacing="6">
@ -124,7 +143,7 @@
<ItemsControl ItemsSource="{Binding Links}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="0,2" Padding="8" Background="#21262d"
<Border Margin="0,2" Padding="6" Background="#21262d"
CornerRadius="4"
Opacity="{Binding Active, Converter={StaticResource ActiveOpacity}}">
<DockPanel>
@ -157,43 +176,17 @@
</ScrollViewer>
</DockPanel>
</Border>
<!-- Right: Logs -->
<Border Grid.Column="1" Background="#161b22" BorderBrush="#30363d"
BorderThickness="1" CornerRadius="8" Padding="10">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="ACTIVITY LOG"
FontSize="11" FontWeight="SemiBold" Foreground="#8b949e"
Margin="0,0,0,8" />
<ListBox ItemsSource="{Binding Logs}" x:Name="LogList"
Background="Transparent" BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:LogEntry">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Time}" Foreground="#484f58"
FontSize="11" FontFamily="Consolas" />
<TextBlock Text="{Binding Message}" FontSize="11"
FontFamily="Consolas" TextWrapping="Wrap"
Foreground="{Binding Level, Converter={StaticResource LogLevelBrush}}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Border>
</Grid>
</Grid>
</TabItem>
<!-- ========== DEBUG TAB ========== -->
<TabItem Header="Debug">
<ScrollViewer DataContext="{Binding DebugVm}" Margin="0,8,0,0">
<StackPanel Spacing="12" Margin="8" x:DataType="vm:DebugViewModel">
<ScrollViewer DataContext="{Binding DebugVm}" Margin="0,6,0,0">
<StackPanel Spacing="8" Margin="6" x:DataType="vm:DebugViewModel">
<!-- Row 1: Quick actions -->
<Border Background="#161b22" BorderBrush="#30363d" BorderThickness="1"
CornerRadius="8" Padding="12">
<StackPanel Spacing="8">
CornerRadius="8" Padding="8">
<StackPanel Spacing="6">
<TextBlock Text="QUICK ACTIONS" FontSize="11" FontWeight="SemiBold"
Foreground="#8b949e" />
<StackPanel Orientation="Horizontal" Spacing="8">
@ -211,8 +204,8 @@
<!-- Row 2: Find text -->
<Border Background="#161b22" BorderBrush="#30363d" BorderThickness="1"
CornerRadius="8" Padding="12">
<StackPanel Spacing="8">
CornerRadius="8" Padding="8">
<StackPanel Spacing="6">
<TextBlock Text="FIND TEXT" FontSize="11" FontWeight="SemiBold"
Foreground="#8b949e" />
<StackPanel Orientation="Horizontal" Spacing="8">
@ -226,8 +219,8 @@
<!-- Row 3: Grid scan -->
<Border Background="#161b22" BorderBrush="#30363d" BorderThickness="1"
CornerRadius="8" Padding="12">
<StackPanel Spacing="8">
CornerRadius="8" Padding="8">
<StackPanel Spacing="6">
<TextBlock Text="GRID SCAN" FontSize="11" FontWeight="SemiBold"
Foreground="#8b949e" />
<StackPanel Orientation="Horizontal" Spacing="8">
@ -240,8 +233,8 @@
<!-- Row 4: Click At -->
<Border Background="#161b22" BorderBrush="#30363d" BorderThickness="1"
CornerRadius="8" Padding="12">
<StackPanel Spacing="8">
CornerRadius="8" Padding="8">
<StackPanel Spacing="6">
<TextBlock Text="CLICK AT POSITION" FontSize="11" FontWeight="SemiBold"
Foreground="#8b949e" />
<StackPanel Orientation="Horizontal" Spacing="8">
@ -256,10 +249,10 @@
<!-- Debug result output -->
<Border Background="#161b22" BorderBrush="#30363d" BorderThickness="1"
CornerRadius="8" Padding="12" MinHeight="60">
CornerRadius="8" Padding="8">
<StackPanel>
<TextBlock Text="OUTPUT" FontSize="11" FontWeight="SemiBold"
Foreground="#8b949e" Margin="0,0,0,6" />
Foreground="#8b949e" Margin="0,0,0,4" />
<TextBlock Text="{Binding DebugResult}" FontFamily="Consolas"
FontSize="11" Foreground="#e6edf3" TextWrapping="Wrap" />
</StackPanel>
@ -270,13 +263,13 @@
<!-- ========== SETTINGS TAB ========== -->
<TabItem Header="Settings">
<ScrollViewer DataContext="{Binding SettingsVm}" Margin="0,8,0,0">
<StackPanel Spacing="12" Margin="8" MaxWidth="600"
<ScrollViewer DataContext="{Binding SettingsVm}" Margin="0,6,0,0">
<StackPanel Spacing="8" Margin="6" MaxWidth="600"
x:DataType="vm:SettingsViewModel">
<Border Background="#161b22" BorderBrush="#30363d" BorderThickness="1"
CornerRadius="8" Padding="16">
<StackPanel Spacing="12">
CornerRadius="8" Padding="10">
<StackPanel Spacing="8">
<TextBlock Text="GENERAL SETTINGS" FontSize="11" FontWeight="SemiBold"
Foreground="#8b949e" />
@ -291,29 +284,32 @@
</StackPanel>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto">
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="4" Margin="0,0,6,8">
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="4" Margin="0,0,4,6">
<TextBlock Text="Travel Timeout (ms)" FontSize="11" Foreground="#8b949e" />
<NumericUpDown Value="{Binding TravelTimeoutMs}" Minimum="1000"
Maximum="60000" Increment="1000" />
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" Spacing="4" Margin="6,0,0,8">
<StackPanel Grid.Row="0" Grid.Column="1" Spacing="4" Margin="4,0,0,6">
<TextBlock Text="Stash Scan Timeout (ms)" FontSize="11" Foreground="#8b949e" />
<NumericUpDown Value="{Binding StashScanTimeoutMs}" Minimum="1000"
Maximum="60000" Increment="1000" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="4" Margin="0,0,6,0">
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="4" Margin="0,0,4,0">
<TextBlock Text="Wait for More Items (ms)" FontSize="11" Foreground="#8b949e" />
<NumericUpDown Value="{Binding WaitForMoreItemsMs}" Minimum="1000"
Maximum="120000" Increment="1000" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" Spacing="4" Margin="6,0,0,0">
<StackPanel Grid.Row="1" Grid.Column="1" Spacing="4" Margin="4,0,0,0">
<TextBlock Text="Delay Between Trades (ms)" FontSize="11" Foreground="#8b949e" />
<NumericUpDown Value="{Binding BetweenTradesDelayMs}" Minimum="0"
Maximum="60000" Increment="1000" />
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,4,0,0">
<CheckBox IsChecked="{Binding Headless}" Content="Headless browser"
Foreground="#e6edf3" Margin="0,4,0,0" />
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,2,0,0">
<Button Content="Save Settings" Command="{Binding SaveSettingsCommand}" />
<TextBlock Text="Saved!" Foreground="#3fb950" VerticalAlignment="Center"
IsVisible="{Binding IsSaved}" FontWeight="SemiBold" />

View file

@ -1,6 +1,8 @@
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Media;
using Poe2Trade.Core;
using Poe2Trade.Ui.ViewModels;
@ -10,6 +12,13 @@ public partial class MainWindow : Window
{
private ConfigStore? _store;
private static readonly IBrush TimeBrush = new SolidColorBrush(Color.Parse("#484f58"));
private static readonly IBrush InfoBrush = new SolidColorBrush(Color.Parse("#58a6ff"));
private static readonly IBrush WarnBrush = new SolidColorBrush(Color.Parse("#d29922"));
private static readonly IBrush ErrorBrush = new SolidColorBrush(Color.Parse("#f85149"));
private static readonly IBrush DebugBrush = new SolidColorBrush(Color.Parse("#8b949e"));
private static readonly IBrush DefaultBrush = new SolidColorBrush(Color.Parse("#e6edf3"));
public MainWindow()
{
InitializeComponent();
@ -42,16 +51,58 @@ public partial class MainWindow : Window
{
vm.Logs.CollectionChanged += (_, args) =>
{
if (args.Action == NotifyCollectionChangedAction.Add)
var block = this.FindControl<SelectableTextBlock>("LogBlock");
var scroll = this.FindControl<ScrollViewer>("LogScroll");
if (block == null) return;
if (args.Action == NotifyCollectionChangedAction.Add && args.NewItems != null)
{
var logList = this.FindControl<ListBox>("LogList");
if (logList != null && vm.Logs.Count > 0)
logList.ScrollIntoView(vm.Logs[^1]);
foreach (LogEntry entry in args.NewItems)
AppendLogEntry(block, entry);
}
else if (args.Action == NotifyCollectionChangedAction.Remove)
{
// Rebuild when old entries trimmed
RebuildLog(block, vm);
}
scroll?.ScrollToEnd();
};
}
}
private static void AppendLogEntry(SelectableTextBlock block, LogEntry entry)
{
if (block.Inlines?.Count > 0)
block.Inlines.Add(new Run("\n"));
block.Inlines ??= [];
block.Inlines.Add(new Run(entry.Time + " ") { Foreground = TimeBrush });
block.Inlines.Add(new Run(entry.Message) { Foreground = LevelBrush(entry.Level) });
}
private static void RebuildLog(SelectableTextBlock block, MainWindowViewModel vm)
{
block.Inlines?.Clear();
block.Inlines ??= [];
for (var i = 0; i < vm.Logs.Count; i++)
{
if (i > 0) block.Inlines.Add(new Run("\n"));
var entry = vm.Logs[i];
block.Inlines.Add(new Run(entry.Time + " ") { Foreground = TimeBrush });
block.Inlines.Add(new Run(entry.Message) { Foreground = LevelBrush(entry.Level) });
}
}
private static IBrush LevelBrush(string level) => level switch
{
"INFO" => InfoBrush,
"WARN" or "WARNING" => WarnBrush,
"ERROR" => ErrorBrush,
"DEBUG" => DebugBrush,
_ => DefaultBrush,
};
protected override void OnClosing(WindowClosingEventArgs e)
{
if (_store != null)