From d7696912e007a2b455a2fd5e1974924d2efe83b3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 24 Feb 2018 16:51:37 -0500 Subject: reimplement log parser with serverside parsing and vue.js frontend --- src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 325 +++++++++++------------ src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 321 ++++++---------------- 2 files changed, 249 insertions(+), 397 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index 9f07f9e8..a3be0c85 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -1,21 +1,23 @@ -.mod-repeat { - font-size: 8pt; +/********* +** Main layout +*********/ +input[type="button"] { + font-size: 20px; + border-radius: 5px; + outline: none; + box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, .2); + cursor: pointer; } -.template { - display: none; +caption { + text-align: left; + padding-top: 2px; } -.popup, #uploader { - position: fixed; - top: 0px; - left: 0px; - right: 0px; - bottom: 0; - background-color: rgba(0, 0, 0, .33); - z-index: 2; - display: none; - padding: 5px; +#output { + padding: 10px; + overflow: auto; + font-family: monospace; } #upload-button { @@ -27,101 +29,81 @@ background: #eef; } - -#uploader:after { - content: attr(data-text); - display: block; - width: 100px; - height: 24px; - line-height: 25px; - border: 1px solid #000; - background: #fff; - position: absolute; - top: 50%; - left: 50%; - margin: -12px -50px 0 0; - font-size: 18px; +/********* +** Log metadata & filters +*********/ +#metadata, #mods, #filters { font-weight: bold; - text-align: center; - border-radius: 5px; + border-bottom: 1px dashed #888888; + padding-bottom: 10px; + margin-bottom: 5px; } -.popup h1 { - position: absolute; - top: 10%; - left: 50%; - margin-left: -150px; - text-align: center; - width: 300px; - border: 1px solid #008; +table#metadata, +table#mods { + border: 1px solid #000000; + background: #ffffff; border-radius: 5px; - background: #fff; - font-family: sans-serif; - font-size: 40px; - margin-top: -25px; - z-index: 10; - border-bottom: 0; + border-spacing: 1px; + overflow: hidden; + cursor: default; + box-shadow: 1px 1px 1px 1px #dddddd; } -.frame { - margin: auto; - margin-top: 25px; - padding: 2em; - position: absolute; - top: 10%; - left: 10%; - right: 10%; - bottom: 10%; - padding-bottom: 30px; - background: #FFF; - border-radius: 5px; - border: 1px solid #008; +#mods { + min-width: 400px; } -input[type="button"] { - font-size: 20px; - border-radius: 5px; - outline: none; - box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 0, .2); - cursor: pointer; +#mods .color-red { + color: red; } -#input[type="button"]:hover { - background-color: #fee; +#mods .color-green { + color: green; } -#cancel, #closeraw { - border: 1px solid #880000; - background-color: #fcc; +#mods tr { + cursor: pointer; } -#submit { - border: 1px solid #008800; - background-color: #cfc; +#metadata tr, +#mods tr { + background: #eee } -#submit:hover { - background-color: #efe; +#mods span.notice { + font-weight: normal; + font-size: 11px; + position: relative; + top: -1px; + display: none; } -#input, #dataraw { - width: 100%; - height: 30em; - max-height: 70%; - margin: auto; - box-sizing: border-box; +#mods span.notice.btn { + cursor: pointer; + border: 1px solid #000; border-radius: 5px; - border: 1px solid #000088; - outline: none; - box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2); + position: relative; + top: -1px; + padding: 0 2px; + background: #eee; } -.color-red { - color: red; +#mods span.notice.txt { + display: inline-block; } -.color-green { - color: green; +#mods .mod-entry.hidden { + opacity: 0.5; +} + +#metadata td:first-child { + padding-right: 5px; +} + +#metadata tr:nth-child(even), +#mods tr:nth-child(even) { + background: #fff } #filters { @@ -155,59 +137,35 @@ input[type="button"] { background: #efe; } -#output { - padding: 10px; - overflow: auto; - font-family: monospace; -} - -#output > * { - display: block; -} - -#output.trace .trace, -#output.debug .debug, -#output.info .info, -#output.alert .alert, -#output.warn .warn, -#output.error .error { - display: none; +/********* +** Log +*********/ +#log .mod-repeat { + font-size: 0.85em; } -#output .trace { +#log .trace { color: #999; } -#output .debug { +#log .debug { color: #595959; } -#output .info { - color: #000 +#log .info { + color: #000; } -#output .alert { +#log .alert { color: #b0b; } -#output .warn { - color: #f80 -} - -#output .error { - color: #f00 +#log .warn { + color: #f80; } -#output .always { - font-weight: bold; - border-bottom: 1px dashed #888888; - padding-bottom: 10px; - margin-bottom: 5px; -} - -caption { - text-align: left; - padding-top: 2px; +#log .error { + color: #f00; } #log { @@ -224,6 +182,7 @@ caption { border-bottom: 1px dotted #ccc; border-top: 2px solid #fff; vertical-align: top; + white-space: pre-wrap; } #log td:not(:last-child) { @@ -259,61 +218,99 @@ caption { width: 100%; } -table#gameinfo, -table#modslist { - border: 1px solid #000000; - background: #ffffff; - border-radius: 5px; - border-spacing: 1px; - overflow: hidden; - cursor: default; - box-shadow: 1px 1px 1px 1px #dddddd; +#error { + color: #f00; } -#modslist { - min-width: 400px; -} -#gameinfo td:first-child { - padding-right: 5px; +/********* +** Upload popup +*********/ +#upload-area .popup, +#upload-area #uploader { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, .33); + z-index: 2; + display: none; + padding: 5px; } -#gameinfo tr, -#modslist tr { - background: #eee +#upload-area #uploader:after { + content: attr(data-text); + display: block; + width: 100px; + height: 24px; + line-height: 25px; + border: 1px solid #000; + background: #fff; + position: absolute; + top: 50%; + left: 50%; + margin: -12px -50px 0 0; + font-size: 18px; + font-weight: bold; + text-align: center; + border-radius: 5px; } -#gameinfo tr:nth-child(even), -#modslist tr:nth-child(even) { - background: #fff +#upload-area .popup h1 { + position: absolute; + top: 10%; + left: 50%; + margin-left: -150px; + text-align: center; + width: 300px; + border: 1px solid #008; + border-radius: 5px; + background: #fff; + font-family: sans-serif; + font-size: 40px; + margin-top: -25px; + z-index: 10; + border-bottom: 0; } -#modslist tr { - cursor: pointer; +#upload-area .frame { + margin: auto; + margin-top: 25px; + padding: 2em; + position: absolute; + top: 10%; + left: 10%; + right: 10%; + bottom: 10%; + padding-bottom: 30px; + background: #FFF; + border-radius: 5px; + border: 1px solid #008; } -span.notice { - font-weight: normal; - font-size: 11px; - position: relative; - top: -1px; - display: none; +#upload-area #cancel { + border: 1px solid #880000; + background-color: #fcc; } -span.notice.btn { - cursor: pointer; - border: 1px solid #000; - border-radius: 5px; - position: relative; - top: -1px; - padding: 0 2px; - background: #eee; +#upload-area #submit { + border: 1px solid #008800; + background-color: #cfc; } -#output:not(.modfilter) span.notice.txt { - display: inline-block; +#upload-area #submit:hover { + background-color: #efe; } -#output.modfilter span.notice.btn { - display: inline-block; +#upload-area #input { + width: 100%; + height: 30em; + max-height: 70%; + margin: auto; + box-sizing: border-box; + border-radius: 5px; + border: 1px solid #000088; + outline: none; + box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2); } diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 914863f6..87a70391 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -1,48 +1,98 @@ /* globals $ */ var smapi = smapi || {}; -smapi.logParser = function(sectionUrl, pasteID) { - /********* - ** Initialisation - *********/ - var stage, - flags = $("#modflags"), - output = $("#output"), - error = $("#error"), - filters = 0, - memory = "", - versionInfo, - modInfo, - modMap, - modErrors, - logInfo, - templateModentry = $("#template-modentry").text(), - templateCss = $("#template-css").text(), - templateLogentry = $("#template-logentry").text(), - templateLognotice = $("#template-lognotice").text(), - regexInfo = /\[[\d\:]+ INFO SMAPI] SMAPI (.*?) with Stardew Valley (.*?) on (.*?)\n/g, - regexMods = /\[[^\]]+\] Loaded \d+ mods:(?:\n\[[^\]]+\] .+)+/g, - regexLog = /\[([\d\:]+) (TRACE|DEBUG|INFO|WARN|ALERT|ERROR) ? ([^\]]+)\] ?((?:\n|.)*?)(?=(?:\[\d\d:|$))/g, - regexMod = /\[(?:.*?)\] *(.*?) (\d+\.?(?:.*?))(?: by (.*?))? \|(?:.*?)$/gm, - regexDate = /\[\d{2}:\d{2}:\d{2} TRACE SMAPI\] Log started at (.*?) UTC/g, - regexPath = /\[\d{2}:\d{2}:\d{2} DEBUG SMAPI\] Mods go here: (.*?)(?:\n|$)/g; +var app; +smapi.logParser = function (data, sectionUrl) { + // internal filter counts + var stats = data.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]) + stats.modsShown++; + else + stats.modsHidden++; + } + } + } - $("#filters span").on("click", function(evt) { - var t = $(evt.currentTarget); - t.toggleClass("active"); - output.toggleClass(t.text().toLowerCase()); + // set local time started + if(data) + data.localTimeStarted = ("0" + data.logStarted.getHours()).slice(-2) + ":" + ("0" + data.logStarted.getMinutes()).slice(-2); + + // init app + app = new Vue({ + el: '#output', + data: data, + computed: { + anyModsHidden: function () { + return stats.modsHidden > 0; + }, + anyModsShown: function () { + return stats.modsShown > 0; + } + }, + methods: { + toggleLevel: function(id) { + this.showLevels[id] = !this.showLevels[id]; + }, + + toggleMod: function (id) { + var curShown = this.showMods[id]; + + // first filter: only show this by default + if (stats.modsHidden === 0) { + this.hideAllMods(); + this.showMods[id] = true; + } + + // unchecked last filter: reset + else if (stats.modsShown === 1 && curShown) + this.showAllMods(); + + // else toggle + else + this.showMods[id] = !this.showMods[id]; + + updateModFilters(); + }, + showAllMods: function () { + for (var key in this.showMods) { + if (this.showMods.hasOwnProperty(key)) { + this.showMods[key] = true; + } + } + updateModFilters(); + }, + hideAllMods: function () { + for (var key in this.showMods) { + if (this.showMods.hasOwnProperty(key)) { + this.showMods[key] = false; + } + } + updateModFilters(); + } + } }); + + /********** + ** Upload form + *********/ + var error = $("#error"); + $("#upload-button").on("click", function() { - memory = $("#input").val() || ""; $("#input").val(""); $("#popup-upload").fadeIn(); }); var closeUploadPopUp = function() { - $("#popup-upload").fadeOut(400, function() { - $("#input").val(memory); - memory = ""; - }); + $("#popup-upload").fadeOut(400); }; $("#popup-upload").on({ @@ -77,7 +127,7 @@ smapi.logParser = function(sectionUrl, pasteID) { $("#popup-upload").fadeOut(); var paste = $("#input").val(); if (paste) { - memory = ""; + //memory = ""; $("#uploader").attr("data-text", "Saving..."); $("#uploader").fadeIn(); $ @@ -105,210 +155,15 @@ smapi.logParser = function(sectionUrl, pasteID) { }); $(document).on("keydown", function(e) { - if (e.which == 27) { - if ($("#popup-upload").css("display") !== "none" && $("#popup-upload").css("opacity") == 1) { + if (e.which === 27) { + if ($("#popup-upload").css("display") !== "none" && $("#popup-upload").css("opacity") === 1) { closeUploadPopUp(); } - - $("#popup-raw").fadeOut(400); } }); $("#cancel").on("click", closeUploadPopUp); - $("#closeraw").on("click", function() { - $("#popup-raw").fadeOut(400); - }); - - $("#popup-raw").on("click", function(e) { - if (e.target.id === "popup-raw") { - $("#popup-raw").fadeOut(400); - } - }); - - if (pasteID) { - getData(pasteID); - } - else + if (data.showPopup) $("#popup-upload").fadeIn(); - - /********* - ** Helpers - *********/ - function modClicked(evt) { - var id = $(evt.currentTarget).attr("id").split("-")[1], - cls = "mod-" + id; - if (output.hasClass(cls)) - filters--; - else - filters++; - output.toggleClass(cls); - if (filters === 0) { - output.removeClass("modfilter"); - } else { - output.addClass("modfilter"); - } - } - - function removeFilter() { - for (var c = 0; c < modInfo.length; c++) { - output.removeClass("mod-" + c); - } - filters = 0; - output.removeClass("modfilter"); - } - - function selectAll() { - for (var c = 0; c < modInfo.length; c++) { - output.addClass("mod-" + c); - } - filters = modInfo.length; - output.addClass("modfilter"); - } - - function parseData() { - stage = "parseData.pre"; - var data = $("#input").val(); - if (!data) { - stage = "parseData.checkNullData"; - throw new Error("Field `data` is null"); - - } - var dataInfo = regexInfo.exec(data) || regexInfo.exec(data) || regexInfo.exec(data), - dataMods = regexMods.exec(data) || regexMods.exec(data) || regexMods.exec(data) || [""], - dataDate = regexDate.exec(data) || regexDate.exec(data) || regexDate.exec(data), - dataPath = regexPath.exec(data) || regexPath.exec(data) || regexPath.exec(data), - match; - stage = "parseData.doNullCheck"; - if (!dataInfo) - throw new Error("Field `dataInfo` is null"); - if (!dataMods) - throw new Error("Field `dataMods` is null"); - if (!dataPath) - throw new Error("Field `dataPath` is null"); - dataMods = dataMods[0]; - stage = "parseData.setupDefaults"; - modMap = { - "SMAPI": 0 - }; - modErrors = { - "SMAPI": 0, - "Console.Out": 0 - }; - logInfo = []; - modInfo = [ - ["SMAPI", dataInfo[1], "Zoryn, CLxS & Pathoschild"] - ]; - stage = "parseData.parseInfo"; - var date = dataDate ? new Date(dataDate[1] + "Z") : null; - versionInfo = { - apiVersion: dataInfo[1], - gameVersion: dataInfo[2], - platform: dataInfo[3], - logDate: date ? date.getFullYear() + "-" + ("0" + date.getMonth().toString()).substr(-2) + "-" + ("0" + date.getDay().toString()).substr(-2) + " at " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds() + " " + date.toLocaleTimeString("en-us", { timeZoneName: "short" }).split(" ")[2] : "No timestamp found", - modsPath: dataPath[1] - }; - stage = "parseData.parseMods"; - while ((match = regexMod.exec(dataMods))) { - modErrors[match[1]] = 0; - modMap[match[1]] = modInfo.length; - modInfo.push([match[1], match[2], match[3] ? ("by " + match[3]) : "Unknown author"]); - } - stage = "parseData.parseLog"; - while ((match = regexLog.exec(data))) { - if (match[2] === "ERROR") - modErrors[match[3]]++; - logInfo.push([match[1], match[2], match[3], match[4]]); - } - stage = "parseData.post"; - modMap["Console.Out"] = modInfo.length; - modInfo.push(["Console.Out", "", ""]); - } - - function renderData() { - stage = "renderData.pre"; - - output.find("#api-version").text(versionInfo.apiVersion); - output.find("#game-version").text(versionInfo.gameVersion); - output.find("#platform").text(versionInfo.platform); - output.find("#log-started").text(versionInfo.logDate); - output.find("#mods-path").text(versionInfo.modsPath); - - var modslist = $("#modslist"), log = $("#log"), modCache = [], y = 0; - for (; y < modInfo.length; y++) { - var errors = modErrors[modInfo[y][0]], - err, cls = "color-red"; - if (errors === 0) { - err = "No Errors"; - cls = "color-green"; - } else if (errors === 1) - err = "1 Error"; - else - err = errors + " Errors"; - modCache.push(prepare(templateModentry, [y, modInfo[y][0], modInfo[y][1], modInfo[y][2], cls, err])); - } - modslist.append(modCache.join("")); - for (var z = 0; z < modInfo.length; z++) - $("#modlink-" + z).on("click", modClicked); - var flagCache = []; - for (var c = 0; c < modInfo.length; c++) - flagCache.push(prepare(templateCss, [c])); - flags.html(flagCache.join("")); - var logCache = [], dupeCount = 0, dupeMemory = "|||"; - for (var x = 0; x < logInfo.length; x++) { - var dm = logInfo[x][1] + "|" + logInfo[x][2] + "|" + logInfo[x][3]; - if (dupeMemory !== dm) { - if (dupeCount > 0) - logCache.push(prepare(templateLognotice, [logInfo[x - 1][1].toLowerCase(), modMap[logInfo[x - 1][2]], dupeCount])); - dupeCount = 0; - dupeMemory = dm; - logCache.push(prepare(templateLogentry, [logInfo[x][1].toLowerCase(), modMap[logInfo[x][2]], logInfo[x][0], logInfo[x][1], logInfo[x][2], logInfo[x][3].split(" ").join("  ").replace(//g, ">").replace(/\n/g, "
")])); - } - else - dupeCount++; - } - log.append(logCache.join("")); - $("#modlink-r").on("click", removeFilter); - $("#modlink-a").on("click", selectAll); - - $("#log-data").show(); - } - - function prepare(str, arr) { - var regex = /\{(\d)\}/g, - match; - while ((match = regex.exec(str))) - str = str.replace(match[0], arr[match[1]]); - return str; - } - function loadData() { - try { - stage = "loadData.Pre"; - parseData(); - renderData(); - $("#viewraw").on("click", function() { - $("#dataraw").val($("#input").val()); - $("#popup-raw").fadeIn(); - }); - stage = "loadData.Post"; - } - catch (err) { - error.html('

Parsing failed!

Parsing of the log failed, details follow.
 

Stage: ' + stage + "

" + err + '
');
-            $("#rawlog").text($("#input").val());
-        }
-    }
-    function getData(pasteID) {
-        $("#uploader").attr("data-text", "Loading...");
-        $("#uploader").fadeIn();
-        $.get(sectionUrl + "/fetch/" + pasteID, function(data) {
-            if (data.success) {
-                $("#input").val(data.content);
-                loadData();
-            } else {
-                error.html('

Fetching the log failed!

' + data.error + '

');
-                $("#rawlog").text($("#input").val());
-            }
-            $("#uploader").fadeOut();
-        });
-    }
 };
