diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-06-03 13:54:26 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-06-03 13:54:26 -0400 |
commit | a463a05607c89922af7e908b39aa897b8d23bfbf (patch) | |
tree | 4d3bb17a499357ac99d929bee695dd316e7e772f /src | |
parent | 045891131ccfdb980fcd84b3d3e52a2b2fcd94e2 (diff) | |
download | SMAPI-a463a05607c89922af7e908b39aa897b8d23bfbf.tar.gz SMAPI-a463a05607c89922af7e908b39aa897b8d23bfbf.tar.bz2 SMAPI-a463a05607c89922af7e908b39aa897b8d23bfbf.zip |
redesign log parser upload page
This makes the instructions much more clear and prominent, so it should be more intuitive for players. The previous design often confused users because they saw the big textbox and ignored the little instructions above it.
Diffstat (limited to 'src')
-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 |
5 files changed, 192 insertions, 230 deletions
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(); - }; |