diff options
-rw-r--r-- | docs/release-notes.md | 1 | ||||
-rw-r--r-- | src/SMAPI.Web/Controllers/LogParserController.cs | 31 | ||||
-rw-r--r-- | src/SMAPI.Web/ViewModels/LogParserModel.cs | 38 | ||||
-rw-r--r-- | src/SMAPI.Web/Views/LogParser/Index.cshtml | 153 | ||||
-rw-r--r-- | src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 110 | ||||
-rw-r--r-- | src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 90 |
6 files changed, 193 insertions, 230 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index 8824c0fb..7668dc57 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -51,6 +51,7 @@ * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). * For the log parser: + * Redesigned upload page to make it more intuitive for new players. * Fixed issue parsing content packs with no description. * For SMAPI developers: diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 62547deb..2bff1392 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.IO.Compression; +using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -72,13 +73,27 @@ namespace StardewModdingAPI.Web.Controllers ** JSON ***/ /// <summary>Save raw log data.</summary> - /// <param name="content">The log content to save.</param> - [HttpPost, Produces("application/json"), AllowLargePosts] - [Route("log/save")] - public async Task<SavePasteResult> PostAsync([FromBody] string content) + [HttpPost, AllowLargePosts] + [Route("log")] + public async Task<ActionResult> PostAsync() { - content = this.CompressString(content); - return await this.Pastebin.PostAsync(content); + // get raw log text + string input = this.Request.Form["input"].FirstOrDefault(); + if (string.IsNullOrWhiteSpace(input)) + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null, null) { UploadError = "The log file seems to be empty." }); + + // upload log + input = this.CompressString(input); + SavePasteResult result = await this.Pastebin.PostAsync(input); + + // handle errors + if (!result.Success) + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID, null) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" }); + + // redirect to view + UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl)); + uri.Path = uri.Path.TrimEnd('/') + '/' + result.ID; + return this.Redirect(uri.Uri.ToString()); } @@ -115,7 +130,7 @@ namespace StardewModdingAPI.Web.Controllers } // prefix length - var zipBuffer = new byte[compressedData.Length + 4]; + byte[] zipBuffer = new byte[compressedData.Length + 4]; Buffer.BlockCopy(compressedData, 0, zipBuffer, 4, compressedData.Length); Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, zipBuffer, 0, 4); @@ -151,7 +166,7 @@ namespace StardewModdingAPI.Web.Controllers memoryStream.Write(zipBuffer, 4, zipBuffer.Length - 4); // read data - var buffer = new byte[dataLength]; + byte[] buffer = new byte[dataLength]; memoryStream.Position = 0; using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) gZipStream.Read(buffer, 0, buffer.Length); diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs index 8c026536..0fbd8ad5 100644 --- a/src/SMAPI.Web/ViewModels/LogParserModel.cs +++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using StardewModdingAPI.Web.Framework.LogParsing.Models; namespace StardewModdingAPI.Web.ViewModels @@ -6,6 +9,13 @@ namespace StardewModdingAPI.Web.ViewModels public class LogParserModel { /********* + ** Properties + *********/ + /// <summary>A regex pattern matching characters to remove from a mod name to create the slug ID.</summary> + private readonly Regex SlugInvalidCharPattern = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + + /********* ** Accessors *********/ /// <summary>The root URL for the log parser controller.</summary> @@ -17,6 +27,12 @@ namespace StardewModdingAPI.Web.ViewModels /// <summary>The parsed log info.</summary> public ParsedLog ParsedLog { get; set; } + /// <summary>An error which occurred while uploading the log to Pastebin.</summary> + public string UploadError { get; set; } + + /// <summary>An error which occurred while parsing the log file.</summary> + public string ParseError => this.ParsedLog?.Error; + /********* ** Public methods @@ -34,5 +50,27 @@ namespace StardewModdingAPI.Web.ViewModels this.PasteID = pasteID; this.ParsedLog = parsedLog; } + + /// <summary>Get all content packs in the log grouped by the mod they're for.</summary> + public IDictionary<string, LogModInfo[]> GetContentPacksByMod() + { + // get all mods & content packs + LogModInfo[] mods = this.ParsedLog?.Mods; + if (mods == null || !mods.Any()) + return new Dictionary<string, LogModInfo[]>(); + + // group by mod + return mods + .Where(mod => mod.ContentPackFor != null) + .GroupBy(mod => mod.ContentPackFor) + .ToDictionary(group => group.Key, group => group.ToArray()); + } + + /// <summary>Get a sanitised mod name that's safe to use in anchors, attributes, and URLs.</summary> + /// <param name="modName">The mod name.</param> + public string GetSlug(string modName) + { + return this.SlugInvalidCharPattern.Replace(modName, ""); + } } } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index d051026f..79cd7a2b 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -1,23 +1,14 @@ -@{ - ViewData["Title"] = "SMAPI log parser"; - - IDictionary<string, LogModInfo[]> contentPacks = Model.ParsedLog?.Mods - ?.GroupBy(mod => mod.ContentPackFor) - .Where(group => group.Key != null) - .ToDictionary(group => group.Key, group => group.ToArray()); - - Regex slugInvalidCharPattern = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase); - string GetSlug(string modName) - { - return slugInvalidCharPattern.Replace(modName, ""); - } -} -@using System.Text.RegularExpressions @using Newtonsoft.Json @using StardewModdingAPI.Web.Framework.LogParsing.Models @model StardewModdingAPI.Web.ViewModels.LogParserModel + +@{ + ViewData["Title"] = "SMAPI log parser"; + IDictionary<string, LogModInfo[]> contentPacks = Model.GetContentPacksByMod(); +} + @section Head { - <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180225" /> + <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180603" /> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script> <script src="~/Content/js/log-parser.js?r=20180225"></script> @@ -26,51 +17,104 @@ smapi.logParser({ logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)), showPopup: @Json.Serialize(Model.ParsedLog == null), - showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), new JsonSerializerSettings { Formatting = Formatting.None }), + showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), new JsonSerializerSettings { Formatting = Formatting.None }), showLevels: { - trace: false, - debug: false, - info: true, - alert: true, - warn: true, - error: true + @LogLevel.Trace.ToString().ToLower(): false, + @LogLevel.Debug.ToString().ToLower(): false, + @LogLevel.Info.ToString().ToLower(): true, + @LogLevel.Alert.ToString().ToLower(): true, + @LogLevel.Warn.ToString().ToLower(): true, + @LogLevel.Error.ToString().ToLower(): true } }, '@Model.SectionUrl'); }); </script> } -@********* -** Intro -*********@ +@* intro and upload result banner *@ <p id="blurb">This page lets you upload, view, and share a SMAPI log to help troubleshoot mod issues.</p> - -@if (Model.ParsedLog?.IsValid == true) +@if (Model.UploadError != null) { - <div class="banner success" v-pre> - <strong>The log was uploaded successfully!</strong><br/> - Share this URL when asking for help: <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br/> - (Or <a id="upload-button" href="#">upload a new log</a>.) + <div class="banner error" v-pre> + <strong>Oops, the server ran into trouble saving that file.</strong><br /> + <small v-pre>Error details: @Model.UploadError</small> </div> } -else if (Model.ParsedLog?.IsValid == false) +else if (Model.ParseError != null) { <div class="banner error" v-pre> - <strong>Oops, couldn't parse that file. (Make sure you upload the log file, not the console text.)</strong><br /> + <strong>Oops, couldn't parse that log. (Make sure you upload the log file, not the console text.)</strong><br /> Share this URL when asking for help: <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br /> - (Or <a id="upload-button" href="#">upload a new log</a>.)<br /> + (Or <a href="@Model.SectionUrl">upload a new log</a>.)<br /> <br /> - <small v-pre>Error details: @Model.ParsedLog.Error</small> + <small v-pre>Error details: @Model.ParseError</small> </div> } -else +else if (Model.ParsedLog?.IsValid == true) +{ + <div class="banner success" v-pre> + <strong>The log was uploaded successfully!</strong><br /> + Share this URL when asking for help: <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br /> + (Or <a href="@Model.SectionUrl">upload a new log</a>.) + </div> +} + +@* upload new log *@ +@if (Model.ParsedLog == null) { - <input type="button" id="upload-button" value="Share a new log" /> + <h2>FAQs</h2> + <h3>Where do I find my SMAPI log?</h3> + <div>What system do you use?</div> + <ul id="os-list"> + <li><input type="radio" name="os" value="linux" id="os-linux" /> <label for="os-linux">Linux</label></li> + <li><input type="radio" name="os" value="mac" id="os-mac" /> <label for="os-mac">Mac</label></li> + <li><input type="radio" name="os" value="windows" id="os-windows" /> <label for="os-windows">Windows</label></li> + </ul> + <div data-os="linux"> + On Linux: + <ol> + <li>Open the Files app.</li> + <li>Click the options menu (might be labeled <em>Go</em> or <code>⋮</code>).</li> + <li>Choose <em>Enter Location</em>.</li> + <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li> + <li>The log file is <code>SMAPI-latest.txt</code>.</li> + </ol> + </div> + <div data-os="mac"> + On Mac: + <ol> + <li>Open the Finder app.</li> + <li>Click <em>Go</em> at the top, then <em>Enter Location</em>.</li> + <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li> + <li>The log file is <code>SMAPI-latest.txt</code>.</li> + </ol> + </div> + <div data-os="windows"> + On Windows: + <ol> + <li>Press the <code>Windows</code> and <code>R</code> buttons at the same time.</li> + <li>In the 'run' box that appears, enter this exact text: <pre>%appdata%\StardewValley\ErrorLogs</pre></li> + <li>The log file is <code>SMAPI-latest.txt</code>.</li> + </ol> + </div> + + <h3>How do I share my log?</h3> + <form action="@Model.SectionUrl" method="post"> + <ol> + <li> + Drag the file onto this textbox (or paste the text in):<br /> + <textarea id="input" name="input" placeholder="paste log here"></textarea> + </li> + <li> + Click this button:<br /> + <input type="submit" id="submit" value="save log" /> + </li> + <li>On the new page, copy the URL and send it to the person helping you.</li> + </ol> + </form> } -@********* -** Parsed log -*********@ +@* parsed log *@ @if (Model.ParsedLog?.IsValid == true) { <h2>Log info</h2> @@ -104,8 +148,8 @@ else </caption> @foreach (var mod in Model.ParsedLog.Mods.Where(p => p.ContentPackFor == null)) { - <tr v-on:click="toggleMod('@GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@GetSlug(mod.Name)'] }"> - <td><input type="checkbox" v-bind:checked="showMods['@GetSlug(mod.Name)']" v-show="anyModsHidden" /></td> + <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-show="anyModsHidden" /></td> <td v-pre> <strong>@mod.Name</strong> @mod.Version @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList)) @@ -149,7 +193,7 @@ else { string levelStr = message.Level.ToString().ToLower(); - <tr class="@levelStr mod" v-show="filtersAllow('@GetSlug(message.Mod)', '@levelStr')"> + <tr class="@levelStr mod" v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr')"> <td v-pre>@message.Time</td> <td v-pre>@message.Level.ToString().ToUpper()</td> <td v-pre data-title="@message.Mod">@message.Mod</td> @@ -157,7 +201,7 @@ else </tr> if (message.Repeated > 0) { - <tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@GetSlug(message.Mod)', '@levelStr')"> + <tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr')"> <td colspan="3"></td> <td v-pre><i>repeats [@message.Repeated] times.</i></td> </tr> @@ -171,22 +215,3 @@ else if (Model.ParsedLog?.IsValid == false) <h3>Raw log</h3> <pre v-pre>@Model.ParsedLog.RawText</pre> } - -<div id="upload-area"> - <div id="popup-upload" class="popup"> - <h1>Upload log file</h1> - <div class="frame"> - <ol> - <li><a href="https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting#Find_your_SMAPI_log" target="_blank">Find your SMAPI log file</a> (not the console text).</li> - <li>Drag the file onto the textbox below (or paste the text in).</li> - <li>Click <em>Parse</em>.</li> - </ol> - <textarea id="input" placeholder="Paste or drag the log here"></textarea> - <div class="buttons"> - <input type="button" id="submit" value="Parse" /> - <input type="button" id="cancel" value="Cancel" /> - </div> - </div> - </div> - <div id="uploader"></div> -</div> diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index 25e874ac..482fc780 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -1,14 +1,6 @@ /********* ** 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; -} - caption { text-align: left; padding-top: 2px; @@ -20,15 +12,6 @@ caption { font-family: monospace; } -input#upload-button { - background: #ccf; - border: 1px solid #000088; -} - -input#upload-button { - background: #eef; -} - table caption { font-weight: bold; } @@ -262,88 +245,19 @@ table#metadata, table#mods { /********* -** Upload popup +** Upload form *********/ -#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; -} - -#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; -} - -#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; -} - -#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; -} - -#upload-area #cancel { - border: 1px solid #880000; - background-color: #fcc; +#os-list { + list-style: none; } -#upload-area #submit { - border: 1px solid #008800; - background-color: #cfc; -} - -#upload-area #submit:hover { - background-color: #efe; +div[data-os] { + display: none; } -#upload-area #input { +#input { width: 100%; - height: 30em; + height: 20em; max-height: 70%; margin: auto; box-sizing: border-box; @@ -352,3 +266,13 @@ table#metadata, table#mods { outline: none; box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2); } + +#submit { + font-size: 1.5em; + border-radius: 5px; + outline: none; + box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, .2); + cursor: pointer; + border: 1px solid #008800; + background-color: #cfc; +} diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index c4a35e96..eba6451d 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -90,27 +90,36 @@ smapi.logParser = function (data, sectionUrl) { /********** ** Upload form *********/ - var error = $("#error"); - - $("#upload-button").on("click", function(e) { - e.preventDefault(); - - $("#input").val(""); - $("#popup-upload").fadeIn(); - }); - - var closeUploadPopUp = function() { - $("#popup-upload").fadeOut(400); - }; + // get elements + var systemOptions = $("input[name='os']"); + var systemInstructions = $("div[data-os]"); + var input = $("#input"); + var submit = $("#submit"); + + // instruction OS chooser + var chooseSystem = function() { + systemInstructions.hide(); + systemInstructions.filter("[data-os='" + $("input[name='os']:checked").val() + "']").show(); + } + systemOptions.on("click", chooseSystem); + chooseSystem(); + + // disable submit if it's empty + var toggleSubmit = function() + { + var hasText = !!input.val().trim(); + submit.prop("disabled", !hasText); + } + input.on("input", toggleSubmit); + toggleSubmit(); - $("#popup-upload").on({ + // drag & drop file + input.on({ 'dragover dragenter': function(e) { e.preventDefault(); e.stopPropagation(); }, 'drop': function(e) { - $("#uploader").attr("data-text", "Reading..."); - $("#uploader").show(); var dataTransfer = e.originalEvent.dataTransfer; if (dataTransfer && dataTransfer.files.length) { e.preventDefault(); @@ -119,59 +128,10 @@ smapi.logParser = function (data, sectionUrl) { var reader = new FileReader(); reader.onload = $.proxy(function(file, $input, event) { $input.val(event.target.result); - $("#uploader").fadeOut(); - $("#submit").click(); + toggleSubmit(); }, this, file, $("#input")); reader.readAsText(file); } - }, - 'click': function(e) { - if (e.target.id === "popup-upload") - closeUploadPopUp(); - } - }); - - $("#submit").on("click", function() { - $("#popup-upload").fadeOut(); - var paste = $("#input").val(); - if (paste) { - //memory = ""; - $("#uploader").attr("data-text", "Saving..."); - $("#uploader").fadeIn(); - $ - .ajax({ - type: "POST", - url: sectionUrl + "/save", - data: JSON.stringify(paste), - contentType: "application/json" // sent to API - }) - .fail(function(xhr, textStatus) { - $("#uploader").fadeOut(); - error.html('<h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br /> <p>Stage: Upload</p>Error: ' + textStatus + ': ' + xhr.responseText + "<hr /><pre>" + $("#input").val() + "</pre>"); - }) - .then(function(data) { - $("#uploader").fadeOut(); - if (!data.success) - error.html('<h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br /> <p>Stage: Upload</p>Error: ' + data.error + "<hr /><pre>" + $("#input").val() + "</pre>"); - else - location.href = (sectionUrl.replace(/\/$/, "") + "/" + data.id); - }); - } else { - alert("Unable to parse log, the input is empty!"); - $("#uploader").fadeOut(); - } - }); - - $(document).on("keydown", function(e) { - if (e.which === 27) { - if ($("#popup-upload").css("display") !== "none" && $("#popup-upload").css("opacity") === 1) { - closeUploadPopUp(); - } } }); - $("#cancel").on("click", closeUploadPopUp); - - if (data.showPopup) - $("#popup-upload").fadeIn(); - }; |