-- 
cgit 


From 691310d16e6873b83c55f62a59d5010dd8bb7e98 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard 
Date: Sat, 24 Feb 2018 16:52:38 -0500
Subject: add content pack support to log parser

---
 docs/release-notes.md                              |  1 +
 src/SMAPI.Web/Framework/LogParsing/LogParser.cs    | 23 ++++++++++++++++++++++
 .../Framework/LogParsing/Models/ModInfo.cs         |  3 +++
 src/SMAPI.Web/Views/LogParser/Index.cshtml         | 19 +++++++++++++++++-
 src/SMAPI.Web/wwwroot/Content/css/log-parser.css   |  6 ++++++
 5 files changed, 51 insertions(+), 1 deletion(-)

(limited to 'src/SMAPI.Web/wwwroot/Content')

diff --git a/docs/release-notes.md b/docs/release-notes.md
index 03b6dd77..da651be2 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -22,6 +22,7 @@
 
 * For the [log parser][]:
   * Significantly reduced download size when viewing files with repeated errors.
+  * Added support for SMAPI 2.5 content packs.
   * Improved parse error handling.
   * Fixed 'log started' field showing incorrect date.
 
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
index 1c3b5671..23a1baa4 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
@@ -31,6 +31,12 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
         /// A regex pattern matching an entry in SMAPI's mod list.
         private readonly Regex ModListEntryPattern = new Regex(@"^   (?.+) (?.+) by (?.+) \| (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
+        /// A regex pattern matching the start of SMAPI's content pack list.
+        private readonly Regex ContentPackListStartPattern = new Regex(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+        /// A regex pattern matching an entry in SMAPI's content pack list.
+        private readonly Regex ContentPackListEntryPattern = new Regex(@"^   (?.+) (?.+) by (?.+) \| for (?.+?) \| (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
 
         /*********
         ** Public methods
@@ -62,6 +68,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
                 LogModInfo smapiMod = new LogModInfo { Name = "SMAPI", Author = "Pathoschild", Description = "" };
                 IDictionary mods = new Dictionary();
                 bool inModList = false;
+                bool inContentPackList = false;
                 foreach (LogMessage message in log.Messages)
                 {
                     // collect stats
@@ -79,6 +86,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
                         // update flags
                         if (inModList && !this.ModListEntryPattern.IsMatch(message.Text))
                             inModList = false;
+                        if (inContentPackList && !this.ContentPackListEntryPattern.IsMatch(message.Text))
+                            inContentPackList = false;
 
                         // mod list
                         if (!inModList && message.Level == LogLevel.Info && this.ModListStartPattern.IsMatch(message.Text))
@@ -93,6 +102,20 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
                             mods[name] = new LogModInfo { Name = name, Author = author, Version = version, Description = description };
                         }
 
+                        // content pack list
+                        else if (!inContentPackList && message.Level == LogLevel.Info && this.ContentPackListStartPattern.IsMatch(message.Text))
+                            inContentPackList = true;
+                        else if (inContentPackList)
+                        {
+                            Match match = this.ContentPackListEntryPattern.Match(message.Text);
+                            string name = match.Groups["name"].Value;
+                            string version = match.Groups["version"].Value;
+                            string author = match.Groups["author"].Value;
+                            string description = match.Groups["description"].Value;
+                            string forMod = match.Groups["for"].Value;
+                            mods[name] = new LogModInfo { Name = name, Author = author, Version = version, Description = description, ContentPackFor = forMod };
+                        }
+
                         // platform info line
                         else if (message.Level == LogLevel.Info && this.InfoLinePattern.IsMatch(message.Text))
                         {
diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ModInfo.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ModInfo.cs
index 2005e61f..8c84ab38 100644
--- a/src/SMAPI.Web/Framework/LogParsing/Models/ModInfo.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/Models/ModInfo.cs
@@ -18,6 +18,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
         /// The mod description.
         public string Description { get; set; }
 
+        /// The name of the mod for which this is a content pack (if applicable).
+        public string ContentPackFor { get; set; }
+
         /// The number of errors logged by this mod.
         public int Errors { get; set; }
     }
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index 8d1abbb1..20e20ee1 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -1,5 +1,10 @@
 @{
     ViewData["Title"] = "SMAPI log parser";
+
+    Dictionary contentPacks = Model.ParsedLog?.Mods
+        ?.GroupBy(mod => mod.ContentPackFor)
+        .Where(group => group.Key != null)
+        .ToDictionary(group => group.Key, group => group.ToArray());
 }
 @using Newtonsoft.Json
 @using StardewModdingAPI.Web.Framework.LogParsing.Models
@@ -69,10 +74,22 @@
                 show all
                 hide all
             
-            @foreach (var mod in Model.ParsedLog.Mods)
+            @foreach (var mod in Model.ParsedLog.Mods.Where(p => p.ContentPackFor == null))
             {
                 
                     
+                    
+                        @mod.Name
+                        @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList))
+                        {
+                            
+ @foreach (var contentPack in contentPackList) + { + +@contentPack.Name @contentPack.Version + } +
+ } + @mod.Version @mod.Author @if (mod.Errors == 0) diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index a3be0c85..cbf09ffe 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -97,6 +97,12 @@ table#mods { opacity: 0.5; } +#mods .content-packs { + margin-left: 1em; + font-size: 0.9em; + font-style: italic; +} + #metadata td:first-child { padding-right: 5px; } -- cgit