From 86cafc77f55ee2b42f4602911d322760d8683972 Mon Sep 17 00:00:00 2001
From: Dan Volchek <volchek2@illinois.edu>
Date: Mon, 23 Apr 2018 01:05:02 -0500
Subject: cool pufferchick on hover

---
 src/SMAPI.Web/wwwroot/Content/js/index.js | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)
 create mode 100644 src/SMAPI.Web/wwwroot/Content/js/index.js

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

diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js
new file mode 100644
index 00000000..54fdf6d9
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/js/index.js
@@ -0,0 +1,16 @@
+document.addEventListener('DOMContentLoaded', setupHover, false);
+
+function setupHover() {
+    var pufferchick = document.getElementById("pufferchick");
+    var downloadLinks = document.getElementsByClassName("download");
+
+    for (var downloadLink of downloadLinks) {
+        downloadLink.addEventListener("mouseenter", function () {
+            pufferchick.src = "Content/images/pufferchick-cool.png";
+        });
+
+        downloadLink.addEventListener("mouseleave", function () {
+            pufferchick.src = "favicon.ico";
+        });
+    }
+}
-- 
cgit 


From 2bc9184464261f918abe142de566cd82cf565918 Mon Sep 17 00:00:00 2001
From: Dan Volchek <volchek2@illinois.edu>
Date: Mon, 23 Apr 2018 01:53:32 -0500
Subject: use jQuery

---
 src/SMAPI.Web/Views/Index/Index.cshtml    |  1 +
 src/SMAPI.Web/wwwroot/Content/js/index.js | 26 ++++++++++----------------
 2 files changed, 11 insertions(+), 16 deletions(-)

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

diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml
index c13c94a5..8ae23a45 100644
--- a/src/SMAPI.Web/Views/Index/Index.cshtml
+++ b/src/SMAPI.Web/Views/Index/Index.cshtml
@@ -4,6 +4,7 @@
 @model StardewModdingAPI.Web.ViewModels.IndexModel
 @section Head {
     <link rel="stylesheet" href="~/Content/css/index.css" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script>
     <script src="~/Content/js/index.js"></script>
 }
 
diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js
index 54fdf6d9..c53592f8 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/index.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/index.js
@@ -1,16 +1,10 @@
-document.addEventListener('DOMContentLoaded', setupHover, false);
-
-function setupHover() {
-    var pufferchick = document.getElementById("pufferchick");
-    var downloadLinks = document.getElementsByClassName("download");
-
-    for (var downloadLink of downloadLinks) {
-        downloadLink.addEventListener("mouseenter", function () {
-            pufferchick.src = "Content/images/pufferchick-cool.png";
-        });
-
-        downloadLink.addEventListener("mouseleave", function () {
-            pufferchick.src = "favicon.ico";
-        });
-    }
-}
+$(document).ready(function () {
+    var pufferchick = $("#pufferchick");
+    $(".download").each(function (index, element) {
+        $(element).hover(function () {
+            pufferchick.attr("src", "Content/images/pufferchick-cool.png");
+        }, function () {
+            pufferchick.attr("src", "favicon.ico");
+        })
+    });
+});
-- 
cgit 


From 82f418a38baeb44f68601708ebbd3c4af03ef6da Mon Sep 17 00:00:00 2001
From: Dan Volchek <volchek2@illinois.edu>
Date: Mon, 23 Apr 2018 01:58:18 -0500
Subject: add missing semicolon

---
 src/SMAPI.Web/wwwroot/Content/js/index.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js
index c53592f8..46f78fbe 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/index.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/index.js
@@ -5,6 +5,6 @@ $(document).ready(function () {
             pufferchick.attr("src", "Content/images/pufferchick-cool.png");
         }, function () {
             pufferchick.attr("src", "favicon.ico");
-        })
+        });
     });
 });
-- 
cgit 


From 371d7fa05317f107f7b97a7b5db93e176039418d Mon Sep 17 00:00:00 2001
From: Dan Volchek <volchek2@illinois.edu>
Date: Mon, 23 Apr 2018 02:01:14 -0500
Subject: use implicit iteration instead

---
 src/SMAPI.Web/wwwroot/Content/js/index.js | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

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

diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js
index 46f78fbe..ac05df05 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/index.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/index.js
@@ -1,10 +1,8 @@
 $(document).ready(function () {
     var pufferchick = $("#pufferchick");
-    $(".download").each(function (index, element) {
-        $(element).hover(function () {
-            pufferchick.attr("src", "Content/images/pufferchick-cool.png");
-        }, function () {
-            pufferchick.attr("src", "favicon.ico");
-        });
+    $(".download").hover(function () {
+        pufferchick.attr("src", "Content/images/pufferchick-cool.png");
+    }, function () {
+        pufferchick.attr("src", "favicon.ico");
     });
 });
-- 
cgit 


From cd3dbc47aa6e112d8695bcb9ed81f0b422f21e5c Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard <github@jplamondonw.com>
Date: Wed, 25 Apr 2018 12:11:28 -0400
Subject: update release notes, tweak formatting (#477)

---
 docs/release-notes.md                     |  3 +++
 src/SMAPI.Web/wwwroot/Content/js/index.js | 13 ++++++++-----
 2 files changed, 11 insertions(+), 5 deletions(-)

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

diff --git a/docs/release-notes.md b/docs/release-notes.md
index 38a8b00d..308007bb 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -18,6 +18,9 @@
   * **Breaking change**: dropped some deprecated APIs.
   * **Breaking change**: mods can't intercept chatbox input, including the game's hotkey to toggle the chatbox (default `T`).
 
+* For the log parser:
+  * The pufferchick is now more stylish.
+
 * For SMAPI developers:
   * Added more consistent crossplatform handling using a new `EnvironmentUtility`.
   * Added MacOS detection.
diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js
index ac05df05..016d5fa4 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/index.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/index.js
@@ -1,8 +1,11 @@
 $(document).ready(function () {
     var pufferchick = $("#pufferchick");
-    $(".download").hover(function () {
-        pufferchick.attr("src", "Content/images/pufferchick-cool.png");
-    }, function () {
-        pufferchick.attr("src", "favicon.ico");
-    });
+    $(".download").hover(
+        function () {
+            pufferchick.attr("src", "Content/images/pufferchick-cool.png");
+        },
+        function () {
+            pufferchick.attr("src", "favicon.ico");
+        }
+    );
 });
-- 
cgit 


From a463a05607c89922af7e908b39aa897b8d23bfbf Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard <github@jplamondonw.com>
Date: Sun, 3 Jun 2018 13:54:26 -0400
Subject: 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.
---
 docs/release-notes.md                            |   1 +
 src/SMAPI.Web/Controllers/LogParserController.cs |  31 +++--
 src/SMAPI.Web/ViewModels/LogParserModel.cs       |  38 ++++++
 src/SMAPI.Web/Views/LogParser/Index.cshtml       | 153 +++++++++++++----------
 src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 110 +++-------------
 src/SMAPI.Web/wwwroot/Content/js/log-parser.js   |  90 ++++---------
 6 files changed, 193 insertions(+), 230 deletions(-)

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

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
@@ -5,6 +8,13 @@ namespace StardewModdingAPI.Web.ViewModels
     /// <summary>The view model for the log parser page.</summary>
     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
         *********/
@@ -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();
-
 };
-- 
cgit 


From 94c1be6154223d070e7f5e5bcce80e81a2098048 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard <github@jplamondonw.com>
Date: Sun, 3 Jun 2018 17:40:54 -0400
Subject: fix script error on log result page

---
 src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 76 +++++++++++++-------------
 1 file changed, 39 insertions(+), 37 deletions(-)

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

diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
index eba6451d..44c3ad5d 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
@@ -90,48 +90,50 @@ smapi.logParser = function (data, sectionUrl) {
     /**********
     ** Upload form
     *********/
-    // get elements
-    var systemOptions = $("input[name='os']");
-    var systemInstructions = $("div[data-os]");
     var input = $("#input");
-    var submit = $("#submit");
+    if (input.length) {
+        // get elements
+        var systemOptions = $("input[name='os']");
+        var systemInstructions = $("div[data-os]");
+        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();
+        // 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();
+        // disable submit if it's empty
+        var toggleSubmit = function()
+        {
+            var hasText = !!input.val().trim();
+            submit.prop("disabled", !hasText);
+        }
+        input.on("input", toggleSubmit);
+        toggleSubmit();
 
-    // drag & drop file
-    input.on({
-        'dragover dragenter': function(e) {
-            e.preventDefault();
-            e.stopPropagation();
-        },
-        'drop': function(e) {
-            var dataTransfer = e.originalEvent.dataTransfer;
-            if (dataTransfer && dataTransfer.files.length) {
+        // drag & drop file
+        input.on({
+            'dragover dragenter': function(e) {
                 e.preventDefault();
                 e.stopPropagation();
-                var file = dataTransfer.files[0];
-                var reader = new FileReader();
-                reader.onload = $.proxy(function(file, $input, event) {
-                    $input.val(event.target.result);
-                    toggleSubmit();
-                }, this, file, $("#input"));
-                reader.readAsText(file);
+            },
+            'drop': function(e) {
+                var dataTransfer = e.originalEvent.dataTransfer;
+                if (dataTransfer && dataTransfer.files.length) {
+                    e.preventDefault();
+                    e.stopPropagation();
+                    var file = dataTransfer.files[0];
+                    var reader = new FileReader();
+                    reader.onload = $.proxy(function(file, $input, event) {
+                        $input.val(event.target.result);
+                        toggleSubmit();
+                    }, this, file, $("#input"));
+                    reader.readAsText(file);
+                }
             }
-        }
-    });
+        });
+    }
 };
-- 
cgit 


From c9a6d327110806d20877c477095238846a8d83bf Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard <github@jplamondonw.com>
Date: Sat, 16 Jun 2018 01:11:13 -0400
Subject: update web UI to let players download from new Nexus page (#547)

---
 docs/release-notes.md                              |   5 +-
 src/SMAPI.Web/Views/Index/Index.cshtml             |  21 ++++++--
 src/SMAPI.Web/wwwroot/Content/css/index.css        |  55 ++++++++++++++++++++-
 .../Content/images/direct-download-icon.png        | Bin 0 -> 250 bytes
 .../wwwroot/Content/images/nexus-icon.png          | Bin 0 -> 927 bytes
 src/SMAPI.Web/wwwroot/Content/js/index.js          |  25 +++++++++-
 6 files changed, 98 insertions(+), 8 deletions(-)
 create mode 100644 src/SMAPI.Web/wwwroot/Content/images/direct-download-icon.png
 create mode 100644 src/SMAPI.Web/wwwroot/Content/images/nexus-icon.png

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

diff --git a/docs/release-notes.md b/docs/release-notes.md
index 152037f8..5bb77762 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -59,9 +59,10 @@
   * Fixed `world_setseason` not normalising the season value.
 
 * For the web UI:
-  * Redesigned log parser upload page to make it more intuitive for new players.
-  * Changed log parser filters to show `DEBUG` messages by default.
+  * Improved log parser design to make it more intuitive.
   * Improved layout on small screens.
+  * Added option to download from Nexus.
+  * Changed log parser filters to show `DEBUG` messages by default.
   * Fixed log parser issue when content packs have no description.
   * Fixed log parser mangling crossplatform paths in some cases.
 
diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml
index 0f056ada..fbfc2239 100644
--- a/src/SMAPI.Web/Views/Index/Index.cshtml
+++ b/src/SMAPI.Web/Views/Index/Index.cshtml
@@ -3,9 +3,9 @@
 }
 @model StardewModdingAPI.Web.ViewModels.IndexModel
 @section Head {
-    <link rel="stylesheet" href="~/Content/css/index.css?r=20180611" />
+    <link rel="stylesheet" href="~/Content/css/index.css?r=20180615" />
     <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script>
-    <script src="~/Content/js/index.js"></script>
+    <script src="~/Content/js/index.js?r=20180615"></script>
 }
 
 <p id="blurb">
@@ -15,10 +15,23 @@
 </p>
 
 <div id="call-to-action">
-    <a href="@Model.StableVersion.DownloadUrl" class="main-cta download">Download SMAPI @Model.StableVersion.Version</a><br />
+    <div class="cta-dropdown">
+        <a href="@Model.StableVersion.DownloadUrl" class="main-cta download">Download SMAPI @Model.StableVersion.Version</a><br/>
+        <div class="dropdown-content">
+            <a href="https://www.nexusmods.com/stardewvalley/mods/2400"><img src="Content/images/nexus-icon.png" /> Download from Nexus</a>
+            <a href="@Model.StableVersion.DownloadUrl"><img src="Content/images/direct-download-icon.png" /> Direct download</a>
+        </div>
+    </div><br />
+
     @if (Model.BetaVersion != null)
     {
-        <a href="@Model.BetaVersion.DownloadUrl" class="secondary-cta download">Download SMAPI @Model.BetaVersion.Version<br /><small>for Stardew Valley 1.3 beta</small></a><br />
+        <div class="cta-dropdown secondary-cta-dropdown">
+            <a href="@Model.BetaVersion.DownloadUrl" class="secondary-cta download">Download SMAPI @Model.BetaVersion.Version<br/><small>for Stardew Valley 1.3 beta</small></a><br/>
+            <div class="dropdown-content">
+                <a href="https://www.nexusmods.com/stardewvalley/mods/2400"><img src="Content/images/nexus-icon.png" /> Download from Nexus</a>
+                <a href="@Model.BetaVersion.DownloadUrl"><img src="Content/images/direct-download-icon.png" /> Direct download</a>
+            </div>
+        </div><br />
     }
     <a href="https://stardewvalleywiki.com/Modding:Player_Guide" class="secondary-cta">Player guide</a><br />
     <img id="pufferchick" src="favicon.ico" />
diff --git a/src/SMAPI.Web/wwwroot/Content/css/index.css b/src/SMAPI.Web/wwwroot/Content/css/index.css
index 6340ed87..514e1a5c 100644
--- a/src/SMAPI.Web/wwwroot/Content/css/index.css
+++ b/src/SMAPI.Web/wwwroot/Content/css/index.css
@@ -18,7 +18,8 @@ h1 {
     text-align: center;
 }
 
-#call-to-action a {
+#call-to-action a.main-cta,
+#call-to-action a.secondary-cta {
     box-shadow: #caefab 0 1px 0 0 inset;
     background: linear-gradient(#77d42a 5%, #5cb811 100%) #77d42a;
     border-radius: 6px;
@@ -40,6 +41,58 @@ h1 {
     text-shadow: #2b665e 0 1px 0;
 }
 
+.cta-dropdown {
+    position: relative;
+    display: inline-block;
+    margin-bottom: 1em;
+}
+
+.cta-dropdown a.download {
+    margin-bottom: 0 !important;
+}
+
+.cta-dropdown .dropdown-content {
+    display: none;
+    position: absolute;
+    text-align: left;
+    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
+    border: 1px solid #566963;
+    background: #5cb811;
+    border-top: 0;
+    border-radius: 0 0 6px 6px;
+    margin-top: -6px;
+    z-index: 1;
+}
+
+.cta-dropdown .dropdown-content a:hover {
+    background-color: #ddd;
+}
+
+.cta-dropdown .dropdown-content img {
+    width: 0.85em;
+    height: 0.85em;
+}
+
+.cta-dropdown.secondary-cta-dropdown .dropdown-content a:hover {
+    background-color: #566963;
+}
+
+.cta-dropdown.secondary-cta-dropdown .dropdown-content {
+    background-color: #768d87;
+    border-color: #566963;
+}
+
+.cta-dropdown.secondary-cta-dropdown .dropdown-content a {
+    color: #fff;
+    text-shadow: #2b665e 0 1px 0;
+}
+
+.cta-dropdown .dropdown-content a {
+    padding: 0.75em 1em;
+    text-decoration: none;
+    display: block;
+}
+
 /*********
 ** Subsections
 *********/
diff --git a/src/SMAPI.Web/wwwroot/Content/images/direct-download-icon.png b/src/SMAPI.Web/wwwroot/Content/images/direct-download-icon.png
new file mode 100644
index 00000000..6c30ca36
Binary files /dev/null and b/src/SMAPI.Web/wwwroot/Content/images/direct-download-icon.png differ
diff --git a/src/SMAPI.Web/wwwroot/Content/images/nexus-icon.png b/src/SMAPI.Web/wwwroot/Content/images/nexus-icon.png
new file mode 100644
index 00000000..10c66712
Binary files /dev/null and b/src/SMAPI.Web/wwwroot/Content/images/nexus-icon.png differ
diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js
index 016d5fa4..8fa1c26f 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/index.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/index.js
@@ -1,6 +1,7 @@
 $(document).ready(function () {
+    /* enable pufferchick */
     var pufferchick = $("#pufferchick");
-    $(".download").hover(
+    $(".cta-dropdown").hover(
         function () {
             pufferchick.attr("src", "Content/images/pufferchick-cool.png");
         },
@@ -8,4 +9,26 @@ $(document).ready(function () {
             pufferchick.attr("src", "favicon.ico");
         }
     );
+
+    /* enable download dropdowns */
+    $(".cta-dropdown a.download").each(function(i, button) {
+        button = $(button);
+        var wrapper = button.parent(".cta-dropdown");
+        var button = wrapper.find(".download");
+        var dropdownContent = wrapper.find(".dropdown-content");
+
+        $(window).on("click", function(e) {
+            var target = $(e.target);
+
+            // toggle dropdown on button click
+            if (target.is(button) || $.contains(button.get(0), target.get(0))) {
+                e.preventDefault();
+                dropdownContent.toggle();
+            }
+
+            // else hide dropdown
+            else
+                dropdownContent.hide();
+        });
+    });
 });
-- 
cgit 


From f1bdafde238280b771abb1d74ae780b6b86bdc79 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard <github@jplamondonw.com>
Date: Sat, 16 Jun 2018 01:13:39 -0400
Subject: use sharper pufferchick images

---
 src/SMAPI.Web/Views/Index/Index.cshtml                |   2 +-
 .../wwwroot/Content/images/pufferchick-cool.png       | Bin 2921 -> 1099 bytes
 src/SMAPI.Web/wwwroot/Content/images/pufferchick.png  | Bin 0 -> 831 bytes
 src/SMAPI.Web/wwwroot/Content/js/index.js             |   2 +-
 4 files changed, 2 insertions(+), 2 deletions(-)
 create mode 100644 src/SMAPI.Web/wwwroot/Content/images/pufferchick.png

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

diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml
index fbfc2239..8145d354 100644
--- a/src/SMAPI.Web/Views/Index/Index.cshtml
+++ b/src/SMAPI.Web/Views/Index/Index.cshtml
@@ -34,7 +34,7 @@
         </div><br />
     }
     <a href="https://stardewvalleywiki.com/Modding:Player_Guide" class="secondary-cta">Player guide</a><br />
-    <img id="pufferchick" src="favicon.ico" />
+    <img id="pufferchick" src="Content/images/pufferchick.png" />
 </div>
 
 <h2>Get help</h2>
diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png
index 63eb8970..f359146c 100644
Binary files a/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png and b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png differ
diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png b/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png
new file mode 100644
index 00000000..1de9cf47
Binary files /dev/null and b/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png differ
diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js
index 8fa1c26f..d0734b02 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/index.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/index.js
@@ -6,7 +6,7 @@ $(document).ready(function () {
             pufferchick.attr("src", "Content/images/pufferchick-cool.png");
         },
         function () {
-            pufferchick.attr("src", "favicon.ico");
+            pufferchick.attr("src", "Content/images/pufferchick.png");
         }
     );
 
-- 
cgit 


From 3e5c109df1f90904c2dcb177e35b35f003e90fd9 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard <github@jplamondonw.com>
Date: Wed, 27 Jun 2018 09:47:31 -0400
Subject: add log parser option to view raw log

---
 docs/release-notes.md                            |  1 +
 src/SMAPI.Web/Controllers/LogParserController.cs | 11 ++--
 src/SMAPI.Web/ViewModels/LogParserModel.cs       | 19 +++++-
 src/SMAPI.Web/Views/LogParser/Index.cshtml       | 79 ++++++++++++++----------
 src/SMAPI.Web/wwwroot/Content/css/log-parser.css |  4 ++
 src/SMAPI.Web/wwwroot/Content/js/log-parser.js   | 14 ++++-
 6 files changed, 88 insertions(+), 40 deletions(-)

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

diff --git a/docs/release-notes.md b/docs/release-notes.md
index 5a9e4e92..329ea3ad 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -65,6 +65,7 @@
   * Redesigned UI to be more mobile-friendly.
   * Added option to download from Nexus.
   * Changed log parser filters to show `DEBUG` messages by default.
+  * Added log parser option to view raw log.
   * Fixed log parser issue when content packs have no description.
   * Fixed log parser mangling crossplatform paths in some cases.
 
diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs
index 2bff1392..354bdb06 100644
--- a/src/SMAPI.Web/Controllers/LogParserController.cs
+++ b/src/SMAPI.Web/Controllers/LogParserController.cs
@@ -52,21 +52,22 @@ namespace StardewModdingAPI.Web.Controllers
         ***/
         /// <summary>Render the log parser UI.</summary>
         /// <param name="id">The paste ID.</param>
+        /// <param name="raw">Whether to display the raw unparsed log.</param>
         [HttpGet]
         [Route("log")]
         [Route("log/{id}")]
-        public async Task<ViewResult> Index(string id = null)
+        public async Task<ViewResult> Index(string id = null, bool raw = false)
         {
             // fresh page
             if (string.IsNullOrWhiteSpace(id))
-                return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, null));
+                return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id));
 
             // log page
             PasteInfo paste = await this.GetAsync(id);
             ParsedLog log = paste.Success
                 ? new LogParser().Parse(paste.Content)
                 : new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error };
-            return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log));
+            return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log, raw));
         }
 
         /***
@@ -80,7 +81,7 @@ namespace StardewModdingAPI.Web.Controllers
             // 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." });
+                return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null) { UploadError = "The log file seems to be empty." });
 
             // upload log
             input = this.CompressString(input);
@@ -88,7 +89,7 @@ namespace StardewModdingAPI.Web.Controllers
 
             // handle errors
             if (!result.Success)
-                return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID, null) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" });
+                return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" });
 
             // redirect to view
             UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl));
diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs
index 0fbd8ad5..df36ca73 100644
--- a/src/SMAPI.Web/ViewModels/LogParserModel.cs
+++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs
@@ -27,6 +27,9 @@ namespace StardewModdingAPI.Web.ViewModels
         /// <summary>The parsed log info.</summary>
         public ParsedLog ParsedLog { get; set; }
 
+        /// <summary>Whether to show the raw unparsed log.</summary>
+        public bool ShowRaw { get; set; }
+
         /// <summary>An error which occurred while uploading the log to Pastebin.</summary>
         public string UploadError { get; set; }
 
@@ -43,12 +46,24 @@ namespace StardewModdingAPI.Web.ViewModels
         /// <summary>Construct an instance.</summary>
         /// <param name="sectionUrl">The root URL for the log parser controller.</param>
         /// <param name="pasteID">The paste ID.</param>
-        /// <param name="parsedLog">The parsed log info.</param>
-        public LogParserModel(string sectionUrl, string pasteID, ParsedLog parsedLog)
+        public LogParserModel(string sectionUrl, string pasteID)
         {
             this.SectionUrl = sectionUrl;
             this.PasteID = pasteID;
+            this.ParsedLog = null;
+            this.ShowRaw = false;
+        }
+
+        /// <summary>Construct an instance.</summary>
+        /// <param name="sectionUrl">The root URL for the log parser controller.</param>
+        /// <param name="pasteID">The paste ID.</param>
+        /// <param name="parsedLog">The parsed log info.</param>
+        /// <param name="showRaw">Whether to show the raw unparsed log.</param>
+        public LogParserModel(string sectionUrl, string pasteID, ParsedLog parsedLog, bool showRaw)
+            : this(sectionUrl, pasteID)
+        {
             this.ParsedLog = parsedLog;
+            this.ShowRaw = showRaw;
         }
 
         /// <summary>Get all content packs in the log grouped by the mod they're for.</summary>
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index 8151c502..f5501fed 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -17,17 +17,18 @@
     {
         <meta name="robots" content="noindex" />
     }
-    <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180611" />
+    <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180627" />
     <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=20180611"></script>
+    <script src="~/Content/js/log-parser.js?r=20180627"></script>
     <script>
         $(function() {
             smapi.logParser({
                 logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)),
                 showPopup: @Json.Serialize(Model.ParsedLog == null),
                 showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), noFormatting),
-                showLevels: @Json.Serialize(defaultFilters, noFormatting)
+                showLevels: @Json.Serialize(defaultFilters, noFormatting),
+                enableFilters: @Json.Serialize(!Model.ShowRaw)
             }, '@Model.SectionUrl');
         });
     </script>
@@ -138,12 +139,15 @@ else if (Model.ParsedLog?.IsValid == true)
             </tr>
         </table>
         <br />
-        <table id="mods">
+        <table id="mods" class="@(Model.ShowRaw ? "filters-disabled" : null)">
             <caption>
                 Installed mods:
-                <span class="notice txt"><i>click any mod to filter</i></span>
-                <span class="notice btn txt" v-on:click="showAllMods" v-show="stats.modsHidden > 0">show all</span>
-                <span class="notice btn txt" v-on:click="hideAllMods" v-show="stats.modsShown > 0 && stats.modsHidden > 0">hide all</span>
+                @if (!Model.ShowRaw)
+                {
+                    <span class="notice txt"><i>click any mod to filter</i></span>
+                    <span class="notice btn txt" v-on:click="showAllMods" v-show="stats.modsHidden > 0">show all</span>
+                    <span class="notice btn txt" v-on:click="hideAllMods" v-show="stats.modsShown > 0 && stats.modsHidden > 0">hide all</span>
+                }
             </caption>
             @foreach (var mod in Model.ParsedLog.Mods.Where(p => p.ContentPackFor == null))
             {
@@ -177,36 +181,47 @@ else if (Model.ParsedLog?.IsValid == true)
                 </tr>
             }
         </table>
-        <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>
 
-        <table id="log">
-            @foreach (var message in Model.ParsedLog.Messages)
-            {
-                string levelStr = message.Level.ToString().ToLower();
+        @if (!Model.ShowRaw)
+        {
+            <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>
 
-                <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>
-                    <td v-pre>@message.Text</td>
-                </tr>
-                if (message.Repeated > 0)
+            <table id="log">
+                @foreach (var message in Model.ParsedLog.Messages)
                 {
-                    <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>
+                    string levelStr = message.Level.ToString().ToLower();
+
+                    <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>
+                        <td v-pre>@message.Text</td>
                     </tr>
+                    if (message.Repeated > 0)
+                    {
+                        <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>
+                    }
                 }
-            }
-        </table>
+            </table>
+
+            <small><a href="@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))?raw=true">view raw log</a></small>
+        }
+        else
+        {
+            <pre v-pre>@Model.ParsedLog.RawText</pre>
+            <small><a href="@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))">view parsed log</a></small>
+        }
     </div>
 }
 else if (Model.ParsedLog?.IsValid == false)
diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
index 09bb97f5..1fcd1bff 100644
--- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
+++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
@@ -79,6 +79,10 @@ table#metadata, table#mods {
     cursor: pointer;
 }
 
+#mods.filters-disabled tr {
+    cursor: default;
+}
+
 #metadata tr,
 #mods tr {
     background: #eee
diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
index 44c3ad5d..0c654205 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
@@ -39,11 +39,17 @@ smapi.logParser = function (data, sectionUrl) {
             }
         },
         methods: {
-            toggleLevel: function(id) {
+            toggleLevel: function (id) {
+                if (!data.enableFilters)
+                    return;
+
                 this.showLevels[id] = !this.showLevels[id];
             },
 
             toggleMod: function (id) {
+                if (!data.enableFilters)
+                    return;
+
                 var curShown = this.showMods[id];
 
                 // first filter: only show this by default
@@ -64,6 +70,9 @@ smapi.logParser = function (data, sectionUrl) {
             },
 
             showAllMods: function () {
+                if (!data.enableFilters)
+                    return;
+
                 for (var key in this.showMods) {
                     if (this.showMods.hasOwnProperty(key)) {
                         this.showMods[key] = true;
@@ -73,6 +82,9 @@ smapi.logParser = function (data, sectionUrl) {
             },
 
             hideAllMods: function () {
+                if (!data.enableFilters)
+                    return;
+
                 for (var key in this.showMods) {
                     if (this.showMods.hasOwnProperty(key)) {
                         this.showMods[key] = false;
-- 
cgit