add mod compatibility page (#597)

This commit is contained in:
Jesse Plamondon-Willard 2018-10-20 15:10:44 -04:00
parent f09befe240
commit 28fdb9e4e7
13 changed files with 460 additions and 0 deletions

View File

@ -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);
}
}
}

View File

@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The root URL for the log parser.</summary> /// <summary>The root URL for the log parser.</summary>
public string LogParserUrl { get; set; } 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> /// <summary>Whether to show SMAPI beta versions on the main page, if any.</summary>
public bool BetaEnabled { get; set; } public bool BetaEnabled { get; set; }

View File

@ -27,6 +27,9 @@
<ProjectReference Include="..\StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj" /> <ProjectReference Include="..\StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Update="Views\Mods\Index.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="wwwroot\StardewModdingAPI.metadata.json"> <Content Update="wwwroot\StardewModdingAPI.metadata.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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");
}
}
}

View File

@ -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>

View File

@ -16,6 +16,7 @@
<h4>SMAPI</h4> <h4>SMAPI</h4>
<ul> <ul>
<li><a href="@SiteConfig.Value.RootUrl">About SMAPI</a></li> <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="@SiteConfig.Value.LogParserUrl">Log parser</a></li>
<li><a href="https://stardewvalleywiki.com/Modding:Index">Docs</a></li> <li><a href="https://stardewvalleywiki.com/Modding:Index">Docs</a></li>
</ul> </ul>

View File

@ -19,6 +19,7 @@
"Site": { "Site": {
"RootUrl": "http://localhost:59482/", "RootUrl": "http://localhost:59482/",
"ModListUrl": "http://localhost:59482/mods/",
"LogParserUrl": "http://localhost:59482/log/", "LogParserUrl": "http://localhost:59482/log/",
"BetaEnabled": false, "BetaEnabled": false,
"BetaBlurb": null "BetaBlurb": null

View File

@ -16,6 +16,7 @@
"Site": { "Site": {
"RootUrl": null, // see top note "RootUrl": null, // see top note
"ModListUrl": null, // see top note
"LogParserUrl": null, // see top note "LogParserUrl": null, // see top note
"BetaEnabled": null, // see top note "BetaEnabled": null, // see top note
"BetaBlurb": null // see top note "BetaBlurb": null // see top note

View File

@ -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;
}

View File

@ -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;
}
}
}
});
};