From 28fdb9e4e7f5419947226171bf6d7efa273802c5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Oct 2018 15:10:44 -0400 Subject: add mod compatibility page (#597) --- src/SMAPI.Web/Controllers/ModsController.cs | 33 +++++++ src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs | 3 + src/SMAPI.Web/StardewModdingAPI.Web.csproj | 3 + src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs | 34 +++++++ src/SMAPI.Web/ViewModels/ModLinkModel.cs | 28 ++++++ src/SMAPI.Web/ViewModels/ModListModel.cs | 36 +++++++ src/SMAPI.Web/ViewModels/ModModel.cs | 107 +++++++++++++++++++++ src/SMAPI.Web/Views/Mods/Index.cshtml | 72 ++++++++++++++ src/SMAPI.Web/Views/Shared/_Layout.cshtml | 1 + src/SMAPI.Web/appsettings.Development.json | 1 + src/SMAPI.Web/appsettings.json | 1 + src/SMAPI.Web/wwwroot/Content/css/mods.css | 85 ++++++++++++++++ src/SMAPI.Web/wwwroot/Content/js/mods.js | 56 +++++++++++ 13 files changed, 460 insertions(+) create mode 100644 src/SMAPI.Web/Controllers/ModsController.cs create mode 100644 src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs create mode 100644 src/SMAPI.Web/ViewModels/ModLinkModel.cs create mode 100644 src/SMAPI.Web/ViewModels/ModListModel.cs create mode 100644 src/SMAPI.Web/ViewModels/ModModel.cs create mode 100644 src/SMAPI.Web/Views/Mods/Index.cshtml create mode 100644 src/SMAPI.Web/wwwroot/Content/css/mods.css create mode 100644 src/SMAPI.Web/wwwroot/Content/js/mods.js (limited to 'src') 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 +{ + /// Provides user-friendly info about SMAPI mods. + internal class ModsController : Controller + { + /********* + ** Public methods + *********/ + /// Display information for all mods. + [HttpGet] + [Route("mods")] + public async Task 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 /// The root URL for the log parser. public string LogParserUrl { get; set; } + /// The root URL for the mod list. + public string ModListUrl { get; set; } + /// Whether to show SMAPI beta versions on the main page, if any. 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 @@ + + $(IncludeRazorContentInPack) + PreserveNewest 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 +{ + /// Metadata about a mod's compatibility with the latest versions of SMAPI and Stardew Valley. + public class ModCompatibilityModel + { + /********* + ** Accessors + *********/ + /// The compatibility status, as a string like "Broken". + public string Status { get; set; } + + /// A link to the unofficial version which fixes compatibility, if any. + public ModLinkModel UnofficialVersion { get; set; } + + /// The human-readable summary, as an HTML block. + public string Summary { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod metadata. + 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 +{ + /// Metadata about a link. + public class ModLinkModel + { + /********* + ** Accessors + *********/ + /// The URL of the linked page. + public string Url { get; set; } + + /// The suggested link text. + public string Text { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The URL of the linked page. + /// The suggested link text. + 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 +{ + /// Metadata for the mod list page. + public class ModListModel + { + /********* + ** Accessors + *********/ + /// The current stable version of the game. + public string StableVersion { get; set; } + + /// The current beta version of the game (if any). + public string BetaVersion { get; set; } + + /// The mods to display. + public ModModel[] Mods { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The current stable version of the game. + /// The current beta version of the game (if any). + /// The mods to display. + public ModListModel(string stableVersion, string betaVersion, IEnumerable 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 +{ + /// Metadata about a mod. + public class ModModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's alternative names, if any. + public string AlternateNames { get; set; } + + /// The mod author's name. + public string Author { get; set; } + + /// The mod author's alternative names, if any. + public string AlternateAuthors { get; set; } + + /// The URL to the mod's source code, if any. + public string SourceUrl { get; set; } + + /// The compatibility status for the stable version of the game. + public ModCompatibilityModel Compatibility { get; set; } + + /// The compatibility status for the beta version of the game. + public ModCompatibilityModel BetaCompatibility { get; set; } + + /// Links to the available mod pages. + public ModLinkModel[] ModPages { get; set; } + + /// The game or SMAPI version which broke this mod (if applicable). + public string BrokeIn { get; set; } + + /// A unique identifier for the mod that can be used in an anchor URL. + public string Slug { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod metadata. + 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 + *********/ + /// Get the web URL for the mod's source code repository, if any. + /// The mod metadata. + 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; + } + + /// Get the web URLs for the mod pages, if any. + /// The mod metadata. + private IEnumerable 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 { + + + + + +} + +

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 edit this list!)

+ +@if (Model.BetaVersion != null) +{ +
+

Note: "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.

+
+} + +
+ + + + + + + + + + + + + + + + + + + + + +
mod namelinksauthorcompatibilitybroke incode 
+ {{mod.Name}} + (aka {{mod.AlternateNames}}) + + {{mod.Author}} + (aka {{mod.AlternateAuthors}}) + +
+
+ SDV beta only: + +
+
+ source + no source + + # +
+
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 @@

SMAPI

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; + } + } + } + }); +}; -- cgit