using System.Collections.ObjectModel;
using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Presentation;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
private static readonly GameEventTypeOption allGameEventTypes=new("","全部事件");
public LiveCollection<GameEventLiveRow,GameEventEntry,long> GameEventRows{get;}=new(x=>x.Id,x=>new(x));
public ObservableCollection<GameEventTypeOption> GameEventTypes{get;}=[allGameEventTypes];
[ObservableProperty] public partial string GameEventSearch{get;set;}="";
[ObservableProperty] public partial GameEventTypeOption? GameEventType{get;set;}=allGameEventTypes;
[ObservableProperty] public partial int GameEventSortIndex{get;set;}
[ObservableProperty] public partial string GameEventLogStatus{get;set;}="正在读取游戏事件";
[ObservableProperty] public partial string GameEventPageLabel{get;set;}="1 / 1";
[ObservableProperty] public partial bool GameEventLogBusy{get;set;}
[ObservableProperty] public partial bool GameEventLogHasMore{get;set;}
[ObservableProperty,NotifyPropertyChangedFor(nameof(HasGameEventDetails))]
public partial GameEventDetails? SelectedGameEventDetail{get;set;}
public bool HasGameEventDetails=>SelectedGameEventDetail is not null;
public bool GameLogPageVisible{get;set;}
private readonly List<GameEventCursor?> gameLogPages=[null];
private int gameLogPage,gameLogQuery;
partial void OnGameEventSearchChanged(string value)=>ResetGameLogSearch();
partial void OnGameEventTypeChanged(GameEventTypeOption? value)=>ResetGameLogSearch();
partial void OnGameEventSortIndexChanged(int value)=>ResetGameLogSearch();
partial void OnGameEventLogBusyChanged(bool value)
{
LatestGameLogCommand.NotifyCanExecuteChanged();
PreviousGameLogPageCommand.NotifyCanExecuteChanged();
NextGameLogPageCommand.NotifyCanExecuteChanged();
}
private async void ResetGameLogSearch()
{
var query=++gameLogQuery;gameLogPages.Clear();gameLogPages.Add(null);gameLogPage=0;
try{await Task.Delay(200,lifetime.Token);if(query==gameLogQuery)await RefreshGameEventsAsync();}
catch(OperationCanceledException){}
}
public async Task RefreshGameEventsAsync()
{
var query=++gameLogQuery;var cursor=gameLogPages[gameLogPage];var search=GameEventSearch;
var eventName=GameEventType?.Event??"";var oldestFirst=GameEventSortIndex==1;var firstPage=gameLogPage==0;
GameEventLogBusy=true;
try
{
var result=await Task.Run(()=>
(Page:runtime.GameEvents.Query(search,cursor,eventName:eventName,oldestFirst:oldestFirst),
Types:firstPage?runtime.GameEvents.EventTypes():null),lifetime.Token);
if(query!=gameLogQuery)return;
if(result.Types is not null)
foreach(var type in result.Types)
if(!GameEventTypes.Any(option=>option.Event==type))
{
var translated=GameEventNames.Translate(type);
var option=new GameEventTypeOption(type,translated=="游戏事件"?type:$"{translated} · {type}");
var position=1;
while(position<GameEventTypes.Count&&StringComparer.OrdinalIgnoreCase.Compare(GameEventTypes[position].Event,type)<0)position++;
GameEventTypes.Insert(position,option);
}
GameEventRows.Apply(result.Page.Entries);GameEventLogHasMore=result.Page.HasMore;
GameEventLogStatus=$"共 {result.Page.Count:N0} 条事件 · 第 {gameLogPage+1} 页 · 当前 {result.Page.Entries.Count} 条";
GameEventPageLabel=$"{gameLogPage+1:N0} / {Math.Max(1,(result.Page.Count+199)/200):N0}";
}
catch(Exception error)when(error is not OperationCanceledException){GameEventLogStatus="游戏日志读取失败 · "+error.Message;}
finally{if(query==gameLogQuery)GameEventLogBusy=false;}
}
public void SelectGameEvent(GameEventLiveRow? row)
{
if(SelectedGameEventDetail?.Id==row?.Id)return;
SelectedGameEventDetail=row is null?null:GameEventDetails.From(row.Record);
}
private bool CanRefreshGameLog()=>!GameEventLogBusy;
[RelayCommand(CanExecute=nameof(CanRefreshGameLog))]
private async Task LatestGameLogAsync(){gameLogPage=0;gameLogPages.Clear();gameLogPages.Add(null);await RefreshGameEventsAsync();}
private bool CanPreviousGameLogPage()=>!GameEventLogBusy&&gameLogPage>0;
private bool CanNextGameLogPage()=>!GameEventLogBusy&&GameEventLogHasMore&&GameEventRows.Count>0;
[RelayCommand(CanExecute=nameof(CanPreviousGameLogPage))]
private async Task PreviousGameLogPageAsync(){gameLogPage--;await RefreshGameEventsAsync();}
[RelayCommand(CanExecute=nameof(CanNextGameLogPage))]
private async Task NextGameLogPageAsync()
{
var cursor=GameEventRows.Last().Record.Cursor;
if(gameLogPages.Count>gameLogPage+1)gameLogPages[gameLogPage+1]=cursor;else gameLogPages.Add(cursor);
gameLogPage++;await RefreshGameEventsAsync();
}
}
public sealed record GameEventTypeOption(string Event,string Display);
public sealed record GameEventDetailField(string Label,string Value);
public sealed record GameEventDetails(long Id,string Name,string OriginalName,string Time,string Source,string Summary,IReadOnlyList<GameEventDetailField> Fields,string Json)
{
public static GameEventDetails From(GameEventEntry entry)
{
using var document=JsonDocument.Parse(entry.Json);
var fields=document.RootElement.EnumerateObject().Where(p=>p.Name is not("timestamp" or "event"))
.Select(p=>new GameEventDetailField(GameEventNames.Field(p.Name),p.Value.ValueKind switch
{
JsonValueKind.Array=>$"{p.Value.GetArrayLength():N0} 项",
JsonValueKind.Object=>$"{p.Value.EnumerateObject().Count():N0} 个字段",
JsonValueKind.True=>"是",JsonValueKind.False=>"否",JsonValueKind.Null=>"—",_=>p.Value.ToString()
})).ToArray();
return new(entry.Id,entry.Name,entry.Event,entry.Time.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss zzz"),
Path.GetFileName(entry.Source),entry.Summary,fields,
JsonSerializer.Serialize(document.RootElement,new JsonSerializerOptions{WriteIndented=true}));
}
}
using System.Collections.ObjectModel;
using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Presentation;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
private static readonly GameEventTypeOption allGameEventTypes=new("","全部事件");
public LiveCollection<GameEventLiveRow,GameEventEntry,long> GameEventRows{get;}=new(x=>x.Id,x=>new(x));
public ObservableCollection<GameEventTypeOption> GameEventTypes{get;}=[allGameEventTypes];
[ObservableProperty] public partial string GameEventSearch{get;set;}="";
[ObservableProperty] public partial GameEventTypeOption? GameEventType{get;set;}=allGameEventTypes;
[ObservableProperty] public partial int GameEventSortIndex{get;set;}
[ObservableProperty] public partial string GameEventLogStatus{get;set;}="正在读取游戏事件";
[ObservableProperty] public partial string GameEventPageLabel{get;set;}="1 / 1";
[ObservableProperty] public partial bool GameEventLogBusy{get;set;}
[ObservableProperty] public partial bool GameEventLogHasMore{get;set;}
[ObservableProperty,NotifyPropertyChangedFor(nameof(HasGameEventDetails))]
public partial GameEventDetails? SelectedGameEventDetail{get;set;}
public bool HasGameEventDetails=>SelectedGameEventDetail is not null;
public bool GameLogPageVisible{get;set;}
private readonly List<GameEventCursor?> gameLogPages=[null];
private int gameLogPage,gameLogQuery;
partial void OnGameEventSearchChanged(string value)=>ResetGameLogSearch();
partial void OnGameEventTypeChanged(GameEventTypeOption? value)=>ResetGameLogSearch();
partial void OnGameEventSortIndexChanged(int value)=>ResetGameLogSearch();
partial void OnGameEventLogBusyChanged(bool value)
{
LatestGameLogCommand.NotifyCanExecuteChanged();
PreviousGameLogPageCommand.NotifyCanExecuteChanged();
NextGameLogPageCommand.NotifyCanExecuteChanged();
}
private async void ResetGameLogSearch()
{
var query=++gameLogQuery;gameLogPages.Clear();gameLogPages.Add(null);gameLogPage=0;
try{await Task.Delay(200,lifetime.Token);if(query==gameLogQuery)await RefreshGameEventsAsync();}
catch(OperationCanceledException){}
}
public async Task RefreshGameEventsAsync()
{
var query=++gameLogQuery;var cursor=gameLogPages[gameLogPage];var search=GameEventSearch;
var eventName=GameEventType?.Event??"";var oldestFirst=GameEventSortIndex==1;var firstPage=gameLogPage==0;
GameEventLogBusy=true;
try
{
var result=await Task.Run(()=>
(Page:runtime.GameEvents.Query(search,cursor,eventName:eventName,oldestFirst:oldestFirst),
Types:firstPage?runtime.GameEvents.EventTypes():null),lifetime.Token);
if(query!=gameLogQuery)return;
if(result.Types is not null)
foreach(var type in result.Types)
if(!GameEventTypes.Any(option=>option.Event==type))
{
var translated=GameEventNames.Translate(type);
var option=new GameEventTypeOption(type,translated=="游戏事件"?type:$"{translated} · {type}");
var position=1;
while(position<GameEventTypes.Count&&StringComparer.OrdinalIgnoreCase.Compare(GameEventTypes[position].Event,type)<0)position++;
GameEventTypes.Insert(position,option);
}
GameEventRows.Apply(result.Page.Entries);GameEventLogHasMore=result.Page.HasMore;
GameEventLogStatus=$"共 {result.Page.Count:N0} 条事件 · 第 {gameLogPage+1} 页 · 当前 {result.Page.Entries.Count} 条";
GameEventPageLabel=$"{gameLogPage+1:N0} / {Math.Max(1,(result.Page.Count+199)/200):N0}";
}
catch(Exception error)when(error is not OperationCanceledException){GameEventLogStatus="游戏日志读取失败 · "+error.Message;}
finally{if(query==gameLogQuery)GameEventLogBusy=false;}
}
public void SelectGameEvent(GameEventLiveRow? row)
{
if(SelectedGameEventDetail?.Id==row?.Id)return;
SelectedGameEventDetail=row is null?null:GameEventDetails.From(row.Record);
}
private bool CanRefreshGameLog()=>!GameEventLogBusy;
[RelayCommand(CanExecute=nameof(CanRefreshGameLog))]
private async Task LatestGameLogAsync(){gameLogPage=0;gameLogPages.Clear();gameLogPages.Add(null);await RefreshGameEventsAsync();}
private bool CanPreviousGameLogPage()=>!GameEventLogBusy&&gameLogPage>0;
private bool CanNextGameLogPage()=>!GameEventLogBusy&&GameEventLogHasMore&&GameEventRows.Count>0;
[RelayCommand(CanExecute=nameof(CanPreviousGameLogPage))]
private async Task PreviousGameLogPageAsync(){gameLogPage--;await RefreshGameEventsAsync();}
[RelayCommand(CanExecute=nameof(CanNextGameLogPage))]
private async Task NextGameLogPageAsync()
{
var cursor=GameEventRows.Last().Record.Cursor;
if(gameLogPages.Count>gameLogPage+1)gameLogPages[gameLogPage+1]=cursor;else gameLogPages.Add(cursor);
gameLogPage++;await RefreshGameEventsAsync();
}
}
public sealed record GameEventTypeOption(string Event,string Display);
public sealed record GameEventDetailField(string Label,string Value);
public sealed record GameEventDetails(long Id,string Name,string OriginalName,string Time,string Source,string Summary,IReadOnlyList<GameEventDetailField> Fields,string Json)
{
public static GameEventDetails From(GameEventEntry entry)
{
using var document=JsonDocument.Parse(entry.Json);
var fields=document.RootElement.EnumerateObject().Where(p=>p.Name is not("timestamp" or "event"))
.Select(p=>new GameEventDetailField(GameEventNames.Field(p.Name),p.Value.ValueKind switch
{
JsonValueKind.Array=>$"{p.Value.GetArrayLength():N0} 项",
JsonValueKind.Object=>$"{p.Value.EnumerateObject().Count():N0} 个字段",
JsonValueKind.True=>"是",JsonValueKind.False=>"否",JsonValueKind.Null=>"—",_=>p.Value.ToString()
})).ToArray();
return new(entry.Id,entry.Name,entry.Event,entry.Time.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss zzz"),
Path.GetFileName(entry.Source),entry.Summary,fields,
JsonSerializer.Serialize(document.RootElement,new JsonSerializerOptions{WriteIndented=true}));
}
}