mirror of
https://github.com/Pathoschild/SMAPI.git
synced 2026-10-12 02:08:51 +00:00
cache mod compatibility list data & fetch it from the frontend
The backend now saves the parsed mod list to storage, and returns a SAS URI which the frontend uses to fetch it. This should significantly reduce bandwidth usage and response times, by avoiding the repeated backend parsing and data transfers.
This commit is contained in:
@@ -18,6 +18,9 @@
|
||||
* Removed support for very old SMAPI logs.
|
||||
* You can now download a JSON representation of the parsed log (see the download link at the bottom of the log page).
|
||||
* Fixed server error if a JSON file contains nested comment syntax.
|
||||
* Improved mod compatibility list:
|
||||
* Reduced response times with a new cache and client-side fetch.
|
||||
* Fixed sort order for mods with non-Latin characters in the name.
|
||||
* Third-party libraries are now served from `smapi.io` instead of external CDNs.
|
||||
|
||||
## 4.2.1
|
||||
|
||||
@@ -44,4 +44,15 @@ public class ModCompatibilityInfo
|
||||
this.UnofficialVersion = unofficialVersion;
|
||||
this.UnofficialUrl = unofficialUrl;
|
||||
}
|
||||
|
||||
/// <summary>Get whether this mod is compatible with no notes.</summary>
|
||||
public bool IsDefault()
|
||||
{
|
||||
return
|
||||
this.Status == ModCompatibilityStatus.Ok
|
||||
&& this.Summary is "use latest version."
|
||||
&& this.BrokeIn is null
|
||||
&& this.UnofficialUrl is null
|
||||
&& this.UnofficialVersion is null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ internal class LogParserController : Controller
|
||||
|
||||
// upload log
|
||||
string id = Guid.NewGuid().ToString("N");
|
||||
UploadResult uploadResult = await this.Storage.SaveAsync($"parsed-{id}", JsonConvert.SerializeObject(log), "application/json");
|
||||
UploadResult uploadResult = await this.Storage.SaveToJsonAsync($"parsed-{id}", log);
|
||||
if (!uploadResult.Succeeded)
|
||||
return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError));
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
using StardewModdingAPI.Web.Framework.Caching;
|
||||
using StardewModdingAPI.Web.Framework.Caching.CompatibilityRepo;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
using StardewModdingAPI.Web.Framework.Storage;
|
||||
using StardewModdingAPI.Web.ViewModels;
|
||||
|
||||
namespace StardewModdingAPI.Web.Controllers;
|
||||
@@ -19,51 +23,93 @@ internal class ModsController : Controller
|
||||
/// <summary>The cache in which to store mod metadata.</summary>
|
||||
private readonly ICompatibilityCacheRepository Cache;
|
||||
|
||||
/// <summary>Provides access to raw data storage.</summary>
|
||||
private readonly IStorageProvider Storage;
|
||||
|
||||
/// <summary>The number of minutes before which compatibility list data should be considered old.</summary>
|
||||
private readonly int StaleMinutes;
|
||||
|
||||
/// <summary>When this server instance last refreshed the mod list cache file.</summary>
|
||||
private DateTimeOffset LastCacheUpdate = DateTimeOffset.MinValue;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="cache">The cache in which to store mod metadata.</param>
|
||||
/// <param name="storage">Provides access to raw data storage.</param>
|
||||
/// <param name="configProvider">The config settings for mod update checks.</param>
|
||||
public ModsController(ICompatibilityCacheRepository cache, IOptions<ModCompatibilityListConfig> configProvider)
|
||||
public ModsController(ICompatibilityCacheRepository cache, IStorageProvider storage, IOptions<ModCompatibilityListConfig> configProvider)
|
||||
{
|
||||
ModCompatibilityListConfig config = configProvider.Value;
|
||||
|
||||
this.Cache = cache;
|
||||
this.Storage = storage;
|
||||
this.StaleMinutes = config.StaleMinutes;
|
||||
}
|
||||
|
||||
/// <summary>Display information for all mods.</summary>
|
||||
[HttpGet]
|
||||
[Route("mods")]
|
||||
public ViewResult Index()
|
||||
public async Task<ViewResult> Index()
|
||||
{
|
||||
return this.View("Index", this.FetchData());
|
||||
return this.View("Index", await this.BuildModelAsync());
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Asynchronously fetch mod metadata from the compatibility list.</summary>
|
||||
public ModListModel FetchData()
|
||||
/// <summary>Build a view model for the current mod compatibility list data.</summary>
|
||||
private async Task<ModListModel> BuildModelAsync()
|
||||
{
|
||||
// fetch cached data
|
||||
// get metadata
|
||||
if (!this.Cache.TryGetCacheMetadata(out Cached<CompatibilityListMetadata>? metadata))
|
||||
return new ModListModel(Array.Empty<ModModel>(), lastUpdated: DateTimeOffset.UtcNow, isStale: true);
|
||||
return new ModListModel(null, [], lastUpdated: DateTimeOffset.UtcNow, isStale: true);
|
||||
|
||||
// fetch data
|
||||
(string? fetchUri, ModModel[]? overrideData) = await this.FetchDataAsync(metadata);
|
||||
|
||||
// build model
|
||||
return new ModListModel(
|
||||
mods: this.Cache
|
||||
.GetMods()
|
||||
.Select(mod => new ModModel(mod.Data))
|
||||
.OrderBy(p => Regex.Replace((p.Name ?? "").ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting
|
||||
fetchUri: fetchUri,
|
||||
mods: overrideData,
|
||||
lastUpdated: metadata.LastUpdated,
|
||||
isStale: this.Cache.IsStale(metadata.LastUpdated, this.StaleMinutes)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get the cached data to send to the frontend, creating it if needed.</summary>
|
||||
/// <param name="metadata">The mod list cache metadata.</param>
|
||||
private async Task<(string? FetchUri, ModModel[]? OverrideData)> FetchDataAsync(Cached<CompatibilityListMetadata> metadata)
|
||||
{
|
||||
const string cacheKey = "mod-list";
|
||||
|
||||
// fetch file if still fresh
|
||||
StoredFileInfo? fileInfo = this.LastCacheUpdate >= metadata.LastRequested
|
||||
? await this.Storage.GetAsync(cacheKey, false)
|
||||
: null;
|
||||
|
||||
// recreate cache file if needed
|
||||
if (fileInfo?.Success != true)
|
||||
{
|
||||
var mods = this.Cache
|
||||
.GetMods()
|
||||
.Select(mod => new ModModel(mod.Data))
|
||||
.OrderBy(p => PathUtilities.CreateSlug(p.Name)?.ToLower() ?? ""); // ignore case, spaces, and special characters when sorting
|
||||
|
||||
await this.Storage.SaveToJsonAsync(cacheKey, mods);
|
||||
this.LastCacheUpdate = DateTimeOffset.UtcNow;
|
||||
|
||||
fileInfo = await this.Storage.GetAsync(cacheKey, false);
|
||||
}
|
||||
|
||||
// get info
|
||||
string? fetchUri = fileInfo.FetchUri;
|
||||
ModModel[]? overrideData = fileInfo.FetchUri is null && fileInfo.FetchedData != null
|
||||
? JsonConvert.DeserializeObject<ModModel[]>(fileInfo.FetchedData)
|
||||
: null;
|
||||
return (fetchUri, overrideData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,14 @@ internal interface IStorageProvider
|
||||
/// <returns>Returns metadata about the save attempt.</returns>
|
||||
Task<UploadResult> SaveAsync(string id, string content, string contentType, bool compress = true);
|
||||
|
||||
/// <summary>Serialize a data model to JSON and save it to storage.</summary>
|
||||
/// <typeparam name="T">The data type to save.</typeparam>
|
||||
/// <param name="id">The ID with which to save the result.</param>
|
||||
/// <param name="content">The content to serialize and upload.</param>
|
||||
/// <param name="compress">Whether to gzip the text.</param>
|
||||
/// <returns>Returns metadata about the save attempt.</returns>
|
||||
Task<UploadResult> SaveToJsonAsync<T>(string id, T content, bool compress = true);
|
||||
|
||||
/// <summary>Fetch raw text from storage.</summary>
|
||||
/// <param name="id">The storage ID used to upload the file.</param>
|
||||
/// <param name="forceRenew">Whether to reset the file expiry.</param>
|
||||
|
||||
@@ -8,6 +8,7 @@ using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Azure.Storage.Sas;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
|
||||
using StardewModdingAPI.Web.Framework.Compression;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
@@ -38,6 +39,12 @@ internal class StorageProvider : IStorageProvider
|
||||
/// <inheritdoc cref="ApiClientsConfig.AzureBlobTempExpiryAutoRenewalDays"/>
|
||||
private int AutoRenewalDays => this.ClientsConfig.AzureBlobTempExpiryAutoRenewalDays;
|
||||
|
||||
/// <summary>The JSON serializer settings to use when saving JSON data to storage.</summary>
|
||||
private JsonSerializerSettings DefaultJsonSettings = new()
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
@@ -84,13 +91,19 @@ internal class StorageProvider : IStorageProvider
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<UploadResult> SaveToJsonAsync<T>(string id, T content, bool compress = true)
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(content, this.DefaultJsonSettings);
|
||||
|
||||
return this.SaveAsync(id, json, "application/json", compress);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StoredFileInfo> GetAsync(string id, bool forceRenew, bool forceDownloadContent = false)
|
||||
{
|
||||
// fetch from blob storage
|
||||
bool isBlobStorage = id.StartsWith("parsed-")
|
||||
? Guid.TryParseExact(id.Substring("parsed-".Length), "N", out _)
|
||||
: Guid.TryParseExact(id, "N", out _);
|
||||
bool isBlobStorage = id.StartsWith("parsed-") || id == "mod-list" || Guid.TryParseExact(id, "N", out _);
|
||||
if (isBlobStorage)
|
||||
{
|
||||
// Azure Blob storage
|
||||
|
||||
@@ -10,8 +10,11 @@ public class ModListModel
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mods to display.</summary>
|
||||
public ModModel[] Mods { get; }
|
||||
/// <summary>The URI from which to fetch the mod data.</summary>
|
||||
public string? FetchUri { get; }
|
||||
|
||||
/// <summary>The mods to display instead of the <see cref="FetchUri"/>, if set.</summary>
|
||||
public ModModel[]? Mods { get; }
|
||||
|
||||
/// <summary>When the data was last updated.</summary>
|
||||
public DateTimeOffset LastUpdated { get; }
|
||||
@@ -20,19 +23,21 @@ public class ModListModel
|
||||
public bool IsStale { get; }
|
||||
|
||||
/// <summary>Whether the mod metadata is available.</summary>
|
||||
public bool HasData => this.Mods.Any();
|
||||
public bool HasData => this.FetchUri != null || this.Mods?.Length > 0;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="mods">The mods to display.</param>
|
||||
/// <param name="fetchUri">The URI from which to fetch the mod data.</param>
|
||||
/// <param name="mods">The mods to display instead of the <paramref name="fetchUri"/>, if set.</param>
|
||||
/// <param name="lastUpdated">When the data was last updated.</param>
|
||||
/// <param name="isStale">Whether the data hasn't been updated in a while.</param>
|
||||
public ModListModel(IEnumerable<ModModel> mods, DateTimeOffset lastUpdated, bool isStale)
|
||||
public ModListModel(string? fetchUri, IEnumerable<ModModel>? mods, DateTimeOffset lastUpdated, bool isStale)
|
||||
{
|
||||
this.Mods = mods.ToArray();
|
||||
this.FetchUri = fetchUri;
|
||||
this.Mods = mods?.ToArray();
|
||||
this.LastUpdated = lastUpdated;
|
||||
this.IsStale = isStale;
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@ public class ModModel
|
||||
public string? Name { get; }
|
||||
|
||||
/// <summary>The mod's alternative names, if any.</summary>
|
||||
public string AlternateNames { get; }
|
||||
public string? AlternateNames { get; }
|
||||
|
||||
/// <summary>The mod author's name.</summary>
|
||||
public string? Author { get; }
|
||||
|
||||
/// <summary>The mod author's alternative names, if any.</summary>
|
||||
public string AlternateAuthors { get; }
|
||||
public string? AlternateAuthors { get; }
|
||||
|
||||
/// <summary>The GitHub repo, if any.</summary>
|
||||
public string? GitHubRepo { get; }
|
||||
@@ -33,14 +33,14 @@ public class ModModel
|
||||
/// <summary>The URL to the mod's source code, if any.</summary>
|
||||
public string? SourceUrl { get; }
|
||||
|
||||
/// <summary>The compatibility status for the stable version of the game.</summary>
|
||||
public ModCompatibilityModel Compatibility { get; }
|
||||
/// <summary>The compatibility status for the stable version of the game, or <c>null</c> if it's compatible with no notes.</summary>
|
||||
public ModCompatibilityModel? Compatibility { get; }
|
||||
|
||||
/// <summary>Links to the available mod pages.</summary>
|
||||
public ModLinkModel[] ModPages { get; }
|
||||
|
||||
/// <summary>The human-readable warnings for players about this mod.</summary>
|
||||
public string[] Warnings { get; }
|
||||
public string[]? Warnings { get; }
|
||||
|
||||
/// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests.</summary>
|
||||
public string? DevNote { get; }
|
||||
@@ -66,7 +66,7 @@ public class ModModel
|
||||
/// <param name="devNote">Special notes intended for developers who maintain unofficial updates or submit pull requests.</param>
|
||||
/// <param name="slug">A unique identifier for the mod that can be used in an anchor URL.</param>
|
||||
[JsonConstructor]
|
||||
public ModModel(string[] id, string? name, string alternateNames, string author, string alternateAuthors, string gitHubRepo, string sourceUrl, ModCompatibilityModel compatibility, ModLinkModel[] modPages, string[] warnings, string devNote, string slug)
|
||||
public ModModel(string[] id, string? name, string? alternateNames, string? author, string? alternateAuthors, string? gitHubRepo, string? sourceUrl, ModCompatibilityModel? compatibility, ModLinkModel[] modPages, string[]? warnings, string? devNote, string? slug)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Name = name;
|
||||
@@ -89,14 +89,14 @@ public class ModModel
|
||||
// basic info
|
||||
this.Id = entry.ID.ToArray();
|
||||
this.Name = entry.Name.FirstOrDefault();
|
||||
this.AlternateNames = string.Join(", ", entry.Name.Skip(1).ToArray());
|
||||
this.AlternateNames = entry.Name.Length > 1 ? string.Join(", ", entry.Name.Skip(1)) : null;
|
||||
this.Author = entry.Author.FirstOrDefault();
|
||||
this.AlternateAuthors = string.Join(", ", entry.Author.Skip(1).ToArray());
|
||||
this.AlternateAuthors = entry.Author.Length > 1 ? string.Join(", ", entry.Author.Skip(1)) : null;
|
||||
this.GitHubRepo = entry.GitHubRepo;
|
||||
this.SourceUrl = this.GetSourceUrl(entry);
|
||||
this.Compatibility = new ModCompatibilityModel(entry.Compatibility);
|
||||
this.Compatibility = !entry.Compatibility.IsDefault() ? new ModCompatibilityModel(entry.Compatibility) : null;
|
||||
this.ModPages = this.GetModPageUrls(entry).ToArray();
|
||||
this.Warnings = entry.Warnings;
|
||||
this.Warnings = entry.Warnings.Length > 1 ? entry.Warnings : null;
|
||||
this.DevNote = entry.DevNote;
|
||||
this.Slug = entry.Anchor;
|
||||
}
|
||||
|
||||
@@ -8,23 +8,24 @@
|
||||
TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated;
|
||||
}
|
||||
@section Head {
|
||||
<!-- jQuery -->
|
||||
@* jQuery *@
|
||||
<script src="@Url.ContentWithCacheBust("~/Content/lib/jquery/dist/jquery.min.js")"></script>
|
||||
|
||||
<!-- jQuery tablesorter -->
|
||||
@* jQuery tablesorter *@
|
||||
<script src="@Url.ContentWithCacheBust("~/Content/lib/tablesorter/dist/js/jquery.tablesorter.min.js")"></script>
|
||||
|
||||
<!-- Vue -->
|
||||
@* Vue *@
|
||||
<script src="@Url.ContentWithCacheBust("~/Content/lib/vue/dist/vue.min.js")"></script>
|
||||
|
||||
<!-- page CSS/JS -->
|
||||
@* page CSS/JS *@
|
||||
<link rel="stylesheet" href="@Url.ContentWithCacheBust("~/Content/css/mods.css")" />
|
||||
<script src="@Url.ContentWithCacheBust("~/Content/js/mods.js")"></script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
var data = @(this.ForJson(Model.Mods));
|
||||
smapi.modList(data);
|
||||
smapi.modList({
|
||||
fetchUri: @this.ForJson(this.Model.FetchUri),
|
||||
fetchedData: @this.ForJson(this.Model.Mods)
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -35,17 +36,28 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@* non-JavaScript error *@
|
||||
<noscript>
|
||||
<div class="banner error">
|
||||
<strong>This page requires JavaScript.</strong><br />
|
||||
This pages uses JavaScript to render the compatibility list. To view the list, please enable JavaScript or <a href="https://github.com/Pathoschild/SmapiCompatibilityList">view the raw data</a>.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
@if (Model.IsStale)
|
||||
{
|
||||
<div class="error">Showing data from @staleAge.Humanize(maxUnit: TimeUnit.Hour, minUnit: TimeUnit.Minute) ago. (Couldn't fetch newer data; the GitHub API may be offline.)</div>
|
||||
}
|
||||
|
||||
<div id="app">
|
||||
<div id="intro">
|
||||
<p>This page shows all known SMAPI mods and (incompatible) content packs, whether they work with the latest versions of Stardew Valley and SMAPI, and how to fix them if not. If a mod doesn't work after following the instructions below, check <a href="https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting">the troubleshooting guide</a> or <a href="https://stardewvalleywiki.com/Modding:Help">ask for help</a>.</p>
|
||||
<div id="intro">
|
||||
<p>This page shows all known SMAPI mods and (incompatible) content packs, whether they work with the latest versions of Stardew Valley and SMAPI, and how to fix them if not. If a mod doesn't work after following the instructions below, check <a href="https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting">the troubleshooting guide</a> or <a href="https://stardewvalleywiki.com/Modding:Help">ask for help</a>.</p>
|
||||
|
||||
<p>The list is updated regularly (<a href="https://github.com/Pathoschild/SmapiCompatibilityList#readme">you can help update it</a>!). It doesn't include XNB mods (see <a href="https://stardewvalleywiki.com/Modding:Using_XNB_mods"><em>using XNB mods</em> on the wiki</a> instead) or compatible content packs.</p>
|
||||
</div>
|
||||
<p>The list is updated regularly (<a href="https://github.com/Pathoschild/SmapiCompatibilityList#readme">you can help update it</a>!). It doesn't include XNB mods (see <a href="https://stardewvalleywiki.com/Modding:Using_XNB_mods"><em>using XNB mods</em> on the wiki</a> instead) or compatible content packs.</p>
|
||||
</div>
|
||||
|
||||
<div v-cloak id="app">
|
||||
@* loading indicator *@
|
||||
<div v-if="false" class="loading-indicator"><div></div><div></div><div></div><div></div></div>
|
||||
|
||||
<div id="options">
|
||||
<div>
|
||||
@@ -97,7 +109,7 @@ else
|
||||
</td>
|
||||
<td>
|
||||
<div v-html="mod.Compatibility.Summary"></div>
|
||||
<div v-for="(warning, i) in mod.Warnings">⚠ {{warning}}</div>
|
||||
<div v-if="mod.Warnings" v-for="(warning, i) in mod.Warnings">⚠ {{warning}}</div>
|
||||
</td>
|
||||
<td class="mod-broke-in" v-html="mod.Compatibility.BrokeIn" v-show="showAdvanced"></td>
|
||||
<td v-show="showAdvanced">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
var smapi = smapi || {};
|
||||
var app;
|
||||
smapi.modList = function (mods) {
|
||||
smapi.modList = async function (state) {
|
||||
// init data
|
||||
var defaultStats = {
|
||||
const defaultStats = {
|
||||
total: 0,
|
||||
compatible: 0,
|
||||
workaround: 0,
|
||||
@@ -14,8 +14,8 @@ smapi.modList = function (mods) {
|
||||
percentBroken: 0,
|
||||
percentObsolete: 0
|
||||
};
|
||||
var data = {
|
||||
mods: mods,
|
||||
const data = {
|
||||
mods: [],
|
||||
showAdvanced: false,
|
||||
visibleMainStats: $.extend({}, defaultStats),
|
||||
filters: {
|
||||
@@ -51,6 +51,12 @@ smapi.modList = function (mods) {
|
||||
search: ""
|
||||
};
|
||||
|
||||
// fetch data
|
||||
if (state.fetchUri)
|
||||
data.mods = await $.getJSON(state.fetchUri);
|
||||
else
|
||||
data.mods = state.fetchedData ?? [];
|
||||
|
||||
// init filters
|
||||
Object.entries(data.filters).forEach(([groupKey, filterGroup]) => {
|
||||
filterGroup.label = filterGroup.label || groupKey;
|
||||
@@ -61,12 +67,16 @@ smapi.modList = function (mods) {
|
||||
});
|
||||
|
||||
// init mods
|
||||
for (var i = 0; i < data.mods.length; i++) {
|
||||
var mod = mods[i];
|
||||
for (let i = 0; i < data.mods.length; i++) {
|
||||
const mod = data.mods[i];
|
||||
|
||||
// set initial visibility
|
||||
mod.Visible = true;
|
||||
|
||||
// set default compatibility
|
||||
if (!mod.Compatibility)
|
||||
mod.Compatibility = { "Status": "ok", "Summary": "use latest version." };
|
||||
|
||||
// concatenate searchable text
|
||||
mod.SearchableText = [mod.Name, mod.AlternateNames, mod.Author, mod.AlternateAuthors, mod.Compatibility.Summary, mod.BrokeIn];
|
||||
if (mod.Compatibility.UnofficialVersion)
|
||||
@@ -75,18 +85,18 @@ smapi.modList = function (mods) {
|
||||
}
|
||||
|
||||
// init app
|
||||
var methods = {
|
||||
const methods = {
|
||||
/**
|
||||
* Update the visibility of all mods based on the current search text and filters.
|
||||
*/
|
||||
applyFilters: function () {
|
||||
// get search terms
|
||||
var words = data.search.toLowerCase().split(" ");
|
||||
const words = data.search.toLowerCase().split(" ");
|
||||
|
||||
// apply criteria
|
||||
var stats = data.visibleMainStats = $.extend({}, defaultStats);
|
||||
for (var i = 0; i < data.mods.length; i++) {
|
||||
var mod = data.mods[i];
|
||||
const stats = data.visibleMainStats = $.extend({}, defaultStats);
|
||||
for (let i = 0; i < data.mods.length; i++) {
|
||||
const mod = data.mods[i];
|
||||
mod.Visible = true;
|
||||
|
||||
// check filters
|
||||
@@ -107,7 +117,7 @@ smapi.modList = function (mods) {
|
||||
* Fix the window position for the current hash.
|
||||
*/
|
||||
fixHashPosition: function () {
|
||||
var anchor = location.hash.substring(1);
|
||||
const anchor = location.hash.substring(1);
|
||||
if (!anchor)
|
||||
return;
|
||||
|
||||
@@ -116,10 +126,10 @@ smapi.modList = function (mods) {
|
||||
return;
|
||||
|
||||
// else jump to mod by ID
|
||||
for (var i = 0; i < data.mods.length; i++) {
|
||||
var mod = data.mods[i];
|
||||
for (let i = 0; i < data.mods.length; i++) {
|
||||
const mod = data.mods[i];
|
||||
|
||||
for (var n = 0; n < mod.Id.length; n++) {
|
||||
for (let n = 0; n < mod.Id.length; n++) {
|
||||
if (anchor.localeCompare(mod.Id[n], undefined, { sensitivity: 'accent' }) === 0) {
|
||||
if (methods.scrollToRow(mod.Slug))
|
||||
return;
|
||||
@@ -134,8 +144,8 @@ smapi.modList = function (mods) {
|
||||
* @returns {boolean} Whether the target was found and scrolled into view.
|
||||
*/
|
||||
scrollToRow: function(id) {
|
||||
var row = $("#" + id);
|
||||
var target = row.prev().get(0) || row.get(0); // show previous row if possible, to push the actual row below the sticky header
|
||||
const row = $("#" + id);
|
||||
const target = row.prev().get(0) || row.get(0); // show previous row if possible, to push the actual row below the sticky header
|
||||
target?.scrollIntoView();
|
||||
return !!target;
|
||||
},
|
||||
@@ -147,7 +157,7 @@ smapi.modList = function (mods) {
|
||||
* @returns {boolean} Whether the mod matches the filters.
|
||||
*/
|
||||
matchesFilters: function (mod, searchWords) {
|
||||
var filters = data.filters;
|
||||
const filters = data.filters;
|
||||
|
||||
// check hash
|
||||
if (location.hash === "#" + mod.Slug)
|
||||
@@ -160,12 +170,12 @@ smapi.modList = function (mods) {
|
||||
return false;
|
||||
|
||||
// check status
|
||||
var mainStatus = mod.Compatibility.Status;
|
||||
const mainStatus = mod.Compatibility.Status;
|
||||
if (filters.status.value[mainStatus] && !filters.status.value[mainStatus].value)
|
||||
return false;
|
||||
|
||||
// check download sites
|
||||
var ignoreSites = [];
|
||||
const ignoreSites = [];
|
||||
|
||||
if (!filters.download.value.chucklefish.value)
|
||||
ignoreSites.push("Chucklefish");
|
||||
@@ -179,8 +189,8 @@ smapi.modList = function (mods) {
|
||||
ignoreSites.push("custom");
|
||||
|
||||
if (ignoreSites.length) {
|
||||
var anyLeft = false;
|
||||
for (var i = 0; i < mod.ModPages.length; i++) {
|
||||
let anyLeft = false;
|
||||
for (let i = 0; i < mod.ModPages.length; i++) {
|
||||
if (ignoreSites.indexOf(mod.ModPages[i].Text) === -1) {
|
||||
anyLeft = true;
|
||||
break;
|
||||
@@ -192,7 +202,7 @@ smapi.modList = function (mods) {
|
||||
}
|
||||
|
||||
// check search terms
|
||||
for (var w = 0; w < searchWords.length; w++) {
|
||||
for (let w = 0; w < searchWords.length; w++) {
|
||||
if (mod.SearchableText.indexOf(searchWords[w]) === -1)
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user