summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/release-notes.md1
-rw-r--r--src/SMAPI.Web/Controllers/LogParserController.cs31
-rw-r--r--src/SMAPI.Web/ViewModels/LogParserModel.cs38
-rw-r--r--src/SMAPI.Web/Views/LogParser/Index.cshtml153
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/log-parser.css110
-rw-r--r--src/SMAPI.Web/wwwroot/Content/js/log-parser.js90
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 />&nbsp;<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 />&nbsp;<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();
-
};