commit
0971a10ea4
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
@ -47,7 +45,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
[HttpGet]
|
||||
[Route("log")]
|
||||
[Route("log/{id}")]
|
||||
public async Task<ActionResult> Index(string id = null, LogViewFormat format = LogViewFormat.Default, bool renew = false)
|
||||
public async Task<ActionResult> Index(string? id = null, LogViewFormat format = LogViewFormat.Default, bool renew = false)
|
||||
{
|
||||
// fresh page
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
|
@ -89,7 +87,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
public async Task<ActionResult> PostAsync()
|
||||
{
|
||||
// get raw log text
|
||||
string input = this.Request.Form["input"].FirstOrDefault();
|
||||
string? input = this.Request.Form["input"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty."));
|
||||
|
||||
|
@ -111,7 +109,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <param name="expiry">When the uploaded file will no longer be available.</param>
|
||||
/// <param name="uploadWarning">A non-blocking warning while uploading the log.</param>
|
||||
/// <param name="uploadError">An error which occurred while uploading the log.</param>
|
||||
private LogParserModel GetModel(string pasteID, DateTime? expiry = null, string uploadWarning = null, string uploadError = null)
|
||||
private LogParserModel GetModel(string? pasteID, DateTime? expiry = null, string? uploadWarning = null, string? uploadError = null)
|
||||
{
|
||||
Platform? platform = this.DetectClientPlatform();
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using StardewModdingAPI.Web.Framework.LogParsing.Models;
|
||||
|
||||
|
@ -13,7 +12,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>The local time when the next log was posted.</summary>
|
||||
public string Time { get; set; }
|
||||
public string? Time { get; set; }
|
||||
|
||||
/// <summary>The log level for the next log message.</summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
@ -22,7 +21,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
public int ScreenId { get; set; }
|
||||
|
||||
/// <summary>The mod name for the next log message.</summary>
|
||||
public string Mod { get; set; }
|
||||
public string? Mod { get; set; }
|
||||
|
||||
/// <summary>The text for the next log message.</summary>
|
||||
private readonly StringBuilder Text = new();
|
||||
|
@ -32,6 +31,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
** Accessors
|
||||
*********/
|
||||
/// <summary>Whether the next log message has been started.</summary>
|
||||
[MemberNotNullWhen(true, nameof(LogMessageBuilder.Time), nameof(LogMessageBuilder.Mod))]
|
||||
public bool Started { get; private set; }
|
||||
|
||||
|
||||
|
@ -72,19 +72,18 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
}
|
||||
|
||||
/// <summary>Get a log message for the accumulated values.</summary>
|
||||
public LogMessage Build()
|
||||
public LogMessage? Build()
|
||||
{
|
||||
if (!this.Started)
|
||||
return null;
|
||||
|
||||
return new LogMessage
|
||||
{
|
||||
Time = this.Time,
|
||||
Level = this.Level,
|
||||
ScreenId = this.ScreenId,
|
||||
Mod = this.Mod,
|
||||
Text = this.Text.ToString()
|
||||
};
|
||||
return new LogMessage(
|
||||
time: this.Time,
|
||||
level: this.Level,
|
||||
screenId: this.ScreenId,
|
||||
mod: this.Mod,
|
||||
text: this.Text.ToString()
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Reset to start a new log message.</summary>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.LogParsing
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
@ -55,7 +53,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
*********/
|
||||
/// <summary>Parse SMAPI log text.</summary>
|
||||
/// <param name="logText">The SMAPI log text.</param>
|
||||
public ParsedLog Parse(string logText)
|
||||
public ParsedLog Parse(string? logText)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -79,8 +77,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
};
|
||||
|
||||
// parse log messages
|
||||
LogModInfo smapiMod = new() { Name = "SMAPI", Author = "Pathoschild", Description = "", Loaded = true };
|
||||
LogModInfo gameMod = new() { Name = "game", Author = "", Description = "", Loaded = true };
|
||||
LogModInfo smapiMod = new(name: "SMAPI", author: "Pathoschild", version: "", description: "", loaded: true);
|
||||
LogModInfo gameMod = new(name: "game", author: "", version: "", description: "", loaded: true);
|
||||
IDictionary<string, List<LogModInfo>> mods = new Dictionary<string, List<LogModInfo>>();
|
||||
bool inModList = false;
|
||||
bool inContentPackList = false;
|
||||
|
@ -103,7 +101,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
default:
|
||||
if (mods.TryGetValue(message.Mod, out var entries))
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
foreach (LogModInfo entry in entries)
|
||||
entry.Errors++;
|
||||
}
|
||||
break;
|
||||
|
@ -133,9 +131,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
string author = match.Groups["author"].Value;
|
||||
string description = match.Groups["description"].Value;
|
||||
|
||||
if (!mods.TryGetValue(name, out List<LogModInfo> entries))
|
||||
if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
|
||||
mods[name] = entries = new List<LogModInfo>();
|
||||
entries.Add(new LogModInfo { Name = name, Author = author, Version = version, Description = description, Loaded = true });
|
||||
entries.Add(new LogModInfo(name: name, author: author, version: version, description: description, loaded: true));
|
||||
|
||||
message.Section = LogSection.ModsList;
|
||||
}
|
||||
|
@ -156,9 +154,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
string description = match.Groups["description"].Value;
|
||||
string forMod = match.Groups["for"].Value;
|
||||
|
||||
if (!mods.TryGetValue(name, out List<LogModInfo> entries))
|
||||
if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
|
||||
mods[name] = entries = new List<LogModInfo>();
|
||||
entries.Add(new LogModInfo { Name = name, Author = author, Version = version, Description = description, ContentPackFor = forMod, Loaded = true });
|
||||
entries.Add(new LogModInfo(name: name, author: author, version: version, description: description, contentPackFor: forMod, loaded: true));
|
||||
|
||||
message.Section = LogSection.ContentPackList;
|
||||
}
|
||||
|
@ -179,23 +177,19 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
|
||||
if (mods.TryGetValue(name, out var entries))
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
entry.UpdateLink = link;
|
||||
entry.UpdateVersion = version;
|
||||
}
|
||||
foreach (LogModInfo entry in entries)
|
||||
entry.SetUpdate(version, link);
|
||||
}
|
||||
|
||||
message.Section = LogSection.ModUpdateList;
|
||||
}
|
||||
|
||||
else if (message.Level == LogLevel.Alert && this.SmapiUpdatePattern.IsMatch(message.Text))
|
||||
{
|
||||
Match match = this.SmapiUpdatePattern.Match(message.Text);
|
||||
string version = match.Groups["version"].Value;
|
||||
string link = match.Groups["link"].Value;
|
||||
smapiMod.UpdateVersion = version;
|
||||
smapiMod.UpdateLink = link;
|
||||
|
||||
smapiMod.SetUpdate(version, link);
|
||||
}
|
||||
|
||||
// platform info line
|
||||
|
@ -205,7 +199,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
log.ApiVersion = match.Groups["apiVersion"].Value;
|
||||
log.GameVersion = match.Groups["gameVersion"].Value;
|
||||
log.OperatingSystem = match.Groups["os"].Value;
|
||||
smapiMod.Version = log.ApiVersion;
|
||||
smapiMod.OverrideVersion(log.ApiVersion);
|
||||
}
|
||||
|
||||
// mod path line
|
||||
|
@ -215,7 +209,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
log.ModPath = match.Groups["path"].Value;
|
||||
int lastDelimiterPos = log.ModPath.LastIndexOfAny(new[] { '/', '\\' });
|
||||
log.GamePath = lastDelimiterPos >= 0
|
||||
? log.ModPath.Substring(0, lastDelimiterPos)
|
||||
? log.ModPath[..lastDelimiterPos]
|
||||
: log.ModPath;
|
||||
}
|
||||
|
||||
|
@ -229,7 +223,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
}
|
||||
|
||||
// finalize log
|
||||
gameMod.Version = log.GameVersion;
|
||||
if (log.GameVersion != null)
|
||||
gameMod.OverrideVersion(log.GameVersion);
|
||||
log.Mods = new[] { gameMod, smapiMod }.Concat(mods.Values.SelectMany(p => p).OrderBy(p => p.Name)).ToArray();
|
||||
return log;
|
||||
}
|
||||
|
@ -261,7 +256,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
/// <param name="messages">The messages to filter.</param>
|
||||
private IEnumerable<LogMessage> CollapseRepeats(IEnumerable<LogMessage> messages)
|
||||
{
|
||||
LogMessage next = null;
|
||||
LogMessage? next = null;
|
||||
|
||||
foreach (LogMessage message in messages)
|
||||
{
|
||||
// new message
|
||||
|
@ -282,7 +278,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
yield return next;
|
||||
next = message;
|
||||
}
|
||||
yield return next;
|
||||
|
||||
if (next != null)
|
||||
yield return next;
|
||||
}
|
||||
|
||||
/// <summary>Split a SMAPI log into individual log messages.</summary>
|
||||
|
@ -295,7 +293,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
while (true)
|
||||
{
|
||||
// read line
|
||||
string line = reader.ReadLine();
|
||||
string? line = reader.ReadLine();
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
|
@ -308,17 +306,17 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
{
|
||||
if (builder.Started)
|
||||
{
|
||||
yield return builder.Build();
|
||||
yield return builder.Build()!;
|
||||
builder.Clear();
|
||||
}
|
||||
|
||||
var screenGroup = header.Groups["screen"];
|
||||
Group screenGroup = header.Groups["screen"];
|
||||
builder.Start(
|
||||
time: header.Groups["time"].Value,
|
||||
level: Enum.Parse<LogLevel>(header.Groups["level"].Value, ignoreCase: true),
|
||||
screenId: screenGroup.Success ? int.Parse(screenGroup.Value) : 0, // main player is always screen ID 0
|
||||
mod: header.Groups["modName"].Value,
|
||||
text: line.Substring(header.Length)
|
||||
text: line[header.Length..]
|
||||
);
|
||||
}
|
||||
else
|
||||
|
@ -332,7 +330,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
|
||||
// end last message
|
||||
if (builder.Started)
|
||||
yield return builder.Build();
|
||||
yield return builder.Build()!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#nullable disable
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
{
|
||||
|
@ -9,19 +9,19 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
|||
** Accessors
|
||||
*********/
|
||||
/// <summary>The local time when the log was posted.</summary>
|
||||
public string Time { get; set; }
|
||||
public string Time { get; }
|
||||
|
||||
/// <summary>The log level.</summary>
|
||||
public LogLevel Level { get; set; }
|
||||
public LogLevel Level { get; }
|
||||
|
||||
/// <summary>The screen ID in split-screen mode.</summary>
|
||||
public int ScreenId { get; set; }
|
||||
public int ScreenId { get; }
|
||||
|
||||
/// <summary>The mod name.</summary>
|
||||
public string Mod { get; set; }
|
||||
public string Mod { get; }
|
||||
|
||||
/// <summary>The log text.</summary>
|
||||
public string Text { get; set; }
|
||||
public string Text { get; }
|
||||
|
||||
/// <summary>The number of times this message was repeated consecutively.</summary>
|
||||
public int Repeated { get; set; }
|
||||
|
@ -30,6 +30,32 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
|||
public LogSection? Section { get; set; }
|
||||
|
||||
/// <summary>Whether this message is the first one of its section.</summary>
|
||||
[MemberNotNullWhen(true, nameof(LogMessage.Section))]
|
||||
public bool IsStartOfSection { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance/</summary>
|
||||
/// <param name="time">The local time when the log was posted.</param>
|
||||
/// <param name="level">The log level.</param>
|
||||
/// <param name="screenId">The screen ID in split-screen mode.</param>
|
||||
/// <param name="mod">The mod name.</param>
|
||||
/// <param name="text">The log text.</param>
|
||||
/// <param name="repeated">The number of times this message was repeated consecutively.</param>
|
||||
/// <param name="section">The section that this log message belongs to.</param>
|
||||
/// <param name="isStartOfSection">Whether this message is the first one of its section.</param>
|
||||
public LogMessage(string time, LogLevel level, int screenId, string mod, string text, int repeated = 0, LogSection? section = null, bool isStartOfSection = false)
|
||||
{
|
||||
this.Time = time;
|
||||
this.Level = level;
|
||||
this.ScreenId = screenId;
|
||||
this.Mod = mod;
|
||||
this.Text = text;
|
||||
this.Repeated = repeated;
|
||||
this.Section = section;
|
||||
this.IsStartOfSection = isStartOfSection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#nullable disable
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
{
|
||||
|
@ -9,36 +9,81 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
|||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mod name.</summary>
|
||||
public string Name { get; set; }
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>The mod author.</summary>
|
||||
public string Author { get; set; }
|
||||
|
||||
/// <summary>The update version.</summary>
|
||||
public string UpdateVersion { get; set; }
|
||||
|
||||
/// <summary>The update link.</summary>
|
||||
public string UpdateLink { get; set; }
|
||||
public string Author { get; }
|
||||
|
||||
/// <summary>The mod version.</summary>
|
||||
public string Version { get; set; }
|
||||
public string Version { get; private set; }
|
||||
|
||||
/// <summary>The mod description.</summary>
|
||||
public string Description { get; set; }
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>The update version.</summary>
|
||||
public string? UpdateVersion { get; private set; }
|
||||
|
||||
/// <summary>The update link.</summary>
|
||||
public string? UpdateLink { get; private set; }
|
||||
|
||||
/// <summary>The name of the mod for which this is a content pack (if applicable).</summary>
|
||||
public string ContentPackFor { get; set; }
|
||||
public string? ContentPackFor { get; }
|
||||
|
||||
/// <summary>The number of errors logged by this mod.</summary>
|
||||
public int Errors { get; set; }
|
||||
|
||||
/// <summary>Whether the mod was loaded into the game.</summary>
|
||||
public bool Loaded { get; set; }
|
||||
public bool Loaded { get; }
|
||||
|
||||
/// <summary>Whether the mod has an update available.</summary>
|
||||
[MemberNotNullWhen(true, nameof(LogModInfo.UpdateVersion), nameof(LogModInfo.UpdateLink))]
|
||||
public bool HasUpdate => this.UpdateVersion != null && this.Version != this.UpdateVersion;
|
||||
|
||||
/// <summary>Whether the mod is a content pack for another mod.</summary>
|
||||
[MemberNotNullWhen(true, nameof(LogModInfo.ContentPackFor))]
|
||||
public bool IsContentPack => !string.IsNullOrWhiteSpace(this.ContentPackFor);
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="name">The mod name.</param>
|
||||
/// <param name="author">The mod author.</param>
|
||||
/// <param name="version">The mod version.</param>
|
||||
/// <param name="description">The mod description.</param>
|
||||
/// <param name="updateVersion">The update version.</param>
|
||||
/// <param name="updateLink">The update link.</param>
|
||||
/// <param name="contentPackFor">The name of the mod for which this is a content pack (if applicable).</param>
|
||||
/// <param name="errors">The number of errors logged by this mod.</param>
|
||||
/// <param name="loaded">Whether the mod was loaded into the game.</param>
|
||||
public LogModInfo(string name, string author, string version, string description, string? updateVersion = null, string? updateLink = null, string? contentPackFor = null, int errors = 0, bool loaded = true)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Author = author;
|
||||
this.Version = version;
|
||||
this.Description = description;
|
||||
this.UpdateVersion = updateVersion;
|
||||
this.UpdateLink = updateLink;
|
||||
this.ContentPackFor = contentPackFor;
|
||||
this.Errors = errors;
|
||||
this.Loaded = loaded;
|
||||
}
|
||||
|
||||
/// <summary>Add an update alert for this mod.</summary>
|
||||
/// <param name="updateVersion">The update version.</param>
|
||||
/// <param name="updateLink">The update link.</param>
|
||||
public void SetUpdate(string updateVersion, string updateLink)
|
||||
{
|
||||
this.UpdateVersion = updateVersion;
|
||||
this.UpdateLink = updateLink;
|
||||
}
|
||||
|
||||
/// <summary>Override the version number, for cases like SMAPI itself where the version is only known later during parsing.</summary>
|
||||
/// <param name="version">The new mod version.</param>
|
||||
public void OverrideVersion(string version)
|
||||
{
|
||||
this.Version = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
{
|
||||
|
@ -14,31 +13,32 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
|||
** Metadata
|
||||
****/
|
||||
/// <summary>Whether the log file was successfully parsed.</summary>
|
||||
[MemberNotNullWhen(true, nameof(ParsedLog.RawText))]
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>An error message indicating why the log file is invalid.</summary>
|
||||
public string Error { get; set; }
|
||||
public string? Error { get; set; }
|
||||
|
||||
/// <summary>The raw log text.</summary>
|
||||
public string RawText { get; set; }
|
||||
public string? RawText { get; set; }
|
||||
|
||||
/****
|
||||
** Log data
|
||||
****/
|
||||
/// <summary>The SMAPI version.</summary>
|
||||
public string ApiVersion { get; set; }
|
||||
public string? ApiVersion { get; set; }
|
||||
|
||||
/// <summary>The game version.</summary>
|
||||
public string GameVersion { get; set; }
|
||||
public string? GameVersion { get; set; }
|
||||
|
||||
/// <summary>The player's operating system.</summary>
|
||||
public string OperatingSystem { get; set; }
|
||||
public string? OperatingSystem { get; set; }
|
||||
|
||||
/// <summary>The game install path.</summary>
|
||||
public string GamePath { get; set; }
|
||||
public string? GamePath { get; set; }
|
||||
|
||||
/// <summary>The mod folder path.</summary>
|
||||
public string ModPath { get; set; }
|
||||
public string? ModPath { get; set; }
|
||||
|
||||
/// <summary>The ISO 8601 timestamp when the log was started.</summary>
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
@ -47,6 +47,6 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
|||
public LogModInfo[] Mods { get; set; } = Array.Empty<LogModInfo>();
|
||||
|
||||
/// <summary>The log messages.</summary>
|
||||
public LogMessage[] Messages { get; set; }
|
||||
public LogMessage[] Messages { get; set; } = Array.Empty<LogMessage>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
using StardewModdingAPI.Web.Framework.LogParsing.Models;
|
||||
|
||||
|
@ -23,40 +23,41 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
** Accessors
|
||||
*********/
|
||||
/// <summary>The paste ID.</summary>
|
||||
public string PasteID { get; set; }
|
||||
public string? PasteID { get; }
|
||||
|
||||
/// <summary>The viewer's detected OS, if known.</summary>
|
||||
public Platform? DetectedPlatform { get; set; }
|
||||
public Platform? DetectedPlatform { get; }
|
||||
|
||||
/// <summary>The parsed log info.</summary>
|
||||
public ParsedLog ParsedLog { get; set; }
|
||||
public ParsedLog? ParsedLog { get; private set; }
|
||||
|
||||
/// <summary>Whether to show the raw unparsed log.</summary>
|
||||
public bool ShowRaw { get; set; }
|
||||
public bool ShowRaw { get; private set; }
|
||||
|
||||
/// <summary>A non-blocking warning while uploading the log.</summary>
|
||||
public string UploadWarning { get; set; }
|
||||
public string? UploadWarning { get; set; }
|
||||
|
||||
/// <summary>An error which occurred while uploading the log.</summary>
|
||||
public string UploadError { get; set; }
|
||||
public string? UploadError { get; set; }
|
||||
|
||||
/// <summary>An error which occurred while parsing the log file.</summary>
|
||||
public string ParseError => this.ParsedLog?.Error;
|
||||
public string? ParseError => this.ParsedLog?.Error;
|
||||
|
||||
/// <summary>When the uploaded file will no longer be available.</summary>
|
||||
public DateTime? Expiry { get; set; }
|
||||
|
||||
/// <summary>Whether parsed log data is available.</summary>
|
||||
[MemberNotNullWhen(true, nameof(LogParserModel.PasteID), nameof(LogParserModel.ParsedLog))]
|
||||
public bool HasLog => this.ParsedLog != null;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public LogParserModel() { }
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="pasteID">The paste ID.</param>
|
||||
/// <param name="platform">The viewer's detected OS, if known.</param>
|
||||
public LogParserModel(string pasteID, Platform? platform)
|
||||
public LogParserModel(string? pasteID, Platform? platform)
|
||||
{
|
||||
this.PasteID = pasteID;
|
||||
this.DetectedPlatform = platform;
|
||||
|
@ -64,6 +65,26 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
this.ShowRaw = false;
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="pasteId">The paste ID.</param>
|
||||
/// <param name="detectedPlatform">The viewer's detected OS, if known.</param>
|
||||
/// <param name="parsedLog">The parsed log info.</param>
|
||||
/// <param name="showRaw">Whether to show the raw unparsed log.</param>
|
||||
/// <param name="uploadWarning">A non-blocking warning while uploading the log.</param>
|
||||
/// <param name="uploadError">An error which occurred while uploading the log.</param>
|
||||
/// <param name="expiry">When the uploaded file will no longer be available.</param>
|
||||
[JsonConstructor]
|
||||
public LogParserModel(string pasteId, Platform? detectedPlatform, ParsedLog? parsedLog, bool showRaw, string? uploadWarning, string? uploadError, DateTime? expiry)
|
||||
{
|
||||
this.PasteID = pasteId;
|
||||
this.DetectedPlatform = detectedPlatform;
|
||||
this.ParsedLog = parsedLog;
|
||||
this.ShowRaw = showRaw;
|
||||
this.UploadWarning = uploadWarning;
|
||||
this.UploadError = uploadError;
|
||||
this.Expiry = expiry;
|
||||
}
|
||||
|
||||
/// <summary>Set the log parser result.</summary>
|
||||
/// <param name="parsedLog">The parsed log info.</param>
|
||||
/// <param name="showRaw">Whether to show the raw unparsed log.</param>
|
||||
|
@ -79,14 +100,14 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
public IDictionary<string, LogModInfo[]> GetContentPacksByMod()
|
||||
{
|
||||
// get all mods & content packs
|
||||
LogModInfo[] mods = this.ParsedLog?.Mods;
|
||||
LogModInfo[]? mods = this.ParsedLog?.Mods;
|
||||
if (mods == null || !mods.Any())
|
||||
return new Dictionary<string, LogModInfo[]>();
|
||||
|
||||
// group by mod
|
||||
return mods
|
||||
.Where(mod => mod.IsContentPack)
|
||||
.GroupBy(mod => mod.ContentPackFor)
|
||||
.GroupBy(mod => mod.ContentPackFor!)
|
||||
.ToDictionary(group => group.Key, group => group.ToArray());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
@{
|
||||
#nullable disable
|
||||
}
|
||||
|
||||
@using Humanizer
|
||||
@using StardewModdingAPI.Toolkit.Utilities
|
||||
@using StardewModdingAPI.Web.Framework
|
||||
|
@ -12,17 +8,24 @@
|
|||
@{
|
||||
ViewData["Title"] = "SMAPI log parser";
|
||||
|
||||
ParsedLog log = Model!.ParsedLog;
|
||||
ParsedLog? log = Model!.ParsedLog;
|
||||
|
||||
IDictionary<string, LogModInfo[]> contentPacks = Model.GetContentPacksByMod();
|
||||
IDictionary<string, bool> defaultFilters = Enum
|
||||
.GetValues(typeof(LogLevel))
|
||||
.Cast<LogLevel>()
|
||||
.GetValues<LogLevel>()
|
||||
.ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace);
|
||||
|
||||
IDictionary<int, string> logLevels = Enum
|
||||
.GetValues<LogLevel>()
|
||||
.ToDictionary(level => (int)level, level => level.ToString().ToLower());
|
||||
|
||||
IDictionary<int, string> logSections = Enum
|
||||
.GetValues<LogSection>()
|
||||
.ToDictionary(section => (int)section, section => section.ToString());
|
||||
|
||||
string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true);
|
||||
|
||||
ISet<int> screenIds = new HashSet<int>(log?.Messages?.Select(p => p.ScreenId) ?? Array.Empty<int>());
|
||||
ISet<int> screenIds = new HashSet<int>(log?.Messages.Select(p => p.ScreenId) ?? Array.Empty<int>());
|
||||
}
|
||||
|
||||
@section Head {
|
||||
|
@ -35,23 +38,53 @@
|
|||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3/dist/css/tabby-ui-vertical.min.css" />
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
|
||||
<script src="~/Content/js/file-upload.js"></script>
|
||||
<script src="~/Content/js/log-parser.js"></script>
|
||||
|
||||
<script id="serializedData" type="application/json">
|
||||
@if (!Model.ShowRaw)
|
||||
{
|
||||
<text>
|
||||
{
|
||||
"messages": @this.ForJson(log?.Messages),
|
||||
"sections": @this.ForJson(logSections),
|
||||
"logLevels": @this.ForJson(logLevels),
|
||||
"modSlugs": @this.ForJson(log?.Mods.DistinctBy(p => p.Name).Select(p => new {p.Name, Slug = Model.GetSlug(p.Name)}).Where(p => p.Name != p.Slug).ToDictionary(p => p.Name, p => p.Slug)),
|
||||
"screenIds": @this.ForJson(screenIds)
|
||||
}
|
||||
</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>
|
||||
{
|
||||
"messages": [],
|
||||
"sections": {},
|
||||
"logLevels": {},
|
||||
"modSlugs": {},
|
||||
"screenIds": []
|
||||
}
|
||||
</text>
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
smapi.logParser({
|
||||
logStarted: new Date(@this.ForJson(log?.Timestamp)),
|
||||
showPopup: @this.ForJson(log == null),
|
||||
showMods: @this.ForJson(log?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, _ => true)),
|
||||
showSections: @this.ForJson(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, _ => false)),
|
||||
showLevels: @this.ForJson(defaultFilters),
|
||||
enableFilters: @this.ForJson(!Model.ShowRaw),
|
||||
screenIds: @this.ForJson(screenIds)
|
||||
}, '@this.Url.PlainAction("Index", "LogParser", values: null)');
|
||||
smapi.logParser(
|
||||
{
|
||||
logStarted: new Date(@this.ForJson(log?.Timestamp)),
|
||||
dataElement: "script#serializedData",
|
||||
showPopup: @this.ForJson(log == null),
|
||||
showMods: @this.ForJson(log?.Mods.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, _ => true)),
|
||||
showSections: @this.ForJson(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, _ => false)),
|
||||
showLevels: @this.ForJson(defaultFilters),
|
||||
enableFilters: @this.ForJson(!Model.ShowRaw)
|
||||
}
|
||||
);
|
||||
|
||||
new Tabby('[data-tabs]');
|
||||
new Tabby("[data-tabs]");
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@ -192,12 +225,12 @@ else if (log?.IsValid == true)
|
|||
Consider updating these mods to fix problems:
|
||||
|
||||
<table id="updates" class="table">
|
||||
@foreach (LogModInfo mod in log.Mods.Where(mod => (mod.HasUpdate && !mod.IsContentPack) || (contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList) && contentPackList.Any(pack => pack.HasUpdate))))
|
||||
@foreach (LogModInfo mod in log.Mods.Where(mod => (mod.HasUpdate && !mod.IsContentPack) || (contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) && contentPackList.Any(pack => pack.HasUpdate))))
|
||||
{
|
||||
<tr class="mod-entry">
|
||||
<td>
|
||||
<strong class=@(!mod.HasUpdate ? "hidden" : "")>@mod.Name</strong>
|
||||
@if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList))
|
||||
@if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList))
|
||||
{
|
||||
<div class="content-packs">
|
||||
@foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate))
|
||||
|
@ -275,29 +308,34 @@ else if (log?.IsValid == true)
|
|||
<span class="notice txt"><i>click any mod to filter</i></span>
|
||||
<span class="notice btn txt" v-on:click="showAllMods" v-bind:class="{ invisible: !anyModsHidden }">show all</span>
|
||||
<span class="notice btn txt" v-on:click="hideAllMods" v-bind:class="{ invisible: !anyModsShown || !anyModsHidden }">hide all</span>
|
||||
<span class="notice btn txt" v-on:click="toggleContentPacks">toggle content packs in list</span>
|
||||
}
|
||||
</caption>
|
||||
@foreach (var mod in log.Mods.Where(p => p.Loaded && !p.IsContentPack))
|
||||
{
|
||||
if (contentPacks == null || !contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList))
|
||||
contentPackList = null;
|
||||
|
||||
<tr v-on:click="toggleMod('@Model.GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@Model.GetSlug(mod.Name)'] }">
|
||||
<td><input type="checkbox" v-bind:checked="showMods['@Model.GetSlug(mod.Name)']" v-bind:class="{ invisible: !anyModsHidden }" /></td>
|
||||
<td v-pre>
|
||||
<strong>@mod.Name</strong> @mod.Version
|
||||
@if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList))
|
||||
<td>
|
||||
<strong v-pre>@mod.Name</strong> @mod.Version
|
||||
@if (contentPackList != null)
|
||||
{
|
||||
<div class="content-packs">
|
||||
<div v-if="!hideContentPacks" class="content-packs">
|
||||
@foreach (var contentPack in contentPackList)
|
||||
{
|
||||
<text>+ @contentPack.Name @contentPack.Version</text><br />
|
||||
}
|
||||
</div>
|
||||
<span v-else class="content-packs-collapsed"> (+ @contentPackList.Length content packs)</span>
|
||||
}
|
||||
</td>
|
||||
<td v-pre>
|
||||
<td>
|
||||
@mod.Author
|
||||
@if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out contentPackList))
|
||||
@if (contentPackList != null)
|
||||
{
|
||||
<div class="content-packs">
|
||||
<div v-if="!hideContentPacks" class="content-packs">
|
||||
@foreach (var contentPack in contentPackList)
|
||||
{
|
||||
<text>+ @contentPack.Author</text><br />
|
||||
|
@ -323,57 +361,64 @@ else if (log?.IsValid == true)
|
|||
|
||||
@if (!Model.ShowRaw)
|
||||
{
|
||||
<div id="filterHolder"></div>
|
||||
<div id="filters">
|
||||
Filter messages:
|
||||
<span v-bind:class="{ active: showLevels['trace'] }" v-on:click="toggleLevel('trace')">TRACE</span> |
|
||||
<span v-bind:class="{ active: showLevels['debug'] }" v-on:click="toggleLevel('debug')">DEBUG</span> |
|
||||
<span v-bind:class="{ active: showLevels['info'] }" v-on:click="toggleLevel('info')">INFO</span> |
|
||||
<span v-bind:class="{ active: showLevels['alert'] }" v-on:click="toggleLevel('alert')">ALERT</span> |
|
||||
<span v-bind:class="{ active: showLevels['warn'] }" v-on:click="toggleLevel('warn')">WARN</span> |
|
||||
<span v-bind:class="{ active: showLevels['error'] }" v-on:click="toggleLevel('error')">ERROR</span>
|
||||
<div class="toggles">
|
||||
<div>
|
||||
Filter messages:
|
||||
</div>
|
||||
<div>
|
||||
<span role="button" v-bind:class="{ active: showLevels['trace'] }" v-on:click="toggleLevel('trace')">TRACE</span> |
|
||||
<span role="button" v-bind:class="{ active: showLevels['debug'] }" v-on:click="toggleLevel('debug')">DEBUG</span> |
|
||||
<span role="button" v-bind:class="{ active: showLevels['info'] }" v-on:click="toggleLevel('info')">INFO</span> |
|
||||
<span role="button" v-bind:class="{ active: showLevels['alert'] }" v-on:click="toggleLevel('alert')">ALERT</span> |
|
||||
<span role="button" v-bind:class="{ active: showLevels['warn'] }" v-on:click="toggleLevel('warn')">WARN</span> |
|
||||
<span role="button" v-bind:class="{ active: showLevels['error'] }" v-on:click="toggleLevel('error')">ERROR</span>
|
||||
<div class="filter-text">
|
||||
<input
|
||||
type="text"
|
||||
v-bind:class="{ active: !!filterText }"
|
||||
v-on:input="updateFilterText"
|
||||
placeholder="search to filter log..."
|
||||
/>
|
||||
<span role="button" v-bind:class="{ active: filterUseRegex }" v-on:click="toggleFilterUseRegex" title="Use regular expression syntax.">.*</span>
|
||||
<span role="button" v-bind:class="{ active: !filterInsensitive }" v-on:click="toggleFilterInsensitive" title="Match exact capitalization only.">aA</span>
|
||||
<span role="button" v-bind:class="{ active: filterUseWord, 'whole-word': true }" v-on:click="toggleFilterWord" title="Match whole word only."><i>“ ”</i></span>
|
||||
<span role="button" v-bind:class="{ active: shouldHighlight }" v-on:click="toggleHighlight" title="Highlight matches in the log text.">HL</span>
|
||||
</div>
|
||||
<filter-stats
|
||||
v-bind:start="start"
|
||||
v-bind:end="end"
|
||||
v-bind:pages="totalPages"
|
||||
v-bind:filtered="filteredMessages.length"
|
||||
v-bind:total="totalMessages"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<pager
|
||||
v-bind:page="page"
|
||||
v-bind:pages="totalPages"
|
||||
v-bind:prevPage="prevPage"
|
||||
v-bind:nextPage="nextPage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table id="log">
|
||||
@foreach (var message in log.Messages)
|
||||
{
|
||||
string levelStr = message.Level.ToString().ToLower();
|
||||
string sectionStartClass = message.IsStartOfSection ? "section-start" : null;
|
||||
string sectionFilter = message.Section != null && !message.IsStartOfSection ? $"&& sectionsAllow('{message.Section}')" : null; // filter the message by section if applicable
|
||||
<noscript>
|
||||
<div>
|
||||
This website uses JavaScript to display a filterable table. To view this log, please enable JavaScript or <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, format = LogViewFormat.RawView })">view the raw log</a>.
|
||||
</div>
|
||||
<br/>
|
||||
</noscript>
|
||||
|
||||
<tr class="mod @levelStr @sectionStartClass"
|
||||
@if (message.IsStartOfSection) { <text> v-on:click="toggleSection('@message.Section')" </text> }
|
||||
v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr') @sectionFilter">
|
||||
<td v-pre>@message.Time</td>
|
||||
@if (screenIds.Count > 1)
|
||||
{
|
||||
<td v-pre>screen_@message.ScreenId</td>
|
||||
}
|
||||
<td v-pre>@message.Level.ToString().ToUpper()</td>
|
||||
<td v-pre data-title="@message.Mod">@message.Mod</td>
|
||||
<td>
|
||||
<span v-pre class="log-message-text">@message.Text</span>
|
||||
@if (message.IsStartOfSection)
|
||||
{
|
||||
<span class="section-toggle-message">
|
||||
<template v-if="sectionsAllow('@message.Section')">
|
||||
This section is shown. Click here to hide it.
|
||||
</template>
|
||||
<template v-else>
|
||||
This section is hidden. Click here to show it.
|
||||
</template>
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
if (message.Repeated > 0)
|
||||
{
|
||||
<tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr') @sectionFilter">
|
||||
<td colspan="4"></td>
|
||||
<td v-pre><i>repeats [@message.Repeated] times.</i></td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
<log-table>
|
||||
<log-line
|
||||
v-for="msg in visibleMessages"
|
||||
v-bind:key="msg.id"
|
||||
v-bind:showScreenId="showScreenId"
|
||||
v-bind:message="msg"
|
||||
v-bind:highlight="shouldHighlight"
|
||||
/>
|
||||
</log-table>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -113,7 +113,7 @@ table caption {
|
|||
}
|
||||
|
||||
.table tr {
|
||||
background: #eee
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
#mods span.notice {
|
||||
|
@ -148,18 +148,74 @@ table caption {
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
.table .content-packs-collapsed {
|
||||
opacity: 0.75;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#metadata td:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.table tr:nth-child(even) {
|
||||
background: #fff
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#filters {
|
||||
margin: 1em 0 0 0;
|
||||
padding: 0;
|
||||
padding: 0 0 0.5em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: calc(100vw - 16em);
|
||||
}
|
||||
|
||||
#filters > div {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
#filters .toggles {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#filters .toggles > div:first-child {
|
||||
font-weight: bold;
|
||||
padding: 0.2em 1em 0 0;
|
||||
}
|
||||
|
||||
#filters .filter-text {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
#filters .stats {
|
||||
margin-top: 0.5em;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
#filters.sticky {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0em;
|
||||
background: #fff;
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
|
||||
@media (min-width: 1020px) and (max-width: 1199px) {
|
||||
#filters:not(.sticky) {
|
||||
width: calc(100vw - 13em);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1019px) {
|
||||
#filters:not(.sticky) {
|
||||
width: calc(100vw - 3em);
|
||||
}
|
||||
|
||||
#filters {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#filters span {
|
||||
|
@ -173,6 +229,17 @@ table caption {
|
|||
color: #000;
|
||||
border-color: #880000;
|
||||
background-color: #fcc;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#filters .filter-text span {
|
||||
padding: 3px 0.5em;
|
||||
}
|
||||
|
||||
#filters .whole-word i {
|
||||
padding: 0 1px;
|
||||
border: 1px dashed;
|
||||
}
|
||||
|
||||
#filters span:hover {
|
||||
|
@ -188,11 +255,48 @@ table caption {
|
|||
background: #efe;
|
||||
}
|
||||
|
||||
#filters .pager {
|
||||
margin-top: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#filters .pager div {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
#filters .pager div span {
|
||||
padding: 0 0.5em;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
#filters .pager span {
|
||||
background-color: #eee;
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
#filters .pager span.active {
|
||||
font-weight: bold;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
#filters .pager span.disabled {
|
||||
opacity: 0.3;
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
#filters .pager span:not(.disabled):hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Log
|
||||
*********/
|
||||
#log .mod-repeat {
|
||||
font-size: 0.85em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#log .trace {
|
||||
|
@ -237,6 +341,11 @@ table caption {
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#log .log-message-text strong {
|
||||
background-color: yellow;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#log {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
|
|
@ -1,20 +1,160 @@
|
|||
/* globals $ */
|
||||
/* globals $, Vue */
|
||||
|
||||
/**
|
||||
* The global SMAPI module.
|
||||
*/
|
||||
var smapi = smapi || {};
|
||||
|
||||
/**
|
||||
* The Vue app for the current page.
|
||||
* @type {Vue}
|
||||
*/
|
||||
var app;
|
||||
smapi.logParser = function (data, sectionUrl) {
|
||||
|
||||
// Use a scroll event to apply a sticky effect to the filters / pagination
|
||||
// bar. We can't just use "position: sticky" due to how the page is structured
|
||||
// but this works well enough.
|
||||
$(function () {
|
||||
let sticking = false;
|
||||
|
||||
document.addEventListener("scroll", function () {
|
||||
const filters = document.getElementById("filters");
|
||||
const holder = document.getElementById("filterHolder");
|
||||
if (!filters || !holder)
|
||||
return;
|
||||
|
||||
const offset = holder.offsetTop;
|
||||
const shouldStick = window.pageYOffset > offset;
|
||||
if (shouldStick === sticking)
|
||||
return;
|
||||
|
||||
sticking = shouldStick;
|
||||
if (sticking) {
|
||||
holder.style.marginBottom = `calc(1em + ${filters.offsetHeight}px)`;
|
||||
filters.classList.add("sticky");
|
||||
}
|
||||
else {
|
||||
filters.classList.remove("sticky");
|
||||
holder.style.marginBottom = "";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize a log parser view on the current page.
|
||||
* @param {object} state The state options to use.
|
||||
* @returns {void}
|
||||
*/
|
||||
smapi.logParser = function (state) {
|
||||
if (!state)
|
||||
state = {};
|
||||
|
||||
// internal helpers
|
||||
const helpers = {
|
||||
/**
|
||||
* Get a handler which invokes the callback after a set delay, resetting the delay each time it's called.
|
||||
* @param {(...*) => void} action The callback to invoke when the delay ends.
|
||||
* @param {number} delay The number of milliseconds to delay the action after each call.
|
||||
* @returns {() => void}
|
||||
*/
|
||||
getDebouncedHandler(action, delay) {
|
||||
let timeoutId = null;
|
||||
|
||||
return function () {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const args = arguments;
|
||||
const self = this;
|
||||
|
||||
timeoutId = setTimeout(
|
||||
function () {
|
||||
action.apply(self, args);
|
||||
},
|
||||
delay
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Escape regex special characters in the given string.
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
escapeRegex(text) {
|
||||
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
},
|
||||
|
||||
/**
|
||||
* Format a number for the user's locale.
|
||||
* @param {number} value The number to format.
|
||||
* @returns {string}
|
||||
*/
|
||||
formatNumber(value) {
|
||||
const formatter = window.Intl && Intl.NumberFormat && new Intl.NumberFormat();
|
||||
return formatter && formatter.format
|
||||
? formatter.format(value)
|
||||
: `${value}`;
|
||||
}
|
||||
};
|
||||
|
||||
// internal event handlers
|
||||
const handlers = {
|
||||
/**
|
||||
* Method called when the user clicks a log line to toggle the visibility of a section. Binding methods is problematic with functional components so we just use the `data-section` parameter and our global reference to the app.
|
||||
* @param {any} event
|
||||
* @returns {false}
|
||||
*/
|
||||
clickLogLine(event) {
|
||||
app.toggleSection(event.currentTarget.dataset.section);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigate to the previous page of messages in the log.
|
||||
* @returns {void}
|
||||
*/
|
||||
prevPage() {
|
||||
app.prevPage();
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigate to the next page of messages in the log.
|
||||
* @returns {void}
|
||||
*/
|
||||
nextPage() {
|
||||
app.nextPage();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a click on a page number element.
|
||||
* @param {number | Event} event
|
||||
* @returns {void}
|
||||
*/
|
||||
changePage(event) {
|
||||
if (typeof event === "number")
|
||||
app.changePage(event);
|
||||
else if (event) {
|
||||
const page = parseInt(event.currentTarget.dataset.page);
|
||||
if (!isNaN(page) && isFinite(page))
|
||||
app.changePage(page);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// internal filter counts
|
||||
var stats = data.stats = {
|
||||
const stats = state.stats = {
|
||||
modsShown: 0,
|
||||
modsHidden: 0
|
||||
};
|
||||
|
||||
function updateModFilters() {
|
||||
// counts
|
||||
stats.modsShown = 0;
|
||||
stats.modsHidden = 0;
|
||||
for (var key in data.showMods) {
|
||||
if (data.showMods.hasOwnProperty(key)) {
|
||||
if (data.showMods[key])
|
||||
for (let key in state.showMods) {
|
||||
if (state.showMods.hasOwnProperty(key)) {
|
||||
if (state.showMods[key])
|
||||
stats.modsShown++;
|
||||
else
|
||||
stats.modsHidden++;
|
||||
|
@ -22,35 +162,607 @@ smapi.logParser = function (data, sectionUrl) {
|
|||
}
|
||||
}
|
||||
|
||||
// load raw log data
|
||||
{
|
||||
const dataElement = document.querySelector(state.dataElement);
|
||||
state.data = JSON.parse(dataElement.textContent.trim());
|
||||
dataElement.remove(); // let browser unload the data element since we won't need it anymore
|
||||
}
|
||||
|
||||
// preprocess data for display
|
||||
state.messages = state.data.messages || [];
|
||||
if (state.messages.length) {
|
||||
const levels = state.data.logLevels;
|
||||
const sections = state.data.sections;
|
||||
const modSlugs = state.data.modSlugs;
|
||||
|
||||
for (let i = 0, length = state.messages.length; i < length; i++) {
|
||||
const message = state.messages[i];
|
||||
|
||||
// add unique ID
|
||||
message.id = i;
|
||||
|
||||
// add display values
|
||||
message.LevelName = levels[message.Level];
|
||||
message.SectionName = sections[message.Section];
|
||||
message.ModSlug = modSlugs[message.Mod] || message.Mod;
|
||||
|
||||
// For repeated messages, since our <log-line /> component
|
||||
// can't return two rows, just insert a second message
|
||||
// which will display as the message repeated notice.
|
||||
if (message.Repeated > 0 && !message.isRepeated) {
|
||||
const repeatNote = {
|
||||
id: i + 1,
|
||||
Level: message.Level,
|
||||
Section: message.Section,
|
||||
Mod: message.Mod,
|
||||
Repeated: message.Repeated,
|
||||
isRepeated: true
|
||||
};
|
||||
|
||||
state.messages.splice(i + 1, 0, repeatNote);
|
||||
length++;
|
||||
}
|
||||
|
||||
// let Vue know the message won't change, so it doesn't need to monitor it
|
||||
Object.freeze(message);
|
||||
}
|
||||
}
|
||||
Object.freeze(state.messages);
|
||||
|
||||
// set local time started
|
||||
if (data)
|
||||
data.localTimeStarted = ("0" + data.logStarted.getHours()).slice(-2) + ":" + ("0" + data.logStarted.getMinutes()).slice(-2);
|
||||
if (state.logStarted)
|
||||
state.localTimeStarted = ("0" + state.logStarted.getHours()).slice(-2) + ":" + ("0" + state.logStarted.getMinutes()).slice(-2);
|
||||
|
||||
// add the properties we're passing to Vue
|
||||
state.totalMessages = state.messages.length;
|
||||
state.filterText = "";
|
||||
state.filterRegex = "";
|
||||
state.showContentPacks = true;
|
||||
state.useHighlight = true;
|
||||
state.useRegex = false;
|
||||
state.useInsensitive = true;
|
||||
state.useWord = false;
|
||||
state.perPage = 1000;
|
||||
state.page = 1;
|
||||
|
||||
// load saved values, if any
|
||||
if (localStorage.settings) {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.settings);
|
||||
|
||||
state.showContentPacks = saved.showContentPacks ?? state.showContentPacks;
|
||||
state.useHighlight = saved.useHighlight ?? state.useHighlight;
|
||||
state.useRegex = saved.useRegex ?? state.useRegex;
|
||||
state.useInsensitive = saved.useInsensitive ?? state.useInsensitive;
|
||||
state.useWord = saved.useWord ?? state.useWord;
|
||||
}
|
||||
catch (error) {
|
||||
// ignore settings if invalid
|
||||
}
|
||||
}
|
||||
|
||||
// add a number formatter so our numbers look nicer
|
||||
Vue.filter("number", handlers.formatNumber);
|
||||
|
||||
// Strictly speaking, we don't need this. However, due to the way our
|
||||
// Vue template is living in-page the browser is "helpful" and moves
|
||||
// our <log-line />s outside of a basic <table> since obviously they
|
||||
// aren't table rows and don't belong inside a table. By using another
|
||||
// Vue component, we avoid that.
|
||||
Vue.component("log-table", {
|
||||
functional: true,
|
||||
render: function (createElement, context) {
|
||||
return createElement(
|
||||
"table",
|
||||
{
|
||||
attrs: {
|
||||
id: "log"
|
||||
}
|
||||
},
|
||||
context.children
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// The <filter-stats /> component draws a nice message under the filters
|
||||
// telling a user how many messages match their filters, and also expands
|
||||
// on how many of them they're seeing because of pagination.
|
||||
Vue.component("filter-stats", {
|
||||
functional: true,
|
||||
render: function (createElement, context) {
|
||||
const props = context.props;
|
||||
if (props.pages > 1) {
|
||||
return createElement(
|
||||
"div",
|
||||
{ class: "stats" },
|
||||
[
|
||||
"showing ",
|
||||
createElement("strong", helpers.formatNumber(props.start + 1)),
|
||||
" to ",
|
||||
createElement("strong", helpers.formatNumber(props.end)),
|
||||
" of ",
|
||||
createElement("strong", helpers.formatNumber(props.filtered)),
|
||||
" (total: ",
|
||||
createElement("strong", helpers.formatNumber(props.total)),
|
||||
")"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return createElement(
|
||||
"div",
|
||||
{ class: "stats" },
|
||||
[
|
||||
"showing ",
|
||||
createElement("strong", helpers.formatNumber(props.filtered)),
|
||||
" out of ",
|
||||
createElement("strong", helpers.formatNumber(props.total))
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Next up we have <pager /> which renders the pagination list. This has a
|
||||
// helper method to make building the list of links easier.
|
||||
function addPageLink(page, links, visited, createElement, currentPage) {
|
||||
if (visited.has(page))
|
||||
return;
|
||||
|
||||
if (page > 1 && !visited.has(page - 1))
|
||||
links.push(" … ");
|
||||
|
||||
visited.add(page);
|
||||
links.push(createElement(
|
||||
"span",
|
||||
{
|
||||
class: page === currentPage ? "active" : null,
|
||||
attrs: {
|
||||
"data-page": page
|
||||
},
|
||||
on: {
|
||||
click: handlers.changePage
|
||||
}
|
||||
},
|
||||
helpers.formatNumber(page)
|
||||
));
|
||||
}
|
||||
|
||||
Vue.component("pager", {
|
||||
functional: true,
|
||||
render: function (createElement, context) {
|
||||
const props = context.props;
|
||||
if (props.pages <= 1)
|
||||
return null;
|
||||
|
||||
const visited = new Set();
|
||||
const pageLinks = [];
|
||||
|
||||
for (let i = 1; i <= 2; i++)
|
||||
addPageLink(i, pageLinks, visited, createElement, props.page);
|
||||
|
||||
for (let i = props.page - 2; i <= props.page + 2; i++) {
|
||||
if (i >= 1 && i <= props.pages)
|
||||
addPageLink(i, pageLinks, visited, createElement, props.page);
|
||||
}
|
||||
|
||||
for (let i = props.pages - 2; i <= props.pages; i++) {
|
||||
if (i >= 1)
|
||||
addPageLink(i, pageLinks, visited, createElement, props.page);
|
||||
}
|
||||
|
||||
return createElement(
|
||||
"div",
|
||||
{ class: "pager" },
|
||||
[
|
||||
createElement(
|
||||
"span",
|
||||
{
|
||||
class: props.page <= 1 ? "disabled" : null,
|
||||
on: {
|
||||
click: handlers.prevPage
|
||||
}
|
||||
},
|
||||
"Prev"
|
||||
),
|
||||
" ",
|
||||
"Page ",
|
||||
helpers.formatNumber(props.page),
|
||||
" of ",
|
||||
helpers.formatNumber(props.pages),
|
||||
" ",
|
||||
createElement(
|
||||
"span",
|
||||
{
|
||||
class: props.page >= props.pages ? "disabled" : null,
|
||||
on: {
|
||||
click: handlers.nextPage
|
||||
}
|
||||
},
|
||||
"Next"
|
||||
),
|
||||
createElement("div", {}, pageLinks)
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Our <log-line /> functional component draws each log line.
|
||||
Vue.component("log-line", {
|
||||
functional: true,
|
||||
props: {
|
||||
showScreenId: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
message: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
highlight: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
render: function (createElement, context) {
|
||||
const message = context.props.message;
|
||||
const level = message.LevelName;
|
||||
|
||||
if (message.isRepeated)
|
||||
return createElement(
|
||||
"tr",
|
||||
{
|
||||
class: [
|
||||
"mod",
|
||||
level,
|
||||
"mod-repeat"
|
||||
]
|
||||
},
|
||||
[
|
||||
createElement(
|
||||
"td",
|
||||
{
|
||||
attrs: {
|
||||
colspan: context.props.showScreenId ? 4 : 3
|
||||
}
|
||||
},
|
||||
""
|
||||
),
|
||||
createElement("td", `repeats ${message.Repeated} times`)
|
||||
]
|
||||
);
|
||||
|
||||
const events = {};
|
||||
let toggleMessage;
|
||||
if (message.IsStartOfSection) {
|
||||
const visible = message.SectionName && window.app && app.sectionsAllow(message.SectionName);
|
||||
events.click = handlers.clickLogLine;
|
||||
toggleMessage = visible
|
||||
? "This section is shown. Click here to hide it."
|
||||
: "This section is hidden. Click here to show it.";
|
||||
}
|
||||
|
||||
let text = message.Text;
|
||||
const filter = window.app && app.filterRegex;
|
||||
if (text && filter && context.props.highlight) {
|
||||
text = [];
|
||||
let match;
|
||||
let consumed = 0;
|
||||
let index = 0;
|
||||
filter.lastIndex = -1;
|
||||
|
||||
// Our logic to highlight the text is a bit funky because we
|
||||
// want to group consecutive matches to avoid a situation
|
||||
// where a ton of single characters are in their own elements
|
||||
// if the user gives us bad input.
|
||||
|
||||
while (true) {
|
||||
match = filter.exec(message.Text);
|
||||
if (!match)
|
||||
break;
|
||||
|
||||
// Do we have an area of non-matching text? This
|
||||
// happens if the new match's index is further
|
||||
// along than the last index.
|
||||
if (match.index > index) {
|
||||
// Alright, do we have a previous match? If
|
||||
// we do, we need to consume some text.
|
||||
if (consumed < index)
|
||||
text.push(createElement("strong", {}, message.Text.slice(consumed, index)));
|
||||
|
||||
text.push(message.Text.slice(index, match.index));
|
||||
consumed = match.index;
|
||||
}
|
||||
|
||||
index = match.index + match[0].length;
|
||||
}
|
||||
|
||||
// Add any trailing text after the last match was found.
|
||||
if (consumed < message.Text.length) {
|
||||
if (consumed < index)
|
||||
text.push(createElement("strong", {}, message.Text.slice(consumed, index)));
|
||||
|
||||
if (index < message.Text.length)
|
||||
text.push(message.Text.slice(index));
|
||||
}
|
||||
}
|
||||
|
||||
return createElement(
|
||||
"tr",
|
||||
{
|
||||
class: [
|
||||
"mod",
|
||||
level,
|
||||
message.IsStartOfSection ? "section-start" : null
|
||||
],
|
||||
attrs: {
|
||||
"data-section": message.SectionName
|
||||
},
|
||||
on: events
|
||||
},
|
||||
[
|
||||
createElement("td", message.Time),
|
||||
context.props.showScreenId ? createElement("td", message.ScreenId) : null,
|
||||
createElement("td", level.toUpperCase()),
|
||||
createElement(
|
||||
"td",
|
||||
{
|
||||
attrs: {
|
||||
"data-title": message.Mod
|
||||
}
|
||||
},
|
||||
message.Mod
|
||||
),
|
||||
createElement(
|
||||
"td",
|
||||
[
|
||||
createElement(
|
||||
"span",
|
||||
{ class: "log-message-text" },
|
||||
text
|
||||
),
|
||||
message.IsStartOfSection
|
||||
? createElement(
|
||||
"span",
|
||||
{ class: "section-toggle-message" },
|
||||
[
|
||||
" ",
|
||||
toggleMessage
|
||||
]
|
||||
)
|
||||
: null
|
||||
]
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// init app
|
||||
app = new Vue({
|
||||
el: '#output',
|
||||
data: data,
|
||||
el: "#output",
|
||||
data: state,
|
||||
computed: {
|
||||
anyModsHidden: function () {
|
||||
return stats.modsHidden > 0;
|
||||
},
|
||||
anyModsShown: function () {
|
||||
return stats.modsShown > 0;
|
||||
},
|
||||
showScreenId: function () {
|
||||
return this.data.screenIds.length > 1;
|
||||
},
|
||||
|
||||
// Maybe not strictly necessary, but the Vue template is being
|
||||
// weird about accessing data entries on the app rather than
|
||||
// computed properties.
|
||||
hideContentPacks: function () {
|
||||
return !state.showContentPacks;
|
||||
},
|
||||
|
||||
// Filter messages for visibility.
|
||||
filterUseRegex: function () {
|
||||
return state.useRegex;
|
||||
},
|
||||
filterInsensitive: function () {
|
||||
return state.useInsensitive;
|
||||
},
|
||||
filterUseWord: function () {
|
||||
return state.useWord;
|
||||
},
|
||||
shouldHighlight: function () {
|
||||
return state.useHighlight;
|
||||
},
|
||||
|
||||
filteredMessages: function () {
|
||||
if (!state.messages)
|
||||
return [];
|
||||
|
||||
const start = performance.now();
|
||||
const filtered = [];
|
||||
|
||||
// This is slightly faster than messages.filter(), which is
|
||||
// important when working with absolutely huge logs.
|
||||
for (let i = 0, length = state.messages.length; i < length; i++) {
|
||||
const msg = state.messages[i];
|
||||
if (msg.SectionName && !msg.IsStartOfSection && !this.sectionsAllow(msg.SectionName))
|
||||
continue;
|
||||
|
||||
if (!this.filtersAllow(msg.ModSlug, msg.LevelName))
|
||||
continue;
|
||||
|
||||
const text = msg.Text || (i > 0 ? state.messages[i - 1].Text : null);
|
||||
|
||||
if (this.filterRegex) {
|
||||
this.filterRegex.lastIndex = -1;
|
||||
if (!text || !this.filterRegex.test(text))
|
||||
continue;
|
||||
}
|
||||
else if (this.filterText && (!text || text.indexOf(this.filterText) === -1))
|
||||
continue;
|
||||
|
||||
filtered.push(msg);
|
||||
}
|
||||
|
||||
const end = performance.now();
|
||||
//console.log(`applied ${(this.filterRegex ? "regex" : "text")} filter '${this.filterRegex || this.filterText}' in ${end - start}ms`);
|
||||
|
||||
return filtered;
|
||||
},
|
||||
|
||||
// And the rest are about pagination.
|
||||
start: function () {
|
||||
return (this.page - 1) * state.perPage;
|
||||
},
|
||||
end: function () {
|
||||
return this.start + this.visibleMessages.length;
|
||||
},
|
||||
totalPages: function () {
|
||||
return Math.ceil(this.filteredMessages.length / state.perPage);
|
||||
},
|
||||
//
|
||||
visibleMessages: function () {
|
||||
if (this.totalPages <= 1)
|
||||
return this.filteredMessages;
|
||||
|
||||
const start = this.start;
|
||||
const end = start + state.perPage;
|
||||
|
||||
return this.filteredMessages.slice(start, end);
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
window.addEventListener("popstate", () => this.loadFromUrl());
|
||||
this.loadFromUrl();
|
||||
},
|
||||
methods: {
|
||||
// Mostly I wanted people to know they can override the PerPage
|
||||
// message count with a URL parameter, but we can read Page too.
|
||||
// In the future maybe we should read *all* filter state so a
|
||||
// user can link to their exact page state for someone else?
|
||||
loadFromUrl: function () {
|
||||
const params = new URL(location).searchParams;
|
||||
if (params.has("PerPage")) {
|
||||
const perPage = parseInt(params.get("PerPage"));
|
||||
if (!isNaN(perPage) && isFinite(perPage) && perPage > 0)
|
||||
state.perPage = perPage;
|
||||
}
|
||||
|
||||
if (params.has("Page")) {
|
||||
const page = parseInt(params.get("Page"));
|
||||
if (!isNaN(page) && isFinite(page) && page > 0)
|
||||
this.page = page;
|
||||
}
|
||||
},
|
||||
|
||||
toggleLevel: function (id) {
|
||||
if (!data.enableFilters)
|
||||
if (!state.enableFilters)
|
||||
return;
|
||||
|
||||
this.showLevels[id] = !this.showLevels[id];
|
||||
},
|
||||
|
||||
toggleContentPacks: function () {
|
||||
state.showContentPacks = !state.showContentPacks;
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
toggleFilterUseRegex: function () {
|
||||
state.useRegex = !state.useRegex;
|
||||
this.saveSettings();
|
||||
this.updateFilterText();
|
||||
},
|
||||
|
||||
toggleFilterInsensitive: function () {
|
||||
state.useInsensitive = !state.useInsensitive;
|
||||
this.saveSettings();
|
||||
this.updateFilterText();
|
||||
},
|
||||
|
||||
toggleFilterWord: function () {
|
||||
state.useWord = !state.useWord;
|
||||
this.saveSettings();
|
||||
this.updateFilterText();
|
||||
},
|
||||
|
||||
toggleHighlight: function () {
|
||||
state.useHighlight = !state.useHighlight;
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
prevPage: function () {
|
||||
if (this.page <= 1)
|
||||
return;
|
||||
this.page--;
|
||||
this.updateUrl();
|
||||
},
|
||||
|
||||
nextPage: function () {
|
||||
if (this.page >= this.totalPages)
|
||||
return;
|
||||
this.page++;
|
||||
this.updateUrl();
|
||||
},
|
||||
|
||||
changePage: function (page) {
|
||||
if (page < 1 || page > this.totalPages)
|
||||
return;
|
||||
this.page = page;
|
||||
this.updateUrl();
|
||||
},
|
||||
|
||||
// Persist settings into localStorage for use the next time
|
||||
// the user opens a log.
|
||||
saveSettings: function () {
|
||||
localStorage.settings = JSON.stringify({
|
||||
showContentPacks: state.showContentPacks,
|
||||
useRegex: state.useRegex,
|
||||
useInsensitive: state.useInsensitive,
|
||||
useWord: state.useWord,
|
||||
useHighlight: state.useHighlight
|
||||
});
|
||||
},
|
||||
|
||||
// Whenever the page is changed, replace the current page URL. Using
|
||||
// replaceState rather than pushState to avoid filling the tab history
|
||||
// with tons of useless history steps the user probably doesn't
|
||||
// really care about.
|
||||
updateUrl: function () {
|
||||
const url = new URL(location);
|
||||
url.searchParams.set("Page", state.page);
|
||||
url.searchParams.set("PerPage", state.perPage);
|
||||
|
||||
window.history.replaceState(null, document.title, url.toString());
|
||||
},
|
||||
|
||||
// We don't want to update the filter text often, so use a debounce with
|
||||
// a quarter second delay. We basically always build a regular expression
|
||||
// since we use it for highlighting, and it also make case insensitivity
|
||||
// much easier.
|
||||
updateFilterText: helpers.getDebouncedHandler(
|
||||
function () {
|
||||
let text = this.filterText = document.querySelector("input[type=text]").value;
|
||||
if (!text || !text.length) {
|
||||
this.filterText = "";
|
||||
this.filterRegex = null;
|
||||
}
|
||||
else {
|
||||
if (!state.useRegex)
|
||||
text = helpers.escapeRegex(text);
|
||||
this.filterRegex = new RegExp(
|
||||
state.useWord ? `\\b${text}\\b` : text,
|
||||
state.useInsensitive ? "ig" : "g"
|
||||
);
|
||||
}
|
||||
},
|
||||
250
|
||||
),
|
||||
|
||||
toggleMod: function (id) {
|
||||
if (!data.enableFilters)
|
||||
if (!state.enableFilters)
|
||||
return;
|
||||
|
||||
var curShown = this.showMods[id];
|
||||
const curShown = this.showMods[id];
|
||||
|
||||
// first filter: only show this by default
|
||||
if (stats.modsHidden === 0) {
|
||||
|
@ -70,17 +782,17 @@ smapi.logParser = function (data, sectionUrl) {
|
|||
},
|
||||
|
||||
toggleSection: function (name) {
|
||||
if (!data.enableFilters)
|
||||
if (!state.enableFilters)
|
||||
return;
|
||||
|
||||
this.showSections[name] = !this.showSections[name];
|
||||
},
|
||||
|
||||
showAllMods: function () {
|
||||
if (!data.enableFilters)
|
||||
if (!state.enableFilters)
|
||||
return;
|
||||
|
||||
for (var key in this.showMods) {
|
||||
for (let key in this.showMods) {
|
||||
if (this.showMods.hasOwnProperty(key)) {
|
||||
this.showMods[key] = true;
|
||||
}
|
||||
|
@ -89,10 +801,10 @@ smapi.logParser = function (data, sectionUrl) {
|
|||
},
|
||||
|
||||
hideAllMods: function () {
|
||||
if (!data.enableFilters)
|
||||
if (!state.enableFilters)
|
||||
return;
|
||||
|
||||
for (var key in this.showMods) {
|
||||
for (let key in this.showMods) {
|
||||
if (this.showMods.hasOwnProperty(key)) {
|
||||
this.showMods[key] = false;
|
||||
}
|
||||
|
@ -113,7 +825,7 @@ smapi.logParser = function (data, sectionUrl) {
|
|||
/**********
|
||||
** Upload form
|
||||
*********/
|
||||
var input = $("#input");
|
||||
const input = $("#input");
|
||||
if (input.length) {
|
||||
// file upload
|
||||
smapi.fileUpload({
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
<s:Boolean x:Key="/Default/UserDictionary/Words/=craftables/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=crossplatform/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=cutscene/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=debounce/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=debounced/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=decoratable/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=devs/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=fallbacks/@EntryIndexedValue">True</s:Boolean>
|
||||
|
|
Loading…
Reference in New Issue