diff options
Diffstat (limited to 'src/SMAPI.Web')
-rw-r--r-- | src/SMAPI.Web/Controllers/ModsApiController.cs | 54 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs | 15 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs | 3 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/VersionConstraint.cs | 2 | ||||
-rw-r--r-- | src/SMAPI.Web/SMAPI.Web.csproj | 6 | ||||
-rw-r--r-- | src/SMAPI.Web/Views/Mods/Index.cshtml | 4 | ||||
-rw-r--r-- | src/SMAPI.Web/appsettings.json | 13 | ||||
-rw-r--r-- | src/SMAPI.Web/wwwroot/Content/css/mods.css | 5 | ||||
-rw-r--r-- | src/SMAPI.Web/wwwroot/Content/js/mods.js | 24 | ||||
-rw-r--r-- | src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 10 | ||||
-rw-r--r-- | src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 8 |
11 files changed, 94 insertions, 50 deletions
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index f194b4d0..06768f03 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -41,11 +41,8 @@ namespace StardewModdingAPI.Web.Controllers /// <summary>The cache in which to store mod data.</summary> private readonly IModCacheRepository ModCache; - /// <summary>The number of minutes successful update checks should be cached before refetching them.</summary> - private readonly int SuccessCacheMinutes; - - /// <summary>The number of minutes failed update checks should be cached before refetching them.</summary> - private readonly int ErrorCacheMinutes; + /// <summary>The config settings for mod update checks.</summary> + private readonly IOptions<ModUpdateCheckConfig> Config; /// <summary>The internal mod metadata list.</summary> private readonly ModDatabase ModDatabase; @@ -58,21 +55,19 @@ namespace StardewModdingAPI.Web.Controllers /// <param name="environment">The web hosting environment.</param> /// <param name="wikiCache">The cache in which to store wiki data.</param> /// <param name="modCache">The cache in which to store mod metadata.</param> - /// <param name="configProvider">The config settings for mod update checks.</param> + /// <param name="config">The config settings for mod update checks.</param> /// <param name="chucklefish">The Chucklefish API client.</param> /// <param name="curseForge">The CurseForge API client.</param> /// <param name="github">The GitHub API client.</param> /// <param name="modDrop">The ModDrop API client.</param> /// <param name="nexus">The Nexus API client.</param> - public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) + public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); - ModUpdateCheckConfig config = configProvider.Value; this.WikiCache = wikiCache; this.ModCache = modCache; - this.SuccessCacheMinutes = config.SuccessCacheMinutes; - this.ErrorCacheMinutes = config.ErrorCacheMinutes; + this.Config = config; this.Repositories = new IModRepository[] { @@ -133,6 +128,8 @@ namespace StardewModdingAPI.Web.Controllers ModDataRecord record = this.ModDatabase.Get(search.ID); WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); + ModOverrideConfig overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID?.Trim(), StringComparison.InvariantCultureIgnoreCase)); + bool allowNonStandardVersions = overrides?.AllowNonStandardVersions ?? false; // get latest versions ModEntryModel result = new ModEntryModel { ID = search.ID }; @@ -151,7 +148,7 @@ namespace StardewModdingAPI.Web.Controllers } // fetch data - ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions); if (data.Error != null) { errors.Add(data.Error); @@ -161,7 +158,7 @@ namespace StardewModdingAPI.Web.Controllers // handle main version if (data.Version != null) { - ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions); + ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions, allowNonStandardVersions); if (version == null) { errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); @@ -175,7 +172,7 @@ namespace StardewModdingAPI.Web.Controllers // handle optional version if (data.PreviewVersion != null) { - ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions); + ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions, allowNonStandardVersions); if (version == null) { errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); @@ -215,16 +212,16 @@ namespace StardewModdingAPI.Web.Controllers } // special cases - if (result.ID == "Pathoschild.SMAPI") + if (overrides?.SetUrl != null) { if (main != null) - main.Url = "https://smapi.io/"; + main.Url = overrides.SetUrl; if (optional != null) - optional.Url = "https://smapi.io/"; + optional.Url = overrides.SetUrl; } // get recommended update (if any) - ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions); + ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions); if (apiVersion != null && installedVersion != null) { // get newer versions @@ -283,10 +280,11 @@ namespace StardewModdingAPI.Web.Controllers /// <summary>Get the mod info for an update key.</summary> /// <param name="updateKey">The namespaced update key.</param> - private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey) + /// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param> + private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions) { // get mod - if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes)) + if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes)) { // get site if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository)) @@ -298,7 +296,7 @@ namespace StardewModdingAPI.Web.Controllers { if (result.Version == null) result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number."); - else if (!SemanticVersion.TryParse(result.Version, out _)) + else if (!SemanticVersion.TryParse(result.Version, allowNonStandardVersions, out _)) result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."); } @@ -357,15 +355,16 @@ namespace StardewModdingAPI.Web.Controllers /// <summary>Get a semantic local version for update checks.</summary> /// <param name="version">The version to parse.</param> /// <param name="map">A map of version replacements.</param> - private ISemanticVersion GetMappedVersion(string version, IDictionary<string, string> map) + /// <param name="allowNonStandard">Whether to allow non-standard versions.</param> + private ISemanticVersion GetMappedVersion(string version, IDictionary<string, string> map, bool allowNonStandard) { // try mapped version - string rawNewVersion = this.GetRawMappedVersion(version, map); - if (SemanticVersion.TryParse(rawNewVersion, out ISemanticVersion parsedNew)) + string rawNewVersion = this.GetRawMappedVersion(version, map, allowNonStandard); + if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion parsedNew)) return parsedNew; // return original version - return SemanticVersion.TryParse(version, out ISemanticVersion parsedOld) + return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsedOld) ? parsedOld : null; } @@ -373,7 +372,8 @@ namespace StardewModdingAPI.Web.Controllers /// <summary>Get a semantic local version for update checks.</summary> /// <param name="version">The version to map.</param> /// <param name="map">A map of version replacements.</param> - private string GetRawMappedVersion(string version, IDictionary<string, string> map) + /// <param name="allowNonStandard">Whether to allow non-standard versions.</param> + private string GetRawMappedVersion(string version, IDictionary<string, string> map, bool allowNonStandard) { if (version == null || map == null || !map.Any()) return version; @@ -383,14 +383,14 @@ namespace StardewModdingAPI.Web.Controllers return map[version]; // match parsed version - if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) + if (SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsed)) { if (map.ContainsKey(parsed.ToString())) return map[parsed.ToString()]; foreach (var pair in map) { - if (SemanticVersion.TryParse(pair.Key, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, out ISemanticVersion newVersion)) + if (SemanticVersion.TryParse(pair.Key, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, allowNonStandard, out ISemanticVersion newVersion)) return newVersion.ToString(); } } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs new file mode 100644 index 00000000..f382d7b5 --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// <summary>Override update-check metadata for a mod.</summary> + internal class ModOverrideConfig + { + /// <summary>The unique ID from the mod's manifest.</summary> + public string ID { get; set; } + + /// <summary>Whether to allow non-standard versions.</summary> + public bool AllowNonStandardVersions { get; set; } + + /// <summary>The mod page URL to use regardless of which site has the update, or <c>null</c> to use the site URL.</summary> + public string SetUrl { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 46073eb8..bd58dba0 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -11,5 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// <summary>The number of minutes failed update checks should be cached before refetching them.</summary> public int ErrorCacheMinutes { get; set; } + + /// <summary>Update-check metadata to override.</summary> + public ModOverrideConfig[] ModOverrides { get; set; } } } diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs index 72f5ef84..f0c57c41 100644 --- a/src/SMAPI.Web/Framework/VersionConstraint.cs +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -28,7 +28,7 @@ namespace StardewModdingAPI.Web.Framework return values.TryGetValue(routeKey, out object routeValue) && routeValue is string routeStr - && SemanticVersion.TryParseNonStandard(routeStr, out _); + && SemanticVersion.TryParse(routeStr, allowNonStandard: true, out _); } } } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 148631a9..97bea0fb 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -12,11 +12,11 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Azure.Storage.Blobs" Version="12.2.0" /> + <PackageReference Include="Azure.Storage.Blobs" Version="12.3.0" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.7.9" /> <PackageReference Include="Hangfire.MemoryStorage" Version="1.6.3" /> <PackageReference Include="Hangfire.Mongo" Version="0.6.6" /> - <PackageReference Include="HtmlAgilityPack" Version="1.11.18" /> + <PackageReference Include="HtmlAgilityPack" Version="1.11.20" /> <PackageReference Include="Humanizer.Core" Version="2.7.9" /> <PackageReference Include="JetBrains.Annotations" Version="2019.1.3" /> <PackageReference Include="Markdig" Version="0.18.1" /> @@ -25,7 +25,7 @@ <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" /> <PackageReference Include="Mongo2Go" Version="2.2.12" /> - <PackageReference Include="MongoDB.Driver" Version="2.10.1" /> + <PackageReference Include="MongoDB.Driver" Version="2.10.2" /> <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" /> <PackageReference Include="Pathoschild.FluentNexus" Version="0.8.0" /> <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" /> diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 5b310d55..b1d9ae2c 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -8,11 +8,11 @@ TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated; } @section Head { - <link rel="stylesheet" href="~/Content/css/mods.css?r=20190302" /> + <link rel="stylesheet" href="~/Content/css/mods.css?r=20200218" /> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/tablesorter@2.31.0/dist/js/jquery.tablesorter.combined.min.js" crossorigin="anonymous"></script> - <script src="~/Content/js/mods.js?r=20190302"></script> + <script src="~/Content/js/mods.js?r=20200218"></script> <script> $(function() { var data = @Json.Serialize(Model.Mods, new JsonSerializerSettings { Formatting = Formatting.None }); diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index caeb381f..9cd1efc8 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -64,6 +64,17 @@ "ModUpdateCheck": { "SuccessCacheMinutes": 60, - "ErrorCacheMinutes": 5 + "ErrorCacheMinutes": 5, + "ModOverrides": [ + { + "ID": "Pathoschild.SMAPI", + "AllowNonStandardVersions": true, + "SetUrl": "https://smapi.io" + }, + { + "ID": "MartyrPher.SMAPI-Android-Installer", + "AllowNonStandardVersions": true + } + ] } } diff --git a/src/SMAPI.Web/wwwroot/Content/css/mods.css b/src/SMAPI.Web/wwwroot/Content/css/mods.css index 1c2b8056..697ba514 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/mods.css +++ b/src/SMAPI.Web/wwwroot/Content/css/mods.css @@ -86,6 +86,11 @@ table.wikitable > caption { font-size: 0.9em; } +#mod-list thead tr { + position: sticky; + top: 0; +} + #mod-list th.header { background-repeat: no-repeat; background-position: center right; diff --git a/src/SMAPI.Web/wwwroot/Content/js/mods.js b/src/SMAPI.Web/wwwroot/Content/js/mods.js index 0394ac4f..35098b60 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/mods.js +++ b/src/SMAPI.Web/wwwroot/Content/js/mods.js @@ -102,7 +102,7 @@ smapi.modList = function (mods, enableBeta) { app = new Vue({ el: "#app", data: data, - mounted: function() { + mounted: function () { // enable table sorting $("#mod-list").tablesorter({ cssHeader: "header", @@ -115,11 +115,7 @@ smapi.modList = function (mods, enableBeta) { $("#search-box").focus(); // jump to anchor (since table is added after page load) - if (location.hash) { - var row = $(location.hash).get(0); - if (row) - row.scrollIntoView(); - } + this.fixHashPosition(); }, methods: { /** @@ -144,6 +140,18 @@ smapi.modList = function (mods, enableBeta) { } }, + /** + * Fix the window position for the current hash. + */ + fixHashPosition: function () { + if (!location.hash) + return; + + var row = $(location.hash); + var target = row.prev().get(0) || row.get(0); + if (target) + target.scrollIntoView(); + }, /** * Get whether a mod matches the current filters. @@ -151,7 +159,7 @@ smapi.modList = function (mods, enableBeta) { * @param {string[]} searchWords The search words to match. * @returns {bool} Whether the mod matches the filters. */ - matchesFilters: function(mod, searchWords) { + matchesFilters: function (mod, searchWords) { var filters = data.filters; // check hash @@ -249,7 +257,9 @@ smapi.modList = function (mods, enableBeta) { } }); app.applyFilters(); + app.fixHashPosition(); window.addEventListener("hashchange", function () { app.applyFilters(); + app.fixHashPosition(); }); }; diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 78918bac..3101fdf1 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -112,7 +112,7 @@ "Default | UpdateKey": "Nexus:2341" }, - "TMX Loader": { + "TMXL Map Toolkit": { "ID": "Platonymous.TMXLoader", "Default | UpdateKey": "Nexus:1820" }, @@ -129,7 +129,7 @@ "Bee House Flower Range Fix": { "ID": "kirbylink.beehousefix", "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "the bee house flower range was fixed in Stardew Valley 1.4." + "~ | StatusReasonPhrase": "the bee house flower range was fixed in Stardew Valley 1.4." }, "Colored Chests": { @@ -153,9 +153,9 @@ /********* ** Broke in SDV 1.4 *********/ - "Fix Dice": { - "ID": "ashley.fixdice", - "~1.1.2 | Status": "AssumeBroken" // crashes game on startup + "Auto Quality Patch": { + "ID": "SilentOak.AutoQualityPatch", + "~2.1.3-unofficial.7 | Status": "AssumeBroken" // runtime errors }, "Fix Dice": { diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index 7e00c28e..e6cd4e65 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -142,7 +142,7 @@ }, "FromFile": { "title": "Source file", - "description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin (map), or .xnb file. This field supports tokens and capitalization doesn't matter.", + "description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin or .tmx (map), or .xnb file. This field supports tokens and capitalization doesn't matter.", "type": "string", "allOf": [ { @@ -151,12 +151,12 @@ } }, { - "pattern": "\\.(json|png|tbin|xnb) *$" + "pattern": "\\.(json|png|tbin|tmx|xnb) *$" } ], "@errorMessages": { "allOf:indexes: 0": "Invalid value; must not contain directory climbing (like '../').", - "allOf:indexes: 1": "Invalid value; must be a file path ending with .json, .png, .tbin, or .xnb." + "allOf:indexes: 1": "Invalid value; must be a file path ending with .json, .png, .tbin, .tmx, or .xnb." } }, "FromArea": { @@ -325,7 +325,7 @@ "then": { "properties": { "FromFile": { - "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin or .xnb file. This field supports tokens and capitalization doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder." + "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin, .tmx, or .xnb file. This field supports tokens and capitalization doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder." }, "FromArea": { "description": "The part of the source map to copy. Defaults to the whole source map." |