summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Web/Controllers/ModsController.cs33
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs3
-rw-r--r--src/SMAPI.Web/StardewModdingAPI.Web.csproj3
-rw-r--r--src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs34
-rw-r--r--src/SMAPI.Web/ViewModels/ModLinkModel.cs28
-rw-r--r--src/SMAPI.Web/ViewModels/ModListModel.cs36
-rw-r--r--src/SMAPI.Web/ViewModels/ModModel.cs107
-rw-r--r--src/SMAPI.Web/Views/Mods/Index.cshtml72
-rw-r--r--src/SMAPI.Web/Views/Shared/_Layout.cshtml1
-rw-r--r--src/SMAPI.Web/appsettings.Development.json1
-rw-r--r--src/SMAPI.Web/appsettings.json1
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/mods.css85
-rw-r--r--src/SMAPI.Web/wwwroot/Content/js/mods.js56
13 files changed, 460 insertions, 0 deletions
diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs
new file mode 100644
index 00000000..99d19f76
--- /dev/null
+++ b/src/SMAPI.Web/Controllers/ModsController.cs
@@ -0,0 +1,33 @@
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+using StardewModdingAPI.Web.ViewModels;
+
+namespace StardewModdingAPI.Web.Controllers
+{
+ /// <summary>Provides user-friendly info about SMAPI mods.</summary>
+ internal class ModsController : Controller
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Display information for all mods.</summary>
+ [HttpGet]
+ [Route("mods")]
+ public async Task<ViewResult> Index()
+ {
+ WikiModEntry[] mods = await new ModToolkit().GetWikiCompatibilityListAsync();
+ ModListModel viewModel = new ModListModel(
+ stableVersion: "1.3.28",
+ betaVersion: "1.3.31-beta",
+ mods: mods
+ .Select(mod => new ModModel(mod))
+ .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting
+ );
+ return this.View("Index", viewModel);
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
index c0e4c4c8..d89a4260 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
@@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The root URL for the log parser.</summary>
public string LogParserUrl { get; set; }
+ /// <summary>The root URL for the mod list.</summary>
+ public string ModListUrl { get; set; }
+
/// <summary>Whether to show SMAPI beta versions on the main page, if any.</summary>
public bool BetaEnabled { get; set; }
diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj
index dc87cc98..4814d169 100644
--- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj
+++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj
@@ -27,6 +27,9 @@
<ProjectReference Include="..\StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
+ <Content Update="Views\Mods\Index.cshtml">
+ <Pack>$(IncludeRazorContentInPack)</Pack>
+ </Content>
<Content Update="wwwroot\StardewModdingAPI.metadata.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
diff --git a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs
new file mode 100644
index 00000000..d331c093
--- /dev/null
+++ b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs
@@ -0,0 +1,34 @@
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+
+namespace StardewModdingAPI.Web.ViewModels
+{
+ /// <summary>Metadata about a mod's compatibility with the latest versions of SMAPI and Stardew Valley.</summary>
+ public class ModCompatibilityModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The compatibility status, as a string like <c>"Broken"</c>.</summary>
+ public string Status { get; set; }
+
+ /// <summary>A link to the unofficial version which fixes compatibility, if any.</summary>
+ public ModLinkModel UnofficialVersion { get; set; }
+
+ /// <summary>The human-readable summary, as an HTML block.</summary>
+ public string Summary { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="info">The mod metadata.</param>
+ public ModCompatibilityModel(WikiCompatibilityInfo info)
+ {
+ this.Status = info.Status.ToString();
+ if (info.UnofficialVersion != null)
+ this.UnofficialVersion = new ModLinkModel(info.UnofficialUrl, info.UnofficialVersion.ToString());
+ this.Summary = info.Summary;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/ViewModels/ModLinkModel.cs b/src/SMAPI.Web/ViewModels/ModLinkModel.cs
new file mode 100644
index 00000000..97dd215c
--- /dev/null
+++ b/src/SMAPI.Web/ViewModels/ModLinkModel.cs
@@ -0,0 +1,28 @@
+namespace StardewModdingAPI.Web.ViewModels
+{
+ /// <summary>Metadata about a link.</summary>
+ public class ModLinkModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The URL of the linked page.</summary>
+ public string Url { get; set; }
+
+ /// <summary>The suggested link text.</summary>
+ public string Text { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="url">The URL of the linked page.</param>
+ /// <param name="text">The suggested link text.</param>
+ public ModLinkModel(string url, string text)
+ {
+ this.Url = url;
+ this.Text = text;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/ViewModels/ModListModel.cs b/src/SMAPI.Web/ViewModels/ModListModel.cs
new file mode 100644
index 00000000..3b87d393
--- /dev/null
+++ b/src/SMAPI.Web/ViewModels/ModListModel.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace StardewModdingAPI.Web.ViewModels
+{
+ /// <summary>Metadata for the mod list page.</summary>
+ public class ModListModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The current stable version of the game.</summary>
+ public string StableVersion { get; set; }
+
+ /// <summary>The current beta version of the game (if any).</summary>
+ public string BetaVersion { get; set; }
+
+ /// <summary>The mods to display.</summary>
+ public ModModel[] Mods { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="stableVersion">The current stable version of the game.</param>
+ /// <param name="betaVersion">The current beta version of the game (if any).</param>
+ /// <param name="mods">The mods to display.</param>
+ public ModListModel(string stableVersion, string betaVersion, IEnumerable<ModModel> mods)
+ {
+ this.StableVersion = stableVersion;
+ this.BetaVersion = betaVersion;
+ this.Mods = mods.ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs
new file mode 100644
index 00000000..4fb9d5b5
--- /dev/null
+++ b/src/SMAPI.Web/ViewModels/ModModel.cs
@@ -0,0 +1,107 @@
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+
+namespace StardewModdingAPI.Web.ViewModels
+{
+ /// <summary>Metadata about a mod.</summary>
+ public class ModModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod name.</summary>
+ public string Name { get; set; }
+
+ /// <summary>The mod's alternative names, if any.</summary>
+ public string AlternateNames { get; set; }
+
+ /// <summary>The mod author's name.</summary>
+ public string Author { get; set; }
+
+ /// <summary>The mod author's alternative names, if any.</summary>
+ public string AlternateAuthors { get; set; }
+
+ /// <summary>The URL to the mod's source code, if any.</summary>
+ public string SourceUrl { get; set; }
+
+ /// <summary>The compatibility status for the stable version of the game.</summary>
+ public ModCompatibilityModel Compatibility { get; set; }
+
+ /// <summary>The compatibility status for the beta version of the game.</summary>
+ public ModCompatibilityModel BetaCompatibility { get; set; }
+
+ /// <summary>Links to the available mod pages.</summary>
+ public ModLinkModel[] ModPages { get; set; }
+
+ /// <summary>The game or SMAPI version which broke this mod (if applicable).</summary>
+ public string BrokeIn { get; set; }
+
+ /// <summary>A unique identifier for the mod that can be used in an anchor URL.</summary>
+ public string Slug { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="entry">The mod metadata.</param>
+ public ModModel(WikiModEntry entry)
+ {
+ // basic info
+ this.Name = entry.Name;
+ this.AlternateNames = entry.AlternateNames;
+ this.Author = entry.Author;
+ this.AlternateAuthors = entry.AlternateAuthors;
+ this.SourceUrl = this.GetSourceUrl(entry);
+ this.Compatibility = new ModCompatibilityModel(entry.Compatibility);
+ this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null;
+ this.ModPages = this.GetModPageUrls(entry).ToArray();
+ this.BrokeIn = entry.BrokeIn;
+ this.Slug = entry.Anchor;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the web URL for the mod's source code repository, if any.</summary>
+ /// <param name="entry">The mod metadata.</param>
+ private string GetSourceUrl(WikiModEntry entry)
+ {
+ if (!string.IsNullOrWhiteSpace(entry.GitHubRepo))
+ return $"https://github.com/{entry.GitHubRepo}";
+ if (!string.IsNullOrWhiteSpace(entry.CustomSourceUrl))
+ return entry.CustomSourceUrl;
+ return null;
+ }
+
+ /// <summary>Get the web URLs for the mod pages, if any.</summary>
+ /// <param name="entry">The mod metadata.</param>
+ private IEnumerable<ModLinkModel> GetModPageUrls(WikiModEntry entry)
+ {
+ bool anyFound = false;
+
+ // normal mod pages
+ if (entry.NexusID.HasValue)
+ {
+ anyFound = true;
+ yield return new ModLinkModel($"https://www.nexusmods.com/stardewvalley/mods/{entry.NexusID}", "Nexus");
+ }
+ if (entry.ChucklefishID.HasValue)
+ {
+ anyFound = true;
+ yield return new ModLinkModel($"https://community.playstarbound.com/resources/{entry.ChucklefishID}", "Chucklefish");
+ }
+
+ // fallback
+ if (!anyFound && !string.IsNullOrWhiteSpace(entry.CustomUrl))
+ {
+ anyFound = true;
+ yield return new ModLinkModel(entry.CustomUrl, "custom");
+ }
+ if (!anyFound && !string.IsNullOrWhiteSpace(entry.GitHubRepo))
+ yield return new ModLinkModel($"https://github.com/{entry.GitHubRepo}/releases", "GitHub");
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml
new file mode 100644
index 00000000..2b33fcaf
--- /dev/null
+++ b/src/SMAPI.Web/Views/Mods/Index.cshtml
@@ -0,0 +1,72 @@
+@using Newtonsoft.Json
+@model StardewModdingAPI.Web.ViewModels.ModListModel
+@{
+ ViewData["Title"] = "SMAPI mod compatibility";
+}
+@section Head {
+ <link rel="stylesheet" href="~/Content/css/mods.css?r=20180615" />
+ <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/mods.js?r=20180615"></script>
+ <script>
+ $(function() {
+ var data = @Json.Serialize(Model.Mods, new JsonSerializerSettings { Formatting = Formatting.None });
+ smapi.modList(data);
+ });
+ </script>
+}
+
+<p id="blurb">This page lists all known SMAPI mods, whether they're compatible with the latest versions of Stardew Valley and SMAPI, and how to fix broken mods if possible. The list is updated every few days. (You can help <a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">edit this list</a>!)</p>
+
+@if (Model.BetaVersion != null)
+{
+ <div id="beta-blurb">
+ <p><strong>Note:</strong> "SDV beta only" means Stardew Valley @Model.BetaVersion; if you didn't opt in to the beta, you have the stable version and can ignore that line. If a mod doesn't have a "SDV beta only" line, the compatibility applies to both versions of the game.</p>
+ </div>
+}
+
+<div id="app">
+ <label for="search-box">Search: </label>
+ <input type="text" id="search-box" v-model="search" v-on:input="applySearch" />
+ <table class="wikitable" id="mod-list">
+ <tr>
+ <th>mod name</th>
+ <th>links</th>
+ <th>author</th>
+ <th>compatibility</th>
+ <th>broke in</th>
+ <th>code</th>
+ <th>&nbsp;</th>
+ </tr>
+ <tr v-for="mod in mods" :key="mod.Name" v-bind:id="mod.Slug" :key="mod.Slug" v-bind:data-status="mod.BetaCompatibility != null ? mod.BetaCompatibility.Status : mod.Compatibility.Status" v-show="mod.Visible">
+ <td>
+ {{mod.Name}}
+ <small class="mod-alt-names" v-if="mod.AlternateNames">(aka {{mod.AlternateNames}})</small>
+ </td>
+ <td class="mod-page-links">
+ <span v-for="(link, i) in mod.ModPages">
+ <a v-bind:href="link.Url">{{link.Text}}</a>{{i < mod.ModPages.length - 1 ? ', ' : ''}}
+ </span>
+ </td>
+ <td>
+ {{mod.Author}}
+ <small class="mod-alt-authors" v-if="mod.AlternateAuthors">(aka {{mod.AlternateAuthors}})</small>
+ </td>
+ <td>
+ <div v-html="mod.Compatibility.Summary"></div>
+ <div v-if="mod.BetaCompatibility">
+ <strong v-if="mod.BetaCompatibility">SDV beta only:</strong>
+ <span v-html="mod.BetaCompatibility.Summary"></span>
+ </div>
+ </td>
+ <td class="mod-broke-in" v-html="mod.BrokeIn"></td>
+ <td>
+ <span v-if="mod.SourceUrl"><a v-bind:href="mod.SourceUrl">source</a></span>
+ <span v-else class="mod-closed-source">no source</span>
+ </td>
+ <td>
+ <small><a v-bind:href="'#' + mod.Slug">#</a></small>
+ </td>
+ </tr>
+ </table>
+</div>
diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml
index 29da9100..4c602b29 100644
--- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml
+++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml
@@ -16,6 +16,7 @@
<h4>SMAPI</h4>
<ul>
<li><a href="@SiteConfig.Value.RootUrl">About SMAPI</a></li>
+ <li><a href="@SiteConfig.Value.ModListUrl">Mod compatibility</a></li>
<li><a href="@SiteConfig.Value.LogParserUrl">Log parser</a></li>
<li><a href="https://stardewvalleywiki.com/Modding:Index">Docs</a></li>
</ul>
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json
index dc22791b..db90a3de 100644
--- a/src/SMAPI.Web/appsettings.Development.json
+++ b/src/SMAPI.Web/appsettings.Development.json
@@ -19,6 +19,7 @@
"Site": {
"RootUrl": "http://localhost:59482/",
+ "ModListUrl": "http://localhost:59482/mods/",
"LogParserUrl": "http://localhost:59482/log/",
"BetaEnabled": false,
"BetaBlurb": null
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index 2c6aa0cc..401b885f 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -16,6 +16,7 @@
"Site": {
"RootUrl": null, // see top note
+ "ModListUrl": null, // see top note
"LogParserUrl": null, // see top note
"BetaEnabled": null, // see top note
"BetaBlurb": null // see top note
diff --git a/src/SMAPI.Web/wwwroot/Content/css/mods.css b/src/SMAPI.Web/wwwroot/Content/css/mods.css
new file mode 100644
index 00000000..d250440f
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/css/mods.css
@@ -0,0 +1,85 @@
+/*********
+** Intro
+*********/
+#content {
+ max-width: calc(100% - 2em); /* allow for wider table if room available */
+}
+
+#blurb {
+ margin-top: 0;
+ width: 50em;
+}
+
+#beta-blurb {
+ width: 50em;
+ margin-bottom: 2em;
+ padding: 1em;
+ border: 3px solid darkgreen;
+}
+
+table.wikitable {
+ background-color:#f8f9fa;
+ color:#222;
+ margin:1em 0;
+ border:1px solid #a2a9b1;
+ border-collapse:collapse
+}
+
+table.wikitable > tr > th,
+table.wikitable > tr > td,
+table.wikitable > * > tr > th,
+table.wikitable > * > tr > td {
+ border:1px solid #a2a9b1;
+ padding:0.2em 0.4em
+}
+
+table.wikitable > tr > th,
+table.wikitable > * > tr > th {
+ background-color:#eaecf0;
+}
+
+table.wikitable > caption {
+ font-weight:bold
+}
+
+#mod-list .mod-page-links,
+#mod-list .mod-alt-authors,
+#mod-list .mod-alt-names,
+#mod-list .mod-broke-in {
+ font-size: 0.8em;
+}
+
+#mod-list .mod-alt-authors,
+#mod-list .mod-alt-names {
+ display: block;
+}
+
+#mod-list tr {
+ font-size: 0.9em;
+}
+
+#mod-list tr[data-status="Ok"],
+#mod-list tr[data-status="Optional"] {
+ background: #9F9;
+}
+
+#mod-list tr[data-status="Workaround"],
+#mod-list tr[data-status="Unofficial"] {
+ background: #CF9;
+}
+
+#mod-list tr[data-status="Broken"] {
+ background: #F99;
+}
+
+#mod-list tr[data-status="Obsolete"],
+#mod-list tr[data-status="Abandoned"] {
+ background: #999;
+ opacity: 0.7;
+}
+
+#mod-list .mod-closed-source {
+ color: red;
+ font-size: 0.8em;
+ opacity: 0.5;
+}
diff --git a/src/SMAPI.Web/wwwroot/Content/js/mods.js b/src/SMAPI.Web/wwwroot/Content/js/mods.js
new file mode 100644
index 00000000..1b15b622
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/js/mods.js
@@ -0,0 +1,56 @@
+/* globals $ */
+
+var smapi = smapi || {};
+var app;
+smapi.modList = function (mods) {
+ // init data
+ var data = { mods: mods, search: "" };
+ for (var i = 0; i < data.mods.length; i++) {
+ var mod = mods[i];
+
+ // set initial visibility
+ mod.Visible = true;
+
+ // concatenate searchable text
+ mod.SearchableText = [mod.Name, mod.AlternateNames, mod.Author, mod.AlternateAuthors, mod.Compatibility.Summary, mod.BrokeIn];
+ if (mod.Compatibility.UnofficialVersion)
+ mod.SearchableText.push(mod.Compatibility.UnofficialVersion);
+ if (mod.BetaCompatibility) {
+ mod.SearchableText.push(mod.BetaCompatibility.Summary);
+ if (mod.BetaCompatibility.UnofficialVersion)
+ mod.SearchableText.push(mod.BetaCompatibility.UnofficialVersion);
+ }
+ for (var p = 0; p < mod.ModPages; p++)
+ mod.SearchableField.push(mod.ModPages[p].Text);
+ mod.SearchableText = mod.SearchableText.join(" ").toLowerCase();
+ }
+
+ // init app
+ app = new Vue({
+ el: "#app",
+ data: data,
+ methods: {
+ /**
+ * Update the visibility of all mods based on the current search text.
+ */
+ applySearch: function () {
+ // get search terms
+ var words = data.search.toLowerCase().split(" ");
+
+ // make sure all words match
+ for (var i = 0; i < data.mods.length; i++) {
+ var mod = data.mods[i];
+ var match = true;
+ for (var w = 0; w < words.length; w++) {
+ if (mod.SearchableText.indexOf(words[w]) === -1) {
+ match = false;
+ break;
+ }
+ }
+
+ mod.Visible = match;
+ }
+ }
+ }
+ });
+};