add mod compatibility page (#597)
This commit is contained in:
parent
f09befe240
commit
28fdb9e4e7
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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> </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>
|
|
@ -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>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
"Site": {
|
||||
"RootUrl": "http://localhost:59482/",
|
||||
"ModListUrl": "http://localhost:59482/mods/",
|
||||
"LogParserUrl": "http://localhost:59482/log/",
|
||||
"BetaEnabled": false,
|
||||
"BetaBlurb": null
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue